diff --git a/CHANGELOG.md b/CHANGELOG.md index 27d2e821..eb69400e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.0.alpha] - 2020-02-XX +## [1.0.0-beta] - Unreleased +### Added +- + +### Changed +- VOC task export now does not use official label map by default, but takes one + from the source task to avoid primary-class and class part name + clashing ([#1275](https://github.com/opencv/cvat/issues/1275)) + +### Deprecated +- + +### Removed +- + +### Fixed +- + +### Security +- Bump acorn from 6.3.0 to 6.4.1 in /cvat-ui ([#1270](https://github.com/opencv/cvat/pull/1270)) + +## [0.6.0] - 2020-03-15 ### Added - Server only support for projects. Extend REST API v1 (/api/v1/projects*) - Ability to get basic information about users without admin permissions ([#750](https://github.com/opencv/cvat/issues/750)) @@ -28,12 +49,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Yolov3 interpretation script fix and changes to mapping.json - YOLO format support ([#1151](https://github.com/opencv/cvat/pull/1151)) -### Deprecated -- - -### Removed -- - ### Fixed - Exception in Git plugin [#826](https://github.com/opencv/cvat/issues/826) - Label ids in TFrecord format now start from 1 [#866](https://github.com/opencv/cvat/issues/866) @@ -42,8 +57,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Output labels for VOC format can be specified with Datumaro [#942](https://github.com/opencv/cvat/issues/942) - Annotations can be filtered before dumping with Datumaro [#994](https://github.com/opencv/cvat/issues/994) -### Security -- +## [0.5.2] - 2019-12-15 +### Fixed +- Frozen version of scikit-image==0.15 in requirements.txt because next releases don't support Python 3.5 ## [0.5.1] - 2019-10-17 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8cb2d483..40faceb7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ Next steps should work on clear Ubuntu 18.04. - Install necessary dependencies: ```sh -$ sudo apt-get update && apt-get --no-install-recommends install -y nodejs npm curl redis-server python3-dev python3-pip python3-venv libldap2-dev libsasl2-dev +$ sudo apt-get update && sudo apt-get --no-install-recommends install -y ffmpeg build-essential nodejs npm curl redis-server python3-dev python3-pip python3-venv libldap2-dev libsasl2-dev ``` - Install [Visual Studio Code](https://code.visualstudio.com/docs/setup/linux#_debian-and-ubuntu-based-distributions) @@ -28,7 +28,7 @@ git clone https://github.com/opencv/cvat cd cvat && mkdir logs keys python3 -m venv .env . .env/bin/activate -pip install -U pip wheel +pip install -U pip wheel setuptools pip install -r cvat/requirements/development.txt pip install -r datumaro/requirements.txt python manage.py migrate diff --git a/cvat-canvas/package-lock.json b/cvat-canvas/package-lock.json index 46c12b74..022e4582 100644 --- a/cvat-canvas/package-lock.json +++ b/cvat-canvas/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "0.1.0", + "version": "0.5.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1203,9 +1203,9 @@ } }, "acorn": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.1.tgz", - "integrity": "sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", "dev": true }, "acorn-jsx": { diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 1ea3bdd6..9068f32d 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -264,9 +264,10 @@ export class CanvasViewImpl implements CanvasView, Listener { y + height / 2, ]); + const canvasOffset = this.canvas.getBoundingClientRect(); const [cx, cy] = [ - this.canvas.clientWidth / 2 + this.canvas.offsetLeft, - this.canvas.clientHeight / 2 + this.canvas.offsetTop, + this.canvas.clientWidth / 2 + canvasOffset.left, + this.canvas.clientHeight / 2 + canvasOffset.top, ]; const dragged = { @@ -748,7 +749,7 @@ export class CanvasViewImpl implements CanvasView, Listener { if (object) { const bbox: SVG.BBox = object.bbox(); this.onFocusRegion(bbox.x - padding, bbox.y - padding, - bbox.width + padding, bbox.height + padding); + bbox.width + padding * 2, bbox.height + padding * 2); } } else if (reason === UpdateReasons.SHAPE_ACTIVATED) { this.activate(this.controller.activeElement); @@ -1037,7 +1038,26 @@ export class CanvasViewImpl implements CanvasView, Listener { this.content.prepend(...sorted.map((pair): SVGElement => pair[0])); } - private deactivate(): void { + private deactivateAttribute(): void { + const { clientID, attributeID } = this.activeElement; + if (clientID !== null && attributeID !== null) { + const text = this.svgTexts[clientID]; + if (text) { + const [span] = text.node + .querySelectorAll(`[attrID="${attributeID}"]`) as any as SVGTSpanElement[]; + if (span) { + span.style.fill = ''; + } + } + + this.activeElement = { + ...this.activeElement, + attributeID: null, + }; + } + } + + private deactivateShape(): void { if (this.activeElement.clientID !== null) { const { clientID } = this.activeElement; const drawnState = this.drawnStates[clientID]; @@ -1070,29 +1090,34 @@ export class CanvasViewImpl implements CanvasView, Listener { this.sortObjects(); this.activeElement = { + ...this.activeElement, clientID: null, - attributeID: null, }; } } - private activate(activeElement: ActiveElement): void { - // Check if other element have been already activated - if (this.activeElement.clientID !== null) { - // Check if it is the same element - if (this.activeElement.clientID === activeElement.clientID) { - return; - } + private deactivate(): void { + this.deactivateAttribute(); + this.deactivateShape(); + } - // Deactivate previous element - this.deactivate(); - } + private activateAttribute(clientID: number, attributeID: number): void { + const text = this.svgTexts[clientID]; + if (text) { + const [span] = text.node + .querySelectorAll(`[attrID="${attributeID}"]`) as any as SVGTSpanElement[]; + if (span) { + span.style.fill = 'red'; + } - const { clientID } = activeElement; - if (clientID === null) { - return; + this.activeElement = { + ...this.activeElement, + attributeID, + }; } + } + private activateShape(clientID: number): void { const [state] = this.controller.objects .filter((_state: any): boolean => _state.clientID === clientID); @@ -1105,8 +1130,8 @@ export class CanvasViewImpl implements CanvasView, Listener { return; } - this.activeElement = { ...activeElement }; const shape = this.svgShapes[clientID]; + let text = this.svgTexts[clientID]; if (!text) { text = this.addText(state); @@ -1211,6 +1236,11 @@ export class CanvasViewImpl implements CanvasView, Listener { } }); + this.activeElement = { + ...this.activeElement, + clientID, + }; + this.canvas.dispatchEvent(new CustomEvent('canvas.activated', { bubbles: false, cancelable: true, @@ -1220,6 +1250,30 @@ export class CanvasViewImpl implements CanvasView, Listener { })); } + private activate(activeElement: ActiveElement): void { + // Check if another element have been already activated + if (this.activeElement.clientID !== null) { + if (this.activeElement.clientID !== activeElement.clientID) { + // Deactivate previous shape and attribute + this.deactivate(); + } else if (this.activeElement.attributeID !== activeElement.attributeID) { + this.deactivateAttribute(); + } + } + + const { clientID, attributeID } = activeElement; + if (clientID !== null && this.activeElement.clientID !== clientID) { + this.activateShape(clientID); + } + + if (clientID !== null + && attributeID !== null + && this.activeElement.attributeID !== attributeID + ) { + this.activateAttribute(clientID, attributeID); + } + } + // Update text position after corresponding box has been moved, resized, etc. private updateTextPosition(text: SVG.Text, shape: SVG.Shape): void { let box = (shape.node as any).getBBox(); diff --git a/cvat-core/src/annotations-collection.js b/cvat-core/src/annotations-collection.js index a3e49f15..309d387d 100644 --- a/cvat-core/src/annotations-collection.js +++ b/cvat-core/src/annotations-collection.js @@ -802,7 +802,9 @@ let minimumState = null; for (const state of objectStates) { checkObjectType('object state', state, null, ObjectState); - if (state.outside || state.hidden) continue; + if (state.outside || state.hidden || state.objectType === ObjectType.TAG) { + continue; + } const object = this.objects[state.clientID]; if (typeof (object) === 'undefined') { @@ -810,9 +812,9 @@ 'The object has not been saved yet. Call annotations.put([state]) before', ); } - const distance = object.constructor.distance(state.points, x, y); - if (distance !== null && (minimumDistance === null || distance < minimumDistance)) { + if (distance !== null && (minimumDistance === null + || distance < minimumDistance)) { minimumDistance = distance; minimumState = state; } diff --git a/cvat-core/src/annotations-filter.js b/cvat-core/src/annotations-filter.js index 62530167..8a00cab2 100644 --- a/cvat-core/src/annotations-filter.js +++ b/cvat-core/src/annotations-filter.js @@ -8,7 +8,10 @@ */ const jsonpath = require('jsonpath'); -const { AttributeType } = require('./enums'); +const { + AttributeType, + ObjectType, +} = require('./enums'); const { ArgumentError } = require('./exceptions'); @@ -165,18 +168,21 @@ class AnnotationsFilter { let xbr = Number.MIN_SAFE_INTEGER; let ytl = Number.MAX_SAFE_INTEGER; let ybr = Number.MIN_SAFE_INTEGER; + let [width, height] = [null, null]; + + if (state.objectType !== ObjectType.TAG) { + state.points.forEach((coord, idx) => { + if (idx % 2) { // y + ytl = Math.min(ytl, coord); + ybr = Math.max(ybr, coord); + } else { // x + xtl = Math.min(xtl, coord); + xbr = Math.max(xbr, coord); + } + }); + [width, height] = [xbr - xtl, ybr - ytl]; + } - state.points.forEach((coord, idx) => { - if (idx % 2) { // y - ytl = Math.min(ytl, coord); - ybr = Math.max(ybr, coord); - } else { // x - xtl = Math.min(xtl, coord); - xbr = Math.max(xbr, coord); - } - }); - - const [width, height] = [xbr - xtl, ybr - ytl]; const attributes = {}; Object.keys(state.attributes).reduce((acc, key) => { const attr = labelAttributes[key]; diff --git a/cvat-core/src/annotations-objects.js b/cvat-core/src/annotations-objects.js index 65a30b56..245d8e37 100644 --- a/cvat-core/src/annotations-objects.js +++ b/cvat-core/src/annotations-objects.js @@ -1139,6 +1139,7 @@ attributes: { ...this.attributes }, label: this.label, group: this.groupObject, + color: this.color, updated: this.updated, frame, }; @@ -1171,6 +1172,10 @@ this._saveLock(data.lock); } + if (updated.color) { + this._saveColor(data.color); + } + this.updateTimestamp(updated); updated.reset(); diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 828118cf..f2453993 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1421,9 +1421,9 @@ } }, "acorn": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", - "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", "dev": true }, "acorn-jsx": { @@ -4293,14 +4293,6 @@ "acorn": "^7.1.0", "acorn-jsx": "^5.1.0", "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "acorn": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", - "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", - "dev": true - } } }, "esprima": { @@ -12354,6 +12346,12 @@ "webpack-sources": "^1.4.1" }, "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 916ddb7c..2652cee3 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -19,12 +19,20 @@ import { FrameSpeed, Rotation, ContextMenuType, + Workspace, } from 'reducers/interfaces'; import getCore from 'cvat-core'; import { RectDrawingMethod } from 'cvat-canvas'; import { getCVATStore } from 'cvat-store'; +interface AnnotationsParameters { + filters: string[]; + frame: number; + showAllInterpolationTracks: boolean; + jobInstance: any; +} + const cvat = getCore(); let store: null | Store = null; @@ -35,19 +43,37 @@ function getStore(): Store { return store; } -function receiveAnnotationsParameters(): -{ filters: string[]; frame: number; showAllInterpolationTracks: boolean } { +function receiveAnnotationsParameters(): AnnotationsParameters { if (store === null) { store = getCVATStore(); } const state: CombinedState = getStore().getState(); - const { filters } = state.annotation.annotations; - const frame = state.annotation.player.frame.number; - const { showAllInterpolationTracks } = state.settings.workspace; + const { + annotation: { + annotations: { + filters, + }, + player: { + frame: { + number: frame, + }, + }, + job: { + instance: jobInstance, + }, + }, + settings: { + workspace: { + showAllInterpolationTracks, + }, + }, + } = state; + return { filters, frame, + jobInstance, showAllInterpolationTracks, }; } @@ -85,10 +111,10 @@ export enum AnnotationActionTypes { COPY_SHAPE = 'COPY_SHAPE', PASTE_SHAPE = 'PASTE_SHAPE', EDIT_SHAPE = 'EDIT_SHAPE', - DRAW_SHAPE = 'DRAW_SHAPE', REPEAT_DRAW_SHAPE = 'REPEAT_DRAW_SHAPE', SHAPE_DRAWN = 'SHAPE_DRAWN', RESET_CANVAS = 'RESET_CANVAS', + REMEMBER_CREATED_OBJECT = 'REMEMBER_CREATED_OBJECT', UPDATE_ANNOTATIONS_SUCCESS = 'UPDATE_ANNOTATIONS_SUCCESS', UPDATE_ANNOTATIONS_FAILED = 'UPDATE_ANNOTATIONS_FAILED', CREATE_ANNOTATIONS_SUCCESS = 'CREATE_ANNOTATIONS_SUCCESS', @@ -139,11 +165,22 @@ export enum AnnotationActionTypes { SWITCH_Z_LAYER = 'SWITCH_Z_LAYER', ADD_Z_LAYER = 'ADD_Z_LAYER', SEARCH_ANNOTATIONS_FAILED = 'SEARCH_ANNOTATIONS_FAILED', + CHANGE_WORKSPACE = 'CHANGE_WORKSPACE', +} + +export function changeWorkspace(workspace: Workspace): AnyAction { + return { + type: AnnotationActionTypes.CHANGE_WORKSPACE, + payload: { + workspace, + }, + }; } export function addZLayer(): AnyAction { return { type: AnnotationActionTypes.ADD_Z_LAYER, + payload: {}, }; } @@ -156,12 +193,17 @@ export function switchZLayer(cur: number): AnyAction { }; } -export function fetchAnnotationsAsync(sessionInstance: any): +export function fetchAnnotationsAsync(): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { - const { filters, frame, showAllInterpolationTracks } = receiveAnnotationsParameters(); - const states = await sessionInstance.annotations + const { + filters, + frame, + showAllInterpolationTracks, + jobInstance, + } = receiveAnnotationsParameters(); + const states = await jobInstance.annotations .get(frame, showAllInterpolationTracks, filters); const [minZ, maxZ] = computeZRange(states); @@ -566,11 +608,15 @@ export function selectObjects(selectedStatesID: number[]): AnyAction { }; } -export function activateObject(activatedStateID: number | null): AnyAction { +export function activateObject( + activatedStateID: number | null, + activatedAttributeID: number | null, +): AnyAction { return { type: AnnotationActionTypes.ACTIVATE_OBJECT, payload: { activatedStateID, + activatedAttributeID, }, }; } @@ -850,15 +896,17 @@ ThunkAction, {}, {}, AnyAction> { }; } -export function drawShape( - shapeType: ShapeType, - labelID: number, +export function rememberObject( objectType: ObjectType, + labelID: number, + shapeType?: ShapeType, points?: number, rectDrawingMethod?: RectDrawingMethod, ): AnyAction { - let activeControl = ActiveControl.DRAW_RECTANGLE; - if (shapeType === ShapeType.POLYGON) { + let activeControl = ActiveControl.CURSOR; + if (shapeType === ShapeType.RECTANGLE) { + activeControl = ActiveControl.DRAW_RECTANGLE; + } else if (shapeType === ShapeType.POLYGON) { activeControl = ActiveControl.DRAW_POLYGON; } else if (shapeType === ShapeType.POLYLINE) { activeControl = ActiveControl.DRAW_POLYLINE; @@ -867,7 +915,7 @@ export function drawShape( } return { - type: AnnotationActionTypes.DRAW_SHAPE, + type: AnnotationActionTypes.REMEMBER_CREATED_OBJECT, payload: { shapeType, labelID, @@ -913,19 +961,26 @@ export function splitTrack(enabled: boolean): AnyAction { }; } -export function updateAnnotationsAsync(sessionInstance: any, frame: number, statesToUpdate: any[]): +export function updateAnnotationsAsync(statesToUpdate: any[]): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { + const { + jobInstance, + filters, + frame, + showAllInterpolationTracks, + } = receiveAnnotationsParameters(); + try { if (statesToUpdate.some((state: any): boolean => state.updateFlags.zOrder)) { // deactivate object to visualize changes immediately (UX) - dispatch(activateObject(null)); + dispatch(activateObject(null, null)); } const promises = statesToUpdate .map((objectState: any): Promise => objectState.save()); const states = await Promise.all(promises); - const history = await sessionInstance.actions.get(); + const history = await jobInstance.actions.get(); const [minZ, maxZ] = computeZRange(states); dispatch({ @@ -938,8 +993,7 @@ ThunkAction, {}, {}, AnyAction> { }, }); } catch (error) { - const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters(); - const states = await sessionInstance.annotations + const states = await jobInstance.annotations .get(frame, showAllInterpolationTracks, filters); dispatch({ type: AnnotationActionTypes.UPDATE_ANNOTATIONS_FAILED, @@ -1117,8 +1171,6 @@ export function changeLabelColorAsync( } export function changeGroupColorAsync( - sessionInstance: any, - frameNumber: number, group: number, color: string, ): ThunkAction, {}, {}, AnyAction> { @@ -1128,9 +1180,9 @@ export function changeGroupColorAsync( .filter((_state: any): boolean => _state.group.id === group); if (groupStates.length) { groupStates[0].group.color = color; - dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, groupStates)); + dispatch(updateAnnotationsAsync(groupStates)); } else { - dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, [])); + dispatch(updateAnnotationsAsync([])); } }; } @@ -1160,12 +1212,28 @@ export function searchAnnotationsAsync( export function pasteShapeAsync(): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { - const initialState = getStore().getState().annotation.drawing.activeInitialState; - const { instance: canvasInstance } = getStore().getState().annotation.canvas; + const { + canvas: { + instance: canvasInstance, + }, + job: { + instance: jobInstance, + }, + player: { + frame: { + number: frameNumber, + }, + }, + drawing: { + activeInitialState: initialState, + }, + } = getStore().getState().annotation; if (initialState) { - let activeControl = ActiveControl.DRAW_RECTANGLE; - if (initialState.shapeType === ShapeType.POINTS) { + let activeControl = ActiveControl.CURSOR; + if (initialState.shapeType === ShapeType.RECTANGLE) { + activeControl = ActiveControl.DRAW_RECTANGLE; + } else if (initialState.shapeType === ShapeType.POINTS) { activeControl = ActiveControl.DRAW_POINTS; } else if (initialState.shapeType === ShapeType.POLYGON) { activeControl = ActiveControl.DRAW_POLYGON; @@ -1181,10 +1249,20 @@ export function pasteShapeAsync(): ThunkAction, {}, {}, AnyAction> }); canvasInstance.cancel(); - canvasInstance.draw({ - enabled: true, - initialState, - }); + if (initialState.objectType === ObjectType.TAG) { + const objectState = new cvat.classes.ObjectState({ + objectType: ObjectType.TAG, + label: initialState.label, + attributes: initialState.attributes, + frame: frameNumber, + }); + dispatch(createAnnotationsAsync(jobInstance, frameNumber, [objectState])); + } else { + canvasInstance.draw({ + enabled: true, + initialState, + }); + } } }; } @@ -1192,20 +1270,36 @@ export function pasteShapeAsync(): ThunkAction, {}, {}, AnyAction> export function repeatDrawShapeAsync(): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { const { - activeShapeType, - activeNumOfPoints, - activeRectDrawingMethod, - } = getStore().getState().annotation.drawing; - - const { instance: canvasInstance } = getStore().getState().annotation.canvas; + canvas: { + instance: canvasInstance, + }, + job: { + labels, + instance: jobInstance, + }, + player: { + frame: { + number: frameNumber, + }, + }, + drawing: { + activeObjectType, + activeLabelID, + activeShapeType, + activeNumOfPoints, + activeRectDrawingMethod, + }, + } = getStore().getState().annotation; - let activeControl = ActiveControl.DRAW_RECTANGLE; - if (activeShapeType === ShapeType.POLYGON) { + let activeControl = ActiveControl.CURSOR; + if (activeShapeType === ShapeType.RECTANGLE) { + activeControl = ActiveControl.DRAW_RECTANGLE; + } else if (activeShapeType === ShapeType.POINTS) { + activeControl = ActiveControl.DRAW_POINTS; + } else if (activeShapeType === ShapeType.POLYGON) { activeControl = ActiveControl.DRAW_POLYGON; } else if (activeShapeType === ShapeType.POLYLINE) { activeControl = ActiveControl.DRAW_POLYLINE; - } else if (activeShapeType === ShapeType.POINTS) { - activeControl = ActiveControl.DRAW_POINTS; } dispatch({ @@ -1216,12 +1310,21 @@ export function repeatDrawShapeAsync(): ThunkAction, {}, {}, AnyAc }); canvasInstance.cancel(); - canvasInstance.draw({ - enabled: true, - rectDrawingMethod: activeRectDrawingMethod, - numberOfPoints: activeNumOfPoints, - shapeType: activeShapeType, - crosshair: activeShapeType === ShapeType.RECTANGLE, - }); + if (activeObjectType === ObjectType.TAG) { + const objectState = new cvat.classes.ObjectState({ + objectType: ObjectType.TAG, + label: labels.filter((label: any) => label.id === activeLabelID)[0], + frame: frameNumber, + }); + dispatch(createAnnotationsAsync(jobInstance, frameNumber, [objectState])); + } else { + canvasInstance.draw({ + enabled: true, + rectDrawingMethod: activeRectDrawingMethod, + numberOfPoints: activeNumOfPoints, + shapeType: activeShapeType, + crosshair: activeShapeType === ShapeType.RECTANGLE, + }); + } }; } diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx index 3ea1c87c..a096afd2 100644 --- a/cvat-ui/src/components/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx @@ -11,14 +11,17 @@ import { Result, } from 'antd'; +import { Workspace } from 'reducers/interfaces'; import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar'; import StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal'; import StandardWorkspaceComponent from './standard-workspace/standard-workspace'; +import AttributeAnnotationWorkspace from './attribute-annotation-workspace/attribute-annotation-workspace'; interface Props { job: any | null | undefined; fetching: boolean; getJob(): void; + workspace: Workspace; } @@ -27,9 +30,9 @@ export default function AnnotationPageComponent(props: Props): JSX.Element { job, fetching, getJob, + workspace, } = props; - if (job === null) { if (!fetching) { getJob(); @@ -51,8 +54,18 @@ export default function AnnotationPageComponent(props: Props): JSX.Element { return ( - - + + + + { workspace === Workspace.STANDARD ? ( + + + + ) : ( + + + + )} ); diff --git a/cvat-ui/src/components/annotation-page/annotations-filters-input.tsx b/cvat-ui/src/components/annotation-page/annotations-filters-input.tsx new file mode 100644 index 00000000..39ff37b6 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/annotations-filters-input.tsx @@ -0,0 +1,92 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import { connect } from 'react-redux'; +import Select, { SelectValue, LabeledValue } from 'antd/lib/select'; +import Icon from 'antd/lib/icon'; + +import { + changeAnnotationsFilters as changeAnnotationsFiltersAction, + fetchAnnotationsAsync, +} from 'actions/annotation-actions'; +import { CombinedState } from 'reducers/interfaces'; + +interface StateToProps { + annotationsFilters: string[]; + annotationsFiltersHistory: string[]; +} + +interface DispatchToProps { + changeAnnotationsFilters(value: SelectValue): void; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { + annotation: { + annotations: { + filters: annotationsFilters, + filtersHistory: annotationsFiltersHistory, + }, + }, + } = state; + + return { + annotationsFilters, + annotationsFiltersHistory, + }; +} + +function mapDispatchToProps(dispatch: any): DispatchToProps { + return { + changeAnnotationsFilters(value: SelectValue) { + if (typeof (value) === 'string') { + dispatch(changeAnnotationsFiltersAction([value])); + dispatch(fetchAnnotationsAsync()); + } else if (Array.isArray(value) + && value.every((element: string | number | LabeledValue): boolean => ( + typeof (element) === 'string' + )) + ) { + dispatch(changeAnnotationsFiltersAction(value as string[])); + dispatch(fetchAnnotationsAsync()); + } + }, + }; +} + +function AnnotationsFiltersInput(props: StateToProps & DispatchToProps): JSX.Element { + const { + annotationsFilters, + annotationsFiltersHistory, + changeAnnotationsFilters, + } = props; + + return ( + + ); +} + + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(AnnotationsFiltersInput); diff --git a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx new file mode 100644 index 00000000..d19b94ef --- /dev/null +++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx @@ -0,0 +1,300 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React, { useState, useEffect } from 'react'; +import { GlobalHotKeys, KeyMap } from 'react-hotkeys'; +import { connect } from 'react-redux'; +import Layout, { SiderProps } from 'antd/lib/layout'; +import { SelectValue } from 'antd/lib/select'; +import { CheckboxChangeEvent } from 'antd/lib/checkbox'; +import { Row, Col } from 'antd/lib/grid'; +import Text from 'antd/lib/typography/Text'; + +import { + activateObject as activateObjectAction, + updateAnnotationsAsync, +} from 'actions/annotation-actions'; +import { CombinedState } from 'reducers/interfaces'; +import AnnotationsFiltersInput from 'components/annotation-page/annotations-filters-input'; +import ObjectSwitcher from './object-switcher'; +import AttributeSwitcher from './attribute-switcher'; +import ObjectBasicsEditor from './object-basics-edtior'; +import AttributeEditor from './attribute-editor'; + + +interface StateToProps { + activatedStateID: number | null; + activatedAttributeID: number | null; + states: any[]; + labels: any[]; +} + +interface DispatchToProps { + activateObject(clientID: number | null, attrID: number | null): void; + updateAnnotations(statesToUpdate: any[]): void; +} + +interface LabelAttrMap { + [index: number]: any; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { + annotation: { + annotations: { + activatedStateID, + activatedAttributeID, + states, + }, + job: { + labels, + }, + }, + } = state; + + return { + labels, + activatedStateID, + activatedAttributeID, + states, + }; +} + +function mapDispatchToProps(dispatch: any): DispatchToProps { + return { + activateObject(clientID: number, attrID: number): void { + dispatch(activateObjectAction(clientID, attrID)); + }, + updateAnnotations(states): void { + dispatch(updateAnnotationsAsync(states)); + }, + }; +} + +function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Element { + const { + labels, + states, + activatedStateID, + activatedAttributeID, + updateAnnotations, + activateObject, + } = props; + + const [labelAttrMap, setLabelAttrMap] = useState( + labels.reduce((acc, label): LabelAttrMap => { + acc[label.id] = label.attributes.length ? label.attributes[0] : null; + return acc; + }, {}), + ); + + const [activeObjectState] = activatedStateID === null + ? [null] : states.filter((objectState: any): boolean => ( + objectState.clientID === activatedStateID + )); + const activeAttribute = activeObjectState + ? labelAttrMap[activeObjectState.label.id] + : null; + + if (activeObjectState) { + const attribute = labelAttrMap[activeObjectState.label.id]; + if (attribute && attribute.id !== activatedAttributeID) { + activateObject(activatedStateID, attribute ? attribute.id : null); + } + } else if (states.length) { + const attribute = labelAttrMap[states[0].label.id]; + activateObject(states[0].clientID, attribute ? attribute.id : null); + } + + const nextObject = (step: number): void => { + if (states.length) { + const index = states.indexOf(activeObjectState); + let nextIndex = index + step; + if (nextIndex > states.length - 1) { + nextIndex = 0; + } else if (nextIndex < 0) { + nextIndex = states.length - 1; + } + if (nextIndex !== index) { + const attribute = labelAttrMap[states[nextIndex].label.id]; + activateObject(states[nextIndex].clientID, attribute ? attribute.id : null); + } + } + }; + + const nextAttribute = (step: number): void => { + if (activeObjectState) { + const { label } = activeObjectState; + const { attributes } = label; + if (attributes.length) { + const index = attributes.indexOf(activeAttribute); + let nextIndex = index + step; + if (nextIndex > attributes.length - 1) { + nextIndex = 0; + } else if (nextIndex < 0) { + nextIndex = attributes.length - 1; + } + if (index !== nextIndex) { + const updatedLabelAttrMap = { ...labelAttrMap }; + updatedLabelAttrMap[label.id] = attributes[nextIndex]; + setLabelAttrMap(updatedLabelAttrMap); + } + } + } + }; + + useEffect(() => { + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + }, []); + + const siderProps: SiderProps = { + className: 'attribute-annotation-sidebar', + theme: 'light', + width: 300, + collapsedWidth: 0, + reverseArrow: true, + collapsible: true, + trigger: null, + }; + + const keyMap = { + NEXT_ATTRIBUTE: { + name: 'Next attribute', + description: 'Go to the next attribute', + sequence: 'ArrowDown', + action: 'keydown', + }, + PREVIOUS_ATTRIBUTE: { + name: 'Previous attribute', + description: 'Go to the previous attribute', + sequence: 'ArrowUp', + action: 'keydown', + }, + NEXT_OBJECT: { + name: 'Next object', + description: 'Go to the next object', + sequence: 'Tab', + action: 'keydown', + }, + PREVIOUS_OBJECT: { + name: 'Previous object', + description: 'Go to the previous object', + sequence: 'Shift+Tab', + action: 'keydown', + }, + }; + + const handlers = { + NEXT_ATTRIBUTE: (event: KeyboardEvent | undefined) => { + if (event) { + event.preventDefault(); + } + + nextAttribute(1); + }, + PREVIOUS_ATTRIBUTE: (event: KeyboardEvent | undefined) => { + if (event) { + event.preventDefault(); + } + + nextAttribute(-1); + }, + NEXT_OBJECT: (event: KeyboardEvent | undefined) => { + if (event) { + event.preventDefault(); + } + + nextObject(1); + }, + PREVIOUS_OBJECT: (event: KeyboardEvent | undefined) => { + if (event) { + event.preventDefault(); + } + + nextObject(-1); + }, + }; + + if (activeObjectState) { + return ( + + + + + + + + + { + const labelName = value as string; + const [newLabel] = labels + .filter((_label): boolean => _label.name === labelName); + activeObjectState.label = newLabel; + updateAnnotations([activeObjectState]); + }} + setOccluded={(event: CheckboxChangeEvent): void => { + activeObjectState.occluded = event.target.checked; + updateAnnotations([activeObjectState]); + }} + /> + { + activeAttribute + ? ( + <> + + { + const { attributes } = activeObjectState; + attributes[activeAttribute.id] = value; + activeObjectState.attributes = attributes; + updateAnnotations([activeObjectState]); + }} + /> + + + ) : ( +
+ No attributes found +
+ ) + } +
+ ); + } + + return ( + +
+ No objects found +
+
+ ); +} + + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(AttributeAnnotationSidebar); diff --git a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-editor.tsx b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-editor.tsx new file mode 100644 index 00000000..5b9150a2 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-editor.tsx @@ -0,0 +1,281 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import { GlobalHotKeys, KeyMap } from 'react-hotkeys'; +import Text from 'antd/lib/typography/Text'; +import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox'; +import Select, { SelectValue } from 'antd/lib/select'; +import Radio, { RadioChangeEvent } from 'antd/lib/radio'; +import Input from 'antd/lib/input'; +import InputNumber from 'antd/lib/input-number'; + +interface InputElementParameters { + attrID: number; + inputType: string; + values: string[]; + currentValue: string; + onChange(value: string): void; + ref: React.RefObject; +} + +function renderInputElement(parameters: InputElementParameters): JSX.Element { + const { + inputType, + attrID, + values, + currentValue, + onChange, + ref, + } = parameters; + + const renderCheckbox = (): JSX.Element => ( + <> + Checkbox: +
+ ( + onChange(event.target.checked ? 'true' : 'false') + )} + checked={currentValue === 'true'} + /> +
+ + ); + + const renderSelect = (): JSX.Element => ( + <> + Values: +
+ +
+ + ); + + const renderRadio = (): JSX.Element => ( + <> + Values: +
+ ( + onChange(event.target.value) + )} + > + {values.map((value: string): JSX.Element => ( + {value} + ))} + +
+ + ); + + const handleKeydown = (event: React.KeyboardEvent): void => { + if (['ArrowDown', 'ArrowUp', 'ArrowLeft', + 'ArrowRight', 'Tab', 'Shift', 'Control'] + .includes(event.key) + ) { + event.preventDefault(); + const copyEvent = new KeyboardEvent('keydown', event); + window.document.dispatchEvent(copyEvent); + } + }; + + const renderText = (): JSX.Element => ( + <> + {inputType === 'number' ? Number: : Text: } +
+ ) => { + const { value } = event.target; + if (inputType === 'number') { + if (value !== '') { + const numberValue = +value; + if (!Number.isNaN(numberValue)) { + onChange(`${numberValue}`); + } + } + } else { + onChange(value); + } + }} + onKeyDown={handleKeydown} + ref={ref as React.RefObject} + /> +
+ + ); + + let element = null; + if (inputType === 'checkbox') { + element = renderCheckbox(); + } else if (inputType === 'select') { + element = renderSelect(); + } else if (inputType === 'radio') { + element = renderRadio(); + } else { + element = renderText(); + } + + return ( +
+ {element} +
+ ); +} + +interface ListParameters { + inputType: string; + values: string[]; + onChange(value: string): void; +} + +function renderList(parameters: ListParameters): JSX.Element | null { + const { inputType, values, onChange } = parameters; + + if (inputType === 'checkbox') { + const sortedValues = ['true', 'false']; + if (values[0].toLowerCase() !== 'true') { + sortedValues.reverse(); + } + + const keyMap: KeyMap = {}; + const handlers: { + [key: string]: (keyEvent?: KeyboardEvent) => void; + } = {}; + + sortedValues.forEach((value: string, index: number): void => { + const key = `SET_${index}_VALUE`; + keyMap[key] = { + name: `Set value "${value}"`, + description: `Change current value for the attribute to "${value}"`, + sequence: `${index}`, + action: 'keydown', + }; + + handlers[key] = (event: KeyboardEvent | undefined) => { + if (event) { + event.preventDefault(); + } + + onChange(value); + }; + }); + + return ( +
+ +
+ 0: + {` ${sortedValues[0]}`} +
+
+ 1: + {` ${sortedValues[1]}`} +
+
+ ); + } + + if (inputType === 'radio' || inputType === 'select') { + const keyMap: KeyMap = {}; + const handlers: { + [key: string]: (keyEvent?: KeyboardEvent) => void; + } = {}; + + values.slice(0, 10).forEach((value: string, index: number): void => { + const key = `SET_${index}_VALUE`; + keyMap[key] = { + name: `Set value "${value}"`, + description: `Change current value for the attribute to "${value}"`, + sequence: `${index}`, + action: 'keydown', + }; + + handlers[key] = (event: KeyboardEvent | undefined) => { + if (event) { + event.preventDefault(); + } + + onChange(value); + }; + }); + + return ( +
+ + {values.map((value: string, index: number): JSX.Element => ( +
+ {`${index}:`} + {` ${value}`} +
+ ))} +
+ ); + } + + if (inputType === 'number') { + return ( +
+
+ From: + {` ${values[0]}`} +
+
+ To: + {` ${values[1]}`} +
+
+ Step: + {` ${values[2]}`} +
+
+ ); + } + + return null; +} + +interface Props { + attribute: any; + currentValue: string; + onChange(value: string): void; +} + +function AttributeEditor(props: Props): JSX.Element { + const { attribute, currentValue, onChange } = props; + const { inputType, values, id: attrID } = attribute; + const ref = inputType === 'number' ? React.createRef() + : React.createRef(); + + return ( +
+ {renderList({ values, inputType, onChange })} +
+ {renderInputElement({ + attrID, + ref, + inputType, + currentValue, + values, + onChange, + })} +
+ ); +} + +export default React.memo(AttributeEditor); diff --git a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-switcher.tsx b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-switcher.tsx new file mode 100644 index 00000000..2ad4a6d7 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-switcher.tsx @@ -0,0 +1,43 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import Icon from 'antd/lib/icon'; +import Text from 'antd/lib/typography/Text'; +import Tooltip from 'antd/lib/tooltip'; +import Button from 'antd/lib/button'; + +interface Props { + currentAttribute: string; + currentIndex: number; + attributesCount: number; + nextAttribute(step: number): void; +} + +function AttributeSwitcher(props: Props): JSX.Element { + const { + currentAttribute, + currentIndex, + attributesCount, + nextAttribute, + } = props; + + const title = `${currentAttribute} [${currentIndex + 1}/${attributesCount}]`; + return ( +
+ + + {currentAttribute} + {` [${currentIndex + 1}/${attributesCount}]`} + + +
+ ); +} + +export default React.memo(AttributeSwitcher); diff --git a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/object-basics-edtior.tsx b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/object-basics-edtior.tsx new file mode 100644 index 00000000..17a689a9 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/object-basics-edtior.tsx @@ -0,0 +1,43 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import Select, { SelectValue } from 'antd/lib/select'; +import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox'; + +interface Props { + currentLabel: string; + labels: any[]; + occluded: boolean; + setOccluded(event: CheckboxChangeEvent): void; + changeLabel(value: SelectValue): void; +} + +function ObjectBasicsEditor(props: Props): JSX.Element { + const { + currentLabel, + occluded, + labels, + setOccluded, + changeLabel, + } = props; + + return ( +
+ + Occluded +
+ ); +} + +export default React.memo(ObjectBasicsEditor); diff --git a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/object-switcher.tsx b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/object-switcher.tsx new file mode 100644 index 00000000..9341396f --- /dev/null +++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/object-switcher.tsx @@ -0,0 +1,48 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import Icon from 'antd/lib/icon'; +import Text from 'antd/lib/typography/Text'; +import Tooltip from 'antd/lib/tooltip'; +import Button from 'antd/lib/button'; + +interface Props { + currentLabel: string; + clientID: number; + occluded: boolean; + objectsCount: number; + currentIndex: number; + nextObject(step: number): void; +} + +function ObjectSwitcher(props: Props): JSX.Element { + const { + currentLabel, + clientID, + objectsCount, + currentIndex, + nextObject, + } = props; + + + const title = `${currentLabel} ${clientID} [${currentIndex + 1}/${objectsCount}]`; + return ( +
+ + + {currentLabel} + {` ${clientID} `} + {`[${currentIndex + 1}/${objectsCount}]`} + + +
+ ); +} + +export default React.memo(ObjectSwitcher); diff --git a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-workspace.tsx b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-workspace.tsx new file mode 100644 index 00000000..d4e5fd8d --- /dev/null +++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-workspace.tsx @@ -0,0 +1,19 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React from 'react'; +import Layout from 'antd/lib/layout'; + +import CanvasWrapperContainer from 'containers/annotation-page/standard-workspace/canvas-wrapper'; +import AttributeAnnotationSidebar from './attribute-annotation-sidebar/attribute-annotation-sidebar'; + +export default function AttributeAnnotationWorkspace(): JSX.Element { + return ( + + + + + ); +} diff --git a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/styles.scss b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/styles.scss new file mode 100644 index 00000000..1f8de841 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/styles.scss @@ -0,0 +1,66 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +@import 'base.scss'; + +.attribute-annotation-workspace.ant-layout { + height: 100%; +} + +.attribute-annotation-sidebar { + background: $background-color-2; + padding: 5px; +} + +.attribute-annotation-sidebar-switcher { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 18px; + margin-top: 10px; + + > span { + max-width: 60%; + text-overflow: ellipsis; + overflow: hidden; + } + + > button > i { + color: $objects-bar-icons-color; + } +} + +.attribute-annotation-sidebar-basics-editor { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 18px; + margin: 10px 0px; +} + +.attribute-annotations-sidebar-not-found-wrapper { + margin-top: 20px; + text-align: center; +} + +.attribute-annotation-sidebar-attr-list-wrapper { + margin: 10px 0px 10px 10px; +} + + +.attribute-annotation-sidebar-attr-elem-wrapper { + display: inline-block; + width: 60%; +} + +.attribute-annotation-sidebar-number-list { + display: flex; + justify-content: space-around; +} + +.attribute-annotation-sidebar-attr-editor { + display: flex; + align-items: center; + justify-content: space-around; +} 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 bbb0aab2..f74e2c2c 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 @@ -18,9 +18,16 @@ import { GridColor, ObjectType, ContextMenuType, + Workspace } from 'reducers/interfaces'; import { Canvas } from 'cvat-canvas'; import getCore from 'cvat-core'; +import { + ColorBy, + GridColor, + ObjectType, + Workspace, +} from 'reducers/interfaces'; const cvat = getCore(); @@ -31,6 +38,7 @@ interface Props { canvasInstance: Canvas; jobInstance: any; activatedStateID: number | null; + activatedAttributeID: number | null; selectedStatesID: number[]; annotations: any[]; frameData: any; @@ -55,6 +63,8 @@ interface Props { resetZoom: boolean; contextVisible: boolean; contextType: ContextMenuType; + aamZoomMargin: number; + workspace: Workspace; onSetupCanvas: () => void; onDragCanvas: (enabled: boolean) => void; onZoomCanvas: (enabled: boolean) => void; @@ -64,7 +74,7 @@ interface Props { onEditShape: (enabled: boolean) => void; onShapeDrawn: () => void; onResetCanvas: () => void; - onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void; + onUpdateAnnotations(states: any[]): void; onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void; onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void; onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void; @@ -120,6 +130,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { brightnessLevel, contrastLevel, saturationLevel, + workspace, } = this.props; if (prevProps.sidebarCollapsed !== sidebarCollapsed) { @@ -168,11 +179,18 @@ export default class CanvasWrapperComponent extends React.PureComponent { } } + if (prevProps.curZLayer !== curZLayer) { + canvasInstance.setZLayer(curZLayer); + } + if (prevProps.annotations !== annotations || prevProps.frameData !== frameData) { this.updateCanvas(); } - if (prevProps.frame !== frameData.number && resetZoom) { + if (prevProps.frame !== frameData.number + && resetZoom + && workspace !== Workspace.ATTRIBUTE_ANNOTATION + ) { canvasInstance.html().addEventListener('canvas.setup', () => { canvasInstance.fit(); }, { once: true }); @@ -183,10 +201,6 @@ export default class CanvasWrapperComponent extends React.PureComponent { this.updateShapesView(); } - if (prevProps.curZLayer !== curZLayer) { - canvasInstance.setZLayer(curZLayer); - } - if (prevProps.frameAngle !== frameAngle) { canvasInstance.rotate(frameAngle); } @@ -195,10 +209,34 @@ export default class CanvasWrapperComponent extends React.PureComponent { } public componentWillUnmount(): void { + const { canvasInstance } = this.props; + + canvasInstance.html().removeEventListener('mousedown', this.onCanvasMouseDown); + canvasInstance.html().removeEventListener('click', this.onCanvasClicked); + canvasInstance.html().removeEventListener('contextmenu', this.onCanvasContextMenu); + canvasInstance.html().removeEventListener('canvas.editstart', this.onCanvasEditStart); + canvasInstance.html().removeEventListener('canvas.edited', this.onCanvasEditDone); + canvasInstance.html().removeEventListener('canvas.dragstart', this.onCanvasDragStart); + canvasInstance.html().removeEventListener('canvas.dragstop', this.onCanvasDragDone); + canvasInstance.html().removeEventListener('canvas.zoomstart', this.onCanvasZoomStart); + canvasInstance.html().removeEventListener('canvas.zoomstop', this.onCanvasZoomDone); + + canvasInstance.html().removeEventListener('canvas.setup', this.onCanvasSetup); + canvasInstance.html().removeEventListener('canvas.canceled', this.onCanvasCancel); + canvasInstance.html().removeEventListener('canvas.find', this.onCanvasFindObject); + canvasInstance.html().removeEventListener('canvas.deactivated', this.onCanvasShapeDeactivated); + canvasInstance.html().removeEventListener('canvas.moved', this.onCanvasCursorMoved); + + canvasInstance.html().removeEventListener('canvas.clicked', this.onCanvasShapeClicked); + canvasInstance.html().removeEventListener('canvas.drawn', this.onCanvasShapeDrawn); + canvasInstance.html().removeEventListener('canvas.merged', this.onCanvasObjectsMerged); + canvasInstance.html().removeEventListener('canvas.groupped', this.onCanvasObjectsGroupped); + canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted); + window.removeEventListener('resize', this.fitCanvas); } - private onShapeDrawn(event: any): void { + private onCanvasShapeDrawn = (event: any): void => { const { jobInstance, activeLabelID, @@ -229,27 +267,9 @@ export default class CanvasWrapperComponent extends React.PureComponent { state.frame = frame; const objectState = new cvat.classes.ObjectState(state); onCreateAnnotations(jobInstance, frame, [objectState]); - } - - private onShapeEdited(event: any): void { - const { - jobInstance, - frame, - onEditShape, - onUpdateAnnotations, - } = this.props; - - onEditShape(false); - - const { - state, - points, - } = event.detail; - state.points = points; - onUpdateAnnotations(jobInstance, frame, [state]); - } + }; - private onObjectsMerged(event: any): void { + private onCanvasObjectsMerged = (event: any): void => { const { jobInstance, frame, @@ -261,9 +281,9 @@ export default class CanvasWrapperComponent extends React.PureComponent { const { states } = event.detail; onMergeAnnotations(jobInstance, frame, states); - } + }; - private onObjectsGroupped(event: any): void { + private onCanvasObjectsGroupped = (event: any): void => { const { jobInstance, frame, @@ -275,9 +295,9 @@ export default class CanvasWrapperComponent extends React.PureComponent { const { states } = event.detail; onGroupAnnotations(jobInstance, frame, states); - } + }; - private onTrackSplitted(event: any): void { + private onCanvasTrackSplitted = (event: any): void => { const { jobInstance, frame, @@ -289,22 +309,179 @@ export default class CanvasWrapperComponent extends React.PureComponent { const { state } = event.detail; onSplitAnnotations(jobInstance, frame, state); - } + }; private fitCanvas = (): void => { const { canvasInstance } = this.props; canvasInstance.fitCanvas(); }; + private onCanvasMouseDown = (e: MouseEvent): void => { + const { workspace, activatedStateID, onActivateObject } = this.props; + + if ((e.target as HTMLElement).tagName === 'svg') { + if (activatedStateID !== null && workspace !== Workspace.ATTRIBUTE_ANNOTATION) { + onActivateObject(null); + } + } + }; + + private onCanvasClicked = (): void => { + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + }; + + private onCanvasContextMenu = (e: MouseEvent): void => { + const { activatedStateID, onUpdateContextMenu } = this.props; + onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY); + }; + + private onCanvasShapeClicked = (e: any): void => { + const { clientID } = e.detail.state; + const sidebarItem = window.document + .getElementById(`cvat-objects-sidebar-state-item-${clientID}`); + if (sidebarItem) { + sidebarItem.scrollIntoView(); + } + }; + + private onCanvasShapeDeactivated = (e: any): void => { + const { onActivateObject, activatedStateID } = this.props; + const { state } = e.detail; + + // when we activate element, canvas deactivates the previous + // and triggers this event + // in this case we do not need to update our state + if (state.clientID === activatedStateID) { + onActivateObject(null); + } + }; + + private onCanvasCursorMoved = async (event: any): Promise => { + const { + jobInstance, + activatedStateID, + workspace, + onActivateObject, + } = this.props; + + if (workspace === Workspace.ATTRIBUTE_ANNOTATION) { + return; + } + + const result = await jobInstance.annotations.select( + event.detail.states, + event.detail.x, + event.detail.y, + ); + + if (result && result.state) { + if (result.state.shapeType === 'polyline' || result.state.shapeType === 'points') { + if (result.distance > MAX_DISTANCE_TO_OPEN_SHAPE) { + return; + } + } + + if (activatedStateID !== result.state.clientID) { + onActivateObject(result.state.clientID); + } + } + }; + + private onCanvasEditStart = (): void => { + const { onActivateObject, onEditShape } = this.props; + onActivateObject(null); + onEditShape(true); + }; + + private onCanvasEditDone = (event: any): void => { + const { + onEditShape, + onUpdateAnnotations, + } = this.props; + + onEditShape(false); + + const { + state, + points, + } = event.detail; + state.points = points; + onUpdateAnnotations([state]); + }; + + private onCanvasDragStart = (): void => { + const { onDragCanvas } = this.props; + onDragCanvas(true); + }; + + private onCanvasDragDone = (): void => { + const { onDragCanvas } = this.props; + onDragCanvas(false); + }; + + private onCanvasZoomStart = (): void => { + const { onZoomCanvas } = this.props; + onZoomCanvas(true); + }; + + private onCanvasZoomDone = (): void => { + const { onZoomCanvas } = this.props; + onZoomCanvas(false); + }; + + private onCanvasSetup = (): void => { + const { onSetupCanvas } = this.props; + onSetupCanvas(); + this.updateShapesView(); + this.activateOnCanvas(); + }; + + private onCanvasCancel = (): void => { + const { onResetCanvas } = this.props; + onResetCanvas(); + }; + + private onCanvasFindObject = async (e: any): Promise => { + const { jobInstance, canvasInstance } = this.props; + + const result = await jobInstance.annotations + .select(e.detail.states, e.detail.x, e.detail.y); + + if (result && result.state) { + if (result.state.shapeType === 'polyline' || result.state.shapeType === 'points') { + if (result.distance > MAX_DISTANCE_TO_OPEN_SHAPE) { + return; + } + } + + canvasInstance.select(result.state); + } + }; + private activateOnCanvas(): void { const { activatedStateID, + activatedAttributeID, canvasInstance, selectedOpacity, + aamZoomMargin, + workspace, + annotations, } = this.props; if (activatedStateID !== null) { - canvasInstance.activate(activatedStateID); + if (workspace === Workspace.ATTRIBUTE_ANNOTATION) { + const [activatedState] = annotations + .filter((state: any): boolean => state.clientID === activatedStateID); + if (activatedState.objectType !== ObjectType.TAG) { + canvasInstance.focus(activatedStateID, aamZoomMargin); + } else { + canvasInstance.fit(); + } + } + canvasInstance.activate(activatedStateID, activatedAttributeID); const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`); if (el) { (el as any as SVGElement).setAttribute('fill-opacity', `${selectedOpacity / 100}`); @@ -352,7 +529,8 @@ export default class CanvasWrapperComponent extends React.PureComponent { } = this.props; if (frameData !== null) { - canvasInstance.setup(frameData, annotations); + canvasInstance.setup(frameData, annotations + .filter((e) => e.objectType !== ObjectType.TAG)); canvasInstance.rotate(frameAngle); } } @@ -364,14 +542,6 @@ export default class CanvasWrapperComponent extends React.PureComponent { gridColor, gridOpacity, canvasInstance, - jobInstance, - onSetupCanvas, - onDragCanvas, - onZoomCanvas, - onResetCanvas, - onActivateObject, - onUpdateContextMenu, - onEditShape, brightnessLevel, contrastLevel, saturationLevel, @@ -402,143 +572,33 @@ export default class CanvasWrapperComponent extends React.PureComponent { } // Events - canvasInstance.html().addEventListener('mousedown', (e: MouseEvent): void => { - const { - activatedStateID, - } = this.props; - - if ((e.target as HTMLElement).tagName === 'svg' && activatedStateID !== null) { - onActivateObject(null); - } - }); - - canvasInstance.html().addEventListener('click', (): void => { - if (document.activeElement) { - (document.activeElement as HTMLElement).blur(); - } - }); - - canvasInstance.html().addEventListener('contextmenu', (e: MouseEvent): void => { - const { - activatedStateID, - contextType, - contextVisible, - } = this.props; - - if (!(contextVisible && contextType === ContextMenuType.CANVAS_SHAPE_POINT)) { - onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY, - ContextMenuType.CANVAS_SHAPE); - } - }); - - canvasInstance.html().addEventListener('canvas.editstart', (): void => { - onActivateObject(null); - onEditShape(true); - }); - - canvasInstance.html().addEventListener('canvas.setup', (): void => { - onSetupCanvas(); - this.updateShapesView(); - this.activateOnCanvas(); - }); - canvasInstance.html().addEventListener('canvas.setup', () => { + const { activatedStateID, activatedAttributeID } = this.props; canvasInstance.fit(); + canvasInstance.activate(activatedStateID, activatedAttributeID); }, { once: true }); - canvasInstance.html().addEventListener('canvas.canceled', () => { - onResetCanvas(); - }); - - canvasInstance.html().addEventListener('canvas.dragstart', () => { - onDragCanvas(true); - }); - - canvasInstance.html().addEventListener('canvas.dragstop', () => { - onDragCanvas(false); - }); - - canvasInstance.html().addEventListener('canvas.zoomstart', () => { - onZoomCanvas(true); - }); - - canvasInstance.html().addEventListener('canvas.zoomstop', () => { - onZoomCanvas(false); - }); - - canvasInstance.html().addEventListener('canvas.clicked', (e: any) => { - const { clientID } = e.detail.state; - const sidebarItem = window.document - .getElementById(`cvat-objects-sidebar-state-item-${clientID}`); - if (sidebarItem) { - sidebarItem.scrollIntoView(); - } - }); - - canvasInstance.html().addEventListener('canvas.deactivated', (e: any): void => { - const { activatedStateID } = this.props; - const { state } = e.detail; - - // when we activate element, canvas deactivates the previous - // and triggers this event - // in this case we do not need to update our state - if (state.clientID === activatedStateID) { - onActivateObject(null); - } - }); - - canvasInstance.html().addEventListener('canvas.moved', async (event: any): Promise => { - const { activatedStateID } = this.props; - const result = await jobInstance.annotations.select( - event.detail.states, - event.detail.x, - event.detail.y, - ); - - if (result && result.state) { - if (result.state.shapeType === 'polyline' || result.state.shapeType === 'points') { - if (result.distance > MAX_DISTANCE_TO_OPEN_SHAPE) { - return; - } - } - - if (activatedStateID !== result.state.clientID) { - onActivateObject(result.state.clientID); - } - } - }); - - canvasInstance.html().addEventListener('canvas.find', async (e: any) => { - const result = await jobInstance.annotations - .select(e.detail.states, e.detail.x, e.detail.y); - - if (result && result.state) { - if (result.state.shapeType === 'polyline' || result.state.shapeType === 'points') { - if (result.distance > MAX_DISTANCE_TO_OPEN_SHAPE) { - return; - } - } - - canvasInstance.select(result.state); - } - }); - - canvasInstance.html().addEventListener('canvas.edited', this.onShapeEdited.bind(this)); - canvasInstance.html().addEventListener('canvas.drawn', this.onShapeDrawn.bind(this)); - canvasInstance.html().addEventListener('canvas.merged', this.onObjectsMerged.bind(this)); - canvasInstance.html().addEventListener('canvas.groupped', this.onObjectsGroupped.bind(this)); - canvasInstance.html().addEventListener('canvas.splitted', this.onTrackSplitted.bind(this)); - - canvasInstance.html().addEventListener('point.contextmenu', (event: any) => { - // const { - // activatedStateID, - // } = this.props; - - // console.log(event); - - // onUpdateContextMenu(activatedStateID !== null, event.detail.mouseEvent.clientX, - // event.detail.mouseEvent.clientY, ContextMenuType.CANVAS_SHAPE_POINT); - }); + canvasInstance.html().addEventListener('mousedown', this.onCanvasMouseDown); + canvasInstance.html().addEventListener('click', this.onCanvasClicked); + canvasInstance.html().addEventListener('contextmenu', this.onCanvasContextMenu); + canvasInstance.html().addEventListener('canvas.editstart', this.onCanvasEditStart); + canvasInstance.html().addEventListener('canvas.edited', this.onCanvasEditDone); + canvasInstance.html().addEventListener('canvas.dragstart', this.onCanvasDragStart); + canvasInstance.html().addEventListener('canvas.dragstop', this.onCanvasDragDone); + canvasInstance.html().addEventListener('canvas.zoomstart', this.onCanvasZoomStart); + canvasInstance.html().addEventListener('canvas.zoomstop', this.onCanvasZoomDone); + + canvasInstance.html().addEventListener('canvas.setup', this.onCanvasSetup); + canvasInstance.html().addEventListener('canvas.canceled', this.onCanvasCancel); + canvasInstance.html().addEventListener('canvas.find', this.onCanvasFindObject); + canvasInstance.html().addEventListener('canvas.deactivated', this.onCanvasShapeDeactivated); + canvasInstance.html().addEventListener('canvas.moved', this.onCanvasCursorMoved); + + canvasInstance.html().addEventListener('canvas.clicked', this.onCanvasShapeClicked); + canvasInstance.html().addEventListener('canvas.drawn', this.onCanvasShapeDrawn); + canvasInstance.html().addEventListener('canvas.merged', this.onCanvasObjectsMerged); + canvasInstance.html().addEventListener('canvas.groupped', this.onCanvasObjectsGroupped); + canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted); } public render(): JSX.Element { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx index 7deeabb4..bf151997 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx @@ -6,9 +6,7 @@ import React from 'react'; import { GlobalHotKeys, KeyMap } from 'react-hotkeys'; import { - Icon, Layout, - Tooltip, } from 'antd'; import { @@ -16,10 +14,6 @@ import { Rotation, } from 'reducers/interfaces'; -import { - TagIcon, -} from 'icons'; - import { Canvas, } from 'cvat-canvas'; @@ -33,6 +27,7 @@ import DrawRectangleControl from './draw-rectangle-control'; import DrawPolygonControl from './draw-polygon-control'; import DrawPolylineControl from './draw-polyline-control'; import DrawPointsControl from './draw-points-control'; +import SetupTagControl from './setup-tag-control'; import MergeControl from './merge-control'; import GroupControl from './group-control'; import SplitControl from './split-control'; @@ -221,9 +216,10 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { isDrawing={activeControl === ActiveControl.DRAW_POINTS} /> - - - +
diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-control.tsx new file mode 100644 index 00000000..0f711ba1 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-control.tsx @@ -0,0 +1,48 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import { + Popover, + Icon, +} from 'antd'; + +import { Canvas } from 'cvat-canvas'; +import { TagIcon } from 'icons'; + +import SetupTagPopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover'; + +interface Props { + canvasInstance: Canvas; + isDrawing: boolean; +} + +function SetupTagControl(props: Props): JSX.Element { + const { + isDrawing, + } = props; + + const dynamcPopoverPros = isDrawing ? { + overlayStyle: { + display: 'none', + }, + } : {}; + + return ( + + )} + > + + + ); +} + +export default React.memo(SetupTagControl); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover.tsx new file mode 100644 index 00000000..3edaa2aa --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover.tsx @@ -0,0 +1,75 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; + +import { + Row, + Col, + Select, + Button, +} from 'antd'; + +import Text from 'antd/lib/typography/Text'; + +interface Props { + labels: any[]; + selectedLabeID: number; + onChangeLabel(value: string): void; + onSetup( + labelID: number, + ): void; +} + +function setupTagPopover(props: Props): JSX.Element { + const { + labels, + selectedLabeID, + onChangeLabel, + onSetup, + } = props; + + return ( +
+ + + Setup tag + + + + + Label + + + + + + + + + + + + +
+ ); +} + +export default React.memo(setupTagPopover); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index 284fc613..dc7952cf 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -43,6 +43,7 @@ import { function ItemMenu( serverID: number | undefined, locked: boolean, + objectType: ObjectType, copy: (() => void), remove: (() => void), propagate: (() => void), @@ -67,18 +68,22 @@ function ItemMenu( Propagate - - - - - - + { objectType !== ObjectType.TAG && ( + <> + + + + + + + + )}
- + + {Workspace.STANDARD} + + + {Workspace.ATTRIBUTE_ANNOTATION} +
diff --git a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx index a3587203..370702fe 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx @@ -7,12 +7,12 @@ import React from 'react'; import { Row, Col, - Layout, InputNumber, } from 'antd'; import { SliderValue } from 'antd/lib/slider'; +import { Workspace } from 'reducers/interfaces'; import LeftGroup from './left-group'; import RightGroup from './right-group'; import PlayerNavigation from './player-navigation'; @@ -28,6 +28,8 @@ interface Props { stopFrame: number; undoAction?: string; redoAction?: string; + workspace: Workspace; + changeWorkspace(workspace: Workspace): void; showStatistics(): void; onSwitchPlay(): void; onSaveAnnotation(): void; @@ -55,7 +57,9 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { inputFrameRef, startFrame, stopFrame, + workspace, showStatistics, + changeWorkspace, onSwitchPlay, onSaveAnnotation, onPrevFrame, @@ -72,42 +76,44 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { } = props; return ( - - - - - - - - - - - - + + + + + + + + + + ); } diff --git a/cvat-ui/src/components/settings-page/workspace-settings.tsx b/cvat-ui/src/components/settings-page/workspace-settings.tsx index 6d265e4a..a7a5fd2c 100644 --- a/cvat-ui/src/components/settings-page/workspace-settings.tsx +++ b/cvat-ui/src/components/settings-page/workspace-settings.tsx @@ -93,7 +93,7 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element { max={1000} value={aamZoomMargin} onChange={(value: number | undefined): void => { - if (value) { + if (typeof (value) === 'number') { onChangeAAMZoomMargin(value); } }} diff --git a/cvat-ui/src/containers/annotation-page/annotation-page.tsx b/cvat-ui/src/containers/annotation-page/annotation-page.tsx index c66a9304..264d1d94 100644 --- a/cvat-ui/src/containers/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/containers/annotation-page/annotation-page.tsx @@ -9,9 +9,7 @@ import { RouteComponentProps } from 'react-router'; import AnnotationPageComponent from 'components/annotation-page/annotation-page'; import { getJobAsync } from 'actions/annotation-actions'; -import { - CombinedState, -} from 'reducers/interfaces'; +import { CombinedState, Workspace } from 'reducers/interfaces'; type OwnProps = RouteComponentProps<{ tid: string; @@ -21,6 +19,7 @@ type OwnProps = RouteComponentProps<{ interface StateToProps { job: any | null | undefined; fetching: boolean; + workspace: Workspace; } interface DispatchToProps { @@ -36,12 +35,14 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { instance: job, fetching, }, + workspace, }, } = state; return { job: !job || jobID === job.id ? job : null, fetching, + workspace, }; } diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx index e52d9994..7d99f592 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -40,6 +40,7 @@ import { ObjectType, CombinedState, ContextMenuType, + Workspace, } from 'reducers/interfaces'; import { Canvas } from 'cvat-canvas'; @@ -49,6 +50,7 @@ interface StateToProps { canvasInstance: Canvas; jobInstance: any; activatedStateID: number | null; + activatedAttributeID: number | null; selectedStatesID: number[]; annotations: any[]; frameData: any; @@ -68,6 +70,8 @@ interface StateToProps { contrastLevel: number; saturationLevel: number; resetZoom: boolean; + aamZoomMargin: number; + workspace: Workspace; minZLayer: number; maxZLayer: number; curZLayer: number; @@ -85,7 +89,7 @@ interface DispatchToProps { onGroupObjects: (enabled: boolean) => void; onSplitTrack: (enabled: boolean) => void; onEditShape: (enabled: boolean) => void; - onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void; + onUpdateAnnotations(states: any[]): void; onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void; onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void; onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void; @@ -130,6 +134,7 @@ function mapStateToProps(state: CombinedState): StateToProps { annotations: { states: annotations, activatedStateID, + activatedAttributeID, selectedStatesID, zLayer: { cur: curZLayer, @@ -138,6 +143,7 @@ function mapStateToProps(state: CombinedState): StateToProps { }, }, sidebarCollapsed, + workspace, }, settings: { player: { @@ -150,6 +156,9 @@ function mapStateToProps(state: CombinedState): StateToProps { saturationLevel, resetZoom, }, + workspace: { + aamZoomMargin, + }, shapes: { opacity, colorBy, @@ -167,6 +176,7 @@ function mapStateToProps(state: CombinedState): StateToProps { frameAngle: frameAngles[frame - jobInstance.startFrame], frame, activatedStateID, + activatedAttributeID, selectedStatesID, annotations, opacity, @@ -183,11 +193,13 @@ function mapStateToProps(state: CombinedState): StateToProps { contrastLevel, saturationLevel, resetZoom, + aamZoomMargin, curZLayer, minZLayer, maxZLayer, contextVisible, contextType, + workspace, }; } @@ -220,8 +232,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { onEditShape(enabled: boolean): void { dispatch(editShape(enabled)); }, - onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void { - dispatch(updateAnnotationsAsync(sessionInstance, frame, states)); + onUpdateAnnotations(states: any[]): void { + dispatch(updateAnnotationsAsync(states)); }, onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void { dispatch(createAnnotationsAsync(sessionInstance, frame, states)); @@ -240,7 +252,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { dispatch(updateCanvasContextMenu(false, 0, 0)); } - dispatch(activateObject(activatedStateID)); + dispatch(activateObject(activatedStateID, null)); }, onSelectObjects(selectedStatesID: number[]): void { dispatch(selectObjects(selectedStatesID)); diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx index 8d418e68..868b1ddb 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx @@ -13,7 +13,7 @@ import { } from 'reducers/interfaces'; import { - drawShape, + rememberObject, } from 'actions/annotation-actions'; import { Canvas, RectDrawingMethod } from 'cvat-canvas'; import DrawShapePopoverComponent from 'components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover'; @@ -47,7 +47,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { points?: number, rectDrawingMethod?: RectDrawingMethod, ): void { - dispatch(drawShape(shapeType, labelID, objectType, points, rectDrawingMethod)); + dispatch(rememberObject(objectType, labelID, shapeType, points, rectDrawingMethod)); }, }; } diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover.tsx new file mode 100644 index 00000000..4ba675c8 --- /dev/null +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover.tsx @@ -0,0 +1,144 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import { connect } from 'react-redux'; + +import { + CombinedState, + ObjectType, +} from 'reducers/interfaces'; + +import { + createAnnotationsAsync, + rememberObject, +} from 'actions/annotation-actions'; +import SetupTagPopoverComponent from 'components/annotation-page/standard-workspace/controls-side-bar/setup-tag-popover'; + +import { Canvas } from 'cvat-canvas'; +import getCore from 'cvat-core'; + +const cvat = getCore(); +interface DispatchToProps { + onAnnotationCreate(sessionInstance: any, frame: number, states: any[]): void; + onRememberObject(labelID: number): void; +} + +interface StateToProps { + canvasInstance: Canvas; + jobInstance: any; + labels: any[]; + frame: number; +} + +function mapDispatchToProps(dispatch: any): DispatchToProps { + return { + onAnnotationCreate(sessionInstance: any, frame: number, states: any[]): void { + dispatch(createAnnotationsAsync(sessionInstance, frame, states)); + }, + onRememberObject(labelID: number): void { + dispatch(rememberObject(ObjectType.TAG, labelID)); + }, + }; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { + annotation: { + canvas: { + instance: canvasInstance, + }, + job: { + instance: jobInstance, + labels, + }, + player: { + frame: { + number: frame, + }, + }, + }, + } = state; + + return { + canvasInstance, + jobInstance, + labels, + frame, + }; +} + +type Props = StateToProps & DispatchToProps; + +interface State { + selectedLabelID: number; +} + +class DrawShapePopoverContainer extends React.PureComponent { + constructor(props: Props) { + super(props); + + const defaultLabelID = props.labels[0].id; + this.state = { + selectedLabelID: defaultLabelID, + }; + } + + private onChangeLabel = (value: string): void => { + this.setState({ + selectedLabelID: +value, + }); + }; + + private onSetup(): void { + const { + frame, + labels, + jobInstance, + canvasInstance, + onAnnotationCreate, + onRememberObject, + } = this.props; + + const { selectedLabelID } = this.state; + + canvasInstance.cancel(); + + onRememberObject(selectedLabelID); + + const objectState = new cvat.classes.ObjectState({ + objectType: ObjectType.TAG, + label: labels.filter((label: any) => label.id === selectedLabelID)[0], + frame, + }); + + onAnnotationCreate(jobInstance, frame, [objectState]); + } + + public render(): JSX.Element { + const { + selectedLabelID, + } = this.state; + + const { + labels, + } = this.props; + + this.onSetup = this.onSetup.bind(this); + + return ( + + ); + } +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(DrawShapePopoverContainer); diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/label-item.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/label-item.tsx index 2e70ed1b..e795eb8d 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/label-item.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/label-item.tsx @@ -29,7 +29,7 @@ interface StateToProps { } interface DispatchToProps { - updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void; + updateAnnotations(states: any[]): void; changeLabelColor(sessionInstance: any, frameNumber: number, label: any, color: string): void; } @@ -68,8 +68,8 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { function mapDispatchToProps(dispatch: any): DispatchToProps { return { - updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void { - dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, states)); + updateAnnotations(states: any[]): void { + dispatch(updateAnnotationsAsync(states)); }, changeLabelColor( sessionInstance: any, @@ -162,8 +162,6 @@ class LabelItemContainer extends React.PureComponent { private switchHidden(value: boolean): void { const { updateAnnotations, - jobInstance, - frameNumber, } = this.props; const { ownObjectStates } = this.state; @@ -171,14 +169,12 @@ class LabelItemContainer extends React.PureComponent { state.hidden = value; } - updateAnnotations(jobInstance, frameNumber, ownObjectStates); + updateAnnotations(ownObjectStates); } private switchLock(value: boolean): void { const { updateAnnotations, - jobInstance, - frameNumber, } = this.props; const { ownObjectStates } = this.state; @@ -186,7 +182,7 @@ class LabelItemContainer extends React.PureComponent { state.lock = value; } - updateAnnotations(jobInstance, frameNumber, ownObjectStates); + updateAnnotations(ownObjectStates); } public render(): JSX.Element { diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index 9c65ee79..fb11fe1d 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -13,6 +13,7 @@ import { import { collapseObjectItems, changeLabelColorAsync, + createAnnotationsAsync, updateAnnotationsAsync, changeFrameAsync, removeObjectAsync, @@ -25,6 +26,7 @@ import { import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item'; + interface OwnProps { clientID: number; } @@ -47,14 +49,15 @@ interface StateToProps { interface DispatchToProps { changeFrame(frame: number): void; - updateState(sessionInstance: any, frameNumber: number, objectState: any): void; + updateState(objectState: any): void; + createAnnotations(sessionInstance: any, frameNumber: number, state: any): void; collapseOrExpand(objectStates: any[], collapsed: boolean): void; activateObject: (activatedStateID: number | null) => void; removeObject: (sessionInstance: any, objectState: any) => void; copyShape: (objectState: any) => void; propagateObject: (objectState: any) => void; changeLabelColor(sessionInstance: any, frameNumber: number, label: any, color: string): void; - changeGroupColor(sessionInstance: any, frameNumber: number, group: number, color: string): void; + changeGroupColor(group: number, color: string): void; } function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { @@ -121,14 +124,17 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { changeFrame(frame: number): void { dispatch(changeFrameAsync(frame)); }, - updateState(sessionInstance: any, frameNumber: number, state: any): void { - dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, [state])); + updateState(state: any): void { + dispatch(updateAnnotationsAsync([state])); + }, + createAnnotations(sessionInstance: any, frameNumber: number, state: any): void { + dispatch(createAnnotationsAsync(sessionInstance, frameNumber, state)); }, collapseOrExpand(objectStates: any[], collapsed: boolean): void { dispatch(collapseObjectItems(objectStates, collapsed)); }, activateObject(activatedStateID: number | null): void { - dispatch(activateObjectAction(activatedStateID)); + dispatch(activateObjectAction(activatedStateID, null)); }, removeObject(sessionInstance: any, objectState: any): void { dispatch(removeObjectAsync(sessionInstance, objectState, true)); @@ -148,13 +154,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { ): void { dispatch(changeLabelColorAsync(sessionInstance, frameNumber, label, color)); }, - changeGroupColor( - sessionInstance: any, - frameNumber: number, - group: number, - color: string, - ): void { - dispatch(changeGroupColorAsync(sessionInstance, frameNumber, group, color)); + changeGroupColor(group: number, color: string): void { + dispatch(changeGroupColorAsync(group, color)); }, }; } @@ -386,7 +387,7 @@ class ObjectItemContainer extends React.PureComponent { objectState.color = color; this.commit(); } else if (colorBy === ColorBy.GROUP) { - changeGroupColor(jobInstance, frameNumber, objectState.group.id, color); + changeGroupColor(objectState.group.id, color); } else if (colorBy === ColorBy.LABEL) { changeLabelColor(jobInstance, frameNumber, objectState.label, color); } @@ -415,11 +416,9 @@ class ObjectItemContainer extends React.PureComponent { const { objectState, updateState, - jobInstance, - frameNumber, } = this.props; - updateState(jobInstance, frameNumber, objectState); + updateState(objectState); } public render(): JSX.Element { diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx index 8bc18e0f..5279620f 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx @@ -6,15 +6,11 @@ import React from 'react'; import { connect } from 'react-redux'; import { GlobalHotKeys, KeyMap } from 'react-hotkeys'; -import { SelectValue } from 'antd/lib/select'; - import ObjectsListComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-list'; import { updateAnnotationsAsync, - fetchAnnotationsAsync, removeObjectAsync, changeFrameAsync, - changeAnnotationsFilters as changeAnnotationsFiltersAction, collapseObjectItems, copyShape as copyShapeAction, propagateObject as propagateObjectAction, @@ -42,8 +38,7 @@ interface StateToProps { } interface DispatchToProps { - updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void; - changeAnnotationsFilters(sessionInstance: any, filters: string[]): void; + updateAnnotations(states: any[]): void; collapseStates(states: any[], value: boolean): void; removeObject: (sessionInstance: any, objectState: any, force: boolean) => void; copyShape: (objectState: any) => void; @@ -84,7 +79,9 @@ function mapStateToProps(state: CombinedState): StateToProps { objectStates.forEach((objectState: any) => { const { clientID, lock } = objectState; if (!lock) { - statesHidden = statesHidden && objectState.hidden; + if (objectState.objectType !== ObjectType.TAG) { + statesHidden = statesHidden && objectState.hidden; + } statesLocked = statesLocked && objectState.lock; } const stateCollapsed = clientID in collapsed ? collapsed[clientID] : true; @@ -109,19 +106,12 @@ function mapStateToProps(state: CombinedState): StateToProps { function mapDispatchToProps(dispatch: any): DispatchToProps { return { - updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void { - dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, states)); + updateAnnotations(states: any[]): void { + dispatch(updateAnnotationsAsync(states)); }, collapseStates(states: any[], collapsed: boolean): void { dispatch(collapseObjectItems(states, collapsed)); }, - changeAnnotationsFilters( - sessionInstance: any, - filters: string[], - ): void { - dispatch(changeAnnotationsFiltersAction(filters)); - dispatch(fetchAnnotationsAsync(sessionInstance)); - }, removeObject(sessionInstance: any, objectState: any, force: boolean): void { dispatch(removeObjectAsync(sessionInstance, objectState, force)); }, @@ -188,15 +178,6 @@ class ObjectsListContainer extends React.PureComponent { }); }; - private onChangeAnnotationsFilters = (value: SelectValue): void => { - const { - jobInstance, - changeAnnotationsFilters, - } = this.props; - const filters = value as string[]; - changeAnnotationsFilters(jobInstance, filters); - }; - private onLockAllStates = (): void => { this.lockAllStates(true); }; @@ -225,28 +206,24 @@ class ObjectsListContainer extends React.PureComponent { const { objectStates, updateAnnotations, - jobInstance, - frameNumber, } = this.props; for (const objectState of objectStates) { objectState.lock = locked; } - updateAnnotations(jobInstance, frameNumber, objectStates); + updateAnnotations(objectStates); } private hideAllStates(hidden: boolean): void { const { objectStates, updateAnnotations, - jobInstance, - frameNumber, } = this.props; for (const objectState of objectStates) { objectState.hidden = hidden; } - updateAnnotations(jobInstance, frameNumber, objectStates); + updateAnnotations(objectStates); } private collapseAllStates(collapsed: boolean): void { @@ -260,12 +237,10 @@ class ObjectsListContainer extends React.PureComponent { public render(): JSX.Element { const { - annotationsFilters, statesHidden, statesLocked, activatedStateID, objectStates, - frameNumber, jobInstance, updateAnnotations, removeObject, @@ -274,7 +249,6 @@ class ObjectsListContainer extends React.PureComponent { changeFrame, maxZLayer, minZLayer, - annotationsFiltersHistory, } = this.props; const { sortedStatesID, @@ -397,7 +371,7 @@ class ObjectsListContainer extends React.PureComponent { const state = activatedStated(); if (state) { state.lock = !state.lock; - updateAnnotations(jobInstance, frameNumber, [state]); + updateAnnotations([state]); } }, SWITCH_ALL_HIDDEN: (event: KeyboardEvent | undefined) => { @@ -409,7 +383,7 @@ class ObjectsListContainer extends React.PureComponent { const state = activatedStated(); if (state) { state.hidden = !state.hidden; - updateAnnotations(jobInstance, frameNumber, [state]); + updateAnnotations([state]); } }, SWITCH_OCCLUDED: (event: KeyboardEvent | undefined) => { @@ -417,7 +391,7 @@ class ObjectsListContainer extends React.PureComponent { const state = activatedStated(); if (state && state.objectType !== ObjectType.TAG) { state.occluded = !state.occluded; - updateAnnotations(jobInstance, frameNumber, [state]); + updateAnnotations([state]); } }, SWITCH_KEYFRAME: (event: KeyboardEvent | undefined) => { @@ -425,7 +399,7 @@ class ObjectsListContainer extends React.PureComponent { const state = activatedStated(); if (state && state.objectType === ObjectType.TRACK) { state.keyframe = !state.keyframe; - updateAnnotations(jobInstance, frameNumber, [state]); + updateAnnotations([state]); } }, SWITCH_OUTSIDE: (event: KeyboardEvent | undefined) => { @@ -433,7 +407,7 @@ class ObjectsListContainer extends React.PureComponent { const state = activatedStated(); if (state && state.objectType === ObjectType.TRACK) { state.outside = !state.outside; - updateAnnotations(jobInstance, frameNumber, [state]); + updateAnnotations([state]); } }, DELETE_OBJECT: (event: KeyboardEvent | undefined) => { @@ -448,7 +422,7 @@ class ObjectsListContainer extends React.PureComponent { const state = activatedStated(); if (state && state.objectType !== ObjectType.TAG) { state.zOrder = minZLayer - 1; - updateAnnotations(jobInstance, frameNumber, [state]); + updateAnnotations([state]); } }, TO_FOREGROUND: (event: KeyboardEvent | undefined) => { @@ -456,20 +430,20 @@ class ObjectsListContainer extends React.PureComponent { const state = activatedStated(); if (state && state.objectType !== ObjectType.TAG) { state.zOrder = maxZLayer + 1; - updateAnnotations(jobInstance, frameNumber, [state]); + updateAnnotations([state]); } }, COPY_SHAPE: (event: KeyboardEvent | undefined) => { preventDefault(event); const state = activatedStated(); - if (state && state.objectType !== ObjectType.TAG) { + if (state) { copyShape(state); } }, PROPAGATE_OBJECT: (event: KeyboardEvent | undefined) => { preventDefault(event); const state = activatedStated(); - if (state && state.objectType !== ObjectType.TAG) { + if (state) { propagateObject(state); } }, @@ -504,10 +478,7 @@ class ObjectsListContainer extends React.PureComponent { {...this.props} statesOrdering={statesOrdering} sortedStatesID={sortedStatesID} - annotationsFilters={annotationsFilters} changeStatesOrdering={this.onChangeStatesOrdering} - changeAnnotationsFilters={this.onChangeAnnotationsFilters} - annotationsFiltersHistory={annotationsFiltersHistory} lockAllStates={this.onLockAllStates} unlockAllStates={this.onUnlockAllStates} collapseAllStates={this.onCollapseAllStates} diff --git a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx index 8bcc1f59..48695dbe 100644 --- a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx @@ -22,10 +22,12 @@ import { undoActionAsync, redoActionAsync, searchAnnotationsAsync, + changeWorkspace as changeWorkspaceAction, + activateObject, } from 'actions/annotation-actions'; import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar'; -import { CombinedState, FrameSpeed } from 'reducers/interfaces'; +import { CombinedState, FrameSpeed, Workspace } from 'reducers/interfaces'; interface StateToProps { jobInstance: any; @@ -41,6 +43,7 @@ interface StateToProps { redoAction?: string; autoSave: boolean; autoSaveInterval: number; + workspace: Workspace; } interface DispatchToProps { @@ -51,6 +54,7 @@ interface DispatchToProps { undo(sessionInstance: any, frameNumber: any): void; redo(sessionInstance: any, frameNumber: any): void; searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void; + changeWorkspace(workspace: Workspace): void; } function mapStateToProps(state: CombinedState): StateToProps { @@ -76,6 +80,7 @@ function mapStateToProps(state: CombinedState): StateToProps { canvas: { ready: canvasIsReady, }, + workspace, }, settings: { player: { @@ -103,6 +108,7 @@ function mapStateToProps(state: CombinedState): StateToProps { redoAction: history.redo[history.redo.length - 1], autoSave, autoSaveInterval, + workspace, }; } @@ -130,6 +136,10 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void { dispatch(searchAnnotationsAsync(sessionInstance, frameFrom, frameTo)); }, + changeWorkspace(workspace: Workspace): void { + dispatch(activateObject(null, null)); + dispatch(changeWorkspaceAction(workspace)); + }, }; } @@ -442,8 +452,10 @@ class AnnotationTopBarContainer extends React.PureComponent { frameNumber, undoAction, redoAction, - searchAnnotations, + workspace, canvasIsReady, + searchAnnotations, + changeWorkspace, } = this.props; const preventDefault = (event: KeyboardEvent | undefined): void => { @@ -602,6 +614,8 @@ class AnnotationTopBarContainer extends React.PureComponent { onSliderChange={this.onChangePlayerSliderValue} onInputChange={this.onChangePlayerInputValue} onURLIconClick={this.onURLIconClick} + changeWorkspace={changeWorkspace} + workspace={workspace} playing={playing} saving={saving} savingStatuses={savingStatuses} diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index 550a61e4..70eaf594 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -13,6 +13,7 @@ import { ShapeType, ObjectType, ContextMenuType, + Workspace, } from './interfaces'; const defaultState: AnnotationState = { @@ -56,6 +57,7 @@ const defaultState: AnnotationState = { annotations: { selectedStatesID: [], activatedStateID: null, + activatedAttributeID: null, saving: { uploading: false, statuses: [], @@ -90,6 +92,7 @@ const defaultState: AnnotationState = { sidebarCollapsed: false, appearanceCollapsed: false, tabContentHeight: 0, + workspace: Workspace.STANDARD, }; export default (state = defaultState, action: AnyAction): AnnotationState => { @@ -395,7 +398,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }, }; } - case AnnotationActionTypes.DRAW_SHAPE: { + case AnnotationActionTypes.REMEMBER_CREATED_OBJECT: { const { shapeType, labelID, @@ -648,7 +651,11 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }; } case AnnotationActionTypes.ACTIVATE_OBJECT: { - const { activatedStateID } = action.payload; + const { + activatedStateID, + activatedAttributeID, + } = action.payload; + const { canvas: { activeControl, @@ -665,6 +672,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { annotations: { ...state.annotations, activatedStateID, + activatedAttributeID, }, }; } @@ -1045,6 +1053,13 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }, }; } + case AnnotationActionTypes.CHANGE_WORKSPACE: { + const { workspace } = action.payload; + return { + ...state, + workspace, + }; + } case AnnotationActionTypes.RESET_CANVAS: { return { ...state, diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index f7bf7131..4c308a7b 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -337,6 +337,7 @@ export interface AnnotationState { annotations: { selectedStatesID: number[]; activatedStateID: number | null; + activatedAttributeID: number | null; collapsed: Record; states: any[]; filters: string[]; @@ -369,6 +370,12 @@ export interface AnnotationState { sidebarCollapsed: boolean; appearanceCollapsed: boolean; tabContentHeight: number; + workspace: Workspace; +} + +export enum Workspace { + STANDARD = 'Standard', + ATTRIBUTE_ANNOTATION = 'Attribute annotation', } export enum GridColor { diff --git a/cvat/__init__.py b/cvat/__init__.py index 0b03de53..59cc506f 100644 --- a/cvat/__init__.py +++ b/cvat/__init__.py @@ -1,10 +1,9 @@ - -# Copyright (C) 2018 Intel Corporation +# Copyright (C) 2018-2020 Intel Corporation # # SPDX-License-Identifier: MIT from cvat.utils.version import get_version -VERSION = (0, 6, 0, 'alpha', 0) +VERSION = (1, 0, 0, 'alpha', 0) __version__ = get_version(VERSION) diff --git a/cvat/apps/annotation/annotation.py b/cvat/apps/annotation/annotation.py index 9c805154..70054255 100644 --- a/cvat/apps/annotation/annotation.py +++ b/cvat/apps/annotation/annotation.py @@ -322,7 +322,7 @@ class Annotation: annotations = {} data_manager = DataManager(self._annotation_ir) - for shape in data_manager.to_shapes(self._db_task.size): + for shape in sorted(data_manager.to_shapes(self._db_task.size), key=lambda s: s.get("z_order", 0)): _get_frame(annotations, shape).labeled_shapes.append(self._export_labeled_shape(shape)) for tag in self._annotation_ir.tags: diff --git a/cvat/apps/annotation/pascal_voc.py b/cvat/apps/annotation/pascal_voc.py index 2dd0aa48..b6bcfaa3 100644 --- a/cvat/apps/annotation/pascal_voc.py +++ b/cvat/apps/annotation/pascal_voc.py @@ -74,7 +74,7 @@ def dump(file_object, annotations): extractor = CvatAnnotationsExtractor('', annotations) extractor = extractor.transform(id_from_image) extractor = Dataset.from_extractors(extractor) # apply lazy transforms - converter = env.make_converter('voc') + converter = env.make_converter('voc', label_map='source') with TemporaryDirectory() as temp_dir: converter(extractor, save_dir=temp_dir) make_zip_archive(temp_dir, file_object) \ No newline at end of file diff --git a/cvat/requirements/development.txt b/cvat/requirements/development.txt index fc0333ad..de046528 100644 --- a/cvat/requirements/development.txt +++ b/cvat/requirements/development.txt @@ -7,7 +7,7 @@ pylint==2.3.1 pylint-django==0.9.4 pylint-plugin-utils==0.2.6 rope==0.11 -wrapt==1.10.11 +wrapt==1.11.1 django-extensions==2.0.6 Werkzeug==0.15.3 snakeviz==0.4.2