diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..4b7ae8b1 --- /dev/null +++ b/.github/CODEOWNERS @@ -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 diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..d9506af8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,53 @@ + + +### My actions before raising this issue +- [ ] Read/searched [the docs](https://github.com/opencv/cvat/tree/master#documentation) +- [ ] Searched [past issues](/issues) + + + +### Expected Behaviour + + +### Current Behaviour + + +### Possible Solution + + +### Steps to Reproduce (for bugs) + +1. +1. +1. +1. + +### Context + + +### Your Environment + +- 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: +
+ Logs from `cvat` container +
+ +### Next steps +You may [join our Gitter](https://gitter.im/opencv-cvat/public) channel for community support. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..23b9bde8 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,46 @@ + + + + +### Motivation and context + + +### How has this been tested? + + +### Checklist + + +- [ ] 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 +``` diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a3778c..e620d960 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Dialog window with some helpful information about using filters - Ability to display a bitmap in the new UI - Button to reset colors settings (brightness, saturation, contrast) in the new UI -- Added option to display shape text always +- Option to display shape text always +- Dedicated message with clarifications when share is unmounted (https://github.com/opencv/cvat/pull/1373) +- Ability to create one tracked point (https://github.com/opencv/cvat/pull/1383) +- Ability to draw/edit polygons and polylines with automatic bordering feature (https://github.com/opencv/cvat/pull/1394) +- Tutorial: instructions for CVAT over HTTPS +- Added deep extreme cut (semi-automatic segmentation) to the new UI (https://github.com/opencv/cvat/pull/1398) + ### Changed - Increase preview size of a task till 256, 256 on the server +- Minor style updates +- Public ssh-keys are displayed in a dedicated window instead of console when create a task with a repository +- React UI has become is a primary UI ### Deprecated - @@ -23,8 +32,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - New shape is added when press ``esc`` when drawing instead of cancellation -- Fixed dextr segmentation. -- Fixed `FileNotFoundError` during dump after moving format files +- Dextr segmentation doesn't work. +- `FileNotFoundError` during dump after moving format files +- CVAT doesn't append outside shapes when merge polyshapes in old UI +- Layout sometimes shows double scroll bars on create task, dashboard and settings pages +- UI fails after trying to change frame during resizing, dragging, editing +- Hidden points (or outsided) are visible after changing a frame +- Merge is allowed for points, but clicks on points conflict with frame dragging logic +- Removed objects are visible for search +- Add missed task_id and job_id fields into exception logs for the new UI (https://github.com/opencv/cvat/pull/1372) +- UI fails when annotations saving occurs during drag/resize/edit (https://github.com/opencv/cvat/pull/1383) +- Multiple savings when hold Ctrl+S (a lot of the same copies of events were sent with the same working time) (https://github.com/opencv/cvat/pull/1383) +- UI doesn't have any reaction when git repos synchronization failed (https://github.com/opencv/cvat/pull/1383) +- Bug when annotations cannot be saved after (delete - save - undo - save) (https://github.com/opencv/cvat/pull/1383) +- VOC format exports Upper case labels correctly in lower case (https://github.com/opencv/cvat/pull/1379) +- Fixed polygon exporting bug in COCO dataset (https://github.com/opencv/cvat/issues/1387) +- Task creation from remote files (https://github.com/opencv/cvat/pull/1392) +- Job cannot be opened in some cases when the previous job was failed during opening (https://github.com/opencv/cvat/issues/1403) +- Deactivated shape is still highlighted on the canvas (https://github.com/opencv/cvat/issues/1403) +- AttributeError: 'tuple' object has no attribute 'read' in ReID algorithm (https://github.com/opencv/cvat/issues/1403) +- Wrong semi-automatic segmentation near edges of an image (https://github.com/opencv/cvat/issues/1403) +- Git repos paths (https://github.com/opencv/cvat/pull/1400) ### Security - diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md deleted file mode 100644 index f21998f1..00000000 --- a/CONTRIBUTORS.md +++ /dev/null @@ -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 diff --git a/LICENSE b/LICENSE index aae0a08e..46056e4f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,6 @@ -Copyright (C) 2018 Intel Corporation +MIT License + +Copyright (C) 2018-2020 Intel Corporation   Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), @@ -18,4 +20,3 @@ OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.   -SPDX-License-Identifier: MIT diff --git a/cvat-canvas/README.md b/cvat-canvas/README.md index 59806746..00d60cef 100644 --- a/cvat-canvas/README.md +++ b/cvat-canvas/README.md @@ -153,6 +153,7 @@ Standard JS events are used. - canvas.fit - canvas.dragshape => {id: number} - canvas.resizeshape => {id: number} + - canvas.contextmenu => { mouseEvent: MouseEvent, objectState: ObjectState, pointID: number } ``` ### WEB @@ -179,23 +180,25 @@ Standard JS events are used. ## API Reaction -| | IDLE | GROUPING | SPLITTING | DRAWING | MERGING | EDITING | DRAG | ZOOM | -|--------------|------|----------|-----------|---------|---------|---------|------|------| -| html() | + | + | + | + | + | + | + | + | -| setup() | + | + | + | + | + | - | + | + | -| activate() | + | - | - | - | - | - | - | - | -| rotate() | + | + | + | + | + | + | + | + | -| focus() | + | + | + | + | + | + | + | + | -| fit() | + | + | + | + | + | + | + | + | -| grid() | + | + | + | + | + | + | + | + | -| draw() | + | - | - | - | - | - | - | - | -| split() | + | - | + | - | - | - | - | - | -| group() | + | + | - | - | - | - | - | - | -| merge() | + | - | - | - | + | - | - | - | -| fitCanvas() | + | + | + | + | + | + | + | + | -| dragCanvas() | + | - | - | - | - | - | + | - | -| zoomCanvas() | + | - | - | - | - | - | - | + | -| cancel() | - | + | + | + | + | + | + | + | -| configure() | + | - | - | - | - | - | - | - | -| bitmap() | + | + | + | + | + | + | + | + | -| setZLayer() | + | + | + | + | + | + | + | + | +| | IDLE | GROUP | SPLIT | DRAW | MERGE | EDIT | DRAG | RESIZE | ZOOM_CANVAS | DRAG_CANVAS | +|--------------|------|-------|-------|------|-------|------|------|--------|-------------|-------------| +| html() | + | + | + | + | + | + | + | + | + | + | +| setup() | + | + | + | + | + | +/- | +/- | +/- | + | + | +| activate() | + | - | - | - | - | - | - | - | - | - | +| rotate() | + | + | + | + | + | + | + | + | + | + | +| focus() | + | + | + | + | + | + | + | + | + | + | +| fit() | + | + | + | + | + | + | + | + | + | + | +| grid() | + | + | + | + | + | + | + | + | + | + | +| draw() | + | - | - | - | - | - | - | - | - | - | +| split() | + | - | + | - | - | - | - | - | - | - | +| group() | + | + | - | - | - | - | - | - | - | - | +| merge() | + | - | - | - | + | - | - | - | - | - | +| fitCanvas() | + | + | + | + | + | + | + | + | + | + | +| dragCanvas() | + | - | - | - | - | - | + | - | - | + | +| zoomCanvas() | + | - | - | - | - | - | - | + | + | - | +| cancel() | - | + | + | + | + | + | + | + | + | + | +| configure() | + | + | + | + | + | + | + | + | + | + | +| bitmap() | + | + | + | + | + | + | + | + | + | + | +| setZLayer() | + | + | + | + | + | + | + | + | + | + | + +You can call setup() during editing, dragging, and resizing only to update objects, not to change a frame. diff --git a/cvat-canvas/src/scss/canvas.scss b/cvat-canvas/src/scss/canvas.scss index fab01d0b..2f8a081c 100644 --- a/cvat-canvas/src/scss/canvas.scss +++ b/cvat-canvas/src/scss/canvas.scss @@ -103,6 +103,24 @@ polyline.cvat_canvas_shape_splitting { stroke-dasharray: 5; } +.cvat_canvas_autoborder_point { + opacity: 0.55; +} + +.cvat_canvas_autoborder_point:hover { + opacity: 1; + fill: red; +} + +.cvat_canvas_autoborder_point:active { + opacity: 0.55; + fill: red; +} + +.cvat_canvas_autoborder_point_direction { + fill: blueviolet; +} + .svg_select_boundingRect { opacity: 0; pointer-events: none; diff --git a/cvat-canvas/src/typescript/autoborderHandler.ts b/cvat-canvas/src/typescript/autoborderHandler.ts new file mode 100644 index 00000000..8f042948 --- /dev/null +++ b/cvat-canvas/src/typescript/autoborderHandler.ts @@ -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 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}`); + }); + }); + } +} diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index 13f99be1..f7f095ea 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -47,6 +47,7 @@ export enum RectDrawingMethod { } export interface Configuration { + autoborders?: boolean; displayAllText?: boolean; undefinedAttrValue?: string; } @@ -206,6 +207,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { }, configuration: { displayAllText: false, + autoborders: false, undefinedAttrValue: '', }, imageBitmap: false, @@ -327,6 +329,12 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { } public setup(frameData: any, objectStates: any[]): void { + if (this.data.imageID !== frameData.number) { + if ([Mode.EDIT, Mode.DRAG, Mode.RESIZE].includes(this.data.mode)) { + throw Error(`Canvas is busy. Action: ${this.data.mode}`); + } + } + if (frameData.number === this.data.imageID) { this.data.objects = objectStates; this.notify(UpdateReasons.OBJECTS_UPDATED); @@ -360,6 +368,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { } public activate(clientID: number | null, attributeID: number | null): void { + if (this.data.activeElement.clientID === clientID) { + return; + } + if (this.data.mode !== Mode.IDLE && clientID !== null) { // Exception or just return? throw Error(`Canvas is busy. Action: ${this.data.mode}`); @@ -509,14 +521,14 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { } public configure(configuration: Configuration): void { - if (this.data.mode !== Mode.IDLE) { - throw Error(`Canvas is busy. Action: ${this.data.mode}`); - } - if (typeof (configuration.displayAllText) !== 'undefined') { this.data.configuration.displayAllText = configuration.displayAllText; } + if (typeof (configuration.autoborders) !== 'undefined') { + this.data.configuration.autoborders = configuration.autoborders; + } + if (typeof (configuration.undefinedAttrValue) !== 'undefined') { this.data.configuration.undefinedAttrValue = configuration.undefinedAttrValue; } diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 27160436..801dde51 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -16,6 +16,7 @@ import { MergeHandler, MergeHandlerImpl } from './mergeHandler'; import { SplitHandler, SplitHandlerImpl } from './splitHandler'; import { GroupHandler, GroupHandlerImpl } from './groupHandler'; import { ZoomHandler, ZoomHandlerImpl } from './zoomHandler'; +import { AutoborderHandler, AutoborderHandlerImpl } from './autoborderHandler'; import consts from './consts'; import { translateToSVG, @@ -23,6 +24,7 @@ import { pointsToArray, displayShapeSize, ShapeSizeElement, + DrawnState, } from './shared'; import { CanvasModel, @@ -58,7 +60,7 @@ export class CanvasViewImpl implements CanvasView, Listener { private controller: CanvasController; private svgShapes: Record; private svgTexts: Record; - private drawnStates: Record; + private drawnStates: Record; private geometry: Geometry; private drawHandler: DrawHandler; private editHandler: EditHandler; @@ -66,6 +68,7 @@ export class CanvasViewImpl implements CanvasView, Listener { private splitHandler: SplitHandler; private groupHandler: GroupHandler; private zoomHandler: ZoomHandler; + private autoborderHandler: AutoborderHandler; private activeElement: ActiveElement; private configuration: Configuration; @@ -358,6 +361,7 @@ export class CanvasViewImpl implements CanvasView, Listener { // Transform handlers this.drawHandler.transform(this.geometry); this.editHandler.transform(this.geometry); + this.autoborderHandler.transform(this.geometry); } private resizeCanvas(): void { @@ -385,6 +389,7 @@ export class CanvasViewImpl implements CanvasView, Listener { created.push(state); } else { const drawnState = this.drawnStates[state.clientID]; + // object has been changed or changed frame for a track if (drawnState.updated !== state.updated || drawnState.frame !== state.frame) { updated.push(state); } @@ -395,30 +400,33 @@ export class CanvasViewImpl implements CanvasView, Listener { .filter((id: number): boolean => !newIDs.includes(id)) .map((id: number): any => this.drawnStates[id]); + if (deleted.length || updated.length || created.length) { + if (this.activeElement.clientID !== null) { + this.deactivate(); + } - if (this.activeElement.clientID !== null) { - this.deactivate(); - } + for (const state of deleted) { + if (state.clientID in this.svgTexts) { + this.svgTexts[state.clientID].remove(); + } - for (const state of deleted) { - if (state.clientID in this.svgTexts) { - this.svgTexts[state.clientID].remove(); + this.svgShapes[state.clientID].off('click.canvas'); + this.svgShapes[state.clientID].remove(); + delete this.drawnStates[state.clientID]; } - this.svgShapes[state.clientID].off('click.canvas'); - this.svgShapes[state.clientID].remove(); - delete this.drawnStates[state.clientID]; - } - - this.addObjects(created, translate); - this.updateObjects(updated, translate); - this.sortObjects(); + this.addObjects(created, translate); + this.updateObjects(updated, translate); + this.sortObjects(); - if (this.controller.activeElement.clientID !== null) { - const { clientID } = this.controller.activeElement; - if (states.map((state: any): number => state.clientID).includes(clientID)) { - this.activate(this.controller.activeElement); + if (this.controller.activeElement.clientID !== null) { + const { clientID } = this.controller.activeElement; + if (states.map((state: any): number => state.clientID).includes(clientID)) { + this.activate(this.controller.activeElement); + } } + + this.autoborderHandler.updateObjects(); } } @@ -459,7 +467,7 @@ export class CanvasViewImpl implements CanvasView, Listener { e.preventDefault(); } - function contextmenuHandler(e: MouseEvent): void { + function contextMenuHandler(e: MouseEvent): void { const pointID = Array.prototype.indexOf .call(((e.target as HTMLElement).parentElement as HTMLElement).children, e.target); if (self.activeElement.clientID !== null) { @@ -467,7 +475,7 @@ export class CanvasViewImpl implements CanvasView, Listener { .filter((_state: any): boolean => ( _state.clientID === self.activeElement.clientID )); - self.canvas.dispatchEvent(new CustomEvent('point.contextmenu', { + self.canvas.dispatchEvent(new CustomEvent('canvas.contextmenu', { bubbles: false, cancelable: true, detail: { @@ -502,7 +510,7 @@ export class CanvasViewImpl implements CanvasView, Listener { }); circle.on('dblclick', dblClickHandler); - circle.on('contextmenu', contextmenuHandler); + circle.on('contextmenu', contextMenuHandler); circle.addClass('cvat_canvas_selected_point'); }); @@ -512,7 +520,7 @@ export class CanvasViewImpl implements CanvasView, Listener { }); circle.off('dblclick', dblClickHandler); - circle.off('contextmenu', contextmenuHandler); + circle.off('contextmenu', contextMenuHandler); circle.removeClass('cvat_canvas_selected_point'); }); @@ -620,14 +628,19 @@ export class CanvasViewImpl implements CanvasView, Listener { const self = this; // Setup API handlers + this.autoborderHandler = new AutoborderHandlerImpl( + this.content, + ); this.drawHandler = new DrawHandlerImpl( this.onDrawDone.bind(this), this.adoptedContent, this.adoptedText, + this.autoborderHandler, ); this.editHandler = new EditHandlerImpl( this.onEditDone.bind(this), this.adoptedContent, + this.autoborderHandler, ); this.mergeHandler = new MergeHandlerImpl( this.onMergeDone.bind(this), @@ -712,8 +725,13 @@ export class CanvasViewImpl implements CanvasView, Listener { this.geometry = this.controller.geometry; if (reason === UpdateReasons.CONFIG_UPDATED) { this.configuration = model.configuration; - this.setupObjects([]); - this.setupObjects(model.objects); + this.editHandler.configurate(this.configuration); + this.drawHandler.configurate(this.configuration); + + // todo: setup text, add if doesn't exist and enabled + // remove if exist and not enabled + // this.setupObjects([]); + // this.setupObjects(model.objects); } else if (reason === UpdateReasons.BITMAP) { const { imageBitmap } = model; if (imageBitmap) { @@ -960,6 +978,8 @@ export class CanvasViewImpl implements CanvasView, Listener { attributes: { ...state.attributes }, zOrder: state.zOrder, pinned: state.pinned, + updated: state.updated, + frame: state.frame, }; } @@ -969,9 +989,9 @@ export class CanvasViewImpl implements CanvasView, Listener { const drawnState = this.drawnStates[clientID]; const shape = this.svgShapes[state.clientID]; const text = this.svgTexts[state.clientID]; + const isInvisible = state.hidden || state.outside; if (drawnState.hidden !== state.hidden || drawnState.outside !== state.outside) { - const isInvisible = state.hidden || state.outside; if (isInvisible) { (state.shapeType === 'points' ? shape.remember('_selectHandler').nested : shape) .style('display', 'none'); @@ -1041,7 +1061,7 @@ export class CanvasViewImpl implements CanvasView, Listener { (shape as any).clear(); shape.attr('points', stringified); - if (state.shapeType === 'points') { + if (state.shapeType === 'points' && !isInvisible) { this.selectize(false, shape); this.setupPoints(shape as SVG.PolyLine, state); } @@ -1049,7 +1069,7 @@ export class CanvasViewImpl implements CanvasView, Listener { } for (const attrID of Object.keys(state.attributes)) { - if (state.attributes[attrID] !== drawnState.attributes[attrID]) { + if (state.attributes[attrID] !== drawnState.attributes[+attrID]) { if (text) { const [span] = text.node .querySelectorAll(`[attrID="${attrID}"]`) as any as SVGTSpanElement[]; @@ -1187,7 +1207,7 @@ export class CanvasViewImpl implements CanvasView, Listener { (shape as any).off('resizestart'); (shape as any).off('resizing'); (shape as any).off('resizedone'); - (shape as any).resize(false); + (shape as any).resize('stop'); // TODO: Hide text only if it is hidden by settings const text = this.svgTexts[clientID]; @@ -1537,12 +1557,15 @@ export class CanvasViewImpl implements CanvasView, Listener { .addClass('cvat_canvas_shape').attr({ clientID: state.clientID, id: `cvat_canvas_shape_${state.clientID}`, + 'data-polyline-id': basicPolyline.attr('id'), 'data-z-order': state.zOrder, }); group.on('click.canvas', (event: MouseEvent): void => { // Need to redispatch the event on another element basicPolyline.fire(new MouseEvent('click', event)); + // redispatch event to canvas to be able merge points clicking them + this.content.dispatchEvent(new MouseEvent('click', event)); }); group.bbox = basicPolyline.bbox.bind(basicPolyline); diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts index a2c401c5..5878c884 100644 --- a/cvat-canvas/src/typescript/drawHandler.ts +++ b/cvat-canvas/src/typescript/drawHandler.ts @@ -7,11 +7,7 @@ import consts from './consts'; import 'svg.draw.js'; import './svg.patch'; -import { - DrawData, - Geometry, - RectDrawingMethod, -} from './canvasModel'; +import { AutoborderHandler } from './autoborderHandler'; import { translateToSVG, @@ -23,7 +19,15 @@ import { Box, } from './shared'; +import { + DrawData, + Geometry, + RectDrawingMethod, + Configuration, +} from './canvasModel'; + export interface DrawHandler { + configurate(configuration: Configuration): void; draw(drawData: DrawData, geometry: Geometry): void; transform(geometry: Geometry): void; cancel(): void; @@ -45,6 +49,8 @@ export class DrawHandlerImpl implements DrawHandler { }; private drawData: DrawData; private geometry: Geometry; + private autoborderHandler: AutoborderHandler; + private autobordersEnabled: boolean; // we should use any instead of SVG.Shape because svg plugins cannot change declared interface // so, methods like draw() just undefined for SVG.Shape, but nevertheless they exist @@ -127,6 +133,7 @@ export class DrawHandlerImpl implements DrawHandler { return; } + this.autoborderHandler.autoborder(false); this.initialized = false; this.canvas.off('mousedown.draw'); this.canvas.off('mouseup.draw'); @@ -334,6 +341,9 @@ export class DrawHandlerImpl implements DrawHandler { }); this.drawPolyshape(); + if (this.autobordersEnabled) { + this.autoborderHandler.autoborder(true, this.drawInstance, false); + } } private drawPolyline(): void { @@ -344,6 +354,9 @@ export class DrawHandlerImpl implements DrawHandler { }); this.drawPolyshape(); + if (this.autobordersEnabled) { + this.autoborderHandler.autoborder(true, this.drawInstance, false); + } } private drawPoints(): void { @@ -599,7 +612,10 @@ export class DrawHandlerImpl implements DrawHandler { onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean) => void, canvas: SVG.Container, text: SVG.Container, + autoborderHandler: AutoborderHandler, ) { + this.autoborderHandler = autoborderHandler; + this.autobordersEnabled = false; this.startTimestamp = Date.now(); this.onDrawDone = onDrawDone; this.canvas = canvas; @@ -629,6 +645,19 @@ export class DrawHandlerImpl implements DrawHandler { }); } + public configurate(configuration: Configuration): void { + if (typeof (configuration.autoborders) === 'boolean') { + this.autobordersEnabled = configuration.autoborders; + if (this.drawInstance) { + if (this.autobordersEnabled) { + this.autoborderHandler.autoborder(true, this.drawInstance, false); + } else { + this.autoborderHandler.autoborder(false); + } + } + } + } + public transform(geometry: Geometry): void { this.geometry = geometry; diff --git a/cvat-canvas/src/typescript/editHandler.ts b/cvat-canvas/src/typescript/editHandler.ts index ba0ed7f6..41d03edc 100644 --- a/cvat-canvas/src/typescript/editHandler.ts +++ b/cvat-canvas/src/typescript/editHandler.ts @@ -6,29 +6,27 @@ import * as SVG from 'svg.js'; import 'svg.select.js'; import consts from './consts'; -import { - translateFromSVG, - pointsToArray, -} from './shared'; -import { - EditData, - Geometry, -} from './canvasModel'; +import { translateFromSVG, pointsToArray } from './shared'; +import { EditData, Geometry, Configuration } from './canvasModel'; +import { AutoborderHandler } from './autoborderHandler'; export interface EditHandler { edit(editData: EditData): void; transform(geometry: Geometry): void; + configurate(configuration: Configuration): void; cancel(): void; } export class EditHandlerImpl implements EditHandler { private onEditDone: (state: any, points: number[]) => void; + private autoborderHandler: AutoborderHandler; private geometry: Geometry; private canvas: SVG.Container; private editData: EditData; private editedShape: SVG.Shape; private editLine: SVG.PolyLine; private clones: SVG.Polygon[]; + private autobordersEnabled: boolean; private startEdit(): void { // get started coordinates @@ -77,6 +75,8 @@ export class EditHandlerImpl implements EditHandler { (this.editLine as any).addClass('cvat_canvas_shape_drawing').style({ 'pointer-events': 'none', 'fill-opacity': 0, + }).attr({ + 'data-origin-client-id': this.editData.state.clientID, }).on('drawstart drawpoint', (e: CustomEvent): void => { this.transform(this.geometry); lastDrawnPoint.x = e.detail.event.clientX; @@ -89,6 +89,9 @@ export class EditHandlerImpl implements EditHandler { } this.setupEditEvents(); + if (this.autobordersEnabled) { + this.autoborderHandler.autoborder(true, this.editLine, true); + } } private setupEditEvents(): void { @@ -273,6 +276,7 @@ export class EditHandlerImpl implements EditHandler { this.canvas.off('mousedown.edit'); this.canvas.off('mouseup.edit'); this.canvas.off('mousemove.edit'); + this.autoborderHandler.autoborder(false); if (this.editedShape) { this.setupPoints(false); @@ -314,7 +318,10 @@ export class EditHandlerImpl implements EditHandler { public constructor( onEditDone: (state: any, points: number[]) => void, canvas: SVG.Container, + autoborderHandler: AutoborderHandler, ) { + this.autoborderHandler = autoborderHandler; + this.autobordersEnabled = false; this.onEditDone = onEditDone; this.canvas = canvas; this.editData = null; @@ -343,6 +350,19 @@ export class EditHandlerImpl implements EditHandler { this.onEditDone(null, null); } + public configurate(configuration: Configuration): void { + if (typeof (configuration.autoborders) === 'boolean') { + this.autobordersEnabled = configuration.autoborders; + if (this.editLine) { + if (this.autobordersEnabled) { + this.autoborderHandler.autoborder(true, this.editLine, true); + } else { + this.autoborderHandler.autoborder(false); + } + } + } + } + public transform(geometry: Geometry): void { this.geometry = geometry; diff --git a/cvat-canvas/src/typescript/shared.ts b/cvat-canvas/src/typescript/shared.ts index 2a1136ca..f230b60d 100644 --- a/cvat-canvas/src/typescript/shared.ts +++ b/cvat-canvas/src/typescript/shared.ts @@ -25,6 +25,21 @@ export interface BBox { y: number; } +export interface DrawnState { + clientID: number; + outside?: boolean; + occluded?: boolean; + hidden?: boolean; + lock: boolean; + shapeType: string; + points?: number[]; + attributes: Record; + zOrder?: number; + pinned?: boolean; + updated: number; + frame: number; +} + // Translate point array from the canvas coordinate system // to the coordinate system of a client export function translateFromSVG(svg: SVGSVGElement, points: number[]): number[] { diff --git a/cvat-canvas/src/typescript/svg.patch.ts b/cvat-canvas/src/typescript/svg.patch.ts index 1c532105..642add62 100644 --- a/cvat-canvas/src/typescript/svg.patch.ts +++ b/cvat-canvas/src/typescript/svg.patch.ts @@ -16,7 +16,9 @@ SVG.Element.prototype.draw = function constructor(...args: any): any { if (!handler) { originalDraw.call(this, ...args); handler = this.remember('_paintHandler'); - if (!handler.set) { + // There is use case (drawing a single point when handler is created and destructed immediately in one stack) + // So, we need to check if handler still exists + if (handler && !handler.set) { handler.set = new SVG.Set(); } } else { diff --git a/cvat-core/src/annotations-collection.js b/cvat-core/src/annotations-collection.js index e3ba2735..e09e6f47 100644 --- a/cvat-core/src/annotations-collection.js +++ b/cvat-core/src/annotations-collection.js @@ -150,6 +150,7 @@ } for (const shape of data.shapes) { + if (shape.type === 'cuboid') continue; const clientID = ++this.count; const shapeModel = shapeFactory(shape, clientID, this.injection); this.shapes[shapeModel.frame] = this.shapes[shapeModel.frame] || []; @@ -871,8 +872,10 @@ // In particular consider first and last frame as keyframes for all frames const statesData = [].concat( (frame in this.shapes ? this.shapes[frame] : []) + .filter((shape) => !shape.removed) .map((shape) => shape.get(frame)), (frame in this.tags ? this.tags[frame] : []) + .filter((tag) => !tag.removed) .map((tag) => tag.get(frame)), ); const tracks = Object.values(this.tracks) @@ -880,7 +883,7 @@ frame in track.shapes || frame === frameFrom || frame === frameTo - )); + )).filter((track) => !track.removed); statesData.push( ...tracks.map((track) => track.get(frame)) .filter((state) => !state.outside), diff --git a/cvat-core/src/annotations-objects.js b/cvat-core/src/annotations-objects.js index 47c65fb9..9620d2c4 100644 --- a/cvat-core/src/annotations-objects.js +++ b/cvat-core/src/annotations-objects.js @@ -184,8 +184,10 @@ this.history.do(HistoryActions.CHANGED_LOCK, () => { this.lock = undoLock; + this.updated = Date.now(); }, () => { this.lock = redoLock; + this.updated = Date.now(); }, [this.clientID], frame); this.lock = lock; @@ -197,8 +199,10 @@ this.history.do(HistoryActions.CHANGED_COLOR, () => { this.color = undoColor; + this.updated = Date.now(); }, () => { this.color = redoColor; + this.updated = Date.now(); }, [this.clientID], frame); this.color = color; @@ -210,8 +214,10 @@ this.history.do(HistoryActions.CHANGED_HIDDEN, () => { this.hidden = undoHidden; + this.updated = Date.now(); }, () => { this.hidden = redoHidden; + this.updated = Date.now(); }, [this.clientID], frame); this.hidden = hidden; @@ -229,9 +235,11 @@ this.history.do(HistoryActions.CHANGED_LABEL, () => { this.label = undoLabel; this.attributes = undoAttributes; + this.updated = Date.now(); }, () => { this.label = redoLabel; this.attributes = redoAttributes; + this.updated = Date.now(); }, [this.clientID], frame); } @@ -246,8 +254,10 @@ this.history.do(HistoryActions.CHANGED_ATTRIBUTES, () => { this.attributes = undoAttributes; + this.updated = Date.now(); }, () => { this.attributes = redoAttributes; + this.updated = Date.now(); }, [this.clientID], frame); } @@ -373,9 +383,12 @@ this.removed = true; this.history.do(HistoryActions.REMOVED_OBJECT, () => { + this.serverID = undefined; this.removed = false; + this.updated = Date.now(); }, () => { this.removed = true; + this.updated = Date.now(); }, [this.clientID], frame); } @@ -398,8 +411,10 @@ this.history.do(HistoryActions.CHANGED_PINNED, () => { this.pinned = undoPinned; + this.updated = Date.now(); }, () => { this.pinned = redoPinned; + this.updated = Date.now(); }, [this.clientID], frame); this.pinned = pinned; @@ -489,8 +504,10 @@ this.history.do(HistoryActions.CHANGED_POINTS, () => { this.points = undoPoints; + this.updated = Date.now(); }, () => { this.points = redoPoints; + this.updated = Date.now(); }, [this.clientID], frame); this.points = points; @@ -502,8 +519,10 @@ this.history.do(HistoryActions.CHANGED_OCCLUDED, () => { this.occluded = undoOccluded; + this.updated = Date.now(); }, () => { this.occluded = redoOccluded; + this.updated = Date.now(); }, [this.clientID], frame); this.occluded = occluded; @@ -515,8 +534,10 @@ this.history.do(HistoryActions.CHANGED_ZORDER, () => { this.zOrder = undoZOrder; + this.updated = Date.now(); }, () => { this.zOrder = redoZOrder; + this.updated = Date.now(); }, [this.clientID], frame); this.zOrder = zOrder; @@ -777,12 +798,14 @@ for (const mutable of undoAttributes.mutable) { this.shapes[mutable.frame].attributes = mutable.attributes; } + this.updated = Date.now(); }, () => { this.label = redoLabel; this.attributes = redoAttributes.unmutable; for (const mutable of redoAttributes.mutable) { this.shapes[mutable.frame].attributes = mutable.attributes; } + this.updated = Date.now(); }, [this.clientID], frame); } @@ -853,11 +876,13 @@ } else if (redoShape) { delete this.shapes[frame]; } + this.updated = Date.now(); }, () => { this.attributes = redoAttributes; if (redoShape) { this.shapes[frame] = redoShape; } + this.updated = Date.now(); }, [this.clientID], frame); } @@ -868,12 +893,14 @@ } else { this.shapes[frame] = undoShape; } + this.updated = Date.now(); }, () => { if (!redoShape) { delete this.shapes[frame]; } else { this.shapes[frame] = redoShape; } + this.updated = Date.now(); }, [this.clientID], frame); } diff --git a/cvat-core/src/log.js b/cvat-core/src/log.js index 68cce5c5..cf3df86d 100644 --- a/cvat-core/src/log.js +++ b/cvat-core/src/log.js @@ -195,12 +195,11 @@ class LogWithExceptionInfo extends Log { } dump() { - const payload = { ...this.payload }; + let body = super.dump(); + const payload = body.payload; const client = detect(); - const body = { - client_id: payload.client_id, - name: this.type, - time: this.time.toISOString(), + body = { + ...body, message: payload.message, filename: payload.filename, line: payload.line, @@ -211,17 +210,13 @@ class LogWithExceptionInfo extends Log { version: client.version, }; - delete payload.client_id; delete payload.message; delete payload.filename; delete payload.line; delete payload.column; delete payload.stack; - return { - ...body, - payload, - }; + return body; } } diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 46989db3..8f24bb65 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -923,7 +923,9 @@ export function getJobAsync( dispatch({ type: AnnotationActionTypes.GET_JOB, - payload: {}, + payload: { + requestedId: jid, + }, }); const loadJobEvent = await logger.log( diff --git a/cvat-ui/src/actions/plugins-actions.ts b/cvat-ui/src/actions/plugins-actions.ts index 9e88b055..36175683 100644 --- a/cvat-ui/src/actions/plugins-actions.ts +++ b/cvat-ui/src/actions/plugins-actions.ts @@ -33,6 +33,7 @@ export function checkPluginsAsync(): ThunkAction { GIT_INTEGRATION: false, TF_ANNOTATION: false, TF_SEGMENTATION: false, + DEXTR_SEGMENTATION: false, }; const promises: Promise[] = [ @@ -41,15 +42,12 @@ export function checkPluginsAsync(): ThunkAction { PluginChecker.check(SupportedPlugins.GIT_INTEGRATION), PluginChecker.check(SupportedPlugins.TF_ANNOTATION), PluginChecker.check(SupportedPlugins.TF_SEGMENTATION), + PluginChecker.check(SupportedPlugins.DEXTR_SEGMENTATION), ]; const values = await Promise.all(promises); - [plugins.ANALYTICS] = values; - [, plugins.AUTO_ANNOTATION] = values; - [,, plugins.GIT_INTEGRATION] = values; - [,,, plugins.TF_ANNOTATION] = values; - [,,,, plugins.TF_SEGMENTATION] = values; - + [plugins.ANALYTICS, plugins.AUTO_ANNOTATION, plugins.GIT_INTEGRATION, + plugins.TF_ANNOTATION, plugins.TF_SEGMENTATION, plugins.DEXTR_SEGMENTATION] = values; dispatch(pluginActions.checkedAllPlugins(plugins)); }; } diff --git a/cvat-ui/src/actions/settings-actions.ts b/cvat-ui/src/actions/settings-actions.ts index f06b0fb0..9a157967 100644 --- a/cvat-ui/src/actions/settings-actions.ts +++ b/cvat-ui/src/actions/settings-actions.ts @@ -28,6 +28,7 @@ export enum SettingsActionTypes { SWITCH_AUTO_SAVE = 'SWITCH_AUTO_SAVE', CHANGE_AUTO_SAVE_INTERVAL = 'CHANGE_AUTO_SAVE_INTERVAL', CHANGE_AAM_ZOOM_MARGIN = 'CHANGE_AAM_ZOOM_MARGIN', + SWITCH_AUTOMATIC_BORDERING = 'SWITCH_AUTOMATIC_BORDERING', SWITCH_SHOWNIG_INTERPOLATED_TRACKS = 'SWITCH_SHOWNIG_INTERPOLATED_TRACKS', SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS = 'SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS', } @@ -220,3 +221,12 @@ export function switchShowingObjectsTextAlways(showObjectsTextAlways: boolean): }, }; } + +export function switchAutomaticBordering(automaticBordering: boolean): AnyAction { + return { + type: SettingsActionTypes.SWITCH_AUTOMATIC_BORDERING, + payload: { + automaticBordering, + }, + }; +} diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx index db5b6500..529ea77e 100644 --- a/cvat-ui/src/components/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx @@ -36,7 +36,17 @@ export default function AnnotationPageComponent(props: Props): JSX.Element { useEffect(() => { saveLogs(); - return saveLogs; + const root = window.document.getElementById('root'); + if (root) { + root.style.minHeight = '768px'; + } + + return () => { + saveLogs(); + if (root) { + root.style.minHeight = ''; + } + }; }, []); if (job === null) { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx index 8296f287..0c6e22cf 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -62,7 +62,9 @@ interface Props { aamZoomMargin: number; showObjectsTextAlways: boolean; workspace: Workspace; + automaticBordering: boolean; keyMap: Record; + switchableAutomaticBordering: boolean; onSetupCanvas: () => void; onDragCanvas: (enabled: boolean) => void; onZoomCanvas: (enabled: boolean) => void; @@ -89,11 +91,13 @@ interface Props { onChangeGridOpacity(opacity: number): void; onChangeGridColor(color: GridColor): void; onSwitchGrid(enabled: boolean): void; + onSwitchAutomaticBordering(enabled: boolean): void; } export default class CanvasWrapperComponent extends React.PureComponent { public componentDidMount(): void { const { + automaticBordering, showObjectsTextAlways, canvasInstance, curZLayer, @@ -106,6 +110,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { wrapper.appendChild(canvasInstance.html()); canvasInstance.configure({ + autoborders: automaticBordering, undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE, displayAllText: showObjectsTextAlways, }); @@ -139,12 +144,16 @@ export default class CanvasWrapperComponent extends React.PureComponent { workspace, frameFetching, showObjectsTextAlways, + automaticBordering, } = this.props; - if (prevProps.showObjectsTextAlways !== showObjectsTextAlways) { + if (prevProps.showObjectsTextAlways !== showObjectsTextAlways + || prevProps.automaticBordering !== automaticBordering + ) { canvasInstance.configure({ undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE, displayAllText: showObjectsTextAlways, + autoborders: automaticBordering, }); } @@ -160,9 +169,6 @@ export default class CanvasWrapperComponent extends React.PureComponent { if (prevProps.activatedStateID !== null && prevProps.activatedStateID !== activatedStateID) { canvasInstance.activate(null); - } - - if (activatedStateID) { const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`); if (el) { (el as any).instance.fill({ opacity: opacity / 100 }); @@ -265,7 +271,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.html().removeEventListener('canvas.groupped', this.onCanvasObjectsGroupped); canvasInstance.html().removeEventListener('canvas.splitted', this.onCanvasTrackSplitted); - canvasInstance.html().removeEventListener('point.contextmenu', this.onCanvasPointContextMenu); + canvasInstance.html().removeEventListener('canvas.contextmenu', this.onCanvasPointContextMenu); window.removeEventListener('resize', this.fitCanvas); } @@ -686,7 +692,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.html().addEventListener('canvas.groupped', this.onCanvasObjectsGroupped); canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted); - canvasInstance.html().addEventListener('point.contextmenu', this.onCanvasPointContextMenu); + canvasInstance.html().addEventListener('canvas.contextmenu', this.onCanvasPointContextMenu); } public render(): JSX.Element { @@ -699,16 +705,19 @@ export default class CanvasWrapperComponent extends React.PureComponent { brightnessLevel, contrastLevel, saturationLevel, + keyMap, grid, gridColor, gridOpacity, + switchableAutomaticBordering, + automaticBordering, onChangeBrightnessLevel, onChangeSaturationLevel, onChangeContrastLevel, onChangeGridColor, onChangeGridOpacity, onSwitchGrid, - keyMap, + onSwitchAutomaticBordering, } = this.props; const preventDefault = (event: KeyboardEvent | undefined): void => { @@ -727,8 +736,10 @@ export default class CanvasWrapperComponent extends React.PureComponent { INCREASE_GRID_OPACITY: keyMap.INCREASE_GRID_OPACITY, DECREASE_GRID_OPACITY: keyMap.DECREASE_GRID_OPACITY, CHANGE_GRID_COLOR: keyMap.CHANGE_GRID_COLOR, + SWITCH_AUTOMATIC_BORDERING: keyMap.SWITCH_AUTOMATIC_BORDERING, }; + const step = 10; const handlers = { INCREASE_BRIGHTNESS: (event: KeyboardEvent | undefined) => { @@ -803,6 +814,12 @@ export default class CanvasWrapperComponent extends React.PureComponent { const color = colors[indexOf >= colors.length ? 0 : indexOf]; onChangeGridColor(color); }, + SWITCH_AUTOMATIC_BORDERING: (event: KeyboardEvent | undefined) => { + if (switchableAutomaticBordering) { + preventDefault(event); + onSwitchAutomaticBordering(!automaticBordering); + } + }, }; return ( diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/dextr-plugin.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/dextr-plugin.tsx new file mode 100644 index 00000000..1215b33e --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/dextr-plugin.tsx @@ -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 ? ( + + { + setActivated(event.target.checked); + if (event.target.checked) { + activate(canvasInstance); + } else { + deactivate(canvasInstance); + } + }} + > + Make AI polygon + + + ) : null + ); +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(DEXTRPlugin); + +// TODO: Add dialog window with cancel button diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx index a32f3ba8..c940c39b 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx @@ -3,7 +3,6 @@ // SPDX-License-Identifier: MIT import React from 'react'; - import { Row, Col } from 'antd/lib/grid'; import Select from 'antd/lib/select'; import Button from 'antd/lib/button'; @@ -15,6 +14,7 @@ import Text from 'antd/lib/typography/Text'; import { RectDrawingMethod } from 'cvat-canvas'; import { ShapeType } from 'reducers/interfaces'; import { clamp } from 'utils/math'; +import DEXTRPlugin from './dextr-plugin'; interface Props { shapeType: ShapeType; @@ -47,6 +47,9 @@ function DrawShapePopoverComponent(props: Props): JSX.Element { onChangeRectDrawingMethod, } = props; + const trackDisabled = shapeType === ShapeType.POLYGON || shapeType === ShapeType.POLYLINE + || (shapeType === ShapeType.POINTS && numberOfPoints !== 1); + return (
@@ -78,6 +81,9 @@ function DrawShapePopoverComponent(props: Props): JSX.Element { + { + shapeType === ShapeType.POLYGON && + } { shapeType === ShapeType.RECTANGLE ? ( <> @@ -148,7 +154,7 @@ function DrawShapePopoverComponent(props: Props): JSX.Element { diff --git a/cvat-ui/src/components/create-model-page/create-model-content.tsx b/cvat-ui/src/components/create-model-page/create-model-content.tsx index 03efda5f..a320b805 100644 --- a/cvat-ui/src/components/create-model-page/create-model-content.tsx +++ b/cvat-ui/src/components/create-model-page/create-model-content.tsx @@ -3,20 +3,16 @@ // SPDX-License-Identifier: MIT import React from 'react'; - -import { - Row, - Col, - Icon, - Alert, - Button, - Tooltip, - message, - notification, -} from 'antd'; - +import { Row, Col } from 'antd/lib/grid'; +import Icon from 'antd/lib/icon'; +import Alert from 'antd/lib/alert'; +import Button from 'antd/lib/button'; +import Tooltip from 'antd/lib/tooltip'; +import message from 'antd/lib/message'; +import notification from 'antd/lib/notification'; import Text from 'antd/lib/typography/Text'; +import consts from 'consts'; import ConnectedFileManager, { FileManagerContainer, } from 'containers/file-manager/file-manager'; @@ -107,7 +103,7 @@ export default class CreateModelContent extends React.PureComponent { const status = modelCreatingStatus && modelCreatingStatus !== 'CREATED' ? modelCreatingStatus : ''; - const guideLink = 'https://github.com/opencv/cvat/blob/develop/cvat/apps/auto_annotation/README.md'; + const { AUTO_ANNOTATION_GUIDE_URL } = consts; return ( @@ -116,7 +112,7 @@ export default class CreateModelContent extends React.PureComponent { onClick={(): void => { // false positive // eslint-disable-next-line - window.open(guideLink, '_blank'); + window.open(AUTO_ANNOTATION_GUIDE_URL, '_blank'); }} type='question-circle' /> diff --git a/cvat-ui/src/components/create-task-page/create-task-page.tsx b/cvat-ui/src/components/create-task-page/create-task-page.tsx index 79618410..439439f9 100644 --- a/cvat-ui/src/components/create-task-page/create-task-page.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-page.tsx @@ -3,30 +3,65 @@ // SPDX-License-Identifier: MIT import './styles.scss'; -import React from 'react'; - -import { - Row, - Col, -} from 'antd'; - +import React, { useEffect } from 'react'; +import { Row, Col } from 'antd/lib/grid'; +import Modal from 'antd/lib/modal'; import Text from 'antd/lib/typography/Text'; +import Paragraph from 'antd/lib/typography/Paragraph'; +import TextArea from 'antd/lib/input/TextArea'; import CreateTaskContent, { CreateTaskData } from './create-task-content'; + interface Props { onCreate: (data: CreateTaskData) => void; status: string; + error: string; installedGit: boolean; } export default function CreateTaskPage(props: Props): JSX.Element { const { + error, status, onCreate, installedGit, } = props; + useEffect(() => { + if (error) { + let errorCopy = error; + const sshKeys: string[] = []; + while (errorCopy.length) { + const startIndex = errorCopy.search(/'ssh/); + if (startIndex === -1) break; + let sshKey = errorCopy.slice(startIndex + 1); + const stopIndex = sshKey.search(/'/); + sshKey = sshKey.slice(0, stopIndex); + sshKeys.push(sshKey); + errorCopy = errorCopy.slice(stopIndex + 1); + } + + if (sshKeys.length) { + Modal.error({ + width: 800, + title: 'Could not clone the repository', + content: ( + <> + + Please make sure it exists and you have access + + + Consider adding the following public ssh keys to git: + +