React UI: Annotation view enhancements (#1106)

* Keyframes navigation

* Synchronized objects on canvas and in side panel

* Fixed minor bug with collapse

* Fixed css property 'pointer-events'

* Drawn appearance block

* Removed extra force reflow

* Finished appearance block, fixed couple bugs

* Improved save() in cvat-core, changed approach to highlight shapes

* Fixed exception in edit function, fixed filling for polylines and points, fixed wrong image navigation, remove and copy

* Added lock

* Some fixes with points

* Minor appearance fixes

* Fixed insert for points

* Fixed unit tests

* Fixed control

* Fixed list size

* Added propagate

* Minor fix with attr saving

* Some div changed to buttons

* Locked some buttons for unimplemented functionalities

* Statistics modal, changing a job status

* Minor fix with shapes counting

* Couple of fixes to improve visibility

* Added fullscreen

* SVG Canvas -> HTML Canvas frame (#1113)

* SVG Frame -> HTML Canvas frame
main
Boris Sekachev 6 years ago committed by GitHub
parent 7f8b96d4e8
commit 42614c28a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -111,11 +111,12 @@ Canvas itself handles:
Standard JS events are used.
```js
- canvas.setup
- canvas.activated => ObjectState
- canvas.deactivated
- canvas.activated => {state: ObjectState}
- canvas.clicked => {state: ObjectState}
- canvas.moved => {states: ObjectState[], x: number, y: number}
- canvas.find => {states: ObjectState[], x: number, y: number}
- canvas.drawn => {state: DrawnData}
- canvas.editstart
- canvas.edited => {state: ObjectState, points: number[]}
- canvas.splitted => {state: ObjectState}
- canvas.groupped => {states: ObjectState[]}

@ -3,15 +3,35 @@
}
.cvat_canvas_shape {
fill-opacity: 0.03;
stroke-opacity: 1;
}
polyline.cvat_canvas_shape {
fill-opacity: 0;
}
.cvat_shape_action_opacity {
fill-opacity: 0.5;
stroke-opacity: 1;
}
polyline.cvat_shape_action_opacity {
fill-opacity: 0;
}
.cvat_shape_drawing_opacity {
fill-opacity: 0.2;
stroke-opacity: 1;
}
polyline.cvat_shape_drawing_opacity {
fill-opacity: 0;
}
.cvat_shape_action_dasharray {
stroke-dasharray: 4 1 2 3;
}
.cvat_canvas_text {
font-weight: bold;
font-size: 1.2em;
@ -27,51 +47,52 @@ polyline.cvat_canvas_shape {
stroke: red;
}
.cvat_canvas_shape_activated {
fill-opacity: 0.3;
}
.cvat_canvas_shape_grouping {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
fill: darkmagenta;
fill-opacity: 0.5;
}
polyline.cvat_canvas_shape_grouping {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
stroke: darkmagenta;
stroke-opacity: 1;
}
.cvat_canvas_shape_merging {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
fill: blue;
fill-opacity: 0.5;
}
polyline.cvat_canvas_shape_merging {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
stroke: blue;
}
polyline.cvat_canvas_shape_splitting {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
stroke: dodgerblue;
stroke-opacity: 1;
}
.cvat_canvas_shape_splitting {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
fill: dodgerblue;
fill-opacity: 0.5;
}
polyline.cvat_canvas_shape_merging {
stroke: blue;
stroke-opacity: 1;
}
.cvat_canvas_shape_drawing {
fill-opacity: 0.1;
stroke-opacity: 1;
@extend .cvat_shape_drawing_opacity;
fill: white;
stroke: black;
}
.cvat_canvas_zoom_selection {
@extend .cvat_shape_action_dasharray;
stroke: #096dd9;
fill-opacity: 0;
stroke-dasharray: 4;
}
.cvat_canvas_shape_occluded {

@ -32,7 +32,7 @@ import '../scss/canvas.scss';
interface Canvas {
html(): HTMLDivElement;
setup(frameData: any, objectStates: any[]): void;
activate(clientID: number, attributeID?: number): void;
activate(clientID: number | null, attributeID?: number): void;
rotate(rotation: Rotation, remember?: boolean): void;
focus(clientID: number, padding?: number): void;
fit(): void;
@ -85,7 +85,7 @@ class CanvasImpl implements Canvas {
this.model.zoomCanvas(enable);
}
public activate(clientID: number, attributeID: number | null = null): void {
public activate(clientID: number | null, attributeID: number | null = null): void {
this.model.activate(clientID, attributeID);
}

@ -110,7 +110,7 @@ export enum Mode {
}
export interface CanvasModel {
readonly image: string;
readonly image: HTMLImageElement | null;
readonly objects: any[];
readonly gridSize: Size;
readonly focusData: FocusData;
@ -127,7 +127,7 @@ export interface CanvasModel {
move(topOffset: number, leftOffset: number): void;
setup(frameData: any, objectStates: any[]): void;
activate(clientID: number, attributeID: number | null): void;
activate(clientID: number | null, attributeID: number | null): void;
rotate(rotation: Rotation, remember: boolean): void;
focus(clientID: number, padding: number): void;
fit(): void;
@ -151,7 +151,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
activeElement: ActiveElement;
angle: number;
canvasSize: Size;
image: string;
image: HTMLImageElement | null;
imageID: number | null;
imageOffset: number;
imageSize: Size;
@ -183,7 +183,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
height: 0,
width: 0,
},
image: '',
image: null,
imageID: null,
imageOffset: 0,
imageSize: {
@ -291,22 +291,33 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
public setup(frameData: any, objectStates: any[]): void {
if (frameData.number === this.data.imageID) {
this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS_UPDATED);
return;
}
this.data.imageID = frameData.number;
frameData.data(
(): void => {
this.data.image = '';
this.data.image = null;
this.notify(UpdateReasons.IMAGE_CHANGED);
},
).then((data: string): void => {
).then((data: HTMLImageElement): void => {
if (frameData.number !== this.data.imageID) {
// already another image
return;
}
if (!this.data.rememberAngle) {
this.data.angle = 0;
}
this.data.imageSize = {
height: (frameData.height as number),
width: (frameData.width as number),
};
if (this.data.imageID !== frameData.number && !this.data.rememberAngle) {
this.data.angle = 0;
}
this.data.imageID = frameData.number;
this.data.image = data;
this.notify(UpdateReasons.IMAGE_CHANGED);
this.data.objects = objectStates;
@ -316,8 +327,8 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
});
}
public activate(clientID: number, attributeID: number | null): void {
if (this.data.mode !== Mode.IDLE) {
public activate(clientID: number | null, attributeID: number | null): void {
if (this.data.mode !== Mode.IDLE && clientID !== null) {
// Exception or just return?
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
@ -503,7 +514,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
));
}
public get image(): string {
public get image(): HTMLImageElement | null {
return this.data.image;
}

@ -21,7 +21,6 @@ import consts from './consts';
import {
translateToSVG,
translateFromSVG,
translateBetweenSVG,
pointsToArray,
displayShapeSize,
ShapeSizeElement,
@ -44,25 +43,11 @@ export interface CanvasView {
html(): HTMLDivElement;
}
function darker(color: string, percentage: number): string {
const R = Math.round(parseInt(color.slice(1, 3), 16) * (1 - percentage / 100));
const G = Math.round(parseInt(color.slice(3, 5), 16) * (1 - percentage / 100));
const B = Math.round(parseInt(color.slice(5, 7), 16) * (1 - percentage / 100));
const rHex = Math.max(0, R).toString(16);
const gHex = Math.max(0, G).toString(16);
const bHex = Math.max(0, B).toString(16);
return `#${rHex.length === 1 ? `0${rHex}` : rHex}`
+ `${gHex.length === 1 ? `0${gHex}` : gHex}`
+ `${bHex.length === 1 ? `0${bHex}` : bHex}`;
}
export class CanvasViewImpl implements CanvasView, Listener {
private loadingAnimation: SVGSVGElement;
private text: SVGSVGElement;
private adoptedText: SVG.Container;
private background: SVGSVGElement;
private background: HTMLCanvasElement;
private grid: SVGSVGElement;
private content: SVGSVGElement;
private adoptedContent: SVG.Container;
@ -80,10 +65,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private splitHandler: SplitHandler;
private groupHandler: GroupHandler;
private zoomHandler: ZoomHandler;
private activeElement: {
state: any;
attributeID: number;
} | null;
private activeElement: ActiveElement;
private set mode(value: Mode) {
this.controller.mode = value;
@ -93,7 +75,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
return this.controller.mode;
}
private onDrawDone(data: object): void {
private onDrawDone(data: object, continueDraw?: boolean): void {
if (data) {
const event: CustomEvent = new CustomEvent('canvas.drawn', {
bubbles: false,
@ -101,6 +83,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
detail: {
// eslint-disable-next-line new-cap
state: data,
continue: continueDraw,
},
});
@ -114,11 +97,18 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.dispatchEvent(event);
}
this.controller.draw({
enabled: false,
});
if (continueDraw) {
this.drawHandler.draw(
this.controller.drawData,
this.geometry,
);
} else {
this.controller.draw({
enabled: false,
});
this.mode = Mode.IDLE;
this.mode = Mode.IDLE;
}
}
private onEditDone(state: any, points: number[]): void {
@ -229,13 +219,14 @@ export class CanvasViewImpl implements CanvasView, Listener {
private onFindObject(e: MouseEvent): void {
if (e.which === 1 || e.which === 0) {
const [x, y] = translateToSVG(this.background, [e.clientX, e.clientY]);
const { offset } = this.controller.geometry;
const [x, y] = translateToSVG(this.content, [e.clientX, e.clientY]);
const event: CustomEvent = new CustomEvent('canvas.find', {
bubbles: false,
cancelable: true,
detail: {
x,
y,
x: x - offset,
y: y - offset,
states: this.controller.objects,
},
});
@ -339,11 +330,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
for (const key in this.svgShapes) {
if (Object.prototype.hasOwnProperty.call(this.svgShapes, key)) {
const object = this.svgShapes[key];
if (object.attr('stroke-width')) {
object.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
}
object.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
}
}
@ -376,7 +365,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
private setupObjects(states: any[]): void {
this.deactivate();
const { offset } = this.controller.geometry;
const translate = (points: number[]): number[] => points
.map((coord: number): number => coord + offset);
const created = [];
const updated = [];
@ -394,17 +385,31 @@ export class CanvasViewImpl implements CanvasView, Listener {
const deleted = Object.keys(this.drawnStates).map((clientID: string): number => +clientID)
.filter((id: number): boolean => !newIDs.includes(id))
.map((id: number): any => this.drawnStates[id]);
if (this.activeElement.clientID !== null) {
this.deactivate();
}
for (const state of deleted) {
if (state.clientID in this.svgTexts) {
this.svgTexts[state.clientID].remove();
}
this.svgShapes[state.clientID].off('click.canvas');
this.svgShapes[state.clientID].remove();
delete this.drawnStates[state.clientID];
}
this.addObjects(created);
this.updateObjects(updated);
this.addObjects(created, translate);
this.updateObjects(updated, translate);
if (this.controller.activeElement.clientID !== null) {
const { clientID } = this.controller.activeElement;
if (states.map((state: any): number => state.clientID).includes(clientID)) {
this.activate(this.controller.activeElement);
}
}
}
private selectize(value: boolean, shape: SVG.Element): void {
@ -414,16 +419,24 @@ export class CanvasViewImpl implements CanvasView, Listener {
const pointID = Array.prototype.indexOf
.call(((e.target as HTMLElement).parentElement as HTMLElement).children, e.target);
if (self.activeElement) {
if (self.activeElement.clientID !== null) {
const [state] = self.controller.objects
.filter((_state: any): boolean => (
_state.clientID === self.activeElement.clientID
));
if (e.ctrlKey) {
const { points } = self.activeElement.state;
const { points } = state;
self.onEditDone(
self.activeElement.state,
state,
points.slice(0, pointID * 2).concat(points.slice(pointID * 2 + 2)),
);
} else if (e.shiftKey) {
self.canvas.dispatchEvent(new CustomEvent('canvas.editstart', {
bubbles: false,
cancelable: true,
}));
self.mode = Mode.EDIT;
const { state } = self.activeElement;
self.deactivate();
self.editHandler.edit({
enabled: true,
@ -483,7 +496,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.svgShapes = {};
this.svgTexts = {};
this.drawnStates = {};
this.activeElement = null;
this.activeElement = {
clientID: null,
attributeID: null,
};
this.mode = Mode.IDLE;
// Create HTML elements
@ -491,7 +507,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.text = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.adoptedText = (SVG.adopt((this.text as any as HTMLElement)) as SVG.Container);
this.background = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.background = window.document.createElement('canvas');
// window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.grid = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.gridPath = window.document.createElementNS('http://www.w3.org/2000/svg', 'path');
@ -560,12 +577,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.onDrawDone.bind(this),
this.adoptedContent,
this.adoptedText,
this.background,
);
this.editHandler = new EditHandlerImpl(
this.onEditDone.bind(this),
this.adoptedContent,
this.background,
);
this.mergeHandler = new MergeHandlerImpl(
this.onMergeDone.bind(this),
@ -613,8 +628,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
});
this.content.addEventListener('wheel', (event): void => {
const point = translateToSVG(self.background, [event.clientX, event.clientY]);
self.controller.zoom(point[0], point[1], event.deltaY > 0 ? -1 : 1);
const { offset } = this.controller.geometry;
const point = translateToSVG(this.content, [event.clientX, event.clientY]);
self.controller.zoom(point[0] - offset, point[1] - offset, event.deltaY > 0 ? -1 : 1);
this.canvas.dispatchEvent(new CustomEvent('canvas.zoom', {
bubbles: false,
cancelable: true,
@ -628,13 +644,14 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (this.mode !== Mode.IDLE) return;
if (e.ctrlKey || e.shiftKey) return;
const [x, y] = translateToSVG(this.background, [e.clientX, e.clientY]);
const { offset } = this.controller.geometry;
const [x, y] = translateToSVG(this.content, [e.clientX, e.clientY]);
const event: CustomEvent = new CustomEvent('canvas.moved', {
bubbles: false,
cancelable: true,
detail: {
x,
y,
x: x - offset,
y: y - offset,
states: this.controller.objects,
},
});
@ -649,11 +666,17 @@ export class CanvasViewImpl implements CanvasView, Listener {
public notify(model: CanvasModel & Master, reason: UpdateReasons): void {
this.geometry = this.controller.geometry;
if (reason === UpdateReasons.IMAGE_CHANGED) {
if (!model.image.length) {
const { image } = model;
if (!image) {
this.loadingAnimation.classList.remove('cvat_canvas_hidden');
} else {
this.loadingAnimation.classList.add('cvat_canvas_hidden');
this.background.style.backgroundImage = `url("${model.image}")`;
const ctx = this.background.getContext('2d');
this.background.setAttribute('width', `${image.width}px`);
this.background.setAttribute('height', `${image.height}px`);
if (ctx) {
ctx.drawImage(image, 0, 0);
}
this.moveCanvas();
this.resizeCanvas();
this.transformCanvas();
@ -696,7 +719,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
} else if (reason === UpdateReasons.SHAPE_ACTIVATED) {
this.activate(this.controller.activeElement);
} else if (reason === UpdateReasons.DRAG_CANVAS) {
this.deactivate();
if (this.mode === Mode.DRAG_CANVAS) {
this.canvas.dispatchEvent(new CustomEvent('canvas.dragstart', {
bubbles: false,
@ -711,7 +733,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.style.cursor = '';
}
} else if (reason === UpdateReasons.ZOOM_CANVAS) {
this.deactivate();
if (this.mode === Mode.ZOOM_CANVAS) {
this.canvas.dispatchEvent(new CustomEvent('canvas.zoomstart', {
bubbles: false,
@ -731,28 +752,24 @@ export class CanvasViewImpl implements CanvasView, Listener {
const data: DrawData = this.controller.drawData;
if (data.enabled) {
this.mode = Mode.DRAW;
this.deactivate();
}
this.drawHandler.draw(data, this.geometry);
} else if (reason === UpdateReasons.MERGE) {
const data: MergeData = this.controller.mergeData;
if (data.enabled) {
this.mode = Mode.MERGE;
this.deactivate();
}
this.mergeHandler.merge(data);
} else if (reason === UpdateReasons.SPLIT) {
const data: SplitData = this.controller.splitData;
if (data.enabled) {
this.mode = Mode.SPLIT;
this.deactivate();
}
this.splitHandler.split(data);
} else if (reason === UpdateReasons.GROUP) {
const data: GroupData = this.controller.groupData;
if (data.enabled) {
this.mode = Mode.GROUP;
this.deactivate();
}
this.groupHandler.group(data);
} else if (reason === UpdateReasons.SELECT) {
@ -807,14 +824,19 @@ export class CanvasViewImpl implements CanvasView, Listener {
};
}
private updateObjects(states: any[]): void {
private updateObjects(states: any[], translate: (points: number[]) => number[]): void {
for (const state of states) {
const { clientID } = state;
const drawnState = this.drawnStates[clientID];
if (drawnState.hidden !== state.hidden || drawnState.outside !== state.outside) {
const none = state.hidden || state.outside;
this.svgShapes[clientID].style('display', none ? 'none' : '');
if (state.shapeType === 'points') {
this.svgShapes[clientID].remember('_selectHandler').nested
.style('display', none ? 'none' : '');
} else {
this.svgShapes[clientID].style('display', none ? 'none' : '');
}
}
if (drawnState.occluded !== state.occluded) {
@ -825,12 +847,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}
if (drawnState.points
.some((p: number, id: number): boolean => p !== state.points[id])
if (state.points
.some((p: number, id: number): boolean => p !== drawnState.points[id])
) {
const translatedPoints: number[] = translateBetweenSVG(
this.background, this.content, state.points,
);
const translatedPoints: number[] = translate(state.points);
if (state.shapeType === 'rectangle') {
const [xtl, ytl, xbr, ybr] = translatedPoints;
@ -851,8 +871,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
return `${acc}${val},`;
}, '',
);
(this.svgShapes[clientID] as any).clear();
this.svgShapes[clientID].attr('points', stringified);
if (state.shapeType === 'points') {
this.selectize(false, this.svgShapes[clientID]);
this.setupPoints(this.svgShapes[clientID] as SVG.PolyLine, state);
}
}
}
@ -874,15 +899,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}
private addObjects(states: any[]): void {
private addObjects(states: any[], translate: (points: number[]) => number[]): void {
for (const state of states) {
if (state.objectType === 'tag') {
this.addTag(state);
} else {
const points: number[] = (state.points as number[]);
const translatedPoints: number[] = translateBetweenSVG(
this.background, this.content, points,
);
const translatedPoints: number[] = translate(points);
// TODO: Use enums after typification cvat-core
if (state.shapeType === 'rectangle') {
@ -910,6 +933,16 @@ export class CanvasViewImpl implements CanvasView, Listener {
.addPoints(stringified, state);
}
}
this.svgShapes[state.clientID].on('click.canvas', (): void => {
this.canvas.dispatchEvent(new CustomEvent('canvas.clicked', {
bubbles: false,
cancelable: true,
detail: {
state,
},
}));
});
}
this.saveState(state);
@ -917,9 +950,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
private deactivate(): void {
if (this.activeElement) {
const { state } = this.activeElement;
const shape = this.svgShapes[this.activeElement.state.clientID];
if (this.activeElement.clientID !== null) {
const { clientID } = this.activeElement;
const [state] = this.controller.objects
.filter((_state: any): boolean => _state.clientID === clientID);
const shape = this.svgShapes[state.clientID];
shape.removeClass('cvat_canvas_shape_activated');
(shape as any).off('dragstart');
@ -941,15 +976,19 @@ export class CanvasViewImpl implements CanvasView, Listener {
text.remove();
delete this.svgTexts[state.clientID];
}
this.activeElement = null;
this.activeElement = {
clientID: null,
attributeID: null,
};
}
}
private activate(activeElement: ActiveElement): void {
// Check if other element have been already activated
if (this.activeElement) {
if (this.activeElement.clientID !== null) {
// Check if it is the same element
if (this.activeElement.state.clientID === activeElement.clientID) {
if (this.activeElement.clientID === activeElement.clientID) {
return;
}
@ -957,16 +996,27 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.deactivate();
}
const state = this.controller.objects
.filter((el): boolean => el.clientID === activeElement.clientID)[0];
this.activeElement = {
attributeID: activeElement.attributeID,
state,
};
const { clientID } = activeElement;
if (clientID === null) {
return;
}
const [state] = this.controller.objects
.filter((_state: any): boolean => _state.clientID === clientID);
if (state.shapeType === 'points') {
this.svgShapes[clientID].remember('_selectHandler').nested
.style('pointer-events', state.lock ? 'none' : '');
}
if (state.hidden || state.lock) {
return;
}
const shape = this.svgShapes[activeElement.clientID];
this.activeElement = { ...activeElement };
const shape = this.svgShapes[clientID];
shape.addClass('cvat_canvas_shape_activated');
let text = this.svgTexts[activeElement.clientID];
let text = this.svgTexts[clientID];
// Draw text if it's hidden by default
if (!text) {
text = this.addText(state);
@ -998,14 +1048,15 @@ export class CanvasViewImpl implements CanvasView, Listener {
const p1 = e.detail.handler.startPoints.point;
const p2 = e.detail.p;
const delta = 1;
const { offset } = this.controller.geometry;
if (Math.sqrt(((p1.x - p2.x) ** 2) + ((p1.y - p2.y) ** 2)) >= delta) {
const points = pointsToArray(
shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} `
+ `${shape.attr('x') + shape.attr('width')},`
+ `${shape.attr('y') + shape.attr('height')}`,
);
).map((x: number): number => x - offset);
this.onEditDone(state, translateBetweenSVG(this.content, this.background, points));
this.onEditDone(state, points);
}
});
@ -1045,15 +1096,25 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.mode = Mode.IDLE;
if (resized) {
const { offset } = this.controller.geometry;
const points = pointsToArray(
shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} `
+ `${shape.attr('x') + shape.attr('width')},`
+ `${shape.attr('y') + shape.attr('height')}`,
);
).map((x: number): number => x - offset);
this.onEditDone(state, translateBetweenSVG(this.content, this.background, points));
this.onEditDone(state, points);
}
});
this.canvas.dispatchEvent(new CustomEvent('canvas.activated', {
bubbles: false,
cancelable: true,
detail: {
state,
},
}));
}
// Update text position after corresponding box has been moved, resized, etc.
@ -1122,7 +1183,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color,
'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 20),
stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
zOrder: state.zOrder,
}).move(xtl, ytl)
@ -1146,7 +1207,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color,
'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 20),
stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
zOrder: state.zOrder,
}).addClass('cvat_canvas_shape');
@ -1169,7 +1230,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color,
'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 20),
stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
zOrder: state.zOrder,
}).addClass('cvat_canvas_shape');
@ -1185,6 +1246,25 @@ export class CanvasViewImpl implements CanvasView, Listener {
return polyline;
}
private setupPoints(basicPolyline: SVG.PolyLine, state: any): any {
this.selectize(true, basicPolyline);
const group = basicPolyline.remember('_selectHandler').nested
.addClass('cvat_canvas_shape').attr({
clientID: state.clientID,
zOrder: state.zOrder,
id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color,
}).style({
'fill-opacity': 1,
});
group.bbox = basicPolyline.bbox.bind(basicPolyline);
group.clone = basicPolyline.clone.bind(basicPolyline);
return group;
}
private addPoints(points: string, state: any): SVG.PolyLine {
const shape = this.adoptedContent.polyline(points).attr({
'color-rendering': 'optimizeQuality',
@ -1196,25 +1276,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
opacity: 0,
});
this.selectize(true, shape);
const group = shape.remember('_selectHandler').nested
.addClass('cvat_canvas_shape').attr({
clientID: state.clientID,
zOrder: state.zOrder,
id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color,
}).style({
'fill-opacity': 1,
});
const group = this.setupPoints(shape, state);
if (state.hidden || state.outside) {
group.style('display', 'none');
}
group.bbox = shape.bbox.bind(shape);
group.clone = shape.clone.bind(shape);
shape.remove = (): SVG.PolyLine => {
this.selectize(false, shape);
shape.constructor.prototype.remove.call(shape);

@ -15,7 +15,6 @@ import {
import {
translateToSVG,
translateBetweenSVG,
displayShapeSize,
ShapeSizeElement,
pointsToString,
@ -32,10 +31,9 @@ export interface DrawHandler {
export class DrawHandlerImpl implements DrawHandler {
// callback is used to notify about creating new shape
private onDrawDone: (data: object) => void;
private onDrawDone: (data: object, continueDraw?: boolean) => void;
private canvas: SVG.Container;
private text: SVG.Container;
private background: SVGSVGElement;
private crosshair: {
x: SVG.Line;
y: SVG.Line;
@ -46,17 +44,16 @@ export class DrawHandlerImpl implements DrawHandler {
// we should use any instead of SVG.Shape because svg plugins cannot change declared interface
// so, methods like draw() just undefined for SVG.Shape, but nevertheless they exist
private drawInstance: any;
private pointsGroup: SVG.G | null;
private shapeSizeElement: ShapeSizeElement;
private getFinalRectCoordinates(bbox: BBox): number[] {
const frameWidth = this.geometry.image.width;
const frameHeight = this.geometry.image.height;
const { offset } = this.geometry;
let [xtl, ytl, xbr, ybr] = translateBetweenSVG(
this.canvas.node as any as SVGSVGElement,
this.background,
[bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height],
);
let [xtl, ytl, xbr, ybr] = [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height]
.map((coord: number): number => coord - offset);
xtl = Math.min(Math.max(xtl, 0), frameWidth);
xbr = Math.min(Math.max(xbr, 0), frameWidth);
@ -70,12 +67,8 @@ export class DrawHandlerImpl implements DrawHandler {
points: number[];
box: Box;
} {
const points = translateBetweenSVG(
this.canvas.node as any as SVGSVGElement,
this.background,
targetPoints,
);
const { offset } = this.geometry;
const points = targetPoints.map((coord: number): number => coord - offset);
const box = {
xtl: Number.MAX_SAFE_INTEGER,
ytl: Number.MAX_SAFE_INTEGER,
@ -125,6 +118,11 @@ export class DrawHandlerImpl implements DrawHandler {
this.canvas.off('mousemove.draw');
this.canvas.off('click.draw');
if (this.pointsGroup) {
this.pointsGroup.remove();
this.pointsGroup = null;
}
if (this.drawInstance) {
// Draw plugin isn't activated when draw from initialState
// So, we don't need to use any draw events
@ -311,7 +309,7 @@ export class DrawHandlerImpl implements DrawHandler {
private drawPolygon(): void {
this.drawInstance = (this.canvas as any).polygon().draw({
snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').style({
}).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
@ -321,7 +319,7 @@ export class DrawHandlerImpl implements DrawHandler {
private drawPolyline(): void {
this.drawInstance = (this.canvas as any).polyline().draw({
snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').style({
}).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': 0,
});
@ -332,7 +330,7 @@ export class DrawHandlerImpl implements DrawHandler {
private drawPoints(): void {
this.drawInstance = (this.canvas as any).polygon().draw({
snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').style({
}).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': 0,
opacity: 0,
});
@ -342,21 +340,22 @@ export class DrawHandlerImpl implements DrawHandler {
private pastePolyshape(): void {
this.canvas.on('click.draw', (e: MouseEvent): void => {
const targetPoints = (e.target as SVGElement)
.getAttribute('points')
const targetPoints = this.drawInstance
.attr('points')
.split(/[,\s]/g)
.map((coord): number => +coord);
.map((coord: string): number => +coord);
const { points } = this.getFinalPolyshapeCoordinates(targetPoints);
this.release();
this.onDrawDone({
shapeType: this.drawData.shapeType,
shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
points,
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
});
}, e.ctrlKey);
});
}
@ -380,30 +379,31 @@ export class DrawHandlerImpl implements DrawHandler {
private pasteBox(box: BBox): void {
this.drawInstance = (this.canvas as any).rect(box.width, box.height)
.move(box.x, box.y)
.addClass('cvat_canvas_shape_drawing').style({
.addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
this.pasteShape();
this.canvas.on('click.draw', (e: MouseEvent): void => {
const bbox = (e.target as SVGRectElement).getBBox();
const bbox = this.drawInstance.node.getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
this.release();
this.onDrawDone({
shapeType: this.drawData.shapeType,
shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
points: [xtl, ytl, xbr, ybr],
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
});
}, e.ctrlKey);
});
}
private pastePolygon(points: string): void {
this.drawInstance = (this.canvas as any).polygon(points)
.addClass('cvat_canvas_shape_drawing').style({
.addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
this.pasteShape();
@ -412,7 +412,7 @@ export class DrawHandlerImpl implements DrawHandler {
private pastePolyline(points: string): void {
this.drawInstance = (this.canvas as any).polyline(points)
.addClass('cvat_canvas_shape_drawing').style({
.addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
this.pasteShape();
@ -424,19 +424,52 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing').style({
'stroke-width': 0,
});
this.pasteShape();
this.pointsGroup = this.canvas.group();
for (const point of points.split(' ')) {
const radius = consts.BASE_POINT_SIZE / this.geometry.scale;
const stroke = consts.POINTS_STROKE_WIDTH / this.geometry.scale;
const [x, y] = point.split(',').map((coord: string): number => +coord);
this.pointsGroup.circle().move(x - radius / 2, y - radius / 2)
.fill('white').stroke('black').attr({
r: radius,
'stroke-width': stroke,
});
}
this.pointsGroup.attr({
z_order: Number.MAX_SAFE_INTEGER,
});
this.canvas.on('mousemove.draw', (e: MouseEvent): void => {
const [x, y] = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
);
const bbox = this.drawInstance.bbox();
this.drawInstance.move(x - bbox.width / 2, y - bbox.height / 2);
const radius = consts.BASE_POINT_SIZE / this.geometry.scale;
const newPoints = this.drawInstance.attr('points').split(' ');
if (this.pointsGroup) {
this.pointsGroup.children()
.forEach((child: SVG.Element, idx: number): void => {
const [px, py] = newPoints[idx].split(',');
child.move(px - radius / 2, py - radius / 2);
});
}
});
this.pastePolyshape();
}
private startDraw(): void {
// TODO: Use enums after typification cvat-core
if (this.drawData.initialState) {
const { offset } = this.geometry;
if (this.drawData.shapeType === 'rectangle') {
const [xtl, ytl, xbr, ybr] = translateBetweenSVG(
this.background,
this.canvas.node as any as SVGSVGElement,
this.drawData.initialState.points,
);
const [xtl, ytl, xbr, ybr] = this.drawData.initialState.points
.map((coord: number): number => coord + offset);
this.pasteBox({
x: xtl,
@ -445,12 +478,8 @@ export class DrawHandlerImpl implements DrawHandler {
height: ybr - ytl,
});
} else {
const points = translateBetweenSVG(
this.background,
this.canvas.node as any as SVGSVGElement,
this.drawData.initialState.points,
);
const points = this.drawData.initialState.points
.map((coord: number): number => coord + offset);
const stringifiedPoints = pointsToString(points);
if (this.drawData.shapeType === 'polygon') {
@ -475,19 +504,18 @@ export class DrawHandlerImpl implements DrawHandler {
}
public constructor(
onDrawDone: (data: object) => void,
onDrawDone: (data: object, continueDraw?: boolean) => void,
canvas: SVG.Container,
text: SVG.Container,
background: SVGSVGElement,
) {
this.onDrawDone = onDrawDone;
this.canvas = canvas;
this.text = text;
this.background = background;
this.drawData = null;
this.geometry = null;
this.crosshair = null;
this.drawInstance = null;
this.pointsGroup = null;
this.canvas.on('mousemove.crosshair', (e: MouseEvent): void => {
if (this.crosshair) {
@ -525,16 +553,25 @@ export class DrawHandlerImpl implements DrawHandler {
});
}
if (this.pointsGroup) {
for (const point of this.pointsGroup.children()) {
point.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / geometry.scale,
r: consts.BASE_POINT_SIZE / geometry.scale,
});
}
}
if (this.drawInstance) {
this.drawInstance.draw('transform');
this.drawInstance.style({
this.drawInstance.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
});
const paintHandler = this.drawInstance.remember('_paintHandler');
for (const point of (paintHandler as any).set.members) {
point.style(
point.attr(
'stroke-width',
`${consts.POINTS_STROKE_WIDTH / geometry.scale}`,
);

@ -9,7 +9,6 @@ import 'svg.select.js';
import consts from './consts';
import {
translateFromSVG,
translateBetweenSVG,
pointsToArray,
} from './shared';
import {
@ -27,7 +26,6 @@ export class EditHandlerImpl implements EditHandler {
private onEditDone: (state: any, points: number[]) => void;
private geometry: Geometry;
private canvas: SVG.Container;
private background: SVGSVGElement;
private editData: EditData;
private editedShape: SVG.Shape;
private editLine: SVG.PolyLine;
@ -94,21 +92,19 @@ export class EditHandlerImpl implements EditHandler {
}
}
private stopEdit(e: MouseEvent): void {
function selectPolygon(shape: SVG.Polygon): void {
const points = translateBetweenSVG(
this.canvas.node as any as SVGSVGElement,
this.background,
pointsToArray(shape.attr('points')),
);
const { state } = this.editData;
this.edit({
enabled: false,
});
this.onEditDone(state, points);
}
private selectPolygon(shape: SVG.Polygon): void {
const { offset } = this.geometry;
const points = pointsToArray(shape.attr('points'))
.map((coord: number): number => coord - offset);
const { state } = this.editData;
this.edit({
enabled: false,
});
this.onEditDone(state, points);
}
private stopEdit(e: MouseEvent): void {
if (!this.editLine) {
return;
}
@ -154,7 +150,7 @@ export class EditHandlerImpl implements EditHandler {
}
for (const clone of this.clones) {
clone.on('click', selectPolygon.bind(this, clone));
clone.on('click', (): void => this.selectPolygon(clone));
clone.on('mouseenter', (): void => {
clone.addClass('cvat_canvas_shape_splitting');
}).on('mouseleave', (): void => {
@ -170,6 +166,7 @@ export class EditHandlerImpl implements EditHandler {
}
let points = null;
const { offset } = this.geometry;
if (this.editData.state.shapeType === 'polyline') {
if (start !== this.editData.pointID) {
linePoints.reverse();
@ -181,11 +178,8 @@ export class EditHandlerImpl implements EditHandler {
points = oldPoints.concat(linePoints.slice(0, -1));
}
points = translateBetweenSVG(
this.canvas.node as any as SVGSVGElement,
this.background,
pointsToArray(points.join(' ')),
);
points = pointsToArray(points.join(' '))
.map((coord: number): number => coord - offset);
const { state } = this.editData;
this.edit({
@ -284,11 +278,9 @@ export class EditHandlerImpl implements EditHandler {
public constructor(
onEditDone: (state: any, points: number[]) => void,
canvas: SVG.Container,
background: SVGSVGElement,
) {
this.onEditDone = onEditDone;
this.canvas = canvas;
this.background = background;
this.editData = null;
this.editedShape = null;
this.editLine = null;

@ -58,23 +58,13 @@ export function translateToSVG(svg: SVGSVGElement, points: number[]): number[] {
return output;
}
// Translate point array from the first canvas coordinate system
// to another
export function translateBetweenSVG(
from: SVGSVGElement,
to: SVGSVGElement,
points: number[],
): number[] {
return translateToSVG(to, translateFromSVG(from, points));
}
export function pointsToString(points: number[]): string {
return points.reduce((acc, val, idx): string => {
if (idx % 2) {
return `${acc},${val}`;
}
return `${acc} ${val}`;
return `${acc} ${val}`.trim();
}, '');
}

@ -529,6 +529,10 @@
}
for (const object of Object.values(this.objects)) {
if (object.removed) {
continue;
}
let objectType = null;
if (object instanceof Shape) {
objectType = 'shape';
@ -712,7 +716,7 @@
let minimumState = null;
for (const state of objectStates) {
checkObjectType('object state', state, null, ObjectState);
if (state.outside) continue;
if (state.outside || state.hidden) continue;
const object = this.objects[state.clientID];
if (typeof (object) === 'undefined') {

@ -13,6 +13,7 @@
checkObjectType,
} = require('./common');
const {
colors,
ObjectShape,
ObjectType,
AttributeType,
@ -26,6 +27,8 @@
const { Label } = require('./labels');
const defaultGroupColor = '#E0E0E0';
// Called with the Annotation context
function objectStateFactory(frame, data) {
const objectState = new ObjectState(data);
@ -165,7 +168,7 @@
updateTimestamp(updated) {
const anyChanges = updated.label || updated.attributes || updated.points
|| updated.outside || updated.occluded || updated.keyframe
|| updated.group || updated.zOrder;
|| updated.zOrder;
if (anyChanges) {
this.updated = Date.now();
@ -202,6 +205,96 @@
return this.collectionZ[frame];
}
validateStateBeforeSave(frame, data) {
let fittedPoints = [];
const updated = data.updateFlags;
if (updated.label) {
checkObjectType('label', data.label, null, Label);
}
const labelAttributes = data.label.attributes
.reduce((accumulator, value) => {
accumulator[value.id] = value;
return accumulator;
}, {});
if (updated.attributes) {
for (const attrID of Object.keys(data.attributes)) {
const value = data.attributes[attrID];
if (attrID in labelAttributes) {
if (!validateAttributeValue(value, labelAttributes[attrID])) {
throw new ArgumentError(
`Trying to save an attribute attribute with id ${attrID} and invalid value ${value}`,
);
}
} else {
throw new ArgumentError(
`The label of the shape doesn't have the attribute with id ${attrID} and value ${value}`,
);
}
}
}
if (updated.points) {
checkObjectType('points', data.points, null, Array);
checkNumberOfPoints(this.shapeType, data.points);
// cut points
const { width, height } = this.frameMeta[frame];
for (let i = 0; i < data.points.length - 1; i += 2) {
const x = data.points[i];
const y = data.points[i + 1];
checkObjectType('coordinate', x, 'number', null);
checkObjectType('coordinate', y, 'number', null);
fittedPoints.push(
Math.clamp(x, 0, width),
Math.clamp(y, 0, height),
);
}
if (!checkShapeArea(this.shapeType, fittedPoints)) {
fittedPoints = false;
}
}
if (updated.occluded) {
checkObjectType('occluded', data.occluded, 'boolean', null);
}
if (updated.outside) {
checkObjectType('outside', data.outside, 'boolean', null);
}
if (updated.zOrder) {
checkObjectType('zOrder', data.zOrder, 'integer', null);
}
if (updated.lock) {
checkObjectType('lock', data.lock, 'boolean', null);
}
if (updated.color) {
checkObjectType('color', data.color, 'string', null);
if (/^#[0-9A-F]{6}$/i.test(data.color)) {
throw new ArgumentError(
`Got invalid color value: "${data.color}"`,
);
}
}
if (updated.hidden) {
checkObjectType('hidden', data.hidden, 'boolean', null);
}
if (updated.keyframe) {
checkObjectType('keyframe', data.keyframe, 'boolean', null);
}
return fittedPoints;
}
save() {
throw new ScriptingError(
'Is not implemented',
@ -289,7 +382,10 @@
points: [...this.points],
attributes: { ...this.attributes },
label: this.label,
group: this.group,
group: {
color: this.group ? colors[this.group % colors.length] : defaultGroupColor,
id: this.group,
},
color: this.color,
hidden: this.hidden,
updated: this.updated,
@ -308,111 +404,46 @@
return objectStateFactory.call(this, frame, this.get(frame));
}
// All changes are done in this temporary object
const copy = this.get(frame);
const fittedPoints = this.validateStateBeforeSave(frame, data);
const updated = data.updateFlags;
// Now when all fields are validated, we can apply them
if (updated.label) {
checkObjectType('label', data.label, null, Label);
copy.label = data.label;
copy.attributes = {};
this.appendDefaultAttributes.call(copy, copy.label);
this.label = data.label;
this.attributes = {};
this.appendDefaultAttributes(data.label);
}
if (updated.attributes) {
const labelAttributes = copy.label.attributes
.reduce((accumulator, value) => {
accumulator[value.id] = value;
return accumulator;
}, {});
for (const attrID of Object.keys(data.attributes)) {
const value = data.attributes[attrID];
if (attrID in labelAttributes) {
if (validateAttributeValue(value, labelAttributes[attrID])) {
copy.attributes[attrID] = value;
} else {
throw new ArgumentError(
`Trying to save an attribute attribute with id ${attrID} and invalid value ${value}`,
);
}
} else {
throw new ArgumentError(
`Trying to save unknown attribute with id ${attrID} and value ${value}`,
);
}
this.attributes[attrID] = data.attributes[attrID];
}
}
if (updated.points) {
checkObjectType('points', data.points, null, Array);
checkNumberOfPoints(this.shapeType, data.points);
// cut points
const { width, height } = this.frameMeta[frame];
const cutPoints = [];
for (let i = 0; i < data.points.length - 1; i += 2) {
const x = data.points[i];
const y = data.points[i + 1];
checkObjectType('coordinate', x, 'number', null);
checkObjectType('coordinate', y, 'number', null);
cutPoints.push(
Math.clamp(x, 0, width),
Math.clamp(y, 0, height),
);
}
if (checkShapeArea(this.shapeType, cutPoints)) {
copy.points = cutPoints;
}
if (updated.points && fittedPoints.length) {
this.points = [...fittedPoints];
}
if (updated.occluded) {
checkObjectType('occluded', data.occluded, 'boolean', null);
copy.occluded = data.occluded;
}
if (updated.group) {
checkObjectType('group', data.group, 'integer', null);
copy.group = data.group;
this.occluded = data.occluded;
}
if (updated.zOrder) {
checkObjectType('zOrder', data.zOrder, 'integer', null);
copy.zOrder = data.zOrder;
this.zOrder = data.zOrder;
}
if (updated.lock) {
checkObjectType('lock', data.lock, 'boolean', null);
copy.lock = data.lock;
this.lock = data.lock;
}
if (updated.color) {
checkObjectType('color', data.color, 'string', null);
if (/^#[0-9A-F]{6}$/i.test(data.color)) {
throw new ArgumentError(
`Got invalid color value: "${data.color}"`,
);
}
copy.color = data.color;
this.color = data.color;
}
if (updated.hidden) {
checkObjectType('hidden', data.hidden, 'boolean', null);
copy.hidden = data.hidden;
this.hidden = data.hidden;
}
// Commit state
for (const prop of Object.keys(copy)) {
if (prop in this) {
this[prop] = copy[prop];
}
}
// Reset flags and commit all changes
this.updateTimestamp(updated);
updated.reset();
@ -496,10 +527,20 @@
// Method is used to construct ObjectState objects
get(frame) {
const {
prev,
next,
first,
last,
} = this.boundedKeyframes(frame);
return {
...this.getPosition(frame),
...this.getPosition(frame, prev, next),
attributes: this.getAttributes(frame),
group: this.group,
group: {
color: this.group ? colors[this.group % colors.length] : defaultGroupColor,
id: this.group,
},
objectType: ObjectType.TRACK,
shapeType: this.shapeType,
clientID: this.clientID,
@ -509,30 +550,48 @@
hidden: this.hidden,
updated: this.updated,
label: this.label,
keyframes: {
prev,
next,
first,
last,
},
frame,
};
}
neighborsFrames(targetFrame) {
boundedKeyframes(targetFrame) {
const frames = Object.keys(this.shapes).map((frame) => +frame);
let lDiff = Number.MAX_SAFE_INTEGER;
let rDiff = Number.MAX_SAFE_INTEGER;
let first = Number.MAX_SAFE_INTEGER;
let last = Number.MIN_SAFE_INTEGER;
for (const frame of frames) {
if (frame < first) {
first = frame;
}
if (frame > last) {
last = frame;
}
const diff = Math.abs(targetFrame - frame);
if (frame <= targetFrame && diff < lDiff) {
if (frame < targetFrame && diff < lDiff) {
lDiff = diff;
} else if (diff < rDiff) {
} else if (frame > targetFrame && diff < rDiff) {
rDiff = diff;
}
}
const leftFrame = lDiff === Number.MAX_SAFE_INTEGER ? null : targetFrame - lDiff;
const rightFrame = rDiff === Number.MAX_SAFE_INTEGER ? null : targetFrame + rDiff;
const prev = lDiff === Number.MAX_SAFE_INTEGER ? null : targetFrame - lDiff;
const next = rDiff === Number.MAX_SAFE_INTEGER ? null : targetFrame + rDiff;
return {
leftFrame,
rightFrame,
prev,
next,
first,
last,
};
}
@ -568,181 +627,80 @@
return objectStateFactory.call(this, frame, this.get(frame));
}
// All changes are done in this temporary object
const copy = Object.assign(this.get(frame));
copy.attributes = Object.assign(copy.attributes);
copy.points = [...copy.points];
const fittedPoints = this.validateStateBeforeSave(frame, data);
const updated = data.updateFlags;
let positionUpdated = false;
if (updated.label) {
checkObjectType('label', data.label, null, Label);
copy.label = data.label;
copy.attributes = {};
// Shape attributes will be removed later after all checks
this.appendDefaultAttributes.call(copy, copy.label);
}
const labelAttributes = copy.label.attributes
const current = this.get(frame);
const labelAttributes = data.label.attributes
.reduce((accumulator, value) => {
accumulator[value.id] = value;
return accumulator;
}, {});
if (updated.attributes) {
for (const attrID of Object.keys(data.attributes)) {
const value = data.attributes[attrID];
if (attrID in labelAttributes) {
if (validateAttributeValue(value, labelAttributes[attrID])) {
copy.attributes[attrID] = value;
} else {
throw new ArgumentError(
`Trying to save an attribute attribute with id ${attrID} and invalid value ${value}`,
);
}
} else {
throw new ArgumentError(
`Trying to save unknown attribute with id ${attrID} and value ${value}`,
);
}
}
}
if (updated.points) {
checkObjectType('points', data.points, null, Array);
checkNumberOfPoints(this.shapeType, data.points);
// cut points
const { width, height } = this.frameMeta[frame];
const cutPoints = [];
for (let i = 0; i < data.points.length - 1; i += 2) {
const x = data.points[i];
const y = data.points[i + 1];
checkObjectType('coordinate', x, 'number', null);
checkObjectType('coordinate', y, 'number', null);
cutPoints.push(
Math.clamp(x, 0, width),
Math.clamp(y, 0, height),
);
}
if (checkShapeArea(this.shapeType, cutPoints)) {
copy.points = cutPoints;
positionUpdated = true;
}
}
if (updated.occluded) {
checkObjectType('occluded', data.occluded, 'boolean', null);
copy.occluded = data.occluded;
positionUpdated = true;
}
if (updated.outside) {
checkObjectType('outside', data.outside, 'boolean', null);
copy.outside = data.outside;
positionUpdated = true;
}
if (updated.group) {
checkObjectType('group', data.group, 'integer', null);
copy.group = data.group;
}
if (updated.zOrder) {
checkObjectType('zOrder', data.zOrder, 'integer', null);
copy.zOrder = data.zOrder;
positionUpdated = true;
}
if (updated.lock) {
checkObjectType('lock', data.lock, 'boolean', null);
copy.lock = data.lock;
}
if (updated.color) {
checkObjectType('color', data.color, 'string', null);
if (/^#[0-9A-F]{6}$/i.test(data.color)) {
throw new ArgumentError(
`Got invalid color value: "${data.color}"`,
);
}
copy.color = data.color;
}
if (updated.hidden) {
checkObjectType('hidden', data.hidden, 'boolean', null);
copy.hidden = data.hidden;
}
if (updated.keyframe) {
// Just check here
checkObjectType('keyframe', data.keyframe, 'boolean', null);
}
// Commit all changes
for (const prop of Object.keys(copy)) {
if (prop in this) {
this[prop] = copy[prop];
if (updated.label) {
this.label = data.label;
this.attributes = {};
for (const shape of Object.values(this.shapes)) {
shape.attributes = {};
}
this.appendDefaultAttributes(data.label);
}
let mutableAttributesUpdated = false;
if (updated.attributes) {
// Mutable attributes will be updated below
for (const attrID of Object.keys(copy.attributes)) {
for (const attrID of Object.keys(data.attributes)) {
if (!labelAttributes[attrID].mutable) {
this.attributes[attrID] = data.attributes[attrID];
this.attributes[attrID] = data.attributes[attrID];
} else if (data.attributes[attrID] !== current.attributes[attrID]) {
mutableAttributesUpdated = mutableAttributesUpdated
// not keyframe yet
|| !(frame in this.shapes)
// keyframe, but without this attrID
|| !(attrID in this.shapes[frame])
// keyframe with attrID, but with another value
|| (this.shapes[frame][attrID] !== data.attributes[attrID]);
}
}
}
if (updated.label) {
for (const shape of Object.values(this.shapes)) {
shape.attributes = {};
}
if (updated.lock) {
this.lock = data.lock;
}
// Remove keyframe
if (updated.keyframe && !data.keyframe) {
if (frame in this.shapes) {
if (Object.keys(this.shapes).length === 1) {
throw new DataError('You cannot remove the latest keyframe of a track');
}
delete this.shapes[frame];
this.updateTimestamp(updated);
updated.reset();
}
return objectStateFactory.call(this, frame, this.get(frame));
if (updated.color) {
this.color = data.color;
}
// Add/update keyframe
if (positionUpdated || updated.attributes || (updated.keyframe && data.keyframe)) {
data.keyframe = true;
if (updated.hidden) {
this.hidden = data.hidden;
}
if (updated.points || updated.keyframe || updated.outside
|| updated.occluded || updated.zOrder || mutableAttributesUpdated) {
const mutableAttributes = frame in this.shapes ? this.shapes[frame].attributes : {};
this.shapes[frame] = {
frame,
zOrder: copy.zOrder,
points: copy.points,
outside: copy.outside,
occluded: copy.occluded,
attributes: {},
zOrder: data.zOrder,
points: updated.points && fittedPoints.length ? fittedPoints : current.points,
outside: data.outside,
occluded: data.occluded,
attributes: mutableAttributes,
};
if (updated.attributes) {
// Unmutable attributes were updated above
for (const attrID of Object.keys(copy.attributes)) {
if (labelAttributes[attrID].mutable) {
this.shapes[frame].attributes[attrID] = data.attributes[attrID];
this.shapes[frame].attributes[attrID] = data.attributes[attrID];
}
for (const attrID of Object.keys(data.attributes)) {
if (labelAttributes[attrID].mutable
&& data.attributes[attrID] !== current.attributes[attrID]) {
this.shapes[frame].attributes[attrID] = data.attributes[attrID];
this.shapes[frame].attributes[attrID] = data.attributes[attrID];
}
}
if (updated.keyframe && !data.keyframe) {
if (Object.keys(this.shapes).length === 1) {
throw new DataError('You are not able to remove the latest keyframe for a track. '
+ 'Consider removing a track instead');
} else {
delete this.shapes[frame];
}
}
}
@ -753,58 +711,45 @@
return objectStateFactory.call(this, frame, this.get(frame));
}
getPosition(targetFrame) {
const {
leftFrame,
rightFrame,
} = this.neighborsFrames(targetFrame);
getPosition(targetFrame, leftKeyframe, rightFrame) {
const leftFrame = targetFrame in this.shapes ? targetFrame : leftKeyframe;
const rightPosition = Number.isInteger(rightFrame) ? this.shapes[rightFrame] : null;
const leftPosition = Number.isInteger(leftFrame) ? this.shapes[leftFrame] : null;
if (leftPosition && leftFrame === targetFrame) {
return {
points: [...leftPosition.points],
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
keyframe: true,
};
}
if (rightPosition && leftPosition) {
if (leftPosition && rightPosition) {
return {
...this.interpolatePosition(
leftPosition,
rightPosition,
(targetFrame - leftFrame) / (rightFrame - leftFrame),
),
keyframe: false,
keyframe: targetFrame in this.shapes,
};
}
if (rightPosition) {
if (leftPosition) {
return {
points: [...rightPosition.points],
occluded: rightPosition.occluded,
outside: true,
points: [...leftPosition.points],
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: 0,
keyframe: false,
keyframe: targetFrame in this.shapes,
};
}
if (leftPosition) {
if (rightPosition) {
return {
points: [...leftPosition.points],
occluded: leftPosition.occluded,
outside: leftPosition.outside,
points: [...rightPosition.points],
occluded: rightPosition.occluded,
outside: true,
zOrder: 0,
keyframe: false,
keyframe: targetFrame in this.shapes,
};
}
throw new ScriptingError(
`No one neightbour frame found for the track with client ID: "${this.id}"`,
throw new DataError(
'No one left position or right position was found. '
+ `Interpolation impossible. Client ID: ${this.id}`,
);
}
@ -813,7 +758,7 @@
this.removed = true;
}
return true;
return this.removed;
}
}
@ -873,46 +818,61 @@
return objectStateFactory.call(this, frame, this.get(frame));
}
// All changes are done in this temporary object
const copy = this.get(frame);
if (this.lock && data.lock) {
return objectStateFactory.call(this, frame, this.get(frame));
}
const updated = data.updateFlags;
// First validate all the fields
if (updated.label) {
checkObjectType('label', data.label, null, Label);
copy.label = data.label;
copy.attributes = {};
this.appendDefaultAttributes.call(copy, copy.label);
}
if (updated.attributes) {
const labelAttributes = copy.label
.attributes.map((attr) => `${attr.id}`);
const labelAttributes = data.label.attributes
.reduce((accumulator, value) => {
accumulator[value.id] = value;
return accumulator;
}, {});
for (const attrID of Object.keys(data.attributes)) {
if (labelAttributes.includes(attrID)) {
copy.attributes[attrID] = data.attributes[attrID];
const value = data.attributes[attrID];
if (attrID in labelAttributes) {
if (!validateAttributeValue(value, labelAttributes[attrID])) {
throw new ArgumentError(
`Trying to save an attribute attribute with id ${attrID} and invalid value ${value}`,
);
}
} else {
throw new ArgumentError(
`Trying to save unknown attribute with id ${attrID} and value ${value}`,
);
}
}
}
if (updated.group) {
checkObjectType('group', data.group, 'integer', null);
copy.group = data.group;
}
if (updated.lock) {
checkObjectType('lock', data.lock, 'boolean', null);
copy.lock = data.lock;
}
// Commit state
for (const prop of Object.keys(copy)) {
if (prop in this) {
this[prop] = copy[prop];
// Now when all fields are validated, we can apply them
if (updated.label) {
this.label = data.label;
this.attributes = {};
this.appendDefaultAttributes(data.label);
}
if (updated.attributes) {
for (const attrID of Object.keys(data.attributes)) {
this.attributes[attrID] = data.attributes[attrID];
}
}
// Reset flags and commit all changes
if (updated.lock) {
this.lock = data.lock;
}
this.updateTimestamp(updated);
updated.reset();

@ -56,7 +56,7 @@
if (typeof (value) !== type) {
// specific case for integers which aren't native type in JS
if (type === 'integer' && Number.isInteger(value)) {
return;
return true;
}
throw new ArgumentError(
@ -77,6 +77,8 @@
);
}
}
return true;
}
module.exports = {

@ -167,7 +167,7 @@
};
/**
* Array of hex color
* Array of hex colors
* @type {module:API.cvat.classes.Loader[]} values
* @name colors
* @memberof module:API.cvat.enums

@ -100,9 +100,14 @@
} else if (isBrowser) {
const reader = new FileReader();
reader.onload = () => {
frameCache[this.tid][this.number] = reader.result;
resolve(frameCache[this.tid][this.number]);
const image = new Image(frame.width, frame.height);
image.onload = () => {
frameCache[this.tid][this.number] = image;
resolve(frameCache[this.tid][this.number]);
};
image.src = reader.result;
};
reader.readAsDataURL(frame);
}
}

@ -21,7 +21,7 @@
* initial information about an ObjectState;
* Necessary fields: objectType, shapeType, frame, updated
* Optional fields: points, group, zOrder, outside, occluded, hidden,
* attributes, lock, label, mode, color, keyframe, clientID, serverID
* attributes, lock, label, mode, color, keyframe, keyframes, clientID, serverID
* These fields can be set later via setters
*/
constructor(serialized) {
@ -34,11 +34,12 @@
occluded: null,
keyframe: null,
group: null,
zOrder: null,
lock: null,
color: null,
hidden: null,
group: serialized.group,
keyframes: serialized.keyframes,
updated: serialized.updated,
clientID: serialized.clientID,
@ -61,7 +62,6 @@
this.occluded = false;
this.keyframe = false;
this.group = false;
this.zOrder = false;
this.lock = false;
this.color = false;
@ -190,16 +190,14 @@
},
group: {
/**
* Object with short group info { color, id }
* @name group
* @type {integer}
* @type {object}
* @memberof module:API.cvat.classes.ObjectState
* @instance
* @readonly
*/
get: () => data.group,
set: (group) => {
data.updateFlags.group = true;
data.group = group;
},
},
zOrder: {
/**
@ -240,6 +238,22 @@
data.keyframe = keyframe;
},
},
keyframes: {
/**
* Object of keyframes { first, prev, next, last }
* @name keyframes
* @type {object}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => {
if (data.keyframes) {
return { ...data.keyframes };
}
return null;
},
},
occluded: {
/**
* @name occluded
@ -306,7 +320,6 @@
}));
this.label = serialized.label;
this.group = serialized.group;
this.zOrder = serialized.zOrder;
this.outside = serialized.outside;
this.keyframe = serialized.keyframe;

@ -548,7 +548,7 @@ describe('Feature: group annotations', () => {
expect(typeof (groupID)).toBe('number');
annotations = await task.annotations.get(0);
for (const state of annotations) {
expect(state.group).toBe(groupID);
expect(state.group.id).toBe(groupID);
}
});
@ -559,7 +559,7 @@ describe('Feature: group annotations', () => {
expect(typeof (groupID)).toBe('number');
annotations = await job.annotations.get(0);
for (const state of annotations) {
expect(state.group).toBe(groupID);
expect(state.group.id).toBe(groupID);
}
});

@ -32,6 +32,8 @@ export enum AnnotationActionTypes {
MERGE_OBJECTS = 'MERGE_OBJECTS',
GROUP_OBJECTS = 'GROUP_OBJECTS',
SPLIT_TRACK = 'SPLIT_TRACK',
COPY_SHAPE = 'COPY_SHAPE',
EDIT_SHAPE = 'EDIT_SHAPE',
DRAW_SHAPE = 'DRAW_SHAPE',
SHAPE_DRAWN = 'SHAPE_DRAWN',
RESET_CANVAS = 'RESET_CANVAS',
@ -50,7 +52,215 @@ export enum AnnotationActionTypes {
UPDATE_TAB_CONTENT_HEIGHT = 'UPDATE_TAB_CONTENT_HEIGHT',
COLLAPSE_SIDEBAR = 'COLLAPSE_SIDEBAR',
COLLAPSE_APPEARANCE = 'COLLAPSE_APPEARANCE',
COLLAPSE_OBJECT_ITEMS = 'COLLAPSE_OBJECT_ITEMS'
COLLAPSE_OBJECT_ITEMS = 'COLLAPSE_OBJECT_ITEMS',
ACTIVATE_OBJECT = 'ACTIVATE_OBJECT',
SELECT_OBJECTS = 'SELECT_OBJECTS',
REMOVE_OBJECT_SUCCESS = 'REMOVE_OBJECT_SUCCESS',
REMOVE_OBJECT_FAILED = 'REMOVE_OBJECT_FAILED',
PROPAGATE_OBJECT = 'PROPAGATE_OBJECT',
PROPAGATE_OBJECT_SUCCESS = 'PROPAGATE_OBJECT_SUCCESS',
PROPAGATE_OBJECT_FAILED = 'PROPAGATE_OBJECT_FAILED',
CHANGE_PROPAGATE_FRAMES = 'CHANGE_PROPAGATE_FRAMES',
SWITCH_SHOWING_STATISTICS = 'SWITCH_SHOWING_STATISTICS',
COLLECT_STATISTICS = 'COLLECT_STATISTICS',
COLLECT_STATISTICS_SUCCESS = 'COLLECT_STATISTICS_SUCCESS',
COLLECT_STATISTICS_FAILED = 'COLLECT_STATISTICS_FAILED',
CHANGE_JOB_STATUS = 'CHANGE_JOB_STATUS',
CHANGE_JOB_STATUS_SUCCESS = 'CHANGE_JOB_STATUS_SUCCESS',
CHANGE_JOB_STATUS_FAILED = 'CHANGE_JOB_STATUS_FAILED',
}
export function changeJobStatusAsync(jobInstance: any, status: string):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const oldStatus = jobInstance.status;
try {
dispatch({
type: AnnotationActionTypes.CHANGE_JOB_STATUS,
payload: {},
});
// eslint-disable-next-line no-param-reassign
jobInstance.status = status;
await jobInstance.save();
dispatch({
type: AnnotationActionTypes.CHANGE_JOB_STATUS_SUCCESS,
payload: {},
});
} catch (error) {
// eslint-disable-next-line no-param-reassign
jobInstance.status = oldStatus;
dispatch({
type: AnnotationActionTypes.CHANGE_JOB_STATUS_FAILED,
payload: {
error,
},
});
}
};
}
export function collectStatisticsAsync(sessionInstance: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
dispatch({
type: AnnotationActionTypes.COLLECT_STATISTICS,
payload: {},
});
const data = await sessionInstance.annotations.statistics();
dispatch({
type: AnnotationActionTypes.COLLECT_STATISTICS_SUCCESS,
payload: {
data,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.COLLECT_STATISTICS_FAILED,
payload: {
error,
},
});
}
};
}
export function showStatistics(visible: boolean): AnyAction {
return {
type: AnnotationActionTypes.SWITCH_SHOWING_STATISTICS,
payload: {
visible,
},
};
}
export function propagateObjectAsync(
sessionInstance: any,
objectState: any,
from: number,
to: number,
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const copy = {
attributes: objectState.attributes,
points: objectState.points,
occluded: objectState.occluded,
objectType: objectState.objectType !== ObjectType.TRACK
? objectState.objectType : ObjectType.SHAPE,
shapeType: objectState.shapeType,
label: objectState.label,
frame: from,
};
const states = [];
for (let frame = from; frame <= to; frame++) {
copy.frame = frame;
const newState = new cvat.classes.ObjectState(copy);
states.push(newState);
}
await sessionInstance.annotations.put(states);
dispatch({
type: AnnotationActionTypes.PROPAGATE_OBJECT_SUCCESS,
payload: {
objectState,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.PROPAGATE_OBJECT_FAILED,
payload: {
error,
},
});
}
};
}
export function propagateObject(objectState: any | null): AnyAction {
return {
type: AnnotationActionTypes.PROPAGATE_OBJECT,
payload: {
objectState,
},
};
}
export function changePropagateFrames(frames: number): AnyAction {
return {
type: AnnotationActionTypes.CHANGE_PROPAGATE_FRAMES,
payload: {
frames,
},
};
}
export function removeObjectAsync(objectState: any, force: boolean):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const removed = await objectState.delete(force);
if (removed) {
dispatch({
type: AnnotationActionTypes.REMOVE_OBJECT_SUCCESS,
payload: {
objectState,
},
});
} else {
throw new Error('Could not remove the object. Is it locked?');
}
} catch (error) {
dispatch({
type: AnnotationActionTypes.REMOVE_OBJECT_FAILED,
payload: {
objectState,
},
});
}
};
}
export function editShape(enabled: boolean): AnyAction {
return {
type: AnnotationActionTypes.EDIT_SHAPE,
payload: {
enabled,
},
};
}
export function copyShape(objectState: any): AnyAction {
return {
type: AnnotationActionTypes.COPY_SHAPE,
payload: {
objectState,
},
};
}
export function selectObjects(selectedStatesID: number[]): AnyAction {
return {
type: AnnotationActionTypes.SELECT_OBJECTS,
payload: {
selectedStatesID,
},
};
}
export function activateObject(activatedStateID: number | null): AnyAction {
return {
type: AnnotationActionTypes.ACTIVATE_OBJECT,
payload: {
activatedStateID,
},
};
}
export function updateTabContentHeight(tabContentHeight: number): AnyAction {
@ -452,23 +662,32 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
};
}
export function changeLabelColor(label: any, color: string): AnyAction {
try {
const updatedLabel = label;
updatedLabel.color = color;
return {
type: AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS,
payload: {
label: updatedLabel,
},
};
} catch (error) {
return {
type: AnnotationActionTypes.CHANGE_LABEL_COLOR_FAILED,
payload: {
error,
},
};
}
export function changeLabelColorAsync(
sessionInstance: any,
frameNumber: number,
label: any,
color: string,
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const updatedLabel = label;
updatedLabel.color = color;
const states = await sessionInstance.annotations.get(frameNumber);
dispatch({
type: AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS,
payload: {
label: updatedLabel,
states,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.CHANGE_LABEL_COLOR_FAILED,
payload: {
error,
},
});
}
};
}

@ -1,6 +1,7 @@
import { AnyAction } from 'redux';
import {
GridColor,
ColorBy,
} from 'reducers/interfaces';
export enum SettingsActionTypes {
@ -9,6 +10,46 @@ export enum SettingsActionTypes {
CHANGE_GRID_SIZE = 'CHANGE_GRID_SIZE',
CHANGE_GRID_COLOR = 'CHANGE_GRID_COLOR',
CHANGE_GRID_OPACITY = 'CHANGE_GRID_OPACITY',
CHANGE_SHAPES_OPACITY = 'CHANGE_SHAPES_OPACITY',
CHANGE_SELECTED_SHAPES_OPACITY = 'CHANGE_SELECTED_SHAPES_OPACITY',
CHANGE_SHAPES_COLOR_BY = 'CHANGE_SHAPES_COLOR_BY',
CHANGE_SHAPES_BLACK_BORDERS = 'CHANGE_SHAPES_BLACK_BORDERS',
}
export function changeShapesOpacity(opacity: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_SHAPES_OPACITY,
payload: {
opacity,
},
};
}
export function changeSelectedShapesOpacity(selectedOpacity: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_SELECTED_SHAPES_OPACITY,
payload: {
selectedOpacity,
},
};
}
export function changeShapesColorBy(colorBy: ColorBy): AnyAction {
return {
type: SettingsActionTypes.CHANGE_SHAPES_COLOR_BY,
payload: {
colorBy,
},
};
}
export function changeShapesBlackBorders(blackBorders: boolean): AnyAction {
return {
type: SettingsActionTypes.CHANGE_SHAPES_BLACK_BORDERS,
payload: {
blackBorders,
},
};
}
export function switchRotateAll(rotateAll: boolean): AnyAction {

@ -17,5 +17,6 @@ $info-icon-color: #0074D9;
$objects-bar-tabs-color: #BEBEBE;
$objects-bar-icons-color: #242424; // #6E6E6E
$active-object-item-background-color: #D8ECFF;
$slider-color: #1890FF;
$monospaced-fonts-stack: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;

@ -8,6 +8,7 @@ import {
} from 'antd';
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';
interface Props {
@ -46,6 +47,7 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
<Layout className='cvat-annotation-page'>
<AnnotationTopBarContainer />
<StandardWorkspaceComponent />
<StatisticsModalContainer />
</Layout>
);
}

@ -5,6 +5,7 @@ import {
} from 'antd';
import {
ColorBy,
GridColor,
ObjectType,
} from 'reducers/interfaces';
@ -23,9 +24,15 @@ interface Props {
sidebarCollapsed: boolean;
canvasInstance: Canvas;
jobInstance: any;
activatedStateID: number | null;
selectedStatesID: number[];
annotations: any[];
frameData: any;
frame: number;
opacity: number;
colorBy: ColorBy;
selectedOpacity: number;
blackBorders: boolean;
grid: boolean;
gridSize: number;
gridColor: GridColor;
@ -38,6 +45,7 @@ interface Props {
onMergeObjects: (enabled: boolean) => void;
onGroupObjects: (enabled: boolean) => void;
onSplitTrack: (enabled: boolean) => void;
onEditShape: (enabled: boolean) => void;
onShapeDrawn: () => void;
onResetCanvas: () => void;
onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
@ -45,6 +53,8 @@ interface Props {
onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void;
onActivateObject: (activatedStateID: number | null) => void;
onSelectObjects: (selectedStatesID: number[]) => void;
}
export default class CanvasWrapperComponent extends React.PureComponent<Props> {
@ -65,12 +75,19 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
public componentDidUpdate(prevProps: Props): void {
const {
opacity,
colorBy,
selectedOpacity,
blackBorders,
grid,
gridSize,
gridColor,
gridOpacity,
frameData,
annotations,
canvasInstance,
sidebarCollapsed,
activatedStateID,
} = this.props;
if (prevProps.sidebarCollapsed !== sidebarCollapsed) {
@ -107,10 +124,32 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
}
this.updateCanvas();
if (prevProps.activatedStateID !== null
&& prevProps.activatedStateID !== activatedStateID) {
canvasInstance.activate(null);
const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`);
if (el) {
(el as any).instance.fill({ opacity: opacity / 100 });
}
}
if (prevProps.annotations !== annotations || prevProps.frameData !== frameData) {
this.updateCanvas();
}
if (prevProps.opacity !== opacity || prevProps.blackBorders !== blackBorders
|| prevProps.selectedOpacity !== selectedOpacity || prevProps.colorBy !== colorBy) {
this.updateShapesView();
}
this.activateOnCanvas();
}
private async onShapeDrawn(event: any): Promise<void> {
public componentWillUnmount(): void {
window.removeEventListener('resize', this.fitCanvas);
}
private onShapeDrawn(event: any): void {
const {
jobInstance,
activeLabelID,
@ -120,7 +159,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onCreateAnnotations,
} = this.props;
onShapeDrawn();
if (!event.detail.continue) {
onShapeDrawn();
}
const { state } = event.detail;
if (!state.objectType) {
@ -132,7 +173,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
.filter((label: any) => label.id === activeLabelID);
}
if (!state.occluded) {
if (typeof (state.occluded) === 'undefined') {
state.occluded = false;
}
@ -141,13 +182,16 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onCreateAnnotations(jobInstance, frame, [objectState]);
}
private async onShapeEdited(event: any): Promise<void> {
private onShapeEdited(event: any): void {
const {
jobInstance,
frame,
onEditShape,
onUpdateAnnotations,
} = this.props;
onEditShape(false);
const {
state,
points,
@ -156,7 +200,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onUpdateAnnotations(jobInstance, frame, [state]);
}
private async onObjectsMerged(event: any): Promise<void> {
private onObjectsMerged(event: any): void {
const {
jobInstance,
frame,
@ -170,7 +214,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onMergeAnnotations(jobInstance, frame, states);
}
private async onObjectsGroupped(event: any): Promise<void> {
private onObjectsGroupped(event: any): void {
const {
jobInstance,
frame,
@ -184,7 +228,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onGroupAnnotations(jobInstance, frame, states);
}
private async onTrackSplitted(event: any): Promise<void> {
private onTrackSplitted(event: any): void {
const {
jobInstance,
frame,
@ -198,6 +242,61 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onSplitAnnotations(jobInstance, frame, state);
}
private fitCanvas = (): void => {
const { canvasInstance } = this.props;
canvasInstance.fitCanvas();
};
private activateOnCanvas(): void {
const {
activatedStateID,
canvasInstance,
selectedOpacity,
} = this.props;
if (activatedStateID !== null) {
canvasInstance.activate(activatedStateID);
const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`);
if (el) {
(el as any as SVGElement).setAttribute('fill-opacity', `${selectedOpacity / 100}`);
}
}
}
private updateShapesView(): void {
const {
annotations,
opacity,
colorBy,
blackBorders,
} = this.props;
for (const state of annotations) {
let shapeColor = '';
if (colorBy === ColorBy.INSTANCE) {
shapeColor = state.color;
} else if (colorBy === ColorBy.GROUP) {
shapeColor = state.group.color;
} else if (colorBy === ColorBy.LABEL) {
shapeColor = state.label.color;
}
// TODO: In this approach CVAT-UI know details of implementations CVAT-CANVAS (svg.js)
const shapeView = window.document.getElementById(`cvat_canvas_shape_${state.clientID}`);
if (shapeView) {
if (['rect', 'polygon', 'polyline'].includes(shapeView.tagName)) {
(shapeView as any).instance.fill({ color: shapeColor, opacity: opacity / 100 });
(shapeView as any).instance.stroke({ color: blackBorders ? 'black' : shapeColor });
} else {
// group of points
for (const child of (shapeView as any).instance.children()) {
child.fill({ color: shapeColor });
}
}
}
}
}
private updateCanvas(): void {
const {
annotations,
@ -222,10 +321,13 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onDragCanvas,
onZoomCanvas,
onResetCanvas,
onActivateObject,
onEditShape,
} = this.props;
// Size
canvasInstance.fitCanvas();
window.addEventListener('resize', this.fitCanvas);
this.fitCanvas();
// Grid
const gridElement = window.document.getElementById('cvat_canvas_grid');
@ -240,8 +342,21 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.grid(gridSize, gridSize);
// Events
canvasInstance.html().addEventListener('click', (e: MouseEvent): void => {
if ((e.target as HTMLElement).tagName === 'svg') {
onActivateObject(null);
}
});
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', () => {
@ -268,7 +383,29 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
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,
@ -282,7 +419,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
}
canvasInstance.activate(result.state.clientID);
if (activatedStateID !== result.state.clientID) {
onActivateObject(result.state.clientID);
}
}
});

@ -87,7 +87,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
/>
<Tooltip overlay='Setup a tag' placement='right'>
<Icon component={TagIcon} />
<Icon component={TagIcon} style={{ pointerEvents: 'none', opacity: 0.5 }} />
</Tooltip>
<hr />

@ -22,7 +22,7 @@ interface Props {
activeControl: ActiveControl;
}
const CursorControl = React.memo((props: Props): JSX.Element => {
function CursorControl(props: Props): JSX.Element {
const {
canvasInstance,
activeControl,
@ -43,6 +43,6 @@ const CursorControl = React.memo((props: Props): JSX.Element => {
/>
</Tooltip>
);
});
}
export default CursorControl;
export default React.memo(CursorControl);

@ -15,7 +15,7 @@ interface Props {
isDrawing: boolean;
}
const DrawPointsControl = React.memo((props: Props): JSX.Element => {
function DrawPointsControl(props: Props): JSX.Element {
const {
canvasInstance,
isDrawing,
@ -49,6 +49,6 @@ const DrawPointsControl = React.memo((props: Props): JSX.Element => {
/>
</Popover>
);
});
}
export default DrawPointsControl;
export default React.memo(DrawPointsControl);

@ -15,7 +15,7 @@ interface Props {
isDrawing: boolean;
}
const DrawPolygonControl = React.memo((props: Props): JSX.Element => {
function DrawPolygonControl(props: Props): JSX.Element {
const {
canvasInstance,
isDrawing,
@ -49,6 +49,6 @@ const DrawPolygonControl = React.memo((props: Props): JSX.Element => {
/>
</Popover>
);
});
}
export default DrawPolygonControl;
export default React.memo(DrawPolygonControl);

@ -15,7 +15,7 @@ interface Props {
isDrawing: boolean;
}
const DrawPolylineControl = React.memo((props: Props): JSX.Element => {
function DrawPolylineControl(props: Props): JSX.Element {
const {
canvasInstance,
isDrawing,
@ -49,6 +49,6 @@ const DrawPolylineControl = React.memo((props: Props): JSX.Element => {
/>
</Popover>
);
});
}
export default DrawPolylineControl;
export default React.memo(DrawPolylineControl);

@ -15,7 +15,7 @@ interface Props {
isDrawing: boolean;
}
const DrawRectangleControl = React.memo((props: Props): JSX.Element => {
function DrawRectangleControl(props: Props): JSX.Element {
const {
canvasInstance,
isDrawing,
@ -49,6 +49,6 @@ const DrawRectangleControl = React.memo((props: Props): JSX.Element => {
/>
</Popover>
);
});
}
export default DrawRectangleControl;
export default React.memo(DrawRectangleControl);

@ -26,7 +26,7 @@ interface Props {
onDrawShape(): void;
}
const DrawShapePopoverComponent = React.memo((props: Props): JSX.Element => {
function DrawShapePopoverComponent(props: Props): JSX.Element {
const {
labels,
shapeType,
@ -106,6 +106,6 @@ const DrawShapePopoverComponent = React.memo((props: Props): JSX.Element => {
</Row>
</div>
);
});
}
export default DrawShapePopoverComponent;
export default React.memo(DrawShapePopoverComponent);

@ -17,7 +17,7 @@ interface Props {
canvasInstance: Canvas;
}
const FitControl = React.memo((props: Props): JSX.Element => {
function FitControl(props: Props): JSX.Element {
const {
canvasInstance,
} = props;
@ -27,6 +27,6 @@ const FitControl = React.memo((props: Props): JSX.Element => {
<Icon component={FitIcon} onClick={(): void => canvasInstance.fit()} />
</Tooltip>
);
});
}
export default FitControl;
export default React.memo(FitControl);

@ -19,7 +19,7 @@ interface Props {
groupObjects(enabled: boolean): void;
}
const GroupControl = React.memo((props: Props): JSX.Element => {
function GroupControl(props: Props): JSX.Element {
const {
activeControl,
canvasInstance,
@ -46,6 +46,6 @@ const GroupControl = React.memo((props: Props): JSX.Element => {
<Icon {...dynamicIconProps} component={GroupIcon} />
</Tooltip>
);
});
}
export default GroupControl;
export default React.memo(GroupControl);

@ -19,7 +19,7 @@ interface Props {
mergeObjects(enabled: boolean): void;
}
const MergeControl = React.memo((props: Props): JSX.Element => {
function MergeControl(props: Props): JSX.Element {
const {
activeControl,
canvasInstance,
@ -46,6 +46,6 @@ const MergeControl = React.memo((props: Props): JSX.Element => {
<Icon {...dynamicIconProps} component={MergeIcon} />
</Tooltip>
);
});
}
export default MergeControl;
export default React.memo(MergeControl);

@ -22,7 +22,7 @@ interface Props {
activeControl: ActiveControl;
}
const MoveControl = React.memo((props: Props): JSX.Element => {
function MoveControl(props: Props): JSX.Element {
const {
canvasInstance,
activeControl,
@ -46,6 +46,6 @@ const MoveControl = React.memo((props: Props): JSX.Element => {
/>
</Tooltip>
);
});
}
export default MoveControl;
export default React.memo(MoveControl);

@ -22,7 +22,7 @@ interface Props {
activeControl: ActiveControl;
}
const ResizeControl = React.memo((props: Props): JSX.Element => {
function ResizeControl(props: Props): JSX.Element {
const {
activeControl,
canvasInstance,
@ -46,6 +46,6 @@ const ResizeControl = React.memo((props: Props): JSX.Element => {
/>
</Tooltip>
);
});
}
export default ResizeControl;
export default React.memo(ResizeControl);

@ -20,7 +20,7 @@ interface Props {
rotateAll: boolean;
}
const RotateControl = React.memo((props: Props): JSX.Element => {
function RotateControl(props: Props): JSX.Element {
const {
rotateAll,
canvasInstance,
@ -55,6 +55,6 @@ const RotateControl = React.memo((props: Props): JSX.Element => {
<Icon component={RotateIcon} />
</Popover>
);
});
}
export default RotateControl;
export default React.memo(RotateControl);

@ -19,7 +19,7 @@ interface Props {
splitTrack(enabled: boolean): void;
}
const SplitControl = React.memo((props: Props): JSX.Element => {
function SplitControl(props: Props): JSX.Element {
const {
activeControl,
canvasInstance,
@ -46,6 +46,6 @@ const SplitControl = React.memo((props: Props): JSX.Element => {
<Icon {...dynamicIconProps} component={SplitIcon} />
</Tooltip>
);
});
}
export default SplitControl;
export default React.memo(SplitControl);

@ -0,0 +1,90 @@
import React from 'react';
import {
Checkbox,
Collapse,
Slider,
Radio,
} from 'antd';
import Text from 'antd/lib/typography/Text';
import { RadioChangeEvent } from 'antd/lib/radio';
import { SliderValue } from 'antd/lib/slider';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import { ColorBy } from 'reducers/interfaces';
interface Props {
appearanceCollapsed: boolean;
colorBy: ColorBy;
opacity: number;
selectedOpacity: number;
blackBorders: boolean;
collapseAppearance(): void;
changeShapesColorBy(event: RadioChangeEvent): void;
changeShapesOpacity(event: SliderValue): void;
changeSelectedShapesOpacity(event: SliderValue): void;
changeShapesBlackBorders(event: CheckboxChangeEvent): void;
}
function AppearanceBlock(props: Props): JSX.Element {
const {
appearanceCollapsed,
colorBy,
opacity,
selectedOpacity,
blackBorders,
collapseAppearance,
changeShapesColorBy,
changeShapesOpacity,
changeSelectedShapesOpacity,
changeShapesBlackBorders,
} = props;
return (
<Collapse
onChange={collapseAppearance}
activeKey={appearanceCollapsed ? [] : ['appearance']}
className='cvat-objects-appearance-collapse'
>
<Collapse.Panel
header={
<Text strong>Appearance</Text>
}
key='appearance'
>
<div className='cvat-objects-appearance-content'>
<Text type='secondary'>Color by</Text>
<Radio.Group value={colorBy} onChange={changeShapesColorBy}>
<Radio.Button value={ColorBy.INSTANCE}>{ColorBy.INSTANCE}</Radio.Button>
<Radio.Button value={ColorBy.GROUP}>{ColorBy.GROUP}</Radio.Button>
<Radio.Button value={ColorBy.LABEL}>{ColorBy.LABEL}</Radio.Button>
</Radio.Group>
<Text type='secondary'>Opacity</Text>
<Slider
onChange={changeShapesOpacity}
value={opacity}
min={0}
max={100}
/>
<Text type='secondary'>Selected opacity</Text>
<Slider
onChange={changeSelectedShapesOpacity}
value={selectedOpacity}
min={0}
max={100}
/>
<Checkbox
onChange={changeShapesBlackBorders}
checked={blackBorders}
>
Black borders
</Checkbox>
</div>
</Collapse.Panel>
</Collapse>
);
}
export default React.memo(AppearanceBlock);

@ -71,7 +71,7 @@ interface Props {
changeColor(color: string): void;
}
const LabelItemComponent = React.memo((props: Props): JSX.Element => {
function LabelItemComponent(props: Props): JSX.Element {
const {
labelName,
labelColor,
@ -125,6 +125,6 @@ const LabelItemComponent = React.memo((props: Props): JSX.Element => {
</Col>
</Row>
);
});
}
export default LabelItemComponent;
export default React.memo(LabelItemComponent);

@ -10,11 +10,15 @@ import {
Collapse,
Checkbox,
InputNumber,
Dropdown,
Menu,
Button,
Modal,
} from 'antd';
import Text from 'antd/lib/typography/Text';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import { RadioChangeEvent } from 'antd/lib/radio';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import {
ObjectOutsideIcon,
@ -28,21 +32,72 @@ import {
ObjectType, ShapeType,
} from 'reducers/interfaces';
interface ItemTopProps {
function ItemMenu(
locked: boolean,
copy: (() => void),
remove: (() => void),
propagate: (() => void),
): JSX.Element {
return (
<Menu key='unique' className='cvat-object-item-menu'>
<Menu.Item>
<Button type='link' icon='copy' onClick={copy}>
Make a copy
</Button>
</Menu.Item>
<Menu.Item>
<Button type='link' icon='block' onClick={propagate}>
Propagate
</Button>
</Menu.Item>
<Menu.Item>
<Button
type='link'
icon='delete'
onClick={(): void => {
if (locked) {
Modal.confirm({
title: 'Object is locked',
content: 'Are you sure you want to remove it?',
onOk() {
remove();
},
});
} else {
remove();
}
}}
>
Remove
</Button>
</Menu.Item>
</Menu>
);
}
interface ItemTopComponentProps {
clientID: number;
labelID: number;
labels: any[];
type: string;
locked: boolean;
changeLabel(labelID: string): void;
copy(): void;
remove(): void;
propagate(): void;
}
const ItemTop = React.memo((props: ItemTopProps): JSX.Element => {
function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
const {
clientID,
labelID,
labels,
type,
locked,
changeLabel,
copy,
remove,
propagate,
} = props;
return (
@ -62,13 +117,20 @@ const ItemTop = React.memo((props: ItemTopProps): JSX.Element => {
</Select>
</Col>
<Col span={2}>
<Icon type='more' />
<Dropdown
placement='bottomLeft'
overlay={ItemMenu(locked, copy, remove, propagate)}
>
<Icon type='more' />
</Dropdown>
</Col>
</Row>
);
});
}
const ItemTop = React.memo(ItemTopComponent);
interface ItemButtonsProps {
interface ItemButtonsComponentProps {
objectType: ObjectType;
occluded: boolean;
outside: boolean | undefined;
@ -76,6 +138,11 @@ interface ItemButtonsProps {
hidden: boolean;
keyframe: boolean | undefined;
navigateFirstKeyframe: null | (() => void);
navigatePrevKeyframe: null | (() => void);
navigateNextKeyframe: null | (() => void);
navigateLastKeyframe: null | (() => void);
setOccluded(): void;
unsetOccluded(): void;
setOutside(): void;
@ -88,7 +155,7 @@ interface ItemButtonsProps {
show(): void;
}
const ItemButtons = React.memo((props: ItemButtonsProps): JSX.Element => {
function ItemButtonsComponent(props: ItemButtonsComponentProps): JSX.Element {
const {
objectType,
occluded,
@ -96,6 +163,12 @@ const ItemButtons = React.memo((props: ItemButtonsProps): JSX.Element => {
locked,
hidden,
keyframe,
navigateFirstKeyframe,
navigatePrevKeyframe,
navigateNextKeyframe,
navigateLastKeyframe,
setOccluded,
unsetOccluded,
setOutside,
@ -114,16 +187,28 @@ const ItemButtons = React.memo((props: ItemButtonsProps): JSX.Element => {
<Col span={20} style={{ textAlign: 'center' }}>
<Row type='flex' justify='space-around'>
<Col span={6}>
<Icon component={FirstIcon} />
{ navigateFirstKeyframe
? <Icon component={FirstIcon} onClick={navigateFirstKeyframe} />
: <Icon component={FirstIcon} style={{ opacity: 0.5, pointerEvents: 'none' }} />
}
</Col>
<Col span={6}>
<Icon component={PreviousIcon} />
{ navigatePrevKeyframe
? <Icon component={PreviousIcon} onClick={navigatePrevKeyframe} />
: <Icon component={PreviousIcon} style={{ opacity: 0.5, pointerEvents: 'none' }} />
}
</Col>
<Col span={6}>
<Icon component={NextIcon} />
{ navigateNextKeyframe
? <Icon component={NextIcon} onClick={navigateNextKeyframe} />
: <Icon component={NextIcon} style={{ opacity: 0.5, pointerEvents: 'none' }} />
}
</Col>
<Col span={6}>
<Icon component={LastIcon} />
{ navigateLastKeyframe
? <Icon component={LastIcon} onClick={navigateLastKeyframe} />
: <Icon component={LastIcon} style={{ opacity: 0.5, pointerEvents: 'none' }} />
}
</Col>
</Row>
<Row type='flex' justify='space-around'>
@ -189,9 +274,11 @@ const ItemButtons = React.memo((props: ItemButtonsProps): JSX.Element => {
</Col>
</Row>
);
});
}
interface ItemAttributeProps {
const ItemButtons = React.memo(ItemButtonsComponent);
interface ItemAttributeComponentProps {
attrInputType: string;
attrValues: string[];
attrValue: string;
@ -200,7 +287,10 @@ interface ItemAttributeProps {
changeAttribute(attrID: number, value: string): void;
}
function attrIsTheSame(prevProps: ItemAttributeProps, nextProps: ItemAttributeProps): boolean {
function attrIsTheSame(
prevProps: ItemAttributeComponentProps,
nextProps: ItemAttributeComponentProps,
): boolean {
return nextProps.attrID === prevProps.attrID
&& nextProps.attrValue === prevProps.attrValue
&& nextProps.attrName === prevProps.attrName
@ -210,7 +300,7 @@ function attrIsTheSame(prevProps: ItemAttributeProps, nextProps: ItemAttributePr
.every((value: boolean): boolean => value);
}
const ItemAttribute = React.memo((props: ItemAttributeProps): JSX.Element => {
function ItemAttributeComponent(props: ItemAttributeComponentProps): JSX.Element {
const {
attrInputType,
attrValues,
@ -332,10 +422,12 @@ const ItemAttribute = React.memo((props: ItemAttributeProps): JSX.Element => {
</Col>
</>
);
}, attrIsTheSame);
}
const ItemAttribute = React.memo(ItemAttributeComponent, attrIsTheSame);
interface ItemAttributesProps {
interface ItemAttributesComponentProps {
collapsed: boolean;
attributes: any[];
values: Record<number, string>;
@ -352,13 +444,16 @@ function attrValuesAreEqual(next: Record<number, string>, prev: Record<number, s
.every((value: boolean) => value);
}
function attrAreTheSame(prevProps: ItemAttributesProps, nextProps: ItemAttributesProps): boolean {
function attrAreTheSame(
prevProps: ItemAttributesComponentProps,
nextProps: ItemAttributesComponentProps,
): boolean {
return nextProps.collapsed === prevProps.collapsed
&& nextProps.attributes === prevProps.attributes
&& attrValuesAreEqual(nextProps.values, prevProps.values);
}
const ItemAttributes = React.memo((props: ItemAttributesProps): JSX.Element => {
function ItemAttributesComponent(props: ItemAttributesComponentProps): JSX.Element {
const {
collapsed,
attributes,
@ -403,9 +498,12 @@ const ItemAttributes = React.memo((props: ItemAttributesProps): JSX.Element => {
</Collapse>
</Row>
);
}, attrAreTheSame);
}
const ItemAttributes = React.memo(ItemAttributesComponent, attrAreTheSame);
interface Props {
activated: boolean;
objectType: ObjectType;
shapeType: ShapeType;
clientID: number;
@ -421,7 +519,15 @@ interface Props {
labels: any[];
attributes: any[];
collapsed: boolean;
navigateFirstKeyframe: null | (() => void);
navigatePrevKeyframe: null | (() => void);
navigateNextKeyframe: null | (() => void);
navigateLastKeyframe: null | (() => void);
activate(): void;
copy(): void;
propagate(): void;
remove(): void;
setOccluded(): void;
unsetOccluded(): void;
setOutside(): void;
@ -438,12 +544,13 @@ interface Props {
}
function objectItemsAreEqual(prevProps: Props, nextProps: Props): boolean {
return nextProps.locked === prevProps.locked
return nextProps.activated === prevProps.activated
&& nextProps.locked === prevProps.locked
&& nextProps.occluded === prevProps.occluded
&& nextProps.outside === prevProps.outside
&& nextProps.hidden === prevProps.hidden
&& nextProps.keyframe === prevProps.keyframe
&& nextProps.label === prevProps.label
&& nextProps.labelID === prevProps.labelID
&& nextProps.color === prevProps.color
&& nextProps.clientID === prevProps.clientID
&& nextProps.objectType === prevProps.objectType
@ -451,11 +558,16 @@ function objectItemsAreEqual(prevProps: Props, nextProps: Props): boolean {
&& nextProps.collapsed === prevProps.collapsed
&& nextProps.labels === prevProps.labels
&& nextProps.attributes === prevProps.attributes
&& nextProps.navigateFirstKeyframe === prevProps.navigateFirstKeyframe
&& nextProps.navigatePrevKeyframe === prevProps.navigatePrevKeyframe
&& nextProps.navigateNextKeyframe === prevProps.navigateNextKeyframe
&& nextProps.navigateLastKeyframe === prevProps.navigateLastKeyframe
&& attrValuesAreEqual(nextProps.attrValues, prevProps.attrValues);
}
const ObjectItem = React.memo((props: Props): JSX.Element => {
function ObjectItemComponent(props: Props): JSX.Element {
const {
activated,
objectType,
shapeType,
clientID,
@ -471,7 +583,15 @@ const ObjectItem = React.memo((props: Props): JSX.Element => {
attributes,
labels,
collapsed,
navigateFirstKeyframe,
navigatePrevKeyframe,
navigateNextKeyframe,
navigateLastKeyframe,
activate,
copy,
propagate,
remove,
setOccluded,
unsetOccluded,
setOutside,
@ -490,9 +610,14 @@ const ObjectItem = React.memo((props: Props): JSX.Element => {
const type = objectType === ObjectType.TAG ? ObjectType.TAG.toUpperCase()
: `${shapeType.toUpperCase()} ${objectType.toUpperCase()}`;
const className = !activated ? 'cvat-objects-sidebar-state-item'
: 'cvat-objects-sidebar-state-item cvat-objects-sidebar-state-active-item';
return (
<div
className='cvat-objects-sidebar-state-item'
onMouseEnter={activate}
id={`cvat-objects-sidebar-state-item-${clientID}`}
className={className}
style={{ borderLeftStyle: 'solid', borderColor: ` ${color}` }}
>
<ItemTop
@ -500,7 +625,11 @@ const ObjectItem = React.memo((props: Props): JSX.Element => {
labelID={labelID}
labels={labels}
type={type}
locked={locked}
changeLabel={changeLabel}
copy={copy}
remove={remove}
propagate={propagate}
/>
<ItemButtons
objectType={objectType}
@ -509,6 +638,10 @@ const ObjectItem = React.memo((props: Props): JSX.Element => {
locked={locked}
hidden={hidden}
keyframe={keyframe}
navigateFirstKeyframe={navigateFirstKeyframe}
navigatePrevKeyframe={navigatePrevKeyframe}
navigateNextKeyframe={navigateNextKeyframe}
navigateLastKeyframe={navigateLastKeyframe}
setOccluded={setOccluded}
unsetOccluded={unsetOccluded}
setOutside={setOutside}
@ -533,6 +666,6 @@ const ObjectItem = React.memo((props: Props): JSX.Element => {
}
</div>
);
}, objectItemsAreEqual);
}
export default ObjectItem;
export default React.memo(ObjectItemComponent, objectItemsAreEqual);

@ -13,12 +13,12 @@ import Text from 'antd/lib/typography/Text';
import { StatesOrdering } from 'reducers/interfaces';
interface StatesOrderingSelectorProps {
interface StatesOrderingSelectorComponentProps {
statesOrdering: StatesOrdering;
changeStatesOrdering(value: StatesOrdering): void;
}
const StatesOrderingSelector = React.memo((props: StatesOrderingSelectorProps): JSX.Element => {
function StatesOrderingSelectorComponent(props: StatesOrderingSelectorComponentProps): JSX.Element {
const {
statesOrdering,
changeStatesOrdering,
@ -49,7 +49,9 @@ const StatesOrderingSelector = React.memo((props: StatesOrderingSelectorProps):
</Select>
</Col>
);
});
}
const StatesOrderingSelector = React.memo(StatesOrderingSelectorComponent);
interface Props {
statesHidden: boolean;
@ -65,7 +67,7 @@ interface Props {
showAllStates(): void;
}
const Header = React.memo((props: Props): JSX.Element => {
function ObjectListHeader(props: Props): JSX.Element {
const {
statesHidden,
statesLocked,
@ -116,6 +118,6 @@ const Header = React.memo((props: Props): JSX.Element => {
</Row>
</div>
);
});
}
export default Header;
export default React.memo(ObjectListHeader);

@ -3,7 +3,7 @@ import React from 'react';
import { StatesOrdering } from 'reducers/interfaces';
import ObjectItemContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-item';
import Header from './objects-list-header';
import ObjectListHeader from './objects-list-header';
interface Props {
@ -22,7 +22,7 @@ interface Props {
showAllStates(): void;
}
const ObjectListComponent = React.memo((props: Props): JSX.Element => {
function ObjectListComponent(props: Props): JSX.Element {
const {
listHeight,
statesHidden,
@ -41,7 +41,7 @@ const ObjectListComponent = React.memo((props: Props): JSX.Element => {
return (
<div style={{ height: listHeight }}>
<Header
<ObjectListHeader
statesHidden={statesHidden}
statesLocked={statesLocked}
statesCollapsed={statesCollapsed}
@ -61,6 +61,6 @@ const ObjectListComponent = React.memo((props: Props): JSX.Element => {
</div>
</div>
);
});
}
export default ObjectListComponent;
export default React.memo(ObjectListComponent);

@ -5,29 +5,66 @@ import {
Icon,
Tabs,
Layout,
Collapse,
} from 'antd';
import Text from 'antd/lib/typography/Text';
import { RadioChangeEvent } from 'antd/lib/radio';
import { SliderValue } from 'antd/lib/slider';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import { ColorBy } from 'reducers/interfaces';
import ObjectsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-list';
import LabelsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/labels-list';
import AppearanceBlock from './appearance-block';
interface Props {
sidebarCollapsed: boolean;
appearanceCollapsed: boolean;
colorBy: ColorBy;
opacity: number;
selectedOpacity: number;
blackBorders: boolean;
collapseSidebar(): void;
collapseAppearance(): void;
changeShapesColorBy(event: RadioChangeEvent): void;
changeShapesOpacity(event: SliderValue): void;
changeSelectedShapesOpacity(event: SliderValue): void;
changeShapesBlackBorders(event: CheckboxChangeEvent): void;
}
const ObjectsSideBar = React.memo((props: Props): JSX.Element => {
function ObjectsSideBar(props: Props): JSX.Element {
const {
sidebarCollapsed,
appearanceCollapsed,
colorBy,
opacity,
selectedOpacity,
blackBorders,
collapseSidebar,
collapseAppearance,
changeShapesColorBy,
changeShapesOpacity,
changeSelectedShapesOpacity,
changeShapesBlackBorders,
} = props;
const appearanceProps = {
collapseAppearance,
appearanceCollapsed,
colorBy,
opacity,
selectedOpacity,
blackBorders,
changeShapesColorBy,
changeShapesOpacity,
changeSelectedShapesOpacity,
changeShapesBlackBorders,
};
return (
<Layout.Sider
className='cvat-objects-sidebar'
@ -65,22 +102,9 @@ const ObjectsSideBar = React.memo((props: Props): JSX.Element => {
</Tabs.TabPane>
</Tabs>
<Collapse
onChange={collapseAppearance}
activeKey={appearanceCollapsed ? [] : ['appearance']}
className='cvat-objects-appearance-collapse'
>
<Collapse.Panel
header={
<Text strong>Appearance</Text>
}
key='appearance'
>
</Collapse.Panel>
</Collapse>
{ !sidebarCollapsed && <AppearanceBlock {...appearanceProps} /> }
</Layout.Sider>
);
});
}
export default ObjectsSideBar;
export default React.memo(ObjectsSideBar);

@ -18,7 +18,7 @@
> .ant-collapse-content {
background: $background-color-2;
border-bottom: none;
height: 150px;
height: 230px;
}
}
}
@ -32,10 +32,6 @@
&:hover {
transform: scale(1.1);
}
&:active {
transform: scale(1);
}
}
.cvat-objects-sidebar-tabs.ant-tabs.ant-tabs-card {
@ -144,10 +140,6 @@
> div:nth-child(3) {
margin-top: 10px;
}
&:hover {
@extend .cvat-objects-sidebar-state-active-item;
}
}
.cvat-objects-sidebar-state-item-collapse {
@ -246,3 +238,28 @@
height: 20px;
border-radius: 5px;
}
.cvat-objects-appearance-content {
> div {
width: 100%;
> label {
text-align: center;
width: 33%;
}
}
}
.cvat-object-item-menu {
> li {
padding: 0px;
> button {
padding: 5px 32px;
color: $text-color;
width: 100%;
height: 100%;
text-align: left;
}
}
}

@ -0,0 +1,55 @@
import React from 'react';
import {
Modal,
InputNumber,
} from 'antd';
import Text from 'antd/lib/typography/Text';
interface Props {
visible: boolean;
propagateFrames: number;
propagateUpToFrame: number;
propagateObject(): void;
cancel(): void;
changePropagateFrames(value: number | undefined): void;
changeUpToFrame(value: number | undefined): void;
}
export default function PropagateConfirmComponent(props: Props): JSX.Element {
const {
visible,
propagateFrames,
propagateUpToFrame,
propagateObject,
changePropagateFrames,
changeUpToFrame,
cancel,
} = props;
return (
<Modal
okType='primary'
okText='Yes'
cancelText='Cancel'
onOk={propagateObject}
onCancel={cancel}
title='Confirm propagation'
visible={visible}
>
<div className='cvat-propagate-confirm'>
<Text>Do you want to make a copy of the object on</Text>
<InputNumber size='small' min={1} value={propagateFrames} onChange={changePropagateFrames} />
{
propagateFrames > 1
? <Text> frames </Text>
: <Text> frame </Text>
}
<Text>up to the </Text>
<InputNumber size='small' value={propagateUpToFrame} onChange={changeUpToFrame} />
<Text>frame</Text>
</div>
</Modal>
);
}

@ -8,7 +8,7 @@ import {
import CanvasWrapperContainer from 'containers/annotation-page/standard-workspace/canvas-wrapper';
import ControlsSideBarContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar';
import ObjectSideBarContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar';
import PropagateConfirmContainer from 'containers/annotation-page/standard-workspace/propagate-confirm';
export default function StandardWorkspaceComponent(): JSX.Element {
return (
@ -16,6 +16,7 @@ export default function StandardWorkspaceComponent(): JSX.Element {
<ControlsSideBarContainer />
<CanvasWrapperContainer />
<ObjectSideBarContainer />
<PropagateConfirmContainer />
</Layout>
);
}

@ -104,3 +104,10 @@
}
}
}
.cvat-propagate-confirm {
> .ant-input-number {
width: 70px;
margin: 0px 5px;
}
}

@ -12,9 +12,7 @@
}
.cvat-annotation-header-left-group {
height: 100%;
> div:first-child {
> button:first-child {
filter: invert(0.9);
background: $background-color-1;
border-radius: 0px;
@ -22,15 +20,21 @@
}
}
.cvat-annotation-header-button {
.ant-btn.cvat-annotation-header-button {
padding: 0px;
width: 54px;
height: 54px;
float: left;
text-align: center;
user-select: none;
color: $text-color;
display: flex;
flex-direction: column;
align-items: center;
margin: 0px 3px;
> span {
margin-left: 0px;
font-size: 10px;
}
@ -40,7 +44,7 @@
}
&:hover > i {
transform: scale(0.9);
transform: scale(0.85);
}
&:active > i {
@ -119,38 +123,62 @@
}
.cvat-annotation-header-right-group {
height: 100%;
> div {
height: 54px;
float: left;
text-align: center;
margin-right: 20px;
display: block;
height: 54px;
margin-right: 15px;
}
}
> span {
font-size: 10px;
}
.cvat-workspace-selector {
width: 150px;
}
.cvat-job-info-modal-window {
> div {
margin-top: 10px;
}
> i {
transform: scale(0.8);
padding: 3px;
> div:nth-child(1) {
> div {
> .ant-select, i {
margin-left: 10px;
}
}
}
&:hover > i {
transform: scale(0.9);
> div:nth-child(2) {
> div {
> span {
font-size: 20px;
}
}
}
&:active > i {
transform: scale(0.8);
> div:nth-child(3) {
> div {
display: grid;
}
}
> div:not(:nth-child(3)) > * {
display: block;
line-height: 0px;
> .cvat-job-info-bug-tracker {
> div {
display: grid;
}
}
}
.cvat-workspace-selector {
width: 150px;
> .cvat-job-info-statistics {
> div {
> span {
font-size: 20px;
}
.ant-table-thead {
> tr > th {
padding: 5px 5px;
}
}
}
}
}

@ -4,6 +4,7 @@ import {
Col,
Icon,
Modal,
Button,
Timeline,
} from 'antd';
@ -20,7 +21,7 @@ interface Props {
onSaveAnnotation(): void;
}
const LeftGroup = React.memo((props: Props): JSX.Element => {
function LeftGroup(props: Props): JSX.Element {
const {
saving,
savingStatuses,
@ -29,20 +30,20 @@ const LeftGroup = React.memo((props: Props): JSX.Element => {
return (
<Col className='cvat-annotation-header-left-group'>
<div className='cvat-annotation-header-button'>
<Button type='link' className='cvat-annotation-header-button'>
<Icon component={MainMenuIcon} />
<span>Menu</span>
</div>
<div
Menu
</Button>
<Button
onClick={saving ? undefined : onSaveAnnotation}
type='link'
className={saving
? 'cvat-annotation-disabled-header-button'
: 'cvat-annotation-header-button'
}
>
<Icon component={SaveIcon} onClick={onSaveAnnotation} />
<span>
{ saving ? 'Saving...' : 'Save' }
</span>
<Icon component={SaveIcon} />
{ saving ? 'Saving...' : 'Save' }
<Modal
title='Saving changes on the server'
visible={saving}
@ -60,17 +61,17 @@ const LeftGroup = React.memo((props: Props): JSX.Element => {
}
</Timeline>
</Modal>
</div>
<div className='cvat-annotation-header-button'>
</Button>
<Button disabled type='link' className='cvat-annotation-header-button'>
<Icon component={UndoIcon} />
<span>Undo</span>
</div>
<div className='cvat-annotation-header-button'>
Undo
</Button>
<Button disabled type='link' className='cvat-annotation-header-button'>
<Icon component={RedoIcon} />
<span>Redo</span>
</div>
Redo
</Button>
</Col>
);
});
}
export default LeftGroup;
export default React.memo(LeftGroup);

@ -28,7 +28,7 @@ interface Props {
onLastFrame(): void;
}
const PlayerButtons = React.memo((props: Props): JSX.Element => {
function PlayerButtons(props: Props): JSX.Element {
const {
playing,
onSwitchPlay,
@ -82,6 +82,6 @@ const PlayerButtons = React.memo((props: Props): JSX.Element => {
</Tooltip>
</Col>
);
});
}
export default PlayerButtons;
export default React.memo(PlayerButtons);

@ -19,7 +19,7 @@ interface Props {
onInputChange(value: number | undefined): void;
}
const PlayerNavigation = React.memo((props: Props): JSX.Element => {
function PlayerNavigation(props: Props): JSX.Element {
const {
startFrame,
stopFrame,
@ -61,6 +61,6 @@ const PlayerNavigation = React.memo((props: Props): JSX.Element => {
</Col>
</>
);
});
}
export default PlayerNavigation;
export default React.memo(PlayerNavigation);

@ -4,6 +4,7 @@ import {
Col,
Icon,
Select,
Button,
} from 'antd';
import {
@ -11,23 +12,43 @@ import {
FullscreenIcon,
} from '../../../icons';
const RightGroup = React.memo((): JSX.Element => (
<Col className='cvat-annotation-header-right-group'>
<div className='cvat-annotation-header-button'>
<Icon component={FullscreenIcon} />
<span>Fullscreen</span>
</div>
<div className='cvat-annotation-header-button'>
<Icon component={InfoIcon} />
<span>Info</span>
</div>
<div>
<Select 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>
</div>
</Col>
));
interface Props {
showStatistics(): void;
}
export default RightGroup;
function RightGroup(props: Props): JSX.Element {
const { showStatistics } = props;
return (
<Col className='cvat-annotation-header-right-group'>
<Button
type='link'
className='cvat-annotation-header-button'
onClick={(): void => {
if (window.document.fullscreenEnabled) {
if (window.document.fullscreenElement) {
window.document.exitFullscreen();
} else {
window.document.documentElement.requestFullscreen();
}
}
}}
>
<Icon component={FullscreenIcon} />
Fullscreen
</Button>
<Button type='link' className='cvat-annotation-header-button' onClick={showStatistics}>
<Icon component={InfoIcon} />
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>
</div>
</Col>
);
}
export default React.memo(RightGroup);

@ -0,0 +1,203 @@
import React from 'react';
import {
Tooltip,
Select,
Table,
Modal,
Spin,
Icon,
Row,
Col,
} from 'antd';
import Text from 'antd/lib/typography/Text';
interface Props {
collecting: boolean;
data: any;
visible: boolean;
assignee: string;
startFrame: number;
stopFrame: number;
zOrder: boolean;
bugTracker: string;
jobStatus: string;
savingJobStatus: boolean;
closeStatistics(): void;
changeJobStatus(status: string): void;
}
export default function StatisticsModalComponent(props: Props): JSX.Element {
const {
collecting,
data,
visible,
jobStatus,
assignee,
startFrame,
stopFrame,
zOrder,
bugTracker,
closeStatistics,
changeJobStatus,
savingJobStatus,
} = props;
const baseProps = {
cancelButtonProps: { style: { display: 'none' } },
okButtonProps: { style: { width: 100 } },
onOk: closeStatistics,
width: 1000,
visible,
closable: false,
};
if (collecting || !data) {
return (
<Modal
{...baseProps}
>
<Spin style={{ margin: '0 50%' }} />
</Modal>
);
}
const rows = Object.keys(data.label).map((key: string) => ({
key,
label: key,
rectangle: `${data.label[key].rectangle.shape} / ${data.label[key].rectangle.track}`,
polygon: `${data.label[key].polygon.shape} / ${data.label[key].polygon.track}`,
polyline: `${data.label[key].polyline.shape} / ${data.label[key].polyline.track}`,
points: `${data.label[key].points.shape} / ${data.label[key].points.track}`,
tags: data.label[key].tags,
manually: data.label[key].manually,
interpolated: data.label[key].interpolated,
total: data.label[key].total,
}));
rows.push({
key: '___total',
label: 'Total',
rectangle: `${data.total.rectangle.shape} / ${data.total.rectangle.track}`,
polygon: `${data.total.polygon.shape} / ${data.total.polygon.track}`,
polyline: `${data.total.polyline.shape} / ${data.total.polyline.track}`,
points: `${data.total.points.shape} / ${data.total.points.track}`,
tags: data.total.tags,
manually: data.total.manually,
interpolated: data.total.interpolated,
total: data.total.total,
});
const makeShapesTracksTitle = (title: string): JSX.Element => (
<Tooltip overlay='Shapes / Tracks'>
<Text strong style={{ marginRight: 5 }}>{title}</Text>
<Icon className='cvat-info-circle-icon' type='question-circle' />
</Tooltip>
);
const columns = [{
title: <Text strong> Label </Text>,
dataIndex: 'label',
key: 'label',
}, {
title: makeShapesTracksTitle('Rectangle'),
dataIndex: 'rectangle',
key: 'rectangle',
}, {
title: makeShapesTracksTitle('Polygon'),
dataIndex: 'polygon',
key: 'polygon',
}, {
title: makeShapesTracksTitle('Polyline'),
dataIndex: 'polyline',
key: 'polyline',
}, {
title: makeShapesTracksTitle('Points'),
dataIndex: 'points',
key: 'points',
}, {
title: <Text strong> Tags </Text>,
dataIndex: 'tags',
key: 'tags',
}, {
title: <Text strong> Manually </Text>,
dataIndex: 'manually',
key: 'manually',
}, {
title: <Text strong> Interpolated </Text>,
dataIndex: 'interpolated',
key: 'interpolated',
}, {
title: <Text strong> Total </Text>,
dataIndex: 'total',
key: 'total',
}];
return (
<Modal
{...baseProps}
>
<div className='cvat-job-info-modal-window'>
<Row type='flex' justify='start'>
<Col>
<Text strong className='cvat-text'>Job status</Text>
<Select value={jobStatus} onChange={changeJobStatus}>
<Select.Option key='1' value='annotation'>annotation</Select.Option>
<Select.Option key='2' value='validation'>validation</Select.Option>
<Select.Option key='3' value='completed'>completed</Select.Option>
</Select>
{savingJobStatus && <Icon type='loading' />}
</Col>
</Row>
<Row type='flex' justify='start'>
<Col>
<Text className='cvat-text'>Overview</Text>
</Col>
</Row>
<Row type='flex' justify='start'>
<Col span={5}>
<Text strong className='cvat-text'>Assignee</Text>
<Text className='cvat-text'>{assignee}</Text>
</Col>
<Col span={5}>
<Text strong className='cvat-text'>Start frame</Text>
<Text className='cvat-text'>{startFrame}</Text>
</Col>
<Col span={5}>
<Text strong className='cvat-text'>Stop frame</Text>
<Text className='cvat-text'>{stopFrame}</Text>
</Col>
<Col span={5}>
<Text strong className='cvat-text'>Frames</Text>
<Text className='cvat-text'>{stopFrame - startFrame + 1}</Text>
</Col>
<Col span={4}>
<Text strong className='cvat-text'>Z-Order</Text>
<Text className='cvat-text'>{zOrder.toString()}</Text>
</Col>
</Row>
{ !!bugTracker && (
<Row type='flex' justify='start' className='cvat-job-info-bug-tracker'>
<Col>
<Text strong className='cvat-text'>Bug tracker</Text>
<a href={bugTracker}>{bugTracker}</a>
</Col>
</Row>
)}
<Row type='flex' justify='space-around' className='cvat-job-info-statistics'>
<Col span={24}>
<Text className='cvat-text'>Annotations statistics</Text>
<Table
scroll={{ y: 400 }}
bordered
pagination={false}
columns={columns}
dataSource={rows}
/>
</Col>
</Row>
</div>
</Modal>
);
}

@ -20,6 +20,7 @@ interface Props {
frameNumber: number;
startFrame: number;
stopFrame: number;
showStatistics(): void;
onSwitchPlay(): void;
onSaveAnnotation(): void;
onPrevFrame(): void;
@ -41,7 +42,7 @@ function propsAreEqual(curProps: Props, prevProps: Props): boolean {
&& curProps.savingStatuses.length === prevProps.savingStatuses.length;
}
const AnnotationTopBarComponent = React.memo((props: Props): JSX.Element => {
function AnnotationTopBarComponent(props: Props): JSX.Element {
const {
saving,
savingStatuses,
@ -49,6 +50,7 @@ const AnnotationTopBarComponent = React.memo((props: Props): JSX.Element => {
frameNumber,
startFrame,
stopFrame,
showStatistics,
onSwitchPlay,
onSaveAnnotation,
onPrevFrame,
@ -90,10 +92,10 @@ const AnnotationTopBarComponent = React.memo((props: Props): JSX.Element => {
/>
</Row>
</Col>
<RightGroup />
<RightGroup showStatistics={showStatistics} />
</Row>
</Layout.Header>
);
}, propsAreEqual);
}
export default AnnotationTopBarComponent;
export default React.memo(AnnotationTopBarComponent, propsAreEqual);

@ -289,7 +289,7 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
</Col>
<Col span={1} offset={1}>
<Tooltip overlay='Specify a label mapping between model labels and task labels'>
<Icon className='cvat-run-model-dialog-info-icon' type='question-circle' />
<Icon className='cvat-info-circle-icon' type='question-circle' />
</Tooltip>
</Col>
</Row>

@ -4,10 +4,6 @@
margin-top: 10px;
}
.cvat-run-model-dialog-info-icon {
color: $info-icon-color;
}
.cvat-run-model-dialog-remove-mapping-icon {
color: $danger-icon-color;
}

@ -61,8 +61,8 @@
}
.cvat-player-settings-step > div > span > i {
vertical-align: -1em;
transform: scale(0.3);
margin: 0px 5px;
font-size: 10px;
}
.cvat-player-settings-speed > div > .ant-select {

@ -12,13 +12,17 @@ import {
mergeObjects,
groupObjects,
splitTrack,
editShape,
updateAnnotationsAsync,
createAnnotationsAsync,
mergeAnnotationsAsync,
groupAnnotationsAsync,
splitAnnotationsAsync,
activateObject,
selectObjects,
} from 'actions/annotation-actions';
import {
ColorBy,
GridColor,
ObjectType,
CombinedState,
@ -30,9 +34,15 @@ interface StateToProps {
sidebarCollapsed: boolean;
canvasInstance: Canvas;
jobInstance: any;
activatedStateID: number | null;
selectedStatesID: number[];
annotations: any[];
frameData: any;
frame: number;
opacity: number;
colorBy: ColorBy;
selectedOpacity: number;
blackBorders: boolean;
grid: boolean;
gridSize: number;
gridColor: GridColor;
@ -50,11 +60,14 @@ interface DispatchToProps {
onMergeObjects: (enabled: boolean) => void;
onGroupObjects: (enabled: boolean) => void;
onSplitTrack: (enabled: boolean) => void;
onEditShape: (enabled: boolean) => void;
onUpdateAnnotations(sessionInstance: any, frame: number, 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;
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void;
onActivateObject: (activatedStateID: number | null) => void;
onSelectObjects: (selectedStatesID: number[]) => void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -78,6 +91,8 @@ function mapStateToProps(state: CombinedState): StateToProps {
},
annotations: {
states: annotations,
activatedStateID,
selectedStatesID,
},
sidebarCollapsed,
},
@ -88,6 +103,12 @@ function mapStateToProps(state: CombinedState): StateToProps {
gridColor,
gridOpacity,
},
shapes: {
opacity,
colorBy,
selectedOpacity,
blackBorders,
},
},
} = state;
@ -97,7 +118,13 @@ function mapStateToProps(state: CombinedState): StateToProps {
jobInstance,
frameData,
frame,
activatedStateID,
selectedStatesID,
annotations,
opacity,
colorBy,
selectedOpacity,
blackBorders,
grid,
gridSize,
gridColor,
@ -133,6 +160,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSplitTrack(enabled: boolean): void {
dispatch(splitTrack(enabled));
},
onEditShape(enabled: boolean): void {
dispatch(editShape(enabled));
},
onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(updateAnnotationsAsync(sessionInstance, frame, states));
},
@ -148,6 +178,12 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void {
dispatch(splitAnnotationsAsync(sessionInstance, frame, state));
},
onActivateObject(activatedStateID: number | null): void {
dispatch(activateObject(activatedStateID));
},
onSelectObjects(selectedStatesID: number[]): void {
dispatch(selectObjects(selectedStatesID));
},
};
}

@ -2,7 +2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import {
changeLabelColor as changeLabelColorAction,
changeLabelColorAsync,
updateAnnotationsAsync,
} from 'actions/annotation-actions';
@ -26,7 +26,7 @@ interface StateToProps {
interface DispatchToProps {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void;
changeLabelColor(label: any, color: string): void;
changeLabelColor(sessionInstance: any, frameNumber: number, label: any, color: string): void;
}
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
@ -36,8 +36,8 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
states: objectStates,
},
job: {
instance: jobInstance,
labels,
instance: jobInstance,
},
player: {
frame: {
@ -48,7 +48,8 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
},
} = state;
const [label] = labels.filter((_label: any) => _label.id === own.labelID);
const [label] = labels
.filter((_label: any) => _label.id === own.labelID);
return {
label,
@ -66,8 +67,13 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, states));
},
changeLabelColor(label: any, color: string): void {
dispatch(changeLabelColorAction(label, color));
changeLabelColor(
sessionInstance: any,
frameNumber: number,
label: any,
color: string,
): void {
dispatch(changeLabelColorAsync(sessionInstance, frameNumber, label, color));
},
};
}
@ -139,9 +145,11 @@ class LabelItemContainer extends React.PureComponent<Props, State> {
const {
changeLabelColor,
label,
frameNumber,
jobInstance,
} = this.props;
changeLabelColor(label, color);
changeLabelColor(jobInstance, frameNumber, label, color);
};
private switchHidden(value: boolean): void {

@ -1,11 +1,18 @@
import React from 'react';
import { connect } from 'react-redux';
import {
ActiveControl,
CombinedState,
ColorBy,
} from 'reducers/interfaces';
import {
collapseObjectItems,
updateAnnotationsAsync,
changeFrameAsync,
removeObjectAsync,
copyShape as copyShapeAction,
activateObject as activateObjectAction,
propagateObject as propagateObjectAction,
} from 'actions/annotation-actions';
import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item';
@ -21,11 +28,20 @@ interface StateToProps {
attributes: any[];
jobInstance: any;
frameNumber: number;
activated: boolean;
colorBy: ColorBy;
ready: boolean;
activeControl: ActiveControl;
}
interface DispatchToProps {
changeFrame(frame: number): void;
updateState(sessionInstance: any, frameNumber: number, objectState: any): void;
collapseOrExpand(objectStates: any[], collapsed: boolean): void;
activateObject: (activatedStateID: number | null) => void;
removeObject: (objectState: any) => void;
copyShape: (objectState: any) => void;
propagateObject: (objectState: any) => void;
}
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
@ -34,17 +50,27 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
annotations: {
states,
collapsed: statesCollapsed,
activatedStateID,
},
job: {
labels,
attributes: jobAttributes,
instance: jobInstance,
labels,
},
player: {
frame: {
number: frameNumber,
},
},
canvas: {
ready,
activeControl,
},
},
settings: {
shapes: {
colorBy,
},
},
} = state;
@ -60,24 +86,135 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
collapsed: collapsedState,
attributes: jobAttributes[states[index].label.id],
labels,
ready,
activeControl,
colorBy,
jobInstance,
frameNumber,
activated: activatedStateID === own.clientID,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
changeFrame(frame: number): void {
dispatch(changeFrameAsync(frame));
},
updateState(sessionInstance: any, frameNumber: number, state: any): void {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, [state]));
},
collapseOrExpand(objectStates: any[], collapsed: boolean): void {
dispatch(collapseObjectItems(objectStates, collapsed));
},
activateObject(activatedStateID: number | null): void {
dispatch(activateObjectAction(activatedStateID));
},
removeObject(objectState: any): void {
dispatch(removeObjectAsync(objectState, true));
},
copyShape(objectState: any): void {
dispatch(copyShapeAction(objectState));
},
propagateObject(objectState: any): void {
dispatch(propagateObjectAction(objectState));
},
};
}
type Props = StateToProps & DispatchToProps;
class ObjectItemContainer extends React.PureComponent<Props> {
private navigateFirstKeyframe = (): void => {
const {
objectState,
changeFrame,
frameNumber,
} = this.props;
const { first } = objectState.keyframes;
if (first !== frameNumber) {
changeFrame(first);
}
};
private navigatePrevKeyframe = (): void => {
const {
objectState,
changeFrame,
frameNumber,
} = this.props;
const { prev } = objectState.keyframes;
if (prev !== null && prev !== frameNumber) {
changeFrame(prev);
}
};
private navigateNextKeyframe = (): void => {
const {
objectState,
changeFrame,
frameNumber,
} = this.props;
const { next } = objectState.keyframes;
if (next !== null && next !== frameNumber) {
changeFrame(next);
}
};
private navigateLastKeyframe = (): void => {
const {
objectState,
changeFrame,
frameNumber,
} = this.props;
const { last } = objectState.keyframes;
if (last !== frameNumber) {
changeFrame(last);
}
};
private copy = (): void => {
const {
objectState,
copyShape,
} = this.props;
copyShape(objectState);
};
private propagate = (): void => {
const {
objectState,
propagateObject,
} = this.props;
propagateObject(objectState);
};
private remove = (): void => {
const {
objectState,
removeObject,
} = this.props;
removeObject(objectState);
};
private activate = (): void => {
const {
activateObject,
objectState,
ready,
activeControl,
} = this.props;
if (ready && activeControl === ActiveControl.CURSOR) {
activateObject(objectState.clientID);
}
};
private lock = (): void => {
const { objectState } = this.props;
objectState.lock = true;
@ -184,10 +321,35 @@ class ObjectItemContainer extends React.PureComponent<Props> {
collapsed,
labels,
attributes,
frameNumber,
activated,
colorBy,
} = this.props;
const {
first,
prev,
next,
last,
} = objectState.keyframes || {
first: null, // shapes don't have keyframes, so we use null
prev: null,
next: null,
last: null,
};
let stateColor = '';
if (colorBy === ColorBy.INSTANCE) {
stateColor = objectState.color;
} else if (colorBy === ColorBy.GROUP) {
stateColor = objectState.group.color;
} else if (colorBy === ColorBy.LABEL) {
stateColor = objectState.label.color;
}
return (
<ObjectStateItemComponent
activated={activated}
objectType={objectState.objectType}
shapeType={objectState.shapeType}
clientID={objectState.clientID}
@ -198,10 +360,30 @@ class ObjectItemContainer extends React.PureComponent<Props> {
keyframe={objectState.keyframe}
attrValues={{ ...objectState.attributes }}
labelID={objectState.label.id}
color={objectState.color}
color={stateColor}
attributes={attributes}
labels={labels}
collapsed={collapsed}
navigateFirstKeyframe={
first >= frameNumber || first === null
? null : this.navigateFirstKeyframe
}
navigatePrevKeyframe={
prev === frameNumber || prev === null
? null : this.navigatePrevKeyframe
}
navigateNextKeyframe={
next === frameNumber || next === null
? null : this.navigateNextKeyframe
}
navigateLastKeyframe={
last <= frameNumber || last === null
? null : this.navigateLastKeyframe
}
activate={this.activate}
remove={this.remove}
copy={this.copy}
propagate={this.propagate}
setOccluded={this.setOccluded}
unsetOccluded={this.unsetOccluded}
setOutside={this.setOutside}

@ -2,23 +2,47 @@
import React from 'react';
import { connect } from 'react-redux';
import { RadioChangeEvent } from 'antd/lib/radio';
import { SliderValue } from 'antd/lib/slider';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import ObjectsSidebarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar';
import { CombinedState } from 'reducers/interfaces';
import {
CombinedState,
ColorBy,
} from 'reducers/interfaces';
import {
collapseSidebar as collapseSidebarAction,
collapseAppearance as collapseAppearanceAction,
updateTabContentHeight as updateTabContentHeightAction,
} from 'actions/annotation-actions';
import {
changeShapesColorBy as changeShapesColorByAction,
changeShapesOpacity as changeShapesOpacityAction,
changeSelectedShapesOpacity as changeSelectedShapesOpacityAction,
changeShapesBlackBorders as changeShapesBlackBordersAction,
} from 'actions/settings-actions';
interface StateToProps {
sidebarCollapsed: boolean;
appearanceCollapsed: boolean;
colorBy: ColorBy;
opacity: number;
selectedOpacity: number;
blackBorders: boolean;
}
interface DispatchToProps {
collapseSidebar(): void;
collapseAppearance(): void;
updateTabContentHeight(): void;
changeShapesColorBy(colorBy: ColorBy): void;
changeShapesOpacity(shapesOpacity: number): void;
changeSelectedShapesOpacity(selectedShapesOpacity: number): void;
changeShapesBlackBorders(blackBorders: boolean): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -27,11 +51,23 @@ function mapStateToProps(state: CombinedState): StateToProps {
sidebarCollapsed,
appearanceCollapsed,
},
settings: {
shapes: {
colorBy,
opacity,
selectedOpacity,
blackBorders,
},
},
} = state;
return {
sidebarCollapsed,
appearanceCollapsed,
colorBy,
opacity,
selectedOpacity,
blackBorders,
};
}
@ -80,19 +116,90 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
),
);
},
changeShapesColorBy(colorBy: ColorBy): void {
dispatch(changeShapesColorByAction(colorBy));
},
changeShapesOpacity(shapesOpacity: number): void {
dispatch(changeShapesOpacityAction(shapesOpacity));
},
changeSelectedShapesOpacity(selectedShapesOpacity: number): void {
dispatch(changeSelectedShapesOpacityAction(selectedShapesOpacity));
},
changeShapesBlackBorders(blackBorders: boolean): void {
dispatch(changeShapesBlackBordersAction(blackBorders));
},
};
}
type Props = StateToProps & DispatchToProps;
class ObjectsSideBarContainer extends React.PureComponent<Props> {
public componentDidMount(): void {
const { updateTabContentHeight } = this.props;
updateTabContentHeight();
window.addEventListener('resize', this.alignTabHeight);
this.alignTabHeight();
}
public componentWillUnmount(): void {
window.removeEventListener('resize', this.alignTabHeight);
}
private alignTabHeight = (): void => {
const {
sidebarCollapsed,
updateTabContentHeight,
} = this.props;
if (!sidebarCollapsed) {
updateTabContentHeight();
}
};
private changeShapesColorBy = (event: RadioChangeEvent): void => {
const { changeShapesColorBy } = this.props;
changeShapesColorBy(event.target.value);
};
private changeShapesOpacity = (value: SliderValue): void => {
const { changeShapesOpacity } = this.props;
changeShapesOpacity(value as number);
};
private changeSelectedShapesOpacity = (value: SliderValue): void => {
const { changeSelectedShapesOpacity } = this.props;
changeSelectedShapesOpacity(value as number);
};
private changeShapesBlackBorders = (event: CheckboxChangeEvent): void => {
const { changeShapesBlackBorders } = this.props;
changeShapesBlackBorders(event.target.checked);
};
public render(): JSX.Element {
const {
sidebarCollapsed,
appearanceCollapsed,
colorBy,
opacity,
selectedOpacity,
blackBorders,
collapseSidebar,
collapseAppearance,
} = this.props;
return (
<ObjectsSidebarComponent {...this.props} />
<ObjectsSidebarComponent
sidebarCollapsed={sidebarCollapsed}
appearanceCollapsed={appearanceCollapsed}
colorBy={colorBy}
opacity={opacity}
selectedOpacity={selectedOpacity}
blackBorders={blackBorders}
collapseSidebar={collapseSidebar}
collapseAppearance={collapseAppearance}
changeShapesColorBy={this.changeShapesColorBy}
changeShapesOpacity={this.changeShapesOpacity}
changeSelectedShapesOpacity={this.changeSelectedShapesOpacity}
changeShapesBlackBorders={this.changeShapesBlackBorders}
/>
);
}
}

@ -0,0 +1,134 @@
import React from 'react';
import { connect } from 'react-redux';
import {
propagateObject as propagateObjectAction,
changePropagateFrames as changePropagateFramesAction,
propagateObjectAsync,
} from 'actions/annotation-actions';
import { CombinedState } from 'reducers/interfaces';
import PropagateConfirmComponent from 'components/annotation-page/standard-workspace/propagate-confirm';
interface StateToProps {
objectState: any | null;
frameNumber: number;
stopFrame: number;
propagateFrames: number;
jobInstance: any;
}
interface DispatchToProps {
cancel(): void;
propagateObject(sessionInstance: any, objectState: any, from: number, to: number): void;
changePropagateFrames(frames: number): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
propagate: {
objectState,
frames: propagateFrames,
},
job: {
instance: {
stopFrame,
},
instance: jobInstance,
},
player: {
frame: {
number: frameNumber,
},
},
},
} = state;
return {
objectState,
frameNumber,
stopFrame,
propagateFrames,
jobInstance,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
propagateObject(sessionInstance: any, objectState: any, from: number, to: number): void {
dispatch(propagateObjectAsync(sessionInstance, objectState, from, to));
},
changePropagateFrames(frames: number): void {
dispatch(changePropagateFramesAction(frames));
},
cancel(): void {
dispatch(propagateObjectAction(null));
},
};
}
type Props = StateToProps & DispatchToProps;
class PropagateConfirmContainer extends React.PureComponent<Props> {
private propagateObject = (): void => {
const {
propagateObject,
objectState,
propagateFrames,
frameNumber,
stopFrame,
jobInstance,
} = this.props;
const propagateUpToFrame = Math.min(frameNumber + propagateFrames, stopFrame);
propagateObject(jobInstance, objectState, frameNumber + 1, propagateUpToFrame);
};
private changePropagateFrames = (value: number | undefined): void => {
const { changePropagateFrames } = this.props;
if (typeof (value) !== 'undefined') {
changePropagateFrames(value);
}
};
private changeUpToFrame = (value: number | undefined): void => {
const {
stopFrame,
frameNumber,
changePropagateFrames,
} = this.props;
if (typeof (value) !== 'undefined') {
const propagateFrames = Math.max(0, Math.min(stopFrame, value)) - frameNumber;
changePropagateFrames(propagateFrames);
}
};
public render(): JSX.Element {
const {
frameNumber,
stopFrame,
propagateFrames,
cancel,
objectState,
} = this.props;
const propagateUpToFrame = Math.min(frameNumber + propagateFrames, stopFrame);
return (
<PropagateConfirmComponent
visible={objectState !== null}
propagateUpToFrame={propagateUpToFrame}
propagateFrames={propagateUpToFrame - frameNumber}
propagateObject={this.propagateObject}
changePropagateFrames={this.changePropagateFrames}
changeUpToFrame={this.changeUpToFrame}
cancel={cancel}
/>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(PropagateConfirmContainer);

@ -0,0 +1,108 @@
import React from 'react';
import { connect } from 'react-redux';
import { CombinedState } from 'reducers/interfaces';
import {
showStatistics,
changeJobStatusAsync,
} from 'actions/annotation-actions';
import StatisticsModalComponent from 'components/annotation-page/top-bar/statistics-modal';
interface StateToProps {
visible: boolean;
collecting: boolean;
data: any;
jobInstance: any;
jobStatus: string;
savingJobStatus: boolean;
}
interface DispatchToProps {
changeJobStatus(jobInstance: any, status: string): void;
closeStatistics(): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
statistics: {
visible,
collecting,
data,
},
job: {
saving: savingJobStatus,
instance: {
status: jobStatus,
},
instance: jobInstance,
},
},
} = state;
return {
visible,
collecting,
data,
jobInstance,
jobStatus,
savingJobStatus,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
changeJobStatus(jobInstance: any, status: string): void {
dispatch(changeJobStatusAsync(jobInstance, status));
},
closeStatistics(): void {
dispatch(showStatistics(false));
},
};
}
type Props = StateToProps & DispatchToProps;
class StatisticsModalContainer extends React.PureComponent<Props> {
private changeJobStatus = (status: string): void => {
const {
jobInstance,
changeJobStatus,
} = this.props;
changeJobStatus(jobInstance, status);
};
public render(): JSX.Element {
const {
jobInstance,
visible,
collecting,
data,
closeStatistics,
jobStatus,
savingJobStatus,
} = this.props;
return (
<StatisticsModalComponent
collecting={collecting}
data={data}
visible={visible}
jobStatus={jobStatus}
bugTracker={jobInstance.task.bugTracker}
zOrder={jobInstance.task.zOrder}
startFrame={jobInstance.startFrame}
stopFrame={jobInstance.stopFrame}
assignee={jobInstance.assignee ? jobInstance.assignee.username : 'Nobody'}
savingJobStatus={savingJobStatus}
closeStatistics={closeStatistics}
changeJobStatus={this.changeJobStatus}
/>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(StatisticsModalContainer);

@ -7,6 +7,8 @@ import {
changeFrameAsync,
switchPlay,
saveAnnotationsAsync,
collectStatisticsAsync,
showStatistics as showStatisticsAction,
} from 'actions/annotation-actions';
import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar';
@ -26,6 +28,7 @@ interface DispatchToProps {
onChangeFrame(frame: number): void;
onSwitchPlay(playing: boolean): void;
onSaveAnnotation(sessionInstance: any): void;
showStatistics(sessionInstance: any): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -79,6 +82,10 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSaveAnnotation(sessionInstance: any): void {
dispatch(saveAnnotationsAsync(sessionInstance));
},
showStatistics(sessionInstance: any): void {
dispatch(collectStatisticsAsync(sessionInstance));
dispatch(showStatisticsAction(true));
},
};
}
@ -108,6 +115,15 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
}
}
private showStatistics = (): void => {
const {
jobInstance,
showStatistics,
} = this.props;
showStatistics(jobInstance);
};
private onSwitchPlay = (): void => {
const {
frameNumber,
@ -288,6 +304,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
return (
<AnnotationTopBarComponent
showStatistics={this.showStatistics}
onSwitchPlay={this.onSwitchPlay}
onSaveAnnotation={this.onSaveAnnotation}
onPrevFrame={this.onPrevFrame}

@ -17,10 +17,11 @@ const defaultState: AnnotationState = {
activeControl: ActiveControl.CURSOR,
},
job: {
instance: null,
labels: [],
instance: null,
attributes: {},
fetching: false,
saving: false,
},
player: {
frame: {
@ -36,6 +37,8 @@ const defaultState: AnnotationState = {
activeObjectType: ObjectType.SHAPE,
},
annotations: {
selectedStatesID: [],
activatedStateID: null,
saving: {
uploading: false,
statuses: [],
@ -43,6 +46,15 @@ const defaultState: AnnotationState = {
collapsed: {},
states: [],
},
propagate: {
objectState: null,
frames: 50,
},
statistics: {
visible: false,
collecting: false,
data: null,
},
colors: [],
sidebarCollapsed: false,
appearanceCollapsed: false,
@ -135,6 +147,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
states,
} = action.payload;
const activatedStateID = states
.map((_state: any) => _state.clientID).includes(state.annotations.activatedStateID)
? state.annotations.activatedStateID : null;
return {
...state,
player: {
@ -147,6 +163,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
annotations: {
...state.annotations,
activatedStateID,
states,
},
};
@ -279,6 +296,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
return {
...state,
annotations: {
...state.annotations,
activatedStateID: null,
},
canvas: {
...state.canvas,
activeControl,
@ -292,6 +313,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
return {
...state,
annotations: {
...state.annotations,
activatedStateID: null,
},
canvas: {
...state.canvas,
activeControl,
@ -309,6 +334,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
return {
...state,
annotations: {
...state.annotations,
activatedStateID: null,
},
canvas: {
...state.canvas,
activeControl,
@ -328,6 +357,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
return {
...state,
annotations: {
...state.annotations,
activatedStateID: null,
},
canvas: {
...state.canvas,
activeControl,
@ -341,6 +374,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
return {
...state,
annotations: {
...state.annotations,
activatedStateID: null,
},
canvas: {
...state.canvas,
activeControl,
@ -354,6 +391,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
return {
...state,
annotations: {
...state.annotations,
activatedStateID: null,
},
canvas: {
...state.canvas,
activeControl,
@ -447,6 +488,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
case AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS: {
const {
label,
states,
} = action.payload;
const { instance: job } = state.job;
@ -454,13 +496,202 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
const index = labels.indexOf(label);
labels[index] = label;
return {
...state,
job: {
...state.job,
labels,
},
annotations: {
...state.annotations,
states,
},
};
}
case AnnotationActionTypes.ACTIVATE_OBJECT: {
const {
activatedStateID,
} = action.payload;
return {
...state,
annotations: {
...state.annotations,
activatedStateID,
},
};
}
case AnnotationActionTypes.SELECT_OBJECTS: {
const {
selectedStatesID,
} = action.payload;
return {
...state,
annotations: {
...state.annotations,
selectedStatesID,
},
};
}
case AnnotationActionTypes.REMOVE_OBJECT_SUCCESS: {
const {
objectState,
} = action.payload;
return {
...state,
annotations: {
...state.annotations,
activatedStateID: null,
states: state.annotations.states
.filter((_objectState: any) => (
_objectState.clientID !== objectState.clientID
)),
},
};
}
case AnnotationActionTypes.COPY_SHAPE: {
const {
objectState,
} = action.payload;
state.canvas.instance.cancel();
state.canvas.instance.draw({
enabled: true,
initialState: objectState,
});
let activeControl = ActiveControl.DRAW_RECTANGLE;
if (objectState.shapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
} else if (objectState.shapeType === ShapeType.POLYGON) {
activeControl = ActiveControl.DRAW_POLYGON;
} else if (objectState.shapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE;
}
return {
...state,
canvas: {
...state.canvas,
activeControl,
},
annotations: {
...state.annotations,
activatedStateID: null,
},
};
}
case AnnotationActionTypes.EDIT_SHAPE: {
const { enabled } = action.payload;
const activeControl = enabled
? ActiveControl.EDIT : ActiveControl.CURSOR;
return {
...state,
canvas: {
...state.canvas,
activeControl,
},
};
}
case AnnotationActionTypes.PROPAGATE_OBJECT: {
const { objectState } = action.payload;
return {
...state,
propagate: {
...state.propagate,
objectState,
},
};
}
case AnnotationActionTypes.PROPAGATE_OBJECT_SUCCESS: {
return {
...state,
propagate: {
...state.propagate,
objectState: null,
},
};
}
case AnnotationActionTypes.CHANGE_PROPAGATE_FRAMES: {
const { frames } = action.payload;
return {
...state,
propagate: {
...state.propagate,
frames,
},
};
}
case AnnotationActionTypes.SWITCH_SHOWING_STATISTICS: {
const { visible } = action.payload;
return {
...state,
statistics: {
...state.statistics,
visible,
},
};
}
case AnnotationActionTypes.COLLECT_STATISTICS: {
return {
...state,
statistics: {
...state.statistics,
collecting: true,
},
};
}
case AnnotationActionTypes.COLLECT_STATISTICS_SUCCESS: {
const { data } = action.payload;
return {
...state,
statistics: {
...state.statistics,
collecting: false,
data,
},
};
}
case AnnotationActionTypes.COLLECT_STATISTICS_FAILED: {
return {
...state,
statistics: {
...state.statistics,
collecting: false,
data: null,
},
};
}
case AnnotationActionTypes.CHANGE_JOB_STATUS: {
return {
...state,
job: {
...state.job,
saving: true,
},
};
}
case AnnotationActionTypes.CHANGE_JOB_STATUS_SUCCESS: {
return {
...state,
job: {
...state.job,
saving: false,
},
};
}
case AnnotationActionTypes.CHANGE_JOB_STATUS_FAILED: {
return {
...state,
job: {
...state.job,
saving: false,
},
};
}
case AnnotationActionTypes.RESET_CANVAS: {

@ -211,6 +211,10 @@ export interface NotificationsState {
merging: null | ErrorState;
grouping: null | ErrorState;
splitting: null | ErrorState;
removing: null | ErrorState;
propagating: null | ErrorState;
collectingStatistics: null | ErrorState;
savingJob: null | ErrorState;
};
[index: string]: any;
@ -238,6 +242,7 @@ export enum ActiveControl {
MERGE = 'merge',
GROUP = 'group',
SPLIT = 'split',
EDIT = 'edit',
}
export enum ShapeType {
@ -266,10 +271,11 @@ export interface AnnotationState {
activeControl: ActiveControl;
};
job: {
instance: any | null | undefined;
labels: any[];
instance: any | null | undefined;
attributes: Record<number, any[]>;
fetching: boolean;
saving: boolean;
};
player: {
frame: {
@ -286,6 +292,8 @@ export interface AnnotationState {
activeObjectType: ObjectType;
};
annotations: {
selectedStatesID: number[];
activatedStateID: number | null;
collapsed: Record<number, boolean>;
states: any[];
saving: {
@ -293,6 +301,15 @@ export interface AnnotationState {
statuses: string[];
};
};
propagate: {
objectState: any | null;
frames: number;
};
statistics: {
collecting: boolean;
visible: boolean;
data: any;
};
colors: any[];
sidebarCollapsed: boolean;
appearanceCollapsed: boolean;
@ -316,6 +333,12 @@ export enum FrameSpeed {
Slowest = 1,
}
export enum ColorBy {
INSTANCE = 'Instance',
GROUP = 'Group',
LABEL = 'Label',
}
export interface PlayerSettingsState {
frameStep: number;
frameSpeed: FrameSpeed;
@ -337,7 +360,15 @@ export interface WorkspaceSettingsState {
showAllInterpolationTracks: boolean;
}
export interface ShapesSettingsState {
colorBy: ColorBy;
opacity: number;
selectedOpacity: number;
blackBorders: boolean;
}
export interface SettingsState {
shapes: ShapesSettingsState;
workspace: WorkspaceSettingsState;
player: PlayerSettingsState;
}

@ -59,6 +59,10 @@ const defaultState: NotificationsState = {
merging: null,
grouping: null,
splitting: null,
removing: null,
propagating: null,
collectingStatistics: null,
savingJob: null,
},
},
messages: {
@ -564,7 +568,67 @@ export default function (state = defaultState, action: AnyAction): Notifications
annotation: {
...state.errors.annotation,
splitting: {
message: 'Could not split a track',
message: 'Could not split the track',
reason: action.payload.error.toString(),
},
},
},
};
}
case AnnotationActionTypes.REMOVE_OBJECT_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
removing: {
message: 'Could not remove the object',
reason: action.payload.error.toString(),
},
},
},
};
}
case AnnotationActionTypes.PROPAGATE_OBJECT_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
propagating: {
message: 'Could not propagate the object',
reason: action.payload.error.toString(),
},
},
},
};
}
case AnnotationActionTypes.COLLECT_STATISTICS_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
collectingStatistics: {
message: 'Could not collect annotations statistics',
reason: action.payload.error.toString(),
},
},
},
};
}
case AnnotationActionTypes.CHANGE_JOB_STATUS_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
savingJob: {
message: 'Could not save the job on the server',
reason: action.payload.error.toString(),
},
},

@ -5,9 +5,16 @@ import {
SettingsState,
GridColor,
FrameSpeed,
ColorBy,
} from './interfaces';
const defaultState: SettingsState = {
shapes: {
colorBy: ColorBy.INSTANCE,
opacity: 3,
selectedOpacity: 30,
blackBorders: false,
},
workspace: {
autoSave: false,
autoSaveInterval: 15 * 60 * 1000,
@ -76,6 +83,42 @@ export default (state = defaultState, action: AnyAction): SettingsState => {
},
};
}
case SettingsActionTypes.CHANGE_SHAPES_COLOR_BY: {
return {
...state,
shapes: {
...state.shapes,
colorBy: action.payload.colorBy,
},
};
}
case SettingsActionTypes.CHANGE_SHAPES_OPACITY: {
return {
...state,
shapes: {
...state.shapes,
opacity: action.payload.opacity,
},
};
}
case SettingsActionTypes.CHANGE_SELECTED_SHAPES_OPACITY: {
return {
...state,
shapes: {
...state.shapes,
selectedOpacity: action.payload.selectedOpacity,
},
};
}
case SettingsActionTypes.CHANGE_SHAPES_BLACK_BORDERS: {
return {
...state,
shapes: {
...state.shapes,
blackBorders: action.payload.blackBorders,
},
};
}
default: {
return state;
}

@ -25,6 +25,20 @@ hr {
padding-top: 5px;
}
.ant-slider {
> .ant-slider-track {
background-color: $slider-color;
}
> .ant-slider-handle {
border-color: $slider-color;
}
}
.cvat-info-circle-icon {
color: $info-icon-color;
}
#root {
width: 100%;
height: 100%;

Loading…
Cancel
Save