Merge remote-tracking branch 'origin/develop' into release-1.0.0

main
Nikita Manovich 6 years ago
commit c515bb19be

39
.github/CODEOWNERS vendored

@ -0,0 +1,39 @@
# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence, they will
# be requested for review when someone opens a pull request.
* @nmanovic
# Order is important; the last matching pattern takes the most
# precedence. When someone opens a pull request that only
# modifies components below, only the list of owners and not
# the global owner(s) will be requested for a review.
# Component: Server
/cvat/ @nmanovic
# Component: CVAT UI
/cvat-ui/ @bsekachev
/cvat-data/ @azhavoro
/cvat-canvas/ @bsekachev
/cvat-core/ @bsekachev
# Component: Datumaro
/datumaro/ @zhiltsov-max
/cvat/apps/dataset_manager/ @zhiltsov-max
# Advanced components (e.g. OpenVINO)
/components/ @azhavoro
# Infrastructure
Dockerfile* @azhavoro
docker-compose* @azhavoro
.* @azhavoro
*.conf @azhavoro
*.sh @azhavoro
/cvat_proxy/ @azhavoro
/tests/ @azhavoro
/utils/ @azhavoro
/LICENSE @nmanovic
/.github/ @nmanovic

@ -0,0 +1,53 @@
<!---
Copyright (C) 2020 Intel Corporation
SPDX-License-Identifier: MIT
-->
### My actions before raising this issue
- [ ] Read/searched [the docs](https://github.com/opencv/cvat/tree/master#documentation)
- [ ] Searched [past issues](/issues)
<!--- Provide a general summary of the issue in the Title above -->
### Expected Behaviour
<!--- If you're describing a bug, tell us what should happen. If you're
suggesting a change/improvement, tell us how it should work -->
### Current Behaviour
<!--- If describing a bug, tell us what happens instead of the expected
behavior. If suggesting a change/improvement, explain the difference from
current behavior -->
### Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, or ideas how
to implement the addition or change -->
### Steps to Reproduce (for bugs)
<!--- Provide a link to a live example, or an unambiguous set of steps to
reproduce this bug. Include code to reproduce, if relevant -->
1.
1.
1.
1.
### Context
<!--- How has this issue affected you? What are you trying to accomplish?
Providing context helps us come up with a solution that is most useful in
the real world -->
### Your Environment
<!--- Include as many relevant details about the environment you experienced
the bug in -->
- Git hash commit (`git log -1`):
- Docker version `docker version` (e.g. Docker 17.0.05):
- Are you using Docker Swarm or Kubernetes?
- Operating System and version (e.g. Linux, Windows, MacOS):
- Code example or link to GitHub repo or gist to reproduce problem:
- Other diagnostic information / logs:
<details>
<summary>Logs from `cvat` container</summary>
</details>
### Next steps
You may [join our Gitter](https://gitter.im/opencv-cvat/public) channel for community support.

@ -0,0 +1,46 @@
<!---
Copyright (C) 2020 Intel Corporation
SPDX-License-Identifier: MIT
-->
<!-- Provide a general summary of your changes in the Title above -->
### Motivation and context
<!-- Why is this change required? What problem does it solve? If it fixes an open
issue, please link to the issue here. Describe your changes in detail, add
screenshots. -->
### How has this been tested?
<!-- Please describe in detail how you tested your changes.
Include details of your testing environment, and the tests you ran to
see how your change affects other areas of the code, etc. -->
### Checklist
<!-- Go over all the following points, and put an `x` in all the boxes that apply.
If an item isn't applicable by a reason then ~~explicitly strikethrough~~ the whole
line. If you don't do that github will show incorrect process for the pull request.
If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] I have raised an issue to propose this change ([required](https://github.com/opencv/cvat/issues))
- [ ] My issue has received approval from the maintainers
- [ ] I've read the [CONTRIBUTION](https://github.com/opencv/cvat/blob/develop/CONTRIBUTING.md) guide
- [ ] I have added description of my changes into [CHANGELOG](https://github.com/opencv/cvat/blob/develop/CHANGELOG.md) file
- [ ] I have updated the [documentation](
https://github.com/opencv/cvat/blob/develop/README.md#documentation) accordingly
- [ ] I have added tests to cover my changes
- [ ] I have linked related issues ([read github docs](
https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword))
### License
- [ ] I submit _my code changes_ under the same [MIT License](
https://github.com/opencv/cvat/blob/develop/LICENSE) that covers the project.
Feel free to contact the maintainers if that's a concern.
- [ ] I have updated the license header for each file (see an example below)
```python
# Copyright (C) 2020 Intel Corporation
#
# SPDX-License-Identifier: MIT
```

@ -10,10 +10,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Dialog window with some helpful information about using filters
- Ability to display a bitmap in the new UI
- Button to reset colors settings (brightness, saturation, contrast) in the new UI
- Added option to display shape text always
- Option to display shape text always
- Dedicated message with clarifications when share is unmounted (https://github.com/opencv/cvat/pull/1373)
- Ability to create one tracked point (https://github.com/opencv/cvat/pull/1383)
- Ability to draw/edit polygons and polylines with automatic bordering feature (https://github.com/opencv/cvat/pull/1394)
- Tutorial: instructions for CVAT over HTTPS
- Added deep extreme cut (semi-automatic segmentation) to the new UI (https://github.com/opencv/cvat/pull/1398)
### Changed
- Increase preview size of a task till 256, 256 on the server
- Minor style updates
- Public ssh-keys are displayed in a dedicated window instead of console when create a task with a repository
- React UI has become is a primary UI
### Deprecated
-
@ -23,8 +32,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- New shape is added when press ``esc`` when drawing instead of cancellation
- Fixed dextr segmentation.
- Fixed `FileNotFoundError` during dump after moving format files
- Dextr segmentation doesn't work.
- `FileNotFoundError` during dump after moving format files
- CVAT doesn't append outside shapes when merge polyshapes in old UI
- Layout sometimes shows double scroll bars on create task, dashboard and settings pages
- UI fails after trying to change frame during resizing, dragging, editing
- Hidden points (or outsided) are visible after changing a frame
- Merge is allowed for points, but clicks on points conflict with frame dragging logic
- Removed objects are visible for search
- Add missed task_id and job_id fields into exception logs for the new UI (https://github.com/opencv/cvat/pull/1372)
- UI fails when annotations saving occurs during drag/resize/edit (https://github.com/opencv/cvat/pull/1383)
- Multiple savings when hold Ctrl+S (a lot of the same copies of events were sent with the same working time) (https://github.com/opencv/cvat/pull/1383)
- UI doesn't have any reaction when git repos synchronization failed (https://github.com/opencv/cvat/pull/1383)
- Bug when annotations cannot be saved after (delete - save - undo - save) (https://github.com/opencv/cvat/pull/1383)
- VOC format exports Upper case labels correctly in lower case (https://github.com/opencv/cvat/pull/1379)
- Fixed polygon exporting bug in COCO dataset (https://github.com/opencv/cvat/issues/1387)
- Task creation from remote files (https://github.com/opencv/cvat/pull/1392)
- Job cannot be opened in some cases when the previous job was failed during opening (https://github.com/opencv/cvat/issues/1403)
- Deactivated shape is still highlighted on the canvas (https://github.com/opencv/cvat/issues/1403)
- AttributeError: 'tuple' object has no attribute 'read' in ReID algorithm (https://github.com/opencv/cvat/issues/1403)
- Wrong semi-automatic segmentation near edges of an image (https://github.com/opencv/cvat/issues/1403)
- Git repos paths (https://github.com/opencv/cvat/pull/1400)
### Security
-

@ -1,38 +0,0 @@
# Core support team
- **[Nikita Manovich](https://github.com/nmanovic)**
* Project lead
* Developer
* Author and maintainer
- **[Boris Sekachev](https://github.com/bsekachev)**
* Primary developer
* Author and maintainer
- **[Andrey Zhavoronkov](https://github.com/azhavoro)**
* Developer
* Author and maintainer
# Contributors
- **[Victor Salimonov](https://github.com/VikTorSalimonov)**
* Documentation, screencasts
- **[Dmitry Sidnev](https://github.com/DmitriySidnev)**
* [convert_to_coco.py](utils/coco) - an utility for converting annotation from CVAT to COCO data annotation format
- **[Sebastián Yonekura](https://github.com/syonekura)**
* [convert_to_voc.py](utils/voc) - an utility for converting CVAT XML to PASCAL VOC data annotation format.
- **[ITLab Team](https://github.com/itlab-vision/cvat):**
**[Vasily Danilin](https://github.com/DanVev)**,
**[Eugene Shashkin](https://github.com/EvgenyShashkin)**,
**[Dmitry Silenko](https://github.com/DimaSilenko)**,
**[Alina Bykovskaya](https://github.com/alinaut)**,
**[Yanina Koltushkina](https://github.com/YaniKolt)**
* Integrating CI tools as Travis CI, Codacy and Coveralls.io

@ -1,4 +1,6 @@
Copyright (C) 2018 Intel Corporation
MIT License
Copyright (C) 2018-2020 Intel Corporation
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"),
@ -18,4 +20,3 @@ OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.
 
SPDX-License-Identifier: MIT

@ -153,6 +153,7 @@ Standard JS events are used.
- canvas.fit
- canvas.dragshape => {id: number}
- canvas.resizeshape => {id: number}
- canvas.contextmenu => { mouseEvent: MouseEvent, objectState: ObjectState, pointID: number }
```
### WEB
@ -179,23 +180,25 @@ Standard JS events are used.
## API Reaction
| | IDLE | GROUPING | SPLITTING | DRAWING | MERGING | EDITING | DRAG | ZOOM |
|--------------|------|----------|-----------|---------|---------|---------|------|------|
| html() | + | + | + | + | + | + | + | + |
| setup() | + | + | + | + | + | - | + | + |
| activate() | + | - | - | - | - | - | - | - |
| rotate() | + | + | + | + | + | + | + | + |
| focus() | + | + | + | + | + | + | + | + |
| fit() | + | + | + | + | + | + | + | + |
| grid() | + | + | + | + | + | + | + | + |
| draw() | + | - | - | - | - | - | - | - |
| split() | + | - | + | - | - | - | - | - |
| group() | + | + | - | - | - | - | - | - |
| merge() | + | - | - | - | + | - | - | - |
| fitCanvas() | + | + | + | + | + | + | + | + |
| dragCanvas() | + | - | - | - | - | - | + | - |
| zoomCanvas() | + | - | - | - | - | - | - | + |
| cancel() | - | + | + | + | + | + | + | + |
| configure() | + | - | - | - | - | - | - | - |
| bitmap() | + | + | + | + | + | + | + | + |
| setZLayer() | + | + | + | + | + | + | + | + |
| | IDLE | GROUP | SPLIT | DRAW | MERGE | EDIT | DRAG | RESIZE | ZOOM_CANVAS | DRAG_CANVAS |
|--------------|------|-------|-------|------|-------|------|------|--------|-------------|-------------|
| html() | + | + | + | + | + | + | + | + | + | + |
| setup() | + | + | + | + | + | +/- | +/- | +/- | + | + |
| activate() | + | - | - | - | - | - | - | - | - | - |
| rotate() | + | + | + | + | + | + | + | + | + | + |
| focus() | + | + | + | + | + | + | + | + | + | + |
| fit() | + | + | + | + | + | + | + | + | + | + |
| grid() | + | + | + | + | + | + | + | + | + | + |
| draw() | + | - | - | - | - | - | - | - | - | - |
| split() | + | - | + | - | - | - | - | - | - | - |
| group() | + | + | - | - | - | - | - | - | - | - |
| merge() | + | - | - | - | + | - | - | - | - | - |
| fitCanvas() | + | + | + | + | + | + | + | + | + | + |
| dragCanvas() | + | - | - | - | - | - | + | - | - | + |
| zoomCanvas() | + | - | - | - | - | - | - | + | + | - |
| cancel() | - | + | + | + | + | + | + | + | + | + |
| configure() | + | + | + | + | + | + | + | + | + | + |
| bitmap() | + | + | + | + | + | + | + | + | + | + |
| setZLayer() | + | + | + | + | + | + | + | + | + | + |
You can call setup() during editing, dragging, and resizing only to update objects, not to change a frame.

@ -103,6 +103,24 @@ polyline.cvat_canvas_shape_splitting {
stroke-dasharray: 5;
}
.cvat_canvas_autoborder_point {
opacity: 0.55;
}
.cvat_canvas_autoborder_point:hover {
opacity: 1;
fill: red;
}
.cvat_canvas_autoborder_point:active {
opacity: 0.55;
fill: red;
}
.cvat_canvas_autoborder_point_direction {
fill: blueviolet;
}
.svg_select_boundingRect {
opacity: 0;
pointer-events: none;

@ -0,0 +1,301 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js';
import consts from './consts';
import { Geometry } from './canvasModel';
interface TransformedShape {
points: string;
color: string;
}
export interface AutoborderHandler {
autoborder(enabled: boolean, currentShape?: SVG.Shape, ignoreCurrent?: boolean): void;
transform(geometry: Geometry): void;
updateObjects(): void;
}
export class AutoborderHandlerImpl implements AutoborderHandler {
private currentShape: SVG.Shape | null;
private ignoreCurrent: boolean;
private frameContent: SVGSVGElement;
private enabled: boolean;
private scale: number;
private groups: SVGGElement[];
private auxiliaryGroupID: number | null;
private auxiliaryClicks: number[];
private listeners: Record<number, Record<number, {
click: (event: MouseEvent) => void;
dblclick: (event: MouseEvent) => void;
}>>;
public constructor(frameContent: SVGSVGElement) {
this.frameContent = frameContent;
this.ignoreCurrent = false;
this.currentShape = null;
this.enabled = false;
this.scale = 1;
this.groups = [];
this.auxiliaryGroupID = null;
this.auxiliaryClicks = [];
this.listeners = {};
}
private removeMarkers(): void {
this.groups.forEach((group: SVGGElement): void => {
const groupID = group.dataset.groupId;
Array.from(group.children)
.forEach((circle: SVGCircleElement, pointID: number): void => {
circle.removeEventListener('click', this.listeners[+groupID][pointID].click);
circle.removeEventListener('dblclick', this.listeners[+groupID][pointID].click);
circle.remove();
});
group.remove();
});
this.groups = [];
this.auxiliaryGroupID = null;
this.auxiliaryClicks = [];
this.listeners = {};
}
private release(): void {
this.removeMarkers();
this.enabled = false;
this.currentShape = null;
}
private addPointToCurrentShape(x: number, y: number): void {
const array: number[][] = (this.currentShape as any).array().valueOf();
array.pop();
// need to append twice (specific of the library)
array.push([x, y]);
array.push([x, y]);
const paintHandler = this.currentShape.remember('_paintHandler');
paintHandler.drawCircles();
paintHandler.set.members.forEach((el: SVG.Circle): void => {
el.attr('stroke-width', 1 / this.scale).attr('r', 2.5 / this.scale);
});
(this.currentShape as any).plot(array);
}
private resetAuxiliaryShape(): void {
if (this.auxiliaryGroupID !== null) {
while (this.auxiliaryClicks.length > 0) {
const resetID = this.auxiliaryClicks.pop();
this.groups[this.auxiliaryGroupID]
.children[resetID].classList.remove('cvat_canvas_autoborder_point_direction');
}
}
this.auxiliaryClicks = [];
this.auxiliaryGroupID = null;
}
// convert each shape to group of clicable points
// save all groups
private drawMarkers(transformedShapes: TransformedShape[]): void {
const svgNamespace = 'http://www.w3.org/2000/svg';
this.groups = transformedShapes
.map((shape: TransformedShape, groupID: number): SVGGElement => {
const group = document.createElementNS(svgNamespace, 'g');
group.setAttribute('data-group-id', `${groupID}`);
this.listeners[groupID] = this.listeners[groupID] || {};
const circles = shape.points.split(/\s/).map((
point: string, pointID: number, points: string[],
): SVGCircleElement => {
const [x, y] = point.split(',');
const circle = document.createElementNS(svgNamespace, 'circle');
circle.classList.add('cvat_canvas_autoborder_point');
circle.setAttribute('fill', shape.color);
circle.setAttribute('stroke', 'black');
circle.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.scale}`);
circle.setAttribute('cx', x);
circle.setAttribute('cy', y);
circle.setAttribute('r', `${consts.BASE_POINT_SIZE / this.scale}`);
const click = (event: MouseEvent): void => {
event.stopPropagation();
// another shape was clicked
if (this.auxiliaryGroupID !== null
&& this.auxiliaryGroupID !== groupID
) {
this.resetAuxiliaryShape();
}
this.auxiliaryGroupID = groupID;
// up clicked group for convenience
this.frameContent.appendChild(group);
if (this.auxiliaryClicks[1] === pointID) {
// the second point was clicked twice
this.addPointToCurrentShape(+x, +y);
this.resetAuxiliaryShape();
return;
}
// the first point can not be clicked twice
// just ignore such a click if it is
if (this.auxiliaryClicks[0] !== pointID) {
this.auxiliaryClicks.push(pointID);
} else {
return;
}
// it is the first click
if (this.auxiliaryClicks.length === 1) {
const handler = this.currentShape.remember('_paintHandler');
// draw and remove initial point just to initialize data structures
if (!handler || !handler.startPoint) {
(this.currentShape as any).draw('point', event);
(this.currentShape as any).draw('undo');
}
this.addPointToCurrentShape(+x, +y);
// is is the second click
} else if (this.auxiliaryClicks.length === 2) {
circle.classList.add('cvat_canvas_autoborder_point_direction');
// it is the third click
} else {
// sign defines bypass direction
const landmarks = this.auxiliaryClicks;
const sign = Math.sign(landmarks[2] - landmarks[0])
* Math.sign(landmarks[1] - landmarks[0])
* Math.sign(landmarks[2] - landmarks[1]);
// go via a polygon and get vertexes
// the first vertex has been already drawn
const way = [];
for (let i = landmarks[0] + sign; ; i += sign) {
if (i < 0) {
i = points.length - 1;
} else if (i === points.length) {
i = 0;
}
way.push(points[i]);
if (i === this.auxiliaryClicks[this.auxiliaryClicks.length - 1]) {
// put the last element twice
// specific of svg.draw.js
// way.push(points[i]);
break;
}
}
// remove the latest cursor position from drawing array
for (const wayPoint of way) {
const [_x, _y] = wayPoint.split(',')
.map((coordinate: string): number => +coordinate);
this.addPointToCurrentShape(_x, _y);
}
this.resetAuxiliaryShape();
}
};
const dblclick = (event: MouseEvent): void => {
event.stopPropagation();
};
this.listeners[groupID][pointID] = {
click,
dblclick,
};
circle.addEventListener('mousedown', this.listeners[groupID][pointID].click);
circle.addEventListener('dblclick', this.listeners[groupID][pointID].click);
return circle;
});
group.append(...circles);
return group;
});
this.frameContent.append(...this.groups);
}
public updateObjects(): void {
if (!this.enabled) return;
this.removeMarkers();
const currentClientID = this.currentShape.node.dataset.originClientId;
const shapes = Array.from(this.frameContent.getElementsByClassName('cvat_canvas_shape'));
const transformedShapes = shapes.map((shape: HTMLElement): TransformedShape | null => {
const color = shape.getAttribute('fill');
const clientID = shape.getAttribute('clientID');
if (color === null || clientID === null) return null;
if (+clientID === +currentClientID) {
return null;
}
let points = '';
if (shape.tagName === 'polyline' || shape.tagName === 'polygon') {
points = shape.getAttribute('points');
} else if (shape.tagName === 'rect') {
const x = +shape.getAttribute('x');
const y = +shape.getAttribute('y');
const width = +shape.getAttribute('width');
const height = +shape.getAttribute('height');
if (Number.isNaN(x) || Number.isNaN(y) || Number.isNaN(x) || Number.isNaN(x)) {
return null;
}
points = `${x},${y} ${x + width},${y} ${x + width},${y + height} ${x},${y + height}`;
} else if (shape.tagName === 'g') {
const polylineID = shape.dataset.polylineId;
const polyline = this.frameContent.getElementById(polylineID);
if (polyline && polyline.getAttribute('points')) {
points = polyline.getAttribute('points');
} else {
return null;
}
}
return {
color,
points: points.trim(),
};
}).filter((state: TransformedShape | null): boolean => state !== null);
this.drawMarkers(transformedShapes);
}
public autoborder(
enabled: boolean,
currentShape?: SVG.Shape,
ignoreCurrent: boolean = false,
): void {
if (enabled && !this.enabled && currentShape) {
this.enabled = true;
this.currentShape = currentShape;
this.ignoreCurrent = ignoreCurrent;
this.updateObjects();
} else {
this.release();
}
}
public transform(geometry: Geometry): void {
this.scale = geometry.scale;
this.groups.forEach((group: SVGGElement): void => {
Array.from(group.children).forEach((circle: SVGCircleElement): void => {
circle.setAttribute('r', `${consts.BASE_POINT_SIZE / this.scale}`);
circle.setAttribute('stroke-width', `${consts.BASE_STROKE_WIDTH / this.scale}`);
});
});
}
}

@ -47,6 +47,7 @@ export enum RectDrawingMethod {
}
export interface Configuration {
autoborders?: boolean;
displayAllText?: boolean;
undefinedAttrValue?: string;
}
@ -206,6 +207,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
},
configuration: {
displayAllText: false,
autoborders: false,
undefinedAttrValue: '',
},
imageBitmap: false,
@ -327,6 +329,12 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
public setup(frameData: any, objectStates: any[]): void {
if (this.data.imageID !== frameData.number) {
if ([Mode.EDIT, Mode.DRAG, Mode.RESIZE].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
}
if (frameData.number === this.data.imageID) {
this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS_UPDATED);
@ -360,6 +368,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
public activate(clientID: number | null, attributeID: number | null): void {
if (this.data.activeElement.clientID === clientID) {
return;
}
if (this.data.mode !== Mode.IDLE && clientID !== null) {
// Exception or just return?
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
@ -509,14 +521,14 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
public configure(configuration: Configuration): void {
if (this.data.mode !== Mode.IDLE) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (typeof (configuration.displayAllText) !== 'undefined') {
this.data.configuration.displayAllText = configuration.displayAllText;
}
if (typeof (configuration.autoborders) !== 'undefined') {
this.data.configuration.autoborders = configuration.autoborders;
}
if (typeof (configuration.undefinedAttrValue) !== 'undefined') {
this.data.configuration.undefinedAttrValue = configuration.undefinedAttrValue;
}

@ -16,6 +16,7 @@ import { MergeHandler, MergeHandlerImpl } from './mergeHandler';
import { SplitHandler, SplitHandlerImpl } from './splitHandler';
import { GroupHandler, GroupHandlerImpl } from './groupHandler';
import { ZoomHandler, ZoomHandlerImpl } from './zoomHandler';
import { AutoborderHandler, AutoborderHandlerImpl } from './autoborderHandler';
import consts from './consts';
import {
translateToSVG,
@ -23,6 +24,7 @@ import {
pointsToArray,
displayShapeSize,
ShapeSizeElement,
DrawnState,
} from './shared';
import {
CanvasModel,
@ -58,7 +60,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private controller: CanvasController;
private svgShapes: Record<number, SVG.Shape>;
private svgTexts: Record<number, SVG.Text>;
private drawnStates: Record<number, any>;
private drawnStates: Record<number, DrawnState>;
private geometry: Geometry;
private drawHandler: DrawHandler;
private editHandler: EditHandler;
@ -66,6 +68,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private splitHandler: SplitHandler;
private groupHandler: GroupHandler;
private zoomHandler: ZoomHandler;
private autoborderHandler: AutoborderHandler;
private activeElement: ActiveElement;
private configuration: Configuration;
@ -358,6 +361,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
// Transform handlers
this.drawHandler.transform(this.geometry);
this.editHandler.transform(this.geometry);
this.autoborderHandler.transform(this.geometry);
}
private resizeCanvas(): void {
@ -385,6 +389,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
created.push(state);
} else {
const drawnState = this.drawnStates[state.clientID];
// object has been changed or changed frame for a track
if (drawnState.updated !== state.updated || drawnState.frame !== state.frame) {
updated.push(state);
}
@ -395,30 +400,33 @@ export class CanvasViewImpl implements CanvasView, Listener {
.filter((id: number): boolean => !newIDs.includes(id))
.map((id: number): any => this.drawnStates[id]);
if (deleted.length || updated.length || created.length) {
if (this.activeElement.clientID !== null) {
this.deactivate();
}
if (this.activeElement.clientID !== null) {
this.deactivate();
}
for (const state of deleted) {
if (state.clientID in this.svgTexts) {
this.svgTexts[state.clientID].remove();
}
for (const state of deleted) {
if (state.clientID in this.svgTexts) {
this.svgTexts[state.clientID].remove();
this.svgShapes[state.clientID].off('click.canvas');
this.svgShapes[state.clientID].remove();
delete this.drawnStates[state.clientID];
}
this.svgShapes[state.clientID].off('click.canvas');
this.svgShapes[state.clientID].remove();
delete this.drawnStates[state.clientID];
}
this.addObjects(created, translate);
this.updateObjects(updated, translate);
this.sortObjects();
this.addObjects(created, translate);
this.updateObjects(updated, translate);
this.sortObjects();
if (this.controller.activeElement.clientID !== null) {
const { clientID } = this.controller.activeElement;
if (states.map((state: any): number => state.clientID).includes(clientID)) {
this.activate(this.controller.activeElement);
if (this.controller.activeElement.clientID !== null) {
const { clientID } = this.controller.activeElement;
if (states.map((state: any): number => state.clientID).includes(clientID)) {
this.activate(this.controller.activeElement);
}
}
this.autoborderHandler.updateObjects();
}
}
@ -459,7 +467,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
e.preventDefault();
}
function contextmenuHandler(e: MouseEvent): void {
function contextMenuHandler(e: MouseEvent): void {
const pointID = Array.prototype.indexOf
.call(((e.target as HTMLElement).parentElement as HTMLElement).children, e.target);
if (self.activeElement.clientID !== null) {
@ -467,7 +475,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
.filter((_state: any): boolean => (
_state.clientID === self.activeElement.clientID
));
self.canvas.dispatchEvent(new CustomEvent('point.contextmenu', {
self.canvas.dispatchEvent(new CustomEvent('canvas.contextmenu', {
bubbles: false,
cancelable: true,
detail: {
@ -502,7 +510,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
});
circle.on('dblclick', dblClickHandler);
circle.on('contextmenu', contextmenuHandler);
circle.on('contextmenu', contextMenuHandler);
circle.addClass('cvat_canvas_selected_point');
});
@ -512,7 +520,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
});
circle.off('dblclick', dblClickHandler);
circle.off('contextmenu', contextmenuHandler);
circle.off('contextmenu', contextMenuHandler);
circle.removeClass('cvat_canvas_selected_point');
});
@ -620,14 +628,19 @@ export class CanvasViewImpl implements CanvasView, Listener {
const self = this;
// Setup API handlers
this.autoborderHandler = new AutoborderHandlerImpl(
this.content,
);
this.drawHandler = new DrawHandlerImpl(
this.onDrawDone.bind(this),
this.adoptedContent,
this.adoptedText,
this.autoborderHandler,
);
this.editHandler = new EditHandlerImpl(
this.onEditDone.bind(this),
this.adoptedContent,
this.autoborderHandler,
);
this.mergeHandler = new MergeHandlerImpl(
this.onMergeDone.bind(this),
@ -712,8 +725,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.geometry = this.controller.geometry;
if (reason === UpdateReasons.CONFIG_UPDATED) {
this.configuration = model.configuration;
this.setupObjects([]);
this.setupObjects(model.objects);
this.editHandler.configurate(this.configuration);
this.drawHandler.configurate(this.configuration);
// todo: setup text, add if doesn't exist and enabled
// remove if exist and not enabled
// this.setupObjects([]);
// this.setupObjects(model.objects);
} else if (reason === UpdateReasons.BITMAP) {
const { imageBitmap } = model;
if (imageBitmap) {
@ -960,6 +978,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
attributes: { ...state.attributes },
zOrder: state.zOrder,
pinned: state.pinned,
updated: state.updated,
frame: state.frame,
};
}
@ -969,9 +989,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
const drawnState = this.drawnStates[clientID];
const shape = this.svgShapes[state.clientID];
const text = this.svgTexts[state.clientID];
const isInvisible = state.hidden || state.outside;
if (drawnState.hidden !== state.hidden || drawnState.outside !== state.outside) {
const isInvisible = state.hidden || state.outside;
if (isInvisible) {
(state.shapeType === 'points' ? shape.remember('_selectHandler').nested : shape)
.style('display', 'none');
@ -1041,7 +1061,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
(shape as any).clear();
shape.attr('points', stringified);
if (state.shapeType === 'points') {
if (state.shapeType === 'points' && !isInvisible) {
this.selectize(false, shape);
this.setupPoints(shape as SVG.PolyLine, state);
}
@ -1049,7 +1069,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
for (const attrID of Object.keys(state.attributes)) {
if (state.attributes[attrID] !== drawnState.attributes[attrID]) {
if (state.attributes[attrID] !== drawnState.attributes[+attrID]) {
if (text) {
const [span] = text.node
.querySelectorAll(`[attrID="${attrID}"]`) as any as SVGTSpanElement[];
@ -1187,7 +1207,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
(shape as any).off('resizestart');
(shape as any).off('resizing');
(shape as any).off('resizedone');
(shape as any).resize(false);
(shape as any).resize('stop');
// TODO: Hide text only if it is hidden by settings
const text = this.svgTexts[clientID];
@ -1537,12 +1557,15 @@ export class CanvasViewImpl implements CanvasView, Listener {
.addClass('cvat_canvas_shape').attr({
clientID: state.clientID,
id: `cvat_canvas_shape_${state.clientID}`,
'data-polyline-id': basicPolyline.attr('id'),
'data-z-order': state.zOrder,
});
group.on('click.canvas', (event: MouseEvent): void => {
// Need to redispatch the event on another element
basicPolyline.fire(new MouseEvent('click', event));
// redispatch event to canvas to be able merge points clicking them
this.content.dispatchEvent(new MouseEvent('click', event));
});
group.bbox = basicPolyline.bbox.bind(basicPolyline);

@ -7,11 +7,7 @@ import consts from './consts';
import 'svg.draw.js';
import './svg.patch';
import {
DrawData,
Geometry,
RectDrawingMethod,
} from './canvasModel';
import { AutoborderHandler } from './autoborderHandler';
import {
translateToSVG,
@ -23,7 +19,15 @@ import {
Box,
} from './shared';
import {
DrawData,
Geometry,
RectDrawingMethod,
Configuration,
} from './canvasModel';
export interface DrawHandler {
configurate(configuration: Configuration): void;
draw(drawData: DrawData, geometry: Geometry): void;
transform(geometry: Geometry): void;
cancel(): void;
@ -45,6 +49,8 @@ export class DrawHandlerImpl implements DrawHandler {
};
private drawData: DrawData;
private geometry: Geometry;
private autoborderHandler: AutoborderHandler;
private autobordersEnabled: boolean;
// we should use any instead of SVG.Shape because svg plugins cannot change declared interface
// so, methods like draw() just undefined for SVG.Shape, but nevertheless they exist
@ -127,6 +133,7 @@ export class DrawHandlerImpl implements DrawHandler {
return;
}
this.autoborderHandler.autoborder(false);
this.initialized = false;
this.canvas.off('mousedown.draw');
this.canvas.off('mouseup.draw');
@ -334,6 +341,9 @@ export class DrawHandlerImpl implements DrawHandler {
});
this.drawPolyshape();
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.drawInstance, false);
}
}
private drawPolyline(): void {
@ -344,6 +354,9 @@ export class DrawHandlerImpl implements DrawHandler {
});
this.drawPolyshape();
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.drawInstance, false);
}
}
private drawPoints(): void {
@ -599,7 +612,10 @@ export class DrawHandlerImpl implements DrawHandler {
onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean) => void,
canvas: SVG.Container,
text: SVG.Container,
autoborderHandler: AutoborderHandler,
) {
this.autoborderHandler = autoborderHandler;
this.autobordersEnabled = false;
this.startTimestamp = Date.now();
this.onDrawDone = onDrawDone;
this.canvas = canvas;
@ -629,6 +645,19 @@ export class DrawHandlerImpl implements DrawHandler {
});
}
public configurate(configuration: Configuration): void {
if (typeof (configuration.autoborders) === 'boolean') {
this.autobordersEnabled = configuration.autoborders;
if (this.drawInstance) {
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.drawInstance, false);
} else {
this.autoborderHandler.autoborder(false);
}
}
}
}
public transform(geometry: Geometry): void {
this.geometry = geometry;

@ -6,29 +6,27 @@ import * as SVG from 'svg.js';
import 'svg.select.js';
import consts from './consts';
import {
translateFromSVG,
pointsToArray,
} from './shared';
import {
EditData,
Geometry,
} from './canvasModel';
import { translateFromSVG, pointsToArray } from './shared';
import { EditData, Geometry, Configuration } from './canvasModel';
import { AutoborderHandler } from './autoborderHandler';
export interface EditHandler {
edit(editData: EditData): void;
transform(geometry: Geometry): void;
configurate(configuration: Configuration): void;
cancel(): void;
}
export class EditHandlerImpl implements EditHandler {
private onEditDone: (state: any, points: number[]) => void;
private autoborderHandler: AutoborderHandler;
private geometry: Geometry;
private canvas: SVG.Container;
private editData: EditData;
private editedShape: SVG.Shape;
private editLine: SVG.PolyLine;
private clones: SVG.Polygon[];
private autobordersEnabled: boolean;
private startEdit(): void {
// get started coordinates
@ -77,6 +75,8 @@ export class EditHandlerImpl implements EditHandler {
(this.editLine as any).addClass('cvat_canvas_shape_drawing').style({
'pointer-events': 'none',
'fill-opacity': 0,
}).attr({
'data-origin-client-id': this.editData.state.clientID,
}).on('drawstart drawpoint', (e: CustomEvent): void => {
this.transform(this.geometry);
lastDrawnPoint.x = e.detail.event.clientX;
@ -89,6 +89,9 @@ export class EditHandlerImpl implements EditHandler {
}
this.setupEditEvents();
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.editLine, true);
}
}
private setupEditEvents(): void {
@ -273,6 +276,7 @@ export class EditHandlerImpl implements EditHandler {
this.canvas.off('mousedown.edit');
this.canvas.off('mouseup.edit');
this.canvas.off('mousemove.edit');
this.autoborderHandler.autoborder(false);
if (this.editedShape) {
this.setupPoints(false);
@ -314,7 +318,10 @@ export class EditHandlerImpl implements EditHandler {
public constructor(
onEditDone: (state: any, points: number[]) => void,
canvas: SVG.Container,
autoborderHandler: AutoborderHandler,
) {
this.autoborderHandler = autoborderHandler;
this.autobordersEnabled = false;
this.onEditDone = onEditDone;
this.canvas = canvas;
this.editData = null;
@ -343,6 +350,19 @@ export class EditHandlerImpl implements EditHandler {
this.onEditDone(null, null);
}
public configurate(configuration: Configuration): void {
if (typeof (configuration.autoborders) === 'boolean') {
this.autobordersEnabled = configuration.autoborders;
if (this.editLine) {
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.editLine, true);
} else {
this.autoborderHandler.autoborder(false);
}
}
}
}
public transform(geometry: Geometry): void {
this.geometry = geometry;

@ -25,6 +25,21 @@ export interface BBox {
y: number;
}
export interface DrawnState {
clientID: number;
outside?: boolean;
occluded?: boolean;
hidden?: boolean;
lock: boolean;
shapeType: string;
points?: number[];
attributes: Record<number, string>;
zOrder?: number;
pinned?: boolean;
updated: number;
frame: number;
}
// Translate point array from the canvas coordinate system
// to the coordinate system of a client
export function translateFromSVG(svg: SVGSVGElement, points: number[]): number[] {

@ -16,7 +16,9 @@ SVG.Element.prototype.draw = function constructor(...args: any): any {
if (!handler) {
originalDraw.call(this, ...args);
handler = this.remember('_paintHandler');
if (!handler.set) {
// There is use case (drawing a single point when handler is created and destructed immediately in one stack)
// So, we need to check if handler still exists
if (handler && !handler.set) {
handler.set = new SVG.Set();
}
} else {

@ -150,6 +150,7 @@
}
for (const shape of data.shapes) {
if (shape.type === 'cuboid') continue;
const clientID = ++this.count;
const shapeModel = shapeFactory(shape, clientID, this.injection);
this.shapes[shapeModel.frame] = this.shapes[shapeModel.frame] || [];
@ -871,8 +872,10 @@
// In particular consider first and last frame as keyframes for all frames
const statesData = [].concat(
(frame in this.shapes ? this.shapes[frame] : [])
.filter((shape) => !shape.removed)
.map((shape) => shape.get(frame)),
(frame in this.tags ? this.tags[frame] : [])
.filter((tag) => !tag.removed)
.map((tag) => tag.get(frame)),
);
const tracks = Object.values(this.tracks)
@ -880,7 +883,7 @@
frame in track.shapes
|| frame === frameFrom
|| frame === frameTo
));
)).filter((track) => !track.removed);
statesData.push(
...tracks.map((track) => track.get(frame))
.filter((state) => !state.outside),

@ -184,8 +184,10 @@
this.history.do(HistoryActions.CHANGED_LOCK, () => {
this.lock = undoLock;
this.updated = Date.now();
}, () => {
this.lock = redoLock;
this.updated = Date.now();
}, [this.clientID], frame);
this.lock = lock;
@ -197,8 +199,10 @@
this.history.do(HistoryActions.CHANGED_COLOR, () => {
this.color = undoColor;
this.updated = Date.now();
}, () => {
this.color = redoColor;
this.updated = Date.now();
}, [this.clientID], frame);
this.color = color;
@ -210,8 +214,10 @@
this.history.do(HistoryActions.CHANGED_HIDDEN, () => {
this.hidden = undoHidden;
this.updated = Date.now();
}, () => {
this.hidden = redoHidden;
this.updated = Date.now();
}, [this.clientID], frame);
this.hidden = hidden;
@ -229,9 +235,11 @@
this.history.do(HistoryActions.CHANGED_LABEL, () => {
this.label = undoLabel;
this.attributes = undoAttributes;
this.updated = Date.now();
}, () => {
this.label = redoLabel;
this.attributes = redoAttributes;
this.updated = Date.now();
}, [this.clientID], frame);
}
@ -246,8 +254,10 @@
this.history.do(HistoryActions.CHANGED_ATTRIBUTES, () => {
this.attributes = undoAttributes;
this.updated = Date.now();
}, () => {
this.attributes = redoAttributes;
this.updated = Date.now();
}, [this.clientID], frame);
}
@ -373,9 +383,12 @@
this.removed = true;
this.history.do(HistoryActions.REMOVED_OBJECT, () => {
this.serverID = undefined;
this.removed = false;
this.updated = Date.now();
}, () => {
this.removed = true;
this.updated = Date.now();
}, [this.clientID], frame);
}
@ -398,8 +411,10 @@
this.history.do(HistoryActions.CHANGED_PINNED, () => {
this.pinned = undoPinned;
this.updated = Date.now();
}, () => {
this.pinned = redoPinned;
this.updated = Date.now();
}, [this.clientID], frame);
this.pinned = pinned;
@ -489,8 +504,10 @@
this.history.do(HistoryActions.CHANGED_POINTS, () => {
this.points = undoPoints;
this.updated = Date.now();
}, () => {
this.points = redoPoints;
this.updated = Date.now();
}, [this.clientID], frame);
this.points = points;
@ -502,8 +519,10 @@
this.history.do(HistoryActions.CHANGED_OCCLUDED, () => {
this.occluded = undoOccluded;
this.updated = Date.now();
}, () => {
this.occluded = redoOccluded;
this.updated = Date.now();
}, [this.clientID], frame);
this.occluded = occluded;
@ -515,8 +534,10 @@
this.history.do(HistoryActions.CHANGED_ZORDER, () => {
this.zOrder = undoZOrder;
this.updated = Date.now();
}, () => {
this.zOrder = redoZOrder;
this.updated = Date.now();
}, [this.clientID], frame);
this.zOrder = zOrder;
@ -777,12 +798,14 @@
for (const mutable of undoAttributes.mutable) {
this.shapes[mutable.frame].attributes = mutable.attributes;
}
this.updated = Date.now();
}, () => {
this.label = redoLabel;
this.attributes = redoAttributes.unmutable;
for (const mutable of redoAttributes.mutable) {
this.shapes[mutable.frame].attributes = mutable.attributes;
}
this.updated = Date.now();
}, [this.clientID], frame);
}
@ -853,11 +876,13 @@
} else if (redoShape) {
delete this.shapes[frame];
}
this.updated = Date.now();
}, () => {
this.attributes = redoAttributes;
if (redoShape) {
this.shapes[frame] = redoShape;
}
this.updated = Date.now();
}, [this.clientID], frame);
}
@ -868,12 +893,14 @@
} else {
this.shapes[frame] = undoShape;
}
this.updated = Date.now();
}, () => {
if (!redoShape) {
delete this.shapes[frame];
} else {
this.shapes[frame] = redoShape;
}
this.updated = Date.now();
}, [this.clientID], frame);
}

@ -195,12 +195,11 @@ class LogWithExceptionInfo extends Log {
}
dump() {
const payload = { ...this.payload };
let body = super.dump();
const payload = body.payload;
const client = detect();
const body = {
client_id: payload.client_id,
name: this.type,
time: this.time.toISOString(),
body = {
...body,
message: payload.message,
filename: payload.filename,
line: payload.line,
@ -211,17 +210,13 @@ class LogWithExceptionInfo extends Log {
version: client.version,
};
delete payload.client_id;
delete payload.message;
delete payload.filename;
delete payload.line;
delete payload.column;
delete payload.stack;
return {
...body,
payload,
};
return body;
}
}

@ -923,7 +923,9 @@ export function getJobAsync(
dispatch({
type: AnnotationActionTypes.GET_JOB,
payload: {},
payload: {
requestedId: jid,
},
});
const loadJobEvent = await logger.log(

@ -33,6 +33,7 @@ export function checkPluginsAsync(): ThunkAction {
GIT_INTEGRATION: false,
TF_ANNOTATION: false,
TF_SEGMENTATION: false,
DEXTR_SEGMENTATION: false,
};
const promises: Promise<boolean>[] = [
@ -41,15 +42,12 @@ export function checkPluginsAsync(): ThunkAction {
PluginChecker.check(SupportedPlugins.GIT_INTEGRATION),
PluginChecker.check(SupportedPlugins.TF_ANNOTATION),
PluginChecker.check(SupportedPlugins.TF_SEGMENTATION),
PluginChecker.check(SupportedPlugins.DEXTR_SEGMENTATION),
];
const values = await Promise.all(promises);
[plugins.ANALYTICS] = values;
[, plugins.AUTO_ANNOTATION] = values;
[,, plugins.GIT_INTEGRATION] = values;
[,,, plugins.TF_ANNOTATION] = values;
[,,,, plugins.TF_SEGMENTATION] = values;
[plugins.ANALYTICS, plugins.AUTO_ANNOTATION, plugins.GIT_INTEGRATION,
plugins.TF_ANNOTATION, plugins.TF_SEGMENTATION, plugins.DEXTR_SEGMENTATION] = values;
dispatch(pluginActions.checkedAllPlugins(plugins));
};
}

@ -28,6 +28,7 @@ export enum SettingsActionTypes {
SWITCH_AUTO_SAVE = 'SWITCH_AUTO_SAVE',
CHANGE_AUTO_SAVE_INTERVAL = 'CHANGE_AUTO_SAVE_INTERVAL',
CHANGE_AAM_ZOOM_MARGIN = 'CHANGE_AAM_ZOOM_MARGIN',
SWITCH_AUTOMATIC_BORDERING = 'SWITCH_AUTOMATIC_BORDERING',
SWITCH_SHOWNIG_INTERPOLATED_TRACKS = 'SWITCH_SHOWNIG_INTERPOLATED_TRACKS',
SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS = 'SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS',
}
@ -220,3 +221,12 @@ export function switchShowingObjectsTextAlways(showObjectsTextAlways: boolean):
},
};
}
export function switchAutomaticBordering(automaticBordering: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_AUTOMATIC_BORDERING,
payload: {
automaticBordering,
},
};
}

@ -36,7 +36,17 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
useEffect(() => {
saveLogs();
return saveLogs;
const root = window.document.getElementById('root');
if (root) {
root.style.minHeight = '768px';
}
return () => {
saveLogs();
if (root) {
root.style.minHeight = '';
}
};
}, []);
if (job === null) {

@ -62,7 +62,9 @@ interface Props {
aamZoomMargin: number;
showObjectsTextAlways: boolean;
workspace: Workspace;
automaticBordering: boolean;
keyMap: Record<string, ExtendedKeyMapOptions>;
switchableAutomaticBordering: boolean;
onSetupCanvas: () => void;
onDragCanvas: (enabled: boolean) => void;
onZoomCanvas: (enabled: boolean) => void;
@ -89,11 +91,13 @@ interface Props {
onChangeGridOpacity(opacity: number): void;
onChangeGridColor(color: GridColor): void;
onSwitchGrid(enabled: boolean): void;
onSwitchAutomaticBordering(enabled: boolean): void;
}
export default class CanvasWrapperComponent extends React.PureComponent<Props> {
public componentDidMount(): void {
const {
automaticBordering,
showObjectsTextAlways,
canvasInstance,
curZLayer,
@ -106,6 +110,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
wrapper.appendChild(canvasInstance.html());
canvasInstance.configure({
autoborders: automaticBordering,
undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE,
displayAllText: showObjectsTextAlways,
});
@ -139,12 +144,16 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
workspace,
frameFetching,
showObjectsTextAlways,
automaticBordering,
} = this.props;
if (prevProps.showObjectsTextAlways !== showObjectsTextAlways) {
if (prevProps.showObjectsTextAlways !== showObjectsTextAlways
|| prevProps.automaticBordering !== automaticBordering
) {
canvasInstance.configure({
undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE,
displayAllText: showObjectsTextAlways,
autoborders: automaticBordering,
});
}
@ -160,9 +169,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
if (prevProps.activatedStateID !== null
&& prevProps.activatedStateID !== activatedStateID) {
canvasInstance.activate(null);
}
if (activatedStateID) {
const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`);
if (el) {
(el as any).instance.fill({ opacity: opacity / 100 });
@ -265,7 +271,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.html().removeEventListener('canvas.groupped', this.onCanvasObjectsGroupped);
canvasInstance.html().removeEventListener('canvas.splitted', this.onCanvasTrackSplitted);
canvasInstance.html().removeEventListener('point.contextmenu', this.onCanvasPointContextMenu);
canvasInstance.html().removeEventListener('canvas.contextmenu', this.onCanvasPointContextMenu);
window.removeEventListener('resize', this.fitCanvas);
}
@ -686,7 +692,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.html().addEventListener('canvas.groupped', this.onCanvasObjectsGroupped);
canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted);
canvasInstance.html().addEventListener('point.contextmenu', this.onCanvasPointContextMenu);
canvasInstance.html().addEventListener('canvas.contextmenu', this.onCanvasPointContextMenu);
}
public render(): JSX.Element {
@ -699,16 +705,19 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
brightnessLevel,
contrastLevel,
saturationLevel,
keyMap,
grid,
gridColor,
gridOpacity,
switchableAutomaticBordering,
automaticBordering,
onChangeBrightnessLevel,
onChangeSaturationLevel,
onChangeContrastLevel,
onChangeGridColor,
onChangeGridOpacity,
onSwitchGrid,
keyMap,
onSwitchAutomaticBordering,
} = this.props;
const preventDefault = (event: KeyboardEvent | undefined): void => {
@ -727,8 +736,10 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
INCREASE_GRID_OPACITY: keyMap.INCREASE_GRID_OPACITY,
DECREASE_GRID_OPACITY: keyMap.DECREASE_GRID_OPACITY,
CHANGE_GRID_COLOR: keyMap.CHANGE_GRID_COLOR,
SWITCH_AUTOMATIC_BORDERING: keyMap.SWITCH_AUTOMATIC_BORDERING,
};
const step = 10;
const handlers = {
INCREASE_BRIGHTNESS: (event: KeyboardEvent | undefined) => {
@ -803,6 +814,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const color = colors[indexOf >= colors.length ? 0 : indexOf];
onChangeGridColor(color);
},
SWITCH_AUTOMATIC_BORDERING: (event: KeyboardEvent | undefined) => {
if (switchableAutomaticBordering) {
preventDefault(event);
onSwitchAutomaticBordering(!automaticBordering);
}
},
};
return (

@ -0,0 +1,90 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState } from 'react';
import { connect } from 'react-redux';
import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox';
import Tooltip from 'antd/lib/tooltip';
import { Canvas } from 'cvat-canvas';
import { CombinedState } from 'reducers/interfaces';
import { activate as activatePlugin, deactivate as deactivatePlugin } from 'utils/dextr-utils';
interface StateToProps {
pluginEnabled: boolean;
canvasInstance: Canvas;
}
interface DispatchToProps {
activate(canvasInstance: Canvas): void;
deactivate(canvasInstance: Canvas): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
plugins: {
list,
},
annotation: {
canvas: {
instance: canvasInstance,
},
},
} = state;
return {
canvasInstance,
pluginEnabled: list.DEXTR_SEGMENTATION,
};
}
function mapDispatchToProps(): DispatchToProps {
return {
activate(canvasInstance: Canvas): void {
activatePlugin(canvasInstance);
},
deactivate(canvasInstance: Canvas): void {
deactivatePlugin(canvasInstance);
},
};
}
function DEXTRPlugin(props: StateToProps & DispatchToProps): JSX.Element | null {
const {
pluginEnabled,
canvasInstance,
activate,
deactivate,
} = props;
const [pluginActivated, setActivated] = useState(false);
return (
pluginEnabled ? (
<Tooltip title='Make AI polygon from at least 4 extreme points using deep extreme cut'>
<Checkbox
style={{ marginTop: 5 }}
checked={pluginActivated}
onChange={(event: CheckboxChangeEvent): void => {
setActivated(event.target.checked);
if (event.target.checked) {
activate(canvasInstance);
} else {
deactivate(canvasInstance);
}
}}
>
Make AI polygon
</Checkbox>
</Tooltip>
) : null
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(DEXTRPlugin);
// TODO: Add dialog window with cancel button

@ -3,7 +3,6 @@
// SPDX-License-Identifier: MIT
import React from 'react';
import { Row, Col } from 'antd/lib/grid';
import Select from 'antd/lib/select';
import Button from 'antd/lib/button';
@ -15,6 +14,7 @@ import Text from 'antd/lib/typography/Text';
import { RectDrawingMethod } from 'cvat-canvas';
import { ShapeType } from 'reducers/interfaces';
import { clamp } from 'utils/math';
import DEXTRPlugin from './dextr-plugin';
interface Props {
shapeType: ShapeType;
@ -47,6 +47,9 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
onChangeRectDrawingMethod,
} = props;
const trackDisabled = shapeType === ShapeType.POLYGON || shapeType === ShapeType.POLYLINE
|| (shapeType === ShapeType.POINTS && numberOfPoints !== 1);
return (
<div className='cvat-draw-shape-popover-content'>
<Row type='flex' justify='start'>
@ -78,6 +81,9 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
</Select>
</Col>
</Row>
{
shapeType === ShapeType.POLYGON && <DEXTRPlugin />
}
{
shapeType === ShapeType.RECTANGLE ? (
<>
@ -148,7 +154,7 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
<Tooltip title={`Press ${repeatShapeShortcut} to draw again`}>
<Button
onClick={onDrawTrack}
disabled={shapeType !== ShapeType.RECTANGLE}
disabled={trackDisabled}
>
Track
</Button>

@ -3,20 +3,16 @@
// SPDX-License-Identifier: MIT
import React from 'react';
import {
Row,
Col,
Icon,
Alert,
Button,
Tooltip,
message,
notification,
} from 'antd';
import { Row, Col } from 'antd/lib/grid';
import Icon from 'antd/lib/icon';
import Alert from 'antd/lib/alert';
import Button from 'antd/lib/button';
import Tooltip from 'antd/lib/tooltip';
import message from 'antd/lib/message';
import notification from 'antd/lib/notification';
import Text from 'antd/lib/typography/Text';
import consts from 'consts';
import ConnectedFileManager, {
FileManagerContainer,
} from 'containers/file-manager/file-manager';
@ -107,7 +103,7 @@ export default class CreateModelContent extends React.PureComponent<Props> {
const status = modelCreatingStatus
&& modelCreatingStatus !== 'CREATED' ? modelCreatingStatus : '';
const guideLink = 'https://github.com/opencv/cvat/blob/develop/cvat/apps/auto_annotation/README.md';
const { AUTO_ANNOTATION_GUIDE_URL } = consts;
return (
<Row type='flex' justify='start' align='middle' className='cvat-create-model-content'>
<Col span={24}>
@ -116,7 +112,7 @@ export default class CreateModelContent extends React.PureComponent<Props> {
onClick={(): void => {
// false positive
// eslint-disable-next-line
window.open(guideLink, '_blank');
window.open(AUTO_ANNOTATION_GUIDE_URL, '_blank');
}}
type='question-circle'
/>

@ -3,30 +3,65 @@
// SPDX-License-Identifier: MIT
import './styles.scss';
import React from 'react';
import {
Row,
Col,
} from 'antd';
import React, { useEffect } from 'react';
import { Row, Col } from 'antd/lib/grid';
import Modal from 'antd/lib/modal';
import Text from 'antd/lib/typography/Text';
import Paragraph from 'antd/lib/typography/Paragraph';
import TextArea from 'antd/lib/input/TextArea';
import CreateTaskContent, { CreateTaskData } from './create-task-content';
interface Props {
onCreate: (data: CreateTaskData) => void;
status: string;
error: string;
installedGit: boolean;
}
export default function CreateTaskPage(props: Props): JSX.Element {
const {
error,
status,
onCreate,
installedGit,
} = props;
useEffect(() => {
if (error) {
let errorCopy = error;
const sshKeys: string[] = [];
while (errorCopy.length) {
const startIndex = errorCopy.search(/'ssh/);
if (startIndex === -1) break;
let sshKey = errorCopy.slice(startIndex + 1);
const stopIndex = sshKey.search(/'/);
sshKey = sshKey.slice(0, stopIndex);
sshKeys.push(sshKey);
errorCopy = errorCopy.slice(stopIndex + 1);
}
if (sshKeys.length) {
Modal.error({
width: 800,
title: 'Could not clone the repository',
content: (
<>
<Paragraph>
<Text>Please make sure it exists and you have access</Text>
</Paragraph>
<Paragraph>
<Text>Consider adding the following public ssh keys to git: </Text>
</Paragraph>
<TextArea rows={10} value={sshKeys.join('\n\n')} />
</>
),
});
}
}
}, [error]);
return (
<Row type='flex' justify='center' align='top' className='cvat-create-task-form-wrapper'>
<Col md={20} lg={16} xl={14} xxl={9}>

@ -6,7 +6,7 @@
.cvat-create-task-form-wrapper {
text-align: center;
margin-top: 40px;
padding-top: 40px;
overflow-y: auto;
height: 90%;

@ -4,13 +4,10 @@
import './styles.scss';
import React from 'react';
import {
Button,
Icon,
Popover,
} from 'antd';
import Button from 'antd/lib/button';
import Icon from 'antd/lib/icon';
import Popover from 'antd/lib/popover';
import Text from 'antd/lib/typography/Text';
import {
FacebookShareButton,
LinkedinShareButton,
@ -30,59 +27,59 @@ import {
LinkedinIcon,
} from 'react-share';
import Text from 'antd/lib/typography/Text';
import consts from 'consts';
function renderContent(): JSX.Element {
const githubURL = 'https://github.com/opencv/cvat';
const githubImage = 'https://raw.githubusercontent.com/opencv/'
+ 'cvat/develop/cvat/apps/documentation/static/documentation/images/cvat.jpg';
const questionsURL = 'https://gitter.im/opencv-cvat/public';
const feedbackURL = 'https://gitter.im/opencv-cvat/public';
const {
GITHUB_URL,
GITHUB_IMAGE_URL,
GITTER_PUBLIC_URL,
} = consts;
return (
<>
<Icon type='star' />
<Text style={{ marginLeft: '10px' }}>
Star us on
<a target='_blank' rel='noopener noreferrer' href={githubURL}> GitHub</a>
<a target='_blank' rel='noopener noreferrer' href={GITHUB_URL}> GitHub</a>
</Text>
<br />
<Icon type='like' />
<Text style={{ marginLeft: '10px' }}>
Leave a
<a target='_blank' rel='noopener noreferrer' href={feedbackURL}> feedback</a>
<a target='_blank' rel='noopener noreferrer' href={GITTER_PUBLIC_URL}> feedback</a>
</Text>
<hr />
<div style={{ display: 'flex' }}>
<FacebookShareButton url={githubURL} quote='Computer Vision Annotation Tool'>
<FacebookShareButton url={GITHUB_URL} quote='Computer Vision Annotation Tool'>
<FacebookIcon size={32} round />
</FacebookShareButton>
<VKShareButton url={githubURL} title='Computer Vision Annotation Tool' image={githubImage} description='CVAT'>
<VKShareButton url={GITHUB_URL} title='Computer Vision Annotation Tool' image={GITHUB_IMAGE_URL} description='CVAT'>
<VKIcon size={32} round />
</VKShareButton>
<TwitterShareButton url={githubURL} title='Computer Vision Annotation Tool' hashtags={['CVAT']}>
<TwitterShareButton url={GITHUB_URL} title='Computer Vision Annotation Tool' hashtags={['CVAT']}>
<TwitterIcon size={32} round />
</TwitterShareButton>
<RedditShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<RedditShareButton url={GITHUB_URL} title='Computer Vision Annotation Tool'>
<RedditIcon size={32} round />
</RedditShareButton>
<LinkedinShareButton url={githubURL}>
<LinkedinShareButton url={GITHUB_URL}>
<LinkedinIcon size={32} round />
</LinkedinShareButton>
<TelegramShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<TelegramShareButton url={GITHUB_URL} title='Computer Vision Annotation Tool'>
<TelegramIcon size={32} round />
</TelegramShareButton>
<WhatsappShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<WhatsappShareButton url={GITHUB_URL} title='Computer Vision Annotation Tool'>
<WhatsappIcon size={32} round />
</WhatsappShareButton>
<ViberShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<ViberShareButton url={GITHUB_URL} title='Computer Vision Annotation Tool'>
<ViberIcon size={32} round />
</ViberShareButton>
</div>
<hr />
<Text style={{ marginTop: '50px' }}>
Do you need help? Contact us on
<a target='_blank' rel='noopener noreferrer' href={questionsURL}> gitter</a>
<a target='_blank' rel='noopener noreferrer' href={GITTER_PUBLIC_URL}> gitter</a>
</Text>
</>
);

@ -4,17 +4,16 @@
import './styles.scss';
import React from 'react';
import {
Tabs,
Icon,
Input,
Upload,
} from 'antd';
import Tree, { AntTreeNode, TreeNodeNormal } from 'antd/lib/tree/Tree';
import { RcFile } from 'antd/lib/upload';
import Tabs from 'antd/lib/tabs';
import Icon from 'antd/lib/icon';
import Input from 'antd/lib/input';
import Text from 'antd/lib/typography/Text';
import Paragraph from 'antd/lib/typography/Paragraph';
import Upload, { RcFile } from 'antd/lib/upload';
import Empty from 'antd/lib/empty';
import Tree, { AntTreeNode, TreeNodeNormal } from 'antd/lib/tree/Tree';
import consts from 'consts';
export interface Files {
local: File[];
@ -148,6 +147,7 @@ export default class FileManager extends React.PureComponent<Props, State> {
});
}
const { SHARE_MOUNT_GUIDE_URL } = consts;
const { treeData } = this.props;
const {
expandedKeys,
@ -156,7 +156,7 @@ export default class FileManager extends React.PureComponent<Props, State> {
return (
<Tabs.TabPane key='share' tab='Connected file share'>
{ treeData.length
{ treeData[0].children && treeData[0].children.length
? (
<Tree
className='cvat-share-tree'
@ -190,7 +190,18 @@ export default class FileManager extends React.PureComponent<Props, State> {
>
{ renderTreeNodes(treeData) }
</Tree>
) : <Text className='cvat-text-color'>No data found</Text>}
) : (
<div className='cvat-empty-share-tree'>
<Empty />
<Paragraph className='cvat-text-color'>
Please, be sure you had
<Text strong>
<a href={SHARE_MOUNT_GUIDE_URL}> mounted </a>
</Text>
share before you built CVAT and the shared storage contains files
</Paragraph>
</div>
)}
</Tabs.TabPane>
);
}

@ -10,3 +10,9 @@
max-height: 20em;
overflow: auto;
}
.cvat-empty-share-tree {
> .ant-typography {
margin-top: 10px;
}
}

@ -19,6 +19,7 @@ import ErrorStackParser from 'error-stack-parser';
import { resetAfterErrorAsync } from 'actions/boundaries-actions';
import { CombinedState } from 'reducers/interfaces';
import logger, { LogType } from 'cvat-logger';
import consts from 'consts';
interface StateToProps {
job: any | null;
@ -161,7 +162,7 @@ class GlobalErrorBoundary extends React.PureComponent<Props, State> {
</li>
<li>
Notify an administrator or submit the issue directly on
<a href='https://github.com/opencv/cvat'> GitHub. </a>
<a href={consts.GITHUB_URL}> GitHub. </a>
Please, provide also:
<ul>
<li>Steps to reproduce the issue</li>

@ -16,6 +16,7 @@ import Modal from 'antd/lib/modal';
import Text from 'antd/lib/typography/Text';
import { CVATLogo, AccountIcon } from 'icons';
import consts from 'consts';
interface HeaderContainerProps {
onLogout: () => void;
@ -60,12 +61,15 @@ function HeaderContainer(props: Props): JSX.Element {
|| installedTFAnnotation
|| installedTFSegmentation;
function aboutModal(): void {
const CHANGELOG = 'https://github.com/opencv/cvat/blob/develop/CHANGELOG.md';
const LICENSE = 'https://github.com/opencv/cvat/blob/develop/LICENSE';
const GITTER = 'https://gitter.im/opencv-cvat';
const FORUM = 'https://software.intel.com/en-us/forums/intel-distribution-of-openvino-toolkit';
const {
CHANGELOG_URL,
LICENSE_URL,
GITTER_URL,
FORUM_URL,
GITHUB_URL,
} = consts;
function aboutModal(): void {
Modal.info({
title: `${toolName}`,
content: (
@ -106,10 +110,10 @@ function HeaderContainer(props: Props): JSX.Element {
</Text>
</p>
<Row type='flex' justify='space-around'>
<Col><a href={CHANGELOG} target='_blank' rel='noopener noreferrer'>{'What\'s new?'}</a></Col>
<Col><a href={LICENSE} target='_blank' rel='noopener noreferrer'>License</a></Col>
<Col><a href={GITTER} target='_blank' rel='noopener noreferrer'>Need help?</a></Col>
<Col><a href={FORUM} target='_blank' rel='noopener noreferrer'>Forum on Intel Developer Zone</a></Col>
<Col><a href={CHANGELOG_URL} target='_blank' rel='noopener noreferrer'>{'What\'s new?'}</a></Col>
<Col><a href={LICENSE_URL} target='_blank' rel='noopener noreferrer'>License</a></Col>
<Col><a href={GITTER_URL} target='_blank' rel='noopener noreferrer'>Need help?</a></Col>
<Col><a href={FORUM_URL} target='_blank' rel='noopener noreferrer'>Forum on Intel Developer Zone</a></Col>
</Row>
</div>
),
@ -199,7 +203,9 @@ function HeaderContainer(props: Props): JSX.Element {
type='link'
onClick={
(): void => {
window.open('https://github.com/opencv/cvat', '_blank');
// false positive
// eslint-disable-next-line security/detect-non-literal-fs-filename
window.open(GITHUB_URL, '_blank');
}
}
>

@ -23,10 +23,14 @@
.cvat-player-settings-grid,
.cvat-workspace-settings-auto-save,
.cvat-workspace-settings-autoborders,
.cvat-workspace-settings-show-text-always,
.cvat-workspace-settings-show-text-always-checkbox,
.cvat-workspace-settings-show-interpolated-checkbox {
margin-bottom: 10px;
.cvat-workspace-settings-show-interpolated {
margin-bottom: 25px;
> div:first-child {
margin-bottom: 10px;
}
}
.cvat-player-settings-grid-size,
@ -36,8 +40,6 @@
.cvat-player-settings-speed,
.cvat-player-settings-reset-zoom,
.cvat-player-settings-rotate-all,
.cvat-workspace-settings-show-text-always,
.cvat-workspace-settings-show-interpolated,
.cvat-workspace-settings-aam-zoom-margin,
.cvat-workspace-settings-auto-save-interval {
margin-bottom: 25px;

@ -17,11 +17,13 @@ interface Props {
aamZoomMargin: number;
showAllInterpolationTracks: boolean;
showObjectsTextAlways: boolean;
automaticBordering: boolean;
onSwitchAutoSave(enabled: boolean): void;
onChangeAutoSaveInterval(interval: number): void;
onChangeAAMZoomMargin(margin: number): void;
onSwitchShowingInterpolatedTracks(enabled: boolean): void;
onSwitchShowingObjectsTextAlways(enabled: boolean): void;
onSwitchAutomaticBordering(enabled: boolean): void;
}
export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
@ -31,11 +33,13 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
aamZoomMargin,
showAllInterpolationTracks,
showObjectsTextAlways,
automaticBordering,
onSwitchAutoSave,
onChangeAutoSaveInterval,
onChangeAAMZoomMargin,
onSwitchShowingInterpolatedTracks,
onSwitchShowingObjectsTextAlways,
onSwitchAutomaticBordering,
} = props;
const minAutoSaveInterval = 5;
@ -82,7 +86,7 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
</Col>
</Row>
<Row className='cvat-workspace-settings-show-interpolated'>
<Col className='cvat-workspace-settings-show-interpolated-checkbox'>
<Col>
<Checkbox
className='cvat-text-color'
checked={showAllInterpolationTracks}
@ -98,7 +102,7 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
</Col>
</Row>
<Row className='cvat-workspace-settings-show-text-always'>
<Col className='cvat-workspace-settings-show-text-always-checkbox'>
<Col>
<Checkbox
className='cvat-text-color'
checked={showObjectsTextAlways}
@ -113,6 +117,22 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
<Text type='secondary'> Show text for an object on the canvas not only when the object is activated </Text>
</Col>
</Row>
<Row className='cvat-workspace-settings-autoborders'>
<Col>
<Checkbox
className='cvat-text-color'
checked={automaticBordering}
onChange={(event: CheckboxChangeEvent): void => {
onSwitchAutomaticBordering(event.target.checked);
}}
>
Automatic bordering
</Checkbox>
</Col>
<Col>
<Text type='secondary'> Enable automatic bordering for polygons and polylines during drawing/editing </Text>
</Col>
</Row>
<Row className='cvat-workspace-settings-aam-zoom-margin'>
<Col>
<Text className='cvat-text-color'> Attribute annotation mode (AAM) zoom margin </Text>

@ -299,8 +299,14 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
repositoryStatus: 'sync',
});
}
}).catch((): void => {
}).catch((error): void => {
if (this.mounted) {
Modal.error({
width: 800,
title: 'Could not synchronize the repository',
content: error.toString(),
});
this.setState({
repositoryStatus: '!sync',
});

@ -51,20 +51,20 @@ function JobListComponent(props: Props & RouteComponentProps): JSX.Element {
key: 'job',
render: (id: number): JSX.Element => (
<div>
<Button type='link' href={`${baseURL}/?id=${id}`}>{`Job #${id}`}</Button>
<Button
type='link'
onClick={(): void => {
push(`/tasks/${taskId}/jobs/${id}`);
}}
>
{`Job #${id}`}
</Button>
|
<Tooltip title='Beta version of new UI written in React. It is to get
acquainted only, we do not recommend use it to annotation
process because it lacks of some features and can be unstable.'
<Tooltip title='Old version of UI is deprecated and will be removed from
new versions of UI. We still recomend it only if you use
specific features from it like cuboids annotation.'
>
<Button
type='link'
onClick={(): void => {
push(`/tasks/${taskId}/jobs/${id}`);
}}
>
Try new UI
</Button>
<Button type='link' href={`${baseURL}/?id=${id}`}>Old UI</Button>
</Tooltip>
</div>
),

@ -9,7 +9,7 @@
height: 100%;
> div:nth-child(1) {
margin-bottom: 10px;
padding-bottom: 10px;
div > {
span {
@ -36,11 +36,11 @@
> div:nth-child(3) {
height: 83%;
margin-top: 10px;
padding-top: 10px;
}
> div:nth-child(4) {
margin-top: 10px;
padding-top: 10px;
}
}

@ -4,8 +4,26 @@
const UNDEFINED_ATTRIBUTE_VALUE = '__undefined__';
const NO_BREAK_SPACE = '\u00a0';
const CHANGELOG_URL = 'https://github.com/opencv/cvat/blob/develop/CHANGELOG.md';
const LICENSE_URL = 'https://github.com/opencv/cvat/blob/develop/LICENSE';
const GITTER_URL = 'https://gitter.im/opencv-cvat';
const GITTER_PUBLIC_URL = 'https://gitter.im/opencv-cvat/public';
const FORUM_URL = 'https://software.intel.com/en-us/forums/intel-distribution-of-openvino-toolkit';
const GITHUB_URL = 'https://github.com/opencv/cvat';
const GITHUB_IMAGE_URL = 'https://raw.githubusercontent.com/opencv/cvat/develop/cvat/apps/documentation/static/documentation/images/cvat.jpg';
const AUTO_ANNOTATION_GUIDE_URL = 'https://github.com/opencv/cvat/blob/develop/cvat/apps/auto_annotation/README.md';
const SHARE_MOUNT_GUIDE_URL = 'https://github.com/opencv/cvat/blob/master/cvat/apps/documentation/installation.md#share-path';
export default {
UNDEFINED_ATTRIBUTE_VALUE,
NO_BREAK_SPACE,
CHANGELOG_URL,
LICENSE_URL,
GITTER_URL,
GITTER_PUBLIC_URL,
FORUM_URL,
GITHUB_URL,
GITHUB_IMAGE_URL,
AUTO_ANNOTATION_GUIDE_URL,
SHARE_MOUNT_GUIDE_URL,
};

@ -33,6 +33,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
const {
annotation: {
job: {
requestedId,
instance: job,
fetching,
},
@ -41,7 +42,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
} = state;
return {
job: !job || jobID === job.id ? job : null,
job: jobID === requestedId ? job : null,
fetching,
workspace,
};

@ -34,6 +34,7 @@ import {
changeBrightnessLevel,
changeContrastLevel,
changeSaturationLevel,
switchAutomaticBordering,
} from 'actions/settings-actions';
import {
ColorBy,
@ -42,6 +43,7 @@ import {
CombinedState,
ContextMenuType,
Workspace,
ActiveControl,
} from 'reducers/interfaces';
import { Canvas } from 'cvat-canvas';
@ -79,8 +81,10 @@ interface StateToProps {
minZLayer: number;
maxZLayer: number;
curZLayer: number;
automaticBordering: boolean;
contextVisible: boolean;
contextType: ContextMenuType;
switchableAutomaticBordering: boolean;
keyMap: Record<string, ExtendedKeyMapOptions>;
}
@ -111,12 +115,14 @@ interface DispatchToProps {
onChangeGridOpacity(opacity: number): void;
onChangeGridColor(color: GridColor): void;
onSwitchGrid(enabled: boolean): void;
onSwitchAutomaticBordering(enabled: boolean): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
canvas: {
activeControl,
contextMenu: {
visible: contextVisible,
type: contextType,
@ -166,6 +172,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
workspace: {
aamZoomMargin,
showObjectsTextAlways,
automaticBordering,
},
shapes: {
opacity,
@ -212,10 +219,14 @@ function mapStateToProps(state: CombinedState): StateToProps {
curZLayer,
minZLayer,
maxZLayer,
automaticBordering,
contextVisible,
contextType,
workspace,
keyMap,
switchableAutomaticBordering: activeControl === ActiveControl.DRAW_POLYGON
|| activeControl === ActiveControl.DRAW_POLYLINE
|| activeControl === ActiveControl.EDIT,
};
}
@ -301,6 +312,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSwitchGrid(enabled: boolean): void {
dispatch(switchGrid(enabled));
},
onSwitchAutomaticBordering(enabled: boolean): void {
dispatch(switchAutomaticBordering(enabled));
},
};
}

@ -7,6 +7,7 @@ import copy from 'copy-to-clipboard';
import { connect } from 'react-redux';
import { LogType } from 'cvat-logger';
import { Canvas, isAbleToChangeFrame } from 'cvat-canvas';
import { ActiveControl, CombinedState, ColorBy } from 'reducers/interfaces';
import {
collapseObjectItems,
@ -24,7 +25,6 @@ import {
import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item';
interface OwnProps {
clientID: number;
}
@ -44,6 +44,7 @@ interface StateToProps {
minZLayer: number;
maxZLayer: number;
normalizedKeyMap: Record<string, string>;
canvasInstance: Canvas;
}
interface DispatchToProps {
@ -84,6 +85,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
canvas: {
ready,
activeControl,
instance: canvasInstance,
},
colors,
},
@ -119,6 +121,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
minZLayer,
maxZLayer,
normalizedKeyMap,
canvasInstance,
};
}
@ -166,72 +169,44 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
type Props = StateToProps & DispatchToProps;
class ObjectItemContainer extends React.PureComponent<Props> {
private navigateFirstKeyframe = (): void => {
const {
objectState,
changeFrame,
frameNumber,
} = this.props;
const { objectState, frameNumber } = this.props;
const { first } = objectState.keyframes;
if (first !== frameNumber) {
changeFrame(first);
this.changeFrame(first);
}
};
private navigatePrevKeyframe = (): void => {
const {
objectState,
changeFrame,
frameNumber,
} = this.props;
const { objectState, frameNumber } = this.props;
const { prev } = objectState.keyframes;
if (prev !== null && prev !== frameNumber) {
changeFrame(prev);
this.changeFrame(prev);
}
};
private navigateNextKeyframe = (): void => {
const {
objectState,
changeFrame,
frameNumber,
} = this.props;
const { objectState, frameNumber } = this.props;
const { next } = objectState.keyframes;
if (next !== null && next !== frameNumber) {
changeFrame(next);
this.changeFrame(next);
}
};
private navigateLastKeyframe = (): void => {
const {
objectState,
changeFrame,
frameNumber,
} = this.props;
const { objectState, frameNumber } = this.props;
const { last } = objectState.keyframes;
if (last !== frameNumber) {
changeFrame(last);
this.changeFrame(last);
}
};
private copy = (): void => {
const {
objectState,
copyShape,
} = this.props;
const { objectState, copyShape } = this.props;
copyShape(objectState);
};
private propagate = (): void => {
const {
objectState,
propagateObject,
} = this.props;
const { objectState, propagateObject } = this.props;
propagateObject(objectState);
};
@ -422,6 +397,13 @@ class ObjectItemContainer extends React.PureComponent<Props> {
this.commit();
};
private changeFrame(frame: number): void {
const { changeFrame, canvasInstance } = this.props;
if (isAbleToChangeFrame(canvasInstance)) {
changeFrame(frame);
}
}
private commit(): void {
const {
objectState,

@ -15,6 +15,7 @@ import {
copyShape as copyShapeAction,
propagateObject as propagateObjectAction,
} from 'actions/annotation-actions';
import { Canvas, isAbleToChangeFrame } from 'cvat-canvas';
import { CombinedState, StatesOrdering, ObjectType } from 'reducers/interfaces';
interface StateToProps {
@ -32,6 +33,7 @@ interface StateToProps {
annotationsFiltersHistory: string[];
keyMap: Record<string, ExtendedKeyMapOptions>;
normalizedKeyMap: Record<string, string>;
canvasInstance: Canvas;
}
interface DispatchToProps {
@ -65,6 +67,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
number: frameNumber,
},
},
canvas: {
instance: canvasInstance,
},
tabContentHeight: listHeight,
},
shortcuts: {
@ -104,6 +109,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
annotationsFiltersHistory,
keyMap,
normalizedKeyMap,
canvasInstance,
};
}
@ -254,6 +260,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
minZLayer,
keyMap,
normalizedKeyMap,
canvasInstance,
} = this.props;
const {
sortedStatesID,
@ -388,7 +395,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
if (state && state.objectType === ObjectType.TRACK) {
const frame = typeof (state.keyframes.next) === 'number'
? state.keyframes.next : null;
if (frame !== null) {
if (frame !== null && isAbleToChangeFrame(canvasInstance)) {
changeFrame(frame);
}
}
@ -399,7 +406,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
if (state && state.objectType === ObjectType.TRACK) {
const frame = typeof (state.keyframes.prev) === 'number'
? state.keyframes.prev : null;
if (frame !== null) {
if (frame !== null && isAbleToChangeFrame(canvasInstance)) {
changeFrame(frame);
}
}

@ -23,6 +23,7 @@ import {
changeWorkspace as changeWorkspaceAction,
activateObject,
} from 'actions/annotation-actions';
import { Canvas, isAbleToChangeFrame } from 'cvat-canvas';
import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar';
import { CombinedState, FrameSpeed, Workspace } from 'reducers/interfaces';
@ -45,6 +46,7 @@ interface StateToProps {
workspace: Workspace;
keyMap: Record<string, ExtendedKeyMapOptions>;
normalizedKeyMap: Record<string, string>;
canvasInstance: Canvas;
}
interface DispatchToProps {
@ -81,6 +83,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
},
canvas: {
ready: canvasIsReady,
instance: canvasInstance,
},
workspace,
},
@ -118,6 +121,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
workspace,
keyMap,
normalizedKeyMap,
canvasInstance,
};
}
@ -197,6 +201,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
frameDelay,
playing,
canvasIsReady,
canvasInstance,
onSwitchPlay,
onChangeFrame,
} = this.props;
@ -217,10 +222,14 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
setTimeout(() => {
const { playing: stillPlaying } = this.props;
if (stillPlaying) {
onChangeFrame(
frameNumber + 1 + framesSkiped,
stillPlaying, framesSkiped + 1,
);
if (isAbleToChangeFrame(canvasInstance)) {
onChangeFrame(
frameNumber + 1 + framesSkiped,
stillPlaying, framesSkiped + 1,
);
} else {
onSwitchPlay(false);
}
}
}, frameDelay);
} else {
@ -240,9 +249,12 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
undo,
jobInstance,
frameNumber,
canvasInstance,
} = this.props;
undo(jobInstance, frameNumber);
if (isAbleToChangeFrame(canvasInstance)) {
undo(jobInstance, frameNumber);
}
};
private redo = (): void => {
@ -250,9 +262,12 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
redo,
jobInstance,
frameNumber,
canvasInstance,
} = this.props;
redo(jobInstance, frameNumber);
if (isAbleToChangeFrame(canvasInstance)) {
redo(jobInstance, frameNumber);
}
};
private showStatistics = (): void => {
@ -285,7 +300,6 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
jobInstance,
playing,
onSwitchPlay,
onChangeFrame,
} = this.props;
const newFrame = jobInstance.startFrame;
@ -293,7 +307,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
if (playing) {
onSwitchPlay(false);
}
onChangeFrame(newFrame);
this.changeFrame(newFrame);
}
};
@ -304,7 +318,6 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
jobInstance,
playing,
onSwitchPlay,
onChangeFrame,
} = this.props;
const newFrame = Math
@ -313,7 +326,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
if (playing) {
onSwitchPlay(false);
}
onChangeFrame(newFrame);
this.changeFrame(newFrame);
}
};
@ -323,7 +336,6 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
jobInstance,
playing,
onSwitchPlay,
onChangeFrame,
} = this.props;
const newFrame = Math
@ -332,7 +344,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
if (playing) {
onSwitchPlay(false);
}
onChangeFrame(newFrame);
this.changeFrame(newFrame);
}
};
@ -342,7 +354,6 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
jobInstance,
playing,
onSwitchPlay,
onChangeFrame,
} = this.props;
const newFrame = Math
@ -351,7 +362,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
if (playing) {
onSwitchPlay(false);
}
onChangeFrame(newFrame);
this.changeFrame(newFrame);
}
};
@ -362,7 +373,6 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
jobInstance,
playing,
onSwitchPlay,
onChangeFrame,
} = this.props;
const newFrame = Math
@ -371,7 +381,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
if (playing) {
onSwitchPlay(false);
}
onChangeFrame(newFrame);
this.changeFrame(newFrame);
}
};
@ -381,7 +391,6 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
jobInstance,
playing,
onSwitchPlay,
onChangeFrame,
} = this.props;
const newFrame = jobInstance.stopFrame;
@ -389,7 +398,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
if (playing) {
onSwitchPlay(false);
}
onChangeFrame(newFrame);
this.changeFrame(newFrame);
}
};
@ -403,22 +412,16 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
};
private onChangePlayerSliderValue = (value: SliderValue): void => {
const {
playing,
onSwitchPlay,
onChangeFrame,
} = this.props;
const { playing, onSwitchPlay } = this.props;
if (playing) {
onSwitchPlay(false);
}
onChangeFrame(value as number);
this.changeFrame(value as number);
};
private onChangePlayerInputValue = (value: number): void => {
const {
onSwitchPlay,
onChangeFrame,
playing,
frameNumber,
} = this.props;
@ -427,7 +430,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
if (playing) {
onSwitchPlay(false);
}
onChangeFrame(value);
this.changeFrame(value);
}
};
@ -441,6 +444,13 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
copy(url);
};
private changeFrame(frame: number): void {
const { onChangeFrame, canvasInstance } = this.props;
if (isAbleToChangeFrame(canvasInstance)) {
onChangeFrame(frame);
}
}
private beforeUnloadCallback(event: BeforeUnloadEvent): any {
const { jobInstance } = this.props;
if (jobInstance.annotations.hasUnsavedChanges()) {
@ -472,6 +482,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
changeWorkspace,
keyMap,
normalizedKeyMap,
canvasInstance,
} = this.props;
const preventDefault = (event: KeyboardEvent | undefined): void => {
@ -509,7 +520,9 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
},
SAVE_JOB: (event: KeyboardEvent | undefined) => {
preventDefault(event);
this.onSaveAnnotation();
if (!saving) {
this.onSaveAnnotation();
}
},
NEXT_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
@ -537,13 +550,17 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
},
SEARCH_FORWARD: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (frameNumber + 1 <= stopFrame && canvasIsReady) {
if (frameNumber + 1 <= stopFrame && canvasIsReady
&& isAbleToChangeFrame(canvasInstance)
) {
searchAnnotations(jobInstance, frameNumber + 1, stopFrame);
}
},
SEARCH_BACKWARD: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (frameNumber - 1 >= startFrame && canvasIsReady) {
if (frameNumber - 1 >= startFrame && canvasIsReady
&& isAbleToChangeFrame(canvasInstance)
) {
searchAnnotations(jobInstance, frameNumber - 1, startFrame);
}
},

@ -12,6 +12,7 @@ import { createTaskAsync } from 'actions/tasks-actions';
interface StateToProps {
status: string;
error: string;
installedGit: boolean;
}

@ -11,6 +11,7 @@ import {
changeAAMZoomMargin,
switchShowingInterpolatedTracks,
switchShowingObjectsTextAlways,
switchAutomaticBordering,
} from 'actions/settings-actions';
import { CombinedState } from 'reducers/interfaces';
@ -23,6 +24,7 @@ interface StateToProps {
aamZoomMargin: number;
showAllInterpolationTracks: boolean;
showObjectsTextAlways: boolean;
automaticBordering: boolean;
}
interface DispatchToProps {
@ -31,6 +33,7 @@ interface DispatchToProps {
onChangeAAMZoomMargin(margin: number): void;
onSwitchShowingInterpolatedTracks(enabled: boolean): void;
onSwitchShowingObjectsTextAlways(enabled: boolean): void;
onSwitchAutomaticBordering(enabled: boolean): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -41,6 +44,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
aamZoomMargin,
showAllInterpolationTracks,
showObjectsTextAlways,
automaticBordering,
} = workspace;
return {
@ -49,6 +53,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
aamZoomMargin,
showAllInterpolationTracks,
showObjectsTextAlways,
automaticBordering,
};
}
@ -69,6 +74,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSwitchShowingObjectsTextAlways(enabled: boolean): void {
dispatch(switchShowingObjectsTextAlways(enabled));
},
onSwitchAutomaticBordering(enabled: boolean): void {
dispatch(switchAutomaticBordering(enabled));
},
};
}

@ -9,9 +9,15 @@ import {
RectDrawingMethod,
} from '../../cvat-canvas/src/typescript/canvas';
function isAbleToChangeFrame(canvas: Canvas): boolean {
return ![CanvasMode.DRAG, CanvasMode.EDIT, CanvasMode.RESIZE]
.includes(canvas.mode());
}
export {
Canvas,
CanvasMode,
CanvasVersion,
RectDrawingMethod,
isAbleToChangeFrame,
};

@ -35,6 +35,7 @@ const defaultState: AnnotationState = {
},
job: {
labels: [],
requestedId: null,
instance: null,
attributes: {},
fetching: false,
@ -105,6 +106,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
...state,
job: {
...state.job,
instance: null,
requestedId: action.payload.requestedId,
fetching: true,
},
};

@ -57,6 +57,7 @@ export interface TasksState {
};
creates: {
status: string;
error: string;
};
};
}
@ -74,6 +75,7 @@ export enum SupportedPlugins {
AUTO_ANNOTATION = 'AUTO_ANNOTATION',
TF_ANNOTATION = 'TF_ANNOTATION',
TF_SEGMENTATION = 'TF_SEGMENTATION',
DEXTR_SEGMENTATION = 'DEXTR_SEGMENTATION',
ANALYTICS = 'ANALYTICS',
}
@ -316,6 +318,7 @@ export interface AnnotationState {
};
job: {
labels: any[];
requestedId: number | null;
instance: any | null | undefined;
attributes: Record<number, any[]>;
fetching: boolean;
@ -426,6 +429,7 @@ export interface WorkspaceSettingsState {
autoSave: boolean;
autoSaveInterval: number; // in ms
aamZoomMargin: number;
automaticBordering: boolean;
showObjectsTextAlways: boolean;
showAllInterpolationTracks: boolean;
}

@ -4,6 +4,7 @@
import { PluginsActionTypes, PluginActions } from 'actions/plugins-actions';
import { registerGitPlugin } from 'utils/git-utils';
import { registerDEXTRPlugin } from 'utils/dextr-utils';
import {
PluginsState,
} from './interfaces';
@ -16,6 +17,7 @@ const defaultState: PluginsState = {
AUTO_ANNOTATION: false,
TF_ANNOTATION: false,
TF_SEGMENTATION: false,
DEXTR_SEGMENTATION: false,
ANALYTICS: false,
},
};
@ -39,6 +41,10 @@ export default function (
registerGitPlugin();
}
if (!state.list.DEXTR_SEGMENTATION && list.DEXTR_SEGMENTATION) {
registerDEXTRPlugin();
}
return {
...state,
initialized: true,

@ -28,6 +28,7 @@ const defaultState: SettingsState = {
autoSave: false,
autoSaveInterval: 15 * 60 * 1000,
aamZoomMargin: 100,
automaticBordering: false,
showObjectsTextAlways: false,
showAllInterpolationTracks: false,
},
@ -237,6 +238,15 @@ export default (state = defaultState, action: AnyAction): SettingsState => {
},
};
}
case SettingsActionTypes.SWITCH_AUTOMATIC_BORDERING: {
return {
...state,
workspace: {
...state.workspace,
automaticBordering: action.payload.automaticBordering,
},
};
}
case BoundariesActionTypes.RESET_AFTER_ERROR:
case AnnotationActionTypes.GET_JOB_SUCCESS: {
const { job } = action.payload;

@ -314,6 +314,12 @@ const defaultKeyMap = {
sequences: ['`', '~'],
action: 'keydown',
},
SWITCH_AUTOMATIC_BORDERING: {
name: 'Switch automatic bordering',
description: 'Switch automatic bordering for polygons and polylines during drawing/editing',
sequences: ['Control'],
action: 'keydown',
},
} as any as Record<string, ExtendedKeyMapOptions>;

@ -32,6 +32,7 @@ const defaultState: TasksState = {
deletes: {},
creates: {
status: '',
error: '',
},
},
};
@ -238,6 +239,7 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
...state.activities,
creates: {
status: '',
error: '',
},
},
};
@ -276,6 +278,7 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
creates: {
...state.activities.creates,
status: 'FAILED',
error: action.payload.error.toString(),
},
},
};

@ -48,5 +48,4 @@ hr {
height: 100%;
display: grid;
min-width: 1280px;
min-height: 768px;
}

@ -0,0 +1,256 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import getCore from 'cvat-core';
import { Canvas } from 'cvat-canvas';
import { ShapeType, RQStatus } from 'reducers/interfaces';
const core = getCore();
const baseURL = core.config.backendAPI.slice(0, -7);
interface DEXTRPlugin {
name: string;
description: string;
cvat: {
classes: {
Job: {
prototype: {
annotations: {
put: {
enter(self: any, objects: any[]): Promise<void>;
};
};
};
};
};
};
data: {
canceled: boolean;
enabled: boolean;
};
}
interface Point {
x: number;
y: number;
}
const antModalRoot = document.createElement('div');
const antModalMask = document.createElement('div');
antModalMask.classList.add('ant-modal-mask');
const antModalWrap = document.createElement('div');
antModalWrap.classList.add('ant-modal-wrap');
antModalWrap.setAttribute('role', 'dialog');
const antModal = document.createElement('div');
antModal.classList.add('ant-modal');
antModal.style.width = '300px';
antModal.style.top = '40%';
antModal.setAttribute('role', 'document');
const antModalContent = document.createElement('div');
antModalContent.classList.add('ant-modal-content');
const antModalBody = document.createElement('div');
antModalBody.classList.add('ant-modal-body');
antModalBody.style.textAlign = 'center';
const antModalSpan = document.createElement('span');
antModalSpan.innerText = 'Segmentation request is being processed';
antModalSpan.style.display = 'block';
const antModalButton = document.createElement('button');
antModalButton.disabled = true;
antModalButton.classList.add('ant-btn', 'ant-btn-primary');
antModalButton.style.width = '100px';
antModalButton.style.margin = '10px auto';
const antModalButtonSpan = document.createElement('span');
antModalButtonSpan.innerText = 'Cancel';
antModalBody.append(antModalSpan, antModalButton);
antModalButton.append(antModalButtonSpan);
antModalContent.append(antModalBody);
antModal.append(antModalContent);
antModalWrap.append(antModal);
antModalRoot.append(antModalMask, antModalWrap);
function serverRequest(
plugin: DEXTRPlugin,
jid: number,
frame: number,
points: number[],
): Promise<number[]> {
return new Promise((resolve, reject) => {
const reducer = (acc: Point[], _: number, index: number, array: number[]): Point[] => {
if (!(index % 2)) { // 0, 2, 4
acc.push({
x: array[index],
y: array[index + 1],
});
}
return acc;
};
const reducedPoints = points.reduce(reducer, []);
core.server.request(
`${baseURL}/dextr/create/${jid}`, {
method: 'POST',
data: JSON.stringify({
frame,
points: reducedPoints,
}),
headers: {
'Content-Type': 'application/json',
},
},
).then(() => {
const timeoutCallback = (): void => {
core.server.request(
`${baseURL}/dextr/check/${jid}`, {
method: 'GET',
},
).then((response: any) => {
const { status } = response;
if (status === RQStatus.finished) {
resolve(response.result.split(/\s|,/).map((coord: string) => +coord));
} else if (status === RQStatus.failed) {
reject(new Error(response.stderr));
} else if (status === RQStatus.unknown) {
reject(new Error('Unknown DEXTR status has been received'));
} else {
if (status === RQStatus.queued) {
antModalButton.disabled = false;
}
if (!plugin.data.canceled) {
setTimeout(timeoutCallback, 1000);
} else {
core.server.request(
`${baseURL}/dextr/cancel/${jid}`, {
method: 'GET',
},
).then(() => {
resolve(points);
}).catch((error: Error) => {
reject(error);
});
}
}
}).catch((error: Error) => {
reject(error);
});
};
setTimeout(timeoutCallback, 1000);
}).catch((error: Error) => {
reject(error);
});
});
// start checking
}
const plugin: DEXTRPlugin = {
name: 'Deep extreme cut',
description: 'Plugin allows to get a polygon from extreme points using AI',
cvat: {
classes: {
Job: {
prototype: {
annotations: {
put: {
async enter(self: DEXTRPlugin, objects: any[]): Promise<void> {
try {
if (self.data.enabled) {
document.body.append(antModalRoot);
const promises: Record<number, Promise<number[]>> = {};
for (let i = 0; i < objects.length; i++) {
if (objects[i].points.length >= 8) {
promises[i] = serverRequest(
self,
(this as any).id,
objects[i].frame,
objects[i].points,
);
} else {
promises[i] = new Promise((resolve) => {
resolve(objects[i].points);
});
}
}
const transformed = await Promise
.all(Object.values(promises));
for (let i = 0; i < objects.length; i++) {
// eslint-disable-next-line no-param-reassign
objects[i] = new core.classes.ObjectState({
frame: objects[i].frame,
objectType: objects[i].objectType,
label: objects[i].label,
shapeType: ShapeType.POLYGON,
points: transformed[i],
occluded: objects[i].occluded,
zOrder: objects[i].zOrder,
});
}
}
return;
} catch (error) {
throw new core.exceptions.PluginError(error.toString());
} finally {
// eslint-disable-next-line no-param-reassign
self.data.canceled = false;
antModalButton.disabled = true;
if (antModalRoot.parentElement === document.body) {
document.body.removeChild(antModalRoot);
}
}
},
},
},
},
},
},
},
data: {
canceled: false,
enabled: false,
},
};
antModalButton.onclick = () => {
plugin.data.canceled = true;
};
export function activate(canvasInstance: Canvas): void {
if (!plugin.data.enabled) {
// eslint-disable-next-line no-param-reassign
canvasInstance.draw = (drawData: any): void => {
if (drawData.enabled && drawData.shapeType === ShapeType.POLYGON
&& (typeof (drawData.numberOfPoints) === 'undefined' || drawData.numberOfPoints >= 4)
&& (typeof (drawData.initialState) === 'undefined')
) {
const patchedData = { ...drawData };
patchedData.shapeType = ShapeType.POINTS;
patchedData.crosshair = true;
Object.getPrototypeOf(canvasInstance)
.draw.call(canvasInstance, patchedData);
} else {
Object.getPrototypeOf(canvasInstance)
.draw.call(canvasInstance, drawData);
}
};
plugin.data.enabled = true;
}
}
export function deactivate(canvasInstance: Canvas): void {
if (plugin.data.enabled) {
// eslint-disable-next-line no-param-reassign
canvasInstance.draw = Object.getPrototypeOf(canvasInstance).draw;
plugin.data.enabled = false;
}
}
export function registerDEXTRPlugin(): void {
core.plugins.register(plugin);
}

@ -180,10 +180,10 @@ export function syncRepos(tid: number): Promise<void> {
resolve();
} else if (response.status === 'failed') {
const message = `Can not push to remote repository. Message: ${response.stderr}`;
throw new Error(message);
reject(new Error(message));
} else {
const message = `Check returned status "${response.status}".`;
throw new Error(message);
reject(new Error(message));
}
}

@ -35,6 +35,9 @@ class PluginChecker {
case SupportedPlugins.TF_SEGMENTATION: {
return isReachable(`${serverHost}/tensorflow/segmentation/meta/get`, 'OPTIONS');
}
case SupportedPlugins.DEXTR_SEGMENTATION: {
return isReachable(`${serverHost}/dextr/enabled`, 'GET');
}
case SupportedPlugins.ANALYTICS: {
return isReachable(`${serverHost}/analytics/app/kibana`, 'GET');
}

@ -26,7 +26,7 @@ class CvatImagesExtractor(datumaro.Extractor):
frames = self._frame_provider.get_frames(
self._frame_provider.Quality.ORIGINAL,
self._frame_provider.Type.NUMPY_ARRAY)
for item_id, image in enumerate(frames):
for item_id, (image, _) in enumerate(frames):
yield datumaro.DatasetItem(
id=item_id,
image=Image(image),

@ -48,11 +48,19 @@ class DEXTR_HANDLER:
image = PIL.Image.open(image[0])
numpy_image = np.array(image)
points = np.asarray([[int(p["x"]), int(p["y"])] for p in points], dtype=int)
# Padding mustn't be more than the closest distance to an edge of an image
[height, width] = numpy_image.shape[:2]
x_values = points[:, 0]
y_values = points[:, 1]
[min_x, max_x] = [np.min(x_values), np.max(x_values)]
[min_y, max_y] = [np.min(y_values), np.max(y_values)]
padding = min(min_x, min_y, width - max_x, height - max_y, _DEXTR_PADDING)
bounding_box = (
max(min(points[:, 0]) - _DEXTR_PADDING, 0),
max(min(points[:, 1]) - _DEXTR_PADDING, 0),
min(max(points[:, 0]) + _DEXTR_PADDING, numpy_image.shape[1] - 1),
min(max(points[:, 1]) + _DEXTR_PADDING, numpy_image.shape[0] - 1)
max(min(points[:, 0]) - padding, 0),
max(min(points[:, 1]) - padding, 0),
min(max(points[:, 0]) + padding, width - 1),
min(max(points[:, 1]) + padding, height - 1)
)
# Prepare an image
@ -61,7 +69,7 @@ class DEXTR_HANDLER:
interpolation = cv2.INTER_CUBIC).astype(np.float32)
# Make a heatmap
points = points - [min(points[:, 0]), min(points[:, 1])] + [_DEXTR_PADDING, _DEXTR_PADDING]
points = points - [min(points[:, 0]), min(points[:, 1])] + [padding, padding]
points = (points * [_DEXTR_SIZE / numpy_cropped.shape[1], _DEXTR_SIZE / numpy_cropped.shape[0]]).astype(int)
heatmap = np.zeros(shape=resized.shape[:2], dtype=np.float64)
for point in points:

@ -8,5 +8,6 @@ from . import views
urlpatterns = [
path('create/<int:jid>', views.create),
path('cancel/<int:jid>', views.cancel),
path('check/<int:jid>', views.check)
]
path('check/<int:jid>', views.check),
path('enabled', views.enabled)
]

@ -123,3 +123,6 @@ def check(request, jid):
except Exception as ex:
slogger.job[jid].error("can't check a dextr request for the job {}".format(jid), exc_info=True)
return HttpResponseBadRequest(str(ex))
def enabled(request):
return HttpResponse()

@ -7,6 +7,7 @@
- [Stop all containers](#stop-all-containers)
- [Advanced settings](#advanced-settings)
- [Share path](#share-path)
- [Serving over HTTPS](#serving-over-https)
# Quick installation guide
@ -306,3 +307,299 @@ volumes:
You can change the share device path to your actual share. For user convenience
we have defined the environment variable $CVAT_SHARE_URL. This variable
contains a text (url for example) which is shown in the client-share browser.
### Serving over HTTPS
We will add [letsencrypt.org](https://letsencrypt.org/) issued certificate to secure
our server connection.
#### Prerequisites
We assume that
- you have sudo access on your server machine,
- you have an IP address to use for remote access, and
- that the local CVAT installation works on your server.
If this is not the case, please complete the steps in the installation manual first.
#### Roadmap
We will go through the following sequence of steps to get CVAT over HTTPS:
- Move Docker Compose CVAT access port to 80/tcp.
- Configure Nginx to pass one of the [ACME challenges](https://letsencrypt.org/docs/challenge-types/).
- Create the certificate files using [acme.sh](https://github.com/acmesh-official/acme.sh).
- Reconfigure Nginx to serve over HTTPS and map CVAT to Docker Compose port 443.
#### Step-by-step instructions
##### 1. Move the CVAT access port
Let's assume the server will be at `my-cvat-server.org`.
```bash
# on the server
docker-compose down
# add docker-compose.override.yml as per instructions below
docker-compose up -d
```
Add the following into your `docker-compose.override.yml`, replacing `my-cvat-server.org` with your own IP address.
This file lives in the same directory as `docker-compose.yml`.
```yaml
# docker-compose.override.yml
version: "2.3"
services:
cvat_proxy:
environment:
CVAT_HOST: my-cvat-server.org
ports:
- "80:80"
cvat:
environment:
ALLOWED_HOSTS: '*'
```
You should now see an unsecured version of CVAT at `http://my-cvat-server.org`.
##### 2. Configure Nginx for the ACME challenge
Temporarily, enable serving `http://my-cvat-server.org/.well-known/acme-challenge/`
route from `/letsencrypt` directory on the server's filesystem.
You can use the [Nginx quickstart guide](http://nginx.org/en/docs/beginners_guide.html) for reference.
```bash
# cvat_proxy/conf.d/cvat.conf.template
server {
listen 80;
server_name _ default;
return 404;
}
server {
listen 80;
server_name ${CVAT_HOST};
# add this temporarily, to pass an acme challenge
location ^~ /.well-known/acme-challenge/ {
allow all;
root /letsencrypt;
}
location ~* /api/.*|git/.*|tensorflow/.*|auto_annotation/.*|analytics/.*|static/.*|admin|admin/.*|documentation/.*|dextr/.*|reid/.* {
proxy_pass http://cvat:8080;
proxy_pass_header X-CSRFToken;
proxy_set_header Host $http_host;
proxy_pass_header Set-Cookie;
}
location / {
# workaround for match location by arguments
error_page 418 = @annotation_ui;
if ( $query_string ~ "^id=\d+.*" ) { return 418; }
proxy_pass http://cvat_ui;
proxy_pass_header X-CSRFToken;
proxy_set_header Host $http_host;
proxy_pass_header Set-Cookie;
}
# old annotation ui, will be removed in the future.
location @annotation_ui {
proxy_pass http://cvat:8080;
proxy_pass_header X-CSRFToken;
proxy_set_header Host $http_host;
proxy_pass_header Set-Cookie;
}
}
```
Now create the `/letsencrypt` directory and mount it into `cvat_proxy` container.
Edit your `docker-compose.override.yml` to look like the following:
```yaml
# docker-compose.override.yml
version: "2.3"
services:
cvat_proxy:
environment:
CVAT_HOST: my-cvat-server.org
ports:
- "80:80"
volumes:
- ./letsencrypt:/letsencrypt
cvat:
environment:
ALLOWED_HOSTS: '*'
```
Finally, create the directory and restart CVAT.
```bash
# in the same directory where docker-compose.override.yml lives
mkdir -p letsencrypt/.well-known/acme-challenge
docker-compose down
docker-compose up -d
```
Your server should still be visible (and unsecured) at `http://my-cvat-server.org`
but you won't see any behavior changes.
##### 3. Create certificate files using an ACME challenge
At this point your deployment is running.
```bash
admin@tempVM:~/cvat$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0a35cd127968 nginx:stable-alpine "/bin/sh -c 'envsubs…" About a minute ago Up About a minute 0.0.0.0:80->80/tcp, 0.0.0.0:8080->80/tcp cvat_proxy
b85497c44836 cvat_cvat_ui "nginx -g 'daemon of…" About a minute ago Up About a minute 80/tcp cvat_ui
d25a00475849 cvat "/usr/bin/supervisord" About a minute ago Up About a minute 8080/tcp, 8443/tcp cvat
6353a43f55c3 redis:4.0-alpine "docker-entrypoint.s…" About a minute ago Up About a minute 6379/tcp cvat_redis
52009636caa8 postgres:10-alpine "docker-entrypoint.s…" About a minute ago Up About a minute 5432/tcp cvat_db
```
We will attach `cvat_proxy` container to run `acme.sh` scripts.
```bash
admin@tempVM:~/cvat$ docker exec -ti cvat_proxy /bin/sh
# install some missing software inside cvat_proxy
/ # apk add openssl curl
/ # curl https://get.acme.sh | sh
/ # ~/.acme.sh/acme.sh -h
[... many lines ...]
/ # ~/.acme.sh/acme.sh --issue -d my-cvat-server.org -w /letsencrypt
[Fri Apr 3 20:49:05 UTC 2020] Create account key ok.
[Fri Apr 3 20:49:05 UTC 2020] Registering account
[Fri Apr 3 20:49:06 UTC 2020] Registered
[Fri Apr 3 20:49:06 UTC 2020] ACCOUNT_THUMBPRINT='tril8-LdJgM8xg6mnN1pMa7vIMdFizVCE0NImNmyZY4'
[Fri Apr 3 20:49:06 UTC 2020] Creating domain key
[ ... many more lines ...]
[Fri Apr 3 20:49:10 UTC 2020] Your cert is in /root/.acme.sh/my-cvat-server.org/my-cvat-server.org.cer
[Fri Apr 3 20:49:10 UTC 2020] Your cert key is in /root/.acme.sh/my-cvat-server.org/my-cvat-server.org.key
[Fri Apr 3 20:49:10 UTC 2020] The intermediate CA cert is in /root/.acme.sh/my-cvat-server.org/ca.cer
[Fri Apr 3 20:49:10 UTC 2020] And the full chain certs is there: /root/.acme.sh/my-cvat-server.org/fullchain.cer
/ # cp ~/.acme.sh/my-cvat-server.org/my-cvat-server.org.cer /letsencrypt/certificate.cer
/ # cp ~/.acme.sh/my-cvat-server.org/my-cvat-server.org.key /letsencrypt/certificate.key
/ # cp ~/.acme.sh/my-cvat-server.org/ca.cer /letsencrypt/ca.cer
/ # cp ~/.acme.sh/my-cvat-server.org/fullchain.cer /letsencrypt/fullchain.cer
/ # exit
admin@tempVM:~/cvat$ ls letsencrypt/
ca.cer certificate.cer certificate.key fullchain.cer
admin@tempVM:~/cvat$ mkdir cert
admin@tempVM:~/cvat$ mv letsencrypt/* ./cert
```
##### 4. Reconfigure Nginx for HTTPS access
Update Docker Compose configuration to mount the certificate directory.
```yml
# docker-compose.override.yml
version: "2.3"
services:
cvat_proxy:
environment:
CVAT_HOST: my-cvat-server.org
ports:
- "443:443"
volumes:
- ./letsencrypt:/letsencrypt
- ./cert:/cert:ro # this is new
cvat:
environment:
ALLOWED_HOSTS: '*'
```
Also, reconfigure Nginx to use `443/tcp` and point it to the new keys.
```bash
server {
listen 80;
server_name _ default;
return 404;
}
server {
listen 443 ssl;
server_name ${CVAT_HOST};
ssl_certificate /cert/certificate.cer;
ssl_certificate_key /cert/certificate.key;
location ~* /api/.*|git/.*|tensorflow/.*|auto_annotation/.*|analytics/.*|static/.*|admin|admin/.*|documentation/.*|dextr/.*|reid/.* {
proxy_pass http://cvat:8080;
proxy_pass_header X-CSRFToken;
proxy_set_header Host $http_host;
proxy_pass_header Set-Cookie;
}
location / {
# workaround for match location by arguments
error_page 418 = @annotation_ui;
if ( $query_string ~ "^id=\d+.*" ) { return 418; }
proxy_pass http://cvat_ui;
proxy_pass_header X-CSRFToken;
proxy_set_header Host $http_host;
proxy_pass_header Set-Cookie;
}
# old annotation ui, will be removed in the future.
location @annotation_ui {
proxy_pass http://cvat:8080;
proxy_pass_header X-CSRFToken;
proxy_set_header Host $http_host;
proxy_pass_header Set-Cookie;
}
}
```
Finally, restart your service.
```bash
admin@tempVM:~/cvat$ docker-compose down
Stopping cvat_proxy ... done
Stopping cvat_ui ... done
Stopping cvat ... done
Stopping cvat_db ... done
Stopping cvat_redis ... done
Removing cvat_proxy ... done
Removing cvat_ui ... done
Removing cvat ... done
Removing cvat_db ... done
Removing cvat_redis ... done
Removing network cvat_default
admin@tempVM:~/cvat$ docker-compose up -d
Creating network "cvat_default" with the default driver
Creating cvat_db ... done
Creating cvat_redis ... done
Creating cvat ... done
Creating cvat_ui ... done
Creating cvat_proxy ... done
admin@tempVM:~/cvat$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
71464aeac87c nginx:stable-alpine "/bin/sh -c 'envsubs…" About a minute ago Up About a minute 0.0.0.0:443->443/tcp, 0.0.0.0:8080->80/tcp cvat_proxy
8428cfbb766e cvat_cvat_ui "nginx -g 'daemon of…" About a minute ago Up About a minute 80/tcp cvat_ui
b5a2f78689da cvat "/usr/bin/supervisord" About a minute ago Up About a minute 8080/tcp, 8443/tcp cvat
ef4a1f47440f redis:4.0-alpine "docker-entrypoint.s…" About a minute ago Up About a minute 6379/tcp cvat_redis
7803bf828d9f postgres:10-alpine "docker-entrypoint.s…" About a minute ago Up About a minute 5432/tcp cvat_db
```
Now you can go to `https://my-cvat-server.org/` and verify that you are using an encrypted connection.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@ -136,6 +136,8 @@ Go to the [Django administration panel](http://localhost:8080/admin). There you
The ``Done`` button applies the changes and the ``Reset`` button cancels the changes.
![](static/documentation/images/image126.jpg)
In ``Raw`` and ``Constructor`` mode, you can press the ``Copy`` button to copy the list of labels.
**Select files**. Press tab ``My computer`` to choose some files for annotation from your PC.
If you select tab ``Connected file share`` you can choose files for annotation from your network.
If you select `` Remote source`` , you'll see a field where you can enter a list of URLs (one URL per line).
@ -147,6 +149,8 @@ Go to the [Django administration panel](http://localhost:8080/admin). There you
![](static/documentation/images/image128.jpg)
**Z-Order**. Defines the order on drawn polygons. Check the box for enable layered displaying.
**Use zip chunks**. Force to use zip chunks as compressed data. Actual for videos only.
**Image Quality**. Use this option to specify quality of uploaded images.
The option helps to load high resolution datasets faster.
@ -182,8 +186,17 @@ Go to the [Django administration panel](http://localhost:8080/admin). There you
**Stop frame**. Frame on which video in task ends.
**Frame Filter**. Use this option to filter video frames.
For example, enter ``step=25`` to leave every twenty fifth frame in the video. Use this option on video files only.
**Frame Step**. Use this option to filter video frames.
For example, enter ``25`` to leave every twenty fifth frame in the video or every twenty fifth image.
**Chunk size**. Defines a number of frames to be packed in a chunk when send from client to server.
Server defines automatically if empty.
Recommended values:
- 1080p or less: 36
- 2k or less: 8 - 16
- 4k or less: 4 - 8
- More: 1 - 4
**Dataset Repository**. URL link of the repository optionally specifies the path to the repository for storage
(``default: annotation / <dump_file_name> .zip``).
@ -232,7 +245,7 @@ Go to the [Django administration panel](http://localhost:8080/admin). There you
- [Pascal VOC 2012](http://host.robots.ox.ac.uk/pascal/VOC/)
- [MS COCO](http://cocodataset.org/#format-data)
- [YOLO](https://pjreddie.com/darknet/yolo/)
- ``Auto Annotation`` — automatic annotation with OpenVINO toolkit.
- ``Automatic Annotation`` — automatic annotation with OpenVINO toolkit.
Presence depends on how you build CVAT instance.
- ``Open bug tracker`` — opens a link to Issue tracker.
- ``Delete`` — delete task.
@ -269,10 +282,10 @@ Go to the [Django administration panel](http://localhost:8080/admin). There you
1. Follow a link inside ``Jobs`` section to start annotation process.
In some cases, you can have several links. It depends on size of your
task and ``Overlap Size`` and ``Segment Size`` parameters. To improve
UX, only the first several frames will be loaded and you will be able
UX, only the first chunk of several frames will be loaded and you will be able
to annotate first images. Other frames will be loaded in background.
![](static/documentation/images/image007.jpg)
![](static/documentation/images/image007_DETRAC.jpg)
### Models
@ -334,34 +347,39 @@ The search is case insensitive.
## Interface of the annotation tool
The tool consists of:
- ``Workspace`` — where images are shown;
- ``Bottom panel`` (under workspace) — for navigation, filtering annotation and accessing tools' menu;
- ``Side panel`` — contains two lists: objects (on the frame) and labels (of objects on the frame);
- ``Bottom side panel`` — contains the main annotation functions (create, merge, group objects).
Here you can choose a type of shape, a label you want to annotate and a mode (annotation or interpolation)
- ``Header`` - pinned header used to navigate CVAT sections and account settings;
- ``Top panel`` — contains navigation buttons, main functions and menu access;
- ``Workspace`` — space where images are shown;
- ``Controls sidebar`` — contains tools for navigating the image, zoom,
creating shapes and editing tracks (merge, split, group)
- ``Objects sidebar`` — contains label filter, two lists:
objects (on the frame) and labels (of objects on the frame) and appearance settings.
![](static/documentation/images/image034.jpg)
There is also:
- ``Settings`` (F2) — the button inside ``Open Menu`` in the bottom panel. Contains different parameters
which can be adjusted according to the user's needs.
- ``Context menu`` — available on right mouse button.
![](static/documentation/images/image034_DETRAC.jpg)
### Basic navigation
1. Use arrows below to move on next/previous frame.
1. Use arrows below to move to the next/previous frame.
Use the scroll bar slider to scroll through frames.
Almost every button is covered by a shortcut.
To get a hint about a shortcut, just put your mouse pointer over an UI element.
Almost every button has a shortcut.
To get a hint about a shortcut, just move your mouse pointer over an UI element.
![](static/documentation/images/image008.jpg)
1. An image can be scaled in/out using mouse's wheel. The image will be zoomed relatively your current cursor position.
Thus, if you point on an object, it will be under your mouse during zooming process.
1. To navigate the image, use the button on the controls sidebar.
Another way an image can be moved/shifted is by holding the left mouse button inside
an area without annotated objects.
If the ``Mouse Wheel`` is pressed, then all annotated objects are ignored. Otherwise the
a highlighted bounding box will be moved instead of the image itself.
![](static/documentation/images/image136.jpg)
1. You can use the button on the sidebar controls to zoom on a region of interest.
Use the button ``Fit the image`` to fit the image in the workspace.
You can also use the mouse wheel to scale the image
(the image will be zoomed relatively to your current cursor position).
1. An image can be moved/shifted by holding left mouse button inside some area without annotated objects.
If ``Mouse Wheel`` is pressed, then all annotated objects are ignored.
Otherwise, a highlighted bounding box will be moved instead of the image itself.
![](static/documentation/images/image137.jpg)
### Types of shapes (basics)
There are four shapes which you can annotate your images with:

@ -1,21 +1,47 @@
# Copyright (C) 2019 Intel Corporation
# Copyright (C) 2020 Intel Corporation
#
# SPDX-License-Identifier: MIT
import math
from io import BytesIO
from enum import Enum
import itertools
from io import BytesIO
import numpy as np
from PIL import Image
from cvat.apps.engine.media_extractors import VideoReader, ZipReader
from cvat.apps.engine.models import DataChoice
from cvat.apps.engine.mime_types import mimetypes
from cvat.apps.engine.models import DataChoice
class RandomAccessIterator:
def __init__(self, iterable):
self.iterable = iterable
self.iterator = None
self.pos = -1
def __iter__(self):
return self
def __next__(self):
return self[self.pos + 1]
class FrameProvider():
def __getitem__(self, idx):
assert 0 <= idx
if self.iterator is None or idx <= self.pos:
self.reset()
v = None
while self.pos < idx:
# NOTE: don't keep the last item in self, it can be expensive
v = next(self.iterator)
self.pos += 1
return v
def reset(self):
self.iterator = iter(self.iterable)
self.pos = -1
class FrameProvider:
class Quality(Enum):
COMPRESSED = 0
ORIGINAL = 100
@ -25,26 +51,34 @@ class FrameProvider():
PIL = 1
NUMPY_ARRAY = 2
def __init__(self, db_data):
self._db_data = db_data
if db_data.compressed_chunk_type == DataChoice.IMAGESET:
self._compressed_chunk_reader_class = ZipReader
elif db_data.compressed_chunk_type == DataChoice.VIDEO:
self._compressed_chunk_reader_class = VideoReader
else:
raise Exception('Unsupported chunk type')
class ChunkLoader:
def __init__(self, reader_class, path_getter):
self.chunk_id = None
self.chunk_reader = None
self.reader_class = reader_class
self.get_chunk_path = path_getter
if db_data.original_chunk_type == DataChoice.IMAGESET:
self._original_chunk_reader_class = ZipReader
elif db_data.original_chunk_type == DataChoice.VIDEO:
self._original_chunk_reader_class = VideoReader
else:
raise Exception('Unsupported chunk type')
def load(self, chunk_id):
if self.chunk_id != chunk_id:
self.chunk_id = chunk_id
self.chunk_reader = RandomAccessIterator(
self.reader_class([self.get_chunk_path(chunk_id)]))
return self.chunk_reader
self._extracted_compressed_chunk = None
self._compressed_chunk_reader = None
self._extracted_original_chunk = None
self._original_chunk_reader = None
def __init__(self, db_data):
self._db_data = db_data
self._loaders = {}
reader_class = {
DataChoice.IMAGESET: ZipReader,
DataChoice.VIDEO: VideoReader,
}
self._loaders[self.Quality.COMPRESSED] = self.ChunkLoader(
reader_class[db_data.compressed_chunk_type],
db_data.get_compressed_chunk_path)
self._loaders[self.Quality.ORIGINAL] = self.ChunkLoader(
reader_class[db_data.original_chunk_type],
db_data.get_original_chunk_path)
def __len__(self):
return self._db_data.size
@ -74,77 +108,41 @@ class FrameProvider():
buf.seek(0)
return buf
def _get_frame(self, frame_number, chunk_path_getter, extracted_chunk, chunk_reader, reader_class):
_, chunk_number, frame_offset = self._validate_frame_number(frame_number)
chunk_path = chunk_path_getter(chunk_number)
if chunk_number != extracted_chunk:
extracted_chunk = chunk_number
chunk_reader = reader_class([chunk_path])
frame, frame_name, _ = next(itertools.islice(chunk_reader, frame_offset, None))
if reader_class is VideoReader:
return (self._av_frame_to_png_bytes(frame), 'image/png')
return (frame, mimetypes.guess_type(frame_name))
def _get_frames(self, chunk_path_getter, reader_class, out_type):
for chunk_idx in range(math.ceil(self._db_data.size / self._db_data.chunk_size)):
chunk_path = chunk_path_getter(chunk_idx)
chunk_reader = reader_class([chunk_path])
for frame, _, _ in chunk_reader:
if out_type == self.Type.BUFFER:
yield self._av_frame_to_png_bytes(frame) if reader_class is VideoReader else frame
elif out_type == self.Type.PIL:
yield frame.to_image() if reader_class is VideoReader else Image.open(frame)
elif out_type == self.Type.NUMPY_ARRAY:
if reader_class is VideoReader:
image = np.array(frame.to_image())
else:
image = np.array(Image.open(frame))
if len(image.shape) == 3 and image.shape[2] in {3, 4}:
image[:, :, :3] = image[:, :, 2::-1] # RGB to BGR
yield image
else:
raise Exception('unsupported output type')
def _convert_frame(self, frame, reader_class, out_type):
if out_type == self.Type.BUFFER:
return self._av_frame_to_png_bytes(frame) if reader_class is VideoReader else frame
elif out_type == self.Type.PIL:
return frame.to_image() if reader_class is VideoReader else Image.open(frame)
elif out_type == self.Type.NUMPY_ARRAY:
if reader_class is VideoReader:
image = np.array(frame.to_image())
else:
image = np.array(Image.open(frame))
if len(image.shape) == 3 and image.shape[2] in {3, 4}:
image[:, :, :3] = image[:, :, 2::-1] # RGB to BGR
return image
else:
raise Exception('unsupported output type')
def get_preview(self):
return self._db_data.get_preview_path()
def get_chunk(self, chunk_number, quality=Quality.ORIGINAL):
chunk_number = self._validate_chunk_number(chunk_number)
if quality == self.Quality.ORIGINAL:
return self._db_data.get_original_chunk_path(chunk_number)
elif quality == self.Quality.COMPRESSED:
return self._db_data.get_compressed_chunk_path(chunk_number)
def get_frame(self, frame_number, quality=Quality.ORIGINAL):
if quality == self.Quality.ORIGINAL:
return self._get_frame(
frame_number=frame_number,
chunk_path_getter=self._db_data.get_original_chunk_path,
extracted_chunk=self._extracted_original_chunk,
chunk_reader=self._original_chunk_reader,
reader_class=self._original_chunk_reader_class,
)
elif quality == self.Quality.COMPRESSED:
return self._get_frame(
frame_number=frame_number,
chunk_path_getter=self._db_data.get_compressed_chunk_path,
extracted_chunk=self._extracted_compressed_chunk,
chunk_reader=self._compressed_chunk_reader,
reader_class=self._compressed_chunk_reader_class,
)
return self._loaders[quality].get_chunk_path(chunk_number)
def get_frame(self, frame_number, quality=Quality.ORIGINAL,
out_type=Type.BUFFER):
_, chunk_number, frame_offset = self._validate_frame_number(frame_number)
loader = self._loaders[quality]
chunk_reader = loader.load(chunk_number)
frame, frame_name, _ = chunk_reader[frame_offset]
frame = self._convert_frame(frame, loader.reader_class, out_type)
if loader.reader_class is VideoReader:
return (frame, 'image/png')
return (frame, mimetypes.guess_type(frame_name))
def get_frames(self, quality=Quality.ORIGINAL, out_type=Type.BUFFER):
if quality == self.Quality.ORIGINAL:
return self._get_frames(
chunk_path_getter=self._db_data.get_original_chunk_path,
reader_class=self._original_chunk_reader_class,
out_type=out_type,
)
elif quality == self.Quality.COMPRESSED:
return self._get_frames(
chunk_path_getter=self._db_data.get_compressed_chunk_path,
reader_class=self._compressed_chunk_reader_class,
out_type=out_type,
)
for idx in range(self._db_data.size):
yield self.get_frame(idx, quality=quality, out_type=out_type)

@ -207,6 +207,7 @@ class DataSerializer(serializers.ModelSerializer):
os.makedirs(db_data.get_compressed_cache_dirname())
os.makedirs(db_data.get_original_cache_dirname())
os.makedirs(db_data.get_upload_dirname())
for f in client_files:
client_file = models.ClientFile(data=db_data, **f)

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Intel Corporation
* Copyright (C) 2018-2020 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
@ -145,7 +145,7 @@ class ShapeMergerModel extends Listener {
let nextFrame = frame + 1;
let stopFrame = window.cvat.player.frames.stop;
let type = shapeDict[frame].shape.type;
if (type === 'annotation_box' && !(nextFrame in shapeDict) && nextFrame <= stopFrame) {
if (type.startsWith('annotation_') && !(nextFrame in shapeDict) && nextFrame <= stopFrame) {
let copy = Object.assign({}, object.shapes[object.shapes.length - 1]);
copy.outside = true;
copy.frame += 1;

@ -1,5 +1,5 @@
<!--
Copyright (C) 2018-2019 Intel Corporation
Copyright (C) 2018-2020 Intel Corporation
SPDX-License-Identifier: MIT
-->
@ -451,7 +451,7 @@
</select>
<select id="shapeTypeSelector" class="regular h2">
<option value="box" class="regular" selected> Box </option>
<option value="box_by_4_points" class="regular" selected> Box by 4 points </option>
<option value="box_by_4_points" class="regular"> Box by 4 points </option>
<option value="polygon" class="regular"> Polygon </option>
<option value="polyline" class="regular"> Polyline </option>
<option value="points" class="regular"> Points </option>

@ -1,4 +1,4 @@
# Copyright (C) 2018 Intel Corporation
# Copyright (C) 2018-2020 Intel Corporation
#
# SPDX-License-Identifier: MIT
@ -52,20 +52,20 @@ def _read_old_diffs(diff_dir, summary):
class Git:
def __init__(self, db_git, tid, user):
def __init__(self, db_git, db_task, user):
self._db_git = db_git
self._url = db_git.url
self._path = db_git.path
self._tid = tid
self._tid = db_task.id
self._user = {
"name": user.username,
"email": user.email or "dummy@cvat.com"
}
self._cwd = os.path.join(os.getcwd(), "data", str(tid), "repos")
self._diffs_dir = os.path.join(os.getcwd(), "data", str(tid), "repos_diffs_v2")
self._task_mode = Task.objects.get(pk = tid).mode
self._task_name = re.sub(r'[\\/*?:"<>|\s]', '_', Task.objects.get(pk = tid).name)[:100]
self._branch_name = 'cvat_{}_{}'.format(tid, self._task_name)
self._cwd = os.path.join(db_task.get_task_artifacts_dirname(), "repos")
self._diffs_dir = os.path.join(db_task.get_task_artifacts_dirname(), "repos_diffs_v2")
self._task_mode = db_task.mode
self._task_name = re.sub(r'[\\/*?:"<>|\s]', '_', db_task.name)[:100]
self._branch_name = 'cvat_{}_{}'.format(db_task.id, self._task_name)
self._annotation_file = os.path.join(self._cwd, self._path)
self._sync_date = db_git.sync_date
self._lfs = db_git.lfs
@ -377,7 +377,7 @@ def initial_create(tid, git_path, lfs, user):
db_git.lfs = lfs
try:
_git = Git(db_git, tid, db_task.owner)
_git = Git(db_git, db_task, db_task.owner)
_git.init_repos()
db_git.save()
except git.exc.GitCommandError as ex:
@ -393,7 +393,7 @@ def push(tid, user, scheme, host):
db_task = Task.objects.get(pk = tid)
db_git = GitData.objects.select_for_update().get(pk = db_task)
try:
_git = Git(db_git, tid, user)
_git = Git(db_git, db_task, user)
_git.init_repos()
_git.push(user, scheme, host, db_task, db_task.updated_date)
@ -427,7 +427,7 @@ def get(tid, user):
response['status']['value'] = str(db_git.status)
else:
try:
_git = Git(db_git, tid, user)
_git = Git(db_git, db_task, user)
_git.init_repos(True)
db_git.status = _git.remote_status(db_task.updated_date)
response['status']['value'] = str(db_git.status)
@ -459,8 +459,8 @@ def _onsave(jid, user, data, action):
db_task = Job.objects.select_related('segment__task').get(pk = jid).segment.task
try:
db_git = GitData.objects.select_for_update().get(pk = db_task.id)
diff_dir = os.path.join(os.getcwd(), "data", str(db_task.id), "repos_diffs")
diff_dir_v2 = os.path.join(os.getcwd(), "data", str(db_task.id), "repos_diffs_v2")
diff_dir = os.path.join(db_task.get_task_artifacts_dirname(), "repos_diffs")
diff_dir_v2 = os.path.join(db_task.get_task_artifacts_dirname(), "repos_diffs_v2")
summary = {
"update": 0,

@ -150,7 +150,7 @@ class ReID:
job = rq.get_current_job()
box_tracks = {}
next_image = cv2.imdecode(numpy.fromstring(next(self.__frame_iter).read(), numpy.uint8), cv2.IMREAD_COLOR)
next_image = cv2.imdecode(numpy.fromstring((next(self.__frame_iter)[0]).read(), numpy.uint8), cv2.IMREAD_COLOR)
for idx, (cur_frame, next_frame) in enumerate(list(zip(frames[:-1], frames[1:]))):
job.refresh()
if "cancel" in job.meta:
@ -172,7 +172,7 @@ class ReID:
continue
cur_image = next_image
next_image = cv2.imdecode(numpy.fromstring(next(self.__frame_iter).read(), numpy.uint8), cv2.IMREAD_COLOR)
next_image = cv2.imdecode(numpy.fromstring((next(self.__frame_iter)[0]).read(), numpy.uint8), cv2.IMREAD_COLOR)
difference_matrix = self.__compute_difference_matrix(cur_boxes, next_boxes, cur_image, next_image)
cur_idxs, next_idxs = linear_sum_assignment(difference_matrix)
for idx, cur_idx in enumerate(cur_idxs):

@ -1,5 +1,5 @@
# Copyright (C) 2019 Intel Corporation
# Copyright (C) 2020 Intel Corporation
#
# SPDX-License-Identifier: MIT
@ -364,9 +364,9 @@ class _KeypointsConverter(_InstancesConverter):
solitary_points = []
for g_id, group in groupby(annotations, lambda a: a.group):
if g_id and not cls.find_instance_anns(group):
if not g_id or g_id and not cls.find_instance_anns(group):
group = [a for a in group if a.type == AnnotationType.points]
solitary_points.extend(group)
solitary_points.extend(group)
return solitary_points
@ -514,7 +514,7 @@ class _Converter:
filename += CocoPath.IMAGE_EXT
path = osp.join(self._images_dir, filename)
save_image(path, image)
return filename
return path
def convert(self):
self._make_dirs()
@ -536,7 +536,7 @@ class _Converter:
for item in subset:
filename = ''
if item.has_image:
filename = item.image.filename
filename = item.image.path
if self._save_images:
if item.has_image:
filename = self._save_image(item)

@ -1,5 +1,5 @@
# Copyright (C) 2019 Intel Corporation
# Copyright (C) 2020 Intel Corporation
#
# SPDX-License-Identifier: MIT
@ -110,8 +110,8 @@ class _Converter:
self._images_dir = images_dir
def get_label(self, label_id):
return self._extractor.categories()[AnnotationType.label] \
.items[label_id].name
return self._strip_label(self._extractor. \
categories()[AnnotationType.label].items[label_id].name)
def save_subsets(self):
subsets = self._extractor.subsets()
@ -426,7 +426,7 @@ class _Converter:
label_map = OrderedDict()
label_map['background'] = [None, [], []]
for item in labels.items:
label_map[item.name] = [None, [], []]
label_map[self._strip_label(item.name)] = [None, [], []]
elif label_map_source in [LabelmapType.guess.name, None]:
# generate colormap for union of VOC and input dataset labels
@ -489,7 +489,7 @@ class _Converter:
def _make_label_id_map(self):
source_labels = {
id: label.name for id, label in
id: self._strip_label(label.name) for id, label in
enumerate(self._extractor.categories().get(
AnnotationType.label, LabelCategories()).items)
}

@ -560,6 +560,9 @@ class CocoConverterTest(TestCase):
annotations=[
# Solitary keypoints
Points([1, 2, 0, 2, 4, 1], label=5, id=3),
# Some other solitary annotations (bug #1387)
Polygon([0, 0, 4, 0, 4, 4], label=3, id=4),
]),
DatasetItem(id=3, subset='val',

@ -649,7 +649,7 @@ class VocConverterTest(TestCase):
def categories(self):
label_cat = LabelCategories()
label_cat.add('label_1')
label_cat.add('Label_1') # should become lowercase
label_cat.add('label_2')
return {
AnnotationType.label: label_cat,

Loading…
Cancel
Save