Added paint brush tools (#4543)

main
Boris Sekachev 4 years ago committed by GitHub
parent 02eba10b45
commit f62aa276cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Objects sorting option in the sidebar, by z order. Additional visualization when the sorting is applied - Objects sorting option in the sidebar, by z order. Additional visualization when the sorting is applied
(<https://github.com/opencv/cvat/pull/5145>) (<https://github.com/opencv/cvat/pull/5145>)
- Added YOLOv5 serverless function NVIDIA GPU support (<https://github.com/opencv/cvat/pull/4960>) - Added YOLOv5 serverless function NVIDIA GPU support (<https://github.com/opencv/cvat/pull/4960>)
- Mask tools are supported now (brush, eraser, polygon-plus, polygon-minus, returning masks
from online detectors & interactors) (<https://github.com/opencv/cvat/pull/4543>)
- Added Webhooks (<https://github.com/opencv/cvat/pull/4863>) - Added Webhooks (<https://github.com/opencv/cvat/pull/4863>)
### Changed ### Changed

@ -1,6 +1,6 @@
{ {
"name": "cvat-canvas", "name": "cvat-canvas",
"version": "2.15.4", "version": "2.16.0",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library", "description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts", "main": "src/canvas.ts",
"scripts": { "scripts": {
@ -17,6 +17,8 @@
], ],
"dependencies": { "dependencies": {
"@types/polylabel": "^1.0.5", "@types/polylabel": "^1.0.5",
"@types/fabric": "^4.5.7",
"fabric": "^5.2.1",
"polylabel": "^1.1.0", "polylabel": "^1.1.0",
"svg.draggable.js": "2.2.2", "svg.draggable.js": "2.2.2",
"svg.draw.js": "^2.0.4", "svg.draw.js": "^2.0.4",

@ -1,5 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corp // Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -72,6 +72,10 @@ polyline.cvat_shape_drawing_opacity {
fill: darkmagenta; fill: darkmagenta;
} }
image.cvat_canvas_shape_grouping {
visibility: hidden;
}
.cvat_canvas_shape_region_selection { .cvat_canvas_shape_region_selection {
@extend .cvat_shape_action_dasharray; @extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity; @extend .cvat_shape_action_opacity;
@ -340,6 +344,11 @@ g.cvat_canvas_shape_occluded {
height: 100%; height: 100%;
} }
.cvat_masks_canvas_wrapper {
z-index: 3;
display: none;
}
#cvat_canvas_attachment_board { #cvat_canvas_attachment_board {
position: absolute; position: absolute;
z-index: 4; z-index: 4;

@ -1,4 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -8,6 +9,7 @@ import {
MergeData, MergeData,
SplitData, SplitData,
GroupData, GroupData,
MasksEditData,
InteractionData as _InteractionData, InteractionData as _InteractionData,
InteractionResult as _InteractionResult, InteractionResult as _InteractionResult,
CanvasModel, CanvasModel,
@ -38,6 +40,7 @@ interface Canvas {
interact(interactionData: InteractionData): void; interact(interactionData: InteractionData): void;
draw(drawData: DrawData): void; draw(drawData: DrawData): void;
edit(editData: MasksEditData): void;
group(groupData: GroupData): void; group(groupData: GroupData): void;
split(splitData: SplitData): void; split(splitData: SplitData): void;
merge(mergeData: MergeData): void; merge(mergeData: MergeData): void;
@ -129,6 +132,10 @@ class CanvasImpl implements Canvas {
this.model.draw(drawData); this.model.draw(drawData);
} }
public edit(editData: MasksEditData): void {
this.model.edit(editData);
}
public split(splitData: SplitData): void { public split(splitData: SplitData): void {
this.model.split(splitData); this.model.split(splitData);
} }

@ -1,4 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -15,6 +16,7 @@ import {
Mode, Mode,
InteractionData, InteractionData,
Configuration, Configuration,
MasksEditData,
} from './canvasModel'; } from './canvasModel';
export interface CanvasController { export interface CanvasController {
@ -24,6 +26,7 @@ export interface CanvasController {
readonly focusData: FocusData; readonly focusData: FocusData;
readonly activeElement: ActiveElement; readonly activeElement: ActiveElement;
readonly drawData: DrawData; readonly drawData: DrawData;
readonly editData: MasksEditData;
readonly interactionData: InteractionData; readonly interactionData: InteractionData;
readonly mergeData: MergeData; readonly mergeData: MergeData;
readonly splitData: SplitData; readonly splitData: SplitData;
@ -35,6 +38,7 @@ export interface CanvasController {
zoom(x: number, y: number, direction: number): void; zoom(x: number, y: number, direction: number): void;
draw(drawData: DrawData): void; draw(drawData: DrawData): void;
edit(editData: MasksEditData): void;
interact(interactionData: InteractionData): void; interact(interactionData: InteractionData): void;
merge(mergeData: MergeData): void; merge(mergeData: MergeData): void;
split(splitData: SplitData): void; split(splitData: SplitData): void;
@ -91,6 +95,10 @@ export class CanvasControllerImpl implements CanvasController {
this.model.draw(drawData); this.model.draw(drawData);
} }
public edit(editData: MasksEditData): void {
this.model.edit(editData);
}
public interact(interactionData: InteractionData): void { public interact(interactionData: InteractionData): void {
this.model.interact(interactionData); this.model.interact(interactionData);
} }
@ -143,6 +151,10 @@ export class CanvasControllerImpl implements CanvasController {
return this.model.drawData; return this.model.drawData;
} }
public get editData(): MasksEditData {
return this.model.editData;
}
public get interactionData(): InteractionData { public get interactionData(): InteractionData {
return this.model.interactionData; return this.model.interactionData;
} }

@ -1,4 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -52,6 +53,12 @@ export enum CuboidDrawingMethod {
CORNER_POINTS = 'By 4 points', CORNER_POINTS = 'By 4 points',
} }
export enum ColorBy {
INSTANCE = 'Instance',
GROUP = 'Group',
LABEL = 'Label',
}
export interface Configuration { export interface Configuration {
smoothImage?: boolean; smoothImage?: boolean;
autoborders?: boolean; autoborders?: boolean;
@ -65,15 +72,23 @@ export interface Configuration {
intelligentPolygonCrop?: boolean; intelligentPolygonCrop?: boolean;
forceFrameUpdate?: boolean; forceFrameUpdate?: boolean;
CSSImageFilter?: string; CSSImageFilter?: string;
colorBy?: string; colorBy?: ColorBy;
selectedShapeOpacity?: number; selectedShapeOpacity?: number;
shapeOpacity?: number; shapeOpacity?: number;
controlPointsSize?: number; controlPointsSize?: number;
outlinedBorders?: string | false; outlinedBorders?: string | false;
} }
export interface BrushTool {
type: 'brush' | 'eraser' | 'polygon-plus' | 'polygon-minus';
color: string;
form: 'circle' | 'square';
size: number;
}
export interface DrawData { export interface DrawData {
enabled: boolean; enabled: boolean;
continue?: boolean;
shapeType?: string; shapeType?: string;
rectDrawingMethod?: RectDrawingMethod; rectDrawingMethod?: RectDrawingMethod;
cuboidDrawingMethod?: CuboidDrawingMethod; cuboidDrawingMethod?: CuboidDrawingMethod;
@ -81,7 +96,10 @@ export interface DrawData {
numberOfPoints?: number; numberOfPoints?: number;
initialState?: any; initialState?: any;
crosshair?: boolean; crosshair?: boolean;
brushTool?: BrushTool;
redraw?: number; redraw?: number;
onDrawDone?: (data: object) => void;
onUpdateConfiguration?: (configuration: { brushTool?: Pick<BrushTool, 'size'> }) => void;
} }
export interface InteractionData { export interface InteractionData {
@ -107,12 +125,19 @@ export interface InteractionResult {
button: number; button: number;
} }
export interface EditData { export interface PolyEditData {
enabled: boolean; enabled: boolean;
state: any; state: any;
pointID: number; pointID: number;
} }
export interface MasksEditData {
enabled: boolean;
state?: any;
brushTool?: BrushTool;
onUpdateConfiguration?: (configuration: { brushTool?: Pick<BrushTool, 'size'> }) => void;
}
export interface GroupData { export interface GroupData {
enabled: boolean; enabled: boolean;
} }
@ -146,6 +171,7 @@ export enum UpdateReasons {
INTERACT = 'interact', INTERACT = 'interact',
DRAW = 'draw', DRAW = 'draw',
EDIT = 'edit',
MERGE = 'merge', MERGE = 'merge',
SPLIT = 'split', SPLIT = 'split',
GROUP = 'group', GROUP = 'group',
@ -186,6 +212,7 @@ export interface CanvasModel {
readonly focusData: FocusData; readonly focusData: FocusData;
readonly activeElement: ActiveElement; readonly activeElement: ActiveElement;
readonly drawData: DrawData; readonly drawData: DrawData;
readonly editData: MasksEditData;
readonly interactionData: InteractionData; readonly interactionData: InteractionData;
readonly mergeData: MergeData; readonly mergeData: MergeData;
readonly splitData: SplitData; readonly splitData: SplitData;
@ -208,6 +235,7 @@ export interface CanvasModel {
grid(stepX: number, stepY: number): void; grid(stepX: number, stepY: number): void;
draw(drawData: DrawData): void; draw(drawData: DrawData): void;
edit(editData: MasksEditData): void;
group(groupData: GroupData): void; group(groupData: GroupData): void;
split(splitData: SplitData): void; split(splitData: SplitData): void;
merge(mergeData: MergeData): void; merge(mergeData: MergeData): void;
@ -226,6 +254,50 @@ export interface CanvasModel {
destroy(): void; destroy(): void;
} }
const defaultData = {
drawData: {
enabled: false,
},
editData: {
enabled: false,
},
interactionData: {
enabled: false,
},
mergeData: {
enabled: false,
},
groupData: {
enabled: false,
},
splitData: {
enabled: false,
},
};
function hasShapeIsBeingDrawn(): boolean {
const [element] = window.document.getElementsByClassName('cvat_canvas_shape_drawing');
if (element) {
return !!(element as any).instance.remember('_paintHandler');
}
return false;
}
function disableInternalSVGDrawing(data: DrawData | MasksEditData, currentData: DrawData | MasksEditData): boolean {
// P.S. spaghetti code, but probably significant refactoring needed to find a better solution
// when it is a mask drawing/editing using polygon fill
// a user needs to close drawing/editing twice
// first close stops internal drawing/editing with svg.js
// the second one stops drawing/editing mask itself
return !data.enabled && currentData.enabled &&
(('shapeType' in currentData && currentData.shapeType === 'mask') ||
('state' in currentData && currentData.state.shapeType === 'mask')) &&
currentData.brushTool?.type?.startsWith('polygon-') &&
hasShapeIsBeingDrawn();
}
export class CanvasModelImpl extends MasterImpl implements CanvasModel { export class CanvasModelImpl extends MasterImpl implements CanvasModel {
private data: { private data: {
activeElement: ActiveElement; activeElement: ActiveElement;
@ -247,6 +319,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
top: number; top: number;
zLayer: number | null; zLayer: number | null;
drawData: DrawData; drawData: DrawData;
editData: MasksEditData;
interactionData: InteractionData; interactionData: InteractionData;
mergeData: MergeData; mergeData: MergeData;
groupData: GroupData; groupData: GroupData;
@ -278,7 +351,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
intelligentPolygonCrop: false, intelligentPolygonCrop: false,
forceFrameUpdate: false, forceFrameUpdate: false,
CSSImageFilter: '', CSSImageFilter: '',
colorBy: 'Label', colorBy: ColorBy.LABEL,
selectedShapeOpacity: 0.5, selectedShapeOpacity: 0.5,
shapeOpacity: 0.2, shapeOpacity: 0.2,
outlinedBorders: false, outlinedBorders: false,
@ -311,25 +384,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
scale: 1, scale: 1,
top: 0, top: 0,
zLayer: null, zLayer: null,
drawData: {
enabled: false,
initialState: null,
},
interactionData: {
enabled: false,
},
mergeData: {
enabled: false,
},
groupData: {
enabled: false,
},
splitData: {
enabled: false,
},
selected: null, selected: null,
mode: Mode.IDLE, mode: Mode.IDLE,
exception: null, exception: null,
...defaultData,
}; };
} }
@ -561,9 +619,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
throw new Error('Skeleton template must be specified when drawing a skeleton'); throw new Error('Skeleton template must be specified when drawing a skeleton');
} }
if (this.data.drawData.enabled) { if (!drawData.shapeType && !drawData.initialState) {
throw new Error('Drawing has been already started');
} else if (!drawData.shapeType && !drawData.initialState) {
throw new Error('A shape type is not specified'); throw new Error('A shape type is not specified');
} else if (typeof drawData.numberOfPoints !== 'undefined') { } else if (typeof drawData.numberOfPoints !== 'undefined') {
if (drawData.shapeType === 'polygon' && drawData.numberOfPoints < 3) { if (drawData.shapeType === 'polygon' && drawData.numberOfPoints < 3) {
@ -585,6 +641,11 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return; return;
} }
} else { } else {
if (disableInternalSVGDrawing(drawData, this.data.drawData)) {
this.notify(UpdateReasons.DRAW);
return;
}
this.data.drawData = { ...drawData }; this.data.drawData = { ...drawData };
if (this.data.drawData.initialState) { if (this.data.drawData.initialState) {
this.data.drawData.shapeType = this.data.drawData.initialState.shapeType; this.data.drawData.shapeType = this.data.drawData.initialState.shapeType;
@ -604,6 +665,33 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.notify(UpdateReasons.DRAW); this.notify(UpdateReasons.DRAW);
} }
public edit(editData: MasksEditData): void {
if (![Mode.IDLE, Mode.EDIT].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (editData.enabled && !editData.state) {
throw Error('State must be specified when call edit() editing process');
}
if (this.data.editData.enabled && editData.enabled &&
editData.state.clientID !== this.data.editData.state.clientID
) {
throw Error('State cannot be updated during editing, need to finish current editing first');
}
if (editData.enabled) {
this.data.editData = { ...editData };
} else if (disableInternalSVGDrawing(editData, this.data.editData)) {
this.notify(UpdateReasons.EDIT);
return;
} else {
this.data.editData = { enabled: false };
}
this.notify(UpdateReasons.EDIT);
}
public interact(interactionData: InteractionData): void { public interact(interactionData: InteractionData): void {
if (![Mode.IDLE, Mode.INTERACT].includes(this.data.mode)) { if (![Mode.IDLE, Mode.INTERACT].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`); throw Error(`Canvas is busy. Action: ${this.data.mode}`);
@ -735,7 +823,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
if (['string', 'boolean'].includes(typeof configuration.outlinedBorders)) { if (['string', 'boolean'].includes(typeof configuration.outlinedBorders)) {
this.data.configuration.outlinedBorders = configuration.outlinedBorders; this.data.configuration.outlinedBorders = configuration.outlinedBorders;
} }
if (['Instance', 'Group', 'Label'].includes(configuration.colorBy)) { if (Object.values(ColorBy).includes(configuration.colorBy)) {
this.data.configuration.colorBy = configuration.colorBy; this.data.configuration.colorBy = configuration.colorBy;
} }
@ -754,6 +842,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
} }
public cancel(): void { public cancel(): void {
this.data = {
...this.data,
...defaultData,
};
this.notify(UpdateReasons.CANCEL); this.notify(UpdateReasons.CANCEL);
} }
@ -837,6 +929,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return { ...this.data.drawData }; return { ...this.data.drawData };
} }
public get editData(): MasksEditData {
return { ...this.data.editData };
}
public get interactionData(): InteractionData { public get interactionData(): InteractionData {
return { ...this.data.interactionData }; return { ...this.data.interactionData };
} }

@ -1,9 +1,10 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corp // Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import polylabel from 'polylabel'; import polylabel from 'polylabel';
import { fabric } from 'fabric';
import * as SVG from 'svg.js'; import * as SVG from 'svg.js';
import 'svg.draggable.js'; import 'svg.draggable.js';
@ -13,6 +14,7 @@ import 'svg.select.js';
import { CanvasController } from './canvasController'; import { CanvasController } from './canvasController';
import { Listener, Master } from './master'; import { Listener, Master } from './master';
import { DrawHandler, DrawHandlerImpl } from './drawHandler'; import { DrawHandler, DrawHandlerImpl } from './drawHandler';
import { MasksHandler, MasksHandlerImpl } from './masksHandler';
import { EditHandler, EditHandlerImpl } from './editHandler'; import { EditHandler, EditHandlerImpl } from './editHandler';
import { MergeHandler, MergeHandlerImpl } from './mergeHandler'; import { MergeHandler, MergeHandlerImpl } from './mergeHandler';
import { SplitHandler, SplitHandlerImpl } from './splitHandler'; import { SplitHandler, SplitHandlerImpl } from './splitHandler';
@ -38,6 +40,8 @@ import {
readPointsFromShape, readPointsFromShape,
setupSkeletonEdges, setupSkeletonEdges,
makeSVGFromTemplate, makeSVGFromTemplate,
imageDataToDataURL,
expandChannels,
} from './shared'; } from './shared';
import { import {
CanvasModel, CanvasModel,
@ -54,6 +58,7 @@ import {
Configuration, Configuration,
InteractionResult, InteractionResult,
InteractionData, InteractionData,
ColorBy,
} from './canvasModel'; } from './canvasModel';
export interface CanvasView { export interface CanvasView {
@ -65,6 +70,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private text: SVGSVGElement; private text: SVGSVGElement;
private adoptedText: SVG.Container; private adoptedText: SVG.Container;
private background: HTMLCanvasElement; private background: HTMLCanvasElement;
private masksContent: HTMLCanvasElement;
private bitmap: HTMLCanvasElement; private bitmap: HTMLCanvasElement;
private grid: SVGSVGElement; private grid: SVGSVGElement;
private content: SVGSVGElement; private content: SVGSVGElement;
@ -82,6 +88,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private drawnIssueRegions: Record<number, SVG.Shape>; private drawnIssueRegions: Record<number, SVG.Shape>;
private geometry: Geometry; private geometry: Geometry;
private drawHandler: DrawHandler; private drawHandler: DrawHandler;
private masksHandler: MasksHandler;
private editHandler: EditHandler; private editHandler: EditHandler;
private mergeHandler: MergeHandler; private mergeHandler: MergeHandler;
private splitHandler: SplitHandler; private splitHandler: SplitHandler;
@ -95,6 +102,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private snapToAngleResize: number; private snapToAngleResize: number;
private innerObjectsFlags: { private innerObjectsFlags: {
drawHidden: Record<number, boolean>; drawHidden: Record<number, boolean>;
editHidden: Record<number, boolean>;
}; };
private set mode(value: Mode) { private set mode(value: Mode) {
@ -180,7 +188,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
return this.innerObjectsFlags.drawHidden[clientID] || false; return this.innerObjectsFlags.drawHidden[clientID] || false;
} }
private setupInnerFlags(clientID: number, path: 'drawHidden', value: boolean): void { private setupInnerFlags(clientID: number, path: 'drawHidden' | 'editHidden', value: boolean): void {
this.innerObjectsFlags[path][clientID] = value; this.innerObjectsFlags[path][clientID] = value;
const shape = this.svgShapes[clientID]; const shape = this.svgShapes[clientID];
const text = this.svgTexts[clientID]; const text = this.svgTexts[clientID];
@ -253,7 +261,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
} }
private onDrawDone(data: any | null, duration: number, continueDraw?: boolean): void { private onDrawDone(data: any | null, duration: number, continueDraw?: boolean, prevDrawData?: DrawData): void {
const hiddenBecauseOfDraw = Object.keys(this.innerObjectsFlags.drawHidden) const hiddenBecauseOfDraw = Object.keys(this.innerObjectsFlags.drawHidden)
.map((_clientID): number => +_clientID); .map((_clientID): number => +_clientID);
if (hiddenBecauseOfDraw.length) { if (hiddenBecauseOfDraw.length) {
@ -266,16 +274,16 @@ export class CanvasViewImpl implements CanvasView, Listener {
const { clientID, elements } = data as any; const { clientID, elements } = data as any;
const points = data.points || elements.map((el: any) => el.points).flat(); const points = data.points || elements.map((el: any) => el.points).flat();
if (typeof clientID === 'number') { if (typeof clientID === 'number') {
const [state] = this.controller.objects
.filter((_state: any): boolean => _state.clientID === clientID);
this.onEditDone(state, points);
const event: CustomEvent = new CustomEvent('canvas.canceled', { const event: CustomEvent = new CustomEvent('canvas.canceled', {
bubbles: false, bubbles: false,
cancelable: true, cancelable: true,
}); });
this.canvas.dispatchEvent(event); this.canvas.dispatchEvent(event);
const [state] = this.controller.objects.filter((_state: any): boolean => _state.clientID === clientID);
this.onEditDone(state, points);
return; return;
} }
@ -296,23 +304,50 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.dispatchEvent(event); this.canvas.dispatchEvent(event);
} else if (!continueDraw) { } else if (!continueDraw) {
const event: CustomEvent = new CustomEvent('canvas.canceled', { this.canvas.dispatchEvent(new CustomEvent('canvas.canceled', {
bubbles: false, bubbles: false,
cancelable: true, cancelable: true,
}); }));
this.canvas.dispatchEvent(event);
} }
if (!continueDraw) { if (continueDraw) {
this.mode = Mode.IDLE; this.canvas.dispatchEvent(
this.controller.draw({ new CustomEvent('canvas.drawstart', {
enabled: false, bubbles: false,
}); cancelable: true,
detail: {
drawData: prevDrawData,
},
}),
);
} else {
// when draw stops from inside canvas (for example if use predefined number of points)
this.controller.draw({ enabled: false });
} }
} }
private onEditDone(state: any, points: number[], rotation?: number): void { private onEditStart = (state?: any): void => {
this.canvas.style.cursor = 'crosshair';
this.deactivate();
this.canvas.dispatchEvent(
new CustomEvent('canvas.editstart', {
bubbles: false,
cancelable: true,
detail: {
state,
},
}),
);
if (state && state.shapeType === 'mask') {
this.setupInnerFlags(state.clientID, 'editHidden', true);
}
this.mode = Mode.EDIT;
};
private onEditDone = (state: any, points: number[], rotation?: number): void => {
this.canvas.style.cursor = '';
if (state && points) { if (state && points) {
const event: CustomEvent = new CustomEvent('canvas.edited', { const event: CustomEvent = new CustomEvent('canvas.edited', {
bubbles: false, bubbles: false,
@ -334,8 +369,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.dispatchEvent(event); this.canvas.dispatchEvent(event);
} }
this.mode = Mode.IDLE; for (const clientID of Object.keys(this.innerObjectsFlags.editHidden)) {
this.setupInnerFlags(+clientID, 'editHidden', false);
} }
this.mode = Mode.IDLE;
};
private onMergeDone(objects: any[] | null, duration?: number): void { private onMergeDone(objects: any[] | null, duration?: number): void {
if (objects) { if (objects) {
@ -358,10 +396,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.dispatchEvent(event); this.canvas.dispatchEvent(event);
} }
this.controller.merge({ this.controller.merge({ enabled: false });
enabled: false,
});
this.mode = Mode.IDLE; this.mode = Mode.IDLE;
} }
@ -386,10 +421,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.dispatchEvent(event); this.canvas.dispatchEvent(event);
} }
this.controller.split({ this.controller.split({ enabled: false });
enabled: false,
});
this.mode = Mode.IDLE; this.mode = Mode.IDLE;
} }
@ -413,10 +445,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.dispatchEvent(event); this.canvas.dispatchEvent(event);
} }
this.controller.group({ this.controller.group({ enabled: false });
enabled: false,
});
this.mode = Mode.IDLE; this.mode = Mode.IDLE;
} }
@ -459,7 +488,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
}); });
this.canvas.dispatchEvent(event); this.canvas.dispatchEvent(event);
e.preventDefault(); e.preventDefault();
} }
} }
@ -523,6 +551,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
// Transform handlers // Transform handlers
this.drawHandler.transform(this.geometry); this.drawHandler.transform(this.geometry);
this.masksHandler.transform(this.geometry);
this.editHandler.transform(this.geometry); this.editHandler.transform(this.geometry);
this.zoomHandler.transform(this.geometry); this.zoomHandler.transform(this.geometry);
this.autoborderHandler.transform(this.geometry); this.autoborderHandler.transform(this.geometry);
@ -533,7 +562,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
private transformCanvas(): void { private transformCanvas(): void {
// Transform canvas // Transform canvas
for (const obj of [ for (const obj of [
this.background, this.grid, this.content, this.bitmap, this.attachmentBoard, this.background,
this.grid,
this.content,
this.bitmap,
this.attachmentBoard,
]) { ]) {
obj.style.transform = `scale(${this.geometry.scale}) rotate(${this.geometry.angle}deg)`; obj.style.transform = `scale(${this.geometry.scale}) rotate(${this.geometry.angle}deg)`;
} }
@ -545,6 +578,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
for (const element of [ for (const element of [
...window.document.getElementsByClassName('svg_select_points'), ...window.document.getElementsByClassName('svg_select_points'),
...window.document.getElementsByClassName('svg_select_points_rot'), ...window.document.getElementsByClassName('svg_select_points_rot'),
...window.document.getElementsByClassName('svg_select_boundingRect'),
]) { ]) {
element.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.geometry.scale}`); element.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.geometry.scale}`);
element.setAttribute('r', `${this.configuration.controlPointsSize / this.geometry.scale}`); element.setAttribute('r', `${this.configuration.controlPointsSize / this.geometry.scale}`);
@ -606,6 +640,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
// Transform handlers // Transform handlers
this.drawHandler.transform(this.geometry); this.drawHandler.transform(this.geometry);
this.masksHandler.transform(this.geometry);
this.editHandler.transform(this.geometry); this.editHandler.transform(this.geometry);
this.zoomHandler.transform(this.geometry); this.zoomHandler.transform(this.geometry);
this.autoborderHandler.transform(this.geometry); this.autoborderHandler.transform(this.geometry);
@ -614,7 +649,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
private resizeCanvas(): void { private resizeCanvas(): void {
for (const obj of [this.background, this.grid, this.bitmap]) { for (const obj of [this.background, this.masksContent, this.grid, this.bitmap]) {
obj.style.width = `${this.geometry.image.width}px`; obj.style.width = `${this.geometry.image.width}px`;
obj.style.height = `${this.geometry.image.height}px`; obj.style.height = `${this.geometry.image.height}px`;
} }
@ -685,6 +720,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private setupObjects(states: any[]): void { private setupObjects(states: any[]): void {
const created = []; const created = [];
const updated = []; const updated = [];
for (const state of states) { for (const state of states) {
if (!(state.clientID in this.drawnStates)) { if (!(state.clientID in this.drawnStates)) {
created.push(state); created.push(state);
@ -831,15 +867,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
const { points } = state; const { points } = state;
this.onEditDone(state, points.slice(0, pointID * 2).concat(points.slice(pointID * 2 + 2))); this.onEditDone(state, points.slice(0, pointID * 2).concat(points.slice(pointID * 2 + 2)));
} else if (e.shiftKey) { } else if (e.shiftKey) {
this.canvas.dispatchEvent( this.onEditStart(state);
new CustomEvent('canvas.editstart', {
bubbles: false,
cancelable: true,
}),
);
this.mode = Mode.EDIT;
this.deactivate();
this.editHandler.edit({ this.editHandler.edit({
enabled: true, enabled: true,
state, state,
@ -901,6 +929,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
deepSelect: true, deepSelect: true,
pointSize: (2 * this.configuration.controlPointsSize) / this.geometry.scale, pointSize: (2 * this.configuration.controlPointsSize) / this.geometry.scale,
rotationPoint: shape.type === 'rect' || shape.type === 'ellipse', rotationPoint: shape.type === 'rect' || shape.type === 'ellipse',
pointsExclude: shape.type === 'image' ? ['lt', 'rt', 'rb', 'lb', 't', 'r', 'b', 'l'] : [],
pointType(cx: number, cy: number): SVG.Circle { pointType(cx: number, cy: number): SVG.Circle {
const circle: SVG.Circle = this.nested const circle: SVG.Circle = this.nested
.circle(this.options.pointSize) .circle(this.options.pointSize)
@ -974,6 +1003,19 @@ export class CanvasViewImpl implements CanvasView, Listener {
title.textContent = 'Hold Shift to snap angle'; title.textContent = 'Hold Shift to snap angle';
rotationPoint.appendChild(title); rotationPoint.appendChild(title);
} }
if (value && shape.type === 'image') {
const [boundingRect] = window.document.getElementsByClassName('svg_select_boundingRect');
if (boundingRect) {
(boundingRect as SVGRectElement).style.opacity = '1';
boundingRect.setAttribute('fill', 'none');
boundingRect.setAttribute('stroke', shape.attr('stroke'));
boundingRect.setAttribute('stroke-width', `${consts.BASE_STROKE_WIDTH / this.geometry.scale}px`);
if (shape.hasClass('cvat_canvas_shape_occluded')) {
boundingRect.setAttribute('stroke-dasharray', '5');
}
}
}
} }
private onShiftKeyDown = (e: KeyboardEvent): void => { private onShiftKeyDown = (e: KeyboardEvent): void => {
@ -981,7 +1023,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.snapToAngleResize = consts.SNAP_TO_ANGLE_RESIZE_SHIFT; this.snapToAngleResize = consts.SNAP_TO_ANGLE_RESIZE_SHIFT;
if (this.activeElement) { if (this.activeElement) {
const shape = this.svgShapes[this.activeElement.clientID]; const shape = this.svgShapes[this.activeElement.clientID];
if (shape && shape.hasClass('cvat_canvas_shape_activated')) { if (shape && shape?.remember('_selectHandler')?.options?.rotationPoint) {
if (this.drawnStates[this.activeElement.clientID]?.shapeType === 'skeleton') { if (this.drawnStates[this.activeElement.clientID]?.shapeType === 'skeleton') {
const wrappingRect = (shape as any).children().find((child: SVG.Element) => child.type === 'rect'); const wrappingRect = (shape as any).children().find((child: SVG.Element) => child.type === 'rect');
if (wrappingRect) { if (wrappingRect) {
@ -1000,7 +1042,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.snapToAngleResize = consts.SNAP_TO_ANGLE_RESIZE_DEFAULT; this.snapToAngleResize = consts.SNAP_TO_ANGLE_RESIZE_DEFAULT;
if (this.activeElement) { if (this.activeElement) {
const shape = this.svgShapes[this.activeElement.clientID]; const shape = this.svgShapes[this.activeElement.clientID];
if (shape && shape.hasClass('cvat_canvas_shape_activated')) { if (shape && shape?.remember('_selectHandler')?.options?.rotationPoint) {
if (this.drawnStates[this.activeElement.clientID]?.shapeType === 'skeleton') { if (this.drawnStates[this.activeElement.clientID]?.shapeType === 'skeleton') {
const wrappingRect = (shape as any).children().find((child: SVG.Element) => child.type === 'rect'); const wrappingRect = (shape as any).children().find((child: SVG.Element) => child.type === 'rect');
if (wrappingRect) { if (wrappingRect) {
@ -1036,6 +1078,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.snapToAngleResize = consts.SNAP_TO_ANGLE_RESIZE_DEFAULT; this.snapToAngleResize = consts.SNAP_TO_ANGLE_RESIZE_DEFAULT;
this.innerObjectsFlags = { this.innerObjectsFlags = {
drawHidden: {}, drawHidden: {},
editHidden: {},
}; };
// Create HTML elements // Create HTML elements
@ -1043,6 +1086,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.text = window.document.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.adoptedText = SVG.adopt((this.text as any) as HTMLElement) as SVG.Container;
this.background = window.document.createElement('canvas'); this.background = window.document.createElement('canvas');
this.masksContent = window.document.createElement('canvas');
this.bitmap = window.document.createElement('canvas'); this.bitmap = window.document.createElement('canvas');
// window.document.createElementNS('http://www.w3.org/2000/svg', 'svg'); // window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
@ -1110,6 +1154,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
// Setup content // Setup content
this.text.setAttribute('id', 'cvat_canvas_text_content'); this.text.setAttribute('id', 'cvat_canvas_text_content');
this.background.setAttribute('id', 'cvat_canvas_background'); this.background.setAttribute('id', 'cvat_canvas_background');
this.masksContent.setAttribute('id', 'cvat_canvas_masks_content');
this.content.setAttribute('id', 'cvat_canvas_content'); this.content.setAttribute('id', 'cvat_canvas_content');
this.bitmap.setAttribute('id', 'cvat_canvas_bitmap'); this.bitmap.setAttribute('id', 'cvat_canvas_bitmap');
this.bitmap.style.display = 'none'; this.bitmap.style.display = 'none';
@ -1131,6 +1176,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.appendChild(this.loadingAnimation); this.canvas.appendChild(this.loadingAnimation);
this.canvas.appendChild(this.text); this.canvas.appendChild(this.text);
this.canvas.appendChild(this.background); this.canvas.appendChild(this.background);
this.canvas.appendChild(this.masksContent);
this.canvas.appendChild(this.bitmap); this.canvas.appendChild(this.bitmap);
this.canvas.appendChild(this.grid); this.canvas.appendChild(this.grid);
this.canvas.appendChild(this.content); this.canvas.appendChild(this.content);
@ -1146,7 +1192,15 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.geometry, this.geometry,
this.configuration, this.configuration,
); );
this.editHandler = new EditHandlerImpl(this.onEditDone.bind(this), this.adoptedContent, this.autoborderHandler); this.masksHandler = new MasksHandlerImpl(
this.onDrawDone.bind(this),
this.controller.draw.bind(this.controller),
this.onEditStart,
this.onEditDone,
this.drawHandler,
this.masksContent,
);
this.editHandler = new EditHandlerImpl(this.onEditDone, this.adoptedContent, this.autoborderHandler);
this.mergeHandler = new MergeHandlerImpl( this.mergeHandler = new MergeHandlerImpl(
this.onMergeDone.bind(this), this.onMergeDone.bind(this),
this.onFindObject.bind(this), this.onFindObject.bind(this),
@ -1177,12 +1231,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
); );
// Setup event handlers // Setup event handlers
this.content.addEventListener('dblclick', (e: MouseEvent): void => { this.canvas.addEventListener('dblclick', (e: MouseEvent): void => {
this.controller.fit(); this.controller.fit();
e.preventDefault(); e.preventDefault();
}); });
this.content.addEventListener('mousedown', (event): void => { this.canvas.addEventListener('mousedown', (event): void => {
if ([0, 1].includes(event.button)) { if ([0, 1].includes(event.button)) {
if ( if (
[Mode.IDLE, Mode.DRAG_CANVAS, Mode.MERGE, Mode.SPLIT] [Mode.IDLE, Mode.DRAG_CANVAS, Mode.MERGE, Mode.SPLIT]
@ -1197,7 +1251,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
window.document.addEventListener('keydown', this.onShiftKeyDown); window.document.addEventListener('keydown', this.onShiftKeyDown);
window.document.addEventListener('keyup', this.onShiftKeyUp); window.document.addEventListener('keyup', this.onShiftKeyUp);
this.content.addEventListener('wheel', (event): void => { this.canvas.addEventListener('wheel', (event): void => {
if (event.ctrlKey) return; if (event.ctrlKey) return;
const { offset } = this.controller.geometry; const { offset } = this.controller.geometry;
const point = translateToSVG(this.content, [event.clientX, event.clientY]); const point = translateToSVG(this.content, [event.clientX, event.clientY]);
@ -1211,7 +1265,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
event.preventDefault(); event.preventDefault();
}); });
this.content.addEventListener('mousemove', (e): void => { this.canvas.addEventListener('mousemove', (e): void => {
this.controller.drag(e.clientX, e.clientY); this.controller.drag(e.clientX, e.clientY);
if (this.mode !== Mode.IDLE) return; if (this.mode !== Mode.IDLE) return;
@ -1244,32 +1298,41 @@ export class CanvasViewImpl implements CanvasView, Listener {
const { configuration } = model; const { configuration } = model;
const updateShapeViews = (states: DrawnState[], parentState?: DrawnState): void => { const updateShapeViews = (states: DrawnState[], parentState?: DrawnState): void => {
for (const state of states) { for (const drawnState of states) {
const { fill, stroke, 'fill-opacity': fillOpacity } = this.getShapeColorization(state, { configuration, parentState }); const {
const shapeView = window.document.getElementById(`cvat_canvas_shape_${state.clientID}`); fill, stroke, 'fill-opacity': fillOpacity,
} = this.getShapeColorization(drawnState, { parentState });
const shapeView = window.document.getElementById(`cvat_canvas_shape_${drawnState.clientID}`);
const [objectState] = this.controller.objects
.filter((_state: any) => _state.clientID === drawnState.clientID);
if (shapeView) { if (shapeView) {
const handler = (shapeView as any).instance.remember('_selectHandler'); const handler = (shapeView as any).instance.remember('_selectHandler');
if (handler && handler.nested) { if (handler && handler.nested) {
handler.nested.fill({ color: fill }); handler.nested.fill({ color: fill });
} }
if (drawnState.shapeType === 'mask') {
// if there are masks, we need to redraw them
this.deleteObjects([drawnState]);
this.addObjects([objectState]);
continue;
}
(shapeView as any).instance (shapeView as any).instance
.fill({ color: fill, opacity: fillOpacity }) .fill({ color: fill, opacity: fillOpacity })
.stroke({ color: stroke }); .stroke({ color: stroke });
} }
if (state.elements) { if (drawnState.elements) {
updateShapeViews(state.elements, state); updateShapeViews(drawnState.elements, drawnState);
} }
} }
}; };
if (configuration.shapeOpacity !== this.configuration.shapeOpacity || const withUpdatingShapeViews = configuration.shapeOpacity !== this.configuration.shapeOpacity ||
configuration.selectedShapeOpacity !== this.configuration.selectedShapeOpacity || configuration.selectedShapeOpacity !== this.configuration.selectedShapeOpacity ||
configuration.outlinedBorders !== this.configuration.outlinedBorders || configuration.outlinedBorders !== this.configuration.outlinedBorders ||
configuration.colorBy !== this.configuration.colorBy) { configuration.colorBy !== this.configuration.colorBy;
updateShapeViews(Object.values(this.drawnStates));
}
if (configuration.displayAllText && !this.configuration.displayAllText) { if (configuration.displayAllText && !this.configuration.displayAllText) {
for (const i in this.drawnStates) { for (const i in this.drawnStates) {
@ -1298,6 +1361,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
this.configuration = configuration; this.configuration = configuration;
if (withUpdatingShapeViews) {
updateShapeViews(Object.values(this.drawnStates));
}
if (recreateText) { if (recreateText) {
const states = this.controller.objects; const states = this.controller.objects;
for (const key of Object.keys(this.drawnStates)) { for (const key of Object.keys(this.drawnStates)) {
@ -1327,6 +1394,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.activate(activeElement); this.activate(activeElement);
this.editHandler.configurate(this.configuration); this.editHandler.configurate(this.configuration);
this.drawHandler.configurate(this.configuration); this.drawHandler.configurate(this.configuration);
this.masksHandler.configurate(this.configuration);
this.autoborderHandler.configurate(this.configuration); this.autoborderHandler.configurate(this.configuration);
this.interactionHandler.configurate(this.configuration); this.interactionHandler.configurate(this.configuration);
this.transformCanvas(); this.transformCanvas();
@ -1495,19 +1563,46 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
} else if (reason === UpdateReasons.DRAW) { } else if (reason === UpdateReasons.DRAW) {
const data: DrawData = this.controller.drawData; const data: DrawData = this.controller.drawData;
if (data.enabled && this.mode === Mode.IDLE) { if (data.enabled && [Mode.IDLE, Mode.DRAW].includes(this.mode)) {
if (data.shapeType !== 'mask') {
this.drawHandler.draw(data, this.geometry);
} else {
this.masksHandler.draw(data);
}
if (this.mode === Mode.IDLE) {
this.canvas.style.cursor = 'crosshair'; this.canvas.style.cursor = 'crosshair';
this.mode = Mode.DRAW; this.mode = Mode.DRAW;
this.canvas.dispatchEvent(
new CustomEvent('canvas.drawstart', {
bubbles: false,
cancelable: true,
detail: {
drawData: data,
},
}),
);
if (typeof data.redraw === 'number') { if (typeof data.redraw === 'number') {
this.setupInnerFlags(data.redraw, 'drawHidden', true); this.setupInnerFlags(data.redraw, 'drawHidden', true);
} }
this.drawHandler.draw(data, this.geometry); }
} else { } else if (this.mode !== Mode.IDLE) {
this.canvas.style.cursor = ''; this.canvas.style.cursor = '';
if (this.mode !== Mode.IDLE) { this.mode = Mode.IDLE;
if (this.masksHandler.enabled) {
this.masksHandler.draw(data);
} else {
this.drawHandler.draw(data, this.geometry); this.drawHandler.draw(data, this.geometry);
} }
} }
} else if (reason === UpdateReasons.EDIT) {
const data = this.controller.editData;
if (data.enabled && data.state.shapeType === 'mask') {
this.masksHandler.edit(data);
} else if (this.masksHandler.enabled) {
this.masksHandler.edit(data);
}
} else if (reason === UpdateReasons.INTERACT) { } else if (reason === UpdateReasons.INTERACT) {
const data: InteractionData = this.controller.interactionData; const data: InteractionData = this.controller.interactionData;
if (data.enabled && (this.mode === Mode.IDLE || data.intermediateShape)) { if (data.enabled && (this.mode === Mode.IDLE || data.intermediateShape)) {
@ -1561,7 +1656,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
} else if (reason === UpdateReasons.CANCEL) { } else if (reason === UpdateReasons.CANCEL) {
if (this.mode === Mode.DRAW) { if (this.mode === Mode.DRAW) {
if (this.masksHandler.enabled) {
this.masksHandler.cancel();
} else {
this.drawHandler.cancel(); this.drawHandler.cancel();
}
} else if (this.mode === Mode.INTERACT) { } else if (this.mode === Mode.INTERACT) {
this.interactionHandler.cancel(); this.interactionHandler.cancel();
} else if (this.mode === Mode.MERGE) { } else if (this.mode === Mode.MERGE) {
@ -1573,7 +1672,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
} else if (this.mode === Mode.SELECT_REGION) { } else if (this.mode === Mode.SELECT_REGION) {
this.regionSelector.cancel(); this.regionSelector.cancel();
} else if (this.mode === Mode.EDIT) { } else if (this.mode === Mode.EDIT) {
if (this.masksHandler.enabled) {
this.masksHandler.cancel();
} else {
this.editHandler.cancel(); this.editHandler.cancel();
}
} else if (this.mode === Mode.DRAG_CANVAS) { } else if (this.mode === Mode.DRAG_CANVAS) {
this.canvas.dispatchEvent( this.canvas.dispatchEvent(
new CustomEvent('canvas.dragstop', { new CustomEvent('canvas.dragstop', {
@ -1684,6 +1787,22 @@ export class CanvasViewImpl implements CanvasView, Listener {
ctx.fill(); ctx.fill();
} }
if (state.shapeType === 'mask') {
const { points } = state;
const [left, top, right, bottom] = points.slice(-4);
const imageBitmap = expandChannels(255, 255, 255, points, 4);
imageDataToDataURL(imageBitmap, right - left + 1, bottom - top + 1,
(dataURL: string) => new Promise((resolve) => {
const img = document.createElement('img');
img.addEventListener('load', () => {
ctx.drawImage(img, left, top);
URL.revokeObjectURL(dataURL);
resolve();
});
img.src = dataURL;
}));
}
if (state.shapeType === 'cuboid') { if (state.shapeType === 'cuboid') {
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
const points = [ const points = [
@ -1737,20 +1856,19 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
private getShapeColorization(state: any, opts: { private getShapeColorization(state: any, opts: {
configuration?: Configuration,
parentState?: any, parentState?: any,
} = {}): { fill: string; stroke: string, 'fill-opacity': number } { } = {}): { fill: string; stroke: string, 'fill-opacity': number } {
const { shapeType } = state; const { shapeType } = state;
const parentShapeType = opts.parentState?.shapeType; const parentShapeType = opts.parentState?.shapeType;
const configuration = opts.configuration || this.configuration; const { configuration } = this;
const { colorBy, shapeOpacity, outlinedBorders } = configuration; const { colorBy, shapeOpacity, outlinedBorders } = configuration;
let shapeColor = ''; let shapeColor = '';
if (colorBy === 'Instance') { if (colorBy === ColorBy.INSTANCE) {
shapeColor = state.color; shapeColor = state.color;
} else if (colorBy === 'Group') { } else if (colorBy === ColorBy.GROUP) {
shapeColor = state.group.color; shapeColor = state.group.color;
} else if (colorBy === 'Label') { } else if (colorBy === ColorBy.LABEL) {
shapeColor = state.label.color; shapeColor = state.label.color;
} }
const outlinedColor = parentShapeType === 'skeleton' ? 'black' : outlinedBorders || shapeColor; const outlinedColor = parentShapeType === 'skeleton' ? 'black' : outlinedBorders || shapeColor;
@ -1820,6 +1938,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
state.points.length !== drawnState.points.length || state.points.length !== drawnState.points.length ||
state.points.some((p: number, id: number): boolean => p !== drawnState.points[id]) state.points.some((p: number, id: number): boolean => p !== drawnState.points[id])
) { ) {
if (state.shapeType === 'mask') {
// if masks points were updated, draw from scratch
this.deleteObjects([this.drawnStates[+clientID]]);
this.addObjects([state]);
continue;
}
const translatedPoints: number[] = this.translateToCanvas(state.points); const translatedPoints: number[] = this.translateToCanvas(state.points);
if (state.shapeType === 'rectangle') { if (state.shapeType === 'rectangle') {
@ -1884,19 +2009,23 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
} }
if (drawnState.label.id !== state.label.id || drawnState.color !== state.color) { if (
drawnState.label.id !== state.label.id ||
drawnState.group.id !== state.group.id ||
drawnState.group.color !== state.group.color ||
drawnState.color !== state.color
) {
// update shape color if necessary // update shape color if necessary
if (shape) { if (shape) {
shape.attr({ if (state.shapeType === 'mask') {
...this.getShapeColorization(state), // if masks points were updated, draw from scratch
}); this.deleteObjects([this.drawnStates[+clientID]]);
this.addObjects([state]);
continue;
} else {
shape.attr({ ...this.getShapeColorization(state) });
} }
} }
if (
drawnState.group.id !== state.group.id || drawnState.group.color !== state.group.color
) {
shape.attr({ ...this.getShapeColorization(state) });
} }
this.drawnStates[state.clientID] = this.saveState(state); this.drawnStates[state.clientID] = this.saveState(state);
@ -1931,13 +2060,16 @@ export class CanvasViewImpl implements CanvasView, Listener {
const { displayAllText } = this.configuration; const { displayAllText } = this.configuration;
for (const state of states) { for (const state of states) {
const points: number[] = state.points as number[]; const points: number[] = state.points as number[];
const translatedPoints: number[] = this.translateToCanvas(points);
// TODO: Use enums after typification cvat-core // TODO: Use enums after typification cvat-core
if (state.shapeType === 'rectangle') { if (state.shapeType === 'mask') {
this.svgShapes[state.clientID] = this.addRect(translatedPoints, state); this.svgShapes[state.clientID] = this.addMask(points, state);
} else if (state.shapeType === 'skeleton') { } else if (state.shapeType === 'skeleton') {
this.svgShapes[state.clientID] = this.addSkeleton(state); this.svgShapes[state.clientID] = this.addSkeleton(state);
} else {
const translatedPoints: number[] = this.translateToCanvas(points);
if (state.shapeType === 'rectangle') {
this.svgShapes[state.clientID] = this.addRect(translatedPoints, state);
} else { } else {
const stringified = this.stringifyToCanvas(translatedPoints); const stringified = this.stringifyToCanvas(translatedPoints);
@ -1955,6 +2087,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
continue; continue;
} }
} }
}
this.svgShapes[state.clientID].on('click.canvas', (): void => { this.svgShapes[state.clientID].on('click.canvas', (): void => {
this.canvas.dispatchEvent( this.canvas.dispatchEvent(
@ -2027,8 +2160,19 @@ export class CanvasViewImpl implements CanvasView, Listener {
const drawnState = this.drawnStates[clientID]; const drawnState = this.drawnStates[clientID];
const shape = this.svgShapes[clientID]; const shape = this.svgShapes[clientID];
if (drawnState.shapeType === 'points') {
this.svgShapes[clientID]
.remember('_selectHandler').nested
.removeClass('cvat_canvas_shape_activated');
} else {
shape.removeClass('cvat_canvas_shape_activated'); shape.removeClass('cvat_canvas_shape_activated');
}
shape.removeClass('cvat_canvas_shape_draggable'); shape.removeClass('cvat_canvas_shape_draggable');
if (drawnState.shapeType === 'mask') {
shape.attr('opacity', `${this.configuration.shapeOpacity}`);
} else {
shape.attr('fill-opacity', `${this.configuration.shapeOpacity}`);
}
if (!drawnState.pinned) { if (!drawnState.pinned) {
(shape as any).off('dragstart'); (shape as any).off('dragstart');
@ -2044,6 +2188,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
(shape as any).attr('projections', false); (shape as any).attr('projections', false);
} }
if (drawnState.shapeType === 'mask') {
(shape as any).off('mousedown');
}
(shape as any).off('resizestart'); (shape as any).off('resizestart');
(shape as any).off('resizing'); (shape as any).off('resizing');
(shape as any).off('resizedone'); (shape as any).off('resizedone');
@ -2109,7 +2257,20 @@ export class CanvasViewImpl implements CanvasView, Listener {
return; return;
} }
if (state.shapeType === 'points') {
this.svgShapes[clientID]
.remember('_selectHandler').nested
.addClass('cvat_canvas_shape_activated');
} else {
shape.addClass('cvat_canvas_shape_activated'); shape.addClass('cvat_canvas_shape_activated');
}
if (state.shapeType === 'mask') {
shape.attr('opacity', `${this.configuration.selectedShapeOpacity}`);
} else {
shape.attr('fill-opacity', `${this.configuration.selectedShapeOpacity}`);
}
if (state.shapeType === 'points') { if (state.shapeType === 'points') {
this.content.append(this.svgShapes[clientID].remember('_selectHandler').nested.node); this.content.append(this.svgShapes[clientID].remember('_selectHandler').nested.node);
} else { } else {
@ -2137,7 +2298,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (!state.pinned) { if (!state.pinned) {
shape.addClass('cvat_canvas_shape_draggable'); shape.addClass('cvat_canvas_shape_draggable');
(shape as any) (shape as any)
.draggable() .draggable({
...(state.shapeType === 'mask' ? { snapToGrid: 1 } : {}),
})
.on('dragstart', (): void => { .on('dragstart', (): void => {
this.mode = Mode.DRAG; this.mode = Mode.DRAG;
hideText(); hideText();
@ -2153,10 +2316,29 @@ export class CanvasViewImpl implements CanvasView, Listener {
showText(); showText();
const p1 = e.detail.handler.startPoints.point; const p1 = e.detail.handler.startPoints.point;
const p2 = e.detail.p; const p2 = e.detail.p;
const delta = 1;
const dx2 = (p1.x - p2.x) ** 2; const dx2 = (p1.x - p2.x) ** 2;
const dy2 = (p1.y - p2.y) ** 2; const dy2 = (p1.y - p2.y) ** 2;
if (Math.sqrt(dx2 + dy2) >= delta) { if (Math.sqrt(dx2 + dy2) > 0) {
if (shape.type === 'image') {
const { points } = state;
const x = Math.trunc(shape.x()) - this.geometry.offset;
const y = Math.trunc(shape.y()) - this.geometry.offset;
points.splice(-4);
points.push(x, y, x + shape.width() - 1, y + shape.height() - 1);
this.drawnStates[state.clientID].points = points;
this.onEditDone(state, points);
this.canvas.dispatchEvent(
new CustomEvent('canvas.dragshape', {
bubbles: false,
cancelable: true,
detail: {
id: state.clientID,
},
}),
);
return;
}
// these points does not take into account possible transformations, applied on the element // these points does not take into account possible transformations, applied on the element
// so, if any (like rotation) we need to map them to canvas coordinate space // so, if any (like rotation) we need to map them to canvas coordinate space
let points = readPointsFromShape(shape); let points = readPointsFromShape(shape);
@ -2213,6 +2395,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.mode = Mode.IDLE; this.mode = Mode.IDLE;
}; };
if (state.shapeType !== 'mask') {
(shape as any) (shape as any)
.resize({ .resize({
snapToGrid: 0.1, snapToGrid: 0.1,
@ -2273,6 +2456,14 @@ export class CanvasViewImpl implements CanvasView, Listener {
); );
} }
}); });
} else {
(shape as any).on('dblclick', (e: MouseEvent) => {
if (e.shiftKey) {
this.controller.edit({ enabled: true, state });
e.stopPropagation();
}
});
}
this.canvas.dispatchEvent( this.canvas.dispatchEvent(
new CustomEvent('canvas.activated', { new CustomEvent('canvas.activated', {
@ -2605,6 +2796,41 @@ export class CanvasViewImpl implements CanvasView, Listener {
return cube; return cube;
} }
private addMask(points: number[], state: any): SVG.Image {
const colorization = this.getShapeColorization(state);
const color = fabric.Color.fromHex(colorization.fill).getSource();
const [left, top, right, bottom] = points.slice(-4);
const imageBitmap = expandChannels(color[0], color[1], color[2], points, 4);
const image = this.adoptedContent.image().attr({
clientID: state.clientID,
'color-rendering': 'optimizeQuality',
id: `cvat_canvas_shape_${state.clientID}`,
'shape-rendering': 'geometricprecision',
'data-z-order': state.zOrder,
opacity: colorization['fill-opacity'],
stroke: colorization.stroke,
}).addClass('cvat_canvas_shape');
image.move(this.geometry.offset + left, this.geometry.offset + top);
imageDataToDataURL(
imageBitmap,
right - left + 1,
bottom - top + 1,
(dataURL: string) => new Promise((resolve, reject) => {
image.loaded(() => {
resolve();
});
image.error(() => {
reject();
});
image.load(dataURL);
}),
);
return image;
}
private addSkeleton(state: any): any { private addSkeleton(state: any): any {
const skeleton = (this.adoptedContent as any) const skeleton = (this.adoptedContent as any)
.group() .group()
@ -2689,7 +2915,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
}; };
const mousemove = (e: MouseEvent) => { const mousemove = (e: MouseEvent): void => {
if (this.mode === Mode.IDLE) { if (this.mode === Mode.IDLE) {
// stop propagation to canvas where it calls another canvas.moved // stop propagation to canvas where it calls another canvas.moved
// and does not allow to activate an element // and does not allow to activate an element

@ -77,7 +77,12 @@ function checkConstraint(shapeType: string, points: number[], box: Box | null =
export class DrawHandlerImpl implements DrawHandler { export class DrawHandlerImpl implements DrawHandler {
// callback is used to notify about creating new shape // callback is used to notify about creating new shape
private onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean) => void; private onDrawDoneDefault: (
data: object | null,
duration?: number,
continueDraw?: boolean,
prevDrawData?: DrawData,
) => void;
private startTimestamp: number; private startTimestamp: number;
private canvas: SVG.Container; private canvas: SVG.Container;
private text: SVG.Container; private text: SVG.Container;
@ -344,6 +349,15 @@ export class DrawHandlerImpl implements DrawHandler {
this.crosshair.hide(); this.crosshair.hide();
} }
private onDrawDone(...args: any[]): void {
if (this.drawData.onDrawDone) {
this.drawData.onDrawDone.call(this, ...args);
return;
}
this.onDrawDoneDefault.call(this, ...args);
}
private release(): void { private release(): void {
if (!this.initialized) { if (!this.initialized) {
// prevents recursive calls // prevents recursive calls
@ -838,10 +852,6 @@ export class DrawHandlerImpl implements DrawHandler {
this.getFinalCuboidCoordinates(targetPoints) : this.getFinalCuboidCoordinates(targetPoints) :
this.getFinalPolyshapeCoordinates(targetPoints, true); this.getFinalPolyshapeCoordinates(targetPoints, true);
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
if (checkConstraint(shapeType, points, box)) { if (checkConstraint(shapeType, points, box)) {
this.onDrawDone( this.onDrawDone(
{ {
@ -855,8 +865,13 @@ export class DrawHandlerImpl implements DrawHandler {
}, },
Date.now() - this.startTimestamp, Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey, e.detail.originalEvent.ctrlKey,
this.drawData,
); );
} }
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
}); });
} }
@ -893,10 +908,6 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawInstance.on('done', (e: CustomEvent): void => { this.drawInstance.on('done', (e: CustomEvent): void => {
const points = readPointsFromShape((e.target as any as { instance: SVG.Rect }).instance); const points = readPointsFromShape((e.target as any as { instance: SVG.Rect }).instance);
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(points, !this.drawData.initialState.rotation); const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(points, !this.drawData.initialState.rotation);
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
if (checkConstraint('rectangle', [xtl, ytl, xbr, ybr])) { if (checkConstraint('rectangle', [xtl, ytl, xbr, ybr])) {
this.onDrawDone( this.onDrawDone(
{ {
@ -911,8 +922,13 @@ export class DrawHandlerImpl implements DrawHandler {
}, },
Date.now() - this.startTimestamp, Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey, e.detail.originalEvent.ctrlKey,
this.drawData,
); );
} }
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
}); });
} }
@ -932,11 +948,6 @@ export class DrawHandlerImpl implements DrawHandler {
const points = this.getFinalEllipseCoordinates( const points = this.getFinalEllipseCoordinates(
readPointsFromShape((e.target as any as { instance: SVG.Ellipse }).instance), false, readPointsFromShape((e.target as any as { instance: SVG.Ellipse }).instance), false,
); );
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
if (checkConstraint('ellipse', points)) { if (checkConstraint('ellipse', points)) {
this.onDrawDone( this.onDrawDone(
{ {
@ -951,8 +962,13 @@ export class DrawHandlerImpl implements DrawHandler {
}, },
Date.now() - this.startTimestamp, Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey, e.detail.originalEvent.ctrlKey,
this.drawData,
); );
} }
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
}); });
} }
@ -1044,15 +1060,16 @@ export class DrawHandlerImpl implements DrawHandler {
rotation: this.drawData.initialState.rotation, rotation: this.drawData.initialState.rotation,
}; };
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
this.onDrawDone( this.onDrawDone(
result, result,
Date.now() - this.startTimestamp, Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey, e.detail.originalEvent.ctrlKey,
this.drawData,
); );
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
}); });
this.canvas.on('mousemove.draw', (): void => { this.canvas.on('mousemove.draw', (): void => {
@ -1213,7 +1230,7 @@ export class DrawHandlerImpl implements DrawHandler {
} }
public constructor( public constructor(
onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean) => void, onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean, prevDrawData?: DrawData) => void,
canvas: SVG.Container, canvas: SVG.Container,
text: SVG.Container, text: SVG.Container,
autoborderHandler: AutoborderHandler, autoborderHandler: AutoborderHandler,
@ -1226,7 +1243,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.outlinedBorders = configuration.outlinedBorders || 'black'; this.outlinedBorders = configuration.outlinedBorders || 'black';
this.autobordersEnabled = false; this.autobordersEnabled = false;
this.startTimestamp = Date.now(); this.startTimestamp = Date.now();
this.onDrawDone = onDrawDone; this.onDrawDoneDefault = onDrawDone;
this.canvas = canvas; this.canvas = canvas;
this.text = text; this.text = text;
this.initialized = false; this.initialized = false;

@ -1,4 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -7,11 +8,11 @@ import 'svg.select.js';
import consts from './consts'; import consts from './consts';
import { translateFromSVG, pointsToNumberArray } from './shared'; import { translateFromSVG, pointsToNumberArray } from './shared';
import { EditData, Geometry, Configuration } from './canvasModel'; import { PolyEditData, Geometry, Configuration } from './canvasModel';
import { AutoborderHandler } from './autoborderHandler'; import { AutoborderHandler } from './autoborderHandler';
export interface EditHandler { export interface EditHandler {
edit(editData: EditData): void; edit(editData: PolyEditData): void;
transform(geometry: Geometry): void; transform(geometry: Geometry): void;
configurate(configuration: Configuration): void; configurate(configuration: Configuration): void;
cancel(): void; cancel(): void;
@ -22,7 +23,7 @@ export class EditHandlerImpl implements EditHandler {
private autoborderHandler: AutoborderHandler; private autoborderHandler: AutoborderHandler;
private geometry: Geometry | null; private geometry: Geometry | null;
private canvas: SVG.Container; private canvas: SVG.Container;
private editData: EditData | null; private editData: PolyEditData | null;
private editedShape: SVG.Shape | null; private editedShape: SVG.Shape | null;
private editLine: SVG.PolyLine | null; private editLine: SVG.PolyLine | null;
private clones: SVG.Polygon[]; private clones: SVG.Polygon[];

@ -1,11 +1,11 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import * as SVG from 'svg.js'; import * as SVG from 'svg.js';
import { GroupData } from './canvasModel'; import { GroupData } from './canvasModel';
import { expandChannels, imageDataToDataURL, translateToSVG } from './shared';
import { translateToSVG } from './shared';
export interface GroupHandler { export interface GroupHandler {
group(groupData: GroupData): void; group(groupData: GroupData): void;
@ -31,6 +31,7 @@ export class GroupHandlerImpl implements GroupHandler {
private initialized: boolean; private initialized: boolean;
private statesToBeGroupped: any[]; private statesToBeGroupped: any[];
private highlightedShapes: Record<number, SVG.Shape>; private highlightedShapes: Record<number, SVG.Shape>;
private groupingCopies: Record<number, SVG.Image>;
private getSelectionBox( private getSelectionBox(
event: MouseEvent, event: MouseEvent,
@ -106,11 +107,7 @@ export class GroupHandlerImpl implements GroupHandler {
(state: any): boolean => state.clientID === clientID, (state: any): boolean => state.clientID === clientID,
)[0]; )[0];
if (objectState) { this.appendToSelection(objectState);
this.statesToBeGroupped.push(objectState);
this.highlightedShapes[clientID] = shape;
(shape as any).addClass('cvat_canvas_shape_grouping');
}
} }
} }
} }
@ -164,6 +161,7 @@ export class GroupHandlerImpl implements GroupHandler {
this.canvas = canvas; this.canvas = canvas;
this.statesToBeGroupped = []; this.statesToBeGroupped = [];
this.highlightedShapes = {}; this.highlightedShapes = {};
this.groupingCopies = {};
this.selectionRect = null; this.selectionRect = null;
this.initialized = false; this.initialized = false;
this.startSelectionPoint = { this.startSelectionPoint = {
@ -185,23 +183,67 @@ export class GroupHandlerImpl implements GroupHandler {
} }
} }
private appendToSelection(objectState: any): void {
const { clientID } = objectState;
const shape = this.canvas.select(`#cvat_canvas_shape_${clientID}`).first();
if (shape) {
if (objectState.shapeType === 'mask') {
const { points } = objectState;
const colorRGB = [139, 0, 139];
const [left, top, right, bottom] = points.slice(-4);
const imageBitmap = expandChannels(colorRGB[0], colorRGB[1], colorRGB[2], points, 4);
const bbox = shape.bbox();
const image = this.canvas.image().attr({
'color-rendering': 'optimizeQuality',
'shape-rendering': 'geometricprecision',
'data-z-order': Number.MAX_SAFE_INTEGER,
'grouping-copy-for': clientID,
}).move(bbox.x, bbox.y);
this.groupingCopies[clientID] = image;
imageDataToDataURL(
imageBitmap,
right - left + 1,
bottom - top + 1,
(dataURL: string) => new Promise((resolve, reject) => {
image.loaded(() => {
resolve();
});
image.error(() => {
reject();
});
image.load(dataURL);
}),
);
}
this.statesToBeGroupped.push(objectState);
this.highlightedShapes[clientID] = shape;
shape.addClass('cvat_canvas_shape_grouping');
}
}
public select(objectState: any): void { public select(objectState: any): void {
const stateIndexes = this.statesToBeGroupped.map((state): number => state.clientID); const stateIndexes = this.statesToBeGroupped.map((state): number => state.clientID);
const includes = stateIndexes.indexOf(objectState.clientID); const { clientID } = objectState;
const includes = stateIndexes.indexOf(clientID);
if (includes !== -1) { if (includes !== -1) {
const shape = this.highlightedShapes[objectState.clientID]; const shape = this.highlightedShapes[clientID];
this.statesToBeGroupped.splice(includes, 1); this.statesToBeGroupped.splice(includes, 1);
if (shape) { if (shape) {
delete this.highlightedShapes[objectState.clientID]; if (this.groupingCopies[clientID]) {
// remove clones for masks
this.groupingCopies[clientID].remove();
delete this.groupingCopies[clientID];
}
delete this.highlightedShapes[clientID];
shape.removeClass('cvat_canvas_shape_grouping'); shape.removeClass('cvat_canvas_shape_grouping');
} }
} else { } else {
const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first(); this.appendToSelection(objectState);
if (shape) {
this.statesToBeGroupped.push(objectState);
this.highlightedShapes[objectState.clientID] = shape;
shape.addClass('cvat_canvas_shape_grouping');
}
} }
} }
@ -210,8 +252,14 @@ export class GroupHandlerImpl implements GroupHandler {
const shape = this.highlightedShapes[state.clientID]; const shape = this.highlightedShapes[state.clientID];
shape.removeClass('cvat_canvas_shape_grouping'); shape.removeClass('cvat_canvas_shape_grouping');
} }
for (const shape of Object.values(this.groupingCopies)) {
shape.remove();
}
this.statesToBeGroupped = []; this.statesToBeGroupped = [];
this.highlightedShapes = {}; this.highlightedShapes = {};
this.groupingCopies = {};
if (this.selectionRect) { if (this.selectionRect) {
this.selectionRect.remove(); this.selectionRect.remove();
this.selectionRect = null; this.selectionRect = null;

@ -6,7 +6,7 @@ import * as SVG from 'svg.js';
import consts from './consts'; import consts from './consts';
import Crosshair from './crosshair'; import Crosshair from './crosshair';
import { import {
translateToSVG, PropType, stringifyPoints, translateToCanvas, translateToSVG, PropType, stringifyPoints, translateToCanvas, expandChannels, imageDataToDataURL,
} from './shared'; } from './shared';
import { import {
@ -304,6 +304,33 @@ export class InteractionHandlerImpl implements InteractionHandler {
.fill({ opacity: this.selectedShapeOpacity, color: 'white' }) .fill({ opacity: this.selectedShapeOpacity, color: 'white' })
.addClass('cvat_canvas_interact_intermediate_shape'); .addClass('cvat_canvas_interact_intermediate_shape');
this.selectize(true, this.drawnIntermediateShape, erroredShape); this.selectize(true, this.drawnIntermediateShape, erroredShape);
} else if (shapeType === 'mask') {
const [left, top, right, bottom] = points.slice(-4);
const imageBitmap = expandChannels(255, 255, 255, points, 4);
const image = this.canvas.image().attr({
'color-rendering': 'optimizeQuality',
'shape-rendering': 'geometricprecision',
'pointer-events': 'none',
opacity: 0.5,
}).addClass('cvat_canvas_interact_intermediate_shape');
image.move(this.geometry.offset, this.geometry.offset);
this.drawnIntermediateShape = image;
imageDataToDataURL(
imageBitmap,
right - left + 1,
bottom - top + 1,
(dataURL: string) => new Promise((resolve, reject) => {
image.loaded(() => {
resolve();
});
image.error(() => {
reject();
});
image.load(dataURL);
}),
);
} else { } else {
throw new Error( throw new Error(
`Shape type "${shapeType}" was not implemented at interactionHandler::updateIntermediateShape`, `Shape type "${shapeType}" was not implemented at interactionHandler::updateIntermediateShape`,

@ -0,0 +1,655 @@
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import { fabric } from 'fabric';
import {
DrawData, MasksEditData, Geometry, Configuration, BrushTool, ColorBy,
} from './canvasModel';
import consts from './consts';
import { DrawHandler } from './drawHandler';
import {
PropType, computeWrappingBox, alphaChannelOnly, expandChannels, imageDataToDataURL,
} from './shared';
interface WrappingBBox {
left: number;
top: number;
right: number;
bottom: number;
}
export interface MasksHandler {
draw(drawData: DrawData): void;
edit(state: MasksEditData): void;
configurate(configuration: Configuration): void;
transform(geometry: Geometry): void;
cancel(): void;
enabled: boolean;
}
export class MasksHandlerImpl implements MasksHandler {
private onDrawDone: (
data: object | null,
duration?: number,
continueDraw?: boolean,
prevDrawData?: DrawData,
) => void;
private onDrawRepeat: (data: DrawData) => void;
private onEditStart: (state: any) => void;
private onEditDone: (state: any, points: number[]) => void;
private vectorDrawHandler: DrawHandler;
private redraw: number | null;
private isDrawing: boolean;
private isEditing: boolean;
private isInsertion: boolean;
private isMouseDown: boolean;
private isBrushSizeChanging: boolean;
private resizeBrushToolLatestX: number;
private brushMarker: fabric.Rect | fabric.Circle | null;
private drawablePolygon: null | fabric.Polygon;
private isPolygonDrawing: boolean;
private drawnObjects: (fabric.Polygon | fabric.Circle | fabric.Rect | fabric.Line | fabric.Image)[];
private tool: DrawData['brushTool'] | null;
private drawData: DrawData | null;
private canvas: fabric.Canvas;
private editData: MasksEditData | null;
private colorBy: ColorBy;
private latestMousePos: { x: number; y: number; };
private startTimestamp: number;
private geometry: Geometry;
private drawingOpacity: number;
private keepDrawnPolygon(): void {
const canvasWrapper = this.canvas.getElement().parentElement;
canvasWrapper.style.pointerEvents = '';
canvasWrapper.style.zIndex = '';
this.isPolygonDrawing = false;
this.vectorDrawHandler.draw({ enabled: false }, this.geometry);
}
private removeBrushMarker(): void {
if (this.brushMarker) {
this.canvas.remove(this.brushMarker);
this.brushMarker = null;
this.canvas.renderAll();
}
}
private setupBrushMarker(): void {
if (['brush', 'eraser'].includes(this.tool.type)) {
const common = {
evented: false,
selectable: false,
opacity: 0.75,
left: this.latestMousePos.x - this.tool.size / 2,
top: this.latestMousePos.y - this.tool.size / 2,
stroke: 'white',
strokeWidth: 1,
};
this.brushMarker = this.tool.form === 'circle' ? new fabric.Circle({
...common,
radius: this.tool.size / 2,
}) : new fabric.Rect({
...common,
width: this.tool.size,
height: this.tool.size,
});
this.canvas.defaultCursor = 'none';
this.canvas.add(this.brushMarker);
} else {
this.canvas.defaultCursor = 'inherit';
}
}
private releaseCanvasWrapperCSS(): void {
const canvasWrapper = this.canvas.getElement().parentElement;
canvasWrapper.style.pointerEvents = '';
canvasWrapper.style.zIndex = '';
canvasWrapper.style.display = '';
}
private releasePaste(): void {
this.releaseCanvasWrapperCSS();
this.canvas.clear();
this.canvas.renderAll();
this.isInsertion = false;
this.drawnObjects = [];
this.onDrawDone(null);
}
private releaseDraw(): void {
this.removeBrushMarker();
this.releaseCanvasWrapperCSS();
if (this.isPolygonDrawing) {
this.isPolygonDrawing = false;
this.vectorDrawHandler.cancel();
}
this.canvas.clear();
this.canvas.renderAll();
this.isDrawing = false;
this.isInsertion = false;
this.redraw = null;
this.drawnObjects = [];
this.onDrawDone(null);
}
private releaseEdit(): void {
this.removeBrushMarker();
this.releaseCanvasWrapperCSS();
if (this.isPolygonDrawing) {
this.isPolygonDrawing = false;
this.vectorDrawHandler.cancel();
}
this.canvas.clear();
this.canvas.renderAll();
this.isEditing = false;
this.drawnObjects = [];
this.onEditDone(null, null);
}
private getStateColor(state: any): string {
if (this.colorBy === ColorBy.INSTANCE) {
return state.color;
}
if (this.colorBy === ColorBy.LABEL) {
return state.label.color;
}
return state.group.color;
}
private getDrawnObjectsWrappingBox(): WrappingBBox {
type BoundingRect = ReturnType<PropType<fabric.Polygon, 'getBoundingRect'>>;
type TwoCornerBox = Pick<BoundingRect, 'top' | 'left'> & { right: number; bottom: number };
const { width, height } = this.geometry.image;
const wrappingBbox = this.drawnObjects
.map((obj) => {
if (obj instanceof fabric.Polygon) {
const bbox = computeWrappingBox(obj.points
.reduce(((acc, val) => {
acc.push(val.x, val.y);
return acc;
}), []));
return {
left: bbox.xtl,
top: bbox.ytl,
width: bbox.width,
height: bbox.height,
};
}
if (obj instanceof fabric.Image) {
return {
left: obj.left,
top: obj.top,
width: obj.width,
height: obj.height,
};
}
return obj.getBoundingRect();
})
.reduce((acc: TwoCornerBox, rect: BoundingRect) => {
acc.top = Math.floor(Math.max(0, Math.min(rect.top, acc.top)));
acc.left = Math.floor(Math.max(0, Math.min(rect.left, acc.left)));
acc.bottom = Math.floor(Math.min(height, Math.max(rect.top + rect.height, acc.bottom)));
acc.right = Math.floor(Math.min(width, Math.max(rect.left + rect.width, acc.right)));
return acc;
}, {
left: Number.MAX_SAFE_INTEGER,
top: Number.MAX_SAFE_INTEGER,
right: Number.MIN_SAFE_INTEGER,
bottom: Number.MIN_SAFE_INTEGER,
});
return wrappingBbox;
}
private imageDataFromCanvas(wrappingBBox: WrappingBBox): Uint8ClampedArray {
const imageData = this.canvas.toCanvasElement()
.getContext('2d').getImageData(
wrappingBBox.left, wrappingBBox.top,
wrappingBBox.right - wrappingBBox.left + 1, wrappingBBox.bottom - wrappingBBox.top + 1,
).data;
return imageData;
}
private updateBrushTools(brushTool?: BrushTool, opts: Partial<BrushTool> = {}): void {
if (this.isPolygonDrawing) {
// tool was switched from polygon to brush for example
this.keepDrawnPolygon();
}
this.removeBrushMarker();
if (brushTool) {
if (brushTool.color && this.tool?.color !== brushTool.color) {
const color = fabric.Color.fromHex(brushTool.color);
for (const object of this.drawnObjects) {
if (object instanceof fabric.Line) {
const alpha = +object.stroke.split(',')[3].slice(0, -1);
color.setAlpha(alpha);
object.set({ stroke: color.toRgba() });
} else if (!(object instanceof fabric.Image)) {
const alpha = +(object.fill as string).split(',')[3].slice(0, -1);
color.setAlpha(alpha);
object.set({ fill: color.toRgba() });
}
}
this.canvas.renderAll();
}
this.tool = { ...brushTool, ...opts };
if (this.isDrawing || this.isEditing) {
this.setupBrushMarker();
}
}
if (this.tool?.type?.startsWith('polygon-')) {
this.isPolygonDrawing = true;
this.vectorDrawHandler.draw({
enabled: true,
shapeType: 'polygon',
onDrawDone: (data: { points: number[] } | null) => {
if (!data) return;
const points = data.points.reduce((acc: fabric.Point[], _: number, idx: number) => {
if (idx % 2) {
acc.push(new fabric.Point(data.points[idx - 1], data.points[idx]));
}
return acc;
}, []);
const color = fabric.Color.fromHex(this.tool.color);
color.setAlpha(this.tool.type === 'polygon-minus' ? 1 : this.drawingOpacity);
const polygon = new fabric.Polygon(points, {
fill: color.toRgba(),
selectable: false,
objectCaching: false,
absolutePositioned: true,
globalCompositeOperation: this.tool.type === 'polygon-minus' ? 'destination-out' : 'xor',
});
this.canvas.add(polygon);
this.drawnObjects.push(polygon);
this.canvas.renderAll();
},
}, this.geometry);
const canvasWrapper = this.canvas.getElement().parentElement as HTMLDivElement;
canvasWrapper.style.pointerEvents = 'none';
canvasWrapper.style.zIndex = '0';
}
}
public constructor(
onDrawDone: (
data: object | null,
duration?: number,
continueDraw?: boolean,
prevDrawData?: DrawData,
) => void,
onDrawRepeat: (data: DrawData) => void,
onEditStart: (state: any) => void,
onEditDone: (state: any, points: number[]) => void,
vectorDrawHandler: DrawHandler,
canvas: HTMLCanvasElement,
) {
this.redraw = null;
this.isDrawing = false;
this.isEditing = false;
this.isMouseDown = false;
this.isBrushSizeChanging = false;
this.isPolygonDrawing = false;
this.drawData = null;
this.editData = null;
this.drawnObjects = [];
this.drawingOpacity = 0.5;
this.brushMarker = null;
this.colorBy = ColorBy.LABEL;
this.onDrawDone = onDrawDone;
this.onDrawRepeat = onDrawRepeat;
this.onEditDone = onEditDone;
this.onEditStart = onEditStart;
this.vectorDrawHandler = vectorDrawHandler;
this.canvas = new fabric.Canvas(canvas, {
containerClass: 'cvat_masks_canvas_wrapper',
fireRightClick: true,
selection: false,
defaultCursor: 'inherit',
});
this.canvas.imageSmoothingEnabled = false;
this.canvas.getElement().parentElement.addEventListener('contextmenu', (e: MouseEvent) => e.preventDefault());
this.latestMousePos = { x: -1, y: -1 };
window.document.addEventListener('mouseup', () => {
this.isMouseDown = false;
this.isBrushSizeChanging = false;
});
this.canvas.on('mouse:down', (options: fabric.IEvent<MouseEvent>) => {
const { isDrawing, isEditing, isInsertion } = this;
this.isMouseDown = (isDrawing || isEditing) && options.e.button === 0 && !options.e.altKey;
this.isBrushSizeChanging = (isDrawing || isEditing) && options.e.button === 2 && options.e.altKey;
if (isInsertion) {
const continueInserting = options.e.ctrlKey;
const wrappingBbox = this.getDrawnObjectsWrappingBox();
const imageData = this.imageDataFromCanvas(wrappingBbox);
const alpha = alphaChannelOnly(imageData);
alpha.push(wrappingBbox.left, wrappingBbox.top, wrappingBbox.right, wrappingBbox.bottom);
this.onDrawDone({
shapeType: this.drawData.shapeType,
points: alpha,
}, Date.now() - this.startTimestamp, continueInserting, this.drawData);
if (!continueInserting) {
this.releasePaste();
}
}
});
this.canvas.on('mouse:move', (e: fabric.IEvent<MouseEvent>) => {
const { image: { width: imageWidth, height: imageHeight } } = this.geometry;
const { angle } = this.geometry;
let [x, y] = [e.pointer.x, e.pointer.y];
if (angle === 180) {
[x, y] = [imageWidth - x, imageHeight - y];
} else if (angle === 270) {
[x, y] = [imageWidth - (y / imageHeight) * imageWidth, (x / imageWidth) * imageHeight];
} else if (angle === 90) {
[x, y] = [(y / imageHeight) * imageWidth, imageHeight - (x / imageWidth) * imageHeight];
}
const position = { x, y };
const {
tool, isMouseDown, isInsertion, isBrushSizeChanging,
} = this;
if (isInsertion) {
const [object] = this.drawnObjects;
if (object && object instanceof fabric.Image) {
object.left = position.x - object.width / 2;
object.top = position.y - object.height / 2;
this.canvas.renderAll();
}
}
if (isBrushSizeChanging && ['brush', 'eraser'].includes(tool?.type)) {
const xDiff = e.pointer.x - this.resizeBrushToolLatestX;
let onUpdateConfiguration = null;
if (this.isDrawing) {
onUpdateConfiguration = this.drawData.onUpdateConfiguration;
} else if (this.isEditing) {
onUpdateConfiguration = this.editData.onUpdateConfiguration;
}
if (onUpdateConfiguration) {
onUpdateConfiguration({
brushTool: {
size: Math.trunc(Math.max(1, this.tool.size + xDiff)),
},
});
}
this.resizeBrushToolLatestX = e.pointer.x;
e.e.stopPropagation();
return;
}
if (this.brushMarker) {
this.brushMarker.left = position.x - tool.size / 2;
this.brushMarker.top = position.y - tool.size / 2;
this.canvas.bringToFront(this.brushMarker);
this.canvas.renderAll();
}
if (isMouseDown && !isBrushSizeChanging && ['brush', 'eraser'].includes(tool?.type)) {
const color = fabric.Color.fromHex(tool.color);
color.setAlpha(tool.type === 'eraser' ? 1 : 0.5);
const commonProperties = {
selectable: false,
evented: false,
globalCompositeOperation: tool.type === 'eraser' ? 'destination-out' : 'xor',
};
const shapeProperties = {
...commonProperties,
fill: color.toRgba(),
left: position.x - tool.size / 2,
top: position.y - tool.size / 2,
};
let shape: fabric.Circle | fabric.Rect | null = null;
if (tool.form === 'circle') {
shape = new fabric.Circle({
...shapeProperties,
radius: tool.size / 2,
});
} else if (tool.form === 'square') {
shape = new fabric.Rect({
...shapeProperties,
width: tool.size,
height: tool.size,
});
}
this.canvas.add(shape);
if (tool.type === 'brush') {
this.drawnObjects.push(shape);
}
// add line to smooth the mask
if (this.latestMousePos.x !== -1 && this.latestMousePos.y !== -1) {
const dx = position.x - this.latestMousePos.x;
const dy = position.y - this.latestMousePos.y;
if (Math.sqrt(dx ** 2 + dy ** 2) > tool.size / 2) {
const line = new fabric.Line([
this.latestMousePos.x - tool.size / 2,
this.latestMousePos.y - tool.size / 2,
position.x - tool.size / 2,
position.y - tool.size / 2,
], {
...commonProperties,
stroke: color.toRgba(),
strokeWidth: tool.size,
strokeLineCap: tool.form === 'circle' ? 'round' : 'square',
});
this.canvas.add(line);
if (tool.type === 'brush') {
this.drawnObjects.push(line);
}
}
}
this.canvas.renderAll();
} else if (tool?.type.startsWith('polygon-') && this.drawablePolygon) {
// update the polygon position
const points = this.drawablePolygon.get('points');
if (points.length) {
points[points.length - 1].setX(e.e.offsetX);
points[points.length - 1].setY(e.e.offsetY);
}
this.canvas.renderAll();
}
this.latestMousePos.x = position.x;
this.latestMousePos.y = position.y;
this.resizeBrushToolLatestX = position.x;
});
}
public configurate(configuration: Configuration): void {
this.colorBy = configuration.colorBy;
}
public transform(geometry: Geometry): void {
this.geometry = geometry;
const {
scale, angle, image: { width, height }, top, left,
} = geometry;
const topCanvas = this.canvas.getElement().parentElement as HTMLDivElement;
if (this.canvas.width !== width || this.canvas.height !== height) {
this.canvas.setHeight(height);
this.canvas.setWidth(width);
this.canvas.setDimensions({ width, height });
}
topCanvas.style.top = `${top}px`;
topCanvas.style.left = `${left}px`;
topCanvas.style.transform = `scale(${scale}) rotate(${angle}deg)`;
if (this.drawablePolygon) {
this.drawablePolygon.set('strokeWidth', consts.BASE_STROKE_WIDTH / scale);
this.canvas.renderAll();
}
}
public draw(drawData: DrawData): void {
if (drawData.enabled && drawData.shapeType === 'mask') {
if (!this.isInsertion && drawData.initialState?.shapeType === 'mask') {
// initialize inserting pipeline if not started
const { points } = drawData.initialState;
const color = fabric.Color.fromHex(this.getStateColor(drawData.initialState)).getSource();
const [left, top, right, bottom] = points.slice(-4);
const imageBitmap = expandChannels(color[0], color[1], color[2], points, 4);
imageDataToDataURL(imageBitmap, right - left + 1, bottom - top + 1,
(dataURL: string) => new Promise((resolve) => {
fabric.Image.fromURL(dataURL, (image: fabric.Image) => {
try {
image.selectable = false;
image.evented = false;
image.globalCompositeOperation = 'xor';
image.opacity = 0.5;
this.canvas.add(image);
this.drawnObjects.push(image);
this.canvas.renderAll();
} finally {
resolve();
}
}, { left, top });
}));
this.isInsertion = true;
} else if (!this.isDrawing) {
// initialize drawing pipeline if not started
this.isDrawing = true;
this.redraw = drawData.redraw || null;
}
this.canvas.getElement().parentElement.style.display = 'block';
this.startTimestamp = Date.now();
}
this.updateBrushTools(drawData.brushTool);
if (!drawData.enabled && this.isDrawing) {
try {
if (this.drawnObjects.length) {
const wrappingBbox = this.getDrawnObjectsWrappingBox();
const imageData = this.imageDataFromCanvas(wrappingBbox);
const alpha = alphaChannelOnly(imageData);
alpha.push(wrappingBbox.left, wrappingBbox.top, wrappingBbox.right, wrappingBbox.bottom);
this.onDrawDone({
shapeType: this.drawData.shapeType,
points: alpha,
...(Number.isInteger(this.redraw) ? { clientID: this.redraw } : {}),
}, Date.now() - this.startTimestamp, drawData.continue, this.drawData);
}
} finally {
this.releaseDraw();
}
if (drawData.continue) {
const newDrawData = {
...this.drawData,
brushTool: { ...this.tool },
...drawData,
enabled: true,
shapeType: 'mask',
};
this.onDrawRepeat(newDrawData);
return;
}
}
this.drawData = drawData;
}
public edit(editData: MasksEditData): void {
if (editData.enabled && editData.state.shapeType === 'mask') {
if (!this.isEditing) {
// start editing pipeline if not started yet
this.canvas.getElement().parentElement.style.display = 'block';
const { points } = editData.state;
const color = fabric.Color.fromHex(this.getStateColor(editData.state)).getSource();
const [left, top, right, bottom] = points.slice(-4);
const imageBitmap = expandChannels(color[0], color[1], color[2], points, 4);
imageDataToDataURL(imageBitmap, right - left + 1, bottom - top + 1,
(dataURL: string) => new Promise((resolve) => {
fabric.Image.fromURL(dataURL, (image: fabric.Image) => {
try {
image.selectable = false;
image.evented = false;
image.globalCompositeOperation = 'xor';
image.opacity = 0.5;
this.canvas.add(image);
this.drawnObjects.push(image);
this.canvas.renderAll();
} finally {
resolve();
}
}, { left, top });
}));
this.isEditing = true;
this.startTimestamp = Date.now();
this.onEditStart(editData.state);
}
}
this.updateBrushTools(
editData.brushTool,
editData.state ? { color: this.getStateColor(editData.state) } : {},
);
if (!editData.enabled && this.isEditing) {
try {
if (this.drawnObjects.length) {
const wrappingBbox = this.getDrawnObjectsWrappingBox();
const imageData = this.imageDataFromCanvas(wrappingBbox);
const alpha = alphaChannelOnly(imageData);
alpha.push(wrappingBbox.left, wrappingBbox.top, wrappingBbox.right, wrappingBbox.bottom);
this.onEditDone(this.editData.state, alpha);
}
} finally {
this.releaseEdit();
}
}
this.editData = editData;
}
get enabled(): boolean {
return this.isDrawing || this.isEditing || this.isInsertion;
}
public cancel(): void {
if (this.isDrawing || this.isInsertion) {
this.releaseDraw();
}
if (this.isEditing) {
this.releaseEdit();
}
}
}

@ -103,6 +103,10 @@ export class MergeHandlerImpl implements MergeHandler {
} }
public select(objectState: any): void { public select(objectState: any): void {
if (objectState.shapeType === 'mask') {
// masks can not be merged
return;
}
const stateIndexes = this.statesToBeMerged.map((state): number => state.clientID); const stateIndexes = this.statesToBeMerged.map((state): number => state.clientID);
const stateFrames = this.statesToBeMerged.map((state): number => state.frame); const stateFrames = this.statesToBeMerged.map((state): number => state.frame);
const includes = stateIndexes.indexOf(objectState.clientID); const includes = stateIndexes.indexOf(objectState.clientID);

@ -361,4 +361,46 @@ export function setupSkeletonEdges(skeleton: SVG.G, referenceSVG: SVG.G): void {
} }
} }
export function imageDataToDataURL(
imageBitmap: Uint8ClampedArray,
width: number,
height: number,
handleResult: (dataURL: string) => Promise<void>,
): void {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').putImageData(
new ImageData(imageBitmap, width, height), 0, 0,
);
canvas.toBlob((blob) => {
const dataURL = URL.createObjectURL(blob);
handleResult(dataURL).finally(() => {
URL.revokeObjectURL(dataURL);
});
}, 'image/png');
}
export function alphaChannelOnly(imageData: Uint8ClampedArray): number[] {
const alpha = new Array(imageData.length / 4);
for (let i = 3; i < imageData.length; i += 4) {
alpha[Math.floor(i / 4)] = imageData[i] > 0 ? 1 : 0;
}
return alpha;
}
export function expandChannels(r: number, g: number, b: number, alpha: number[], endOffset = 0): Uint8ClampedArray {
const imageBitmap = new Uint8ClampedArray((alpha.length - endOffset) * 4);
for (let i = 0; i < alpha.length - endOffset; i++) {
const val = alpha[i] ? 1 : 0;
imageBitmap[i * 4] = r;
imageBitmap[i * 4 + 1] = g;
imageBitmap[i * 4 + 2] = b;
imageBitmap[i * 4 + 3] = val * 255;
}
return imageBitmap;
}
export type PropType<T, Prop extends keyof T> = T[Prop]; export type PropType<T, Prop extends keyof T> = T[Prop];

@ -3,7 +3,7 @@
"baseUrl": ".", "baseUrl": ".",
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"module": "es6", "module": "es6",
"target": "es2016", "target": "es2019",
"noImplicitAny": true, "noImplicitAny": true,
"preserveConstEnums": true, "preserveConstEnums": true,
"declaration": true, "declaration": true,

@ -1,6 +1,6 @@
{ {
"name": "cvat-core", "name": "cvat-core",
"version": "7.0.3", "version": "7.1.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration", "description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "src/api.ts", "main": "src/api.ts",
"scripts": { "scripts": {

@ -1,5 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corp // Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -13,10 +13,12 @@
} = require('./annotations-objects'); } = require('./annotations-objects');
const AnnotationsFilter = require('./annotations-filter').default; const AnnotationsFilter = require('./annotations-filter').default;
const { checkObjectType } = require('./common'); const { checkObjectType } = require('./common');
const Statistics = require('./statistics'); const Statistics = require('./statistics').default;
const { Label } = require('./labels'); const { Label } = require('./labels');
const { ArgumentError, ScriptingError } = require('./exceptions'); const { ArgumentError, ScriptingError } = require('./exceptions');
const ObjectState = require('./object-state').default; const ObjectState = require('./object-state').default;
const { mask2Rle, truncateMask } = require('./object-utils');
const config = require('./config').default;
const { const {
HistoryActions, ShapeType, ObjectType, colors, Source, HistoryActions, ShapeType, ObjectType, colors, Source,
@ -55,6 +57,8 @@
history: this.history, history: this.history,
nextClientID: () => ++this.count, nextClientID: () => ++this.count,
groupColors: {}, groupColors: {},
getMasksOnFrame: (frame: number) => this.shapes[frame]
.filter((object) => object.objectShape === ObjectType.MASK),
}; };
} }
@ -93,9 +97,8 @@
// In this case a corresponded message will be sent to the console // In this case a corresponded message will be sent to the console
if (trackModel) { if (trackModel) {
this.tracks.push(trackModel); this.tracks.push(trackModel);
this.objects[clientID] = trackModel;
result.tracks.push(trackModel); result.tracks.push(trackModel);
this.objects[clientID] = trackModel;
} }
} }
@ -106,15 +109,15 @@
const data = { const data = {
tracks: this.tracks.filter((track) => !track.removed).map((track) => track.toJSON()), tracks: this.tracks.filter((track) => !track.removed).map((track) => track.toJSON()),
shapes: Object.values(this.shapes) shapes: Object.values(this.shapes)
.reduce((accumulator, value) => { .reduce((accumulator, frameShapes) => {
accumulator.push(...value); accumulator.push(...frameShapes);
return accumulator; return accumulator;
}, []) }, [])
.filter((shape) => !shape.removed) .filter((shape) => !shape.removed)
.map((shape) => shape.toJSON()), .map((shape) => shape.toJSON()),
tags: Object.values(this.tags) tags: Object.values(this.tags)
.reduce((accumulator, value) => { .reduce((accumulator, frameTags) => {
accumulator.push(...value); accumulator.push(...frameTags);
return accumulator; return accumulator;
}, []) }, [])
.filter((tag) => !tag.removed) .filter((tag) => !tag.removed)
@ -356,6 +359,12 @@
'The object is not in collection yet. Call ObjectState.put([state]) before you can merge it', 'The object is not in collection yet. Call ObjectState.put([state]) before you can merge it',
); );
} }
if (state.shapeType === ShapeType.MASK) {
throw new ArgumentError(
'Merging for masks is not supported',
);
}
return object; return object;
}); });
@ -601,6 +610,7 @@
[val]: { shape: 0, track: 0 }, [val]: { shape: 0, track: 0 },
}), {})), }), {})),
mask: { shape: 0 },
tag: 0, tag: 0,
manually: 0, manually: 0,
interpolated: 0, interpolated: 0,
@ -787,7 +797,14 @@
group: 0, group: 0,
label_id: state.label.id, label_id: state.label.id,
occluded: state.occluded || false, occluded: state.occluded || false,
points: [...state.points], points: state.shapeType === 'mask' ? (() => {
const { width, height } = this.frameMeta[state.frame];
const points = truncateMask(state.points, 0, width, height);
const [left, top, right, bottom] = points.splice(-4);
const rlePoints = mask2Rle(points);
rlePoints.push(left, top, right, bottom);
return rlePoints;
})() : state.points,
rotation: state.rotation || 0, rotation: state.rotation || 0,
type: state.shapeType, type: state.shapeType,
z_order: state.zOrder, z_order: state.zOrder,
@ -865,6 +882,11 @@
// eslint-disable-next-line no-unsanitized/method // eslint-disable-next-line no-unsanitized/method
const imported = this.import(constructed); const imported = this.import(constructed);
const importedArray = imported.tags.concat(imported.tracks).concat(imported.shapes); const importedArray = imported.tags.concat(imported.tracks).concat(imported.shapes);
for (const object of importedArray) {
if (object.shapeType === ShapeType.MASK && config.removeUnderlyingMaskPixels) {
object.removeUnderlyingPixels(object.frame);
}
}
if (objectStates.length) { if (objectStates.length) {
this.history.do( this.history.do(

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import jsonLogic from 'json-logic-js'; import jsonLogic from 'json-logic-js';
import { AttributeType, ObjectType } from './enums'; import { AttributeType, ObjectType, ShapeType } from './enums';
function adjustName(name): string { function adjustName(name): string {
return name.replace(/\./g, '\u2219'); return name.replace(/\./g, '\u2219');
@ -17,13 +17,18 @@ export default class AnnotationsFilter {
return acc; return acc;
}, {}); }, {});
let [width, height] = [null, null];
if (state.objectType !== ObjectType.TAG) {
if (state.shapeType === ShapeType.MASK) {
const [xtl, ytl, xbr, ybr] = state.points.slice(-4);
[width, height] = [xbr - xtl + 1, ybr - ytl + 1];
} else {
let xtl = Number.MAX_SAFE_INTEGER; let xtl = Number.MAX_SAFE_INTEGER;
let xbr = Number.MIN_SAFE_INTEGER; let xbr = Number.MIN_SAFE_INTEGER;
let ytl = Number.MAX_SAFE_INTEGER; let ytl = Number.MAX_SAFE_INTEGER;
let ybr = Number.MIN_SAFE_INTEGER; let ybr = Number.MIN_SAFE_INTEGER;
let [width, height] = [null, null];
if (state.objectType !== ObjectType.TAG) {
const points = state.points || state.elements.reduce((acc, val) => { const points = state.points || state.elements.reduce((acc, val) => {
acc.push(val.points); acc.push(val.points);
return acc; return acc;
@ -41,6 +46,7 @@ export default class AnnotationsFilter {
}); });
[width, height] = [xbr - xtl, ybr - ytl]; [width, height] = [xbr - xtl, ybr - ytl];
} }
}
const attributes = {}; const attributes = {};
Object.keys(state.attributes).reduce((acc, key) => { Object.keys(state.attributes).reduce((acc, key) => {

File diff suppressed because it is too large Load Diff

@ -62,7 +62,7 @@ async function getAnnotationsFromServer(session) {
} }
} }
export async function closeSession(session) { export async function clearCache(session) {
const sessionType = session instanceof Task ? 'task' : 'job'; const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType); const cache = getCache(sessionType);
@ -284,29 +284,57 @@ export function importDataset(
useDefaultSettings: boolean, useDefaultSettings: boolean,
sourceStorage: Storage, sourceStorage: Storage,
file: File | string, file: File | string,
updateStatusCallback = () => {}, options: {
) { convMaskToPoly?: boolean,
updateStatusCallback?: (s: string, n: number) => void,
} = {},
): Promise<void> {
const updateStatusCallback = options.updateStatusCallback || (() => {});
const convMaskToPoly = 'convMaskToPoly' in options ? options.convMaskToPoly : true;
const adjustedOptions = {
updateStatusCallback,
convMaskToPoly,
};
if (!(instance instanceof Project || instance instanceof Task || instance instanceof Job)) { if (!(instance instanceof Project || instance instanceof Task || instance instanceof Job)) {
throw new ArgumentError('Instance should be a Project || Task || Job instance'); throw new ArgumentError('Instance must be a Project || Task || Job instance');
} }
if (!(typeof updateStatusCallback === 'function')) { if (!(typeof updateStatusCallback === 'function')) {
throw new ArgumentError('Callback should be a function'); throw new ArgumentError('Callback must be a function');
}
if (!(typeof convMaskToPoly === 'boolean')) {
throw new ArgumentError('Option "convMaskToPoly" must be a boolean');
} }
if (typeof file === 'string' && !file.toLowerCase().endsWith('.zip')) { if (typeof file === 'string' && !file.toLowerCase().endsWith('.zip')) {
throw new ArgumentError('File should be file instance with ZIP extension'); throw new ArgumentError('File must be file instance with ZIP extension');
} }
if (file instanceof File && !(['application/zip', 'application/x-zip-compressed'].includes(file.type))) { if (file instanceof File && !(['application/zip', 'application/x-zip-compressed'].includes(file.type))) {
throw new ArgumentError('File should be file instance with ZIP extension'); throw new ArgumentError('File must be file instance with ZIP extension');
} }
if (instance instanceof Project) { if (instance instanceof Project) {
return serverProxy.projects return serverProxy.projects
.importDataset(instance.id, format, useDefaultSettings, sourceStorage, file, updateStatusCallback); .importDataset(
instance.id,
format,
useDefaultSettings,
sourceStorage,
file,
adjustedOptions,
);
} }
const instanceType = instance instanceof Task ? 'task' : 'job'; const instanceType = instance instanceof Task ? 'task' : 'job';
return serverProxy.annotations return serverProxy.annotations
.uploadAnnotations(instanceType, instance.id, format, useDefaultSettings, sourceStorage, file); .uploadAnnotations(
instanceType,
instance.id,
format,
useDefaultSettings,
sourceStorage,
file,
adjustedOptions,
);
} }
export function getHistory(session) { export function getHistory(session) {

@ -8,7 +8,7 @@ const config = require('./config').default;
(() => { (() => {
const PluginRegistry = require('./plugins').default; const PluginRegistry = require('./plugins').default;
const serverProxy = require('./server-proxy').default; const serverProxy = require('./server-proxy').default;
const lambdaManager = require('./lambda-manager'); const lambdaManager = require('./lambda-manager').default;
const { const {
isBoolean, isBoolean,
isInteger, isInteger,

@ -13,14 +13,14 @@ function build() {
const loggerStorage = require('./logger-storage').default; const loggerStorage = require('./logger-storage').default;
const { Log } = require('./log'); const { Log } = require('./log');
const ObjectState = require('./object-state').default; const ObjectState = require('./object-state').default;
const Statistics = require('./statistics'); const Statistics = require('./statistics').default;
const Comment = require('./comment').default; const Comment = require('./comment').default;
const Issue = require('./issue').default; const Issue = require('./issue').default;
const { Job, Task } = require('./session'); const { Job, Task } = require('./session');
const Project = require('./project').default; const Project = require('./project').default;
const implementProject = require('./project-implementation').default; const implementProject = require('./project-implementation').default;
const { Attribute, Label } = require('./labels'); const { Attribute, Label } = require('./labels');
const MLModel = require('./ml-model'); const MLModel = require('./ml-model').default;
const { FrameData } = require('./frames'); const { FrameData } = require('./frames');
const CloudStorage = require('./cloud-storage').default; const CloudStorage = require('./cloud-storage').default;
const Organization = require('./organization').default; const Organization = require('./organization').default;
@ -703,6 +703,9 @@ function build() {
* @memberof module:API.cvat.config * @memberof module:API.cvat.config
* @property {number} uploadChunkSize max size of one data request in mb * @property {number} uploadChunkSize max size of one data request in mb
* @memberof module:API.cvat.config * @memberof module:API.cvat.config
* @property {number} removeUnderlyingMaskPixels defines if after adding/changing
* a mask it should remove overlapped pixels from other objects
* @memberof module:API.cvat.config
*/ */
get backendAPI() { get backendAPI() {
return config.backendAPI; return config.backendAPI;
@ -728,6 +731,12 @@ function build() {
set uploadChunkSize(value) { set uploadChunkSize(value) {
config.uploadChunkSize = value; config.uploadChunkSize = value;
}, },
get removeUnderlyingMaskPixels(): boolean {
return config.removeUnderlyingMaskPixels;
},
set removeUnderlyingMaskPixels(value: boolean) {
config.removeUnderlyingMaskPixels = value;
},
}, },
/** /**
* Namespace contains some library information e.g. api version * Namespace contains some library information e.g. api version

@ -1,4 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -8,6 +9,7 @@ const config = {
organizationID: null, organizationID: null,
origin: '', origin: '',
uploadChunkSize: 100, uploadChunkSize: 100,
removeUnderlyingMaskPixels: false,
}; };
export default config; export default config;

@ -172,6 +172,7 @@ export enum ShapeType {
ELLIPSE = 'ellipse', ELLIPSE = 'ellipse',
CUBOID = 'cuboid', CUBOID = 'cuboid',
SKELETON = 'skeleton', SKELETON = 'skeleton',
MASK = 'mask',
} }
/** /**

@ -1,10 +1,10 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import Platform from 'platform'; import Platform from 'platform';
import ErrorStackParser from 'error-stack-parser'; import ErrorStackParser from 'error-stack-parser';
// import config from './config';
/** /**
* Base exception class * Base exception class

@ -1,4 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -740,19 +741,19 @@
return frameDataCache[jobID].frameBuffer.require(frame, jobID, isPlaying, step); return frameDataCache[jobID].frameBuffer.require(frame, jobID, isPlaying, step);
} }
async function getDeletedFrames(sessionType, id) { async function getDeletedFrames(instanceType, id) {
if (sessionType === 'job') { if (instanceType === 'job') {
const { meta } = frameDataCache[id]; const { meta } = frameDataCache[id];
return meta.deleted_frames; return meta.deleted_frames;
} }
if (sessionType === 'task') { if (instanceType === 'task') {
const meta = await serverProxy.frames.getMeta('job', id); const meta = await serverProxy.frames.getMeta('job', id);
meta.deleted_frames = Object.fromEntries(meta.deleted_frames.map((_frame) => [_frame, true])); meta.deleted_frames = Object.fromEntries(meta.deleted_frames.map((_frame) => [_frame, true]));
return meta; return meta;
} }
throw Exception('getDeletedFrames is not implemented for tasks'); throw new Exception(`getDeletedFrames is not implemented for ${instanceType}`);
} }
function deleteFrame(jobID, frame) { function deleteFrame(jobID, frame) {

@ -139,7 +139,7 @@ export class Attribute {
} }
} }
type LabelType = 'rectangle' | 'polygon' | 'polyline' | 'points' | 'ellipse' | 'cuboid' | 'skeleton' | 'any'; type LabelType = 'rectangle' | 'polygon' | 'polyline' | 'points' | 'ellipse' | 'cuboid' | 'skeleton' | 'mask' | 'tag' | 'any';
export interface RawLabel { export interface RawLabel {
id?: number; id?: number;
name: string; name: string;

@ -1,19 +1,23 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
const serverProxy = require('./server-proxy').default; import serverProxy from './server-proxy';
const { ArgumentError } = require('./exceptions'); import { ArgumentError } from './exceptions';
const MLModel = require('./ml-model'); import MLModel from './ml-model';
const { RQStatus } = require('./enums'); import { RQStatus } from './enums';
class LambdaManager { class LambdaManager {
private listening: any;
private cachedList: any;
constructor() { constructor() {
this.listening = {}; this.listening = {};
this.cachedList = null; this.cachedList = null;
} }
async list() { async list(): Promise<MLModel[]> {
if (Array.isArray(this.cachedList)) { if (Array.isArray(this.cachedList)) {
return [...this.cachedList]; return [...this.cachedList];
} }
@ -34,7 +38,7 @@ class LambdaManager {
return models; return models;
} }
async run(taskID, model, args) { async run(taskID: number, model: MLModel, args: any) {
if (!Number.isInteger(taskID) || taskID < 0) { if (!Number.isInteger(taskID) || taskID < 0) {
throw new ArgumentError(`Argument taskID must be a positive integer. Got "${taskID}"`); throw new ArgumentError(`Argument taskID must be a positive integer. Got "${taskID}"`);
} }
@ -78,7 +82,7 @@ class LambdaManager {
return result.filter((request) => ['queued', 'started'].includes(request.status)); return result.filter((request) => ['queued', 'started'].includes(request.status));
} }
async cancel(requestID) { async cancel(requestID): Promise<void> {
if (typeof requestID !== 'string') { if (typeof requestID !== 'string') {
throw new ArgumentError(`Request id argument is required to be a string. But got ${requestID}`); throw new ArgumentError(`Request id argument is required to be a string. But got ${requestID}`);
} }
@ -90,8 +94,8 @@ class LambdaManager {
await serverProxy.lambda.cancel(requestID); await serverProxy.lambda.cancel(requestID);
} }
async listen(requestID, onUpdate) { async listen(requestID, onUpdate): Promise<void> {
const timeoutCallback = async () => { const timeoutCallback = async (): Promise<void> => {
try { try {
this.listening[requestID].timeout = null; this.listening[requestID].timeout = null;
const response = await serverProxy.lambda.status(requestID); const response = await serverProxy.lambda.status(requestID);
@ -124,4 +128,4 @@ class LambdaManager {
} }
} }
module.exports = new LambdaManager(); export default new LambdaManager();

@ -1,134 +1,111 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
/** import { ModelType } from './enums';
* Class representing a serverless function
* @memberof module:API.cvat.classes interface ModelAttribute {
*/ name: string;
class MLModel { values: string[];
constructor(data) { input_type: 'select' | 'number' | 'checkbox' | 'radio' | 'text';
this._id = data.id; }
this._name = data.name;
this._labels = data.labels; interface ModelParams {
this._attributes = data.attributes || [];
this._framework = data.framework;
this._description = data.description;
this._type = data.type;
this._tip = {
message: data.help_message,
gif: data.animated_gif,
};
this._params = {
canvas: { canvas: {
minPosVertices: data.min_pos_points, minPosVertices?: number;
minNegVertices: data.min_neg_points, minNegVertices?: number;
startWithBox: data.startswith_box, startWithBox?: boolean;
}, onChangeToolsBlockerState?: (event: string) => void;
}; };
}
interface ModelTip {
message: string;
gif: string;
}
interface SerializedModel {
id: string;
name: string;
labels: string[];
version: number;
attributes: Record<string, ModelAttribute>;
framework: string;
description: string;
type: ModelType;
help_message?: string;
animated_gif?: string;
min_pos_points?: number;
min_neg_points?: number;
startswith_box?: boolean;
}
export default class MLModel {
private serialized: SerializedModel;
private changeToolsBlockerStateCallback?: (event: string) => void;
constructor(serialized: SerializedModel) {
this.serialized = { ...serialized };
} }
/** public get id(): string {
* @type {string} return this.serialized.id;
* @readonly
*/
get id() {
return this._id;
} }
/** public get name(): string {
* @type {string} return this.serialized.name;
* @readonly
*/
get name() {
return this._name;
} }
/** public get labels(): string[] {
* @description labels supported by the model return Array.isArray(this.serialized.labels) ? [...this.serialized.labels] : [];
* @type {string[]}
* @readonly
*/
get labels() {
if (Array.isArray(this._labels)) {
return [...this._labels];
} }
return []; public get version(): number {
return this.serialized.version;
} }
/** public get attributes(): Record<string, ModelAttribute> {
* @typedef ModelAttribute return this.serialized.attributes || {};
* @property {string} name
* @property {string[]} values
* @property {'select'|'number'|'checkbox'|'radio'|'text'} input_type
*/
/**
* @type {Object<string, ModelAttribute>}
* @readonly
*/
get attributes() {
return { ...this._attributes };
} }
/** public get framework(): string {
* @type {string} return this.serialized.framework;
* @readonly
*/
get framework() {
return this._framework;
} }
/** public get description(): string {
* @type {string} return this.serialized.description;
* @readonly
*/
get description() {
return this._description;
} }
/** public get type(): ModelType {
* @type {module:API.cvat.enums.ModelType} return this.serialized.type;
* @readonly
*/
get type() {
return this._type;
} }
/** public get params(): ModelParams {
* @type {object} const result: ModelParams = {
* @readonly canvas: {
*/ minPosVertices: this.serialized.min_pos_points,
get params() { minNegVertices: this.serialized.min_neg_points,
return { startWithBox: this.serialized.startswith_box,
canvas: { ...this._params.canvas }, },
}; };
if (this.changeToolsBlockerStateCallback) {
result.canvas.onChangeToolsBlockerState = this.changeToolsBlockerStateCallback;
} }
/** return result;
* @type {MlModelTip}
* @property {string} message A short message for a user about the model
* @property {string} gif A gif URL to be shown to a user as an example
* @readonly
*/
get tip() {
return { ...this._tip };
} }
/** public get tip(): ModelTip {
* @typedef onRequestStatusChange return {
* @param {string} event message: this.serialized.help_message,
* @global gif: this.serialized.animated_gif,
*/ };
/**
* @param {onRequestStatusChange} onRequestStatusChange
* @instance
* @description Used to set a callback when the tool is blocked in UI
* @returns {void}
*/
set onChangeToolsBlockerState(onChangeToolsBlockerState) {
this._params.canvas.onChangeToolsBlockerState = onChangeToolsBlockerState;
} }
}
module.exports = MLModel; // Used to set a callback when the tool is blocked in UI
public set onChangeToolsBlockerState(onChangeToolsBlockerState: (event: string) => void) {
this.changeToolsBlockerStateCallback = onChangeToolsBlockerState;
}
}

@ -1,4 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -26,7 +27,7 @@ interface UpdateFlags {
reset: () => void; reset: () => void;
} }
interface SerializedData { export interface SerializedData {
objectType: ObjectType; objectType: ObjectType;
label: Label; label: Label;
frame: number; frame: number;
@ -333,7 +334,7 @@ export default class ObjectState {
} }
if (Array.isArray(data.points)) { if (Array.isArray(data.points)) {
return [...data.points]; return data.points;
} }
return []; return [];
@ -365,7 +366,7 @@ export default class ObjectState {
data.updateFlags.points = true; data.updateFlags.points = true;
} }
data.points = [...points]; data.points = points.slice();
}, },
}, },
rotation: { rotation: {

@ -0,0 +1,279 @@
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import { DataError, ArgumentError } from './exceptions';
import { Attribute } from './labels';
import { ShapeType, AttributeType } from './enums';
export function checkNumberOfPoints(shapeType: ShapeType, points: number[]): void {
if (shapeType === ShapeType.RECTANGLE) {
if (points.length / 2 !== 2) {
throw new DataError(`Rectangle must have 2 points, but got ${points.length / 2}`);
}
} else if (shapeType === ShapeType.POLYGON) {
if (points.length / 2 < 3) {
throw new DataError(`Polygon must have at least 3 points, but got ${points.length / 2}`);
}
} else if (shapeType === ShapeType.POLYLINE) {
if (points.length / 2 < 2) {
throw new DataError(`Polyline must have at least 2 points, but got ${points.length / 2}`);
}
} else if (shapeType === ShapeType.POINTS) {
if (points.length / 2 < 1) {
throw new DataError(`Points must have at least 1 points, but got ${points.length / 2}`);
}
} else if (shapeType === ShapeType.CUBOID) {
if (points.length / 2 !== 8) {
throw new DataError(`Cuboid must have 8 points, but got ${points.length / 2}`);
}
} else if (shapeType === ShapeType.ELLIPSE) {
if (points.length / 2 !== 2) {
throw new DataError(`Ellipse must have 1 point, rx and ry but got ${points.toString()}`);
}
} else if (shapeType === ShapeType.MASK) {
const [left, top, right, bottom] = points.slice(-4);
const [width, height] = [right - left, bottom - top];
if (width < 0 || !Number.isInteger(width) || height < 0 || !Number.isInteger(height)) {
throw new DataError(`Mask width, height must be positive integers, but got ${width}x${height}`);
}
if (points.length !== width * height + 4) {
throw new DataError(`Points array must have length ${width}x${height} + 4, got ${points.length}`);
}
} else {
throw new ArgumentError(`Unknown value of shapeType has been received ${shapeType}`);
}
}
export function attrsAsAnObject(attributes: Attribute[]): Record<number, Attribute> {
return attributes.reduce((accumulator, value) => {
accumulator[value.id] = value;
return accumulator;
}, {});
}
export function findAngleDiff(rightAngle: number, leftAngle: number): number {
let angleDiff = rightAngle - leftAngle;
angleDiff = ((angleDiff + 180) % 360) - 180;
if (Math.abs(angleDiff) >= 180) {
// if the main arc is bigger than 180, go another arc
// to find it, just substract absolute value from 360 and inverse sign
angleDiff = 360 - Math.abs(angleDiff) * Math.sign(angleDiff) * -1;
}
return angleDiff;
}
export function checkShapeArea(shapeType: ShapeType, points: number[]): boolean {
const MIN_SHAPE_LENGTH = 3;
const MIN_SHAPE_AREA = 9;
const MIN_MASK_SHAPE_AREA = 1;
if (shapeType === ShapeType.POINTS) {
return true;
}
if (shapeType === ShapeType.MASK) {
const [left, top, right, bottom] = points.slice(-4);
const area = (right - left + 1) * (bottom - top + 1);
return area >= MIN_MASK_SHAPE_AREA;
}
if (shapeType === ShapeType.ELLIPSE) {
const [cx, cy, rightX, topY] = points;
const [rx, ry] = [rightX - cx, cy - topY];
return rx * ry * Math.PI > MIN_SHAPE_AREA;
}
let xmin = Number.MAX_SAFE_INTEGER;
let xmax = Number.MIN_SAFE_INTEGER;
let ymin = Number.MAX_SAFE_INTEGER;
let ymax = Number.MIN_SAFE_INTEGER;
for (let i = 0; i < points.length - 1; i += 2) {
xmin = Math.min(xmin, points[i]);
xmax = Math.max(xmax, points[i]);
ymin = Math.min(ymin, points[i + 1]);
ymax = Math.max(ymax, points[i + 1]);
}
if (shapeType === ShapeType.POLYLINE) {
const length = Math.max(xmax - xmin, ymax - ymin);
return length >= MIN_SHAPE_LENGTH;
}
const area = (xmax - xmin) * (ymax - ymin);
return area >= MIN_SHAPE_AREA;
}
export function rotatePoint(x: number, y: number, angle: number, cx = 0, cy = 0): number[] {
const sin = Math.sin((angle * Math.PI) / 180);
const cos = Math.cos((angle * Math.PI) / 180);
const rotX = (x - cx) * cos - (y - cy) * sin + cx;
const rotY = (y - cy) * cos + (x - cx) * sin + cy;
return [rotX, rotY];
}
export function computeWrappingBox(points: number[], margin = 0): {
xtl: number;
ytl: number;
xbr: number;
ybr: number;
x: number;
y: number;
width: number;
height: number;
} {
let xtl = Number.MAX_SAFE_INTEGER;
let ytl = Number.MAX_SAFE_INTEGER;
let xbr = Number.MIN_SAFE_INTEGER;
let ybr = Number.MIN_SAFE_INTEGER;
for (let i = 0; i < points.length; i += 2) {
const [x, y] = [points[i], points[i + 1]];
xtl = Math.min(xtl, x);
ytl = Math.min(ytl, y);
xbr = Math.max(xbr, x);
ybr = Math.max(ybr, y);
}
const box = {
xtl: xtl - margin,
ytl: ytl - margin,
xbr: xbr + margin,
ybr: ybr + margin,
};
return {
...box,
x: box.xtl,
y: box.ytl,
width: box.xbr - box.xtl,
height: box.ybr - box.ytl,
};
}
export function validateAttributeValue(value: string, attr: Attribute): boolean {
const { values } = attr;
const type = attr.inputType;
if (typeof value !== 'string') {
throw new ArgumentError(`Attribute value is expected to be string, but got ${typeof value}`);
}
if (type === AttributeType.NUMBER) {
return +value >= +values[0] && +value <= +values[1];
}
if (type === AttributeType.CHECKBOX) {
return ['true', 'false'].includes(value.toLowerCase());
}
if (type === AttributeType.TEXT) {
return true;
}
return values.includes(value);
}
export function truncateMask(points: number[], _: number, width: number, height: number): number[] {
const [currentLeft, currentTop, currentRight, currentBottom] = points.slice(-4);
const [currentWidth, currentHeight] = [currentRight - currentLeft + 1, currentBottom - currentTop + 1];
let left = width;
let right = 0;
let top = height;
let bottom = 0;
let atLeastOnePixel = false;
const truncatedPoints = [];
for (let y = 0; y < currentHeight; y++) {
const absY = y + currentTop;
for (let x = 0; x < currentWidth; x++) {
const absX = x + currentLeft;
const offset = y * currentWidth + x;
if (absX >= width || absY >= height || absX < 0 || absY < 0) {
points[offset] = 0;
}
if (points[offset]) {
atLeastOnePixel = true;
left = Math.min(left, absX);
top = Math.min(top, absY);
right = Math.max(right, absX);
bottom = Math.max(bottom, absY);
}
}
}
if (!atLeastOnePixel) {
// if mask is empty, set its size as 0
left = 0;
top = 0;
}
// TODO: check corner case when right = left = 0
const [newWidth, newHeight] = [right - left + 1, bottom - top + 1];
for (let y = 0; y < newHeight; y++) {
for (let x = 0; x < newWidth; x++) {
const leftDiff = left - currentLeft;
const topDiff = top - currentTop;
const offset = (y + topDiff) * currentWidth + (x + leftDiff);
truncatedPoints.push(points[offset]);
}
}
truncatedPoints.push(left, top, right, bottom);
if (!checkShapeArea(ShapeType.MASK, truncatedPoints)) {
return [];
}
return truncatedPoints;
}
export function mask2Rle(mask: number[]): number[] {
return mask.reduce((acc, val, idx, arr) => {
if (idx > 0) {
if (arr[idx - 1] === val) {
acc[acc.length - 1] += 1;
} else {
acc.push(1);
}
return acc;
}
if (val > 0) {
// 0, 0, 0, 1 => [3, 1]
// 1, 1, 0, 0 => [0, 2, 2]
acc.push(0, 1);
} else {
acc.push(1);
}
return acc;
}, []);
}
export function rle2Mask(rle: number[], width: number, height: number): number[] {
const decoded = Array(width * height).fill(0);
const { length } = rle;
let decodedIdx = 0;
let value = 0;
let i = 0;
while (i < length) {
let count = rle[i];
while (count > 0) {
decoded[decodedIdx] = value;
decodedIdx++;
count--;
}
i++;
value = Math.abs(value - 1);
}
return decoded;
}

@ -85,9 +85,12 @@ export default function implementProject(projectClass) {
useDefaultSettings: boolean, useDefaultSettings: boolean,
sourceStorage: Storage, sourceStorage: Storage,
file: File | string, file: File | string,
updateStatusCallback, options?: {
convMaskToPoly?: boolean,
updateStatusCallback?: (s: string, n: number) => void,
},
) { ) {
return importDataset(this, format, useDefaultSettings, sourceStorage, file, updateStatusCallback); return importDataset(this, format, useDefaultSettings, sourceStorage, file, options);
}; };
projectClass.prototype.backup.implementation = async function ( projectClass.prototype.backup.implementation = async function (

@ -409,7 +409,10 @@ Object.defineProperties(
useDefaultSettings: boolean, useDefaultSettings: boolean,
sourceStorage: Storage, sourceStorage: Storage,
file: File | string, file: File | string,
updateStatusCallback = null, options?: {
convMaskToPoly?: boolean,
updateStatusCallback?: (s: string, n: number) => void,
},
) { ) {
const result = await PluginRegistry.apiWrapper.call( const result = await PluginRegistry.apiWrapper.call(
this, this,
@ -418,7 +421,7 @@ Object.defineProperties(
useDefaultSettings, useDefaultSettings,
sourceStorage, sourceStorage,
file, file,
updateStatusCallback, options,
); );
return result; return result;
}, },

@ -684,14 +684,18 @@ class ServerProxy {
useDefaultLocation: boolean, useDefaultLocation: boolean,
sourceStorage: Storage, sourceStorage: Storage,
file: File | string, file: File | string,
onUpdate, options: {
) { convMaskToPoly: boolean,
updateStatusCallback: (s: string, n: number) => void,
},
): Promise<void> {
const { backendAPI, origin } = config; const { backendAPI, origin } = config;
const params: Params = { const params: Params & { conv_mask_to_poly: boolean } = {
...enableOrganization(), ...enableOrganization(),
...configureStorage(sourceStorage, useDefaultLocation), ...configureStorage(sourceStorage, useDefaultLocation),
format, format,
filename: typeof file === 'string' ? file : file.name, filename: typeof file === 'string' ? file : file.name,
conv_mask_to_poly: options.convMaskToPoly,
}; };
const url = `${backendAPI}/projects/${id}/dataset`; const url = `${backendAPI}/projects/${id}/dataset`;
@ -705,8 +709,8 @@ class ServerProxy {
proxy: config.proxy, proxy: config.proxy,
}); });
if (response.status === 202) { if (response.status === 202) {
if (onUpdate && response.data.message) { if (response.data.message) {
onUpdate(response.data.message, response.data.progress || 0); options.updateStatusCallback(response.data.message, response.data.progress || 0);
} }
setTimeout(requestStatus, 3000); setTimeout(requestStatus, 3000);
} else if (response.status === 201) { } else if (response.status === 201) {
@ -740,7 +744,7 @@ class ServerProxy {
totalSentSize: 0, totalSentSize: 0,
totalSize: (file as File).size, totalSize: (file as File).size,
onUpdate: (percentage) => { onUpdate: (percentage) => {
onUpdate('The dataset is being uploaded to the server', percentage); options.updateStatusCallback('The dataset is being uploaded to the server', percentage);
}, },
}; };
@ -1449,13 +1453,15 @@ class ServerProxy {
useDefaultLocation: boolean, useDefaultLocation: boolean,
sourceStorage: Storage, sourceStorage: Storage,
file: File | string, file: File | string,
) { options: { convMaskToPoly: boolean },
): Promise<void> {
const { backendAPI, origin } = config; const { backendAPI, origin } = config;
const params: Params = { const params: Params & { conv_mask_to_poly: boolean } = {
...enableOrganization(), ...enableOrganization(),
...configureStorage(sourceStorage, useDefaultLocation), ...configureStorage(sourceStorage, useDefaultLocation),
format, format,
filename: typeof file === 'string' ? file : file.name, filename: typeof file === 'string' ? file : file.name,
conv_mask_to_poly: options.convMaskToPoly,
}; };
const url = `${backendAPI}/${session}s/${id}/annotations`; const url = `${backendAPI}/${session}s/${id}/annotations`;

@ -34,7 +34,13 @@ function buildDuplicatedAPI(prototype) {
Object.defineProperties(prototype, { Object.defineProperties(prototype, {
annotations: Object.freeze({ annotations: Object.freeze({
value: { value: {
async upload(format: string, useDefaultLocation: boolean, sourceStorage: Storage, file: File | string) { async upload(
format: string,
useDefaultLocation: boolean,
sourceStorage: Storage,
file: File | string,
options?: { convMaskToPoly?: boolean },
) {
const result = await PluginRegistry.apiWrapper.call( const result = await PluginRegistry.apiWrapper.call(
this, this,
prototype.annotations.upload, prototype.annotations.upload,
@ -42,6 +48,7 @@ function buildDuplicatedAPI(prototype) {
useDefaultLocation, useDefaultLocation,
sourceStorage, sourceStorage,
file, file,
options,
); );
return result; return result;
}, },
@ -1939,36 +1946,24 @@ export class Task extends Session {
} }
} }
const {
getAnnotations,
putAnnotations,
saveAnnotations,
hasUnsavedChanges,
searchAnnotations,
searchEmptyFrame,
mergeAnnotations,
splitAnnotations,
groupAnnotations,
clearAnnotations,
selectObject,
annotationsStatistics,
importCollection,
exportCollection,
importDataset,
exportDataset,
undoActions,
redoActions,
freezeHistory,
clearActions,
getActions,
closeSession,
getHistory,
} = require('./annotations');
buildDuplicatedAPI(Job.prototype); buildDuplicatedAPI(Job.prototype);
buildDuplicatedAPI(Task.prototype); buildDuplicatedAPI(Task.prototype);
Job.prototype.save.implementation = async function () { (async () => {
const annotations = await import('./annotations');
const {
getAnnotations, putAnnotations, saveAnnotations,
hasUnsavedChanges, searchAnnotations, searchEmptyFrame,
mergeAnnotations, splitAnnotations, groupAnnotations,
clearAnnotations, selectObject, annotationsStatistics,
importCollection, exportCollection, importDataset,
exportDataset, undoActions, redoActions,
freezeHistory, clearActions, getActions,
clearCache, getHistory,
} = annotations;
Job.prototype.save.implementation = async function () {
if (this.id) { if (this.id) {
const jobData = this._updateTrigger.getUpdated(this); const jobData = this._updateTrigger.getUpdated(this);
if (jobData.assignee) { if (jobData.assignee) {
@ -1981,14 +1976,14 @@ Job.prototype.save.implementation = async function () {
} }
throw new ArgumentError('Could not save job without id'); throw new ArgumentError('Could not save job without id');
}; };
Job.prototype.issues.implementation = async function () { Job.prototype.issues.implementation = async function () {
const result = await serverProxy.issues.get(this.id); const result = await serverProxy.issues.get(this.id);
return result.map((issue) => new Issue(issue)); return result.map((issue) => new Issue(issue));
}; };
Job.prototype.openIssue.implementation = async function (issue, message) { Job.prototype.openIssue.implementation = async function (issue, message) {
checkObjectType('issue', issue, null, Issue); checkObjectType('issue', issue, null, Issue);
checkObjectType('message', message, 'string'); checkObjectType('message', message, 'string');
const result = await serverProxy.issues.create({ const result = await serverProxy.issues.create({
@ -1996,9 +1991,9 @@ Job.prototype.openIssue.implementation = async function (issue, message) {
message, message,
}); });
return new Issue(result); return new Issue(result);
}; };
Job.prototype.frames.get.implementation = async function (frame, isPlaying, step) { Job.prototype.frames.get.implementation = async function (frame, isPlaying, step) {
if (!Number.isInteger(frame) || frame < 0) { if (!Number.isInteger(frame) || frame < 0) {
throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`);
} }
@ -2020,10 +2015,10 @@ Job.prototype.frames.get.implementation = async function (frame, isPlaying, step
this.dimension, this.dimension,
); );
return frameData; return frameData;
}; };
// must be called with task/job context // must be called with task/job context
async function deleteFrameWrapper(jobID, frame) { async function deleteFrameWrapper(jobID, frame) {
const history = getHistory(this); const history = getHistory(this);
const redo = async () => { const redo = async () => {
deleteFrame(jobID, frame); deleteFrame(jobID, frame);
@ -2033,9 +2028,9 @@ async function deleteFrameWrapper(jobID, frame) {
history.do(HistoryActions.REMOVED_FRAME, async () => { history.do(HistoryActions.REMOVED_FRAME, async () => {
restoreFrame(jobID, frame); restoreFrame(jobID, frame);
}, redo, [], frame); }, redo, [], frame);
} }
async function restoreFrameWrapper(jobID, frame) { async function restoreFrameWrapper(jobID, frame) {
const history = getHistory(this); const history = getHistory(this);
const redo = async () => { const redo = async () => {
restoreFrame(jobID, frame); restoreFrame(jobID, frame);
@ -2045,9 +2040,9 @@ async function restoreFrameWrapper(jobID, frame) {
history.do(HistoryActions.RESTORED_FRAME, async () => { history.do(HistoryActions.RESTORED_FRAME, async () => {
deleteFrame(jobID, frame); deleteFrame(jobID, frame);
}, redo, [], frame); }, redo, [], frame);
} }
Job.prototype.frames.delete.implementation = async function (frame) { Job.prototype.frames.delete.implementation = async function (frame) {
if (!Number.isInteger(frame)) { if (!Number.isInteger(frame)) {
throw new Error(`Frame must be an integer. Got: "${frame}"`); throw new Error(`Frame must be an integer. Got: "${frame}"`);
} }
@ -2057,9 +2052,9 @@ Job.prototype.frames.delete.implementation = async function (frame) {
} }
await deleteFrameWrapper.call(this, this.id, frame); await deleteFrameWrapper.call(this, this.id, frame);
}; };
Job.prototype.frames.restore.implementation = async function (frame) { Job.prototype.frames.restore.implementation = async function (frame) {
if (!Number.isInteger(frame)) { if (!Number.isInteger(frame)) {
throw new Error(`Frame must be an integer. Got: "${frame}"`); throw new Error(`Frame must be an integer. Got: "${frame}"`);
} }
@ -2069,33 +2064,33 @@ Job.prototype.frames.restore.implementation = async function (frame) {
} }
await restoreFrameWrapper.call(this, this.id, frame); await restoreFrameWrapper.call(this, this.id, frame);
}; };
Job.prototype.frames.save.implementation = async function () { Job.prototype.frames.save.implementation = async function () {
const result = await patchMeta(this.id); const result = await patchMeta(this.id);
return result; return result;
}; };
Job.prototype.frames.ranges.implementation = async function () { Job.prototype.frames.ranges.implementation = async function () {
const rangesData = await getRanges(this.id); const rangesData = await getRanges(this.id);
return rangesData; return rangesData;
}; };
Job.prototype.frames.preview.implementation = async function () { Job.prototype.frames.preview.implementation = async function () {
if (this.id === null || this.taskId === null) { if (this.id === null || this.taskId === null) {
return ''; return '';
} }
const frameData = await getPreview(this.taskId, this.id); const frameData = await getPreview(this.taskId, this.id);
return frameData; return frameData;
}; };
Job.prototype.frames.contextImage.implementation = async function (frameId) { Job.prototype.frames.contextImage.implementation = async function (frameId) {
const result = await getContextImage(this.id, frameId); const result = await getContextImage(this.id, frameId);
return result; return result;
}; };
Job.prototype.frames.search.implementation = async function (filters, frameFrom, frameTo) { Job.prototype.frames.search.implementation = async function (filters, frameFrom, frameTo) {
if (typeof filters !== 'object') { if (typeof filters !== 'object') {
throw new ArgumentError('Filters should be an object'); throw new ArgumentError('Filters should be an object');
} }
@ -2115,10 +2110,10 @@ Job.prototype.frames.search.implementation = async function (filters, frameFrom,
return findNotDeletedFrame(this.id, frameFrom, frameTo, filters.offset || 1); return findNotDeletedFrame(this.id, frameFrom, frameTo, filters.offset || 1);
} }
return null; return null;
}; };
// TODO: Check filter for annotations // TODO: Check filter for annotations
Job.prototype.annotations.get.implementation = async function (frame, allTracks, filters) { Job.prototype.annotations.get.implementation = async function (frame, allTracks, filters) {
if (!Array.isArray(filters)) { if (!Array.isArray(filters)) {
throw new ArgumentError('Filters must be an array'); throw new ArgumentError('Filters must be an array');
} }
@ -2138,9 +2133,9 @@ Job.prototype.annotations.get.implementation = async function (frame, allTracks,
} }
return annotationsData; return annotationsData;
}; };
Job.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) { Job.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) {
if (!Array.isArray(filters)) { if (!Array.isArray(filters)) {
throw new ArgumentError('Filters must be an array'); throw new ArgumentError('Filters must be an array');
} }
@ -2159,9 +2154,9 @@ Job.prototype.annotations.search.implementation = function (filters, frameFrom,
const result = searchAnnotations(this, filters, frameFrom, frameTo); const result = searchAnnotations(this, filters, frameFrom, frameTo);
return result; return result;
}; };
Job.prototype.annotations.searchEmpty.implementation = function (frameFrom, frameTo) { Job.prototype.annotations.searchEmpty.implementation = function (frameFrom, frameTo) {
if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) {
throw new ArgumentError('The start and end frames both must be an integer'); throw new ArgumentError('The start and end frames both must be an integer');
} }
@ -2176,117 +2171,118 @@ Job.prototype.annotations.searchEmpty.implementation = function (frameFrom, fram
const result = searchEmptyFrame(this, frameFrom, frameTo); const result = searchEmptyFrame(this, frameFrom, frameTo);
return result; return result;
}; };
Job.prototype.annotations.save.implementation = async function (onUpdate) { Job.prototype.annotations.save.implementation = async function (onUpdate) {
const result = await saveAnnotations(this, onUpdate); const result = await saveAnnotations(this, onUpdate);
return result; return result;
}; };
Job.prototype.annotations.merge.implementation = async function (objectStates) { Job.prototype.annotations.merge.implementation = async function (objectStates) {
const result = await mergeAnnotations(this, objectStates); const result = await mergeAnnotations(this, objectStates);
return result; return result;
}; };
Job.prototype.annotations.split.implementation = async function (objectState, frame) { Job.prototype.annotations.split.implementation = async function (objectState, frame) {
const result = await splitAnnotations(this, objectState, frame); const result = await splitAnnotations(this, objectState, frame);
return result; return result;
}; };
Job.prototype.annotations.group.implementation = async function (objectStates, reset) { Job.prototype.annotations.group.implementation = async function (objectStates, reset) {
const result = await groupAnnotations(this, objectStates, reset); const result = await groupAnnotations(this, objectStates, reset);
return result; return result;
}; };
Job.prototype.annotations.hasUnsavedChanges.implementation = function () { Job.prototype.annotations.hasUnsavedChanges.implementation = function () {
const result = hasUnsavedChanges(this); const result = hasUnsavedChanges(this);
return result; return result;
}; };
Job.prototype.annotations.clear.implementation = async function ( Job.prototype.annotations.clear.implementation = async function (
reload, startframe, endframe, delTrackKeyframesOnly, reload, startframe, endframe, delTrackKeyframesOnly,
) { ) {
const result = await clearAnnotations(this, reload, startframe, endframe, delTrackKeyframesOnly); const result = await clearAnnotations(this, reload, startframe, endframe, delTrackKeyframesOnly);
return result; return result;
}; };
Job.prototype.annotations.select.implementation = function (frame, x, y) { Job.prototype.annotations.select.implementation = function (frame, x, y) {
const result = selectObject(this, frame, x, y); const result = selectObject(this, frame, x, y);
return result; return result;
}; };
Job.prototype.annotations.statistics.implementation = function () { Job.prototype.annotations.statistics.implementation = function () {
const result = annotationsStatistics(this); const result = annotationsStatistics(this);
return result; return result;
}; };
Job.prototype.annotations.put.implementation = function (objectStates) { Job.prototype.annotations.put.implementation = function (objectStates) {
const result = putAnnotations(this, objectStates); const result = putAnnotations(this, objectStates);
return result; return result;
}; };
Job.prototype.annotations.upload.implementation = async function ( Job.prototype.annotations.upload.implementation = async function (
format: string, format: string,
useDefaultLocation: boolean, useDefaultLocation: boolean,
sourceStorage: Storage, sourceStorage: Storage,
file: File | string, file: File | string,
) { options?: { convMaskToPoly?: boolean },
const result = await importDataset(this, format, useDefaultLocation, sourceStorage, file); ) {
const result = await importDataset(this, format, useDefaultLocation, sourceStorage, file, options);
return result; return result;
}; };
Job.prototype.annotations.import.implementation = function (data) { Job.prototype.annotations.import.implementation = function (data) {
const result = importCollection(this, data); const result = importCollection(this, data);
return result; return result;
}; };
Job.prototype.annotations.export.implementation = function () { Job.prototype.annotations.export.implementation = function () {
const result = exportCollection(this); const result = exportCollection(this);
return result; return result;
}; };
Job.prototype.annotations.exportDataset.implementation = async function ( Job.prototype.annotations.exportDataset.implementation = async function (
format: string, format: string,
saveImages: boolean, saveImages: boolean,
useDefaultSettings: boolean, useDefaultSettings: boolean,
targetStorage: Storage, targetStorage: Storage,
customName?: string, customName?: string,
) { ) {
const result = await exportDataset(this, format, saveImages, useDefaultSettings, targetStorage, customName); const result = await exportDataset(this, format, saveImages, useDefaultSettings, targetStorage, customName);
return result; return result;
}; };
Job.prototype.actions.undo.implementation = async function (count) { Job.prototype.actions.undo.implementation = async function (count) {
const result = await undoActions(this, count); const result = await undoActions(this, count);
return result; return result;
}; };
Job.prototype.actions.redo.implementation = async function (count) { Job.prototype.actions.redo.implementation = async function (count) {
const result = await redoActions(this, count); const result = await redoActions(this, count);
return result; return result;
}; };
Job.prototype.actions.freeze.implementation = function (frozen) { Job.prototype.actions.freeze.implementation = function (frozen) {
const result = freezeHistory(this, frozen); const result = freezeHistory(this, frozen);
return result; return result;
}; };
Job.prototype.actions.clear.implementation = function () { Job.prototype.actions.clear.implementation = function () {
const result = clearActions(this); const result = clearActions(this);
return result; return result;
}; };
Job.prototype.actions.get.implementation = function () { Job.prototype.actions.get.implementation = function () {
const result = getActions(this); const result = getActions(this);
return result; return result;
}; };
Job.prototype.logger.log.implementation = async function (logType, payload, wait) { Job.prototype.logger.log.implementation = async function (logType, payload, wait) {
const result = await loggerStorage.log(logType, { ...payload, task_id: this.taskId, job_id: this.id }, wait); const result = await loggerStorage.log(logType, { ...payload, task_id: this.taskId, job_id: this.id }, wait);
return result; return result;
}; };
Job.prototype.predictor.status.implementation = async function () { Job.prototype.predictor.status.implementation = async function () {
if (!Number.isInteger(this.projectId)) { if (!Number.isInteger(this.projectId)) {
throw new DataError('The job must belong to a project to use the feature'); throw new DataError('The job must belong to a project to use the feature');
} }
@ -2300,9 +2296,9 @@ Job.prototype.predictor.status.implementation = async function () {
mediaAmount: result.media_amount, mediaAmount: result.media_amount,
annotationAmount: result.annotation_amount, annotationAmount: result.annotation_amount,
}; };
}; };
Job.prototype.predictor.predict.implementation = async function (frame) { Job.prototype.predictor.predict.implementation = async function (frame) {
if (!Number.isInteger(frame) || frame < 0) { if (!Number.isInteger(frame) || frame < 0) {
throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`);
} }
@ -2317,25 +2313,25 @@ Job.prototype.predictor.predict.implementation = async function (frame) {
const result = await serverProxy.predictor.predict(this.taskId, frame); const result = await serverProxy.predictor.predict(this.taskId, frame);
return result; return result;
}; };
Job.prototype.close.implementation = function closeTask() { Job.prototype.close.implementation = function closeTask() {
clearFrames(this.id); clearFrames(this.id);
closeSession(this); clearCache(this);
return this; return this;
}; };
Task.prototype.close.implementation = function closeTask() { Task.prototype.close.implementation = function closeTask() {
for (const job of this.jobs) { for (const job of this.jobs) {
clearFrames(job.id); clearFrames(job.id);
closeSession(job); clearCache(job);
} }
closeSession(this); clearCache(this);
return this; return this;
}; };
Task.prototype.save.implementation = async function (onUpdate) { Task.prototype.save.implementation = async function (onUpdate) {
// TODO: Add ability to change an owner and an assignee // TODO: Add ability to change an owner and an assignee
if (typeof this.id !== 'undefined') { if (typeof this.id !== 'undefined') {
// If the task has been already created, we update it // If the task has been already created, we update it
@ -2417,29 +2413,29 @@ Task.prototype.save.implementation = async function (onUpdate) {
const task = await serverProxy.tasks.create(taskSpec, taskDataSpec, onUpdate); const task = await serverProxy.tasks.create(taskSpec, taskDataSpec, onUpdate);
return new Task(task); return new Task(task);
}; };
Task.prototype.delete.implementation = async function () { Task.prototype.delete.implementation = async function () {
const result = await serverProxy.tasks.delete(this.id); const result = await serverProxy.tasks.delete(this.id);
return result; return result;
}; };
Task.prototype.backup.implementation = async function ( Task.prototype.backup.implementation = async function (
targetStorage: Storage, targetStorage: Storage,
useDefaultSettings: boolean, useDefaultSettings: boolean,
fileName?: string, fileName?: string,
) { ) {
const result = await serverProxy.tasks.backup(this.id, targetStorage, useDefaultSettings, fileName); const result = await serverProxy.tasks.backup(this.id, targetStorage, useDefaultSettings, fileName);
return result; return result;
}; };
Task.restore.implementation = async function (storage: Storage, file: File | string) { Task.restore.implementation = async function (storage: Storage, file: File | string) {
// eslint-disable-next-line no-unsanitized/method // eslint-disable-next-line no-unsanitized/method
const result = await serverProxy.tasks.restore(storage, file); const result = await serverProxy.tasks.restore(storage, file);
return result; return result;
}; };
Task.prototype.frames.get.implementation = async function (frame, isPlaying, step) { Task.prototype.frames.get.implementation = async function (frame, isPlaying, step) {
if (!Number.isInteger(frame) || frame < 0) { if (!Number.isInteger(frame) || frame < 0) {
throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`);
} }
@ -2462,9 +2458,9 @@ Task.prototype.frames.get.implementation = async function (frame, isPlaying, ste
step, step,
); );
return result; return result;
}; };
Task.prototype.frames.ranges.implementation = async function () { Task.prototype.frames.ranges.implementation = async function () {
const rangesData = { const rangesData = {
decoded: [], decoded: [],
buffered: [], buffered: [],
@ -2475,18 +2471,18 @@ Task.prototype.frames.ranges.implementation = async function () {
rangesData.buffered.push(buffered); rangesData.buffered.push(buffered);
} }
return rangesData; return rangesData;
}; };
Task.prototype.frames.preview.implementation = async function () { Task.prototype.frames.preview.implementation = async function () {
if (this.id === null) { if (this.id === null) {
return ''; return '';
} }
const frameData = await getPreview(this.id); const frameData = await getPreview(this.id);
return frameData; return frameData;
}; };
Task.prototype.frames.delete.implementation = async function (frame) { Task.prototype.frames.delete.implementation = async function (frame) {
if (!Number.isInteger(frame)) { if (!Number.isInteger(frame)) {
throw new Error(`Frame must be an integer. Got: "${frame}"`); throw new Error(`Frame must be an integer. Got: "${frame}"`);
} }
@ -2499,9 +2495,9 @@ Task.prototype.frames.delete.implementation = async function (frame) {
if (job) { if (job) {
await deleteFrameWrapper.call(this, job.id, frame); await deleteFrameWrapper.call(this, job.id, frame);
} }
}; };
Task.prototype.frames.restore.implementation = async function (frame) { Task.prototype.frames.restore.implementation = async function (frame) {
if (!Number.isInteger(frame)) { if (!Number.isInteger(frame)) {
throw new Error(`Frame must be an integer. Got: "${frame}"`); throw new Error(`Frame must be an integer. Got: "${frame}"`);
} }
@ -2514,13 +2510,13 @@ Task.prototype.frames.restore.implementation = async function (frame) {
if (job) { if (job) {
await restoreFrameWrapper.call(this, job.id, frame); await restoreFrameWrapper.call(this, job.id, frame);
} }
}; };
Task.prototype.frames.save.implementation = async function () { Task.prototype.frames.save.implementation = async function () {
return Promise.all(this.jobs.map((job) => patchMeta(job.id))); return Promise.all(this.jobs.map((job) => patchMeta(job.id)));
}; };
Task.prototype.frames.search.implementation = async function (filters, frameFrom, frameTo) { Task.prototype.frames.search.implementation = async function (filters, frameFrom, frameTo) {
if (typeof filters !== 'object') { if (typeof filters !== 'object') {
throw new ArgumentError('Filters should be an object'); throw new ArgumentError('Filters should be an object');
} }
@ -2554,10 +2550,10 @@ Task.prototype.frames.search.implementation = async function (filters, frameFrom
} }
return null; return null;
}; };
// TODO: Check filter for annotations // TODO: Check filter for annotations
Task.prototype.annotations.get.implementation = async function (frame, allTracks, filters) { Task.prototype.annotations.get.implementation = async function (frame, allTracks, filters) {
if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) { if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) {
throw new ArgumentError('The filters argument must be an array of strings'); throw new ArgumentError('The filters argument must be an array of strings');
} }
@ -2577,9 +2573,9 @@ Task.prototype.annotations.get.implementation = async function (frame, allTracks
} }
return result; return result;
}; };
Task.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) { Task.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) {
if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) { if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) {
throw new ArgumentError('The filters argument must be an array of strings'); throw new ArgumentError('The filters argument must be an array of strings');
} }
@ -2598,9 +2594,9 @@ Task.prototype.annotations.search.implementation = function (filters, frameFrom,
const result = searchAnnotations(this, filters, frameFrom, frameTo); const result = searchAnnotations(this, filters, frameFrom, frameTo);
return result; return result;
}; };
Task.prototype.annotations.searchEmpty.implementation = function (frameFrom, frameTo) { Task.prototype.annotations.searchEmpty.implementation = function (frameFrom, frameTo) {
if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) {
throw new ArgumentError('The start and end frames both must be an integer'); throw new ArgumentError('The start and end frames both must be an integer');
} }
@ -2615,115 +2611,116 @@ Task.prototype.annotations.searchEmpty.implementation = function (frameFrom, fra
const result = searchEmptyFrame(this, frameFrom, frameTo); const result = searchEmptyFrame(this, frameFrom, frameTo);
return result; return result;
}; };
Task.prototype.annotations.save.implementation = async function (onUpdate) { Task.prototype.annotations.save.implementation = async function (onUpdate) {
const result = await saveAnnotations(this, onUpdate); const result = await saveAnnotations(this, onUpdate);
return result; return result;
}; };
Task.prototype.annotations.merge.implementation = async function (objectStates) { Task.prototype.annotations.merge.implementation = async function (objectStates) {
const result = await mergeAnnotations(this, objectStates); const result = await mergeAnnotations(this, objectStates);
return result; return result;
}; };
Task.prototype.annotations.split.implementation = async function (objectState, frame) { Task.prototype.annotations.split.implementation = async function (objectState, frame) {
const result = await splitAnnotations(this, objectState, frame); const result = await splitAnnotations(this, objectState, frame);
return result; return result;
}; };
Task.prototype.annotations.group.implementation = async function (objectStates, reset) { Task.prototype.annotations.group.implementation = async function (objectStates, reset) {
const result = await groupAnnotations(this, objectStates, reset); const result = await groupAnnotations(this, objectStates, reset);
return result; return result;
}; };
Task.prototype.annotations.hasUnsavedChanges.implementation = function () { Task.prototype.annotations.hasUnsavedChanges.implementation = function () {
const result = hasUnsavedChanges(this); const result = hasUnsavedChanges(this);
return result; return result;
}; };
Task.prototype.annotations.clear.implementation = async function (reload) { Task.prototype.annotations.clear.implementation = async function (reload) {
const result = await clearAnnotations(this, reload); const result = await clearAnnotations(this, reload);
return result; return result;
}; };
Task.prototype.annotations.select.implementation = function (frame, x, y) { Task.prototype.annotations.select.implementation = function (frame, x, y) {
const result = selectObject(this, frame, x, y); const result = selectObject(this, frame, x, y);
return result; return result;
}; };
Task.prototype.annotations.statistics.implementation = function () { Task.prototype.annotations.statistics.implementation = function () {
const result = annotationsStatistics(this); const result = annotationsStatistics(this);
return result; return result;
}; };
Task.prototype.annotations.put.implementation = function (objectStates) { Task.prototype.annotations.put.implementation = function (objectStates) {
const result = putAnnotations(this, objectStates); const result = putAnnotations(this, objectStates);
return result; return result;
}; };
Task.prototype.annotations.upload.implementation = async function ( Task.prototype.annotations.upload.implementation = async function (
format: string, format: string,
useDefaultLocation: boolean, useDefaultLocation: boolean,
sourceStorage: Storage, sourceStorage: Storage,
file: File | string, file: File | string,
) { options?: { convMaskToPoly?: boolean },
const result = await importDataset(this, format, useDefaultLocation, sourceStorage, file); ) {
const result = await importDataset(this, format, useDefaultLocation, sourceStorage, file, options);
return result; return result;
}; };
Task.prototype.annotations.import.implementation = function (data) { Task.prototype.annotations.import.implementation = function (data) {
const result = importCollection(this, data); const result = importCollection(this, data);
return result; return result;
}; };
Task.prototype.annotations.export.implementation = function () { Task.prototype.annotations.export.implementation = function () {
const result = exportCollection(this); const result = exportCollection(this);
return result; return result;
}; };
Task.prototype.annotations.exportDataset.implementation = async function ( Task.prototype.annotations.exportDataset.implementation = async function (
format: string, format: string,
saveImages: boolean, saveImages: boolean,
useDefaultSettings: boolean, useDefaultSettings: boolean,
targetStorage: Storage, targetStorage: Storage,
customName?: string, customName?: string,
) { ) {
const result = await exportDataset(this, format, saveImages, useDefaultSettings, targetStorage, customName); const result = await exportDataset(this, format, saveImages, useDefaultSettings, targetStorage, customName);
return result; return result;
}; };
Task.prototype.actions.undo.implementation = function (count) { Task.prototype.actions.undo.implementation = function (count) {
const result = undoActions(this, count); const result = undoActions(this, count);
return result; return result;
}; };
Task.prototype.actions.redo.implementation = function (count) { Task.prototype.actions.redo.implementation = function (count) {
const result = redoActions(this, count); const result = redoActions(this, count);
return result; return result;
}; };
Task.prototype.actions.freeze.implementation = function (frozen) { Task.prototype.actions.freeze.implementation = function (frozen) {
const result = freezeHistory(this, frozen); const result = freezeHistory(this, frozen);
return result; return result;
}; };
Task.prototype.actions.clear.implementation = function () { Task.prototype.actions.clear.implementation = function () {
const result = clearActions(this); const result = clearActions(this);
return result; return result;
}; };
Task.prototype.actions.get.implementation = function () { Task.prototype.actions.get.implementation = function () {
const result = getActions(this); const result = getActions(this);
return result; return result;
}; };
Task.prototype.logger.log.implementation = async function (logType, payload, wait) { Task.prototype.logger.log.implementation = async function (logType, payload, wait) {
const result = await loggerStorage.log(logType, { ...payload, task_id: this.id }, wait); const result = await loggerStorage.log(logType, { ...payload, task_id: this.id }, wait);
return result; return result;
}; };
Task.prototype.predictor.status.implementation = async function () { Task.prototype.predictor.status.implementation = async function () {
if (!Number.isInteger(this.projectId)) { if (!Number.isInteger(this.projectId)) {
throw new DataError('The task must belong to a project to use the feature'); throw new DataError('The task must belong to a project to use the feature');
} }
@ -2737,9 +2734,9 @@ Task.prototype.predictor.status.implementation = async function () {
mediaAmount: result.media_amount, mediaAmount: result.media_amount,
annotationAmount: result.annotation_amount, annotationAmount: result.annotation_amount,
}; };
}; };
Task.prototype.predictor.predict.implementation = async function (frame) { Task.prototype.predictor.predict.implementation = async function (frame) {
if (!Number.isInteger(frame) || frame < 0) { if (!Number.isInteger(frame) || frame < 0) {
throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`);
} }
@ -2754,4 +2751,6 @@ Task.prototype.predictor.predict.implementation = async function (frame) {
const result = await serverProxy.predictor.predict(this.id, frame); const result = await serverProxy.predictor.predict(this.id, frame);
return result; return result;
}; };
})();

@ -1,116 +1,44 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
(() => { interface ObjectStatistics {
/** track: number;
* Class representing collection statistics shape: number;
* @memberof module:API.cvat.classes }
* @hideconstructor
*/ interface StatisticsBody {
class Statistics { rectangle: ObjectStatistics;
constructor(label, total) { polygon: ObjectStatistics;
Object.defineProperties( polyline: ObjectStatistics;
this, points: ObjectStatistics;
Object.freeze({ ellipse: ObjectStatistics;
/** cuboid: ObjectStatistics;
* Statistics collected by labels, has the following structure: skeleton: ObjectStatistics;
* @example mask: {
* { shape: number;
* label: { };
* rectangle: { tag: number;
* track: 10, manually: number;
* shape: 11, interpolated: number;
* }, total: number;
* polygon: { }
* track: 13,
* shape: 14, export default class Statistics {
* }, private labelData: Record<string, StatisticsBody>;
* polyline: { private totalData: StatisticsBody;
* track: 16,
* shape: 17, constructor(label: Statistics['labelData'], total: Statistics['totalData']) {
* }, this.labelData = label;
* points: { this.totalData = total;
* track: 19,
* shape: 20,
* },
* ellipse: {
* track: 13,
* shape: 15,
* },
* cuboid: {
* track: 21,
* shape: 22,
* },
* skeleton: {
* track: 21,
* shape: 22,
* },
* tag: 66,
* manually: 207,
* interpolated: 500,
* total: 630,
* }
* }
* @name label
* @type {Object}
* @memberof module:API.cvat.classes.Statistics
* @readonly
* @instance
*/
label: {
get: () => JSON.parse(JSON.stringify(label)),
},
/**
* Total objects statistics (within all the labels), has the following structure:
* @example
* {
* rectangle: {
* tracks: 10,
* shapes: 11,
* },
* polygon: {
* tracks: 13,
* shapes: 14,
* },
* polyline: {
* tracks: 16,
* shapes: 17,
* },
* point: {
* tracks: 19,
* shapes: 20,
* },
* ellipse: {
* tracks: 13,
* shapes: 15,
* },
* cuboid: {
* tracks: 21,
* shapes: 22,
* },
* skeleton: {
* tracks: 21,
* shapes: 22,
* },
* tag: 66,
* manually: 186,
* interpolated: 500,
* total: 608,
* }
* @name total
* @type {Object}
* @memberof module:API.cvat.classes.Statistics
* @readonly
* @instance
*/
total: {
get: () => JSON.parse(JSON.stringify(total)),
},
}),
);
} }
public get label(): Record<string, StatisticsBody> {
return JSON.parse(JSON.stringify(this.labelData));
} }
module.exports = Statistics; public get total(): StatisticsBody {
})(); return JSON.parse(JSON.stringify(this.totalData));
}
}

@ -1,6 +1,6 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.42.5", "version": "1.43.0",
"description": "CVAT single-page application", "description": "CVAT single-page application",
"main": "src/index.tsx", "main": "src/index.tsx",
"scripts": { "scripts": {

@ -208,6 +208,7 @@ export enum AnnotationActionTypes {
RESTORE_FRAME = 'RESTORE_FRAME', RESTORE_FRAME = 'RESTORE_FRAME',
RESTORE_FRAME_SUCCESS = 'RESTORE_FRAME_SUCCESS', RESTORE_FRAME_SUCCESS = 'RESTORE_FRAME_SUCCESS',
RESTORE_FRAME_FAILED = 'RESTORE_FRAME_FAILED', RESTORE_FRAME_FAILED = 'RESTORE_FRAME_FAILED',
UPDATE_BRUSH_TOOLS_CONFIG = 'UPDATE_BRUSH_TOOLS_CONFIG',
} }
export function saveLogsAsync(): ThunkAction { export function saveLogsAsync(): ThunkAction {
@ -319,6 +320,15 @@ export function updateCanvasContextMenu(
}; };
} }
export function updateCanvasBrushTools(config: {
visible?: boolean, left?: number, top?: number
}): AnyAction {
return {
type: AnnotationActionTypes.UPDATE_BRUSH_TOOLS_CONFIG,
payload: config,
};
}
export function removeAnnotationsAsync( export function removeAnnotationsAsync(
startFrame: number, endFrame: number, delTrackKeyframesOnly: boolean, startFrame: number, endFrame: number, delTrackKeyframesOnly: boolean,
): ThunkAction { ): ThunkAction {
@ -1212,8 +1222,9 @@ export function updateAnnotationsAsync(statesToUpdate: any[]): ThunkAction {
const promises = statesToUpdate.map((objectState: any): Promise<any> => objectState.save()); const promises = statesToUpdate.map((objectState: any): Promise<any> => objectState.save());
const states = await Promise.all(promises); const states = await Promise.all(promises);
const withSkeletonElements = states.some((state: any) => state.parentID !== null); const needToUpdateAll = states
if (withSkeletonElements) { .some((state: any) => [ShapeType.MASK, ShapeType.SKELETON].includes(state.shapeType));
if (needToUpdateAll) {
dispatch(fetchAnnotationsAsync()); dispatch(fetchAnnotationsAsync());
return; return;
} }
@ -1458,6 +1469,7 @@ const ShapeTypeToControl: Record<ShapeType, ActiveControl> = {
[ShapeType.CUBOID]: ActiveControl.DRAW_CUBOID, [ShapeType.CUBOID]: ActiveControl.DRAW_CUBOID,
[ShapeType.ELLIPSE]: ActiveControl.DRAW_ELLIPSE, [ShapeType.ELLIPSE]: ActiveControl.DRAW_ELLIPSE,
[ShapeType.SKELETON]: ActiveControl.DRAW_SKELETON, [ShapeType.SKELETON]: ActiveControl.DRAW_SKELETON,
[ShapeType.MASK]: ActiveControl.DRAW_MASK,
}; };
export function pasteShapeAsync(): ThunkAction { export function pasteShapeAsync(): ThunkAction {

@ -70,6 +70,7 @@ export const importDatasetAsync = (
useDefaultSettings: boolean, useDefaultSettings: boolean,
sourceStorage: Storage, sourceStorage: Storage,
file: File | string, file: File | string,
convMaskToPoly: boolean,
): ThunkAction => ( ): ThunkAction => (
async (dispatch, getState) => { async (dispatch, getState) => {
const resource = instance instanceof core.classes.Project ? 'dataset' : 'annotation'; const resource = instance instanceof core.classes.Project ? 'dataset' : 'annotation';
@ -83,18 +84,20 @@ export const importDatasetAsync = (
} }
dispatch(importActions.importDataset(instance, format)); dispatch(importActions.importDataset(instance, format));
await instance.annotations await instance.annotations
.importDataset(format, useDefaultSettings, sourceStorage, file, .importDataset(format, useDefaultSettings, sourceStorage, file, {
(message: string, progress: number) => ( convMaskToPoly,
updateStatusCallback: (message: string, progress: number) => (
dispatch(importActions.importDatasetUpdateStatus( dispatch(importActions.importDatasetUpdateStatus(
instance, Math.floor(progress * 100), message, instance, Math.floor(progress * 100), message,
)) ))
)); ),
});
} else if (instance instanceof core.classes.Task) { } else if (instance instanceof core.classes.Task) {
if (state.import.tasks.dataset.current?.[instance.id]) { if (state.import.tasks.dataset.current?.[instance.id]) {
throw Error('Only one importing of annotation/dataset allowed at the same time'); throw Error('Only one importing of annotation/dataset allowed at the same time');
} }
dispatch(importActions.importDataset(instance, format)); dispatch(importActions.importDataset(instance, format));
await instance.annotations.upload(format, useDefaultSettings, sourceStorage, file); await instance.annotations.upload(format, useDefaultSettings, sourceStorage, file, { convMaskToPoly });
} else { // job } else { // job
if (state.import.tasks.dataset.current?.[instance.taskId]) { if (state.import.tasks.dataset.current?.[instance.taskId]) {
throw Error('Annotations is being uploaded for the task'); throw Error('Annotations is being uploaded for the task');
@ -107,7 +110,7 @@ export const importDatasetAsync = (
dispatch(importActions.importDataset(instance, format)); dispatch(importActions.importDataset(instance, format));
const frame = state.annotation.player.frame.number; const frame = state.annotation.player.frame.number;
await instance.annotations.upload(format, useDefaultSettings, sourceStorage, file); await instance.annotations.upload(format, useDefaultSettings, sourceStorage, file, { convMaskToPoly });
await instance.logger.log(LogType.uploadAnnotations, { await instance.logger.log(LogType.uploadAnnotations, {
...(await jobInfoGenerator(instance)), ...(await jobInfoGenerator(instance)),

@ -0,0 +1,3 @@
<svg width="40px" height="40px" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.248 4.15283L12.6799 12.9155C12.0119 13.0658 11.3672 13.333 10.85 13.8502L10.8324 13.8663L10.8149 13.8854C10.0755 14.7293 9.82742 15.8244 9.42596 16.9781C9.05378 18.0478 8.55224 19.1753 7.68984 20.2526H2.25V21.7528H8.83553V21.6986C12.8813 21.5609 14.9145 20.4462 16.1565 19.1494C17.0676 18.2357 17.3401 16.9703 17.1088 15.7856L22.4813 5.60033L21.1539 4.90002L18.0626 10.7633L16.0803 9.63955L18.5783 4.84728L17.248 4.15283ZM15.3859 10.9713L17.3623 12.0906L16.3104 14.0861C16.2475 14.0125 16.2231 13.9197 16.1536 13.8502C15.648 13.3445 15.0192 13.0793 14.3677 12.9243L15.3859 10.9713ZM13.5018 14.2472C14.0762 14.2472 14.6504 14.4684 15.0929 14.9109C15.9779 15.7959 15.9779 17.2081 15.0929 18.0931L15.087 18.0989L15.0812 18.1048C14.1191 19.112 12.7313 19.9498 9.54023 20.1618C10.1124 19.2227 10.557 18.2913 10.8427 17.4704C11.2594 16.2728 11.5636 15.3363 11.9327 14.9021C12.3727 14.4717 12.9353 14.2472 13.5018 14.2472Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -0,0 +1,3 @@
<svg width="24px" height="25px" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.2109 5.46094L8.25 18.4219L2.78906 12.9609L1.71094 14.0391L7.71094 20.0391L8.25 20.5547L8.78906 20.0391L22.2891 6.53906L21.2109 5.46094Z" fill="black" />
</svg>

After

Width:  |  Height:  |  Size: 275 B

@ -0,0 +1,3 @@
<svg width="40px" height="40px" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.1797 3.82031C13.5762 3.82031 12.9492 4.02539 12.4687 4.45312V4.47656L12.4453 4.5L3.67969 13.1953C2.77148 14.1035 2.77734 15.583 3.63281 16.5469L3.65625 16.5703H3.67969L8.17968 21.0703C8.63379 21.5244 9.24023 21.7441 9.84375 21.75H21V20.25H12.375L20.25 12.375C21.1992 11.4258 21.2285 9.9082 20.3203 9L15.8203 4.5C15.3662 4.0459 14.7832 3.82031 14.1797 3.82031ZM14.1562 5.34375C14.3965 5.34375 14.6191 5.4082 14.7656 5.55469L19.2656 10.0547C19.5557 10.3447 19.5967 10.9189 19.1953 11.3203L15.4219 15.0938L9.67968 9.35156L13.4766 5.57812L13.5 5.55469C13.6904 5.39648 13.9277 5.34375 14.1562 5.34375ZM8.60156 10.4297L14.3437 16.1719L10.5234 19.9922C10.5146 19.998 10.5088 20.0098 10.5 20.0156C10.1133 20.3379 9.51855 20.2998 9.23437 20.0156L4.75781 15.5625C4.74609 15.5479 4.74609 15.5303 4.73437 15.5156C4.42676 15.1289 4.45312 14.5312 4.73437 14.25L8.60156 10.4297Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1000 B

@ -1 +1,3 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M36.918 19.593a1.05 1.05 0 0 0-.231-.346l-3.633-3.633a1.064 1.064 0 1 0-1.505 1.505l1.817 1.816H21.062l.001-12.301L22.96 8.53a1.062 1.062 0 0 0 1.505 0 1.064 1.064 0 0 0 0-1.505L20.752 3.31a1.063 1.063 0 0 0-1.16-.23c-.13.054-.246.13-.343.228l-.002.001-3.636 3.634a1.066 1.066 0 0 0 .753 1.818c.272 0 .545-.105.753-.312l1.818-1.817-.001 12.302H6.635l1.816-1.816a1.065 1.065 0 0 0-1.506-1.505l-3.633 3.634a1.059 1.059 0 0 0 0 1.505l3.713 3.713a1.059 1.059 0 0 0 1.506 0 1.065 1.065 0 0 0 0-1.505l-1.898-1.897h12.3v12.3l-1.816-1.815a1.066 1.066 0 0 0-1.506 1.506l3.636 3.633a1.059 1.059 0 0 0 1.504 0l3.714-3.714a1.064 1.064 0 1 0-1.506-1.505l-1.897 1.898V21.064h12.304l-1.896 1.897a1.065 1.065 0 0 0 1.505 1.505l3.713-3.713c.002-.003.003-.007.007-.01a1.064 1.064 0 0 0 .223-1.15z" fill="#000" fill-rule="evenodd"/></svg> <svg width="40px" height="40px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 1.93945L8.46973 5.46973L9.53027 6.53027L11.25 4.81055V9H12.75V4.81055L14.4697 6.53027L15.5303 5.46973L12 1.93945ZM5.46973 8.46973L1.93945 12L5.46973 15.5303L6.53027 14.4697L4.81055 12.75H9.75V11.25H4.81055L6.53027 9.53027L5.46973 8.46973ZM18.5303 8.46973L17.4697 9.53027L19.1895 11.25H14.25V12.75H19.1895L17.4697 14.4697L18.5303 15.5303L22.0605 12L18.5303 8.46973ZM11.25 14.25V19.1895L9.53027 17.4697L8.46973 18.5303L12 22.0605L15.5303 18.5303L14.4697 17.4697L12.75 19.1895V14.25H11.25Z" fill="black" />
</svg>

Before

Width:  |  Height:  |  Size: 891 B

After

Width:  |  Height:  |  Size: 626 B

@ -4,7 +4,7 @@
The file has been modified The file has been modified
--> -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="40" height="40"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="40" height="40">
<g style="transform: scale(0.078)"> <g style="transform: scale(0.078)" stroke-width="0.5">
<path d="M148.6458282,81.0641403C191.8570709-0.3458452,307.612915-4.617764,356.5062561,73.3931732c37.8880615,60.4514771,13.7960815,135.4847717-41.8233948,167.7876129l-36.121521-62.5643005c22.1270447-12.8510284,31.7114563-42.7013397,16.6385498-66.750618c-19.4511414-31.034935-65.5021057-29.3354645-82.692749,3.0517044c-12.7206879,23.9658356-2.6391449,51.5502472,18.3088379,63.7294922l-36.1482544,62.6105804C142.0118256,210.643219,116.6704254,141.3057709,148.6458282,81.0641403z M167.9667206,374.4708557c-0.0435791,24.2778625-18.934967,46.8978271-46.092804,47.9000549c-36.6418304,1.3522339-61.0877724-37.6520386-43.8971252-70.0392151c13.2918015-25.0418091,43.8297424-31.7192383,65.9928284-19.1222839l36.2165222-62.7288513c-55.7241974-31.7991638-132.6246796-15.0146027-166.0706635,47.9976501c-43.2111893,81.4099731,18.2372913,179.4530945,110.3418884,176.0540161c68.1375427-2.5146179,115.5750122-59.1652527,115.8612366-120.0613708H167.9667206z M451.714386,270.7571411l-36.1215515,62.5642395c22.2027588,12.816864,31.8418274,42.7249451,16.744751,66.8127441c-19.4511414,31.0349426-65.5021057,29.3354797-82.692688-3.0516968c-12.742218-24.0063782-2.6048279-51.643219,18.4154358-63.7908325l-36.1482544-62.6105652c-52.7280579,30.5827942-78.1254272,99.9726562-46.128479,160.2548218c43.2111816,81.4099731,158.9670105,85.6818848,207.8603821,7.6710205C531.5561523,378.1168213,507.4096069,303.0259705,451.714386,270.7571411z"/> <path d="M148.6458282,81.0641403C191.8570709-0.3458452,307.612915-4.617764,356.5062561,73.3931732c37.8880615,60.4514771,13.7960815,135.4847717-41.8233948,167.7876129l-36.121521-62.5643005c22.1270447-12.8510284,31.7114563-42.7013397,16.6385498-66.750618c-19.4511414-31.034935-65.5021057-29.3354645-82.692749,3.0517044c-12.7206879,23.9658356-2.6391449,51.5502472,18.3088379,63.7294922l-36.1482544,62.6105804C142.0118256,210.643219,116.6704254,141.3057709,148.6458282,81.0641403z M167.9667206,374.4708557c-0.0435791,24.2778625-18.934967,46.8978271-46.092804,47.9000549c-36.6418304,1.3522339-61.0877724-37.6520386-43.8971252-70.0392151c13.2918015-25.0418091,43.8297424-31.7192383,65.9928284-19.1222839l36.2165222-62.7288513c-55.7241974-31.7991638-132.6246796-15.0146027-166.0706635,47.9976501c-43.2111893,81.4099731,18.2372913,179.4530945,110.3418884,176.0540161c68.1375427-2.5146179,115.5750122-59.1652527,115.8612366-120.0613708H167.9667206z M451.714386,270.7571411l-36.1215515,62.5642395c22.2027588,12.816864,31.8418274,42.7249451,16.744751,66.8127441c-19.4511414,31.0349426-65.5021057,29.3354797-82.692688-3.0516968c-12.742218-24.0063782-2.6048279-51.643219,18.4154358-63.7908325l-36.1482544-62.6105652c-52.7280579,30.5827942-78.1254272,99.9726562-46.128479,160.2548218c43.2111816,81.4099731,158.9670105,85.6818848,207.8603821,7.6710205C531.5561523,378.1168213,507.4096069,303.0259705,451.714386,270.7571411z"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -0,0 +1,3 @@
<svg width="24px" height="25px" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.25 4.5V12H3.75V13.5H11.25V21H12.75V13.5H20.25V12H12.75V4.5H11.25Z" fill="black" />
</svg>

After

Width:  |  Height:  |  Size: 205 B

@ -0,0 +1,11 @@
<svg width="24px" height="25px" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_402_12038)">
<path d="M18 1C14.6916 1 12 3.6916 12 7C12 10.3084 14.6916 13 18 13C21.3084 13 24 10.3084 24 7C24 3.6916 21.3084 1 18 1ZM20.55 7.45C17.5517 7.45 18.7266 7.45 15.45 7.45C15.2013 7.45 15 7.2487 15 7C15 6.7513 15.2013 6.55 15.45 6.55C18.7266 6.55 17.5517 6.55 20.55 6.55C20.7987 6.55 21 6.7513 21 7C21 7.2487 20.7987 7.45 20.55 7.45Z" fill="black" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.37 4.74853L9.83243 3.63143C9.7201 3.54995 9.5858 3.50425 9.44709 3.50028C9.29488 3.49591 9.1455 3.54202 9.02223 3.63143L0.284204 9.98015C0.166471 10.0657 0.0788289 10.1864 0.033816 10.3248C-0.0111969 10.4632 -0.0112737 10.6124 0.0335966 10.7508L3.37054 21.023C3.41554 21.1617 3.50333 21.2826 3.62129 21.3683C3.73926 21.454 3.88134 21.5001 4.02715 21.5H14.8275C14.9733 21.5001 15.1154 21.454 15.2334 21.3683C15.3513 21.2826 15.4391 21.1617 15.4841 21.023L17.7668 13.9962C17.2897 13.9806 16.8247 13.9172 16.3764 13.8107L14.3263 20.1203H4.52836L1.50041 10.8011L9.42733 5.04255L11.0434 6.21653C11.0999 5.70882 11.2107 5.21751 11.37 4.74853Z" fill="black" />
</g>
<defs>
<clipPath id="clip0_402_12038">
<rect width="24" height="24" fill="white" transform="translate(0 0.75)" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -0,0 +1,11 @@
<svg width="24px" height="25px" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_402_12031)">
<path d="M18 1C14.6916 1 12 3.6916 12 7C12 10.3084 14.6916 13 18 13C21.3084 13 24 10.3084 24 7C24 3.6916 21.3084 1 18 1ZM20.55 7.45H18.45V9.55C18.45 9.7987 18.2487 10 18 10C17.7513 10 17.55 9.7987 17.55 9.55V7.45H15.45C15.2013 7.45 15 7.2487 15 7C15 6.7513 15.2013 6.55 15.45 6.55H17.55V4.45C17.55 4.2013 17.7513 4 18 4C18.2487 4 18.45 4.2013 18.45 4.45V6.55H20.55C20.7987 6.55 21 6.7513 21 7C21 7.2487 20.7987 7.45 20.55 7.45Z" fill="black" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.37 4.74853L9.83243 3.63143C9.7201 3.54995 9.5858 3.50425 9.44709 3.50028C9.29488 3.49591 9.1455 3.54202 9.02223 3.63143L0.284204 9.98015C0.166471 10.0657 0.0788289 10.1864 0.033816 10.3248C-0.0111969 10.4632 -0.0112737 10.6124 0.0335966 10.7508L3.37054 21.023C3.41554 21.1617 3.50333 21.2826 3.62129 21.3683C3.73926 21.454 3.88134 21.5001 4.02715 21.5H14.8275C14.9733 21.5001 15.1154 21.454 15.2334 21.3683C15.3513 21.2826 15.4391 21.1617 15.4841 21.023L17.7668 13.9962C17.2897 13.9806 16.8247 13.9172 16.3764 13.8107L14.3263 20.1203H4.52836L1.50041 10.8011L9.42733 5.04255L11.0434 6.21653C11.0999 5.70882 11.2107 5.21751 11.37 4.74853Z" fill="black" />
</g>
<defs>
<clipPath id="clip0_402_12031">
<rect width="24" height="24" fill="white" transform="translate(0 0.75)" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@ -0,0 +1,57 @@
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@import '../../../base.scss';
.cvat-brush-tools-toolbox {
position: absolute;
margin: $grid-unit-size;
padding: 0 $grid-unit-size;
border-radius: 4px;
background: $background-color-2;
display: flex;
align-items: center;
z-index: 100;
box-shadow: $box-shadow-base;
> hr {
width: 1px;
height: $grid-unit-size * 4;
background: $border-color-1;
}
> * {
margin-right: $grid-unit-size;
}
> button {
font-size: 20px;
> span.anticon {
font-size: inherit;
}
}
.cvat-brush-tools-brush,
.cvat-brush-tools-eraser {
svg {
width: 24px;
height: 25px;
}
}
.cvat-brush-tools-draggable-area {
display: flex;
font-size: 20px;
svg {
width: 24px;
height: 25px;
}
}
.cvat-brush-tools-active-tool {
background: $header-color;
}
}

@ -0,0 +1,275 @@
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import './brush-toolbox-styles.scss';
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import { useDispatch, useSelector } from 'react-redux';
import Button from 'antd/lib/button';
import Icon, { VerticalAlignBottomOutlined } from '@ant-design/icons';
import InputNumber from 'antd/lib/input-number';
import Select from 'antd/lib/select';
import { getCore } from 'cvat-core-wrapper';
import { Canvas, CanvasMode } from 'cvat-canvas-wrapper';
import {
BrushIcon, EraserIcon, PolygonMinusIcon, PolygonPlusIcon,
PlusIcon, CheckIcon, MoveIcon,
} from 'icons';
import CVATTooltip from 'components/common/cvat-tooltip';
import { CombinedState, ObjectType, ShapeType } from 'reducers';
import LabelSelector from 'components/label-selector/label-selector';
import { rememberObject, updateCanvasBrushTools } from 'actions/annotation-actions';
import useDraggable from './draggable-hoc';
const DraggableArea = (
<div className='cvat-brush-tools-draggable-area'>
<Icon component={MoveIcon} />
</div>
);
const MIN_BRUSH_SIZE = 1;
function BrushTools(): React.ReactPortal | null {
const dispatch = useDispatch();
const defaultLabelID = useSelector((state: CombinedState) => state.annotation.drawing.activeLabelID);
const config = useSelector((state: CombinedState) => state.annotation.canvas.brushTools);
const canvasInstance = useSelector((state: CombinedState) => state.annotation.canvas.instance);
const labels = useSelector((state: CombinedState) => state.annotation.job.labels);
const { visible } = config;
const [editableState, setEditableState] = useState<any | null>(null);
const [currentTool, setCurrentTool] = useState<'brush' | 'eraser' | 'polygon-plus' | 'polygon-minus'>('brush');
const [brushForm, setBrushForm] = useState<'circle' | 'square'>('circle');
const [[top, left], setTopLeft] = useState([0, 0]);
const [brushSize, setBrushSize] = useState(10);
const [removeUnderlyingPixels, setRemoveUnderlyingPixels] = useState(false);
const dragBar = useDraggable(
(): number[] => {
const [element] = window.document.getElementsByClassName('cvat-brush-tools-toolbox');
if (element) {
const { offsetTop, offsetLeft } = element as HTMLDivElement;
return [offsetTop, offsetLeft];
}
return [0, 0];
},
(newTop, newLeft) => setTopLeft([newTop, newLeft]),
DraggableArea,
);
useEffect(() => {
const label = labels.find((_label: any) => _label.id === defaultLabelID);
getCore().config.removeUnderlyingMaskPixels = removeUnderlyingPixels;
if (visible && label && canvasInstance instanceof Canvas) {
const onUpdateConfiguration = ({ brushTool }: any): void => {
if (brushTool?.size) {
setBrushSize(Math.max(MIN_BRUSH_SIZE, brushTool.size));
}
};
if (canvasInstance.mode() === CanvasMode.DRAW) {
canvasInstance.draw({
enabled: true,
shapeType: ShapeType.MASK,
crosshair: false,
brushTool: {
type: currentTool,
size: brushSize,
form: brushForm,
color: label.color,
},
onUpdateConfiguration,
});
} else if (canvasInstance.mode() === CanvasMode.EDIT && editableState) {
canvasInstance.edit({
enabled: true,
state: editableState,
brushTool: {
type: currentTool,
size: brushSize,
form: brushForm,
color: label.color,
},
onUpdateConfiguration,
});
}
}
}, [currentTool, brushSize, brushForm, visible, defaultLabelID, editableState]);
useEffect(() => {
const canvasContainer = window.document.getElementsByClassName('cvat-canvas-container')[0];
if (canvasContainer) {
const { offsetTop, offsetLeft } = canvasContainer.parentElement as HTMLElement;
setTopLeft([offsetTop, offsetLeft]);
}
}, []);
useEffect(() => {
const hideToolset = (): void => {
if (visible) {
dispatch(updateCanvasBrushTools({ visible: false }));
}
};
const showToolset = (e: Event): void => {
const evt = e as CustomEvent;
if (evt.detail?.state?.shapeType === ShapeType.MASK ||
(evt.detail?.drawData?.shapeType === ShapeType.MASK && !evt.detail?.drawData?.initialState)) {
dispatch(updateCanvasBrushTools({ visible: true }));
}
};
const updateEditableState = (e: Event): void => {
const evt = e as CustomEvent;
if (evt.type === 'canvas.editstart' && evt.detail.state) {
setEditableState(evt.detail.state);
} else if (editableState) {
setEditableState(null);
}
};
if (canvasInstance instanceof Canvas) {
canvasInstance.html().addEventListener('canvas.drawn', hideToolset);
canvasInstance.html().addEventListener('canvas.canceled', hideToolset);
canvasInstance.html().addEventListener('canvas.canceled', updateEditableState);
canvasInstance.html().addEventListener('canvas.drawstart', showToolset);
canvasInstance.html().addEventListener('canvas.editstart', showToolset);
canvasInstance.html().addEventListener('canvas.editstart', updateEditableState);
canvasInstance.html().addEventListener('canvas.editdone', updateEditableState);
}
return () => {
if (canvasInstance instanceof Canvas) {
canvasInstance.html().removeEventListener('canvas.drawn', hideToolset);
canvasInstance.html().removeEventListener('canvas.canceled', hideToolset);
canvasInstance.html().removeEventListener('canvas.canceled', updateEditableState);
canvasInstance.html().removeEventListener('canvas.drawstart', showToolset);
canvasInstance.html().removeEventListener('canvas.editstart', showToolset);
canvasInstance.html().removeEventListener('canvas.editstart', updateEditableState);
canvasInstance.html().removeEventListener('canvas.editdone', updateEditableState);
}
};
}, [visible, editableState]);
if (!labels.length) {
return null;
}
return ReactDOM.createPortal((
<div className='cvat-brush-tools-toolbox' style={{ top, left, display: visible ? '' : 'none' }}>
<Button
type='text'
className='cvat-brush-tools-finish'
icon={<Icon component={CheckIcon} />}
onClick={() => {
if (canvasInstance instanceof Canvas) {
if (editableState) {
canvasInstance.edit({ enabled: false });
} else {
canvasInstance.draw({ enabled: false });
}
}
}}
/>
{!editableState && (
<Button
type='text'
disabled={!!editableState}
className='cvat-brush-tools-continue'
icon={<Icon component={PlusIcon} />}
onClick={() => {
if (canvasInstance instanceof Canvas) {
canvasInstance.draw({ enabled: false, continue: true });
dispatch(
rememberObject({
activeObjectType: ObjectType.SHAPE,
activeShapeType: ShapeType.MASK,
activeLabelID: defaultLabelID,
}),
);
}
}}
/>
)}
<hr />
<Button
type='text'
className={['cvat-brush-tools-brush', ...(currentTool === 'brush' ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
icon={<Icon component={BrushIcon} />}
onClick={() => setCurrentTool('brush')}
/>
<Button
type='text'
className={['cvat-brush-tools-eraser', ...(currentTool === 'eraser' ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
icon={<Icon component={EraserIcon} />}
onClick={() => setCurrentTool('eraser')}
/>
<Button
type='text'
className={['cvat-brush-tools-polygon-plus', ...(currentTool === 'polygon-plus' ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
icon={<Icon component={PolygonPlusIcon} />}
onClick={() => setCurrentTool('polygon-plus')}
/>
<Button
type='text'
className={['cvat-brush-tools-polygon-minus', ...(currentTool === 'polygon-minus' ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
icon={<Icon component={PolygonMinusIcon} />}
onClick={() => setCurrentTool('polygon-minus')}
/>
{ ['brush', 'eraser'].includes(currentTool) ? (
<CVATTooltip title='Brush size [Hold Alt + Right Mouse Click + Drag Left/Right]'>
<InputNumber
className='cvat-brush-tools-brush-size'
value={brushSize}
min={MIN_BRUSH_SIZE}
formatter={(val: number | undefined) => {
if (val) return `${val}px`;
return '';
}}
parser={(val: string | undefined): number => {
if (val) return +val.replace('px', '');
return 0;
}}
onChange={(value: number) => {
if (Number.isInteger(value) && value >= MIN_BRUSH_SIZE) {
setBrushSize(value);
}
}}
/>
</CVATTooltip>
) : null}
{ ['brush', 'eraser'].includes(currentTool) ? (
<Select value={brushForm} onChange={(value: 'circle' | 'square') => setBrushForm(value)}>
<Select.Option value='circle'>Circle</Select.Option>
<Select.Option value='square'>Square</Select.Option>
</Select>
) : null}
<Button
type='text'
className={['cvat-brush-tools-underlying-pixels', ...(removeUnderlyingPixels ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
icon={<VerticalAlignBottomOutlined />}
onClick={() => setRemoveUnderlyingPixels(!removeUnderlyingPixels)}
/>
{ !editableState && (
<LabelSelector
labels={labels}
value={defaultLabelID}
onChange={({ id: labelID }: { id: number }) => {
if (Number.isInteger(labelID)) {
dispatch(
rememberObject({ activeLabelID: labelID }),
);
}
}}
/>
)}
{ dragBar }
</div>
), window.document.body);
}
export default React.memo(BrushTools);

@ -11,7 +11,7 @@ import { MenuInfo } from 'rc-menu/lib/interface';
import ObjectItemElementComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item-element'; import ObjectItemElementComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item-element';
import ObjectItemContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-item'; import ObjectItemContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-item';
import { Workspace } from 'reducers'; import { ShapeType, Workspace } from 'reducers';
import { rotatePoint } from 'utils/math'; import { rotatePoint } from 'utils/math';
import consts from 'consts'; import consts from 'consts';
@ -24,9 +24,9 @@ interface Props {
visible: boolean; visible: boolean;
left: number; left: number;
top: number; top: number;
latestComments: string[];
onStartIssue(position: number[]): void; onStartIssue(position: number[]): void;
openIssue(position: number[], message: string): void; openIssue(position: number[], message: string): void;
latestComments: string[];
} }
interface ReviewContextMenuProps { interface ReviewContextMenuProps {
@ -109,7 +109,7 @@ export default function CanvasContextMenu(props: Props): JSX.Element | null {
const state = objectStates.find((_state: any): boolean => _state.clientID === contextMenuClientID); const state = objectStates.find((_state: any): boolean => _state.clientID === contextMenuClientID);
if (state) { if (state) {
let { points } = state; let { points } = state;
if (['ellipse', 'rectangle'].includes(state.shapeType)) { if ([ShapeType.ELLIPSE, ShapeType.RECTANGLE].includes(state.shapeType)) {
const [cx, cy] = state.shapeType === 'ellipse' ? state.points : [ const [cx, cy] = state.shapeType === 'ellipse' ? state.points : [
(state.points[0] + state.points[2]) / 2, (state.points[0] + state.points[2]) / 2,
(state.points[1] + state.points[3]) / 2, (state.points[1] + state.points[3]) / 2,
@ -128,6 +128,14 @@ export default function CanvasContextMenu(props: Props): JSX.Element | null {
[points[2], points[3]], [points[2], points[3]],
[points[0], points[3]], [points[0], points[3]],
].map(([x, y]: number[]) => rotatePoint(x, y, state.rotation, cx, cy)).flat(); ].map(([x, y]: number[]) => rotatePoint(x, y, state.rotation, cx, cy)).flat();
} else if (state.shapeType === ShapeType.MASK) {
points = state.points.slice(-4);
points = [
points[0], points[1],
points[2], points[1],
points[2], points[3],
points[0], points[3],
];
} }
if (param.key === ReviewContextMenuKeys.OPEN_ISSUE) { if (param.key === ReviewContextMenuKeys.OPEN_ISSUE) {

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -20,6 +21,7 @@ import consts from 'consts';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
import FrameTags from 'components/annotation-page/tag-annotation-workspace/frame-tags'; import FrameTags from 'components/annotation-page/tag-annotation-workspace/frame-tags';
import ImageSetupsContent from './image-setups-content'; import ImageSetupsContent from './image-setups-content';
import BrushTools from './brush-tools';
import ContextImage from '../standard-workspace/context-image/context-image'; import ContextImage from '../standard-workspace/context-image/context-image';
const cvat = getCore(); const cvat = getCore();
@ -243,10 +245,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
if (prevProps.activatedStateID !== null && prevProps.activatedStateID !== activatedStateID) { if (prevProps.activatedStateID !== null && prevProps.activatedStateID !== activatedStateID) {
canvasInstance.activate(null); canvasInstance.activate(null);
const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`);
if (el) {
(el as any).instance.fill({ opacity });
}
} }
if (gridSize !== prevProps.gridSize) { if (gridSize !== prevProps.gridSize) {
@ -342,7 +340,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.html().removeEventListener('mousedown', this.onCanvasMouseDown); canvasInstance.html().removeEventListener('mousedown', this.onCanvasMouseDown);
canvasInstance.html().removeEventListener('click', this.onCanvasClicked); canvasInstance.html().removeEventListener('click', this.onCanvasClicked);
canvasInstance.html().removeEventListener('contextmenu', this.onCanvasContextMenu);
canvasInstance.html().removeEventListener('canvas.editstart', this.onCanvasEditStart); canvasInstance.html().removeEventListener('canvas.editstart', this.onCanvasEditStart);
canvasInstance.html().removeEventListener('canvas.edited', this.onCanvasEditDone); canvasInstance.html().removeEventListener('canvas.edited', this.onCanvasEditDone);
canvasInstance.html().removeEventListener('canvas.dragstart', this.onCanvasDragStart); canvasInstance.html().removeEventListener('canvas.dragstart', this.onCanvasDragStart);
@ -367,7 +364,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.html().removeEventListener('canvas.regionselected', this.onCanvasPositionSelected); canvasInstance.html().removeEventListener('canvas.regionselected', this.onCanvasPositionSelected);
canvasInstance.html().removeEventListener('canvas.splitted', this.onCanvasTrackSplitted); canvasInstance.html().removeEventListener('canvas.splitted', this.onCanvasTrackSplitted);
canvasInstance.html().removeEventListener('canvas.contextmenu', this.onCanvasPointContextMenu);
canvasInstance.html().removeEventListener('canvas.error', this.onCanvasErrorOccurrence); canvasInstance.html().removeEventListener('canvas.error', this.onCanvasErrorOccurrence);
window.removeEventListener('resize', this.fitCanvas); window.removeEventListener('resize', this.fitCanvas);
@ -480,22 +476,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}; };
private onCanvasClicked = (): void => { private onCanvasClicked = (): void => {
const { onUpdateContextMenu } = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas }; const { canvasInstance } = this.props as { canvasInstance: Canvas };
onUpdateContextMenu(false, 0, 0, ContextMenuType.CANVAS_SHAPE);
if (!canvasInstance.html().contains(document.activeElement) && document.activeElement instanceof HTMLElement) { if (!canvasInstance.html().contains(document.activeElement) && document.activeElement instanceof HTMLElement) {
document.activeElement.blur(); document.activeElement.blur();
} }
}; };
private onCanvasContextMenu = (e: MouseEvent): void => {
const { activatedStateID, onUpdateContextMenu } = this.props;
if (e.target && !(e.target as HTMLElement).classList.contains('svg_select_points')) {
onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY, ContextMenuType.CANVAS_SHAPE);
}
};
private onCanvasShapeDragged = (e: any): void => { private onCanvasShapeDragged = (e: any): void => {
const { jobInstance } = this.props; const { jobInstance } = this.props;
const { id } = e.detail; const { id } = e.detail;
@ -653,7 +639,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const { const {
activatedStateID, activatedStateID,
activatedAttributeID, activatedAttributeID,
selectedOpacity,
aamZoomMargin, aamZoomMargin,
workspace, workspace,
annotations, annotations,
@ -672,10 +657,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
if (activatedState && activatedState.objectType !== ObjectType.TAG) { if (activatedState && activatedState.objectType !== ObjectType.TAG) {
canvasInstance.activate(activatedStateID, activatedAttributeID); canvasInstance.activate(activatedStateID, activatedAttributeID);
} }
const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`);
if (el) {
((el as any) as SVGElement).setAttribute('fill-opacity', `${selectedOpacity}`);
}
} }
} }
@ -746,7 +727,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.html().addEventListener('mousedown', this.onCanvasMouseDown); canvasInstance.html().addEventListener('mousedown', this.onCanvasMouseDown);
canvasInstance.html().addEventListener('click', this.onCanvasClicked); canvasInstance.html().addEventListener('click', this.onCanvasClicked);
canvasInstance.html().addEventListener('contextmenu', this.onCanvasContextMenu);
canvasInstance.html().addEventListener('canvas.editstart', this.onCanvasEditStart); canvasInstance.html().addEventListener('canvas.editstart', this.onCanvasEditStart);
canvasInstance.html().addEventListener('canvas.edited', this.onCanvasEditDone); canvasInstance.html().addEventListener('canvas.edited', this.onCanvasEditDone);
canvasInstance.html().addEventListener('canvas.dragstart', this.onCanvasDragStart); canvasInstance.html().addEventListener('canvas.dragstart', this.onCanvasDragStart);
@ -771,7 +751,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.html().addEventListener('canvas.regionselected', this.onCanvasPositionSelected); canvasInstance.html().addEventListener('canvas.regionselected', this.onCanvasPositionSelected);
canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted); canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted);
canvasInstance.html().addEventListener('canvas.contextmenu', this.onCanvasPointContextMenu);
canvasInstance.html().addEventListener('canvas.error', this.onCanvasErrorOccurrence); canvasInstance.html().addEventListener('canvas.error', this.onCanvasErrorOccurrence);
} }
@ -826,6 +805,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
/> />
<ContextImage /> <ContextImage />
<BrushTools />
<Dropdown trigger={['click']} placement='topCenter' overlay={<ImageSetupsContent />}> <Dropdown trigger={['click']} placement='topCenter' overlay={<ImageSetupsContent />}>
<UpOutlined className='cvat-canvas-image-setups-trigger' /> <UpOutlined className='cvat-canvas-image-setups-trigger' />

@ -0,0 +1,58 @@
// Copyright (C) 2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import React, { useEffect, useRef } from 'react';
export default function useDraggable(
getPosition: () => number[],
onDrag: (diffX: number, diffY: number) => void,
component: JSX.Element,
): JSX.Element {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ref.current) return () => {};
const click = [0, 0];
const position = getPosition();
const mouseMoveListener = (event: MouseEvent): void => {
const dy = event.clientY - click[0];
const dx = event.clientX - click[1];
onDrag(position[0] + dy, position[1] + dx);
event.stopPropagation();
event.preventDefault();
};
const mouseDownListener = (event: MouseEvent): void => {
const [initialTop, initialLeft] = getPosition();
position[0] = initialTop;
position[1] = initialLeft;
click[0] = event.clientY;
click[1] = event.clientX;
window.addEventListener('mousemove', mouseMoveListener);
event.stopPropagation();
event.preventDefault();
};
const mouseUpListener = (): void => {
window.removeEventListener('mousemove', mouseMoveListener);
};
window.document.addEventListener('mouseup', mouseUpListener);
ref.current.addEventListener('mousedown', mouseDownListener);
return () => {
window.document.removeEventListener('mouseup', mouseUpListener);
if (ref.current) {
ref.current.removeEventListener('mousedown', mouseDownListener);
}
};
}, [ref.current]);
return (
<div ref={ref}>
{component}
</div>
);
}

@ -1,4 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -8,6 +9,7 @@ import { SmallDashOutlined } from '@ant-design/icons';
import Popover from 'antd/lib/popover'; import Popover from 'antd/lib/popover';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { ConnectedComponent } from 'react-redux';
const extraControlsContentClassName = 'cvat-extra-controls-control-content'; const extraControlsContentClassName = 'cvat-extra-controls-control-content';
@ -48,7 +50,7 @@ export function ExtraControlsControl(): JSX.Element {
} }
export default function ControlVisibilityObserver<P = {}>( export default function ControlVisibilityObserver<P = {}>(
ControlComponent: React.FunctionComponent<P>, ControlComponent: React.FunctionComponent<P> | ConnectedComponent<any, any>,
): React.FunctionComponent<P> { ): React.FunctionComponent<P> {
let visibilityHeightThreshold = 0; // minimum value of height when element can be pushed to main panel let visibilityHeightThreshold = 0; // minimum value of height when element can be pushed to main panel

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -9,8 +10,8 @@ import {
ActiveControl, ObjectType, Rotation, ShapeType, ActiveControl, ObjectType, Rotation, ShapeType,
} from 'reducers'; } from 'reducers';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas, CanvasMode } from 'cvat-canvas-wrapper';
import { Label } from 'components/labels-editor/common'; import { LabelOptColor } from 'components/labels-editor/common';
import ControlVisibilityObserver, { ExtraControlsControl } from './control-visibility-observer'; import ControlVisibilityObserver, { ExtraControlsControl } from './control-visibility-observer';
import RotateControl, { Props as RotateControlProps } from './rotate-control'; import RotateControl, { Props as RotateControlProps } from './rotate-control';
@ -26,6 +27,7 @@ import DrawPolylineControl, { Props as DrawPolylineControlProps } from './draw-p
import DrawPointsControl, { Props as DrawPointsControlProps } from './draw-points-control'; import DrawPointsControl, { Props as DrawPointsControlProps } from './draw-points-control';
import DrawEllipseControl, { Props as DrawEllipseControlProps } from './draw-ellipse-control'; import DrawEllipseControl, { Props as DrawEllipseControlProps } from './draw-ellipse-control';
import DrawCuboidControl, { Props as DrawCuboidControlProps } from './draw-cuboid-control'; import DrawCuboidControl, { Props as DrawCuboidControlProps } from './draw-cuboid-control';
import DrawMaskControl, { Props as DrawMaskControlProps } from './draw-mask-control';
import DrawSkeletonControl, { Props as DrawSkeletonControlProps } from './draw-skeleton-control'; import DrawSkeletonControl, { Props as DrawSkeletonControlProps } from './draw-skeleton-control';
import SetupTagControl, { Props as SetupTagControlProps } from './setup-tag-control'; import SetupTagControl, { Props as SetupTagControlProps } from './setup-tag-control';
import MergeControl, { Props as MergeControlProps } from './merge-control'; import MergeControl, { Props as MergeControlProps } from './merge-control';
@ -65,6 +67,7 @@ const ObservedDrawPolylineControl = ControlVisibilityObserver<DrawPolylineContro
const ObservedDrawPointsControl = ControlVisibilityObserver<DrawPointsControlProps>(DrawPointsControl); const ObservedDrawPointsControl = ControlVisibilityObserver<DrawPointsControlProps>(DrawPointsControl);
const ObservedDrawEllipseControl = ControlVisibilityObserver<DrawEllipseControlProps>(DrawEllipseControl); const ObservedDrawEllipseControl = ControlVisibilityObserver<DrawEllipseControlProps>(DrawEllipseControl);
const ObservedDrawCuboidControl = ControlVisibilityObserver<DrawCuboidControlProps>(DrawCuboidControl); const ObservedDrawCuboidControl = ControlVisibilityObserver<DrawCuboidControlProps>(DrawCuboidControl);
const ObservedDrawMaskControl = ControlVisibilityObserver<DrawMaskControlProps>(DrawMaskControl);
const ObservedDrawSkeletonControl = ControlVisibilityObserver<DrawSkeletonControlProps>(DrawSkeletonControl); const ObservedDrawSkeletonControl = ControlVisibilityObserver<DrawSkeletonControlProps>(DrawSkeletonControl);
const ObservedSetupTagControl = ControlVisibilityObserver<SetupTagControlProps>(SetupTagControl); const ObservedSetupTagControl = ControlVisibilityObserver<SetupTagControlProps>(SetupTagControl);
const ObservedMergeControl = ControlVisibilityObserver<MergeControlProps>(MergeControl); const ObservedMergeControl = ControlVisibilityObserver<MergeControlProps>(MergeControl);
@ -97,15 +100,17 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
let pointsControlVisible = withUnspecifiedType; let pointsControlVisible = withUnspecifiedType;
let ellipseControlVisible = withUnspecifiedType; let ellipseControlVisible = withUnspecifiedType;
let cuboidControlVisible = withUnspecifiedType; let cuboidControlVisible = withUnspecifiedType;
let maskControlVisible = withUnspecifiedType;
let tagControlVisible = withUnspecifiedType; let tagControlVisible = withUnspecifiedType;
const skeletonControlVisible = labels.some((label: Label) => label.type === 'skeleton'); const skeletonControlVisible = labels.some((label: LabelOptColor) => label.type === 'skeleton');
labels.forEach((label: Label) => { labels.forEach((label: LabelOptColor) => {
rectangleControlVisible = rectangleControlVisible || label.type === ShapeType.RECTANGLE; rectangleControlVisible = rectangleControlVisible || label.type === ShapeType.RECTANGLE;
polygonControlVisible = polygonControlVisible || label.type === ShapeType.POLYGON; polygonControlVisible = polygonControlVisible || label.type === ShapeType.POLYGON;
polylineControlVisible = polylineControlVisible || label.type === ShapeType.POLYLINE; polylineControlVisible = polylineControlVisible || label.type === ShapeType.POLYLINE;
pointsControlVisible = pointsControlVisible || label.type === ShapeType.POINTS; pointsControlVisible = pointsControlVisible || label.type === ShapeType.POINTS;
ellipseControlVisible = ellipseControlVisible || label.type === ShapeType.ELLIPSE; ellipseControlVisible = ellipseControlVisible || label.type === ShapeType.ELLIPSE;
cuboidControlVisible = cuboidControlVisible || label.type === ShapeType.CUBOID; cuboidControlVisible = cuboidControlVisible || label.type === ShapeType.CUBOID;
maskControlVisible = maskControlVisible || label.type === ShapeType.MASK;
tagControlVisible = tagControlVisible || label.type === ObjectType.TAG; tagControlVisible = tagControlVisible || label.type === ObjectType.TAG;
}); });
@ -156,11 +161,20 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
ActiveControl.DRAW_CUBOID, ActiveControl.DRAW_CUBOID,
ActiveControl.DRAW_ELLIPSE, ActiveControl.DRAW_ELLIPSE,
ActiveControl.DRAW_SKELETON, ActiveControl.DRAW_SKELETON,
ActiveControl.DRAW_MASK,
ActiveControl.AI_TOOLS, ActiveControl.AI_TOOLS,
ActiveControl.OPENCV_TOOLS, ActiveControl.OPENCV_TOOLS,
].includes(activeControl); ].includes(activeControl);
const editing = canvasInstance.mode() === CanvasMode.EDIT;
if (!drawing) { if (!drawing) {
if (editing) {
// users probably will press N as they are used to do when they want to finish editing
// in this case, if a mask is being edited we probably want to finish editing first
canvasInstance.edit({ enabled: false });
return;
}
canvasInstance.cancel(); canvasInstance.cancel();
// repeateDrawShapes gets all the latest parameters // repeateDrawShapes gets all the latest parameters
// and calls canvasInstance.draw() with them // and calls canvasInstance.draw() with them
@ -306,6 +320,15 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
/> />
) )
} }
{
maskControlVisible && (
<ObservedDrawMaskControl
canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_MASK}
disabled={controlsDisabled}
/>
)
}
{ {
skeletonControlVisible && ( skeletonControlVisible && (
<ObservedDrawSkeletonControl <ObservedDrawSkeletonControl

@ -0,0 +1,53 @@
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Popover from 'antd/lib/popover';
import Icon from '@ant-design/icons';
import { Canvas } from 'cvat-canvas-wrapper';
import { BrushIcon } from 'icons';
import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-mask-popover';
import withVisibilityHandling from './handle-popover-visibility';
export interface Props {
canvasInstance: Canvas;
isDrawing: boolean;
disabled?: boolean;
}
const CustomPopover = withVisibilityHandling(Popover, 'draw-mask');
function DrawPointsControl(props: Props): JSX.Element {
const { canvasInstance, isDrawing, disabled } = props;
const dynamicPopoverProps = isDrawing ? {
overlayStyle: {
display: 'none',
},
} : {};
const dynamicIconProps = isDrawing ? {
className: 'cvat-draw-mask-control cvat-active-canvas-control',
onClick: (): void => {
canvasInstance.draw({ enabled: false });
},
} : {
className: 'cvat-draw-mask-control',
};
return disabled ? (
<Icon className='cvat-draw-mask-control cvat-disabled-canvas-control' component={BrushIcon} />
) : (
<CustomPopover
{...dynamicPopoverProps}
overlayClassName='cvat-draw-shape-popover'
placement='right'
content={<DrawShapePopoverContainer />}
>
<Icon {...dynamicIconProps} component={BrushIcon} />
</CustomPopover>
);
}
export default React.memo(DrawPointsControl);

@ -0,0 +1,61 @@
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { Row, Col } from 'antd/lib/grid';
import Button from 'antd/lib/button';
import Text from 'antd/lib/typography/Text';
import LabelSelector from 'components/label-selector/label-selector';
import CVATTooltip from 'components/common/cvat-tooltip';
interface Props {
labels: any[];
selectedLabelID: number;
repeatShapeShortcut: string;
onChangeLabel(value: string): void;
onDraw(labelID: number): void;
}
function DrawMaskPopover(props: Props): JSX.Element {
const {
labels, selectedLabelID, repeatShapeShortcut, onChangeLabel, onDraw,
} = props;
return (
<div className='cvat-draw-shape-popover-content'>
<Row justify='start'>
<Col>
<Text className='cvat-text-color' strong>
Draw new mask
</Text>
</Col>
</Row>
<Row justify='start'>
<Col>
<Text className='cvat-text-color'>Label</Text>
</Col>
</Row>
<Row justify='center'>
<Col span={24}>
<LabelSelector
style={{ width: '100%' }}
labels={labels}
value={selectedLabelID}
onChange={onChangeLabel}
/>
</Col>
</Row>
<Row justify='space-around'>
<Col span={24}>
<CVATTooltip title={`Press ${repeatShapeShortcut} to draw a mask again`}>
<Button onClick={() => onDraw(selectedLabelID)}>Draw a mask</Button>
</CVATTooltip>
</Col>
</Row>
</div>
);
}
export default React.memo(DrawMaskPopover);

@ -21,11 +21,12 @@ import { Row, Col } from 'antd/lib/grid';
import notification from 'antd/lib/notification'; import notification from 'antd/lib/notification';
import message from 'antd/lib/message'; import message from 'antd/lib/message';
import Dropdown from 'antd/lib/dropdown'; import Dropdown from 'antd/lib/dropdown';
import Switch from 'antd/lib/switch';
import lodash from 'lodash'; import lodash from 'lodash';
import { AIToolsIcon } from 'icons'; import { AIToolsIcon } from 'icons';
import { Canvas, convertShapesForInteractor } from 'cvat-canvas-wrapper'; import { Canvas, convertShapesForInteractor } from 'cvat-canvas-wrapper';
import { getCore } from 'cvat-core-wrapper'; import { getCore, Attribute, Label } from 'cvat-core-wrapper';
import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper'; import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper';
import { import {
CombinedState, ActiveControl, Model, ObjectType, ShapeType, ToolsBlockerState, ModelAttribute, CombinedState, ActiveControl, Model, ObjectType, ShapeType, ToolsBlockerState, ModelAttribute,
@ -40,7 +41,6 @@ import {
import DetectorRunner, { DetectorRequestBody } from 'components/model-runner-modal/detector-runner'; import DetectorRunner, { DetectorRequestBody } from 'components/model-runner-modal/detector-runner';
import LabelSelector from 'components/label-selector/label-selector'; import LabelSelector from 'components/label-selector/label-selector';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
import { Attribute, Label } from 'components/labels-editor/common';
import ApproximationAccuracy, { import ApproximationAccuracy, {
thresholdFromAccuracy, thresholdFromAccuracy,
@ -75,6 +75,7 @@ interface DispatchToProps {
switchNavigationBlocked(navigationBlocked: boolean): void; switchNavigationBlocked(navigationBlocked: boolean): void;
} }
const MIN_SUPPORTED_INTERACTOR_VERSION = 2;
const core = getCore(); const core = getCore();
const CustomPopover = withVisibilityHandling(Popover, 'tools-control'); const CustomPopover = withVisibilityHandling(Popover, 'tools-control');
@ -139,6 +140,7 @@ interface State {
activeInteractor: Model | null; activeInteractor: Model | null;
activeLabelID: number; activeLabelID: number;
activeTracker: Model | null; activeTracker: Model | null;
convertMasksToPolygons: boolean;
trackedShapes: TrackedShape[]; trackedShapes: TrackedShape[];
fetching: boolean; fetching: boolean;
pointsRecieved: boolean; pointsRecieved: boolean;
@ -203,8 +205,11 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
private interaction: { private interaction: {
id: string | null; id: string | null;
isAborted: boolean; isAborted: boolean;
latestResponse: number[][]; latestResponse: {
latestResult: number[][]; mask: number[][],
points: number[][],
};
lastestApproximatedPoints: number[][];
latestRequest: null | { latestRequest: null | {
interactor: Model; interactor: Model;
data: { data: {
@ -219,6 +224,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
public constructor(props: Props) { public constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
convertMasksToPolygons: false,
activeInteractor: props.interactors.length ? props.interactors[0] : null, activeInteractor: props.interactors.length ? props.interactors[0] : null,
activeTracker: props.trackers.length ? props.trackers[0] : null, activeTracker: props.trackers.length ? props.trackers[0] : null,
activeLabelID: props.labels.length ? props.labels[0].id : null, activeLabelID: props.labels.length ? props.labels[0].id : null,
@ -233,8 +239,11 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
this.interaction = { this.interaction = {
id: null, id: null,
isAborted: false, isAborted: false,
latestResponse: [], latestResponse: {
latestResult: [], mask: [],
points: [],
},
lastestApproximatedPoints: [],
latestRequest: null, latestRequest: null,
hideMessage: null, hideMessage: null,
}; };
@ -277,8 +286,8 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
this.interaction = { this.interaction = {
id: null, id: null,
isAborted: false, isAborted: false,
latestResponse: [], latestResponse: { mask: [], points: [] },
latestResult: [], lastestApproximatedPoints: [],
latestRequest: null, latestRequest: null,
hideMessage: null, hideMessage: null,
}; };
@ -291,14 +300,15 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
} }
if (prevState.approxPolyAccuracy !== approxPolyAccuracy) { if (prevState.approxPolyAccuracy !== approxPolyAccuracy) {
if (isActivated && mode === 'interaction' && this.interaction.latestResponse.length) { if (isActivated && mode === 'interaction' && this.interaction.latestResponse.points.length) {
this.approximateResponsePoints(this.interaction.latestResponse).then((points: number[][]) => { this.approximateResponsePoints(this.interaction.latestResponse.points)
this.interaction.latestResult = points; .then((points: number[][]) => {
this.interaction.lastestApproximatedPoints = points;
canvasInstance.interact({ canvasInstance.interact({
enabled: true, enabled: true,
intermediateShape: { intermediateShape: {
shapeType: ShapeType.POLYGON, shapeType: ShapeType.POLYGON,
points: this.interaction.latestResult.flat(), points: this.interaction.lastestApproximatedPoints.flat(),
}, },
onChangeToolsBlockerState: this.onChangeToolsBlockerState, onChangeToolsBlockerState: this.onChangeToolsBlockerState,
}); });
@ -337,7 +347,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
private runInteractionRequest = async (interactionId: string): Promise<void> => { private runInteractionRequest = async (interactionId: string): Promise<void> => {
const { jobInstance, canvasInstance } = this.props; const { jobInstance, canvasInstance } = this.props;
const { activeInteractor, fetching } = this.state; const { activeInteractor, fetching, convertMasksToPolygons } = this.state;
const { id, latestRequest } = this.interaction; const { id, latestRequest } = this.interaction;
if (id !== interactionId || !latestRequest || fetching) { if (id !== interactionId || !latestRequest || fetching) {
@ -360,18 +370,22 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
// run server request // run server request
this.setState({ fetching: true }); this.setState({ fetching: true });
const response = await core.lambda.call(jobInstance.taskId, interactor, data); const response = await core.lambda.call(jobInstance.taskId, interactor, data);
// approximation with cv.approxPolyDP // approximation with cv.approxPolyDP
const approximated = await this.approximateResponsePoints(response); const approximated = await this.approximateResponsePoints(response.points);
if (this.interaction.id !== interactionId || this.interaction.isAborted) { if (this.interaction.id !== interactionId || this.interaction.isAborted) {
// new interaction session or the session is aborted // new interaction session or the session is aborted
return; return;
} }
this.interaction.latestResponse = response; this.interaction.latestResponse = {
this.interaction.latestResult = approximated; mask: response.mask,
points: response.points,
};
this.interaction.lastestApproximatedPoints = approximated;
this.setState({ pointsRecieved: !!response.length }); this.setState({ pointsRecieved: !!response.points.length });
} finally { } finally {
if (this.interaction.id === interactionId && this.interaction.hideMessage) { if (this.interaction.id === interactionId && this.interaction.hideMessage) {
this.interaction.hideMessage(); this.interaction.hideMessage();
@ -381,12 +395,17 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
this.setState({ fetching: false }); this.setState({ fetching: false });
} }
if (this.interaction.latestResult.length) { if (this.interaction.lastestApproximatedPoints.length) {
const height = this.interaction.latestResponse.mask.length;
const width = this.interaction.latestResponse.mask[0].length;
const maskPoints = this.interaction.latestResponse.mask.flat();
maskPoints.push(0, 0, width - 1, height - 1);
canvasInstance.interact({ canvasInstance.interact({
enabled: true, enabled: true,
intermediateShape: { intermediateShape: {
shapeType: ShapeType.POLYGON, shapeType: convertMasksToPolygons ? ShapeType.POLYGON : ShapeType.MASK,
points: this.interaction.latestResult.flat(), points: convertMasksToPolygons ? this.interaction.lastestApproximatedPoints.flat() :
maskPoints,
}, },
onChangeToolsBlockerState: this.onChangeToolsBlockerState, onChangeToolsBlockerState: this.onChangeToolsBlockerState,
}); });
@ -420,8 +439,8 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
// prevent future requests if possible // prevent future requests if possible
this.interaction.isAborted = true; this.interaction.isAborted = true;
this.interaction.latestRequest = null; this.interaction.latestRequest = null;
if (this.interaction.latestResult.length) { if (this.interaction.lastestApproximatedPoints.length) {
this.constructFromPoints(this.interaction.latestResult); this.constructFromPoints(this.interaction.lastestApproximatedPoints);
} }
} else if (shapesUpdated) { } else if (shapesUpdated) {
const interactor = activeInteractor as Model; const interactor = activeInteractor as Model;
@ -507,8 +526,17 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
private setActiveInteractor = (value: string): void => { private setActiveInteractor = (value: string): void => {
const { interactors } = this.props; const { interactors } = this.props;
const [interactor] = interactors.filter((_interactor: Model) => _interactor.id === value);
if (interactor.version < MIN_SUPPORTED_INTERACTOR_VERSION) {
notification.warning({
message: 'Interactor API is outdated',
description: 'Probably, you should consider updating the serverless function',
});
}
this.setState({ this.setState({
activeInteractor: interactors.filter((interactor: Model) => interactor.id === value)[0], activeInteractor: interactor,
}); });
}; };
@ -756,7 +784,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
}); });
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const response = await core.lambda.call(jobInstance.taskId, tracker, { const response = await core.lambda.call(jobInstance.taskId, tracker, {
frame: frame , frame,
shapes: trackableObjects.shapes, shapes: trackableObjects.shapes,
states: trackableObjects.states, states: trackableObjects.states,
}); });
@ -797,10 +825,12 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
} }
private constructFromPoints(points: number[][]): void { private constructFromPoints(points: number[][]): void {
const { convertMasksToPolygons } = this.state;
const { const {
frame, labels, curZOrder, jobInstance, activeLabelID, createAnnotations, frame, labels, curZOrder, jobInstance, activeLabelID, createAnnotations,
} = this.props; } = this.props;
if (convertMasksToPolygons) {
const object = new core.classes.ObjectState({ const object = new core.classes.ObjectState({
frame, frame,
objectType: ObjectType.SHAPE, objectType: ObjectType.SHAPE,
@ -812,6 +842,23 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
}); });
createAnnotations(jobInstance, frame, [object]); createAnnotations(jobInstance, frame, [object]);
} else {
const height = this.interaction.latestResponse.mask.length;
const width = this.interaction.latestResponse.mask[0].length;
const maskPoints = this.interaction.latestResponse.mask.flat();
maskPoints.push(0, 0, width - 1, height - 1);
const object = new core.classes.ObjectState({
frame,
objectType: ObjectType.SHAPE,
label: labels.length ? labels.filter((label: any) => label.id === activeLabelID)[0] : null,
shapeType: ShapeType.MASK,
points: maskPoints,
occluded: false,
zOrder: curZOrder,
});
createAnnotations(jobInstance, frame, [object]);
}
} }
private async approximateResponsePoints(points: number[][]): Promise<number[][]> { private async approximateResponsePoints(points: number[][]): Promise<number[][]> {
@ -833,6 +880,21 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
return points; return points;
} }
private renderMasksConvertingBlock(): JSX.Element {
const { convertMasksToPolygons } = this.state;
return (
<Row className='cvat-interactors-setups-container'>
<Switch
checked={convertMasksToPolygons}
onChange={(checked: boolean) => {
this.setState({ convertMasksToPolygons: checked });
}}
/>
<Text>Convert masks to polygons</Text>
</Row>
);
}
private renderLabelBlock(): JSX.Element { private renderLabelBlock(): JSX.Element {
const { labels } = this.props; const { labels } = this.props;
const { activeLabelID } = this.state; const { activeLabelID } = this.state;
@ -932,7 +994,9 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
private renderInteractorBlock(): JSX.Element { private renderInteractorBlock(): JSX.Element {
const { interactors, canvasInstance, onInteractionStart } = this.props; const { interactors, canvasInstance, onInteractionStart } = this.props;
const { activeInteractor, activeLabelID, fetching } = this.state; const {
activeInteractor, activeLabelID, fetching,
} = this.state;
if (!interactors.length) { if (!interactors.length) {
return ( return (
@ -995,7 +1059,9 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
type='primary' type='primary'
loading={fetching} loading={fetching}
className='cvat-tools-interact-button' className='cvat-tools-interact-button'
disabled={!activeInteractor || fetching} disabled={!activeInteractor ||
fetching ||
activeInteractor.version < MIN_SUPPORTED_INTERACTOR_VERSION}
onClick={() => { onClick={() => {
this.setState({ mode: 'interaction' }); this.setState({ mode: 'interaction' });
@ -1074,7 +1140,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
case 'number': case 'number':
return dbAttribute.values.includes(value) || inputType === 'text'; return dbAttribute.values.includes(value) || inputType === 'text';
case 'text': case 'text':
return ['select', 'radio'].includes(dbAttribute.input_type) && dbAttribute.values.includes(value); return ['select', 'radio'].includes(dbAttribute.inputType) && dbAttribute.values.includes(value);
case 'select': case 'select':
return (inputType === 'radio' && dbAttribute.values.includes(value)) || inputType === 'text'; return (inputType === 'radio' && dbAttribute.values.includes(value)) || inputType === 'text';
case 'radio': case 'radio':
@ -1105,10 +1171,8 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
if (!jobLabel || !modelLabel) return null; if (!jobLabel || !modelLabel) return null;
return new core.classes.ObjectState({ const objectData = {
shapeType: data.type,
label: jobLabel, label: jobLabel,
points: data.points,
objectType: ObjectType.SHAPE, objectType: ObjectType.SHAPE,
frame, frame,
occluded: false, occluded: false,
@ -1118,7 +1182,8 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
const [modelAttr] = Object.entries(body.mapping[modelLabel].attributes) const [modelAttr] = Object.entries(body.mapping[modelLabel].attributes)
.find((value: string[]) => value[1] === attr.name) || []; .find((value: string[]) => value[1] === attr.name) || [];
const areCompatible = checkAttributesCompatibility( const areCompatible = checkAttributesCompatibility(
model.attributes[modelLabel].find((mAttr) => mAttr.name === modelAttr), model.attributes[modelLabel]
.find((mAttr) => mAttr.name === modelAttr),
jobLabel.attributes.find((jobAttr: Attribute) => ( jobLabel.attributes.find((jobAttr: Attribute) => (
jobAttr.name === attr.name jobAttr.name === attr.name
)), )),
@ -1132,6 +1197,28 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
return acc; return acc;
}, {} as Record<number, string>), }, {} as Record<number, string>),
zOrder: curZOrder, zOrder: curZOrder,
};
if (data.type === 'mask' && data.points && body.convMaskToPoly) {
return new core.classes.ObjectState({
...objectData,
shapeType: 'polygon',
points: data.points,
});
}
if (data.type === 'mask') {
return new core.classes.ObjectState({
...objectData,
shapeType: data.type,
points: data.mask,
});
}
return new core.classes.ObjectState({
...objectData,
shapeType: data.type,
points: data.points,
}); });
}, },
).filter((state: any) => state); ).filter((state: any) => state);
@ -1164,6 +1251,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
</Row> </Row>
<Tabs type='card' tabBarGutter={8}> <Tabs type='card' tabBarGutter={8}>
<Tabs.TabPane key='interactors' tab='Interactors'> <Tabs.TabPane key='interactors' tab='Interactors'>
{this.renderMasksConvertingBlock()}
{this.renderLabelBlock()} {this.renderLabelBlock()}
{this.renderInteractorBlock()} {this.renderInteractorBlock()}
</Tabs.TabPane> </Tabs.TabPane>
@ -1184,7 +1272,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
interactors, detectors, trackers, isActivated, canvasInstance, labels, frameIsDeleted, interactors, detectors, trackers, isActivated, canvasInstance, labels, frameIsDeleted,
} = this.props; } = this.props;
const { const {
fetching, approxPolyAccuracy, pointsRecieved, mode, portals, fetching, approxPolyAccuracy, pointsRecieved, mode, portals, convertMasksToPolygons,
} = this.state; } = this.state;
if (![...interactors, ...detectors, ...trackers].length) return null; if (![...interactors, ...detectors, ...trackers].length) return null;
@ -1209,7 +1297,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
}; };
const showAnyContent = labels.length && !frameIsDeleted; const showAnyContent = labels.length && !frameIsDeleted;
const showInteractionContent = isActivated && mode === 'interaction' && pointsRecieved; const showInteractionContent = isActivated && mode === 'interaction' && pointsRecieved && convertMasksToPolygons;
const showDetectionContent = fetching && mode === 'detection'; const showDetectionContent = fetching && mode === 'detection';
const interactionContent: JSX.Element | null = showInteractionContent ? ( const interactionContent: JSX.Element | null = showInteractionContent ? (

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -43,6 +44,7 @@ interface Props {
toBackground(): void; toBackground(): void;
toForeground(): void; toForeground(): void;
resetCuboidPerspective(): void; resetCuboidPerspective(): void;
edit(): void;
} }
function ItemTopComponent(props: Props): JSX.Element { function ItemTopComponent(props: Props): JSX.Element {
@ -75,6 +77,7 @@ function ItemTopComponent(props: Props): JSX.Element {
toBackground, toBackground,
toForeground, toForeground,
resetCuboidPerspective, resetCuboidPerspective,
edit,
jobInstance, jobInstance,
} = props; } = props;
@ -150,6 +153,7 @@ function ItemTopComponent(props: Props): JSX.Element {
toForeground, toForeground,
resetCuboidPerspective, resetCuboidPerspective,
changeColorPickerVisible, changeColorPickerVisible,
edit,
})} })}
> >
<MoreOutlined /> <MoreOutlined />

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -6,7 +7,7 @@ import React from 'react';
import Menu from 'antd/lib/menu'; import Menu from 'antd/lib/menu';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
import Icon, { import Icon, {
LinkOutlined, CopyOutlined, BlockOutlined, RetweetOutlined, DeleteOutlined, LinkOutlined, CopyOutlined, BlockOutlined, RetweetOutlined, DeleteOutlined, EditOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { import {
@ -44,6 +45,7 @@ interface Props {
toForeground(): void; toForeground(): void;
resetCuboidPerspective(): void; resetCuboidPerspective(): void;
changeColorPickerVisible(visible: boolean): void; changeColorPickerVisible(visible: boolean): void;
edit(): void;
jobInstance: any; jobInstance: any;
} }
@ -77,6 +79,20 @@ function MakeCopyItem(props: ItemProps): JSX.Element {
); );
} }
function EditMaskItem(props: ItemProps): JSX.Element {
const { toolProps, ...rest } = props;
const { edit } = toolProps;
return (
<Menu.Item {...rest}>
<CVATTooltip title='Shift + Double click'>
<Button type='link' icon={<EditOutlined />} onClick={edit}>
Edit
</Button>
</CVATTooltip>
</Menu.Item>
);
}
function PropagateItem(props: ItemProps): JSX.Element { function PropagateItem(props: ItemProps): JSX.Element {
const { toolProps, ...rest } = props; const { toolProps, ...rest } = props;
const { propagateShortcut, propagate } = toolProps; const { propagateShortcut, propagate } = toolProps;
@ -209,6 +225,7 @@ export default function ItemMenu(props: Props): JSX.Element {
TO_FOREGROUND = 'to_foreground', TO_FOREGROUND = 'to_foreground',
SWITCH_COLOR = 'switch_color', SWITCH_COLOR = 'switch_color',
REMOVE_ITEM = 'remove_item', REMOVE_ITEM = 'remove_item',
EDIT_MASK = 'edit_mask',
} }
const is2D = jobInstance.dimension === DimensionType.DIM_2D; const is2D = jobInstance.dimension === DimensionType.DIM_2D;
@ -219,6 +236,7 @@ export default function ItemMenu(props: Props): JSX.Element {
{!readonly && objectType !== ObjectType.TAG && ( {!readonly && objectType !== ObjectType.TAG && (
<MakeCopyItem key={MenuKeys.COPY} toolProps={props} /> <MakeCopyItem key={MenuKeys.COPY} toolProps={props} />
)} )}
{!readonly && <EditMaskItem key={MenuKeys.EDIT_MASK} toolProps={props} />}
{!readonly && <PropagateItem key={MenuKeys.PROPAGATE} toolProps={props} />} {!readonly && <PropagateItem key={MenuKeys.PROPAGATE} toolProps={props} />}
{is2D && !readonly && [ShapeType.POLYGON, ShapeType.POLYLINE, ShapeType.CUBOID].includes(shapeType) && ( {is2D && !readonly && [ShapeType.POLYGON, ShapeType.POLYLINE, ShapeType.CUBOID].includes(shapeType) && (
<SwitchOrientationItem key={MenuKeys.SWITCH_ORIENTATION} toolProps={props} /> <SwitchOrientationItem key={MenuKeys.SWITCH_ORIENTATION} toolProps={props} />

@ -1,4 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -40,6 +41,7 @@ interface Props {
changeLabel(label: any): void; changeLabel(label: any): void;
changeColor(color: string): void; changeColor(color: string): void;
resetCuboidPerspective(): void; resetCuboidPerspective(): void;
edit(): void;
} }
function ObjectItemComponent(props: Props): JSX.Element { function ObjectItemComponent(props: Props): JSX.Element {
@ -69,6 +71,7 @@ function ObjectItemComponent(props: Props): JSX.Element {
changeLabel, changeLabel,
changeColor, changeColor,
resetCuboidPerspective, resetCuboidPerspective,
edit,
jobInstance, jobInstance,
} = props; } = props;
@ -124,6 +127,7 @@ function ObjectItemComponent(props: Props): JSX.Element {
toBackground={toBackground} toBackground={toBackground}
toForeground={toForeground} toForeground={toForeground}
resetCuboidPerspective={resetCuboidPerspective} resetCuboidPerspective={resetCuboidPerspective}
edit={edit}
/> />
<ObjectButtonsContainer readonly={readonly} clientID={clientID} /> <ObjectButtonsContainer readonly={readonly} clientID={clientID} />
{!!attributes.length && ( {!!attributes.length && (

@ -115,6 +115,7 @@
.cvat-draw-polyline-control, .cvat-draw-polyline-control,
.cvat-draw-points-control, .cvat-draw-points-control,
.cvat-draw-ellipse-control, .cvat-draw-ellipse-control,
.cvat-draw-mask-control,
.cvat-draw-cuboid-control, .cvat-draw-cuboid-control,
.cvat-draw-skeleton-control, .cvat-draw-skeleton-control,
.cvat-setup-tag-control, .cvat-setup-tag-control,
@ -191,7 +192,7 @@
.cvat-tools-track-button, .cvat-tools-track-button,
.cvat-tools-interact-button { .cvat-tools-interact-button {
width: 100%; width: 100%;
margin-top: 10px; margin-top: $grid-unit-size;
} }
.cvat-interactors-tips-icon-container { .cvat-interactors-tips-icon-container {
@ -199,6 +200,15 @@
font-size: 20px; font-size: 20px;
} }
.cvat-interactors-setups-container {
margin-top: $grid-unit-size;
margin-bottom: $grid-unit-size;
> button {
margin-right: $grid-unit-size;
}
}
.cvat-interactor-tip-container { .cvat-interactor-tip-container {
background: $background-color-2; background: $background-color-2;
padding: $grid-unit-size; padding: $grid-unit-size;

@ -112,6 +112,7 @@ function FiltersModalComponent(): JSX.Element {
{ value: 'cuboid', title: 'Cuboid' }, { value: 'cuboid', title: 'Cuboid' },
{ value: 'ellipse', title: 'Ellipse' }, { value: 'ellipse', title: 'Ellipse' },
{ value: 'skeleton', title: 'Skeleton' }, { value: 'skeleton', title: 'Skeleton' },
{ value: 'mask', title: 'Mask' },
], ],
}, },
}, },

@ -5,7 +5,7 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import { ArrowRightOutlined, QuestionCircleOutlined, RightOutlined } from '@ant-design/icons'; import { QuestionCircleOutlined } from '@ant-design/icons';
import Table from 'antd/lib/table'; import Table from 'antd/lib/table';
import Modal from 'antd/lib/modal'; import Modal from 'antd/lib/modal';
import Spin from 'antd/lib/spin'; import Spin from 'antd/lib/spin';
@ -114,6 +114,7 @@ function StatisticsModalComponent(props: StateToProps & DispatchToProps): JSX.El
ellipse: `${data.label[key].ellipse.shape} / ${data.label[key].ellipse.track}`, ellipse: `${data.label[key].ellipse.shape} / ${data.label[key].ellipse.track}`,
cuboid: `${data.label[key].cuboid.shape} / ${data.label[key].cuboid.track}`, cuboid: `${data.label[key].cuboid.shape} / ${data.label[key].cuboid.track}`,
skeleton: `${data.label[key].skeleton.shape} / ${data.label[key].skeleton.track}`, skeleton: `${data.label[key].skeleton.shape} / ${data.label[key].skeleton.track}`,
mask: `${data.label[key].mask.shape}`,
tag: data.label[key].tag, tag: data.label[key].tag,
manually: data.label[key].manually, manually: data.label[key].manually,
interpolated: data.label[key].interpolated, interpolated: data.label[key].interpolated,
@ -130,6 +131,7 @@ function StatisticsModalComponent(props: StateToProps & DispatchToProps): JSX.El
ellipse: `${data.total.ellipse.shape} / ${data.total.ellipse.track}`, ellipse: `${data.total.ellipse.shape} / ${data.total.ellipse.track}`,
cuboid: `${data.total.cuboid.shape} / ${data.total.cuboid.track}`, cuboid: `${data.total.cuboid.shape} / ${data.total.cuboid.track}`,
skeleton: `${data.total.skeleton.shape} / ${data.total.skeleton.track}`, skeleton: `${data.total.skeleton.shape} / ${data.total.skeleton.track}`,
mask: `${data.total.mask.shape}`,
tag: data.total.tag, tag: data.total.tag,
manually: data.total.manually, manually: data.total.manually,
interpolated: data.total.interpolated, interpolated: data.total.interpolated,
@ -137,7 +139,7 @@ function StatisticsModalComponent(props: StateToProps & DispatchToProps): JSX.El
}); });
const makeShapesTracksTitle = (title: string): JSX.Element => ( const makeShapesTracksTitle = (title: string): JSX.Element => (
<CVATTooltip title={is2D ? 'Shapes / Tracks' : 'Shapes'}> <CVATTooltip title={is2D && !(title.toLowerCase() === 'mask') ? 'Shapes / Tracks' : 'Shapes'}>
<Text strong style={{ marginRight: 5 }}> <Text strong style={{ marginRight: 5 }}>
{title} {title}
</Text> </Text>
@ -210,6 +212,12 @@ function StatisticsModalComponent(props: StateToProps & DispatchToProps): JSX.El
key: 'skeleton', key: 'skeleton',
width: 100, width: 100,
}, },
{
title: makeShapesTracksTitle('Mask'),
dataIndex: 'mask',
key: 'mask',
width: 100,
},
{ {
title: <Text strong> Tag </Text>, title: <Text strong> Tag </Text>,
dataIndex: 'tag', dataIndex: 'tag',

@ -49,6 +49,7 @@ const initialValues: FormValues = {
interface UploadParams { interface UploadParams {
resource: 'annotation' | 'dataset'; resource: 'annotation' | 'dataset';
convMaskToPoly: boolean;
useDefaultSettings: boolean; useDefaultSettings: boolean;
sourceStorage: Storage; sourceStorage: Storage;
selectedFormat: string | null; selectedFormat: string | null;
@ -75,6 +76,7 @@ function ImportDatasetModal(props: StateToProps): JSX.Element {
const [helpMessage, setHelpMessage] = useState(''); const [helpMessage, setHelpMessage] = useState('');
const [selectedSourceStorageLocation, setSelectedSourceStorageLocation] = useState(StorageLocation.LOCAL); const [selectedSourceStorageLocation, setSelectedSourceStorageLocation] = useState(StorageLocation.LOCAL);
const [uploadParams, setUploadParams] = useState<UploadParams>({ const [uploadParams, setUploadParams] = useState<UploadParams>({
convMaskToPoly: true,
useDefaultSettings: true, useDefaultSettings: true,
} as UploadParams); } as UploadParams);
const [resource, setResource] = useState(''); const [resource, setResource] = useState('');
@ -242,7 +244,8 @@ function ImportDatasetModal(props: StateToProps): JSX.Element {
instance, uploadParams.selectedFormat as string, instance, uploadParams.selectedFormat as string,
uploadParams.useDefaultSettings, uploadParams.sourceStorage, uploadParams.useDefaultSettings, uploadParams.sourceStorage,
uploadParams.file || uploadParams.fileName as string, uploadParams.file || uploadParams.fileName as string,
)); uploadParams.convMaskToPoly,
) as any);
const resToPrint = uploadParams.resource.charAt(0).toUpperCase() + uploadParams.resource.slice(1); const resToPrint = uploadParams.resource.charAt(0).toUpperCase() + uploadParams.resource.slice(1);
Notification.info({ Notification.info({
message: `${resToPrint} import started`, message: `${resToPrint} import started`,
@ -314,11 +317,15 @@ function ImportDatasetModal(props: StateToProps): JSX.Element {
onCancel={closeModal} onCancel={closeModal}
onOk={() => form.submit()} onOk={() => form.submit()}
className='cvat-modal-import-dataset' className='cvat-modal-import-dataset'
destroyOnClose
> >
<Form <Form
name={`Import ${resource}`} name={`Import ${resource}`}
form={form} form={form}
initialValues={initialValues} initialValues={{
...initialValues,
convMaskToPoly: uploadParams.convMaskToPoly,
}}
onFinish={handleImport} onFinish={handleImport}
layout='vertical' layout='vertical'
> >
@ -371,7 +378,27 @@ function ImportDatasetModal(props: StateToProps): JSX.Element {
)} )}
</Select> </Select>
</Form.Item> </Form.Item>
<Space> <Space className='cvat-modal-import-switch-conv-mask-to-poly-container'>
<Form.Item
name='convMaskToPoly'
valuePropName='checked'
className='cvat-modal-import-switch-conv-mask-to-poly'
>
<Switch
onChange={(value: boolean) => {
setUploadParams({
...uploadParams,
convMaskToPoly: value,
} as UploadParams);
}}
/>
</Form.Item>
<Text strong>Convert masks to polygons</Text>
<CVATTooltip title='The option is relevant for formats that work with masks only'>
<QuestionCircleOutlined />
</CVATTooltip>
</Space>
<Space className='cvat-modal-import-switch-use-default-storage-container'>
<Form.Item <Form.Item
name='useDefaultSettings' name='useDefaultSettings'
valuePropName='checked' valuePropName='checked'

@ -34,3 +34,8 @@
width: 100%; width: 100%;
} }
} }
.cvat-modal-import-switch-use-default-storage-container,
.cvat-modal-import-switch-conv-mask-to-poly-container {
width: 100%;
}

@ -7,11 +7,11 @@ import React, { useState } from 'react';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import { DeleteOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { DeleteOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import Select, { BaseOptionType } from 'antd/lib/select'; import Select, { BaseOptionType } from 'antd/lib/select';
import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox';
import Tag from 'antd/lib/tag'; import Tag from 'antd/lib/tag';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import InputNumber from 'antd/lib/input-number'; import InputNumber from 'antd/lib/input-number';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
import Switch from 'antd/lib/switch';
import notification from 'antd/lib/notification'; import notification from 'antd/lib/notification';
import { Model, ModelAttribute, StringObject } from 'reducers'; import { Model, ModelAttribute, StringObject } from 'reducers';
@ -40,6 +40,7 @@ type MappedLabelsList = Record<string, MappedLabel>;
export interface DetectorRequestBody { export interface DetectorRequestBody {
mapping: MappedLabelsList; mapping: MappedLabelsList;
cleanup: boolean; cleanup: boolean;
convMaskToPoly: boolean;
} }
interface Match { interface Match {
@ -57,6 +58,7 @@ function DetectorRunner(props: Props): JSX.Element {
const [threshold, setThreshold] = useState<number>(0.5); const [threshold, setThreshold] = useState<number>(0.5);
const [distance, setDistance] = useState<number>(50); const [distance, setDistance] = useState<number>(50);
const [cleanup, setCleanup] = useState<boolean>(false); const [cleanup, setCleanup] = useState<boolean>(false);
const [convertMasksToPolygons, setConvertMasksToPolygons] = useState<boolean>(false);
const [match, setMatch] = useState<Match>({ model: null, task: null }); const [match, setMatch] = useState<Match>({ model: null, task: null });
const [attrMatches, setAttrMatch] = useState<Record<string, Match>>({}); const [attrMatches, setAttrMatch] = useState<Record<string, Match>>({});
@ -352,14 +354,24 @@ function DetectorRunner(props: Props): JSX.Element {
</Row> </Row>
</> </>
) : null} ) : null}
{isDetector && (
<div className='detector-runner-convert-masks-to-polygons-wrapper'>
<Switch
checked={convertMasksToPolygons}
onChange={(checked: boolean) => {
setConvertMasksToPolygons(checked);
}}
/>
<Text>Convert masks to polygons</Text>
</div>
)}
{isDetector && withCleanup ? ( {isDetector && withCleanup ? (
<div> <div className='detector-runner-clean-previous-annotations-wrapper'>
<Checkbox <Switch
checked={cleanup} checked={cleanup}
onChange={(e: CheckboxChangeEvent): void => setCleanup(e.target.checked)} onChange={(checked: boolean): void => setCleanup(checked)}
> />
Clean old annotations <Text>Clean previous annotations</Text>
</Checkbox>
</div> </div>
) : null} ) : null}
{isReId ? ( {isReId ? (
@ -414,6 +426,7 @@ function DetectorRunner(props: Props): JSX.Element {
const detectorRequestBody: DetectorRequestBody = { const detectorRequestBody: DetectorRequestBody = {
mapping, mapping,
cleanup, cleanup,
convMaskToPoly: convertMasksToPolygons,
}; };
runInference( runInference(

@ -15,3 +15,10 @@
.cvat-run-model-label-attribute-block { .cvat-run-model-label-attribute-block {
padding-left: $grid-unit-size * 4; padding-left: $grid-unit-size * 4;
} }
.detector-runner-clean-previous-annotations-wrapper,
.detector-runner-convert-masks-to-polygons-wrapper {
span {
margin-left: $grid-unit-size;
}
}

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -6,12 +7,15 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { CombinedState, ContextMenuType, Workspace } from 'reducers'; import {
CombinedState, ContextMenuType, ShapeType, Workspace,
} from 'reducers';
import CanvasContextMenuComponent from 'components/annotation-page/canvas/canvas-context-menu'; import CanvasContextMenuComponent from 'components/annotation-page/canvas/canvas-context-menu';
import { updateCanvasContextMenu } from 'actions/annotation-actions'; import { updateCanvasContextMenu } from 'actions/annotation-actions';
import { reviewActions, finishIssueAsync } from 'actions/review-actions'; import { reviewActions, finishIssueAsync } from 'actions/review-actions';
import { ThunkDispatch } from 'utils/redux'; import { ThunkDispatch } from 'utils/redux';
import { Canvas } from 'cvat-canvas-wrapper';
import { ObjectState } from 'cvat-core-wrapper'; import { ObjectState } from 'cvat-core-wrapper';
interface OwnProps { interface OwnProps {
@ -21,6 +25,7 @@ interface OwnProps {
interface StateToProps { interface StateToProps {
contextMenuParentID: number | null; contextMenuParentID: number | null;
contextMenuClientID: number | null; contextMenuClientID: number | null;
canvasInstance: Canvas | null;
objectStates: any[]; objectStates: any[];
visible: boolean; visible: boolean;
top: number; top: number;
@ -29,9 +34,14 @@ interface StateToProps {
collapsed: boolean | undefined; collapsed: boolean | undefined;
workspace: Workspace; workspace: Workspace;
latestComments: string[]; latestComments: string[];
activatedStateID: number | null;
} }
interface DispatchToProps { interface DispatchToProps {
onUpdateContextMenu(
visible: boolean, left: number, top: number,
pointID: number | null, type?: ContextMenuType,
): void;
onStartIssue(position: number[]): void; onStartIssue(position: number[]): void;
openIssue(position: number[], message: string): void; openIssue(position: number[], message: string): void;
} }
@ -39,8 +49,9 @@ interface DispatchToProps {
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
const { const {
annotation: { annotation: {
annotations: { collapsed, states: objectStates }, annotations: { collapsed, states: objectStates, activatedStateID },
canvas: { canvas: {
instance,
contextMenu: { contextMenu: {
visible, top, left, type, clientID, parentID, visible, top, left, type, clientID, parentID,
}, },
@ -63,7 +74,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
contextMenuClientID: clientID, contextMenuClientID: clientID,
contextMenuParentID: parentID, contextMenuParentID: parentID,
collapsed: clientID !== null ? collapsed[clientID] : undefined, collapsed: clientID !== null ? collapsed[clientID] : undefined,
activatedStateID,
objectStates, objectStates,
canvasInstance: instance instanceof Canvas ? instance : null,
visible: visible:
clientID !== null && clientID !== null &&
visible && visible &&
@ -79,6 +92,12 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps { function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps {
return { return {
onUpdateContextMenu(
visible: boolean, left: number, top: number,
pointID: number | null, type?: ContextMenuType,
): void {
dispatch(updateCanvasContextMenu(visible, left, top, pointID, type));
},
onStartIssue(position: number[]): void { onStartIssue(position: number[]): void {
dispatch(reviewActions.startIssue(position)); dispatch(reviewActions.startIssue(position));
dispatch(updateCanvasContextMenu(false, 0, 0)); dispatch(updateCanvasContextMenu(false, 0, 0));
@ -144,8 +163,15 @@ class CanvasContextMenuContainer extends React.PureComponent<Props, State> {
} }
public componentDidMount(): void { public componentDidMount(): void {
const { canvasInstance } = this.props;
this.updatePositionIfOutOfScreen(); this.updatePositionIfOutOfScreen();
window.addEventListener('mousemove', this.moveContextMenu); window.addEventListener('mousemove', this.moveContextMenu);
if (canvasInstance) {
canvasInstance.html().addEventListener('canvas.clicked', this.onClickCanvas);
canvasInstance.html().addEventListener('contextmenu', this.onOpenCanvasContextMenu);
canvasInstance.html().addEventListener('canvas.contextmenu', this.onCanvasPointContextMenu);
}
} }
public componentDidUpdate(prevProps: Props): void { public componentDidUpdate(prevProps: Props): void {
@ -180,9 +206,46 @@ class CanvasContextMenuContainer extends React.PureComponent<Props, State> {
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
const { canvasInstance } = this.props;
window.removeEventListener('mousemove', this.moveContextMenu); window.removeEventListener('mousemove', this.moveContextMenu);
if (canvasInstance) {
canvasInstance.html().removeEventListener('canvas.clicked', this.onClickCanvas);
canvasInstance.html().removeEventListener('contextmenu', this.onOpenCanvasContextMenu);
canvasInstance.html().removeEventListener('canvas.contextmenu', this.onCanvasPointContextMenu);
}
} }
private onClickCanvas = (): void => {
const { visible, onUpdateContextMenu } = this.props;
if (visible) {
onUpdateContextMenu(false, 0, 0, null, ContextMenuType.CANVAS_SHAPE);
}
};
private onOpenCanvasContextMenu = (e: MouseEvent): void => {
const { activatedStateID, onUpdateContextMenu } = this.props;
if (e.target && !(e.target as HTMLElement).classList.contains('svg_select_points')) {
onUpdateContextMenu(
activatedStateID !== null, e.clientX, e.clientY, null, ContextMenuType.CANVAS_SHAPE,
);
}
};
private onCanvasPointContextMenu = (e: any): void => {
const { objectStates, activatedStateID, onUpdateContextMenu } = this.props;
const [state] = objectStates.filter((el: any) => el.clientID === activatedStateID);
if (![ShapeType.CUBOID, ShapeType.RECTANGLE, ShapeType.MASK].includes(state.shapeType)) {
onUpdateContextMenu(
activatedStateID !== null,
e.detail.mouseEvent.clientX,
e.detail.mouseEvent.clientY,
e.detail.pointID,
ContextMenuType.CANVAS_SHAPE_POINT,
);
}
};
private moveContextMenu = (e: MouseEvent): void => { private moveContextMenu = (e: MouseEvent): void => {
if (this.dragging) { if (this.dragging) {
this.setState((state) => { this.setState((state) => {
@ -219,7 +282,7 @@ class CanvasContextMenuContainer extends React.PureComponent<Props, State> {
} }
} }
public render(): JSX.Element { public render(): JSX.Element | null {
const { left, top } = this.state; const { left, top } = this.state;
const { const {
visible, visible,
@ -235,8 +298,7 @@ class CanvasContextMenuContainer extends React.PureComponent<Props, State> {
} = this.props; } = this.props;
return ( return (
<> type === ContextMenuType.CANVAS_SHAPE ? (
{type === ContextMenuType.CANVAS_SHAPE && (
<CanvasContextMenuComponent <CanvasContextMenuComponent
contextMenuClientID={contextMenuClientID} contextMenuClientID={contextMenuClientID}
contextMenuParentID={contextMenuParentID} contextMenuParentID={contextMenuParentID}
@ -250,8 +312,7 @@ class CanvasContextMenuContainer extends React.PureComponent<Props, State> {
onStartIssue={onStartIssue} onStartIssue={onStartIssue}
openIssue={openIssue} openIssue={openIssue}
/> />
)} ) : null
</>
); );
} }
} }

@ -232,6 +232,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
switchableAutomaticBordering: switchableAutomaticBordering:
activeControl === ActiveControl.DRAW_POLYGON || activeControl === ActiveControl.DRAW_POLYGON ||
activeControl === ActiveControl.DRAW_POLYLINE || activeControl === ActiveControl.DRAW_POLYLINE ||
activeControl === ActiveControl.DRAW_MASK ||
activeControl === ActiveControl.EDIT, activeControl === ActiveControl.EDIT,
}; };
} }

@ -0,0 +1,108 @@
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux';
import DrawMaskPopoverComponent from 'components/annotation-page/standard-workspace/controls-side-bar/draw-mask-popover';
import { rememberObject } from 'actions/annotation-actions';
import { CombinedState, ShapeType, ObjectType } from 'reducers';
import { Canvas } from 'cvat-canvas-wrapper';
interface DispatchToProps {
onDrawStart(
shapeType: ShapeType,
labelID: number,
objectType: ObjectType,
): void;
}
interface StateToProps {
normalizedKeyMap: Record<string, string>;
canvasInstance: Canvas;
labels: any[];
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onDrawStart(
shapeType: ShapeType,
labelID: number,
objectType: ObjectType,
): void {
dispatch(
rememberObject({
activeObjectType: objectType,
activeShapeType: shapeType,
activeLabelID: labelID,
}),
);
},
};
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
canvas: { instance: canvasInstance },
job: { labels },
},
shortcuts: { normalizedKeyMap },
} = state;
return {
canvasInstance: canvasInstance as Canvas,
normalizedKeyMap,
labels,
};
}
type Props = StateToProps & DispatchToProps;
interface State {
selectedLabelID: number;
}
class DrawShapePopoverContainer extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
const defaultLabelID = props.labels.length ? props.labels[0].id : null;
this.state = { selectedLabelID: defaultLabelID };
}
private onDraw = (): void => {
const { canvasInstance, onDrawStart } = this.props;
const { selectedLabelID } = this.state;
canvasInstance.cancel();
canvasInstance.draw({
enabled: true,
shapeType: ShapeType.MASK,
crosshair: false,
});
onDrawStart(ShapeType.MASK, selectedLabelID, ObjectType.SHAPE);
};
private onChangeLabel = (value: any): void => {
this.setState({ selectedLabelID: value.id });
};
public render(): JSX.Element {
const { selectedLabelID } = this.state;
const { normalizedKeyMap, labels } = this.props;
return (
<DrawMaskPopoverComponent
labels={labels}
selectedLabelID={selectedLabelID}
repeatShapeShortcut={normalizedKeyMap.SWITCH_DRAW_MODE}
onChangeLabel={this.onChangeLabel}
onDraw={this.onDraw}
/>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(DrawShapePopoverContainer);

@ -1,4 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -23,7 +24,7 @@ import ObjectStateItemComponent from 'components/annotation-page/standard-worksp
import { getColor } from 'components/annotation-page/standard-workspace/objects-side-bar/shared'; import { getColor } from 'components/annotation-page/standard-workspace/objects-side-bar/shared';
import { shift } from 'utils/math'; import { shift } from 'utils/math';
import { Label } from 'cvat-core-wrapper'; import { Label } from 'cvat-core-wrapper';
import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas, CanvasMode } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper'; import { Canvas3d } from 'cvat-canvas3d-wrapper';
interface OwnProps { interface OwnProps {
@ -142,6 +143,18 @@ class ObjectItemContainer extends React.PureComponent<Props> {
} }
}; };
private edit = (): void => {
const { objectState, readonly, canvasInstance } = this.props;
if (!readonly && canvasInstance instanceof Canvas) {
if (canvasInstance.mode() !== CanvasMode.IDLE) {
canvasInstance.cancel();
}
canvasInstance.edit({ enabled: true, state: objectState });
}
}
private remove = (): void => { private remove = (): void => {
const { const {
objectState, readonly, removeObject, objectState, readonly, removeObject,
@ -342,6 +355,7 @@ class ObjectItemContainer extends React.PureComponent<Props> {
toForeground={this.toForeground} toForeground={this.toForeground}
changeColor={this.changeColor} changeColor={this.changeColor}
changeLabel={this.changeLabel} changeLabel={this.changeLabel}
edit={this.edit}
resetCuboidPerspective={() => this.resetCuboidPerspective()} resetCuboidPerspective={() => this.resetCuboidPerspective()}
/> />
); );

@ -53,7 +53,13 @@ import SVGCVATAzureProvider from './assets/vscode-icons_file-type-azure.svg';
import SVGCVATS3Provider from './assets/S3.svg'; import SVGCVATS3Provider from './assets/S3.svg';
import SVGCVATGoogleCloudProvider from './assets/google-cloud.svg'; import SVGCVATGoogleCloudProvider from './assets/google-cloud.svg';
import SVGRestoreIcon from './assets/restore-icon.svg'; import SVGRestoreIcon from './assets/restore-icon.svg';
import SVGBrushIcon from './assets/brush-icon.svg';
import SVGEraserIcon from './assets/eraser-icon.svg';
import SVGPolygonPlusIcon from './assets/polygon-plus.svg';
import SVGPolygonMinusIcon from './assets/polygon-minus.svg';
import SVGMultiPlusIcon from './assets/multi-plus-icon.svg'; import SVGMultiPlusIcon from './assets/multi-plus-icon.svg';
import SVGPlusIcon from './assets/plus-icon.svg';
import SVGCheckIcon from './assets/check-icon.svg';
export const CVATLogo = React.memo((): JSX.Element => <SVGCVATLogo />); export const CVATLogo = React.memo((): JSX.Element => <SVGCVATLogo />);
export const CursorIcon = React.memo((): JSX.Element => <SVGCursorIcon />); export const CursorIcon = React.memo((): JSX.Element => <SVGCursorIcon />);
@ -103,4 +109,10 @@ export const AzureProvider = React.memo((): JSX.Element => <SVGCVATAzureProvider
export const S3Provider = React.memo((): JSX.Element => <SVGCVATS3Provider />); export const S3Provider = React.memo((): JSX.Element => <SVGCVATS3Provider />);
export const GoogleCloudProvider = React.memo((): JSX.Element => <SVGCVATGoogleCloudProvider />); export const GoogleCloudProvider = React.memo((): JSX.Element => <SVGCVATGoogleCloudProvider />);
export const RestoreIcon = React.memo((): JSX.Element => <SVGRestoreIcon />); export const RestoreIcon = React.memo((): JSX.Element => <SVGRestoreIcon />);
export const BrushIcon = React.memo((): JSX.Element => <SVGBrushIcon />);
export const EraserIcon = React.memo((): JSX.Element => <SVGEraserIcon />);
export const PolygonPlusIcon = React.memo((): JSX.Element => <SVGPolygonPlusIcon />);
export const PolygonMinusIcon = React.memo((): JSX.Element => <SVGPolygonMinusIcon />);
export const MutliPlusIcon = React.memo((): JSX.Element => <SVGMultiPlusIcon />); export const MutliPlusIcon = React.memo((): JSX.Element => <SVGMultiPlusIcon />);
export const PlusIcon = React.memo((): JSX.Element => <SVGPlusIcon />);
export const CheckIcon = React.memo((): JSX.Element => <SVGCheckIcon />);

@ -42,6 +42,11 @@ const defaultState: AnnotationState = {
clientID: null, clientID: null,
parentID: null, parentID: null,
}, },
brushTools: {
visible: false,
top: 0,
left: 0,
},
instance: null, instance: null,
ready: false, ready: false,
activeControl: ActiveControl.CURSOR, activeControl: ActiveControl.CURSOR,
@ -484,22 +489,21 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
const { payload } = action; const { payload } = action;
let { activeControl } = state.canvas; let { activeControl } = state.canvas;
if (payload.activeShapeType === ShapeType.RECTANGLE) { if ('activeObjectType' in payload && payload.activeObjectType === ObjectType.TAG) {
activeControl = ActiveControl.DRAW_RECTANGLE;
} else if (payload.activeShapeType === ShapeType.POLYGON) {
activeControl = ActiveControl.DRAW_POLYGON;
} else if (payload.activeShapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE;
} else if (payload.activeShapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
} else if (payload.activeShapeType === ShapeType.ELLIPSE) {
activeControl = ActiveControl.DRAW_ELLIPSE;
} else if (payload.activeShapeType === ShapeType.CUBOID) {
activeControl = ActiveControl.DRAW_CUBOID;
} else if (payload.activeShapeType === ShapeType.SKELETON) {
activeControl = ActiveControl.DRAW_SKELETON;
} else if (payload.activeObjectType === ObjectType.TAG) {
activeControl = ActiveControl.CURSOR; activeControl = ActiveControl.CURSOR;
} else if ('activeShapeType' in payload) {
const controlMapping = {
[ShapeType.RECTANGLE]: ActiveControl.DRAW_RECTANGLE,
[ShapeType.POLYGON]: ActiveControl.DRAW_POLYGON,
[ShapeType.POLYLINE]: ActiveControl.DRAW_POLYLINE,
[ShapeType.POINTS]: ActiveControl.DRAW_POINTS,
[ShapeType.ELLIPSE]: ActiveControl.DRAW_ELLIPSE,
[ShapeType.CUBOID]: ActiveControl.DRAW_CUBOID,
[ShapeType.SKELETON]: ActiveControl.DRAW_SKELETON,
[ShapeType.MASK]: ActiveControl.DRAW_MASK,
};
activeControl = controlMapping[payload.activeShapeType as ShapeType];
} }
return { return {
@ -793,6 +797,9 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
canvas: { canvas: {
...state.canvas, ...state.canvas,
activeControl, activeControl,
contextMenu: {
...defaultState.canvas.contextMenu,
},
}, },
annotations: { annotations: {
...state.annotations, ...state.annotations,
@ -991,6 +998,18 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}, },
}; };
} }
case AnnotationActionTypes.UPDATE_BRUSH_TOOLS_CONFIG: {
return {
...state,
canvas: {
...state.canvas,
brushTools: {
...state.canvas.brushTools,
...action.payload,
},
},
};
}
case AnnotationActionTypes.REDO_ACTION_SUCCESS: case AnnotationActionTypes.REDO_ACTION_SUCCESS:
case AnnotationActionTypes.UNDO_ACTION_SUCCESS: { case AnnotationActionTypes.UNDO_ACTION_SUCCESS: {
const { activatedStateID } = state.annotations; const { activatedStateID } = state.annotations;

@ -317,6 +317,7 @@ export interface Model {
id: string; id: string;
name: string; name: string;
labels: string[]; labels: string[];
version: number;
attributes: Record<string, ModelAttribute[]>; attributes: Record<string, ModelAttribute[]>;
framework: string; framework: string;
description: string; description: string;
@ -558,6 +559,7 @@ export enum ActiveControl {
DRAW_POLYLINE = 'draw_polyline', DRAW_POLYLINE = 'draw_polyline',
DRAW_POINTS = 'draw_points', DRAW_POINTS = 'draw_points',
DRAW_ELLIPSE = 'draw_ellipse', DRAW_ELLIPSE = 'draw_ellipse',
DRAW_MASK = 'draw_mask',
DRAW_CUBOID = 'draw_cuboid', DRAW_CUBOID = 'draw_cuboid',
DRAW_SKELETON = 'draw_skeleton', DRAW_SKELETON = 'draw_skeleton',
MERGE = 'merge', MERGE = 'merge',
@ -577,6 +579,7 @@ export enum ShapeType {
POINTS = 'points', POINTS = 'points',
ELLIPSE = 'ellipse', ELLIPSE = 'ellipse',
CUBOID = 'cuboid', CUBOID = 'cuboid',
MASK = 'mask',
SKELETON = 'skeleton', SKELETON = 'skeleton',
} }
@ -633,6 +636,11 @@ export interface AnnotationState {
parentID: number | null; parentID: number | null;
clientID: number | null; clientID: number | null;
}; };
brushTools: {
visible: boolean;
top: number;
left: number;
};
instance: Canvas | Canvas3d | null; instance: Canvas | Canvas3d | null;
ready: boolean; ready: boolean;
activeControl: ActiveControl; activeControl: ActiveControl;

@ -8,10 +8,10 @@ import { BoundariesActionTypes } from 'actions/boundaries-actions';
import { AuthActionTypes } from 'actions/auth-actions'; import { AuthActionTypes } from 'actions/auth-actions';
import { SettingsActionTypes } from 'actions/settings-actions'; import { SettingsActionTypes } from 'actions/settings-actions';
import { AnnotationActionTypes } from 'actions/annotation-actions'; import { AnnotationActionTypes } from 'actions/annotation-actions';
import { import {
SettingsState, GridColor, FrameSpeed, ColorBy, DimensionType, SettingsState, GridColor, FrameSpeed, ColorBy, DimensionType,
} from '.'; } from 'reducers';
import { ObjectState, ShapeType } from 'cvat-core-wrapper';
const defaultState: SettingsState = { const defaultState: SettingsState = {
shapes: { shapes: {
@ -383,6 +383,27 @@ export default (state = defaultState, action: AnyAction): SettingsState => {
}, },
}; };
} }
case AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS:
case AnnotationActionTypes.CREATE_ANNOTATIONS_SUCCESS:
case AnnotationActionTypes.CHANGE_FRAME_SUCCESS: {
const { states } = action.payload;
if (states.some((_state: ObjectState): boolean => _state.shapeType === ShapeType.MASK)) {
const MIN_OPACITY = 30;
const { shapes: { opacity } } = state;
if (opacity < MIN_OPACITY) {
return {
...state,
shapes: {
...state.shapes,
opacity: MIN_OPACITY,
selectedOpacity: MIN_OPACITY * 2,
},
};
}
}
return state;
}
case BoundariesActionTypes.RESET_AFTER_ERROR: case BoundariesActionTypes.RESET_AFTER_ERROR:
case AnnotationActionTypes.GET_JOB_SUCCESS: { case AnnotationActionTypes.GET_JOB_SUCCESS: {
const { job } = action.payload; const { job } = action.payload;

@ -4,9 +4,11 @@
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from functools import reduce
import os.path as osp import os.path as osp
import re import re
import sys import sys
import numpy as np
from collections import namedtuple from collections import namedtuple
from pathlib import Path from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
@ -26,7 +28,7 @@ from cvat.apps.engine.models import Image as Img
from cvat.apps.engine.models import Label, LabelType, Project, ShapeType, Task from cvat.apps.engine.models import Label, LabelType, Project, ShapeType, Task
from .annotation import AnnotationIR, AnnotationManager, TrackManager from .annotation import AnnotationIR, AnnotationManager, TrackManager
from .formats.transformations import EllipsesToMasks from .formats.transformations import EllipsesToMasks, CVATRleToCOCORle
CVAT_INTERNAL_ATTRIBUTES = {'occluded', 'outside', 'keyframe', 'track_id', 'rotation'} CVAT_INTERNAL_ATTRIBUTES = {'occluded', 'outside', 'keyframe', 'track_id', 'rotation'}
@ -1568,6 +1570,15 @@ def convert_cvat_anno_to_dm(cvat_frame_anno, label_attrs, map_label, format_name
"group": anno_group, "group": anno_group,
"attributes": anno_attr, "attributes": anno_attr,
}), cvat_frame_anno.height, cvat_frame_anno.width) }), cvat_frame_anno.height, cvat_frame_anno.width)
elif shape_obj.type == ShapeType.MASK:
anno = CVATRleToCOCORle.convert_mask(SimpleNamespace(**{
"points": shape_obj.points,
"label": anno_label,
"z_order": shape_obj.z_order,
"rotation": shape_obj.rotation,
"group": anno_group,
"attributes": anno_attr,
}), cvat_frame_anno.height, cvat_frame_anno.width)
elif shape_obj.type == ShapeType.POLYLINE: elif shape_obj.type == ShapeType.POLYLINE:
anno = dm.PolyLine(anno_points, anno = dm.PolyLine(anno_points,
label=anno_label, attributes=anno_attr, group=anno_group, label=anno_label, attributes=anno_attr, group=anno_group,
@ -1671,7 +1682,8 @@ def import_dm_annotations(dm_dataset: dm.Dataset, instance_data: Union[ProjectDa
dm.AnnotationType.polyline: ShapeType.POLYLINE, dm.AnnotationType.polyline: ShapeType.POLYLINE,
dm.AnnotationType.points: ShapeType.POINTS, dm.AnnotationType.points: ShapeType.POINTS,
dm.AnnotationType.cuboid_3d: ShapeType.CUBOID, dm.AnnotationType.cuboid_3d: ShapeType.CUBOID,
dm.AnnotationType.skeleton: ShapeType.SKELETON dm.AnnotationType.skeleton: ShapeType.SKELETON,
dm.AnnotationType.mask: ShapeType.MASK
} }
label_cat = dm_dataset.categories()[dm.AnnotationType.label] label_cat = dm_dataset.categories()[dm.AnnotationType.label]
@ -1715,6 +1727,23 @@ def import_dm_annotations(dm_dataset: dm.Dataset, instance_data: Union[ProjectDa
points = [] points = []
if ann.type == dm.AnnotationType.cuboid_3d: if ann.type == dm.AnnotationType.cuboid_3d:
points = [*ann.position, *ann.rotation, *ann.scale, 0, 0, 0, 0, 0, 0, 0] points = [*ann.position, *ann.rotation, *ann.scale, 0, 0, 0, 0, 0, 0, 0]
elif ann.type == dm.AnnotationType.mask:
istrue = np.argwhere(ann.image == 1).transpose()
top = int(istrue[0].min())
left = int(istrue[1].min())
bottom = int(istrue[0].max())
right = int(istrue[1].max())
points = ann.image[top:bottom + 1, left:right + 1]
def reduce_fn(acc, v):
if v == acc['val']:
acc['res'][-1] += 1
else:
acc['val'] = v
acc['res'].append(1)
return acc
points = reduce(reduce_fn, points.reshape(np.prod(points.shape)), { 'res': [0], 'val': False })['res']
points.extend([int(left), int(top), int(right), int(bottom)])
elif ann.type != dm.AnnotationType.skeleton: elif ann.type != dm.AnnotationType.skeleton:
points = ann.points points = ann.points

@ -11,7 +11,7 @@ from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor,
import_dm_annotations) import_dm_annotations)
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from .transformations import RotatedBoxesToPolygons from .transformations import MaskToPolygonTransformation, RotatedBoxesToPolygons
from .registry import dm_env, exporter, importer from .registry import dm_env, exporter, importer
from .utils import make_colormap from .utils import make_colormap
@ -33,12 +33,12 @@ def _export(dst_file, instance_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='CamVid', ext='ZIP', version='1.0') @importer(name='CamVid', ext='ZIP', version='1.0')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
Archive(src_file.name).extractall(tmp_dir) Archive(src_file.name).extractall(tmp_dir)
dataset = Dataset.import_from(tmp_dir, 'camvid', env=dm_env) dataset = Dataset.import_from(tmp_dir, 'camvid', env=dm_env)
dataset.transform('masks_to_polygons') dataset = MaskToPolygonTransformation.convert_dataset(dataset, **kwargs)
if load_data_callback is not None: if load_data_callback is not None:
load_data_callback(dataset, instance_data) load_data_callback(dataset, instance_data)
import_dm_annotations(dataset, instance_data) import_dm_annotations(dataset, instance_data)

@ -13,7 +13,7 @@ from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor,
import_dm_annotations) import_dm_annotations)
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from .transformations import RotatedBoxesToPolygons from .transformations import MaskToPolygonTransformation, RotatedBoxesToPolygons
from .registry import dm_env, exporter, importer from .registry import dm_env, exporter, importer
from .utils import make_colormap from .utils import make_colormap
@ -34,7 +34,7 @@ def _export(dst_file, instance_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='Cityscapes', ext='ZIP', version='1.0') @importer(name='Cityscapes', ext='ZIP', version='1.0')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
Archive(src_file.name).extractall(tmp_dir) Archive(src_file.name).extractall(tmp_dir)
@ -45,7 +45,7 @@ def _import(src_file, instance_data, load_data_callback=None):
write_label_map(labelmap_file, colormap) write_label_map(labelmap_file, colormap)
dataset = Dataset.import_from(tmp_dir, 'cityscapes', env=dm_env) dataset = Dataset.import_from(tmp_dir, 'cityscapes', env=dm_env)
dataset.transform('masks_to_polygons') dataset = MaskToPolygonTransformation.convert_dataset(dataset, **kwargs)
if load_data_callback is not None: if load_data_callback is not None:
load_data_callback(dataset, instance_data) load_data_callback(dataset, instance_data)
import_dm_annotations(dataset, instance_data) import_dm_annotations(dataset, instance_data)

@ -25,7 +25,7 @@ def _export(dst_file, instance_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='COCO', ext='JSON, ZIP', version='1.0') @importer(name='COCO', ext='JSON, ZIP', version='1.0')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
if zipfile.is_zipfile(src_file): if zipfile.is_zipfile(src_file):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
zipfile.ZipFile(src_file).extractall(tmp_dir) zipfile.ZipFile(src_file).extractall(tmp_dir)
@ -49,7 +49,7 @@ def _export(dst_file, instance_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='COCO Keypoints', ext='JSON, ZIP', version='1.0') @importer(name='COCO Keypoints', ext='JSON, ZIP', version='1.0')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
def remove_extra_annotations(dataset): def remove_extra_annotations(dataset):
for item in dataset: for item in dataset:
annotations = [ann for ann in item.annotations annotations = [ann for ann in item.annotations

@ -611,6 +611,11 @@ def create_xml_dumper(file_object):
self.xmlgen.startElement("points", points) self.xmlgen.startElement("points", points)
self._level += 1 self._level += 1
def open_mask(self, points):
self._indent()
self.xmlgen.startElement("mask", points)
self._level += 1
def open_cuboid(self, cuboid): def open_cuboid(self, cuboid):
self._indent() self._indent()
self.xmlgen.startElement("cuboid", cuboid) self.xmlgen.startElement("cuboid", cuboid)
@ -657,6 +662,11 @@ def create_xml_dumper(file_object):
self._indent() self._indent()
self.xmlgen.endElement("points") self.xmlgen.endElement("points")
def close_mask(self):
self._level -= 1
self._indent()
self.xmlgen.endElement("mask")
def close_cuboid(self): def close_cuboid(self):
self._level -= 1 self._level -= 1
self._indent() self._indent()
@ -775,7 +785,14 @@ def dump_as_cvat_annotation(dumper, annotations):
("points", ''), ("points", ''),
("rotation", "{:.2f}".format(shape.rotation)) ("rotation", "{:.2f}".format(shape.rotation))
])) ]))
elif shape.type == "mask":
dump_data.update(OrderedDict([
("rle", f"{list(int (v) for v in shape.points[:-4])}"[1:-1]),
("left", f"{int(shape.points[-4])}"),
("top", f"{int(shape.points[-3])}"),
("width", f"{int(shape.points[-2] - shape.points[-4])}"),
("height", f"{int(shape.points[-1] - shape.points[-3])}"),
]))
else: else:
dump_data.update(OrderedDict([ dump_data.update(OrderedDict([
("points", ';'.join(( ("points", ';'.join((
@ -800,6 +817,8 @@ def dump_as_cvat_annotation(dumper, annotations):
dumper.open_polyline(dump_data) dumper.open_polyline(dump_data)
elif shape.type == "points": elif shape.type == "points":
dumper.open_points(dump_data) dumper.open_points(dump_data)
elif shape.type == "mask":
dumper.open_mask(dump_data)
elif shape.type == "cuboid": elif shape.type == "cuboid":
dumper.open_cuboid(dump_data) dumper.open_cuboid(dump_data)
elif shape.type == "skeleton": elif shape.type == "skeleton":
@ -826,6 +845,8 @@ def dump_as_cvat_annotation(dumper, annotations):
dumper.close_points() dumper.close_points()
elif shape.type == "cuboid": elif shape.type == "cuboid":
dumper.close_cuboid() dumper.close_cuboid()
elif shape.type == "mask":
dumper.close_mask()
elif shape.type == "skeleton": elif shape.type == "skeleton":
dumper.close_skeleton() dumper.close_skeleton()
else: else:
@ -907,6 +928,14 @@ def dump_as_cvat_interpolation(dumper, annotations):
dump_data.update(OrderedDict([ dump_data.update(OrderedDict([
("rotation", "{:.2f}".format(shape.rotation)) ("rotation", "{:.2f}".format(shape.rotation))
])) ]))
elif shape.type == "mask":
dump_data.update(OrderedDict([
("rle", f"{list(int (v) for v in shape.points[:-4])}"[1:-1]),
("left", f"{int(shape.points[-4])}"),
("top", f"{int(shape.points[-3])}"),
("width", f"{int(shape.points[-2] - shape.points[-4])}"),
("height", f"{int(shape.points[-1] - shape.points[-3])}"),
]))
elif shape.type == "cuboid": elif shape.type == "cuboid":
dump_data.update(OrderedDict([ dump_data.update(OrderedDict([
("xtl1", "{:.2f}".format(shape.points[0])), ("xtl1", "{:.2f}".format(shape.points[0])),
@ -944,6 +973,8 @@ def dump_as_cvat_interpolation(dumper, annotations):
dumper.open_polyline(dump_data) dumper.open_polyline(dump_data)
elif shape.type == "points": elif shape.type == "points":
dumper.open_points(dump_data) dumper.open_points(dump_data)
elif shape.type == 'mask':
dumper.open_mask(dump_data)
elif shape.type == "cuboid": elif shape.type == "cuboid":
dumper.open_cuboid(dump_data) dumper.open_cuboid(dump_data)
elif shape.type == 'skeleton': elif shape.type == 'skeleton':
@ -967,6 +998,8 @@ def dump_as_cvat_interpolation(dumper, annotations):
dumper.close_polyline() dumper.close_polyline()
elif shape.type == "points": elif shape.type == "points":
dumper.close_points() dumper.close_points()
elif shape.type == 'mask':
dumper.close_mask()
elif shape.type == "cuboid": elif shape.type == "cuboid":
dumper.close_cuboid() dumper.close_cuboid()
elif shape.type == "skeleton": elif shape.type == "skeleton":
@ -1063,7 +1096,7 @@ def dump_as_cvat_interpolation(dumper, annotations):
dumper.close_root() dumper.close_root()
def load_anno(file_object, annotations): def load_anno(file_object, annotations):
supported_shapes = ('box', 'ellipse', 'polygon', 'polyline', 'points', 'cuboid', 'skeleton') supported_shapes = ('box', 'ellipse', 'polygon', 'polyline', 'points', 'cuboid', 'skeleton', 'mask')
context = ElementTree.iterparse(file_object, events=("start", "end")) context = ElementTree.iterparse(file_object, events=("start", "end"))
context = iter(context) context = iter(context)
next(context) next(context)
@ -1211,6 +1244,12 @@ def load_anno(file_object, annotations):
shape['points'].append(el.attrib['cy']) shape['points'].append(el.attrib['cy'])
shape['points'].append("{:.2f}".format(float(el.attrib['cx']) + float(el.attrib['rx']))) shape['points'].append("{:.2f}".format(float(el.attrib['cx']) + float(el.attrib['rx'])))
shape['points'].append("{:.2f}".format(float(el.attrib['cy']) - float(el.attrib['ry']))) shape['points'].append("{:.2f}".format(float(el.attrib['cy']) - float(el.attrib['ry'])))
elif el.tag == 'mask':
shape['points'] = el.attrib['rle'].split(',')
shape['points'].append(el.attrib['left'])
shape['points'].append(el.attrib['top'])
shape['points'].append("{}".format(int(el.attrib['left']) + int(el.attrib['width'])))
shape['points'].append("{}".format(int(el.attrib['top']) + int(el.attrib['height'])))
elif el.tag == 'cuboid': elif el.tag == 'cuboid':
shape['points'].append(el.attrib['xtl1']) shape['points'].append(el.attrib['xtl1'])
shape['points'].append(el.attrib['ytl1']) shape['points'].append(el.attrib['ytl1'])
@ -1248,6 +1287,22 @@ def load_anno(file_object, annotations):
if track_element: if track_element:
track.elements.append(track_element) track.elements.append(track_element)
track_element = None track_element = None
else:
if track.shapes[0].type == 'mask':
# convert mask tracks to shapes
# because mask track are not supported
annotations.add_shape(annotations.LabeledShape(**{
'attributes': track.shapes[0].attributes,
'points': track.shapes[0].points,
'type': track.shapes[0].type,
'occluded': track.shapes[0].occluded,
'frame': track.shapes[0].frame,
'source': track.shapes[0].source,
'rotation': track.shapes[0].rotation,
'z_order': track.shapes[0].z_order,
'group': track.shapes[0].group,
'label': track.label,
}))
else: else:
annotations.add_track(track) annotations.add_track(track)
track = None track = None
@ -1334,7 +1389,7 @@ def _export_images(dst_file, instance_data, save_images=False):
anno_callback=dump_as_cvat_annotation, save_images=save_images) anno_callback=dump_as_cvat_annotation, save_images=save_images)
@importer(name='CVAT', ext='XML, ZIP', version='1.1') @importer(name='CVAT', ext='XML, ZIP', version='1.1')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
is_zip = zipfile.is_zipfile(src_file) is_zip = zipfile.is_zipfile(src_file)
src_file.seek(0) src_file.seek(0)
if is_zip: if is_zip:

@ -36,7 +36,7 @@ def _export(dst_file, instance_data, save_images=False):
make_zip_archive(tmp_dir, dst_file) make_zip_archive(tmp_dir, dst_file)
@importer(name="Datumaro", ext="ZIP", version="1.0") @importer(name="Datumaro", ext="ZIP", version="1.0")
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
Archive(src_file.name).extractall(tmp_dir) Archive(src_file.name).extractall(tmp_dir)
@ -60,7 +60,7 @@ def _export(dst_file, instance_data, save_images=False):
make_zip_archive(tmp_dir, dst_file) make_zip_archive(tmp_dir, dst_file)
@importer(name="Datumaro 3D", ext="ZIP", version="1.0", dimension=DimensionType.DIM_3D) @importer(name="Datumaro 3D", ext="ZIP", version="1.0", dimension=DimensionType.DIM_3D)
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
Archive(src_file.name).extractall(tmp_dir) Archive(src_file.name).extractall(tmp_dir)

@ -15,7 +15,7 @@ from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor,
import_dm_annotations) import_dm_annotations)
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from .transformations import RotatedBoxesToPolygons from .transformations import MaskToPolygonTransformation, RotatedBoxesToPolygons
from .registry import dm_env, exporter, importer from .registry import dm_env, exporter, importer
@ -87,7 +87,7 @@ def _export_recognition(dst_file, instance_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='ICDAR Recognition', ext='ZIP', version='1.0') @importer(name='ICDAR Recognition', ext='ZIP', version='1.0')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
zipfile.ZipFile(src_file).extractall(tmp_dir) zipfile.ZipFile(src_file).extractall(tmp_dir)
dataset = Dataset.import_from(tmp_dir, 'icdar_word_recognition', env=dm_env) dataset = Dataset.import_from(tmp_dir, 'icdar_word_recognition', env=dm_env)
@ -106,7 +106,7 @@ def _export_localization(dst_file, instance_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='ICDAR Localization', ext='ZIP', version='1.0') @importer(name='ICDAR Localization', ext='ZIP', version='1.0')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
zipfile.ZipFile(src_file).extractall(tmp_dir) zipfile.ZipFile(src_file).extractall(tmp_dir)
@ -130,12 +130,12 @@ def _export_segmentation(dst_file, instance_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='ICDAR Segmentation', ext='ZIP', version='1.0') @importer(name='ICDAR Segmentation', ext='ZIP', version='1.0')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
zipfile.ZipFile(src_file).extractall(tmp_dir) zipfile.ZipFile(src_file).extractall(tmp_dir)
dataset = Dataset.import_from(tmp_dir, 'icdar_text_segmentation', env=dm_env) dataset = Dataset.import_from(tmp_dir, 'icdar_text_segmentation', env=dm_env)
dataset.transform(AddLabelToAnns, label='icdar') dataset.transform(AddLabelToAnns, label='icdar')
dataset.transform('masks_to_polygons') dataset = MaskToPolygonTransformation.convert_dataset(dataset, **kwargs)
if load_data_callback is not None: if load_data_callback is not None:
load_data_callback(dataset, instance_data) load_data_callback(dataset, instance_data)
import_dm_annotations(dataset, instance_data) import_dm_annotations(dataset, instance_data)

@ -29,7 +29,7 @@ def _export(dst_file, instance_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='ImageNet', ext='ZIP', version='1.0') @importer(name='ImageNet', ext='ZIP', version='1.0')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
zipfile.ZipFile(src_file).extractall(tmp_dir) zipfile.ZipFile(src_file).extractall(tmp_dir)
if glob(osp.join(tmp_dir, '*.txt')): if glob(osp.join(tmp_dir, '*.txt')):

@ -13,7 +13,7 @@ from pyunpack import Archive
from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, import_dm_annotations) from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, import_dm_annotations)
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from .transformations import RotatedBoxesToPolygons from .transformations import MaskToPolygonTransformation, RotatedBoxesToPolygons
from .registry import dm_env, exporter, importer from .registry import dm_env, exporter, importer
from .utils import make_colormap from .utils import make_colormap
@ -35,7 +35,7 @@ def _export(dst_file, instance_data, save_images=False):
make_zip_archive(tmp_dir, dst_file) make_zip_archive(tmp_dir, dst_file)
@importer(name='KITTI', ext='ZIP', version='1.0') @importer(name='KITTI', ext='ZIP', version='1.0')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
Archive(src_file.name).extractall(tmp_dir) Archive(src_file.name).extractall(tmp_dir)
@ -49,7 +49,7 @@ def _import(src_file, instance_data, load_data_callback=None):
if 'background' not in [label['name'] for _, label in labels_meta]: if 'background' not in [label['name'] for _, label in labels_meta]:
dataset.filter('/item/annotation[label != "background"]', dataset.filter('/item/annotation[label != "background"]',
filter_annotations=True) filter_annotations=True)
dataset.transform('masks_to_polygons') dataset = MaskToPolygonTransformation.convert_dataset(dataset, **kwargs)
if load_data_callback is not None: if load_data_callback is not None:
load_data_callback(dataset, instance_data) load_data_callback(dataset, instance_data)

@ -9,6 +9,7 @@ from pyunpack import Archive
from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor,
import_dm_annotations) import_dm_annotations)
from cvat.apps.dataset_manager.formats.transformations import MaskToPolygonTransformation
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from .registry import dm_env, exporter, importer from .registry import dm_env, exporter, importer
@ -24,12 +25,12 @@ def _export(dst_file, instance_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='LabelMe', ext='ZIP', version='3.0') @importer(name='LabelMe', ext='ZIP', version='3.0')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
Archive(src_file.name).extractall(tmp_dir) Archive(src_file.name).extractall(tmp_dir)
dataset = Dataset.import_from(tmp_dir, 'label_me', env=dm_env) dataset = Dataset.import_from(tmp_dir, 'label_me', env=dm_env)
dataset.transform('masks_to_polygons') dataset = MaskToPolygonTransformation.convert_dataset(dataset, **kwargs)
if load_data_callback is not None: if load_data_callback is not None:
load_data_callback(dataset, instance_data) load_data_callback(dataset, instance_data)
import_dm_annotations(dataset, instance_data) import_dm_annotations(dataset, instance_data)

@ -14,7 +14,7 @@ from .registry import dm_env, exporter, importer
@importer(name='LFW', ext='ZIP', version='1.0') @importer(name='LFW', ext='ZIP', version='1.0')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
Archive(src_file.name).extractall(tmp_dir) Archive(src_file.name).extractall(tmp_dir)

@ -71,7 +71,7 @@ def _export(dst_file, instance_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='Market-1501', ext='ZIP', version='1.0') @importer(name='Market-1501', ext='ZIP', version='1.0')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
zipfile.ZipFile(src_file).extractall(tmp_dir) zipfile.ZipFile(src_file).extractall(tmp_dir)

@ -11,7 +11,7 @@ from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor,
import_dm_annotations) import_dm_annotations)
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from .transformations import RotatedBoxesToPolygons from .transformations import MaskToPolygonTransformation, RotatedBoxesToPolygons
from .registry import dm_env, exporter, importer from .registry import dm_env, exporter, importer
from .utils import make_colormap from .utils import make_colormap
@ -30,12 +30,12 @@ def _export(dst_file, instance_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='Segmentation mask', ext='ZIP', version='1.1') @importer(name='Segmentation mask', ext='ZIP', version='1.1')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
Archive(src_file.name).extractall(tmp_dir) Archive(src_file.name).extractall(tmp_dir)
dataset = Dataset.import_from(tmp_dir, 'voc', env=dm_env) dataset = Dataset.import_from(tmp_dir, 'voc', env=dm_env)
dataset.transform('masks_to_polygons') dataset = MaskToPolygonTransformation.convert_dataset(dataset, **kwargs)
if load_data_callback is not None: if load_data_callback is not None:
load_data_callback(dataset, instance_data) load_data_callback(dataset, instance_data)
import_dm_annotations(dataset, instance_data) import_dm_annotations(dataset, instance_data)

@ -99,7 +99,7 @@ def _export(dst_file, instance_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='MOT', ext='ZIP', version='1.1') @importer(name='MOT', ext='ZIP', version='1.1')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
Archive(src_file.name).extractall(tmp_dir) Archive(src_file.name).extractall(tmp_dir)

@ -14,7 +14,7 @@ from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor,
find_dataset_root, match_dm_item) find_dataset_root, match_dm_item)
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from .transformations import RotatedBoxesToPolygons from .transformations import MaskToPolygonTransformation, RotatedBoxesToPolygons
from .registry import dm_env, exporter, importer from .registry import dm_env, exporter, importer
@ -109,12 +109,12 @@ def _export(dst_file, instance_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='MOTS PNG', ext='ZIP', version='1.0') @importer(name='MOTS PNG', ext='ZIP', version='1.0')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
Archive(src_file.name).extractall(tmp_dir) Archive(src_file.name).extractall(tmp_dir)
dataset = Dataset.import_from(tmp_dir, 'mots', env=dm_env) dataset = Dataset.import_from(tmp_dir, 'mots', env=dm_env)
dataset.transform('masks_to_polygons') dataset = MaskToPolygonTransformation.convert_dataset(dataset, **kwargs)
if load_data_callback is not None: if load_data_callback is not None:
load_data_callback(dataset, instance_data) load_data_callback(dataset, instance_data)

@ -15,7 +15,7 @@ from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor,
find_dataset_root, import_dm_annotations, match_dm_item) find_dataset_root, import_dm_annotations, match_dm_item)
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from .transformations import RotatedBoxesToPolygons from .transformations import MaskToPolygonTransformation, RotatedBoxesToPolygons
from .registry import dm_env, exporter, importer from .registry import dm_env, exporter, importer
@ -51,7 +51,7 @@ def _export(dst_file, task_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='Open Images V6', ext='ZIP', version='1.0') @importer(name='Open Images V6', ext='ZIP', version='1.0')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
Archive(src_file.name).extractall(tmp_dir) Archive(src_file.name).extractall(tmp_dir)
@ -79,7 +79,7 @@ def _import(src_file, instance_data, load_data_callback=None):
dataset = Dataset.import_from(tmp_dir, 'open_images', dataset = Dataset.import_from(tmp_dir, 'open_images',
image_meta=image_meta, env=dm_env) image_meta=image_meta, env=dm_env)
dataset.transform('masks_to_polygons') dataset = MaskToPolygonTransformation.convert_dataset(dataset, **kwargs)
if load_data_callback is not None: if load_data_callback is not None:
load_data_callback(dataset, instance_data) load_data_callback(dataset, instance_data)
import_dm_annotations(dataset, instance_data) import_dm_annotations(dataset, instance_data)

@ -13,6 +13,7 @@ from datumaro.components.dataset import Dataset
from pyunpack import Archive from pyunpack import Archive
from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, import_dm_annotations) from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, import_dm_annotations)
from cvat.apps.dataset_manager.formats.transformations import MaskToPolygonTransformation
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from .registry import dm_env, exporter, importer from .registry import dm_env, exporter, importer
@ -29,7 +30,7 @@ def _export(dst_file, instance_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='PASCAL VOC', ext='ZIP', version='1.1') @importer(name='PASCAL VOC', ext='ZIP', version='1.1')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
Archive(src_file.name).extractall(tmp_dir) Archive(src_file.name).extractall(tmp_dir)
@ -56,7 +57,7 @@ def _import(src_file, instance_data, load_data_callback=None):
shutil.move(f, anno_dir) shutil.move(f, anno_dir)
dataset = Dataset.import_from(tmp_dir, 'voc', env=dm_env) dataset = Dataset.import_from(tmp_dir, 'voc', env=dm_env)
dataset.transform('masks_to_polygons') dataset = MaskToPolygonTransformation.convert_dataset(dataset, **kwargs)
if load_data_callback is not None: if load_data_callback is not None:
load_data_callback(dataset, instance_data) load_data_callback(dataset, instance_data)
import_dm_annotations(dataset, instance_data) import_dm_annotations(dataset, instance_data)

@ -28,7 +28,7 @@ def _export_images(dst_file, task_data, save_images=False):
@importer(name='Sly Point Cloud Format', ext='ZIP', version='1.0', dimension=DimensionType.DIM_3D) @importer(name='Sly Point Cloud Format', ext='ZIP', version='1.0', dimension=DimensionType.DIM_3D)
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
if zipfile.is_zipfile(src_file): if zipfile.is_zipfile(src_file):

@ -32,7 +32,7 @@ def _export(dst_file, instance_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='TFRecord', ext='ZIP', version='1.0', enabled=tf_available) @importer(name='TFRecord', ext='ZIP', version='1.0', enabled=tf_available)
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
Archive(src_file.name).extractall(tmp_dir) Archive(src_file.name).extractall(tmp_dir)

@ -36,6 +36,28 @@ class RotatedBoxesToPolygons(ItemTransform):
return item.wrap(annotations=annotations) return item.wrap(annotations=annotations)
class CVATRleToCOCORle(ItemTransform):
@staticmethod
def convert_mask(shape, img_h, img_w):
rle = shape.points[:-4]
left, top, right = list(math.trunc(v) for v in shape.points[-4:-1])
mat = np.zeros((img_h, img_w), dtype=np.uint8)
width = right - left + 1
value = 0
offset = 0
for rleCount in rle:
rleCount = math.trunc(rleCount)
while rleCount > 0:
x, y = offset % width, offset // width
mat[y + top][x + left] = value
rleCount -= 1
offset += 1
value = abs(value - 1)
rle = mask_utils.encode(np.asfortranarray(mat))
return dm.RleMask(rle=rle, label=shape.label, z_order=shape.z_order,
attributes=shape.attributes, group=shape.group)
class EllipsesToMasks: class EllipsesToMasks:
@staticmethod @staticmethod
def convert_ellipse(ellipse, img_h, img_w): def convert_ellipse(ellipse, img_h, img_w):
@ -50,3 +72,19 @@ class EllipsesToMasks:
rle = mask_utils.encode(np.asfortranarray(mat)) rle = mask_utils.encode(np.asfortranarray(mat))
return dm.RleMask(rle=rle, label=ellipse.label, z_order=ellipse.z_order, return dm.RleMask(rle=rle, label=ellipse.label, z_order=ellipse.z_order,
attributes=ellipse.attributes, group=ellipse.group) attributes=ellipse.attributes, group=ellipse.group)
class MaskToPolygonTransformation:
"""
Manages common logic for mask to polygons conversion in dataset import.
This usecase is supposed for backward compatibility for the transition period.
"""
@classmethod
def declare_arg_names(cls):
return ['conv_mask_to_poly']
@classmethod
def convert_dataset(cls, dataset, **kwargs):
if kwargs.get('conv_mask_to_poly', True):
dataset.transform('masks_to_polygons')
return dataset

@ -29,7 +29,7 @@ def _export_images(dst_file, task_data, save_images=False):
@importer(name='Kitti Raw Format', ext='ZIP', version='1.0', dimension=DimensionType.DIM_3D) @importer(name='Kitti Raw Format', ext='ZIP', version='1.0', dimension=DimensionType.DIM_3D)
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
if zipfile.is_zipfile(src_file): if zipfile.is_zipfile(src_file):
zipfile.ZipFile(src_file).extractall(tmp_dir) zipfile.ZipFile(src_file).extractall(tmp_dir)

@ -25,7 +25,7 @@ def _export(dst_file, instance_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='VGGFace2', ext='ZIP', version='1.0') @importer(name='VGGFace2', ext='ZIP', version='1.0')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
zipfile.ZipFile(src_file).extractall(tmp_dir) zipfile.ZipFile(src_file).extractall(tmp_dir)

@ -24,7 +24,7 @@ def _export(dst_file, instance_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='WiderFace', ext='ZIP', version='1.0') @importer(name='WiderFace', ext='ZIP', version='1.0')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
zipfile.ZipFile(src_file).extractall(tmp_dir) zipfile.ZipFile(src_file).extractall(tmp_dir)

@ -28,7 +28,7 @@ def _export(dst_file, instance_data, save_images=False):
make_zip_archive(temp_dir, dst_file) make_zip_archive(temp_dir, dst_file)
@importer(name='YOLO', ext='ZIP', version='1.1') @importer(name='YOLO', ext='ZIP', version='1.1')
def _import(src_file, instance_data, load_data_callback=None): def _import(src_file, instance_data, load_data_callback=None, **kwargs):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
Archive(src_file.name).extractall(tmp_dir) Archive(src_file.name).extractall(tmp_dir)

@ -132,7 +132,7 @@ class ProjectAnnotationAndData:
def load_dataset_data(self, *args, **kwargs): def load_dataset_data(self, *args, **kwargs):
load_dataset_data(self, *args, **kwargs) load_dataset_data(self, *args, **kwargs)
def import_dataset(self, dataset_file, importer): def import_dataset(self, dataset_file, importer, **options):
project_data = ProjectData( project_data = ProjectData(
annotation_irs=self.annotation_irs, annotation_irs=self.annotation_irs,
db_project=self.db_project, db_project=self.db_project,
@ -141,7 +141,7 @@ class ProjectAnnotationAndData:
) )
project_data.soft_attribute_import = True project_data.soft_attribute_import = True
importer(dataset_file, project_data, self.load_dataset_data) importer(dataset_file, project_data, self.load_dataset_data, **options)
self.create({tid: ir.serialize() for tid, ir in self.annotation_irs.items() if tid in project_data.new_tasks}) self.create({tid: ir.serialize() for tid, ir in self.annotation_irs.items() if tid in project_data.new_tasks})
@ -150,7 +150,7 @@ class ProjectAnnotationAndData:
raise NotImplementedError() raise NotImplementedError()
@transaction.atomic @transaction.atomic
def import_dataset_as_project(project_id, dataset_file, format_name): def import_dataset_as_project(project_id, dataset_file, format_name, conv_mask_to_poly):
rq_job = rq.get_current_job() rq_job = rq.get_current_job()
rq_job.meta['status'] = 'Dataset import has been started...' rq_job.meta['status'] = 'Dataset import has been started...'
rq_job.meta['progress'] = 0. rq_job.meta['progress'] = 0.
@ -161,4 +161,4 @@ def import_dataset_as_project(project_id, dataset_file, format_name):
importer = make_importer(format_name) importer = make_importer(format_name)
with open(dataset_file, 'rb') as f: with open(dataset_file, 'rb') as f:
project.import_dataset(f, importer) project.import_dataset(f, importer, conv_mask_to_poly=conv_mask_to_poly)

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save