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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0.alpha] - 2020-02-XX
## [1.0.0-beta] - Unreleased
### Added
-
### Changed
- VOC task export now does not use official label map by default, but takes one
from the source task to avoid primary-class and class part name
clashing ([#1275](https://github.com/opencv/cvat/issues/1275))
### Deprecated
-
### Removed
-
### Fixed
-
### Security
- Bump acorn from 6.3.0 to 6.4.1 in /cvat-ui ([#1270](https://github.com/opencv/cvat/pull/1270))
## [0.6.0] - 2020-03-15
### Added
- Server only support for projects. Extend REST API v1 (/api/v1/projects*)
- Ability to get basic information about users without admin permissions ([#750](https://github.com/opencv/cvat/issues/750))
@ -28,12 +49,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Yolov3 interpretation script fix and changes to mapping.json
- YOLO format support ([#1151](https://github.com/opencv/cvat/pull/1151))
### Deprecated
-
### Removed
-
### Fixed
- Exception in Git plugin [#826](https://github.com/opencv/cvat/issues/826)
- Label ids in TFrecord format now start from 1 [#866](https://github.com/opencv/cvat/issues/866)
@ -42,8 +57,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Output labels for VOC format can be specified with Datumaro [#942](https://github.com/opencv/cvat/issues/942)
- Annotations can be filtered before dumping with Datumaro [#994](https://github.com/opencv/cvat/issues/994)
### Security
-
## [0.5.2] - 2019-12-15
### Fixed
- Frozen version of scikit-image==0.15 in requirements.txt because next releases don't support Python 3.5
## [0.5.1] - 2019-10-17
### Added

@ -15,7 +15,7 @@ Next steps should work on clear Ubuntu 18.04.
- Install necessary dependencies:
```sh
$ sudo apt-get update && apt-get --no-install-recommends install -y nodejs npm curl redis-server python3-dev python3-pip python3-venv libldap2-dev libsasl2-dev
$ sudo apt-get update && sudo apt-get --no-install-recommends install -y ffmpeg build-essential nodejs npm curl redis-server python3-dev python3-pip python3-venv libldap2-dev libsasl2-dev
```
- Install [Visual Studio Code](https://code.visualstudio.com/docs/setup/linux#_debian-and-ubuntu-based-distributions)
@ -28,7 +28,7 @@ git clone https://github.com/opencv/cvat
cd cvat && mkdir logs keys
python3 -m venv .env
. .env/bin/activate
pip install -U pip wheel
pip install -U pip wheel setuptools
pip install -r cvat/requirements/development.txt
pip install -r datumaro/requirements.txt
python manage.py migrate

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

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

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

@ -8,7 +8,10 @@
*/
const jsonpath = require('jsonpath');
const { AttributeType } = require('./enums');
const {
AttributeType,
ObjectType,
} = require('./enums');
const { ArgumentError } = require('./exceptions');
@ -165,18 +168,21 @@ class AnnotationsFilter {
let xbr = Number.MIN_SAFE_INTEGER;
let ytl = Number.MAX_SAFE_INTEGER;
let ybr = Number.MIN_SAFE_INTEGER;
let [width, height] = [null, null];
if (state.objectType !== ObjectType.TAG) {
state.points.forEach((coord, idx) => {
if (idx % 2) { // y
ytl = Math.min(ytl, coord);
ybr = Math.max(ybr, coord);
} else { // x
xtl = Math.min(xtl, coord);
xbr = Math.max(xbr, coord);
}
});
[width, height] = [xbr - xtl, ybr - ytl];
}
state.points.forEach((coord, idx) => {
if (idx % 2) { // y
ytl = Math.min(ytl, coord);
ybr = Math.max(ybr, coord);
} else { // x
xtl = Math.min(xtl, coord);
xbr = Math.max(xbr, coord);
}
});
const [width, height] = [xbr - xtl, ybr - ytl];
const attributes = {};
Object.keys(state.attributes).reduce((acc, key) => {
const attr = labelAttributes[key];

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

@ -1421,9 +1421,9 @@
}
},
"acorn": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz",
"integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
"integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
"dev": true
},
"acorn-jsx": {
@ -4293,14 +4293,6 @@
"acorn": "^7.1.0",
"acorn-jsx": "^5.1.0",
"eslint-visitor-keys": "^1.1.0"
},
"dependencies": {
"acorn": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz",
"integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==",
"dev": true
}
}
},
"esprima": {
@ -12354,6 +12346,12 @@
"webpack-sources": "^1.4.1"
},
"dependencies": {
"acorn": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
"integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
"dev": true
},
"schema-utils": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",

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

@ -11,14 +11,17 @@ import {
Result,
} from 'antd';
import { Workspace } from 'reducers/interfaces';
import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar';
import StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal';
import StandardWorkspaceComponent from './standard-workspace/standard-workspace';
import AttributeAnnotationWorkspace from './attribute-annotation-workspace/attribute-annotation-workspace';
interface Props {
job: any | null | undefined;
fetching: boolean;
getJob(): void;
workspace: Workspace;
}
@ -27,9 +30,9 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
job,
fetching,
getJob,
workspace,
} = props;
if (job === null) {
if (!fetching) {
getJob();
@ -51,8 +54,18 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
return (
<Layout className='cvat-annotation-page'>
<AnnotationTopBarContainer />
<StandardWorkspaceComponent />
<Layout.Header className='cvat-annotation-header'>
<AnnotationTopBarContainer />
</Layout.Header>
{ workspace === Workspace.STANDARD ? (
<Layout.Content>
<StandardWorkspaceComponent />
</Layout.Content>
) : (
<Layout.Content>
<AttributeAnnotationWorkspace />
</Layout.Content>
)}
<StatisticsModalContainer />
</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,
ObjectType,
ContextMenuType,
Workspace
} from 'reducers/interfaces';
import { Canvas } from 'cvat-canvas';
import getCore from 'cvat-core';
import {
ColorBy,
GridColor,
ObjectType,
Workspace,
} from 'reducers/interfaces';
const cvat = getCore();
@ -31,6 +38,7 @@ interface Props {
canvasInstance: Canvas;
jobInstance: any;
activatedStateID: number | null;
activatedAttributeID: number | null;
selectedStatesID: number[];
annotations: any[];
frameData: any;
@ -55,6 +63,8 @@ interface Props {
resetZoom: boolean;
contextVisible: boolean;
contextType: ContextMenuType;
aamZoomMargin: number;
workspace: Workspace;
onSetupCanvas: () => void;
onDragCanvas: (enabled: boolean) => void;
onZoomCanvas: (enabled: boolean) => void;
@ -64,7 +74,7 @@ interface Props {
onEditShape: (enabled: boolean) => void;
onShapeDrawn: () => void;
onResetCanvas: () => void;
onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onUpdateAnnotations(states: any[]): void;
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
@ -120,6 +130,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
brightnessLevel,
contrastLevel,
saturationLevel,
workspace,
} = this.props;
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) {
this.updateCanvas();
}
if (prevProps.frame !== frameData.number && resetZoom) {
if (prevProps.frame !== frameData.number
&& resetZoom
&& workspace !== Workspace.ATTRIBUTE_ANNOTATION
) {
canvasInstance.html().addEventListener('canvas.setup', () => {
canvasInstance.fit();
}, { once: true });
@ -183,10 +201,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
this.updateShapesView();
}
if (prevProps.curZLayer !== curZLayer) {
canvasInstance.setZLayer(curZLayer);
}
if (prevProps.frameAngle !== frameAngle) {
canvasInstance.rotate(frameAngle);
}
@ -195,10 +209,34 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
public componentWillUnmount(): void {
const { canvasInstance } = this.props;
canvasInstance.html().removeEventListener('mousedown', this.onCanvasMouseDown);
canvasInstance.html().removeEventListener('click', this.onCanvasClicked);
canvasInstance.html().removeEventListener('contextmenu', this.onCanvasContextMenu);
canvasInstance.html().removeEventListener('canvas.editstart', this.onCanvasEditStart);
canvasInstance.html().removeEventListener('canvas.edited', this.onCanvasEditDone);
canvasInstance.html().removeEventListener('canvas.dragstart', this.onCanvasDragStart);
canvasInstance.html().removeEventListener('canvas.dragstop', this.onCanvasDragDone);
canvasInstance.html().removeEventListener('canvas.zoomstart', this.onCanvasZoomStart);
canvasInstance.html().removeEventListener('canvas.zoomstop', this.onCanvasZoomDone);
canvasInstance.html().removeEventListener('canvas.setup', this.onCanvasSetup);
canvasInstance.html().removeEventListener('canvas.canceled', this.onCanvasCancel);
canvasInstance.html().removeEventListener('canvas.find', this.onCanvasFindObject);
canvasInstance.html().removeEventListener('canvas.deactivated', this.onCanvasShapeDeactivated);
canvasInstance.html().removeEventListener('canvas.moved', this.onCanvasCursorMoved);
canvasInstance.html().removeEventListener('canvas.clicked', this.onCanvasShapeClicked);
canvasInstance.html().removeEventListener('canvas.drawn', this.onCanvasShapeDrawn);
canvasInstance.html().removeEventListener('canvas.merged', this.onCanvasObjectsMerged);
canvasInstance.html().removeEventListener('canvas.groupped', this.onCanvasObjectsGroupped);
canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted);
window.removeEventListener('resize', this.fitCanvas);
}
private onShapeDrawn(event: any): void {
private onCanvasShapeDrawn = (event: any): void => {
const {
jobInstance,
activeLabelID,
@ -229,27 +267,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
state.frame = frame;
const objectState = new cvat.classes.ObjectState(state);
onCreateAnnotations(jobInstance, frame, [objectState]);
}
private onShapeEdited(event: any): void {
const {
jobInstance,
frame,
onEditShape,
onUpdateAnnotations,
} = this.props;
onEditShape(false);
const {
state,
points,
} = event.detail;
state.points = points;
onUpdateAnnotations(jobInstance, frame, [state]);
}
};
private onObjectsMerged(event: any): void {
private onCanvasObjectsMerged = (event: any): void => {
const {
jobInstance,
frame,
@ -261,9 +281,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const { states } = event.detail;
onMergeAnnotations(jobInstance, frame, states);
}
};
private onObjectsGroupped(event: any): void {
private onCanvasObjectsGroupped = (event: any): void => {
const {
jobInstance,
frame,
@ -275,9 +295,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const { states } = event.detail;
onGroupAnnotations(jobInstance, frame, states);
}
};
private onTrackSplitted(event: any): void {
private onCanvasTrackSplitted = (event: any): void => {
const {
jobInstance,
frame,
@ -289,22 +309,179 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const { state } = event.detail;
onSplitAnnotations(jobInstance, frame, state);
}
};
private fitCanvas = (): void => {
const { canvasInstance } = this.props;
canvasInstance.fitCanvas();
};
private onCanvasMouseDown = (e: MouseEvent): void => {
const { workspace, activatedStateID, onActivateObject } = this.props;
if ((e.target as HTMLElement).tagName === 'svg') {
if (activatedStateID !== null && workspace !== Workspace.ATTRIBUTE_ANNOTATION) {
onActivateObject(null);
}
}
};
private onCanvasClicked = (): void => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
};
private onCanvasContextMenu = (e: MouseEvent): void => {
const { activatedStateID, onUpdateContextMenu } = this.props;
onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY);
};
private onCanvasShapeClicked = (e: any): void => {
const { clientID } = e.detail.state;
const sidebarItem = window.document
.getElementById(`cvat-objects-sidebar-state-item-${clientID}`);
if (sidebarItem) {
sidebarItem.scrollIntoView();
}
};
private onCanvasShapeDeactivated = (e: any): void => {
const { onActivateObject, activatedStateID } = this.props;
const { state } = e.detail;
// when we activate element, canvas deactivates the previous
// and triggers this event
// in this case we do not need to update our state
if (state.clientID === activatedStateID) {
onActivateObject(null);
}
};
private onCanvasCursorMoved = async (event: any): Promise<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 {
const {
activatedStateID,
activatedAttributeID,
canvasInstance,
selectedOpacity,
aamZoomMargin,
workspace,
annotations,
} = this.props;
if (activatedStateID !== null) {
canvasInstance.activate(activatedStateID);
if (workspace === Workspace.ATTRIBUTE_ANNOTATION) {
const [activatedState] = annotations
.filter((state: any): boolean => state.clientID === activatedStateID);
if (activatedState.objectType !== ObjectType.TAG) {
canvasInstance.focus(activatedStateID, aamZoomMargin);
} else {
canvasInstance.fit();
}
}
canvasInstance.activate(activatedStateID, activatedAttributeID);
const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`);
if (el) {
(el as any as SVGElement).setAttribute('fill-opacity', `${selectedOpacity / 100}`);
@ -352,7 +529,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} = this.props;
if (frameData !== null) {
canvasInstance.setup(frameData, annotations);
canvasInstance.setup(frameData, annotations
.filter((e) => e.objectType !== ObjectType.TAG));
canvasInstance.rotate(frameAngle);
}
}
@ -364,14 +542,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
gridColor,
gridOpacity,
canvasInstance,
jobInstance,
onSetupCanvas,
onDragCanvas,
onZoomCanvas,
onResetCanvas,
onActivateObject,
onUpdateContextMenu,
onEditShape,
brightnessLevel,
contrastLevel,
saturationLevel,
@ -402,143 +572,33 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
// Events
canvasInstance.html().addEventListener('mousedown', (e: MouseEvent): void => {
const {
activatedStateID,
} = this.props;
if ((e.target as HTMLElement).tagName === 'svg' && activatedStateID !== null) {
onActivateObject(null);
}
});
canvasInstance.html().addEventListener('click', (): void => {
if (document.activeElement) {
(document.activeElement as HTMLElement).blur();
}
});
canvasInstance.html().addEventListener('contextmenu', (e: MouseEvent): void => {
const {
activatedStateID,
contextType,
contextVisible,
} = this.props;
if (!(contextVisible && contextType === ContextMenuType.CANVAS_SHAPE_POINT)) {
onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY,
ContextMenuType.CANVAS_SHAPE);
}
});
canvasInstance.html().addEventListener('canvas.editstart', (): void => {
onActivateObject(null);
onEditShape(true);
});
canvasInstance.html().addEventListener('canvas.setup', (): void => {
onSetupCanvas();
this.updateShapesView();
this.activateOnCanvas();
});
canvasInstance.html().addEventListener('canvas.setup', () => {
const { activatedStateID, activatedAttributeID } = this.props;
canvasInstance.fit();
canvasInstance.activate(activatedStateID, activatedAttributeID);
}, { once: true });
canvasInstance.html().addEventListener('canvas.canceled', () => {
onResetCanvas();
});
canvasInstance.html().addEventListener('canvas.dragstart', () => {
onDragCanvas(true);
});
canvasInstance.html().addEventListener('canvas.dragstop', () => {
onDragCanvas(false);
});
canvasInstance.html().addEventListener('canvas.zoomstart', () => {
onZoomCanvas(true);
});
canvasInstance.html().addEventListener('canvas.zoomstop', () => {
onZoomCanvas(false);
});
canvasInstance.html().addEventListener('canvas.clicked', (e: any) => {
const { clientID } = e.detail.state;
const sidebarItem = window.document
.getElementById(`cvat-objects-sidebar-state-item-${clientID}`);
if (sidebarItem) {
sidebarItem.scrollIntoView();
}
});
canvasInstance.html().addEventListener('canvas.deactivated', (e: any): void => {
const { activatedStateID } = this.props;
const { state } = e.detail;
// when we activate element, canvas deactivates the previous
// and triggers this event
// in this case we do not need to update our state
if (state.clientID === activatedStateID) {
onActivateObject(null);
}
});
canvasInstance.html().addEventListener('canvas.moved', async (event: any): Promise<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);
});
canvasInstance.html().addEventListener('mousedown', this.onCanvasMouseDown);
canvasInstance.html().addEventListener('click', this.onCanvasClicked);
canvasInstance.html().addEventListener('contextmenu', this.onCanvasContextMenu);
canvasInstance.html().addEventListener('canvas.editstart', this.onCanvasEditStart);
canvasInstance.html().addEventListener('canvas.edited', this.onCanvasEditDone);
canvasInstance.html().addEventListener('canvas.dragstart', this.onCanvasDragStart);
canvasInstance.html().addEventListener('canvas.dragstop', this.onCanvasDragDone);
canvasInstance.html().addEventListener('canvas.zoomstart', this.onCanvasZoomStart);
canvasInstance.html().addEventListener('canvas.zoomstop', this.onCanvasZoomDone);
canvasInstance.html().addEventListener('canvas.setup', this.onCanvasSetup);
canvasInstance.html().addEventListener('canvas.canceled', this.onCanvasCancel);
canvasInstance.html().addEventListener('canvas.find', this.onCanvasFindObject);
canvasInstance.html().addEventListener('canvas.deactivated', this.onCanvasShapeDeactivated);
canvasInstance.html().addEventListener('canvas.moved', this.onCanvasCursorMoved);
canvasInstance.html().addEventListener('canvas.clicked', this.onCanvasShapeClicked);
canvasInstance.html().addEventListener('canvas.drawn', this.onCanvasShapeDrawn);
canvasInstance.html().addEventListener('canvas.merged', this.onCanvasObjectsMerged);
canvasInstance.html().addEventListener('canvas.groupped', this.onCanvasObjectsGroupped);
canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted);
}
public render(): JSX.Element {

@ -6,9 +6,7 @@ import React from 'react';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import {
Icon,
Layout,
Tooltip,
} from 'antd';
import {
@ -16,10 +14,6 @@ import {
Rotation,
} from 'reducers/interfaces';
import {
TagIcon,
} from 'icons';
import {
Canvas,
} from 'cvat-canvas';
@ -33,6 +27,7 @@ import DrawRectangleControl from './draw-rectangle-control';
import DrawPolygonControl from './draw-polygon-control';
import DrawPolylineControl from './draw-polyline-control';
import DrawPointsControl from './draw-points-control';
import SetupTagControl from './setup-tag-control';
import MergeControl from './merge-control';
import GroupControl from './group-control';
import SplitControl from './split-control';
@ -221,9 +216,10 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
isDrawing={activeControl === ActiveControl.DRAW_POINTS}
/>
<Tooltip title='Setup a tag' placement='right'>
<Icon component={TagIcon} style={{ pointerEvents: 'none', opacity: 0.5 }} />
</Tooltip>
<SetupTagControl
canvasInstance={canvasInstance}
isDrawing={false}
/>
<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(
serverID: number | undefined,
locked: boolean,
objectType: ObjectType,
copy: (() => void),
remove: (() => void),
propagate: (() => void),
@ -67,18 +68,22 @@ function ItemMenu(
Propagate
</Button>
</Menu.Item>
<Menu.Item>
<Button type='link' onClick={toBackground}>
<Icon component={BackgroundIcon} />
To background
</Button>
</Menu.Item>
<Menu.Item>
<Button type='link' onClick={toForeground}>
<Icon component={ForegroundIcon} />
To foreground
</Button>
</Menu.Item>
{ objectType !== ObjectType.TAG && (
<>
<Menu.Item>
<Button type='link' onClick={toBackground}>
<Icon component={BackgroundIcon} />
To background
</Button>
</Menu.Item>
<Menu.Item>
<Button type='link' onClick={toForeground}>
<Icon component={ForegroundIcon} />
To foreground
</Button>
</Menu.Item>
</>
)}
<Menu.Item>
<Button
type='link'
@ -109,6 +114,7 @@ interface ItemTopComponentProps {
serverID: number | undefined;
labelID: number;
labels: any[];
objectType: ObjectType;
type: string;
locked: boolean;
changeLabel(labelID: string): void;
@ -126,6 +132,7 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
serverID,
labelID,
labels,
objectType,
type,
locked,
changeLabel,
@ -159,6 +166,7 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
overlay={ItemMenu(
serverID,
locked,
objectType,
copy,
remove,
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 (
<Row type='flex' align='middle' justify='space-around'>
<Col span={20} style={{ textAlign: 'center' }}>
@ -714,7 +738,6 @@ function ObjectItemComponent(props: Props): JSX.Element {
style={{ background: ` ${color}` }}
/>
</Popover>
<div
onMouseEnter={activate}
id={`cvat-objects-sidebar-state-item-${clientID}`}
@ -726,6 +749,7 @@ function ObjectItemComponent(props: Props): JSX.Element {
clientID={clientID}
labelID={labelID}
labels={labels}
objectType={objectType}
type={type}
locked={locked}
changeLabel={changeLabel}

@ -3,20 +3,14 @@
// SPDX-License-Identifier: MIT
import React from 'react';
import {
Row,
Col,
Icon,
Select,
} from 'antd';
import { Row, Col } from 'antd/lib/grid';
import Icon from 'antd/lib/icon';
import Select from 'antd/lib/select';
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';
interface StatesOrderingSelectorComponentProps {
statesOrdering: StatesOrdering;
changeStatesOrdering(value: StatesOrdering): void;
@ -62,10 +56,7 @@ interface Props {
statesLocked: boolean;
statesCollapsed: boolean;
statesOrdering: StatesOrdering;
annotationsFilters: string[];
annotationsFiltersHistory: string[];
changeStatesOrdering(value: StatesOrdering): void;
changeAnnotationsFilters(value: SelectValue): void;
lockAllStates(): void;
unlockAllStates(): void;
collapseAllStates(): void;
@ -76,8 +67,6 @@ interface Props {
function ObjectListHeader(props: Props): JSX.Element {
const {
annotationsFilters,
annotationsFiltersHistory,
statesHidden,
statesLocked,
statesCollapsed,
@ -89,30 +78,13 @@ function ObjectListHeader(props: Props): JSX.Element {
expandAllStates,
hideAllStates,
showAllStates,
changeAnnotationsFilters,
} = props;
return (
<div className='cvat-objects-sidebar-states-header'>
<Row>
<Col>
<Select
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>
<AnnotationsFiltersInput />
</Col>
</Row>
<Row type='flex' justify='space-between' align='middle'>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -322,7 +322,7 @@ class Annotation:
annotations = {}
data_manager = DataManager(self._annotation_ir)
for shape in data_manager.to_shapes(self._db_task.size):
for shape in sorted(data_manager.to_shapes(self._db_task.size), key=lambda s: s.get("z_order", 0)):
_get_frame(annotations, shape).labeled_shapes.append(self._export_labeled_shape(shape))
for tag in self._annotation_ir.tags:

@ -74,7 +74,7 @@ def dump(file_object, annotations):
extractor = CvatAnnotationsExtractor('', annotations)
extractor = extractor.transform(id_from_image)
extractor = Dataset.from_extractors(extractor) # apply lazy transforms
converter = env.make_converter('voc')
converter = env.make_converter('voc', label_map='source')
with TemporaryDirectory() as temp_dir:
converter(extractor, save_dir=temp_dir)
make_zip_archive(temp_dir, file_object)

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

Loading…
Cancel
Save