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 - Dialog window with some helpful information about using filters
- Ability to display a bitmap in the new UI - Ability to display a bitmap in the new UI
- Button to reset colors settings (brightness, saturation, contrast) 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 ### Changed
- Increase preview size of a task till 256, 256 on the server - 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 ### Deprecated
- -
@ -23,8 +32,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- New shape is added when press ``esc`` when drawing instead of cancellation - New shape is added when press ``esc`` when drawing instead of cancellation
- Fixed dextr segmentation. - Dextr segmentation doesn't work.
- Fixed `FileNotFoundError` during dump after moving format files - `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 ### 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), 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 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE. OR OTHER DEALINGS IN THE SOFTWARE.
   
SPDX-License-Identifier: MIT

@ -153,6 +153,7 @@ Standard JS events are used.
- canvas.fit - canvas.fit
- canvas.dragshape => {id: number} - canvas.dragshape => {id: number}
- canvas.resizeshape => {id: number} - canvas.resizeshape => {id: number}
- canvas.contextmenu => { mouseEvent: MouseEvent, objectState: ObjectState, pointID: number }
``` ```
### WEB ### WEB
@ -179,23 +180,25 @@ Standard JS events are used.
## API Reaction ## API Reaction
| | IDLE | GROUPING | SPLITTING | DRAWING | MERGING | EDITING | DRAG | ZOOM | | | IDLE | GROUP | SPLIT | DRAW | MERGE | EDIT | DRAG | RESIZE | ZOOM_CANVAS | DRAG_CANVAS |
|--------------|------|----------|-----------|---------|---------|---------|------|------| |--------------|------|-------|-------|------|-------|------|------|--------|-------------|-------------|
| html() | + | + | + | + | + | + | + | + | | html() | + | + | + | + | + | + | + | + | + | + |
| setup() | + | + | + | + | + | - | + | + | | setup() | + | + | + | + | + | +/- | +/- | +/- | + | + |
| activate() | + | - | - | - | - | - | - | - | | activate() | + | - | - | - | - | - | - | - | - | - |
| rotate() | + | + | + | + | + | + | + | + | | rotate() | + | + | + | + | + | + | + | + | + | + |
| focus() | + | + | + | + | + | + | + | + | | focus() | + | + | + | + | + | + | + | + | + | + |
| fit() | + | + | + | + | + | + | + | + | | fit() | + | + | + | + | + | + | + | + | + | + |
| grid() | + | + | + | + | + | + | + | + | | grid() | + | + | + | + | + | + | + | + | + | + |
| draw() | + | - | - | - | - | - | - | - | | draw() | + | - | - | - | - | - | - | - | - | - |
| split() | + | - | + | - | - | - | - | - | | split() | + | - | + | - | - | - | - | - | - | - |
| group() | + | + | - | - | - | - | - | - | | group() | + | + | - | - | - | - | - | - | - | - |
| merge() | + | - | - | - | + | - | - | - | | merge() | + | - | - | - | + | - | - | - | - | - |
| fitCanvas() | + | + | + | + | + | + | + | + | | fitCanvas() | + | + | + | + | + | + | + | + | + | + |
| dragCanvas() | + | - | - | - | - | - | + | - | | dragCanvas() | + | - | - | - | - | - | + | - | - | + |
| zoomCanvas() | + | - | - | - | - | - | - | + | | zoomCanvas() | + | - | - | - | - | - | - | + | + | - |
| cancel() | - | + | + | + | + | + | + | + | | cancel() | - | + | + | + | + | + | + | + | + | + |
| configure() | + | - | - | - | - | - | - | - | | configure() | + | + | + | + | + | + | + | + | + | + |
| bitmap() | + | + | + | + | + | + | + | + | | bitmap() | + | + | + | + | + | + | + | + | + | + |
| setZLayer() | + | + | + | + | + | + | + | + | | 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; 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 { .svg_select_boundingRect {
opacity: 0; opacity: 0;
pointer-events: none; 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 { export interface Configuration {
autoborders?: boolean;
displayAllText?: boolean; displayAllText?: boolean;
undefinedAttrValue?: string; undefinedAttrValue?: string;
} }
@ -206,6 +207,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}, },
configuration: { configuration: {
displayAllText: false, displayAllText: false,
autoborders: false,
undefinedAttrValue: '', undefinedAttrValue: '',
}, },
imageBitmap: false, imageBitmap: false,
@ -327,6 +329,12 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
} }
public setup(frameData: any, objectStates: any[]): void { 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) { if (frameData.number === this.data.imageID) {
this.data.objects = objectStates; this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS_UPDATED); 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 { public activate(clientID: number | null, attributeID: number | null): void {
if (this.data.activeElement.clientID === clientID) {
return;
}
if (this.data.mode !== Mode.IDLE && clientID !== null) { if (this.data.mode !== Mode.IDLE && clientID !== null) {
// Exception or just return? // Exception or just return?
throw Error(`Canvas is busy. Action: ${this.data.mode}`); 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 { 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') { if (typeof (configuration.displayAllText) !== 'undefined') {
this.data.configuration.displayAllText = configuration.displayAllText; this.data.configuration.displayAllText = configuration.displayAllText;
} }
if (typeof (configuration.autoborders) !== 'undefined') {
this.data.configuration.autoborders = configuration.autoborders;
}
if (typeof (configuration.undefinedAttrValue) !== 'undefined') { if (typeof (configuration.undefinedAttrValue) !== 'undefined') {
this.data.configuration.undefinedAttrValue = configuration.undefinedAttrValue; this.data.configuration.undefinedAttrValue = configuration.undefinedAttrValue;
} }

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

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

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

@ -25,6 +25,21 @@ export interface BBox {
y: number; 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 // Translate point array from the canvas coordinate system
// to the coordinate system of a client // to the coordinate system of a client
export function translateFromSVG(svg: SVGSVGElement, points: number[]): number[] { export function translateFromSVG(svg: SVGSVGElement, points: number[]): number[] {

@ -16,7 +16,9 @@ SVG.Element.prototype.draw = function constructor(...args: any): any {
if (!handler) { if (!handler) {
originalDraw.call(this, ...args); originalDraw.call(this, ...args);
handler = this.remember('_paintHandler'); 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(); handler.set = new SVG.Set();
} }
} else { } else {

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

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

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

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

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

@ -28,6 +28,7 @@ export enum SettingsActionTypes {
SWITCH_AUTO_SAVE = 'SWITCH_AUTO_SAVE', SWITCH_AUTO_SAVE = 'SWITCH_AUTO_SAVE',
CHANGE_AUTO_SAVE_INTERVAL = 'CHANGE_AUTO_SAVE_INTERVAL', CHANGE_AUTO_SAVE_INTERVAL = 'CHANGE_AUTO_SAVE_INTERVAL',
CHANGE_AAM_ZOOM_MARGIN = 'CHANGE_AAM_ZOOM_MARGIN', CHANGE_AAM_ZOOM_MARGIN = 'CHANGE_AAM_ZOOM_MARGIN',
SWITCH_AUTOMATIC_BORDERING = 'SWITCH_AUTOMATIC_BORDERING',
SWITCH_SHOWNIG_INTERPOLATED_TRACKS = 'SWITCH_SHOWNIG_INTERPOLATED_TRACKS', SWITCH_SHOWNIG_INTERPOLATED_TRACKS = 'SWITCH_SHOWNIG_INTERPOLATED_TRACKS',
SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS = 'SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS', 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(() => { useEffect(() => {
saveLogs(); 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) { if (job === null) {

@ -62,7 +62,9 @@ interface Props {
aamZoomMargin: number; aamZoomMargin: number;
showObjectsTextAlways: boolean; showObjectsTextAlways: boolean;
workspace: Workspace; workspace: Workspace;
automaticBordering: boolean;
keyMap: Record<string, ExtendedKeyMapOptions>; keyMap: Record<string, ExtendedKeyMapOptions>;
switchableAutomaticBordering: boolean;
onSetupCanvas: () => void; onSetupCanvas: () => void;
onDragCanvas: (enabled: boolean) => void; onDragCanvas: (enabled: boolean) => void;
onZoomCanvas: (enabled: boolean) => void; onZoomCanvas: (enabled: boolean) => void;
@ -89,11 +91,13 @@ interface Props {
onChangeGridOpacity(opacity: number): void; onChangeGridOpacity(opacity: number): void;
onChangeGridColor(color: GridColor): void; onChangeGridColor(color: GridColor): void;
onSwitchGrid(enabled: boolean): void; onSwitchGrid(enabled: boolean): void;
onSwitchAutomaticBordering(enabled: boolean): void;
} }
export default class CanvasWrapperComponent extends React.PureComponent<Props> { export default class CanvasWrapperComponent extends React.PureComponent<Props> {
public componentDidMount(): void { public componentDidMount(): void {
const { const {
automaticBordering,
showObjectsTextAlways, showObjectsTextAlways,
canvasInstance, canvasInstance,
curZLayer, curZLayer,
@ -106,6 +110,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
wrapper.appendChild(canvasInstance.html()); wrapper.appendChild(canvasInstance.html());
canvasInstance.configure({ canvasInstance.configure({
autoborders: automaticBordering,
undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE, undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE,
displayAllText: showObjectsTextAlways, displayAllText: showObjectsTextAlways,
}); });
@ -139,12 +144,16 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
workspace, workspace,
frameFetching, frameFetching,
showObjectsTextAlways, showObjectsTextAlways,
automaticBordering,
} = this.props; } = this.props;
if (prevProps.showObjectsTextAlways !== showObjectsTextAlways) { if (prevProps.showObjectsTextAlways !== showObjectsTextAlways
|| prevProps.automaticBordering !== automaticBordering
) {
canvasInstance.configure({ canvasInstance.configure({
undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE, undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE,
displayAllText: showObjectsTextAlways, displayAllText: showObjectsTextAlways,
autoborders: automaticBordering,
}); });
} }
@ -160,9 +169,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
if (prevProps.activatedStateID !== null if (prevProps.activatedStateID !== null
&& prevProps.activatedStateID !== activatedStateID) { && prevProps.activatedStateID !== activatedStateID) {
canvasInstance.activate(null); canvasInstance.activate(null);
}
if (activatedStateID) {
const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`); const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`);
if (el) { if (el) {
(el as any).instance.fill({ opacity: opacity / 100 }); (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.groupped', this.onCanvasObjectsGroupped);
canvasInstance.html().removeEventListener('canvas.splitted', this.onCanvasTrackSplitted); 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); 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.groupped', this.onCanvasObjectsGroupped);
canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted); 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 { public render(): JSX.Element {
@ -699,16 +705,19 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
brightnessLevel, brightnessLevel,
contrastLevel, contrastLevel,
saturationLevel, saturationLevel,
keyMap,
grid, grid,
gridColor, gridColor,
gridOpacity, gridOpacity,
switchableAutomaticBordering,
automaticBordering,
onChangeBrightnessLevel, onChangeBrightnessLevel,
onChangeSaturationLevel, onChangeSaturationLevel,
onChangeContrastLevel, onChangeContrastLevel,
onChangeGridColor, onChangeGridColor,
onChangeGridOpacity, onChangeGridOpacity,
onSwitchGrid, onSwitchGrid,
keyMap, onSwitchAutomaticBordering,
} = this.props; } = this.props;
const preventDefault = (event: KeyboardEvent | undefined): void => { 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, INCREASE_GRID_OPACITY: keyMap.INCREASE_GRID_OPACITY,
DECREASE_GRID_OPACITY: keyMap.DECREASE_GRID_OPACITY, DECREASE_GRID_OPACITY: keyMap.DECREASE_GRID_OPACITY,
CHANGE_GRID_COLOR: keyMap.CHANGE_GRID_COLOR, CHANGE_GRID_COLOR: keyMap.CHANGE_GRID_COLOR,
SWITCH_AUTOMATIC_BORDERING: keyMap.SWITCH_AUTOMATIC_BORDERING,
}; };
const step = 10; const step = 10;
const handlers = { const handlers = {
INCREASE_BRIGHTNESS: (event: KeyboardEvent | undefined) => { 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]; const color = colors[indexOf >= colors.length ? 0 : indexOf];
onChangeGridColor(color); onChangeGridColor(color);
}, },
SWITCH_AUTOMATIC_BORDERING: (event: KeyboardEvent | undefined) => {
if (switchableAutomaticBordering) {
preventDefault(event);
onSwitchAutomaticBordering(!automaticBordering);
}
},
}; };
return ( 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 // SPDX-License-Identifier: MIT
import React from 'react'; import React from 'react';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import Select from 'antd/lib/select'; import Select from 'antd/lib/select';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
@ -15,6 +14,7 @@ import Text from 'antd/lib/typography/Text';
import { RectDrawingMethod } from 'cvat-canvas'; import { RectDrawingMethod } from 'cvat-canvas';
import { ShapeType } from 'reducers/interfaces'; import { ShapeType } from 'reducers/interfaces';
import { clamp } from 'utils/math'; import { clamp } from 'utils/math';
import DEXTRPlugin from './dextr-plugin';
interface Props { interface Props {
shapeType: ShapeType; shapeType: ShapeType;
@ -47,6 +47,9 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
onChangeRectDrawingMethod, onChangeRectDrawingMethod,
} = props; } = props;
const trackDisabled = shapeType === ShapeType.POLYGON || shapeType === ShapeType.POLYLINE
|| (shapeType === ShapeType.POINTS && numberOfPoints !== 1);
return ( return (
<div className='cvat-draw-shape-popover-content'> <div className='cvat-draw-shape-popover-content'>
<Row type='flex' justify='start'> <Row type='flex' justify='start'>
@ -78,6 +81,9 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
</Select> </Select>
</Col> </Col>
</Row> </Row>
{
shapeType === ShapeType.POLYGON && <DEXTRPlugin />
}
{ {
shapeType === ShapeType.RECTANGLE ? ( shapeType === ShapeType.RECTANGLE ? (
<> <>
@ -148,7 +154,7 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
<Tooltip title={`Press ${repeatShapeShortcut} to draw again`}> <Tooltip title={`Press ${repeatShapeShortcut} to draw again`}>
<Button <Button
onClick={onDrawTrack} onClick={onDrawTrack}
disabled={shapeType !== ShapeType.RECTANGLE} disabled={trackDisabled}
> >
Track Track
</Button> </Button>

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

@ -3,30 +3,65 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import './styles.scss'; import './styles.scss';
import React from 'react'; import React, { useEffect } from 'react';
import { Row, Col } from 'antd/lib/grid';
import { import Modal from 'antd/lib/modal';
Row,
Col,
} from 'antd';
import Text from 'antd/lib/typography/Text'; 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'; import CreateTaskContent, { CreateTaskData } from './create-task-content';
interface Props { interface Props {
onCreate: (data: CreateTaskData) => void; onCreate: (data: CreateTaskData) => void;
status: string; status: string;
error: string;
installedGit: boolean; installedGit: boolean;
} }
export default function CreateTaskPage(props: Props): JSX.Element { export default function CreateTaskPage(props: Props): JSX.Element {
const { const {
error,
status, status,
onCreate, onCreate,
installedGit, installedGit,
} = props; } = 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 ( return (
<Row type='flex' justify='center' align='top' className='cvat-create-task-form-wrapper'> <Row type='flex' justify='center' align='top' className='cvat-create-task-form-wrapper'>
<Col md={20} lg={16} xl={14} xxl={9}> <Col md={20} lg={16} xl={14} xxl={9}>

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

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

@ -4,17 +4,16 @@
import './styles.scss'; import './styles.scss';
import React from 'react'; import React from 'react';
import Tabs from 'antd/lib/tabs';
import { import Icon from 'antd/lib/icon';
Tabs, import Input from 'antd/lib/input';
Icon,
Input,
Upload,
} from 'antd';
import Tree, { AntTreeNode, TreeNodeNormal } from 'antd/lib/tree/Tree';
import { RcFile } from 'antd/lib/upload';
import Text from 'antd/lib/typography/Text'; 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 { export interface Files {
local: File[]; 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 { treeData } = this.props;
const { const {
expandedKeys, expandedKeys,
@ -156,7 +156,7 @@ export default class FileManager extends React.PureComponent<Props, State> {
return ( return (
<Tabs.TabPane key='share' tab='Connected file share'> <Tabs.TabPane key='share' tab='Connected file share'>
{ treeData.length { treeData[0].children && treeData[0].children.length
? ( ? (
<Tree <Tree
className='cvat-share-tree' className='cvat-share-tree'
@ -190,7 +190,18 @@ export default class FileManager extends React.PureComponent<Props, State> {
> >
{ renderTreeNodes(treeData) } { renderTreeNodes(treeData) }
</Tree> </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> </Tabs.TabPane>
); );
} }

@ -10,3 +10,9 @@
max-height: 20em; max-height: 20em;
overflow: auto; 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 { resetAfterErrorAsync } from 'actions/boundaries-actions';
import { CombinedState } from 'reducers/interfaces'; import { CombinedState } from 'reducers/interfaces';
import logger, { LogType } from 'cvat-logger'; import logger, { LogType } from 'cvat-logger';
import consts from 'consts';
interface StateToProps { interface StateToProps {
job: any | null; job: any | null;
@ -161,7 +162,7 @@ class GlobalErrorBoundary extends React.PureComponent<Props, State> {
</li> </li>
<li> <li>
Notify an administrator or submit the issue directly on 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: Please, provide also:
<ul> <ul>
<li>Steps to reproduce the issue</li> <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 Text from 'antd/lib/typography/Text';
import { CVATLogo, AccountIcon } from 'icons'; import { CVATLogo, AccountIcon } from 'icons';
import consts from 'consts';
interface HeaderContainerProps { interface HeaderContainerProps {
onLogout: () => void; onLogout: () => void;
@ -60,12 +61,15 @@ function HeaderContainer(props: Props): JSX.Element {
|| installedTFAnnotation || installedTFAnnotation
|| installedTFSegmentation; || installedTFSegmentation;
function aboutModal(): void { const {
const CHANGELOG = 'https://github.com/opencv/cvat/blob/develop/CHANGELOG.md'; CHANGELOG_URL,
const LICENSE = 'https://github.com/opencv/cvat/blob/develop/LICENSE'; LICENSE_URL,
const GITTER = 'https://gitter.im/opencv-cvat'; GITTER_URL,
const FORUM = 'https://software.intel.com/en-us/forums/intel-distribution-of-openvino-toolkit'; FORUM_URL,
GITHUB_URL,
} = consts;
function aboutModal(): void {
Modal.info({ Modal.info({
title: `${toolName}`, title: `${toolName}`,
content: ( content: (
@ -106,10 +110,10 @@ function HeaderContainer(props: Props): JSX.Element {
</Text> </Text>
</p> </p>
<Row type='flex' justify='space-around'> <Row type='flex' justify='space-around'>
<Col><a href={CHANGELOG} target='_blank' rel='noopener noreferrer'>{'What\'s new?'}</a></Col> <Col><a href={CHANGELOG_URL} 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={LICENSE_URL} target='_blank' rel='noopener noreferrer'>License</a></Col>
<Col><a href={GITTER} target='_blank' rel='noopener noreferrer'>Need help?</a></Col> <Col><a href={GITTER_URL} 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={FORUM_URL} target='_blank' rel='noopener noreferrer'>Forum on Intel Developer Zone</a></Col>
</Row> </Row>
</div> </div>
), ),
@ -199,7 +203,9 @@ function HeaderContainer(props: Props): JSX.Element {
type='link' type='link'
onClick={ onClick={
(): void => { (): 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-player-settings-grid,
.cvat-workspace-settings-auto-save, .cvat-workspace-settings-auto-save,
.cvat-workspace-settings-autoborders,
.cvat-workspace-settings-show-text-always, .cvat-workspace-settings-show-text-always,
.cvat-workspace-settings-show-text-always-checkbox, .cvat-workspace-settings-show-interpolated {
.cvat-workspace-settings-show-interpolated-checkbox { margin-bottom: 25px;
margin-bottom: 10px;
> div:first-child {
margin-bottom: 10px;
}
} }
.cvat-player-settings-grid-size, .cvat-player-settings-grid-size,
@ -36,8 +40,6 @@
.cvat-player-settings-speed, .cvat-player-settings-speed,
.cvat-player-settings-reset-zoom, .cvat-player-settings-reset-zoom,
.cvat-player-settings-rotate-all, .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-aam-zoom-margin,
.cvat-workspace-settings-auto-save-interval { .cvat-workspace-settings-auto-save-interval {
margin-bottom: 25px; margin-bottom: 25px;

@ -17,11 +17,13 @@ interface Props {
aamZoomMargin: number; aamZoomMargin: number;
showAllInterpolationTracks: boolean; showAllInterpolationTracks: boolean;
showObjectsTextAlways: boolean; showObjectsTextAlways: boolean;
automaticBordering: boolean;
onSwitchAutoSave(enabled: boolean): void; onSwitchAutoSave(enabled: boolean): void;
onChangeAutoSaveInterval(interval: number): void; onChangeAutoSaveInterval(interval: number): void;
onChangeAAMZoomMargin(margin: number): void; onChangeAAMZoomMargin(margin: number): void;
onSwitchShowingInterpolatedTracks(enabled: boolean): void; onSwitchShowingInterpolatedTracks(enabled: boolean): void;
onSwitchShowingObjectsTextAlways(enabled: boolean): void; onSwitchShowingObjectsTextAlways(enabled: boolean): void;
onSwitchAutomaticBordering(enabled: boolean): void;
} }
export default function WorkspaceSettingsComponent(props: Props): JSX.Element { export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
@ -31,11 +33,13 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
aamZoomMargin, aamZoomMargin,
showAllInterpolationTracks, showAllInterpolationTracks,
showObjectsTextAlways, showObjectsTextAlways,
automaticBordering,
onSwitchAutoSave, onSwitchAutoSave,
onChangeAutoSaveInterval, onChangeAutoSaveInterval,
onChangeAAMZoomMargin, onChangeAAMZoomMargin,
onSwitchShowingInterpolatedTracks, onSwitchShowingInterpolatedTracks,
onSwitchShowingObjectsTextAlways, onSwitchShowingObjectsTextAlways,
onSwitchAutomaticBordering,
} = props; } = props;
const minAutoSaveInterval = 5; const minAutoSaveInterval = 5;
@ -82,7 +86,7 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
</Col> </Col>
</Row> </Row>
<Row className='cvat-workspace-settings-show-interpolated'> <Row className='cvat-workspace-settings-show-interpolated'>
<Col className='cvat-workspace-settings-show-interpolated-checkbox'> <Col>
<Checkbox <Checkbox
className='cvat-text-color' className='cvat-text-color'
checked={showAllInterpolationTracks} checked={showAllInterpolationTracks}
@ -98,7 +102,7 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
</Col> </Col>
</Row> </Row>
<Row className='cvat-workspace-settings-show-text-always'> <Row className='cvat-workspace-settings-show-text-always'>
<Col className='cvat-workspace-settings-show-text-always-checkbox'> <Col>
<Checkbox <Checkbox
className='cvat-text-color' className='cvat-text-color'
checked={showObjectsTextAlways} 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> <Text type='secondary'> Show text for an object on the canvas not only when the object is activated </Text>
</Col> </Col>
</Row> </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'> <Row className='cvat-workspace-settings-aam-zoom-margin'>
<Col> <Col>
<Text className='cvat-text-color'> Attribute annotation mode (AAM) zoom margin </Text> <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', repositoryStatus: 'sync',
}); });
} }
}).catch((): void => { }).catch((error): void => {
if (this.mounted) { if (this.mounted) {
Modal.error({
width: 800,
title: 'Could not synchronize the repository',
content: error.toString(),
});
this.setState({ this.setState({
repositoryStatus: '!sync', repositoryStatus: '!sync',
}); });

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

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

@ -4,8 +4,26 @@
const UNDEFINED_ATTRIBUTE_VALUE = '__undefined__'; const UNDEFINED_ATTRIBUTE_VALUE = '__undefined__';
const NO_BREAK_SPACE = '\u00a0'; 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 { export default {
UNDEFINED_ATTRIBUTE_VALUE, UNDEFINED_ATTRIBUTE_VALUE,
NO_BREAK_SPACE, 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 { const {
annotation: { annotation: {
job: { job: {
requestedId,
instance: job, instance: job,
fetching, fetching,
}, },
@ -41,7 +42,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
} = state; } = state;
return { return {
job: !job || jobID === job.id ? job : null, job: jobID === requestedId ? job : null,
fetching, fetching,
workspace, workspace,
}; };

@ -34,6 +34,7 @@ import {
changeBrightnessLevel, changeBrightnessLevel,
changeContrastLevel, changeContrastLevel,
changeSaturationLevel, changeSaturationLevel,
switchAutomaticBordering,
} from 'actions/settings-actions'; } from 'actions/settings-actions';
import { import {
ColorBy, ColorBy,
@ -42,6 +43,7 @@ import {
CombinedState, CombinedState,
ContextMenuType, ContextMenuType,
Workspace, Workspace,
ActiveControl,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import { Canvas } from 'cvat-canvas'; import { Canvas } from 'cvat-canvas';
@ -79,8 +81,10 @@ interface StateToProps {
minZLayer: number; minZLayer: number;
maxZLayer: number; maxZLayer: number;
curZLayer: number; curZLayer: number;
automaticBordering: boolean;
contextVisible: boolean; contextVisible: boolean;
contextType: ContextMenuType; contextType: ContextMenuType;
switchableAutomaticBordering: boolean;
keyMap: Record<string, ExtendedKeyMapOptions>; keyMap: Record<string, ExtendedKeyMapOptions>;
} }
@ -111,12 +115,14 @@ interface DispatchToProps {
onChangeGridOpacity(opacity: number): void; onChangeGridOpacity(opacity: number): void;
onChangeGridColor(color: GridColor): void; onChangeGridColor(color: GridColor): void;
onSwitchGrid(enabled: boolean): void; onSwitchGrid(enabled: boolean): void;
onSwitchAutomaticBordering(enabled: boolean): void;
} }
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
const { const {
annotation: { annotation: {
canvas: { canvas: {
activeControl,
contextMenu: { contextMenu: {
visible: contextVisible, visible: contextVisible,
type: contextType, type: contextType,
@ -166,6 +172,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
workspace: { workspace: {
aamZoomMargin, aamZoomMargin,
showObjectsTextAlways, showObjectsTextAlways,
automaticBordering,
}, },
shapes: { shapes: {
opacity, opacity,
@ -212,10 +219,14 @@ function mapStateToProps(state: CombinedState): StateToProps {
curZLayer, curZLayer,
minZLayer, minZLayer,
maxZLayer, maxZLayer,
automaticBordering,
contextVisible, contextVisible,
contextType, contextType,
workspace, workspace,
keyMap, 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 { onSwitchGrid(enabled: boolean): void {
dispatch(switchGrid(enabled)); 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 { connect } from 'react-redux';
import { LogType } from 'cvat-logger'; import { LogType } from 'cvat-logger';
import { Canvas, isAbleToChangeFrame } from 'cvat-canvas';
import { ActiveControl, CombinedState, ColorBy } from 'reducers/interfaces'; import { ActiveControl, CombinedState, ColorBy } from 'reducers/interfaces';
import { import {
collapseObjectItems, collapseObjectItems,
@ -24,7 +25,6 @@ import {
import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item'; import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item';
interface OwnProps { interface OwnProps {
clientID: number; clientID: number;
} }
@ -44,6 +44,7 @@ interface StateToProps {
minZLayer: number; minZLayer: number;
maxZLayer: number; maxZLayer: number;
normalizedKeyMap: Record<string, string>; normalizedKeyMap: Record<string, string>;
canvasInstance: Canvas;
} }
interface DispatchToProps { interface DispatchToProps {
@ -84,6 +85,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
canvas: { canvas: {
ready, ready,
activeControl, activeControl,
instance: canvasInstance,
}, },
colors, colors,
}, },
@ -119,6 +121,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
minZLayer, minZLayer,
maxZLayer, maxZLayer,
normalizedKeyMap, normalizedKeyMap,
canvasInstance,
}; };
} }
@ -166,72 +169,44 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
type Props = StateToProps & DispatchToProps; type Props = StateToProps & DispatchToProps;
class ObjectItemContainer extends React.PureComponent<Props> { class ObjectItemContainer extends React.PureComponent<Props> {
private navigateFirstKeyframe = (): void => { private navigateFirstKeyframe = (): void => {
const { const { objectState, frameNumber } = this.props;
objectState,
changeFrame,
frameNumber,
} = this.props;
const { first } = objectState.keyframes; const { first } = objectState.keyframes;
if (first !== frameNumber) { if (first !== frameNumber) {
changeFrame(first); this.changeFrame(first);
} }
}; };
private navigatePrevKeyframe = (): void => { private navigatePrevKeyframe = (): void => {
const { const { objectState, frameNumber } = this.props;
objectState,
changeFrame,
frameNumber,
} = this.props;
const { prev } = objectState.keyframes; const { prev } = objectState.keyframes;
if (prev !== null && prev !== frameNumber) { if (prev !== null && prev !== frameNumber) {
changeFrame(prev); this.changeFrame(prev);
} }
}; };
private navigateNextKeyframe = (): void => { private navigateNextKeyframe = (): void => {
const { const { objectState, frameNumber } = this.props;
objectState,
changeFrame,
frameNumber,
} = this.props;
const { next } = objectState.keyframes; const { next } = objectState.keyframes;
if (next !== null && next !== frameNumber) { if (next !== null && next !== frameNumber) {
changeFrame(next); this.changeFrame(next);
} }
}; };
private navigateLastKeyframe = (): void => { private navigateLastKeyframe = (): void => {
const { const { objectState, frameNumber } = this.props;
objectState,
changeFrame,
frameNumber,
} = this.props;
const { last } = objectState.keyframes; const { last } = objectState.keyframes;
if (last !== frameNumber) { if (last !== frameNumber) {
changeFrame(last); this.changeFrame(last);
} }
}; };
private copy = (): void => { private copy = (): void => {
const { const { objectState, copyShape } = this.props;
objectState,
copyShape,
} = this.props;
copyShape(objectState); copyShape(objectState);
}; };
private propagate = (): void => { private propagate = (): void => {
const { const { objectState, propagateObject } = this.props;
objectState,
propagateObject,
} = this.props;
propagateObject(objectState); propagateObject(objectState);
}; };
@ -422,6 +397,13 @@ class ObjectItemContainer extends React.PureComponent<Props> {
this.commit(); this.commit();
}; };
private changeFrame(frame: number): void {
const { changeFrame, canvasInstance } = this.props;
if (isAbleToChangeFrame(canvasInstance)) {
changeFrame(frame);
}
}
private commit(): void { private commit(): void {
const { const {
objectState, objectState,

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

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

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

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

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

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

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

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

@ -28,6 +28,7 @@ const defaultState: SettingsState = {
autoSave: false, autoSave: false,
autoSaveInterval: 15 * 60 * 1000, autoSaveInterval: 15 * 60 * 1000,
aamZoomMargin: 100, aamZoomMargin: 100,
automaticBordering: false,
showObjectsTextAlways: false, showObjectsTextAlways: false,
showAllInterpolationTracks: 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 BoundariesActionTypes.RESET_AFTER_ERROR:
case AnnotationActionTypes.GET_JOB_SUCCESS: { case AnnotationActionTypes.GET_JOB_SUCCESS: {
const { job } = action.payload; const { job } = action.payload;

@ -314,6 +314,12 @@ const defaultKeyMap = {
sequences: ['`', '~'], sequences: ['`', '~'],
action: 'keydown', 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>; } as any as Record<string, ExtendedKeyMapOptions>;

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

@ -48,5 +48,4 @@ hr {
height: 100%; height: 100%;
display: grid; display: grid;
min-width: 1280px; 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(); resolve();
} else if (response.status === 'failed') { } else if (response.status === 'failed') {
const message = `Can not push to remote repository. Message: ${response.stderr}`; const message = `Can not push to remote repository. Message: ${response.stderr}`;
throw new Error(message); reject(new Error(message));
} else { } else {
const message = `Check returned status "${response.status}".`; 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: { case SupportedPlugins.TF_SEGMENTATION: {
return isReachable(`${serverHost}/tensorflow/segmentation/meta/get`, 'OPTIONS'); return isReachable(`${serverHost}/tensorflow/segmentation/meta/get`, 'OPTIONS');
} }
case SupportedPlugins.DEXTR_SEGMENTATION: {
return isReachable(`${serverHost}/dextr/enabled`, 'GET');
}
case SupportedPlugins.ANALYTICS: { case SupportedPlugins.ANALYTICS: {
return isReachable(`${serverHost}/analytics/app/kibana`, 'GET'); return isReachable(`${serverHost}/analytics/app/kibana`, 'GET');
} }

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

@ -48,11 +48,19 @@ class DEXTR_HANDLER:
image = PIL.Image.open(image[0]) image = PIL.Image.open(image[0])
numpy_image = np.array(image) numpy_image = np.array(image)
points = np.asarray([[int(p["x"]), int(p["y"])] for p in points], dtype=int) 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 = ( bounding_box = (
max(min(points[:, 0]) - _DEXTR_PADDING, 0), max(min(points[:, 0]) - padding, 0),
max(min(points[:, 1]) - _DEXTR_PADDING, 0), max(min(points[:, 1]) - padding, 0),
min(max(points[:, 0]) + _DEXTR_PADDING, numpy_image.shape[1] - 1), min(max(points[:, 0]) + padding, width - 1),
min(max(points[:, 1]) + _DEXTR_PADDING, numpy_image.shape[0] - 1) min(max(points[:, 1]) + padding, height - 1)
) )
# Prepare an image # Prepare an image
@ -61,7 +69,7 @@ class DEXTR_HANDLER:
interpolation = cv2.INTER_CUBIC).astype(np.float32) interpolation = cv2.INTER_CUBIC).astype(np.float32)
# Make a heatmap # 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) 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) heatmap = np.zeros(shape=resized.shape[:2], dtype=np.float64)
for point in points: for point in points:

@ -8,5 +8,6 @@ from . import views
urlpatterns = [ urlpatterns = [
path('create/<int:jid>', views.create), path('create/<int:jid>', views.create),
path('cancel/<int:jid>', views.cancel), 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: except Exception as ex:
slogger.job[jid].error("can't check a dextr request for the job {}".format(jid), exc_info=True) slogger.job[jid].error("can't check a dextr request for the job {}".format(jid), exc_info=True)
return HttpResponseBadRequest(str(ex)) return HttpResponseBadRequest(str(ex))
def enabled(request):
return HttpResponse()

@ -7,6 +7,7 @@
- [Stop all containers](#stop-all-containers) - [Stop all containers](#stop-all-containers)
- [Advanced settings](#advanced-settings) - [Advanced settings](#advanced-settings)
- [Share path](#share-path) - [Share path](#share-path)
- [Serving over HTTPS](#serving-over-https)
# Quick installation guide # Quick installation guide
@ -306,3 +307,299 @@ volumes:
You can change the share device path to your actual share. For user convenience 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 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. 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. The ``Done`` button applies the changes and the ``Reset`` button cancels the changes.
![](static/documentation/images/image126.jpg) ![](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. **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 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). If you select `` Remote source`` , you'll see a field where you can enter a list of URLs (one URL per line).
@ -148,6 +150,8 @@ Go to the [Django administration panel](http://localhost:8080/admin). There you
**Z-Order**. Defines the order on drawn polygons. Check the box for enable layered displaying. **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. **Image Quality**. Use this option to specify quality of uploaded images.
The option helps to load high resolution datasets faster. The option helps to load high resolution datasets faster.
Use the value from ``1`` (completely compressed images) to ``95`` (almost not compressed images). Use the value from ``1`` (completely compressed images) to ``95`` (almost not compressed images).
@ -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. **Stop frame**. Frame on which video in task ends.
**Frame Filter**. Use this option to filter video frames. **Frame Step**. 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. 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 **Dataset Repository**. URL link of the repository optionally specifies the path to the repository for storage
(``default: annotation / <dump_file_name> .zip``). (``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/) - [Pascal VOC 2012](http://host.robots.ox.ac.uk/pascal/VOC/)
- [MS COCO](http://cocodataset.org/#format-data) - [MS COCO](http://cocodataset.org/#format-data)
- [YOLO](https://pjreddie.com/darknet/yolo/) - [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. Presence depends on how you build CVAT instance.
- ``Open bug tracker`` — opens a link to Issue tracker. - ``Open bug tracker`` — opens a link to Issue tracker.
- ``Delete`` — delete task. - ``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. 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 In some cases, you can have several links. It depends on size of your
task and ``Overlap Size`` and ``Segment Size`` parameters. To improve 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. to annotate first images. Other frames will be loaded in background.
![](static/documentation/images/image007.jpg) ![](static/documentation/images/image007_DETRAC.jpg)
### Models ### Models
@ -334,34 +347,39 @@ The search is case insensitive.
## Interface of the annotation tool ## Interface of the annotation tool
The tool consists of: The tool consists of:
- ``Workspace`` — where images are shown; - ``Header`` - pinned header used to navigate CVAT sections and account settings;
- ``Bottom panel`` (under workspace) — for navigation, filtering annotation and accessing tools' menu; - ``Top panel`` — contains navigation buttons, main functions and menu access;
- ``Side panel`` — contains two lists: objects (on the frame) and labels (of objects on the frame); - ``Workspace`` — space where images are shown;
- ``Bottom side panel`` — contains the main annotation functions (create, merge, group objects). - ``Controls sidebar`` — contains tools for navigating the image, zoom,
Here you can choose a type of shape, a label you want to annotate and a mode (annotation or interpolation) 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) ![](static/documentation/images/image034_DETRAC.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.
### Basic navigation ### 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. Use the scroll bar slider to scroll through frames.
Almost every button is covered by a shortcut. Almost every button has a shortcut.
To get a hint about a shortcut, just put your mouse pointer over an UI element. To get a hint about a shortcut, just move your mouse pointer over an UI element.
![](static/documentation/images/image008.jpg) ![](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. 1. To navigate the image, use the button on the controls sidebar.
Thus, if you point on an object, it will be under your mouse during zooming process. 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. ![](static/documentation/images/image137.jpg)
If ``Mouse Wheel`` is pressed, then all annotated objects are ignored.
Otherwise, a highlighted bounding box will be moved instead of the image itself.
### Types of shapes (basics) ### Types of shapes (basics)
There are four shapes which you can annotate your images with: 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 # SPDX-License-Identifier: MIT
import math import math
from io import BytesIO
from enum import Enum from enum import Enum
import itertools from io import BytesIO
import numpy as np import numpy as np
from PIL import Image from PIL import Image
from cvat.apps.engine.media_extractors import VideoReader, ZipReader 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.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): class Quality(Enum):
COMPRESSED = 0 COMPRESSED = 0
ORIGINAL = 100 ORIGINAL = 100
@ -25,26 +51,34 @@ class FrameProvider():
PIL = 1 PIL = 1
NUMPY_ARRAY = 2 NUMPY_ARRAY = 2
def __init__(self, db_data): class ChunkLoader:
self._db_data = db_data def __init__(self, reader_class, path_getter):
if db_data.compressed_chunk_type == DataChoice.IMAGESET: self.chunk_id = None
self._compressed_chunk_reader_class = ZipReader self.chunk_reader = None
elif db_data.compressed_chunk_type == DataChoice.VIDEO: self.reader_class = reader_class
self._compressed_chunk_reader_class = VideoReader self.get_chunk_path = path_getter
else:
raise Exception('Unsupported chunk type')
if db_data.original_chunk_type == DataChoice.IMAGESET: def load(self, chunk_id):
self._original_chunk_reader_class = ZipReader if self.chunk_id != chunk_id:
elif db_data.original_chunk_type == DataChoice.VIDEO: self.chunk_id = chunk_id
self._original_chunk_reader_class = VideoReader self.chunk_reader = RandomAccessIterator(
else: self.reader_class([self.get_chunk_path(chunk_id)]))
raise Exception('Unsupported chunk type') return self.chunk_reader
self._extracted_compressed_chunk = None def __init__(self, db_data):
self._compressed_chunk_reader = None self._db_data = db_data
self._extracted_original_chunk = None self._loaders = {}
self._original_chunk_reader = None
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): def __len__(self):
return self._db_data.size return self._db_data.size
@ -74,77 +108,41 @@ class FrameProvider():
buf.seek(0) buf.seek(0)
return buf return buf
def _get_frame(self, frame_number, chunk_path_getter, extracted_chunk, chunk_reader, reader_class): def _convert_frame(self, frame, reader_class, out_type):
_, chunk_number, frame_offset = self._validate_frame_number(frame_number) if out_type == self.Type.BUFFER:
chunk_path = chunk_path_getter(chunk_number) return self._av_frame_to_png_bytes(frame) if reader_class is VideoReader else frame
if chunk_number != extracted_chunk: elif out_type == self.Type.PIL:
extracted_chunk = chunk_number return frame.to_image() if reader_class is VideoReader else Image.open(frame)
chunk_reader = reader_class([chunk_path]) elif out_type == self.Type.NUMPY_ARRAY:
if reader_class is VideoReader:
frame, frame_name, _ = next(itertools.islice(chunk_reader, frame_offset, None)) image = np.array(frame.to_image())
if reader_class is VideoReader: else:
return (self._av_frame_to_png_bytes(frame), 'image/png') image = np.array(Image.open(frame))
if len(image.shape) == 3 and image.shape[2] in {3, 4}:
return (frame, mimetypes.guess_type(frame_name)) image[:, :, :3] = image[:, :, 2::-1] # RGB to BGR
return image
def _get_frames(self, chunk_path_getter, reader_class, out_type): else:
for chunk_idx in range(math.ceil(self._db_data.size / self._db_data.chunk_size)): raise Exception('unsupported output type')
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 get_preview(self): def get_preview(self):
return self._db_data.get_preview_path() return self._db_data.get_preview_path()
def get_chunk(self, chunk_number, quality=Quality.ORIGINAL): def get_chunk(self, chunk_number, quality=Quality.ORIGINAL):
chunk_number = self._validate_chunk_number(chunk_number) chunk_number = self._validate_chunk_number(chunk_number)
if quality == self.Quality.ORIGINAL: return self._loaders[quality].get_chunk_path(chunk_number)
return self._db_data.get_original_chunk_path(chunk_number)
elif quality == self.Quality.COMPRESSED: def get_frame(self, frame_number, quality=Quality.ORIGINAL,
return self._db_data.get_compressed_chunk_path(chunk_number) out_type=Type.BUFFER):
_, chunk_number, frame_offset = self._validate_frame_number(frame_number)
def get_frame(self, frame_number, quality=Quality.ORIGINAL): loader = self._loaders[quality]
if quality == self.Quality.ORIGINAL: chunk_reader = loader.load(chunk_number)
return self._get_frame( frame, frame_name, _ = chunk_reader[frame_offset]
frame_number=frame_number,
chunk_path_getter=self._db_data.get_original_chunk_path, frame = self._convert_frame(frame, loader.reader_class, out_type)
extracted_chunk=self._extracted_original_chunk, if loader.reader_class is VideoReader:
chunk_reader=self._original_chunk_reader, return (frame, 'image/png')
reader_class=self._original_chunk_reader_class, return (frame, mimetypes.guess_type(frame_name))
)
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,
)
def get_frames(self, quality=Quality.ORIGINAL, out_type=Type.BUFFER): def get_frames(self, quality=Quality.ORIGINAL, out_type=Type.BUFFER):
if quality == self.Quality.ORIGINAL: for idx in range(self._db_data.size):
return self._get_frames( yield self.get_frame(idx, quality=quality, out_type=out_type)
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,
)

@ -207,6 +207,7 @@ class DataSerializer(serializers.ModelSerializer):
os.makedirs(db_data.get_compressed_cache_dirname()) os.makedirs(db_data.get_compressed_cache_dirname())
os.makedirs(db_data.get_original_cache_dirname()) os.makedirs(db_data.get_original_cache_dirname())
os.makedirs(db_data.get_upload_dirname())
for f in client_files: for f in client_files:
client_file = models.ClientFile(data=db_data, **f) 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 * SPDX-License-Identifier: MIT
*/ */
@ -145,7 +145,7 @@ class ShapeMergerModel extends Listener {
let nextFrame = frame + 1; let nextFrame = frame + 1;
let stopFrame = window.cvat.player.frames.stop; let stopFrame = window.cvat.player.frames.stop;
let type = shapeDict[frame].shape.type; 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]); let copy = Object.assign({}, object.shapes[object.shapes.length - 1]);
copy.outside = true; copy.outside = true;
copy.frame += 1; copy.frame += 1;

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

@ -150,7 +150,7 @@ class ReID:
job = rq.get_current_job() job = rq.get_current_job()
box_tracks = {} 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:]))): for idx, (cur_frame, next_frame) in enumerate(list(zip(frames[:-1], frames[1:]))):
job.refresh() job.refresh()
if "cancel" in job.meta: if "cancel" in job.meta:
@ -172,7 +172,7 @@ class ReID:
continue continue
cur_image = next_image 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) difference_matrix = self.__compute_difference_matrix(cur_boxes, next_boxes, cur_image, next_image)
cur_idxs, next_idxs = linear_sum_assignment(difference_matrix) cur_idxs, next_idxs = linear_sum_assignment(difference_matrix)
for idx, cur_idx in enumerate(cur_idxs): 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 # SPDX-License-Identifier: MIT
@ -364,9 +364,9 @@ class _KeypointsConverter(_InstancesConverter):
solitary_points = [] solitary_points = []
for g_id, group in groupby(annotations, lambda a: a.group): 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] group = [a for a in group if a.type == AnnotationType.points]
solitary_points.extend(group) solitary_points.extend(group)
return solitary_points return solitary_points
@ -514,7 +514,7 @@ class _Converter:
filename += CocoPath.IMAGE_EXT filename += CocoPath.IMAGE_EXT
path = osp.join(self._images_dir, filename) path = osp.join(self._images_dir, filename)
save_image(path, image) save_image(path, image)
return filename return path
def convert(self): def convert(self):
self._make_dirs() self._make_dirs()
@ -536,7 +536,7 @@ class _Converter:
for item in subset: for item in subset:
filename = '' filename = ''
if item.has_image: if item.has_image:
filename = item.image.filename filename = item.image.path
if self._save_images: if self._save_images:
if item.has_image: if item.has_image:
filename = self._save_image(item) filename = self._save_image(item)

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

@ -560,6 +560,9 @@ class CocoConverterTest(TestCase):
annotations=[ annotations=[
# Solitary keypoints # Solitary keypoints
Points([1, 2, 0, 2, 4, 1], label=5, id=3), 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', DatasetItem(id=3, subset='val',

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

Loading…
Cancel
Save