Merge branch 'develop' into dk/point-deletion

main
Dmitry Kalinin 6 years ago
commit b0dbbb3faa

@ -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/), 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). 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 ### Added
- Server only support for projects. Extend REST API v1 (/api/v1/projects*) - 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)) - 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 - Yolov3 interpretation script fix and changes to mapping.json
- YOLO format support ([#1151](https://github.com/opencv/cvat/pull/1151)) - YOLO format support ([#1151](https://github.com/opencv/cvat/pull/1151))
### Deprecated
-
### Removed
-
### Fixed ### Fixed
- Exception in Git plugin [#826](https://github.com/opencv/cvat/issues/826) - 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) - 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) - 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) - 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 ## [0.5.1] - 2019-10-17
### Added ### Added

@ -15,7 +15,7 @@ Next steps should work on clear Ubuntu 18.04.
- Install necessary dependencies: - Install necessary dependencies:
```sh ```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) - 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 cd cvat && mkdir logs keys
python3 -m venv .env python3 -m venv .env
. .env/bin/activate . .env/bin/activate
pip install -U pip wheel pip install -U pip wheel setuptools
pip install -r cvat/requirements/development.txt pip install -r cvat/requirements/development.txt
pip install -r datumaro/requirements.txt pip install -r datumaro/requirements.txt
python manage.py migrate python manage.py migrate

@ -1,6 +1,6 @@
{ {
"name": "cvat-canvas", "name": "cvat-canvas",
"version": "0.1.0", "version": "0.5.2",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -1203,9 +1203,9 @@
} }
}, },
"acorn": { "acorn": {
"version": "6.2.1", "version": "6.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
"integrity": "sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q==", "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
"dev": true "dev": true
}, },
"acorn-jsx": { "acorn-jsx": {

@ -264,9 +264,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
y + height / 2, y + height / 2,
]); ]);
const canvasOffset = this.canvas.getBoundingClientRect();
const [cx, cy] = [ const [cx, cy] = [
this.canvas.clientWidth / 2 + this.canvas.offsetLeft, this.canvas.clientWidth / 2 + canvasOffset.left,
this.canvas.clientHeight / 2 + this.canvas.offsetTop, this.canvas.clientHeight / 2 + canvasOffset.top,
]; ];
const dragged = { const dragged = {
@ -748,7 +749,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (object) { if (object) {
const bbox: SVG.BBox = object.bbox(); const bbox: SVG.BBox = object.bbox();
this.onFocusRegion(bbox.x - padding, bbox.y - padding, 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) { } else if (reason === UpdateReasons.SHAPE_ACTIVATED) {
this.activate(this.controller.activeElement); this.activate(this.controller.activeElement);
@ -1037,7 +1038,26 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.content.prepend(...sorted.map((pair): SVGElement => pair[0])); 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) { if (this.activeElement.clientID !== null) {
const { clientID } = this.activeElement; const { clientID } = this.activeElement;
const drawnState = this.drawnStates[clientID]; const drawnState = this.drawnStates[clientID];
@ -1070,29 +1090,34 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.sortObjects(); this.sortObjects();
this.activeElement = { this.activeElement = {
...this.activeElement,
clientID: null, clientID: null,
attributeID: null,
}; };
} }
} }
private activate(activeElement: ActiveElement): void { private deactivate(): void {
// Check if other element have been already activated this.deactivateAttribute();
if (this.activeElement.clientID !== null) { this.deactivateShape();
// Check if it is the same element }
if (this.activeElement.clientID === activeElement.clientID) {
return;
}
// Deactivate previous element private activateAttribute(clientID: number, attributeID: number): void {
this.deactivate(); 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; this.activeElement = {
if (clientID === null) { ...this.activeElement,
return; attributeID,
};
} }
}
private activateShape(clientID: number): void {
const [state] = this.controller.objects const [state] = this.controller.objects
.filter((_state: any): boolean => _state.clientID === clientID); .filter((_state: any): boolean => _state.clientID === clientID);
@ -1105,8 +1130,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
return; return;
} }
this.activeElement = { ...activeElement };
const shape = this.svgShapes[clientID]; const shape = this.svgShapes[clientID];
let text = this.svgTexts[clientID]; let text = this.svgTexts[clientID];
if (!text) { if (!text) {
text = this.addText(state); 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', { this.canvas.dispatchEvent(new CustomEvent('canvas.activated', {
bubbles: false, bubbles: false,
cancelable: true, 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. // Update text position after corresponding box has been moved, resized, etc.
private updateTextPosition(text: SVG.Text, shape: SVG.Shape): void { private updateTextPosition(text: SVG.Text, shape: SVG.Shape): void {
let box = (shape.node as any).getBBox(); let box = (shape.node as any).getBBox();

@ -802,7 +802,9 @@
let minimumState = null; let minimumState = null;
for (const state of objectStates) { for (const state of objectStates) {
checkObjectType('object state', state, null, ObjectState); 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]; const object = this.objects[state.clientID];
if (typeof (object) === 'undefined') { if (typeof (object) === 'undefined') {
@ -810,9 +812,9 @@
'The object has not been saved yet. Call annotations.put([state]) before', 'The object has not been saved yet. Call annotations.put([state]) before',
); );
} }
const distance = object.constructor.distance(state.points, x, y); 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; minimumDistance = distance;
minimumState = state; minimumState = state;
} }

@ -8,7 +8,10 @@
*/ */
const jsonpath = require('jsonpath'); const jsonpath = require('jsonpath');
const { AttributeType } = require('./enums'); const {
AttributeType,
ObjectType,
} = require('./enums');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
@ -165,18 +168,21 @@ class AnnotationsFilter {
let xbr = Number.MIN_SAFE_INTEGER; let xbr = Number.MIN_SAFE_INTEGER;
let ytl = Number.MAX_SAFE_INTEGER; let ytl = Number.MAX_SAFE_INTEGER;
let ybr = Number.MIN_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 = {}; const attributes = {};
Object.keys(state.attributes).reduce((acc, key) => { Object.keys(state.attributes).reduce((acc, key) => {
const attr = labelAttributes[key]; const attr = labelAttributes[key];

@ -1139,6 +1139,7 @@
attributes: { ...this.attributes }, attributes: { ...this.attributes },
label: this.label, label: this.label,
group: this.groupObject, group: this.groupObject,
color: this.color,
updated: this.updated, updated: this.updated,
frame, frame,
}; };
@ -1171,6 +1172,10 @@
this._saveLock(data.lock); this._saveLock(data.lock);
} }
if (updated.color) {
this._saveColor(data.color);
}
this.updateTimestamp(updated); this.updateTimestamp(updated);
updated.reset(); updated.reset();

@ -1421,9 +1421,9 @@
} }
}, },
"acorn": { "acorn": {
"version": "6.3.0", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
"integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==", "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
"dev": true "dev": true
}, },
"acorn-jsx": { "acorn-jsx": {
@ -4293,14 +4293,6 @@
"acorn": "^7.1.0", "acorn": "^7.1.0",
"acorn-jsx": "^5.1.0", "acorn-jsx": "^5.1.0",
"eslint-visitor-keys": "^1.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": { "esprima": {
@ -12354,6 +12346,12 @@
"webpack-sources": "^1.4.1" "webpack-sources": "^1.4.1"
}, },
"dependencies": { "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": { "schema-utils": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",

@ -19,12 +19,20 @@ import {
FrameSpeed, FrameSpeed,
Rotation, Rotation,
ContextMenuType, ContextMenuType,
Workspace,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import getCore from 'cvat-core'; import getCore from 'cvat-core';
import { RectDrawingMethod } from 'cvat-canvas'; import { RectDrawingMethod } from 'cvat-canvas';
import { getCVATStore } from 'cvat-store'; import { getCVATStore } from 'cvat-store';
interface AnnotationsParameters {
filters: string[];
frame: number;
showAllInterpolationTracks: boolean;
jobInstance: any;
}
const cvat = getCore(); const cvat = getCore();
let store: null | Store<CombinedState> = null; let store: null | Store<CombinedState> = null;
@ -35,19 +43,37 @@ function getStore(): Store<CombinedState> {
return store; return store;
} }
function receiveAnnotationsParameters(): function receiveAnnotationsParameters(): AnnotationsParameters {
{ filters: string[]; frame: number; showAllInterpolationTracks: boolean } {
if (store === null) { if (store === null) {
store = getCVATStore(); store = getCVATStore();
} }
const state: CombinedState = getStore().getState(); const state: CombinedState = getStore().getState();
const { filters } = state.annotation.annotations; const {
const frame = state.annotation.player.frame.number; annotation: {
const { showAllInterpolationTracks } = state.settings.workspace; annotations: {
filters,
},
player: {
frame: {
number: frame,
},
},
job: {
instance: jobInstance,
},
},
settings: {
workspace: {
showAllInterpolationTracks,
},
},
} = state;
return { return {
filters, filters,
frame, frame,
jobInstance,
showAllInterpolationTracks, showAllInterpolationTracks,
}; };
} }
@ -85,10 +111,10 @@ export enum AnnotationActionTypes {
COPY_SHAPE = 'COPY_SHAPE', COPY_SHAPE = 'COPY_SHAPE',
PASTE_SHAPE = 'PASTE_SHAPE', PASTE_SHAPE = 'PASTE_SHAPE',
EDIT_SHAPE = 'EDIT_SHAPE', EDIT_SHAPE = 'EDIT_SHAPE',
DRAW_SHAPE = 'DRAW_SHAPE',
REPEAT_DRAW_SHAPE = 'REPEAT_DRAW_SHAPE', REPEAT_DRAW_SHAPE = 'REPEAT_DRAW_SHAPE',
SHAPE_DRAWN = 'SHAPE_DRAWN', SHAPE_DRAWN = 'SHAPE_DRAWN',
RESET_CANVAS = 'RESET_CANVAS', RESET_CANVAS = 'RESET_CANVAS',
REMEMBER_CREATED_OBJECT = 'REMEMBER_CREATED_OBJECT',
UPDATE_ANNOTATIONS_SUCCESS = 'UPDATE_ANNOTATIONS_SUCCESS', UPDATE_ANNOTATIONS_SUCCESS = 'UPDATE_ANNOTATIONS_SUCCESS',
UPDATE_ANNOTATIONS_FAILED = 'UPDATE_ANNOTATIONS_FAILED', UPDATE_ANNOTATIONS_FAILED = 'UPDATE_ANNOTATIONS_FAILED',
CREATE_ANNOTATIONS_SUCCESS = 'CREATE_ANNOTATIONS_SUCCESS', CREATE_ANNOTATIONS_SUCCESS = 'CREATE_ANNOTATIONS_SUCCESS',
@ -139,11 +165,22 @@ export enum AnnotationActionTypes {
SWITCH_Z_LAYER = 'SWITCH_Z_LAYER', SWITCH_Z_LAYER = 'SWITCH_Z_LAYER',
ADD_Z_LAYER = 'ADD_Z_LAYER', ADD_Z_LAYER = 'ADD_Z_LAYER',
SEARCH_ANNOTATIONS_FAILED = 'SEARCH_ANNOTATIONS_FAILED', 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 { export function addZLayer(): AnyAction {
return { return {
type: AnnotationActionTypes.ADD_Z_LAYER, 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<Promise<void>, {}, {}, AnyAction> { ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try { try {
const { filters, frame, showAllInterpolationTracks } = receiveAnnotationsParameters(); const {
const states = await sessionInstance.annotations filters,
frame,
showAllInterpolationTracks,
jobInstance,
} = receiveAnnotationsParameters();
const states = await jobInstance.annotations
.get(frame, showAllInterpolationTracks, filters); .get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states); 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 { return {
type: AnnotationActionTypes.ACTIVATE_OBJECT, type: AnnotationActionTypes.ACTIVATE_OBJECT,
payload: { payload: {
activatedStateID, activatedStateID,
activatedAttributeID,
}, },
}; };
} }
@ -850,15 +896,17 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}; };
} }
export function drawShape( export function rememberObject(
shapeType: ShapeType,
labelID: number,
objectType: ObjectType, objectType: ObjectType,
labelID: number,
shapeType?: ShapeType,
points?: number, points?: number,
rectDrawingMethod?: RectDrawingMethod, rectDrawingMethod?: RectDrawingMethod,
): AnyAction { ): AnyAction {
let activeControl = ActiveControl.DRAW_RECTANGLE; let activeControl = ActiveControl.CURSOR;
if (shapeType === ShapeType.POLYGON) { if (shapeType === ShapeType.RECTANGLE) {
activeControl = ActiveControl.DRAW_RECTANGLE;
} else if (shapeType === ShapeType.POLYGON) {
activeControl = ActiveControl.DRAW_POLYGON; activeControl = ActiveControl.DRAW_POLYGON;
} else if (shapeType === ShapeType.POLYLINE) { } else if (shapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE; activeControl = ActiveControl.DRAW_POLYLINE;
@ -867,7 +915,7 @@ export function drawShape(
} }
return { return {
type: AnnotationActionTypes.DRAW_SHAPE, type: AnnotationActionTypes.REMEMBER_CREATED_OBJECT,
payload: { payload: {
shapeType, shapeType,
labelID, 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<Promise<void>, {}, {}, AnyAction> { ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const {
jobInstance,
filters,
frame,
showAllInterpolationTracks,
} = receiveAnnotationsParameters();
try { try {
if (statesToUpdate.some((state: any): boolean => state.updateFlags.zOrder)) { if (statesToUpdate.some((state: any): boolean => state.updateFlags.zOrder)) {
// deactivate object to visualize changes immediately (UX) // deactivate object to visualize changes immediately (UX)
dispatch(activateObject(null)); dispatch(activateObject(null, null));
} }
const promises = statesToUpdate const promises = statesToUpdate
.map((objectState: any): Promise<any> => objectState.save()); .map((objectState: any): Promise<any> => objectState.save());
const states = await Promise.all(promises); const states = await Promise.all(promises);
const history = await sessionInstance.actions.get(); const history = await jobInstance.actions.get();
const [minZ, maxZ] = computeZRange(states); const [minZ, maxZ] = computeZRange(states);
dispatch({ dispatch({
@ -938,8 +993,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}, },
}); });
} catch (error) { } catch (error) {
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters(); const states = await jobInstance.annotations
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters); .get(frame, showAllInterpolationTracks, filters);
dispatch({ dispatch({
type: AnnotationActionTypes.UPDATE_ANNOTATIONS_FAILED, type: AnnotationActionTypes.UPDATE_ANNOTATIONS_FAILED,
@ -1117,8 +1171,6 @@ export function changeLabelColorAsync(
} }
export function changeGroupColorAsync( export function changeGroupColorAsync(
sessionInstance: any,
frameNumber: number,
group: number, group: number,
color: string, color: string,
): ThunkAction<Promise<void>, {}, {}, AnyAction> { ): ThunkAction<Promise<void>, {}, {}, AnyAction> {
@ -1128,9 +1180,9 @@ export function changeGroupColorAsync(
.filter((_state: any): boolean => _state.group.id === group); .filter((_state: any): boolean => _state.group.id === group);
if (groupStates.length) { if (groupStates.length) {
groupStates[0].group.color = color; groupStates[0].group.color = color;
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, groupStates)); dispatch(updateAnnotationsAsync(groupStates));
} else { } else {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, [])); dispatch(updateAnnotationsAsync([]));
} }
}; };
} }
@ -1160,12 +1212,28 @@ export function searchAnnotationsAsync(
export function pasteShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> { export function pasteShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const initialState = getStore().getState().annotation.drawing.activeInitialState; const {
const { instance: canvasInstance } = getStore().getState().annotation.canvas; canvas: {
instance: canvasInstance,
},
job: {
instance: jobInstance,
},
player: {
frame: {
number: frameNumber,
},
},
drawing: {
activeInitialState: initialState,
},
} = getStore().getState().annotation;
if (initialState) { if (initialState) {
let activeControl = ActiveControl.DRAW_RECTANGLE; let activeControl = ActiveControl.CURSOR;
if (initialState.shapeType === ShapeType.POINTS) { if (initialState.shapeType === ShapeType.RECTANGLE) {
activeControl = ActiveControl.DRAW_RECTANGLE;
} else if (initialState.shapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS; activeControl = ActiveControl.DRAW_POINTS;
} else if (initialState.shapeType === ShapeType.POLYGON) { } else if (initialState.shapeType === ShapeType.POLYGON) {
activeControl = ActiveControl.DRAW_POLYGON; activeControl = ActiveControl.DRAW_POLYGON;
@ -1181,10 +1249,20 @@ export function pasteShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction>
}); });
canvasInstance.cancel(); canvasInstance.cancel();
canvasInstance.draw({ if (initialState.objectType === ObjectType.TAG) {
enabled: true, const objectState = new cvat.classes.ObjectState({
initialState, 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<Promise<void>, {}, {}, AnyAction>
export function repeatDrawShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> { export function repeatDrawShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const { const {
activeShapeType, canvas: {
activeNumOfPoints, instance: canvasInstance,
activeRectDrawingMethod, },
} = getStore().getState().annotation.drawing; job: {
labels,
const { instance: canvasInstance } = getStore().getState().annotation.canvas; instance: jobInstance,
},
player: {
frame: {
number: frameNumber,
},
},
drawing: {
activeObjectType,
activeLabelID,
activeShapeType,
activeNumOfPoints,
activeRectDrawingMethod,
},
} = getStore().getState().annotation;
let activeControl = ActiveControl.DRAW_RECTANGLE; let activeControl = ActiveControl.CURSOR;
if (activeShapeType === ShapeType.POLYGON) { 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; activeControl = ActiveControl.DRAW_POLYGON;
} else if (activeShapeType === ShapeType.POLYLINE) { } else if (activeShapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE; activeControl = ActiveControl.DRAW_POLYLINE;
} else if (activeShapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
} }
dispatch({ dispatch({
@ -1216,12 +1310,21 @@ export function repeatDrawShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAc
}); });
canvasInstance.cancel(); canvasInstance.cancel();
canvasInstance.draw({ if (activeObjectType === ObjectType.TAG) {
enabled: true, const objectState = new cvat.classes.ObjectState({
rectDrawingMethod: activeRectDrawingMethod, objectType: ObjectType.TAG,
numberOfPoints: activeNumOfPoints, label: labels.filter((label: any) => label.id === activeLabelID)[0],
shapeType: activeShapeType, frame: frameNumber,
crosshair: activeShapeType === ShapeType.RECTANGLE, });
}); dispatch(createAnnotationsAsync(jobInstance, frameNumber, [objectState]));
} else {
canvasInstance.draw({
enabled: true,
rectDrawingMethod: activeRectDrawingMethod,
numberOfPoints: activeNumOfPoints,
shapeType: activeShapeType,
crosshair: activeShapeType === ShapeType.RECTANGLE,
});
}
}; };
} }

@ -11,14 +11,17 @@ import {
Result, Result,
} from 'antd'; } from 'antd';
import { Workspace } from 'reducers/interfaces';
import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar'; import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar';
import StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal'; import StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal';
import StandardWorkspaceComponent from './standard-workspace/standard-workspace'; import StandardWorkspaceComponent from './standard-workspace/standard-workspace';
import AttributeAnnotationWorkspace from './attribute-annotation-workspace/attribute-annotation-workspace';
interface Props { interface Props {
job: any | null | undefined; job: any | null | undefined;
fetching: boolean; fetching: boolean;
getJob(): void; getJob(): void;
workspace: Workspace;
} }
@ -27,9 +30,9 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
job, job,
fetching, fetching,
getJob, getJob,
workspace,
} = props; } = props;
if (job === null) { if (job === null) {
if (!fetching) { if (!fetching) {
getJob(); getJob();
@ -51,8 +54,18 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
return ( return (
<Layout className='cvat-annotation-page'> <Layout className='cvat-annotation-page'>
<AnnotationTopBarContainer /> <Layout.Header className='cvat-annotation-header'>
<StandardWorkspaceComponent /> <AnnotationTopBarContainer />
</Layout.Header>
{ workspace === Workspace.STANDARD ? (
<Layout.Content>
<StandardWorkspaceComponent />
</Layout.Content>
) : (
<Layout.Content>
<AttributeAnnotationWorkspace />
</Layout.Content>
)}
<StatisticsModalContainer /> <StatisticsModalContainer />
</Layout> </Layout>
); );

@ -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 (
<Select
className='cvat-annotations-filters-input'
allowClear
value={annotationsFilters}
mode='tags'
style={{ width: '100%' }}
placeholder={(
<>
<Icon type='filter' />
<span style={{ marginLeft: 5 }}>Annotations filter</span>
</>
)}
onChange={changeAnnotationsFilters}
>
{annotationsFiltersHistory.map((element: string): JSX.Element => (
<Select.Option key={element} value={element}>{element}</Select.Option>
))}
</Select>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(AnnotationsFiltersInput);

@ -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 (
<Layout.Sider {...siderProps}>
<GlobalHotKeys keyMap={keyMap as any as KeyMap} handlers={handlers} allowChanges />
<Row>
<Col>
<AnnotationsFiltersInput />
</Col>
</Row>
<ObjectSwitcher
currentLabel={activeObjectState.label.name}
clientID={activeObjectState.clientID}
occluded={activeObjectState.occluded}
objectsCount={states.length}
currentIndex={states.indexOf(activeObjectState)}
nextObject={nextObject}
/>
<ObjectBasicsEditor
currentLabel={activeObjectState.label.name}
labels={labels}
occluded={activeObjectState.occluded}
changeLabel={(value: SelectValue): void => {
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
? (
<>
<AttributeSwitcher
currentAttribute={activeAttribute.name}
currentIndex={activeObjectState.label.attributes
.indexOf(activeAttribute)}
attributesCount={activeObjectState.label.attributes.length}
nextAttribute={nextAttribute}
/>
<AttributeEditor
attribute={activeAttribute}
currentValue={activeObjectState.attributes[activeAttribute.id]}
onChange={(value: string) => {
const { attributes } = activeObjectState;
attributes[activeAttribute.id] = value;
activeObjectState.attributes = attributes;
updateAnnotations([activeObjectState]);
}}
/>
</>
) : (
<div className='attribute-annotations-sidebar-not-found-wrapper'>
<Text strong>No attributes found</Text>
</div>
)
}
</Layout.Sider>
);
}
return (
<Layout.Sider {...siderProps}>
<div className='attribute-annotations-sidebar-not-found-wrapper'>
<Text strong>No objects found</Text>
</div>
</Layout.Sider>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(AttributeAnnotationSidebar);

@ -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<Input | InputNumber>;
}
function renderInputElement(parameters: InputElementParameters): JSX.Element {
const {
inputType,
attrID,
values,
currentValue,
onChange,
ref,
} = parameters;
const renderCheckbox = (): JSX.Element => (
<>
<Text strong>Checkbox: </Text>
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<Checkbox
onChange={(event: CheckboxChangeEvent): void => (
onChange(event.target.checked ? 'true' : 'false')
)}
checked={currentValue === 'true'}
/>
</div>
</>
);
const renderSelect = (): JSX.Element => (
<>
<Text strong>Values: </Text>
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<Select
value={currentValue}
style={{ width: '80%' }}
onChange={(value: SelectValue) => (
onChange(value as string)
)}
>
{values.map((value: string): JSX.Element => (
<Select.Option key={value} value={value}>{value}</Select.Option>
))}
</Select>
</div>
</>
);
const renderRadio = (): JSX.Element => (
<>
<Text strong>Values: </Text>
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<Radio.Group
value={currentValue}
onChange={(event: RadioChangeEvent) => (
onChange(event.target.value)
)}
>
{values.map((value: string): JSX.Element => (
<Radio style={{ display: 'block' }} key={value} value={value}>{value}</Radio>
))}
</Radio.Group>
</div>
</>
);
const handleKeydown = (event: React.KeyboardEvent<HTMLInputElement>): 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' ? <Text strong>Number: </Text> : <Text strong>Text: </Text>}
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<Input
autoFocus
key={attrID}
defaultValue={currentValue}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
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<Input>}
/>
</div>
</>
);
let element = null;
if (inputType === 'checkbox') {
element = renderCheckbox();
} else if (inputType === 'select') {
element = renderSelect();
} else if (inputType === 'radio') {
element = renderRadio();
} else {
element = renderText();
}
return (
<div className='attribute-annotation-sidebar-attr-editor'>
{element}
</div>
);
}
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 (
<div className='attribute-annotation-sidebar-attr-list-wrapper'>
<GlobalHotKeys keyMap={keyMap as KeyMap} handlers={handlers} allowChanges />
<div>
<Text strong>0:</Text>
<Text>{` ${sortedValues[0]}`}</Text>
</div>
<div>
<Text strong>1:</Text>
<Text>{` ${sortedValues[1]}`}</Text>
</div>
</div>
);
}
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 (
<div className='attribute-annotation-sidebar-attr-list-wrapper'>
<GlobalHotKeys keyMap={keyMap as KeyMap} handlers={handlers} allowChanges />
{values.map((value: string, index: number): JSX.Element => (
<div key={value}>
<Text strong>{`${index}:`}</Text>
<Text>{` ${value}`}</Text>
</div>
))}
</div>
);
}
if (inputType === 'number') {
return (
<div className='attribute-annotation-sidebar-attr-list-wrapper'>
<div>
<Text strong>From:</Text>
<Text>{` ${values[0]}`}</Text>
</div>
<div>
<Text strong>To:</Text>
<Text>{` ${values[1]}`}</Text>
</div>
<div>
<Text strong>Step:</Text>
<Text>{` ${values[2]}`}</Text>
</div>
</div>
);
}
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<InputNumber>()
: React.createRef<Input>();
return (
<div>
{renderList({ values, inputType, onChange })}
<hr />
{renderInputElement({
attrID,
ref,
inputType,
currentValue,
values,
onChange,
})}
</div>
);
}
export default React.memo(AttributeEditor);

@ -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 (
<div className='attribute-annotation-sidebar-switcher'>
<Button disabled={attributesCount <= 1} onClick={() => nextAttribute(-1)}>
<Icon type='left' />
</Button>
<Tooltip title={title}>
<Text className='cvat-text'>{currentAttribute}</Text>
<Text strong>{` [${currentIndex + 1}/${attributesCount}]`}</Text>
</Tooltip>
<Button disabled={attributesCount <= 1} onClick={() => nextAttribute(1)}>
<Icon type='right' />
</Button>
</div>
);
}
export default React.memo(AttributeSwitcher);

@ -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 (
<div className='attribute-annotation-sidebar-basics-editor'>
<Select value={currentLabel} onChange={changeLabel} style={{ width: '50%' }}>
{labels.map((label: any): JSX.Element => (
<Select.Option
value={label.name}
key={label.name}
>
{label.name}
</Select.Option>
))}
</Select>
<Checkbox checked={occluded} onChange={setOccluded}>Occluded</Checkbox>
</div>
);
}
export default React.memo(ObjectBasicsEditor);

@ -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 (
<div className='attribute-annotation-sidebar-switcher'>
<Button disabled={objectsCount <= 1} onClick={() => nextObject(-1)}>
<Icon type='left' />
</Button>
<Tooltip title={title}>
<Text className='cvat-text'>{currentLabel}</Text>
<Text className='cvat-text'>{` ${clientID} `}</Text>
<Text strong>{`[${currentIndex + 1}/${objectsCount}]`}</Text>
</Tooltip>
<Button disabled={objectsCount <= 1} onClick={() => nextObject(1)}>
<Icon type='right' />
</Button>
</div>
);
}
export default React.memo(ObjectSwitcher);

@ -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 (
<Layout hasSider className='attribute-annotation-workspace'>
<CanvasWrapperContainer />
<AttributeAnnotationSidebar />
</Layout>
);
}

@ -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;
}

@ -18,9 +18,16 @@ import {
GridColor, GridColor,
ObjectType, ObjectType,
ContextMenuType, ContextMenuType,
Workspace
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import { Canvas } from 'cvat-canvas'; import { Canvas } from 'cvat-canvas';
import getCore from 'cvat-core'; import getCore from 'cvat-core';
import {
ColorBy,
GridColor,
ObjectType,
Workspace,
} from 'reducers/interfaces';
const cvat = getCore(); const cvat = getCore();
@ -31,6 +38,7 @@ interface Props {
canvasInstance: Canvas; canvasInstance: Canvas;
jobInstance: any; jobInstance: any;
activatedStateID: number | null; activatedStateID: number | null;
activatedAttributeID: number | null;
selectedStatesID: number[]; selectedStatesID: number[];
annotations: any[]; annotations: any[];
frameData: any; frameData: any;
@ -55,6 +63,8 @@ interface Props {
resetZoom: boolean; resetZoom: boolean;
contextVisible: boolean; contextVisible: boolean;
contextType: ContextMenuType; contextType: ContextMenuType;
aamZoomMargin: number;
workspace: Workspace;
onSetupCanvas: () => void; onSetupCanvas: () => void;
onDragCanvas: (enabled: boolean) => void; onDragCanvas: (enabled: boolean) => void;
onZoomCanvas: (enabled: boolean) => void; onZoomCanvas: (enabled: boolean) => void;
@ -64,7 +74,7 @@ interface Props {
onEditShape: (enabled: boolean) => void; onEditShape: (enabled: boolean) => void;
onShapeDrawn: () => void; onShapeDrawn: () => void;
onResetCanvas: () => void; onResetCanvas: () => void;
onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void; onUpdateAnnotations(states: any[]): void;
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void; onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void; onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onGroupAnnotations(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<Props> {
brightnessLevel, brightnessLevel,
contrastLevel, contrastLevel,
saturationLevel, saturationLevel,
workspace,
} = this.props; } = this.props;
if (prevProps.sidebarCollapsed !== sidebarCollapsed) { if (prevProps.sidebarCollapsed !== sidebarCollapsed) {
@ -168,11 +179,18 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
} }
if (prevProps.curZLayer !== curZLayer) {
canvasInstance.setZLayer(curZLayer);
}
if (prevProps.annotations !== annotations || prevProps.frameData !== frameData) { if (prevProps.annotations !== annotations || prevProps.frameData !== frameData) {
this.updateCanvas(); this.updateCanvas();
} }
if (prevProps.frame !== frameData.number && resetZoom) { if (prevProps.frame !== frameData.number
&& resetZoom
&& workspace !== Workspace.ATTRIBUTE_ANNOTATION
) {
canvasInstance.html().addEventListener('canvas.setup', () => { canvasInstance.html().addEventListener('canvas.setup', () => {
canvasInstance.fit(); canvasInstance.fit();
}, { once: true }); }, { once: true });
@ -183,10 +201,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
this.updateShapesView(); this.updateShapesView();
} }
if (prevProps.curZLayer !== curZLayer) {
canvasInstance.setZLayer(curZLayer);
}
if (prevProps.frameAngle !== frameAngle) { if (prevProps.frameAngle !== frameAngle) {
canvasInstance.rotate(frameAngle); canvasInstance.rotate(frameAngle);
} }
@ -195,10 +209,34 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
public componentWillUnmount(): void { 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); window.removeEventListener('resize', this.fitCanvas);
} }
private onShapeDrawn(event: any): void { private onCanvasShapeDrawn = (event: any): void => {
const { const {
jobInstance, jobInstance,
activeLabelID, activeLabelID,
@ -229,27 +267,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
state.frame = frame; state.frame = frame;
const objectState = new cvat.classes.ObjectState(state); const objectState = new cvat.classes.ObjectState(state);
onCreateAnnotations(jobInstance, frame, [objectState]); 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 { const {
jobInstance, jobInstance,
frame, frame,
@ -261,9 +281,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const { states } = event.detail; const { states } = event.detail;
onMergeAnnotations(jobInstance, frame, states); onMergeAnnotations(jobInstance, frame, states);
} };
private onObjectsGroupped(event: any): void { private onCanvasObjectsGroupped = (event: any): void => {
const { const {
jobInstance, jobInstance,
frame, frame,
@ -275,9 +295,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const { states } = event.detail; const { states } = event.detail;
onGroupAnnotations(jobInstance, frame, states); onGroupAnnotations(jobInstance, frame, states);
} };
private onTrackSplitted(event: any): void { private onCanvasTrackSplitted = (event: any): void => {
const { const {
jobInstance, jobInstance,
frame, frame,
@ -289,22 +309,179 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const { state } = event.detail; const { state } = event.detail;
onSplitAnnotations(jobInstance, frame, state); onSplitAnnotations(jobInstance, frame, state);
} };
private fitCanvas = (): void => { private fitCanvas = (): void => {
const { canvasInstance } = this.props; const { canvasInstance } = this.props;
canvasInstance.fitCanvas(); 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<void> => {
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<void> => {
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 { private activateOnCanvas(): void {
const { const {
activatedStateID, activatedStateID,
activatedAttributeID,
canvasInstance, canvasInstance,
selectedOpacity, selectedOpacity,
aamZoomMargin,
workspace,
annotations,
} = this.props; } = this.props;
if (activatedStateID !== null) { 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}`); const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`);
if (el) { if (el) {
(el as any as SVGElement).setAttribute('fill-opacity', `${selectedOpacity / 100}`); (el as any as SVGElement).setAttribute('fill-opacity', `${selectedOpacity / 100}`);
@ -352,7 +529,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} = this.props; } = this.props;
if (frameData !== null) { if (frameData !== null) {
canvasInstance.setup(frameData, annotations); canvasInstance.setup(frameData, annotations
.filter((e) => e.objectType !== ObjectType.TAG));
canvasInstance.rotate(frameAngle); canvasInstance.rotate(frameAngle);
} }
} }
@ -364,14 +542,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
gridColor, gridColor,
gridOpacity, gridOpacity,
canvasInstance, canvasInstance,
jobInstance,
onSetupCanvas,
onDragCanvas,
onZoomCanvas,
onResetCanvas,
onActivateObject,
onUpdateContextMenu,
onEditShape,
brightnessLevel, brightnessLevel,
contrastLevel, contrastLevel,
saturationLevel, saturationLevel,
@ -402,143 +572,33 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
// Events // 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', () => { canvasInstance.html().addEventListener('canvas.setup', () => {
const { activatedStateID, activatedAttributeID } = this.props;
canvasInstance.fit(); canvasInstance.fit();
canvasInstance.activate(activatedStateID, activatedAttributeID);
}, { once: true }); }, { once: true });
canvasInstance.html().addEventListener('canvas.canceled', () => { canvasInstance.html().addEventListener('mousedown', this.onCanvasMouseDown);
onResetCanvas(); canvasInstance.html().addEventListener('click', this.onCanvasClicked);
}); canvasInstance.html().addEventListener('contextmenu', this.onCanvasContextMenu);
canvasInstance.html().addEventListener('canvas.editstart', this.onCanvasEditStart);
canvasInstance.html().addEventListener('canvas.dragstart', () => { canvasInstance.html().addEventListener('canvas.edited', this.onCanvasEditDone);
onDragCanvas(true); 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.dragstop', () => { canvasInstance.html().addEventListener('canvas.zoomstop', this.onCanvasZoomDone);
onDragCanvas(false);
}); canvasInstance.html().addEventListener('canvas.setup', this.onCanvasSetup);
canvasInstance.html().addEventListener('canvas.canceled', this.onCanvasCancel);
canvasInstance.html().addEventListener('canvas.zoomstart', () => { canvasInstance.html().addEventListener('canvas.find', this.onCanvasFindObject);
onZoomCanvas(true); canvasInstance.html().addEventListener('canvas.deactivated', this.onCanvasShapeDeactivated);
}); canvasInstance.html().addEventListener('canvas.moved', this.onCanvasCursorMoved);
canvasInstance.html().addEventListener('canvas.zoomstop', () => { canvasInstance.html().addEventListener('canvas.clicked', this.onCanvasShapeClicked);
onZoomCanvas(false); 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.clicked', (e: any) => { canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted);
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<void> => {
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);
});
} }
public render(): JSX.Element { public render(): JSX.Element {

@ -6,9 +6,7 @@ import React from 'react';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys'; import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import { import {
Icon,
Layout, Layout,
Tooltip,
} from 'antd'; } from 'antd';
import { import {
@ -16,10 +14,6 @@ import {
Rotation, Rotation,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import {
TagIcon,
} from 'icons';
import { import {
Canvas, Canvas,
} from 'cvat-canvas'; } from 'cvat-canvas';
@ -33,6 +27,7 @@ import DrawRectangleControl from './draw-rectangle-control';
import DrawPolygonControl from './draw-polygon-control'; import DrawPolygonControl from './draw-polygon-control';
import DrawPolylineControl from './draw-polyline-control'; import DrawPolylineControl from './draw-polyline-control';
import DrawPointsControl from './draw-points-control'; import DrawPointsControl from './draw-points-control';
import SetupTagControl from './setup-tag-control';
import MergeControl from './merge-control'; import MergeControl from './merge-control';
import GroupControl from './group-control'; import GroupControl from './group-control';
import SplitControl from './split-control'; import SplitControl from './split-control';
@ -221,9 +216,10 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
isDrawing={activeControl === ActiveControl.DRAW_POINTS} isDrawing={activeControl === ActiveControl.DRAW_POINTS}
/> />
<Tooltip title='Setup a tag' placement='right'> <SetupTagControl
<Icon component={TagIcon} style={{ pointerEvents: 'none', opacity: 0.5 }} /> canvasInstance={canvasInstance}
</Tooltip> isDrawing={false}
/>
<hr /> <hr />

@ -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 (
<Popover
{...dynamcPopoverPros}
placement='right'
overlayClassName='cvat-draw-shape-popover'
content={(
<SetupTagPopoverContainer />
)}
>
<Icon
component={TagIcon}
/>
</Popover>
);
}
export default React.memo(SetupTagControl);

@ -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 (
<div className='cvat-draw-shape-popover-content'>
<Row type='flex' justify='start'>
<Col>
<Text className='cvat-text-color' strong>Setup tag</Text>
</Col>
</Row>
<Row type='flex' justify='start'>
<Col>
<Text className='cvat-text-color'>Label</Text>
</Col>
</Row>
<Row type='flex' justify='center'>
<Col span={24}>
<Select
value={`${selectedLabeID}`}
onChange={onChangeLabel}
>
{
labels.map((label: any) => (
<Select.Option
key={label.id}
value={`${label.id}`}
>
{label.name}
</Select.Option>
))
}
</Select>
</Col>
</Row>
<Row type='flex' justify='space-around'>
<Col span={24}>
<Button onClick={() => onSetup(selectedLabeID)}>
Tag
</Button>
</Col>
</Row>
</div>
);
}
export default React.memo(setupTagPopover);

@ -43,6 +43,7 @@ import {
function ItemMenu( function ItemMenu(
serverID: number | undefined, serverID: number | undefined,
locked: boolean, locked: boolean,
objectType: ObjectType,
copy: (() => void), copy: (() => void),
remove: (() => void), remove: (() => void),
propagate: (() => void), propagate: (() => void),
@ -67,18 +68,22 @@ function ItemMenu(
Propagate Propagate
</Button> </Button>
</Menu.Item> </Menu.Item>
<Menu.Item> { objectType !== ObjectType.TAG && (
<Button type='link' onClick={toBackground}> <>
<Icon component={BackgroundIcon} /> <Menu.Item>
To background <Button type='link' onClick={toBackground}>
</Button> <Icon component={BackgroundIcon} />
</Menu.Item> To background
<Menu.Item> </Button>
<Button type='link' onClick={toForeground}> </Menu.Item>
<Icon component={ForegroundIcon} /> <Menu.Item>
To foreground <Button type='link' onClick={toForeground}>
</Button> <Icon component={ForegroundIcon} />
</Menu.Item> To foreground
</Button>
</Menu.Item>
</>
)}
<Menu.Item> <Menu.Item>
<Button <Button
type='link' type='link'
@ -109,6 +114,7 @@ interface ItemTopComponentProps {
serverID: number | undefined; serverID: number | undefined;
labelID: number; labelID: number;
labels: any[]; labels: any[];
objectType: ObjectType;
type: string; type: string;
locked: boolean; locked: boolean;
changeLabel(labelID: string): void; changeLabel(labelID: string): void;
@ -126,6 +132,7 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
serverID, serverID,
labelID, labelID,
labels, labels,
objectType,
type, type,
locked, locked,
changeLabel, changeLabel,
@ -159,6 +166,7 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
overlay={ItemMenu( overlay={ItemMenu(
serverID, serverID,
locked, locked,
objectType,
copy, copy,
remove, remove,
propagate, propagate,
@ -302,6 +310,22 @@ function ItemButtonsComponent(props: ItemButtonsComponentProps): JSX.Element {
); );
} }
if (objectType === ObjectType.TAG) {
return (
<Row type='flex' align='middle' justify='space-around'>
<Col span={20} style={{ textAlign: 'center' }}>
<Row type='flex' justify='space-around'>
<Col>
{ locked
? <Icon type='lock' onClick={unlock} />
: <Icon type='unlock' onClick={lock} />}
</Col>
</Row>
</Col>
</Row>
);
}
return ( return (
<Row type='flex' align='middle' justify='space-around'> <Row type='flex' align='middle' justify='space-around'>
<Col span={20} style={{ textAlign: 'center' }}> <Col span={20} style={{ textAlign: 'center' }}>
@ -714,7 +738,6 @@ function ObjectItemComponent(props: Props): JSX.Element {
style={{ background: ` ${color}` }} style={{ background: ` ${color}` }}
/> />
</Popover> </Popover>
<div <div
onMouseEnter={activate} onMouseEnter={activate}
id={`cvat-objects-sidebar-state-item-${clientID}`} id={`cvat-objects-sidebar-state-item-${clientID}`}
@ -726,6 +749,7 @@ function ObjectItemComponent(props: Props): JSX.Element {
clientID={clientID} clientID={clientID}
labelID={labelID} labelID={labelID}
labels={labels} labels={labels}
objectType={objectType}
type={type} type={type}
locked={locked} locked={locked}
changeLabel={changeLabel} changeLabel={changeLabel}

@ -3,20 +3,14 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React from 'react';
import { Row, Col } from 'antd/lib/grid';
import { import Icon from 'antd/lib/icon';
Row, import Select from 'antd/lib/select';
Col,
Icon,
Select,
} from 'antd';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { SelectValue } from 'antd/lib/select';
import AnnotationsFiltersInput from 'components/annotation-page/annotations-filters-input';
import { StatesOrdering } from 'reducers/interfaces'; import { StatesOrdering } from 'reducers/interfaces';
interface StatesOrderingSelectorComponentProps { interface StatesOrderingSelectorComponentProps {
statesOrdering: StatesOrdering; statesOrdering: StatesOrdering;
changeStatesOrdering(value: StatesOrdering): void; changeStatesOrdering(value: StatesOrdering): void;
@ -62,10 +56,7 @@ interface Props {
statesLocked: boolean; statesLocked: boolean;
statesCollapsed: boolean; statesCollapsed: boolean;
statesOrdering: StatesOrdering; statesOrdering: StatesOrdering;
annotationsFilters: string[];
annotationsFiltersHistory: string[];
changeStatesOrdering(value: StatesOrdering): void; changeStatesOrdering(value: StatesOrdering): void;
changeAnnotationsFilters(value: SelectValue): void;
lockAllStates(): void; lockAllStates(): void;
unlockAllStates(): void; unlockAllStates(): void;
collapseAllStates(): void; collapseAllStates(): void;
@ -76,8 +67,6 @@ interface Props {
function ObjectListHeader(props: Props): JSX.Element { function ObjectListHeader(props: Props): JSX.Element {
const { const {
annotationsFilters,
annotationsFiltersHistory,
statesHidden, statesHidden,
statesLocked, statesLocked,
statesCollapsed, statesCollapsed,
@ -89,30 +78,13 @@ function ObjectListHeader(props: Props): JSX.Element {
expandAllStates, expandAllStates,
hideAllStates, hideAllStates,
showAllStates, showAllStates,
changeAnnotationsFilters,
} = props; } = props;
return ( return (
<div className='cvat-objects-sidebar-states-header'> <div className='cvat-objects-sidebar-states-header'>
<Row> <Row>
<Col> <Col>
<Select <AnnotationsFiltersInput />
allowClear
value={annotationsFilters}
mode='tags'
style={{ width: '100%' }}
placeholder={(
<>
<Icon type='filter' />
<span style={{ marginLeft: 5 }}>Annotations filter</span>
</>
)}
onChange={changeAnnotationsFilters}
>
{annotationsFiltersHistory.map((element: string): JSX.Element => (
<Select.Option key={element} value={element}>{element}</Select.Option>
))}
</Select>
</Col> </Col>
</Row> </Row>
<Row type='flex' justify='space-between' align='middle'> <Row type='flex' justify='space-between' align='middle'>

@ -4,7 +4,6 @@
import React from 'react'; import React from 'react';
import { SelectValue } from 'antd/lib/select';
import { StatesOrdering } from 'reducers/interfaces'; import { StatesOrdering } from 'reducers/interfaces';
import ObjectItemContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-item'; import ObjectItemContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-item';
import ObjectListHeader from './objects-list-header'; import ObjectListHeader from './objects-list-header';
@ -17,10 +16,7 @@ interface Props {
statesCollapsed: boolean; statesCollapsed: boolean;
statesOrdering: StatesOrdering; statesOrdering: StatesOrdering;
sortedStatesID: number[]; sortedStatesID: number[];
annotationsFilters: string[];
annotationsFiltersHistory: string[];
changeStatesOrdering(value: StatesOrdering): void; changeStatesOrdering(value: StatesOrdering): void;
changeAnnotationsFilters(value: SelectValue): void;
lockAllStates(): void; lockAllStates(): void;
unlockAllStates(): void; unlockAllStates(): void;
collapseAllStates(): void; collapseAllStates(): void;
@ -37,10 +33,7 @@ function ObjectListComponent(props: Props): JSX.Element {
statesCollapsed, statesCollapsed,
statesOrdering, statesOrdering,
sortedStatesID, sortedStatesID,
annotationsFilters,
annotationsFiltersHistory,
changeStatesOrdering, changeStatesOrdering,
changeAnnotationsFilters,
lockAllStates, lockAllStates,
unlockAllStates, unlockAllStates,
collapseAllStates, collapseAllStates,
@ -56,16 +49,13 @@ function ObjectListComponent(props: Props): JSX.Element {
statesLocked={statesLocked} statesLocked={statesLocked}
statesCollapsed={statesCollapsed} statesCollapsed={statesCollapsed}
statesOrdering={statesOrdering} statesOrdering={statesOrdering}
annotationsFilters={annotationsFilters}
changeStatesOrdering={changeStatesOrdering} changeStatesOrdering={changeStatesOrdering}
changeAnnotationsFilters={changeAnnotationsFilters}
lockAllStates={lockAllStates} lockAllStates={lockAllStates}
unlockAllStates={unlockAllStates} unlockAllStates={unlockAllStates}
collapseAllStates={collapseAllStates} collapseAllStates={collapseAllStates}
expandAllStates={expandAllStates} expandAllStates={expandAllStates}
hideAllStates={hideAllStates} hideAllStates={hideAllStates}
showAllStates={showAllStates} showAllStates={showAllStates}
annotationsFiltersHistory={annotationsFiltersHistory}
/> />
<div className='cvat-objects-sidebar-states-list'> <div className='cvat-objects-sidebar-states-list'>
{ sortedStatesID.map((id: number): JSX.Element => ( { sortedStatesID.map((id: number): JSX.Element => (

@ -69,16 +69,6 @@
> div:nth-child(1) > div:nth-child(1) { > div:nth-child(1) > div:nth-child(1) {
height: 32px; height: 32px;
> .ant-select > div {
height: 32px;
> div {
height: 32px;
ul {
display: flex;
}
}
}
} }
> div:nth-child(2) { > div:nth-child(2) {

@ -17,7 +17,7 @@ import CanvasContextMenuContainer from 'containers/annotation-page/standard-work
export default function StandardWorkspaceComponent(): JSX.Element { export default function StandardWorkspaceComponent(): JSX.Element {
return ( return (
<Layout hasSider> <Layout hasSider className='cvat-standard-workspace'>
<ControlsSideBarContainer /> <ControlsSideBarContainer />
<CanvasWrapperContainer /> <CanvasWrapperContainer />
<ObjectSideBarContainer /> <ObjectSideBarContainer />

@ -4,6 +4,10 @@
@import 'base.scss'; @import 'base.scss';
.cvat-standard-workspace.ant-layout {
height: 100%
}
.cvat-canvas-container { .cvat-canvas-container {
background-color: $background-color-1; background-color: $background-color-1;
} }

@ -213,3 +213,75 @@
width: 15em; width: 15em;
} }
} }
// TODO: Move canvas from standard workspace and create its own .scss
.cvat-canvas-context-menu {
opacity: 0.6;
position: fixed;
width: 300px;
z-index: 10;
max-height: 50%;
overflow-y: auto;
&:hover {
opacity: 1;
}
}
.cvat-canvas-z-axis-wrapper {
position: absolute;
background: $background-color-2;
bottom: 10px;
right: 10px;
height: 150px;
z-index: 100;
border-radius: 6px;
opacity: 0.5;
border: 1px solid $border-color-3;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 3px;
&:hover {
opacity: 1;
}
> .ant-slider {
height: 75%;
margin: 5px 3px;
> .ant-slider-rail {
background-color: #979797;
}
> .ant-slider-handle {
transform: none !important;
}
}
> i {
opacity: 0.7;
color: $objects-bar-icons-color;
&:hover {
opacity: 1;
}
&:active {
opacity: 0.7;
}
}
}
.cvat-annotations-filters-input.ant-select > div {
height: 32px;
> div {
height: 32px;
ul {
display: flex;
}
}
}

@ -11,17 +11,17 @@ import {
Button, Button,
} from 'antd'; } from 'antd';
import { import { Workspace } from 'reducers/interfaces';
InfoIcon, import { InfoIcon, FullscreenIcon } from '../../../icons';
FullscreenIcon,
} from '../../../icons';
interface Props { interface Props {
workspace: Workspace;
showStatistics(): void; showStatistics(): void;
changeWorkspace(workspace: Workspace): void;
} }
function RightGroup(props: Props): JSX.Element { function RightGroup(props: Props): JSX.Element {
const { showStatistics } = props; const { showStatistics, changeWorkspace, workspace } = props;
return ( return (
<Col className='cvat-annotation-header-right-group'> <Col className='cvat-annotation-header-right-group'>
@ -46,9 +46,23 @@ function RightGroup(props: Props): JSX.Element {
Info Info
</Button> </Button>
<div> <div>
<Select disabled className='cvat-workspace-selector' defaultValue='standard'> <Select
<Select.Option key='standard' value='standard'>Standard</Select.Option> className='cvat-workspace-selector'
<Select.Option key='aam' value='aam'>Attribute annotation</Select.Option> onChange={changeWorkspace}
value={workspace}
>
<Select.Option
key={Workspace.STANDARD}
value={Workspace.STANDARD}
>
{Workspace.STANDARD}
</Select.Option>
<Select.Option
key={Workspace.ATTRIBUTE_ANNOTATION}
value={Workspace.ATTRIBUTE_ANNOTATION}
>
{Workspace.ATTRIBUTE_ANNOTATION}
</Select.Option>
</Select> </Select>
</div> </div>
</Col> </Col>

@ -7,12 +7,12 @@ import React from 'react';
import { import {
Row, Row,
Col, Col,
Layout,
InputNumber, InputNumber,
} from 'antd'; } from 'antd';
import { SliderValue } from 'antd/lib/slider'; import { SliderValue } from 'antd/lib/slider';
import { Workspace } from 'reducers/interfaces';
import LeftGroup from './left-group'; import LeftGroup from './left-group';
import RightGroup from './right-group'; import RightGroup from './right-group';
import PlayerNavigation from './player-navigation'; import PlayerNavigation from './player-navigation';
@ -28,6 +28,8 @@ interface Props {
stopFrame: number; stopFrame: number;
undoAction?: string; undoAction?: string;
redoAction?: string; redoAction?: string;
workspace: Workspace;
changeWorkspace(workspace: Workspace): void;
showStatistics(): void; showStatistics(): void;
onSwitchPlay(): void; onSwitchPlay(): void;
onSaveAnnotation(): void; onSaveAnnotation(): void;
@ -55,7 +57,9 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
inputFrameRef, inputFrameRef,
startFrame, startFrame,
stopFrame, stopFrame,
workspace,
showStatistics, showStatistics,
changeWorkspace,
onSwitchPlay, onSwitchPlay,
onSaveAnnotation, onSaveAnnotation,
onPrevFrame, onPrevFrame,
@ -72,42 +76,44 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
} = props; } = props;
return ( return (
<Layout.Header className='cvat-annotation-header'> <Row type='flex' justify='space-between'>
<Row type='flex' justify='space-between'> <LeftGroup
<LeftGroup saving={saving}
saving={saving} savingStatuses={savingStatuses}
savingStatuses={savingStatuses} onSaveAnnotation={onSaveAnnotation}
onSaveAnnotation={onSaveAnnotation} undoAction={undoAction}
undoAction={undoAction} redoAction={redoAction}
redoAction={redoAction} onUndoClick={onUndoClick}
onUndoClick={onUndoClick} onRedoClick={onRedoClick}
onRedoClick={onRedoClick} />
/> <Col className='cvat-annotation-header-player-group'>
<Col className='cvat-annotation-header-player-group'> <Row type='flex' align='middle'>
<Row type='flex' align='middle'> <PlayerButtons
<PlayerButtons playing={playing}
playing={playing} onPrevFrame={onPrevFrame}
onPrevFrame={onPrevFrame} onNextFrame={onNextFrame}
onNextFrame={onNextFrame} onForward={onForward}
onForward={onForward} onBackward={onBackward}
onBackward={onBackward} onFirstFrame={onFirstFrame}
onFirstFrame={onFirstFrame} onLastFrame={onLastFrame}
onLastFrame={onLastFrame} onSwitchPlay={onSwitchPlay}
onSwitchPlay={onSwitchPlay} />
/> <PlayerNavigation
<PlayerNavigation startFrame={startFrame}
startFrame={startFrame} stopFrame={stopFrame}
stopFrame={stopFrame} frameNumber={frameNumber}
frameNumber={frameNumber} inputFrameRef={inputFrameRef}
inputFrameRef={inputFrameRef} onSliderChange={onSliderChange}
onSliderChange={onSliderChange} onInputChange={onInputChange}
onInputChange={onInputChange} onURLIconClick={onURLIconClick}
onURLIconClick={onURLIconClick} />
/> </Row>
</Row> </Col>
</Col> <RightGroup
<RightGroup showStatistics={showStatistics} /> workspace={workspace}
</Row> changeWorkspace={changeWorkspace}
</Layout.Header> showStatistics={showStatistics}
/>
</Row>
); );
} }

@ -93,7 +93,7 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
max={1000} max={1000}
value={aamZoomMargin} value={aamZoomMargin}
onChange={(value: number | undefined): void => { onChange={(value: number | undefined): void => {
if (value) { if (typeof (value) === 'number') {
onChangeAAMZoomMargin(value); onChangeAAMZoomMargin(value);
} }
}} }}

@ -9,9 +9,7 @@ import { RouteComponentProps } from 'react-router';
import AnnotationPageComponent from 'components/annotation-page/annotation-page'; import AnnotationPageComponent from 'components/annotation-page/annotation-page';
import { getJobAsync } from 'actions/annotation-actions'; import { getJobAsync } from 'actions/annotation-actions';
import { import { CombinedState, Workspace } from 'reducers/interfaces';
CombinedState,
} from 'reducers/interfaces';
type OwnProps = RouteComponentProps<{ type OwnProps = RouteComponentProps<{
tid: string; tid: string;
@ -21,6 +19,7 @@ type OwnProps = RouteComponentProps<{
interface StateToProps { interface StateToProps {
job: any | null | undefined; job: any | null | undefined;
fetching: boolean; fetching: boolean;
workspace: Workspace;
} }
interface DispatchToProps { interface DispatchToProps {
@ -36,12 +35,14 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
instance: job, instance: job,
fetching, fetching,
}, },
workspace,
}, },
} = state; } = state;
return { return {
job: !job || jobID === job.id ? job : null, job: !job || jobID === job.id ? job : null,
fetching, fetching,
workspace,
}; };
} }

@ -40,6 +40,7 @@ import {
ObjectType, ObjectType,
CombinedState, CombinedState,
ContextMenuType, ContextMenuType,
Workspace,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import { Canvas } from 'cvat-canvas'; import { Canvas } from 'cvat-canvas';
@ -49,6 +50,7 @@ interface StateToProps {
canvasInstance: Canvas; canvasInstance: Canvas;
jobInstance: any; jobInstance: any;
activatedStateID: number | null; activatedStateID: number | null;
activatedAttributeID: number | null;
selectedStatesID: number[]; selectedStatesID: number[];
annotations: any[]; annotations: any[];
frameData: any; frameData: any;
@ -68,6 +70,8 @@ interface StateToProps {
contrastLevel: number; contrastLevel: number;
saturationLevel: number; saturationLevel: number;
resetZoom: boolean; resetZoom: boolean;
aamZoomMargin: number;
workspace: Workspace;
minZLayer: number; minZLayer: number;
maxZLayer: number; maxZLayer: number;
curZLayer: number; curZLayer: number;
@ -85,7 +89,7 @@ interface DispatchToProps {
onGroupObjects: (enabled: boolean) => void; onGroupObjects: (enabled: boolean) => void;
onSplitTrack: (enabled: boolean) => void; onSplitTrack: (enabled: boolean) => void;
onEditShape: (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; onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void; onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onGroupAnnotations(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: { annotations: {
states: annotations, states: annotations,
activatedStateID, activatedStateID,
activatedAttributeID,
selectedStatesID, selectedStatesID,
zLayer: { zLayer: {
cur: curZLayer, cur: curZLayer,
@ -138,6 +143,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
}, },
}, },
sidebarCollapsed, sidebarCollapsed,
workspace,
}, },
settings: { settings: {
player: { player: {
@ -150,6 +156,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
saturationLevel, saturationLevel,
resetZoom, resetZoom,
}, },
workspace: {
aamZoomMargin,
},
shapes: { shapes: {
opacity, opacity,
colorBy, colorBy,
@ -167,6 +176,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
frameAngle: frameAngles[frame - jobInstance.startFrame], frameAngle: frameAngles[frame - jobInstance.startFrame],
frame, frame,
activatedStateID, activatedStateID,
activatedAttributeID,
selectedStatesID, selectedStatesID,
annotations, annotations,
opacity, opacity,
@ -183,11 +193,13 @@ function mapStateToProps(state: CombinedState): StateToProps {
contrastLevel, contrastLevel,
saturationLevel, saturationLevel,
resetZoom, resetZoom,
aamZoomMargin,
curZLayer, curZLayer,
minZLayer, minZLayer,
maxZLayer, maxZLayer,
contextVisible, contextVisible,
contextType, contextType,
workspace,
}; };
} }
@ -220,8 +232,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onEditShape(enabled: boolean): void { onEditShape(enabled: boolean): void {
dispatch(editShape(enabled)); dispatch(editShape(enabled));
}, },
onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void { onUpdateAnnotations(states: any[]): void {
dispatch(updateAnnotationsAsync(sessionInstance, frame, states)); dispatch(updateAnnotationsAsync(states));
}, },
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void { onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(createAnnotationsAsync(sessionInstance, frame, states)); dispatch(createAnnotationsAsync(sessionInstance, frame, states));
@ -240,7 +252,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
dispatch(updateCanvasContextMenu(false, 0, 0)); dispatch(updateCanvasContextMenu(false, 0, 0));
} }
dispatch(activateObject(activatedStateID)); dispatch(activateObject(activatedStateID, null));
}, },
onSelectObjects(selectedStatesID: number[]): void { onSelectObjects(selectedStatesID: number[]): void {
dispatch(selectObjects(selectedStatesID)); dispatch(selectObjects(selectedStatesID));

@ -13,7 +13,7 @@ import {
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import { import {
drawShape, rememberObject,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import { Canvas, RectDrawingMethod } from 'cvat-canvas'; import { Canvas, RectDrawingMethod } from 'cvat-canvas';
import DrawShapePopoverComponent from 'components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover'; 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, points?: number,
rectDrawingMethod?: RectDrawingMethod, rectDrawingMethod?: RectDrawingMethod,
): void { ): void {
dispatch(drawShape(shapeType, labelID, objectType, points, rectDrawingMethod)); dispatch(rememberObject(objectType, labelID, shapeType, points, rectDrawingMethod));
}, },
}; };
} }

@ -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<Props, State> {
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 (
<SetupTagPopoverComponent
labels={labels}
selectedLabeID={selectedLabelID}
onChangeLabel={this.onChangeLabel}
onSetup={this.onSetup}
/>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(DrawShapePopoverContainer);

@ -29,7 +29,7 @@ interface StateToProps {
} }
interface DispatchToProps { interface DispatchToProps {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void; updateAnnotations(states: any[]): void;
changeLabelColor(sessionInstance: any, frameNumber: number, label: any, color: string): 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 { function mapDispatchToProps(dispatch: any): DispatchToProps {
return { return {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void { updateAnnotations(states: any[]): void {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, states)); dispatch(updateAnnotationsAsync(states));
}, },
changeLabelColor( changeLabelColor(
sessionInstance: any, sessionInstance: any,
@ -162,8 +162,6 @@ class LabelItemContainer extends React.PureComponent<Props, State> {
private switchHidden(value: boolean): void { private switchHidden(value: boolean): void {
const { const {
updateAnnotations, updateAnnotations,
jobInstance,
frameNumber,
} = this.props; } = this.props;
const { ownObjectStates } = this.state; const { ownObjectStates } = this.state;
@ -171,14 +169,12 @@ class LabelItemContainer extends React.PureComponent<Props, State> {
state.hidden = value; state.hidden = value;
} }
updateAnnotations(jobInstance, frameNumber, ownObjectStates); updateAnnotations(ownObjectStates);
} }
private switchLock(value: boolean): void { private switchLock(value: boolean): void {
const { const {
updateAnnotations, updateAnnotations,
jobInstance,
frameNumber,
} = this.props; } = this.props;
const { ownObjectStates } = this.state; const { ownObjectStates } = this.state;
@ -186,7 +182,7 @@ class LabelItemContainer extends React.PureComponent<Props, State> {
state.lock = value; state.lock = value;
} }
updateAnnotations(jobInstance, frameNumber, ownObjectStates); updateAnnotations(ownObjectStates);
} }
public render(): JSX.Element { public render(): JSX.Element {

@ -13,6 +13,7 @@ import {
import { import {
collapseObjectItems, collapseObjectItems,
changeLabelColorAsync, changeLabelColorAsync,
createAnnotationsAsync,
updateAnnotationsAsync, updateAnnotationsAsync,
changeFrameAsync, changeFrameAsync,
removeObjectAsync, removeObjectAsync,
@ -25,6 +26,7 @@ import {
import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item'; import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item';
interface OwnProps { interface OwnProps {
clientID: number; clientID: number;
} }
@ -47,14 +49,15 @@ interface StateToProps {
interface DispatchToProps { interface DispatchToProps {
changeFrame(frame: number): void; 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; collapseOrExpand(objectStates: any[], collapsed: boolean): void;
activateObject: (activatedStateID: number | null) => void; activateObject: (activatedStateID: number | null) => void;
removeObject: (sessionInstance: any, objectState: any) => void; removeObject: (sessionInstance: any, objectState: any) => void;
copyShape: (objectState: any) => void; copyShape: (objectState: any) => void;
propagateObject: (objectState: any) => void; propagateObject: (objectState: any) => void;
changeLabelColor(sessionInstance: any, frameNumber: number, label: any, color: string): 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 { function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
@ -121,14 +124,17 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
changeFrame(frame: number): void { changeFrame(frame: number): void {
dispatch(changeFrameAsync(frame)); dispatch(changeFrameAsync(frame));
}, },
updateState(sessionInstance: any, frameNumber: number, state: any): void { updateState(state: any): void {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, [state])); dispatch(updateAnnotationsAsync([state]));
},
createAnnotations(sessionInstance: any, frameNumber: number, state: any): void {
dispatch(createAnnotationsAsync(sessionInstance, frameNumber, state));
}, },
collapseOrExpand(objectStates: any[], collapsed: boolean): void { collapseOrExpand(objectStates: any[], collapsed: boolean): void {
dispatch(collapseObjectItems(objectStates, collapsed)); dispatch(collapseObjectItems(objectStates, collapsed));
}, },
activateObject(activatedStateID: number | null): void { activateObject(activatedStateID: number | null): void {
dispatch(activateObjectAction(activatedStateID)); dispatch(activateObjectAction(activatedStateID, null));
}, },
removeObject(sessionInstance: any, objectState: any): void { removeObject(sessionInstance: any, objectState: any): void {
dispatch(removeObjectAsync(sessionInstance, objectState, true)); dispatch(removeObjectAsync(sessionInstance, objectState, true));
@ -148,13 +154,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
): void { ): void {
dispatch(changeLabelColorAsync(sessionInstance, frameNumber, label, color)); dispatch(changeLabelColorAsync(sessionInstance, frameNumber, label, color));
}, },
changeGroupColor( changeGroupColor(group: number, color: string): void {
sessionInstance: any, dispatch(changeGroupColorAsync(group, color));
frameNumber: number,
group: number,
color: string,
): void {
dispatch(changeGroupColorAsync(sessionInstance, frameNumber, group, color));
}, },
}; };
} }
@ -386,7 +387,7 @@ class ObjectItemContainer extends React.PureComponent<Props> {
objectState.color = color; objectState.color = color;
this.commit(); this.commit();
} else if (colorBy === ColorBy.GROUP) { } else if (colorBy === ColorBy.GROUP) {
changeGroupColor(jobInstance, frameNumber, objectState.group.id, color); changeGroupColor(objectState.group.id, color);
} else if (colorBy === ColorBy.LABEL) { } else if (colorBy === ColorBy.LABEL) {
changeLabelColor(jobInstance, frameNumber, objectState.label, color); changeLabelColor(jobInstance, frameNumber, objectState.label, color);
} }
@ -415,11 +416,9 @@ class ObjectItemContainer extends React.PureComponent<Props> {
const { const {
objectState, objectState,
updateState, updateState,
jobInstance,
frameNumber,
} = this.props; } = this.props;
updateState(jobInstance, frameNumber, objectState); updateState(objectState);
} }
public render(): JSX.Element { public render(): JSX.Element {

@ -6,15 +6,11 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys'; 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 ObjectsListComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-list';
import { import {
updateAnnotationsAsync, updateAnnotationsAsync,
fetchAnnotationsAsync,
removeObjectAsync, removeObjectAsync,
changeFrameAsync, changeFrameAsync,
changeAnnotationsFilters as changeAnnotationsFiltersAction,
collapseObjectItems, collapseObjectItems,
copyShape as copyShapeAction, copyShape as copyShapeAction,
propagateObject as propagateObjectAction, propagateObject as propagateObjectAction,
@ -42,8 +38,7 @@ interface StateToProps {
} }
interface DispatchToProps { interface DispatchToProps {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void; updateAnnotations(states: any[]): void;
changeAnnotationsFilters(sessionInstance: any, filters: string[]): void;
collapseStates(states: any[], value: boolean): void; collapseStates(states: any[], value: boolean): void;
removeObject: (sessionInstance: any, objectState: any, force: boolean) => void; removeObject: (sessionInstance: any, objectState: any, force: boolean) => void;
copyShape: (objectState: any) => void; copyShape: (objectState: any) => void;
@ -84,7 +79,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
objectStates.forEach((objectState: any) => { objectStates.forEach((objectState: any) => {
const { clientID, lock } = objectState; const { clientID, lock } = objectState;
if (!lock) { if (!lock) {
statesHidden = statesHidden && objectState.hidden; if (objectState.objectType !== ObjectType.TAG) {
statesHidden = statesHidden && objectState.hidden;
}
statesLocked = statesLocked && objectState.lock; statesLocked = statesLocked && objectState.lock;
} }
const stateCollapsed = clientID in collapsed ? collapsed[clientID] : true; const stateCollapsed = clientID in collapsed ? collapsed[clientID] : true;
@ -109,19 +106,12 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps { function mapDispatchToProps(dispatch: any): DispatchToProps {
return { return {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void { updateAnnotations(states: any[]): void {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, states)); dispatch(updateAnnotationsAsync(states));
}, },
collapseStates(states: any[], collapsed: boolean): void { collapseStates(states: any[], collapsed: boolean): void {
dispatch(collapseObjectItems(states, collapsed)); dispatch(collapseObjectItems(states, collapsed));
}, },
changeAnnotationsFilters(
sessionInstance: any,
filters: string[],
): void {
dispatch(changeAnnotationsFiltersAction(filters));
dispatch(fetchAnnotationsAsync(sessionInstance));
},
removeObject(sessionInstance: any, objectState: any, force: boolean): void { removeObject(sessionInstance: any, objectState: any, force: boolean): void {
dispatch(removeObjectAsync(sessionInstance, objectState, force)); dispatch(removeObjectAsync(sessionInstance, objectState, force));
}, },
@ -188,15 +178,6 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
}); });
}; };
private onChangeAnnotationsFilters = (value: SelectValue): void => {
const {
jobInstance,
changeAnnotationsFilters,
} = this.props;
const filters = value as string[];
changeAnnotationsFilters(jobInstance, filters);
};
private onLockAllStates = (): void => { private onLockAllStates = (): void => {
this.lockAllStates(true); this.lockAllStates(true);
}; };
@ -225,28 +206,24 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const { const {
objectStates, objectStates,
updateAnnotations, updateAnnotations,
jobInstance,
frameNumber,
} = this.props; } = this.props;
for (const objectState of objectStates) { for (const objectState of objectStates) {
objectState.lock = locked; objectState.lock = locked;
} }
updateAnnotations(jobInstance, frameNumber, objectStates); updateAnnotations(objectStates);
} }
private hideAllStates(hidden: boolean): void { private hideAllStates(hidden: boolean): void {
const { const {
objectStates, objectStates,
updateAnnotations, updateAnnotations,
jobInstance,
frameNumber,
} = this.props; } = this.props;
for (const objectState of objectStates) { for (const objectState of objectStates) {
objectState.hidden = hidden; objectState.hidden = hidden;
} }
updateAnnotations(jobInstance, frameNumber, objectStates); updateAnnotations(objectStates);
} }
private collapseAllStates(collapsed: boolean): void { private collapseAllStates(collapsed: boolean): void {
@ -260,12 +237,10 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
public render(): JSX.Element { public render(): JSX.Element {
const { const {
annotationsFilters,
statesHidden, statesHidden,
statesLocked, statesLocked,
activatedStateID, activatedStateID,
objectStates, objectStates,
frameNumber,
jobInstance, jobInstance,
updateAnnotations, updateAnnotations,
removeObject, removeObject,
@ -274,7 +249,6 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
changeFrame, changeFrame,
maxZLayer, maxZLayer,
minZLayer, minZLayer,
annotationsFiltersHistory,
} = this.props; } = this.props;
const { const {
sortedStatesID, sortedStatesID,
@ -397,7 +371,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated(); const state = activatedStated();
if (state) { if (state) {
state.lock = !state.lock; state.lock = !state.lock;
updateAnnotations(jobInstance, frameNumber, [state]); updateAnnotations([state]);
} }
}, },
SWITCH_ALL_HIDDEN: (event: KeyboardEvent | undefined) => { SWITCH_ALL_HIDDEN: (event: KeyboardEvent | undefined) => {
@ -409,7 +383,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated(); const state = activatedStated();
if (state) { if (state) {
state.hidden = !state.hidden; state.hidden = !state.hidden;
updateAnnotations(jobInstance, frameNumber, [state]); updateAnnotations([state]);
} }
}, },
SWITCH_OCCLUDED: (event: KeyboardEvent | undefined) => { SWITCH_OCCLUDED: (event: KeyboardEvent | undefined) => {
@ -417,7 +391,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated(); const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) { if (state && state.objectType !== ObjectType.TAG) {
state.occluded = !state.occluded; state.occluded = !state.occluded;
updateAnnotations(jobInstance, frameNumber, [state]); updateAnnotations([state]);
} }
}, },
SWITCH_KEYFRAME: (event: KeyboardEvent | undefined) => { SWITCH_KEYFRAME: (event: KeyboardEvent | undefined) => {
@ -425,7 +399,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated(); const state = activatedStated();
if (state && state.objectType === ObjectType.TRACK) { if (state && state.objectType === ObjectType.TRACK) {
state.keyframe = !state.keyframe; state.keyframe = !state.keyframe;
updateAnnotations(jobInstance, frameNumber, [state]); updateAnnotations([state]);
} }
}, },
SWITCH_OUTSIDE: (event: KeyboardEvent | undefined) => { SWITCH_OUTSIDE: (event: KeyboardEvent | undefined) => {
@ -433,7 +407,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated(); const state = activatedStated();
if (state && state.objectType === ObjectType.TRACK) { if (state && state.objectType === ObjectType.TRACK) {
state.outside = !state.outside; state.outside = !state.outside;
updateAnnotations(jobInstance, frameNumber, [state]); updateAnnotations([state]);
} }
}, },
DELETE_OBJECT: (event: KeyboardEvent | undefined) => { DELETE_OBJECT: (event: KeyboardEvent | undefined) => {
@ -448,7 +422,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated(); const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) { if (state && state.objectType !== ObjectType.TAG) {
state.zOrder = minZLayer - 1; state.zOrder = minZLayer - 1;
updateAnnotations(jobInstance, frameNumber, [state]); updateAnnotations([state]);
} }
}, },
TO_FOREGROUND: (event: KeyboardEvent | undefined) => { TO_FOREGROUND: (event: KeyboardEvent | undefined) => {
@ -456,20 +430,20 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated(); const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) { if (state && state.objectType !== ObjectType.TAG) {
state.zOrder = maxZLayer + 1; state.zOrder = maxZLayer + 1;
updateAnnotations(jobInstance, frameNumber, [state]); updateAnnotations([state]);
} }
}, },
COPY_SHAPE: (event: KeyboardEvent | undefined) => { COPY_SHAPE: (event: KeyboardEvent | undefined) => {
preventDefault(event); preventDefault(event);
const state = activatedStated(); const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) { if (state) {
copyShape(state); copyShape(state);
} }
}, },
PROPAGATE_OBJECT: (event: KeyboardEvent | undefined) => { PROPAGATE_OBJECT: (event: KeyboardEvent | undefined) => {
preventDefault(event); preventDefault(event);
const state = activatedStated(); const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) { if (state) {
propagateObject(state); propagateObject(state);
} }
}, },
@ -504,10 +478,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
{...this.props} {...this.props}
statesOrdering={statesOrdering} statesOrdering={statesOrdering}
sortedStatesID={sortedStatesID} sortedStatesID={sortedStatesID}
annotationsFilters={annotationsFilters}
changeStatesOrdering={this.onChangeStatesOrdering} changeStatesOrdering={this.onChangeStatesOrdering}
changeAnnotationsFilters={this.onChangeAnnotationsFilters}
annotationsFiltersHistory={annotationsFiltersHistory}
lockAllStates={this.onLockAllStates} lockAllStates={this.onLockAllStates}
unlockAllStates={this.onUnlockAllStates} unlockAllStates={this.onUnlockAllStates}
collapseAllStates={this.onCollapseAllStates} collapseAllStates={this.onCollapseAllStates}

@ -22,10 +22,12 @@ import {
undoActionAsync, undoActionAsync,
redoActionAsync, redoActionAsync,
searchAnnotationsAsync, searchAnnotationsAsync,
changeWorkspace as changeWorkspaceAction,
activateObject,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar'; 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 { interface StateToProps {
jobInstance: any; jobInstance: any;
@ -41,6 +43,7 @@ interface StateToProps {
redoAction?: string; redoAction?: string;
autoSave: boolean; autoSave: boolean;
autoSaveInterval: number; autoSaveInterval: number;
workspace: Workspace;
} }
interface DispatchToProps { interface DispatchToProps {
@ -51,6 +54,7 @@ interface DispatchToProps {
undo(sessionInstance: any, frameNumber: any): void; undo(sessionInstance: any, frameNumber: any): void;
redo(sessionInstance: any, frameNumber: any): void; redo(sessionInstance: any, frameNumber: any): void;
searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void; searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void;
changeWorkspace(workspace: Workspace): void;
} }
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
@ -76,6 +80,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
canvas: { canvas: {
ready: canvasIsReady, ready: canvasIsReady,
}, },
workspace,
}, },
settings: { settings: {
player: { player: {
@ -103,6 +108,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
redoAction: history.redo[history.redo.length - 1], redoAction: history.redo[history.redo.length - 1],
autoSave, autoSave,
autoSaveInterval, autoSaveInterval,
workspace,
}; };
} }
@ -130,6 +136,10 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void { searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void {
dispatch(searchAnnotationsAsync(sessionInstance, frameFrom, frameTo)); 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<Props> {
frameNumber, frameNumber,
undoAction, undoAction,
redoAction, redoAction,
searchAnnotations, workspace,
canvasIsReady, canvasIsReady,
searchAnnotations,
changeWorkspace,
} = this.props; } = this.props;
const preventDefault = (event: KeyboardEvent | undefined): void => { const preventDefault = (event: KeyboardEvent | undefined): void => {
@ -602,6 +614,8 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
onSliderChange={this.onChangePlayerSliderValue} onSliderChange={this.onChangePlayerSliderValue}
onInputChange={this.onChangePlayerInputValue} onInputChange={this.onChangePlayerInputValue}
onURLIconClick={this.onURLIconClick} onURLIconClick={this.onURLIconClick}
changeWorkspace={changeWorkspace}
workspace={workspace}
playing={playing} playing={playing}
saving={saving} saving={saving}
savingStatuses={savingStatuses} savingStatuses={savingStatuses}

@ -13,6 +13,7 @@ import {
ShapeType, ShapeType,
ObjectType, ObjectType,
ContextMenuType, ContextMenuType,
Workspace,
} from './interfaces'; } from './interfaces';
const defaultState: AnnotationState = { const defaultState: AnnotationState = {
@ -56,6 +57,7 @@ const defaultState: AnnotationState = {
annotations: { annotations: {
selectedStatesID: [], selectedStatesID: [],
activatedStateID: null, activatedStateID: null,
activatedAttributeID: null,
saving: { saving: {
uploading: false, uploading: false,
statuses: [], statuses: [],
@ -90,6 +92,7 @@ const defaultState: AnnotationState = {
sidebarCollapsed: false, sidebarCollapsed: false,
appearanceCollapsed: false, appearanceCollapsed: false,
tabContentHeight: 0, tabContentHeight: 0,
workspace: Workspace.STANDARD,
}; };
export default (state = defaultState, action: AnyAction): AnnotationState => { 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 { const {
shapeType, shapeType,
labelID, labelID,
@ -648,7 +651,11 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}; };
} }
case AnnotationActionTypes.ACTIVATE_OBJECT: { case AnnotationActionTypes.ACTIVATE_OBJECT: {
const { activatedStateID } = action.payload; const {
activatedStateID,
activatedAttributeID,
} = action.payload;
const { const {
canvas: { canvas: {
activeControl, activeControl,
@ -665,6 +672,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
annotations: { annotations: {
...state.annotations, ...state.annotations,
activatedStateID, 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: { case AnnotationActionTypes.RESET_CANVAS: {
return { return {
...state, ...state,

@ -337,6 +337,7 @@ export interface AnnotationState {
annotations: { annotations: {
selectedStatesID: number[]; selectedStatesID: number[];
activatedStateID: number | null; activatedStateID: number | null;
activatedAttributeID: number | null;
collapsed: Record<number, boolean>; collapsed: Record<number, boolean>;
states: any[]; states: any[];
filters: string[]; filters: string[];
@ -369,6 +370,12 @@ export interface AnnotationState {
sidebarCollapsed: boolean; sidebarCollapsed: boolean;
appearanceCollapsed: boolean; appearanceCollapsed: boolean;
tabContentHeight: number; tabContentHeight: number;
workspace: Workspace;
}
export enum Workspace {
STANDARD = 'Standard',
ATTRIBUTE_ANNOTATION = 'Attribute annotation',
} }
export enum GridColor { export enum GridColor {

@ -1,10 +1,9 @@
# Copyright (C) 2018-2020 Intel Corporation
# Copyright (C) 2018 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from cvat.utils.version import get_version from cvat.utils.version import get_version
VERSION = (0, 6, 0, 'alpha', 0) VERSION = (1, 0, 0, 'alpha', 0)
__version__ = get_version(VERSION) __version__ = get_version(VERSION)

@ -322,7 +322,7 @@ class Annotation:
annotations = {} annotations = {}
data_manager = DataManager(self._annotation_ir) 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)) _get_frame(annotations, shape).labeled_shapes.append(self._export_labeled_shape(shape))
for tag in self._annotation_ir.tags: for tag in self._annotation_ir.tags:

@ -74,7 +74,7 @@ def dump(file_object, annotations):
extractor = CvatAnnotationsExtractor('', annotations) extractor = CvatAnnotationsExtractor('', annotations)
extractor = extractor.transform(id_from_image) extractor = extractor.transform(id_from_image)
extractor = Dataset.from_extractors(extractor) # apply lazy transforms 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: with TemporaryDirectory() as temp_dir:
converter(extractor, save_dir=temp_dir) converter(extractor, save_dir=temp_dir)
make_zip_archive(temp_dir, file_object) make_zip_archive(temp_dir, file_object)

@ -7,7 +7,7 @@ pylint==2.3.1
pylint-django==0.9.4 pylint-django==0.9.4
pylint-plugin-utils==0.2.6 pylint-plugin-utils==0.2.6
rope==0.11 rope==0.11
wrapt==1.10.11 wrapt==1.11.1
django-extensions==2.0.6 django-extensions==2.0.6
Werkzeug==0.15.3 Werkzeug==0.15.3
snakeviz==0.4.2 snakeviz==0.4.2

Loading…
Cancel
Save