Improved interface of interactors on UI (#2054)

main
Boris Sekachev 6 years ago committed by GitHub
parent 174fe1690b
commit 908e0569d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
-
### Changed
-
- UI models (like DEXTR) were redesigned to be more interactive (<https://github.com/opencv/cvat/pull/2054>)
### Deprecated
-

@ -46,6 +46,7 @@ Canvas itself handles:
IDLE = 'idle',
DRAG = 'drag',
RESIZE = 'resize',
INTERACT = 'interact',
DRAW = 'draw',
EDIT = 'edit',
MERGE = 'merge',
@ -70,6 +71,11 @@ Canvas itself handles:
crosshair?: boolean;
}
interface InteractionData {
shapeType: string;
minVertices?: number;
}
interface GroupData {
enabled: boolean;
resetGroup?: boolean;
@ -83,6 +89,12 @@ Canvas itself handles:
enabled: boolean;
}
interface InteractionResult {
points: number[];
shapeType: string;
button: number;
};
interface DrawnData {
shapeType: string;
points: number[];
@ -104,6 +116,7 @@ Canvas itself handles:
grid(stepX: number, stepY: number): void;
draw(drawData: DrawData): void;
interact(interactionData: InteractionData): void;
group(groupData: GroupData): void;
split(splitData: SplitData): void;
merge(mergeData: MergeData): void;
@ -146,6 +159,7 @@ Standard JS events are used.
- canvas.moved => {states: ObjectState[], x: number, y: number}
- canvas.find => {states: ObjectState[], x: number, y: number}
- canvas.drawn => {state: DrawnData}
- canvas.interacted => {shapes: InteractionResult[]}
- canvas.editstart
- canvas.edited => {state: ObjectState, points: number[]}
- canvas.splitted => {state: ObjectState}
@ -187,25 +201,26 @@ Standard JS events are used.
## API Reaction
| | IDLE | GROUP | SPLIT | DRAW | MERGE | EDIT | DRAG | RESIZE | ZOOM_CANVAS | DRAG_CANVAS |
|--------------|------|-------|-------|------|-------|------|------|--------|-------------|-------------|
| setup() | + | + | + | +/- | + | +/- | +/- | +/- | + | + |
| activate() | + | - | - | - | - | - | - | - | - | - |
| rotate() | + | + | + | + | + | + | + | + | + | + |
| focus() | + | + | + | + | + | + | + | + | + | + |
| fit() | + | + | + | + | + | + | + | + | + | + |
| grid() | + | + | + | + | + | + | + | + | + | + |
| draw() | + | - | - | - | - | - | - | - | - | - |
| split() | + | - | + | - | - | - | - | - | - | - |
| group() | + | + | - | - | - | - | - | - | - | - |
| merge() | + | - | - | - | + | - | - | - | - | - |
| fitCanvas() | + | + | + | + | + | + | + | + | + | + |
| dragCanvas() | + | - | - | - | - | - | + | - | - | + |
| zoomCanvas() | + | - | - | - | - | - | - | + | + | - |
| cancel() | - | + | + | + | + | + | + | + | + | + |
| configure() | + | + | + | + | + | + | + | + | + | + |
| bitmap() | + | + | + | + | + | + | + | + | + | + |
| setZLayer() | + | + | + | + | + | + | + | + | + | + |
| | IDLE | GROUP | SPLIT | DRAW | MERGE | EDIT | DRAG | RESIZE | ZOOM_CANVAS | DRAG_CANVAS | INTERACT |
|--------------|------|-------|-------|------|-------|------|------|--------|-------------|-------------|----------|
| setup() | + | + | + | +/- | + | +/- | +/- | +/- | + | + | + |
| activate() | + | - | - | - | - | - | - | - | - | - | - |
| rotate() | + | + | + | + | + | + | + | + | + | + | + |
| focus() | + | + | + | + | + | + | + | + | + | + | + |
| fit() | + | + | + | + | + | + | + | + | + | + | + |
| grid() | + | + | + | + | + | + | + | + | + | + | + |
| draw() | + | - | - | + | - | - | - | - | - | - | - |
| interact() | + | - | - | - | - | - | - | - | - | - | + |
| split() | + | - | + | - | - | - | - | - | - | - | - |
| group() | + | + | - | - | - | - | - | - | - | - | - |
| merge() | + | - | - | - | + | - | - | - | - | - | - |
| fitCanvas() | + | + | + | + | + | + | + | + | + | + | + |
| dragCanvas() | + | - | - | - | - | - | + | - | - | + | - |
| zoomCanvas() | + | - | - | - | - | - | - | + | + | - | - |
| cancel() | - | + | + | + | + | + | + | + | + | + | + |
| configure() | + | + | + | + | + | + | + | + | + | + | + |
| bitmap() | + | + | + | + | + | + | + | + | + | + | + |
| setZLayer() | + | + | + | + | + | + | + | + | + | + | + |
You can call setup() during editing, dragging, and resizing only to update objects, not to change a frame.
You can change frame during draw only when you do not redraw an existing object

@ -1,6 +1,6 @@
{
"name": "cvat-canvas",
"version": "2.0.2",
"version": "2.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

@ -1,6 +1,6 @@
{
"name": "cvat-canvas",
"version": "2.0.2",
"version": "2.1.0",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts",
"scripts": {

@ -8,26 +8,17 @@ import {
MergeData,
SplitData,
GroupData,
InteractionData,
InteractionResult,
CanvasModel,
CanvasModelImpl,
RectDrawingMethod,
CuboidDrawingMethod,
Configuration,
} from './canvasModel';
import {
Master,
} from './master';
import {
CanvasController,
CanvasControllerImpl,
} from './canvasController';
import {
CanvasView,
CanvasViewImpl,
} from './canvasView';
import { Master } from './master';
import { CanvasController, CanvasControllerImpl } from './canvasController';
import { CanvasView, CanvasViewImpl } from './canvasView';
import '../scss/canvas.scss';
import pjson from '../../package.json';
@ -43,6 +34,7 @@ interface Canvas {
fit(): void;
grid(stepX: number, stepY: number): void;
interact(interactionData: InteractionData): void;
draw(drawData: DrawData): void;
group(groupData: GroupData): void;
split(splitData: SplitData): void;
@ -118,6 +110,10 @@ class CanvasImpl implements Canvas {
this.model.grid(stepX, stepY);
}
public interact(interactionData: InteractionData): void {
this.model.interact(interactionData);
}
public draw(drawData: DrawData): void {
this.model.draw(drawData);
}
@ -162,4 +158,6 @@ export {
RectDrawingMethod,
CuboidDrawingMethod,
Mode as CanvasMode,
InteractionData,
InteractionResult,
};

@ -13,6 +13,7 @@ import {
SplitData,
GroupData,
Mode,
InteractionData,
} from './canvasModel';
export interface CanvasController {
@ -21,6 +22,7 @@ export interface CanvasController {
readonly focusData: FocusData;
readonly activeElement: ActiveElement;
readonly drawData: DrawData;
readonly interactionData: InteractionData;
readonly mergeData: MergeData;
readonly splitData: SplitData;
readonly groupData: GroupData;
@ -30,6 +32,7 @@ export interface CanvasController {
zoom(x: number, y: number, direction: number): void;
draw(drawData: DrawData): void;
interact(interactionData: InteractionData): void;
merge(mergeData: MergeData): void;
split(splitData: SplitData): void;
group(groupData: GroupData): void;
@ -84,6 +87,10 @@ export class CanvasControllerImpl implements CanvasController {
this.model.draw(drawData);
}
public interact(interactionData: InteractionData): void {
this.model.interact(interactionData);
}
public merge(mergeData: MergeData): void {
this.model.merge(mergeData);
}
@ -124,6 +131,10 @@ export class CanvasControllerImpl implements CanvasController {
return this.model.drawData;
}
public get interactionData(): InteractionData {
return this.model.interactionData;
}
public get mergeData(): MergeData {
return this.model.mergeData;
}

@ -69,6 +69,20 @@ export interface DrawData {
redraw?: number;
}
export interface InteractionData {
enabled: boolean;
shapeType?: string;
crosshair?: boolean;
minPosVertices?: number;
minNegVertices?: number;
}
export interface InteractionResult {
points: number[];
shapeType: string;
button: number;
}
export interface EditData {
enabled: boolean;
state: any;
@ -105,6 +119,7 @@ export enum UpdateReasons {
FITTED_CANVAS = 'fitted_canvas',
INTERACT = 'interact',
DRAW = 'draw',
MERGE = 'merge',
SPLIT = 'split',
@ -126,6 +141,7 @@ export enum Mode {
MERGE = 'merge',
SPLIT = 'split',
GROUP = 'group',
INTERACT = 'interact',
DRAG_CANVAS = 'drag_canvas',
ZOOM_CANVAS = 'zoom_canvas',
}
@ -139,6 +155,7 @@ export interface CanvasModel {
readonly focusData: FocusData;
readonly activeElement: ActiveElement;
readonly drawData: DrawData;
readonly interactionData: InteractionData;
readonly mergeData: MergeData;
readonly splitData: SplitData;
readonly groupData: GroupData;
@ -162,6 +179,7 @@ export interface CanvasModel {
split(splitData: SplitData): void;
merge(mergeData: MergeData): void;
select(objectState: any): void;
interact(interactionData: InteractionData): void;
fitCanvas(width: number, height: number): void;
bitmap(enabled: boolean): void;
@ -192,6 +210,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
top: number;
zLayer: number | null;
drawData: DrawData;
interactionData: InteractionData;
mergeData: MergeData;
groupData: GroupData;
splitData: SplitData;
@ -242,6 +261,9 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
enabled: false,
initialState: null,
},
interactionData: {
enabled: false,
},
mergeData: {
enabled: false,
},
@ -490,6 +512,27 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.notify(UpdateReasons.DRAW);
}
public interact(interactionData: InteractionData): void {
if (![Mode.IDLE, Mode.INTERACT].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (interactionData.enabled) {
if (this.data.interactionData.enabled) {
throw new Error('Interaction has been already started');
} else if (!interactionData.shapeType) {
throw new Error('A shape type was not specified');
}
}
this.data.interactionData = interactionData;
if (typeof (this.data.interactionData.crosshair) !== 'boolean') {
this.data.interactionData.crosshair = true;
}
this.notify(UpdateReasons.INTERACT);
}
public split(splitData: SplitData): void {
if (![Mode.IDLE, Mode.SPLIT].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
@ -567,7 +610,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
public isAbleToChangeFrame(): boolean {
const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE].includes(this.data.mode)
const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE, Mode.INTERACT].includes(this.data.mode)
|| (this.data.mode === Mode.DRAW && typeof (this.data.drawData.redraw) === 'number');
return !isUnable;
@ -647,6 +690,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return { ...this.data.drawData };
}
public get interactionData(): InteractionData {
return { ...this.data.interactionData };
}
public get mergeData(): MergeData {
return { ...this.data.mergeData };
}

@ -16,6 +16,7 @@ import { MergeHandler, MergeHandlerImpl } from './mergeHandler';
import { SplitHandler, SplitHandlerImpl } from './splitHandler';
import { GroupHandler, GroupHandlerImpl } from './groupHandler';
import { ZoomHandler, ZoomHandlerImpl } from './zoomHandler';
import { InteractionHandler, InteractionHandlerImpl } from './interactionHandler';
import { AutoborderHandler, AutoborderHandlerImpl } from './autoborderHandler';
import consts from './consts';
import {
@ -42,6 +43,8 @@ import {
Mode,
Size,
Configuration,
InteractionResult,
InteractionData,
} from './canvasModel';
export interface CanvasView {
@ -72,6 +75,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private groupHandler: GroupHandler;
private zoomHandler: ZoomHandler;
private autoborderHandler: AutoborderHandler;
private interactionHandler: InteractionHandler;
private activeElement: ActiveElement;
private configuration: Configuration;
private serviceFlags: {
@ -127,6 +131,41 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}
private onInteraction(
shapes: InteractionResult[] | null,
shapesUpdated: boolean = true,
isDone: boolean = false,
): void {
const { zLayer } = this.controller;
if (Array.isArray(shapes)) {
const event: CustomEvent = new CustomEvent('canvas.interacted', {
bubbles: false,
cancelable: true,
detail: {
shapesUpdated,
isDone,
shapes,
zOrder: zLayer || 0,
},
});
this.canvas.dispatchEvent(event);
}
if (shapes === null || isDone) {
const event: CustomEvent = new CustomEvent('canvas.canceled', {
bubbles: false,
cancelable: true,
});
this.canvas.dispatchEvent(event);
this.mode = Mode.IDLE;
this.controller.interact({
enabled: false,
});
}
}
private onDrawDone(data: object | null, duration: number, continueDraw?: boolean): void {
const hiddenBecauseOfDraw = Object.keys(this.serviceFlags.drawHidden)
.map((_clientID): number => +_clientID);
@ -373,6 +412,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.drawHandler.transform(this.geometry);
this.editHandler.transform(this.geometry);
this.zoomHandler.transform(this.geometry);
this.autoborderHandler.transform(this.geometry);
this.interactionHandler.transform(this.geometry);
}
private transformCanvas(): void {
@ -438,7 +479,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
// Transform handlers
this.drawHandler.transform(this.geometry);
this.editHandler.transform(this.geometry);
this.zoomHandler.transform(this.geometry);
this.autoborderHandler.transform(this.geometry);
this.interactionHandler.transform(this.geometry);
}
private resizeCanvas(): void {
@ -846,6 +889,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.adoptedContent,
this.geometry,
);
this.interactionHandler = new InteractionHandlerImpl(
this.onInteraction.bind(this),
this.adoptedContent,
this.geometry,
);
// Setup event handlers
this.content.addEventListener('dblclick', (e: MouseEvent): void => {
@ -1063,6 +1111,18 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.drawHandler.draw(data, this.geometry);
}
}
} else if (reason === UpdateReasons.INTERACT) {
const data: InteractionData = this.controller.interactionData;
if (data.enabled && this.mode === Mode.IDLE) {
this.canvas.style.cursor = 'crosshair';
this.mode = Mode.INTERACT;
this.interactionHandler.interact(data);
} else {
this.canvas.style.cursor = '';
if (this.mode !== Mode.IDLE) {
this.interactionHandler.interact(data);
}
}
} else if (reason === UpdateReasons.MERGE) {
const data: MergeData = this.controller.mergeData;
if (data.enabled) {
@ -1101,6 +1161,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
} else if (reason === UpdateReasons.CANCEL) {
if (this.mode === Mode.DRAW) {
this.drawHandler.cancel();
} else if (this.mode === Mode.INTERACT) {
this.interactionHandler.cancel();
} else if (this.mode === Mode.MERGE) {
this.mergeHandler.cancel();
} else if (this.mode === Mode.SPLIT) {
@ -1405,6 +1467,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
[state, +state.getAttribute('data-z-order')]
));
const crosshair = Array.from(this.content.getElementsByClassName('cvat_canvas_crosshair'));
crosshair.forEach((line: SVGLineElement): void => this.content.append(line));
const interaction = Array.from(this.content.getElementsByClassName('cvat_interaction_point'));
interaction.forEach((circle: SVGCircleElement): void => this.content.append(circle));
const needSort = states.some((pair): boolean => pair[1] !== states[0][1]);
if (!states.length || !needSort) {
return;

@ -0,0 +1,70 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js';
import consts from './consts';
export default class Crosshair {
private x: SVG.Line | null;
private y: SVG.Line | null;
private canvas: SVG.Container | null;
public constructor() {
this.x = null;
this.y = null;
this.canvas = null;
}
public show(canvas: SVG.Container, x: number, y: number, scale: number): void {
if (this.canvas && this.canvas !== canvas) {
if (this.x) this.x.remove();
if (this.y) this.y.remove();
this.x = null;
this.y = null;
}
this.canvas = canvas;
this.x = this.canvas.line(0, y, this.canvas.node.clientWidth, y).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * scale),
}).addClass('cvat_canvas_crosshair');
this.y = this.canvas.line(x, 0, x, this.canvas.node.clientHeight).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * scale),
}).addClass('cvat_canvas_crosshair');
}
public hide(): void {
if (this.x) {
this.x.remove();
this.x = null;
}
if (this.y) {
this.y.remove();
this.y = null;
}
this.canvas = null;
}
public move(x: number, y: number): void {
if (this.x) {
this.x.attr({ y1: y, y2: y });
}
if (this.y) {
this.y.attr({ x1: x, x2: x });
}
}
public scale(scale: number): void {
if (this.x) {
this.x.attr('stroke-width', consts.BASE_STROKE_WIDTH / (2 * scale));
}
if (this.y) {
this.y.attr('stroke-width', consts.BASE_STROKE_WIDTH / (2 * scale));
}
}
}

@ -16,6 +16,7 @@ import {
BBox,
Box,
} from './shared';
import Crosshair from './crosshair';
import consts from './consts';
import {
DrawData,
@ -44,10 +45,7 @@ export class DrawHandlerImpl implements DrawHandler {
x: number;
y: number;
};
private crosshair: {
x: SVG.Line;
y: SVG.Line;
};
private crosshair: Crosshair;
private drawData: DrawData;
private geometry: Geometry;
private autoborderHandler: AutoborderHandler;
@ -188,22 +186,11 @@ export class DrawHandlerImpl implements DrawHandler {
private addCrosshair(): void {
const { x, y } = this.cursorPosition;
this.crosshair = {
x: this.canvas.line(0, y, this.canvas.node.clientWidth, y).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
zOrder: Number.MAX_SAFE_INTEGER,
}).addClass('cvat_canvas_crosshair'),
y: this.canvas.line(x, 0, x, this.canvas.node.clientHeight).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
zOrder: Number.MAX_SAFE_INTEGER,
}).addClass('cvat_canvas_crosshair'),
};
this.crosshair.show(this.canvas, x, y, this.geometry.scale);
}
private removeCrosshair(): void {
this.crosshair.x.remove();
this.crosshair.y.remove();
this.crosshair = null;
this.crosshair.hide();
}
private release(): void {
@ -741,7 +728,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.canceled = false;
this.drawData = null;
this.geometry = null;
this.crosshair = null;
this.crosshair = new Crosshair();
this.drawInstance = null;
this.pointsGroup = null;
this.cursorPosition = {
@ -756,8 +743,7 @@ export class DrawHandlerImpl implements DrawHandler {
);
this.cursorPosition = { x, y };
if (this.crosshair) {
this.crosshair.x.attr({ y1: y, y2: y });
this.crosshair.y.attr({ x1: x, x2: x });
this.crosshair.move(x, y);
}
});
}
@ -787,12 +773,7 @@ export class DrawHandlerImpl implements DrawHandler {
}
if (this.crosshair) {
this.crosshair.x.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * geometry.scale),
});
this.crosshair.y.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * geometry.scale),
});
this.crosshair.scale(this.geometry.scale);
}
if (this.pointsGroup) {

@ -115,7 +115,7 @@ export class EditHandlerImpl implements EditHandler {
(this.editLine as any).addClass('cvat_canvas_shape_drawing').style({
'pointer-events': 'none',
'fill-opacity': 0,
'stroke': strokeColor,
stroke: strokeColor,
}).attr({
'data-origin-client-id': this.editData.state.clientID,
}).on('drawstart drawpoint', (e: CustomEvent): void => {
@ -213,20 +213,20 @@ export class EditHandlerImpl implements EditHandler {
const cutIndexes2 = oldPoints.reduce((acc: string[], _: string, i: number) =>
i <= stop && i >= start ? [...acc, i] : acc, []);
const curveLength = (indexes: number[]) => {
const curveLength = (indexes: number[]): number => {
const points = indexes.map((index: number): string => oldPoints[index])
.map((point: string): string[] => point.split(','))
.map((point: string[]): number[] => [+point[0], +point[1]]);
let length = 0;
for (let i = 1; i < points.length; i++) {
length += Math.sqrt(
(points[i][0] - points[i - 1][0]) ** 2
+ (points[i][1] - points[i - 1][1]) ** 2,
((points[i][0] - points[i - 1][0]) ** 2)
+ ((points[i][1] - points[i - 1][1]) ** 2),
);
}
return length;
}
};
const pointsCriteria = cutIndexes1.length > cutIndexes2.length;
const lengthCriteria = curveLength(cutIndexes1) > curveLength(cutIndexes2);
@ -278,8 +278,6 @@ export class EditHandlerImpl implements EditHandler {
});
}
}
return;
}
private setupPoints(enabled: boolean): void {

@ -0,0 +1,281 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js';
import consts from './consts';
import Crosshair from './crosshair';
import { translateToSVG } from './shared';
import { InteractionData, InteractionResult, Geometry } from './canvasModel';
export interface InteractionHandler {
transform(geometry: Geometry): void;
interact(interactData: InteractionData): void;
cancel(): void;
}
export class InteractionHandlerImpl implements InteractionHandler {
private onInteraction: (
shapes: InteractionResult[] | null,
shapesUpdated?: boolean,
isDone?: boolean,
) => void;
private geometry: Geometry;
private canvas: SVG.Container;
private interactionData: InteractionData;
private cursorPosition: { x: number; y: number };
private shapesWereUpdated: boolean;
private interactionShapes: SVG.Shape[];
private currentInteractionShape: SVG.Shape | null;
private crosshair: Crosshair;
private prepareResult(): InteractionResult[] {
return this.interactionShapes.map((shape: SVG.Shape): InteractionResult => {
if (shape.type === 'circle') {
const points = [(shape as SVG.Circle).cx(), (shape as SVG.Circle).cy()];
return {
points: points.map((coord: number): number => coord - this.geometry.offset),
shapeType: 'points',
button: shape.attr('stroke') === 'green' ? 0 : 2,
};
}
const bbox = (shape.node as any as SVGRectElement).getBBox();
const points = [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height];
return {
points: points.map((coord: number): number => coord - this.geometry.offset),
shapeType: 'rectangle',
button: 0,
};
});
}
private shouldRaiseEvent(ctrlKey: boolean): boolean {
const { interactionData, interactionShapes, shapesWereUpdated } = this;
const { minPosVertices, minNegVertices, enabled } = interactionData;
const positiveShapes = interactionShapes
.filter((shape: SVG.Shape): boolean => (shape as any).attr('stroke') === 'green');
const negativeShapes = interactionShapes
.filter((shape: SVG.Shape): boolean => (shape as any).attr('stroke') !== 'green');
if (interactionData.shapeType === 'rectangle') {
return enabled && !ctrlKey && !!interactionShapes.length;
}
const minimumVerticesAchieved = (typeof (minPosVertices) === 'undefined'
|| minPosVertices <= positiveShapes.length) && (typeof (minNegVertices) === 'undefined'
|| minPosVertices <= negativeShapes.length);
return enabled && !ctrlKey && minimumVerticesAchieved && shapesWereUpdated;
}
private addCrosshair(): void {
const { x, y } = this.cursorPosition;
this.crosshair.show(this.canvas, x, y, this.geometry.scale);
}
private removeCrosshair(): void {
this.crosshair.hide();
}
private interactPoints(): void {
const eventListener = (e: MouseEvent): void => {
if ((e.button === 0 || e.button === 2) && !e.altKey) {
e.preventDefault();
const [cx, cy] = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
);
this.currentInteractionShape = this.canvas
.circle(consts.BASE_POINT_SIZE * 2 / this.geometry.scale).center(cx, cy)
.fill('white')
.stroke(e.button === 0 ? 'green' : 'red')
.addClass('cvat_interaction_point')
.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale,
});
this.interactionShapes.push(this.currentInteractionShape);
this.shapesWereUpdated = true;
if (this.shouldRaiseEvent(e.ctrlKey)) {
this.onInteraction(this.prepareResult(), true, false);
}
const self = this.currentInteractionShape;
self.on('mouseenter', (): void => {
self.attr({
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale,
});
self.on('mousedown', (_e: MouseEvent): void => {
_e.preventDefault();
_e.stopPropagation();
self.remove();
this.interactionShapes = this.interactionShapes.filter(
(shape: SVG.Shape): boolean => shape !== self,
);
this.shapesWereUpdated = true;
if (this.shouldRaiseEvent(_e.ctrlKey)) {
this.onInteraction(this.prepareResult(), true, false);
}
});
});
self.on('mouseleave', (): void => {
self.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale,
});
self.off('mousedown');
});
}
};
// clear this listener in relese()
this.canvas.on('mousedown.interaction', eventListener);
}
private interactRectangle(): void {
let initialized = false;
const eventListener = (e: MouseEvent): void => {
if (e.button === 0 && !e.altKey) {
if (!initialized) {
(this.currentInteractionShape as any).draw(e, { snapToGrid: 0.1 });
initialized = true;
} else {
(this.currentInteractionShape as any).draw(e);
}
}
};
this.currentInteractionShape = this.canvas.rect();
this.canvas.on('mousedown.interaction', eventListener);
this.currentInteractionShape.on('drawstop', (): void => {
this.interactionShapes.push(this.currentInteractionShape);
this.shapesWereUpdated = true;
this.canvas.off('mousedown.interaction', eventListener);
if (this.shouldRaiseEvent(false)) {
this.onInteraction(this.prepareResult(), true, false);
}
this.interact({ enabled: false });
}).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
}
private initInteraction(): void {
if (this.interactionData.crosshair) {
this.addCrosshair();
}
}
private startInteraction(): void {
if (this.interactionData.shapeType === 'rectangle') {
this.interactRectangle();
} else if (this.interactionData.shapeType === 'points') {
this.interactPoints();
} else {
throw new Error('Interactor implementation supports only rectangle and points');
}
}
private release(): void {
if (this.crosshair) {
this.removeCrosshair();
}
this.canvas.off('mousedown.interaction');
this.interactionShapes.forEach((shape: SVG.Shape): SVG.Shape => shape.remove());
this.interactionShapes = [];
if (this.currentInteractionShape) {
this.currentInteractionShape.remove();
this.currentInteractionShape = null;
}
}
public constructor(
onInteraction: (
shapes: InteractionResult[] | null,
shapesUpdated?: boolean,
isDone?: boolean,
) => void,
canvas: SVG.Container,
geometry: Geometry,
) {
this.onInteraction = (
shapes: InteractionResult[] | null,
shapesUpdated?: boolean,
isDone?: boolean,
): void => {
this.shapesWereUpdated = false;
onInteraction(shapes, shapesUpdated, isDone);
};
this.canvas = canvas;
this.geometry = geometry;
this.shapesWereUpdated = false;
this.interactionShapes = [];
this.interactionData = { enabled: false };
this.currentInteractionShape = null;
this.crosshair = new Crosshair();
this.cursorPosition = {
x: 0,
y: 0,
};
this.canvas.on('mousemove.interaction', (e: MouseEvent): void => {
const [x, y] = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
);
this.cursorPosition = { x, y };
if (this.crosshair) {
this.crosshair.move(x, y);
}
});
document.body.addEventListener('keyup', (e: KeyboardEvent): void => {
if (e.keyCode === 17 && this.shouldRaiseEvent(false)) { // 17 is ctrl
this.onInteraction(this.prepareResult(), true, false);
}
});
}
public transform(geometry: Geometry): void {
this.geometry = geometry;
if (this.crosshair) {
this.crosshair.scale(this.geometry.scale);
}
const shapesToBeScaled = this.currentInteractionShape
? [...this.interactionShapes, this.currentInteractionShape]
: [...this.interactionShapes];
for (const shape of shapesToBeScaled) {
if (shape.type === 'circle') {
(shape as SVG.Circle).radius(consts.BASE_POINT_SIZE / this.geometry.scale);
shape.attr('stroke-width', consts.POINTS_STROKE_WIDTH / this.geometry.scale);
} else {
shape.attr('stroke-width', consts.BASE_STROKE_WIDTH / this.geometry.scale);
}
}
}
public interact(interactionData: InteractionData): void {
if (interactionData.enabled) {
this.interactionData = interactionData;
this.initInteraction();
this.startInteraction();
} else {
this.onInteraction(this.prepareResult(), this.shouldRaiseEvent(false), true);
this.release();
this.interactionData = interactionData;
}
}
public cancel(): void {
this.release();
this.onInteraction(null);
}
}

@ -7,9 +7,14 @@ const MAX_HISTORY_LENGTH = 128;
class AnnotationHistory {
constructor() {
this.frozen = false;
this.clear();
}
freeze(frozen) {
this.frozen = frozen;
}
get() {
return {
undo: this._undo.map((undo) => [undo.action, undo.frame]),
@ -18,6 +23,7 @@ class AnnotationHistory {
}
do(action, undo, redo, clientIDs, frame) {
if (this.frozen) return;
const actionItem = {
clientIDs,
action,

@ -327,6 +327,19 @@
);
}
function freezeHistory(session, frozen) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).history.freeze(frozen);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function clearActions(session) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
@ -372,6 +385,7 @@
exportDataset,
undoActions,
redoActions,
freezeHistory,
clearActions,
getActions,
closeSession,

@ -170,6 +170,11 @@
.apiWrapper.call(this, prototype.actions.redo, count);
return result;
},
async freeze(frozen) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.actions.freeze, frozen);
return result;
},
async clear() {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.actions.clear);
@ -545,6 +550,14 @@
* @instance
* @async
*/
/**
* Freeze history (do not save new actions)
* @method freeze
* @memberof Session.actions
* @throws {module:API.cvat.exceptions.PluginError}
* @instance
* @async
*/
/**
* Remove all actions from history
* @method clear
@ -745,6 +758,7 @@
this.actions = {
undo: Object.getPrototypeOf(this).actions.undo.bind(this),
redo: Object.getPrototypeOf(this).actions.redo.bind(this),
freeze: Object.getPrototypeOf(this).actions.freeze.bind(this),
clear: Object.getPrototypeOf(this).actions.clear.bind(this),
get: Object.getPrototypeOf(this).actions.get.bind(this),
};
@ -1299,6 +1313,7 @@
this.actions = {
undo: Object.getPrototypeOf(this).actions.undo.bind(this),
redo: Object.getPrototypeOf(this).actions.redo.bind(this),
freeze: Object.getPrototypeOf(this).actions.freeze.bind(this),
clear: Object.getPrototypeOf(this).actions.clear.bind(this),
get: Object.getPrototypeOf(this).actions.get.bind(this),
};
@ -1390,6 +1405,7 @@
exportDataset,
undoActions,
redoActions,
freezeHistory,
clearActions,
getActions,
closeSession,
@ -1582,6 +1598,11 @@
return result;
};
Job.prototype.actions.freeze.implementation = function (frozen) {
const result = freezeHistory(this, frozen);
return result;
};
Job.prototype.actions.clear.implementation = function () {
const result = clearActions(this);
return result;
@ -1846,6 +1867,11 @@
return result;
};
Task.prototype.actions.freeze.implementation = function (frozen) {
const result = freezeHistory(this, frozen);
return result;
};
Task.prototype.actions.clear.implementation = function () {
const result = clearActions(this);
return result;

@ -20,6 +20,7 @@ import {
Rotation,
ContextMenuType,
Workspace,
Model,
} from 'reducers/interfaces';
import getCore from 'cvat-core-wrapper';
@ -187,6 +188,7 @@ export enum AnnotationActionTypes {
CHANGE_WORKSPACE = 'CHANGE_WORKSPACE',
SAVE_LOGS_SUCCESS = 'SAVE_LOGS_SUCCESS',
SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED',
INTERACT_WITH_CANVAS = 'INTERACT_WITH_CANVAS',
}
export function saveLogsAsync(): ThunkAction {
@ -1385,6 +1387,16 @@ export function pasteShapeAsync(): ThunkAction {
};
}
export function interactWithCanvas(activeInteractor: Model, activeLabelID: number): AnyAction {
return {
type: AnnotationActionTypes.INTERACT_WITH_CANVAS,
payload: {
activeInteractor,
activeLabelID,
},
};
}
export function repeatDrawShapeAsync(): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const {
@ -1401,6 +1413,7 @@ export function repeatDrawShapeAsync(): ThunkAction {
},
},
drawing: {
activeInteractor,
activeObjectType,
activeLabelID,
activeShapeType,
@ -1410,6 +1423,16 @@ export function repeatDrawShapeAsync(): ThunkAction {
} = getStore().getState().annotation;
let activeControl = ActiveControl.CURSOR;
if (activeInteractor) {
canvasInstance.interact({
enabled: true,
shapeType: 'points',
minPosVertices: 4, // TODO: Add parameter to interactor
});
dispatch(interactWithCanvas(activeInteractor, activeLabelID));
return;
}
if (activeShapeType === ShapeType.RECTANGLE) {
activeControl = ActiveControl.DRAW_RECTANGLE;
} else if (activeShapeType === ShapeType.POINTS) {
@ -1443,7 +1466,7 @@ export function repeatDrawShapeAsync(): ThunkAction {
rectDrawingMethod: activeRectDrawingMethod,
numberOfPoints: activeNumOfPoints,
shapeType: activeShapeType,
crosshair: activeShapeType === ShapeType.RECTANGLE,
crosshair: [ShapeType.RECTANGLE, ShapeType.CUBOID].includes(activeShapeType),
});
}
};
@ -1490,7 +1513,7 @@ export function redrawShapeAsync(): ThunkAction {
enabled: true,
redraw: activatedStateID,
shapeType: state.shapeType,
crosshair: state.shapeType === ShapeType.RECTANGLE,
crosshair: [ShapeType.RECTANGLE, ShapeType.CUBOID].includes(state.shapeType),
});
}
}

@ -10,11 +10,6 @@ export enum ModelsActionTypes {
GET_MODELS = 'GET_MODELS',
GET_MODELS_SUCCESS = 'GET_MODELS_SUCCESS',
GET_MODELS_FAILED = 'GET_MODELS_FAILED',
DELETE_MODEL = 'DELETE_MODEL',
CREATE_MODEL = 'CREATE_MODEL',
CREATE_MODEL_SUCCESS = 'CREATE_MODEL_SUCCESS',
CREATE_MODEL_FAILED = 'CREATE_MODEL_FAILED',
CREATE_MODEL_STATUS_UPDATED = 'CREATE_MODEL_STATUS_UPDATED',
START_INFERENCE_FAILED = 'START_INFERENCE_FAILED',
GET_INFERENCE_STATUS_SUCCESS = 'GET_INFERENCE_STATUS_SUCCESS',
GET_INFERENCE_STATUS_FAILED = 'GET_INFERENCE_STATUS_FAILED',
@ -84,8 +79,7 @@ export function getModelsAsync(): ThunkAction {
dispatch(modelsActions.getModels());
try {
const models = (await core.lambda.list())
.filter((model: Model) => ['detector', 'reid'].includes(model.type));
const models = await core.lambda.list();
dispatch(modelsActions.getModelsSuccess(models));
} catch (error) {
dispatch(modelsActions.getModelsFailed(error));
@ -162,7 +156,6 @@ export function startInferenceAsync(
return async (dispatch): Promise<void> => {
try {
const requestID: string = await core.lambda.run(taskInstance, model, body);
const dispatchCallback = (action: ModelsActions): void => {
dispatch(action);
};

@ -0,0 +1,18 @@
<svg width="40px" height="40px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<g>
<path d="M509.2,455.7L183.4,128.9c-3.7-3.7-11.2-3.7-14.9,0l-38.2,38.2c-3.7,3.7-3.7,11.2,0,14.9l325.8,326.7
c3.7,3.7,11.2,3.7,14.9,0l38.2-38.2C512.9,466.9,512.9,460.3,509.2,455.7L509.2,455.7z M474.8,475.2L474.8,475.2
c-4.7,4.7-11.2,4.7-14.9,0L206.7,221.1c-3.7-3.7-3.7-11.2,0-14.9l0,0c3.7-3.7,11.2-3.7,14.9,0l253.2,254.1
C479.4,464.1,479.4,470.6,474.8,475.2L474.8,475.2z M171.3,74.9c8.4,0,15.8-7.4,15.8-15.8V16.3c0-8.4-7.4-15.8-15.8-15.8
c-8.4,0-15.8,7.4-15.8,15.8v42.8C155.5,68.4,162.9,74.9,171.3,74.9z M171.3,265.8c-8.4,0-15.8,7.4-15.8,15.8v42.8
c0,8.4,7.4,15.8,15.8,15.8c8.4,0,15.8-7.4,15.8-15.8v-42.8C188,273.2,180.6,265.8,171.3,265.8z M275.5,169
c0,8.4,7.4,15.8,15.8,15.8h42.8c8.4,0,15.8-7.4,15.8-15.8c0-8.4-7.4-15.8-15.8-15.8h-42.8C283,153.1,275.5,159.7,275.5,169z
M74.5,169c0-8.4-7.4-15.8-15.8-15.8H15.8C7.4,153.1,0,160.6,0,169c0,8.4,7.4,15.8,15.8,15.8h42.8C67,184.8,74.5,177.3,74.5,169z
M81,102.9c2.8,2.8,7.4,4.7,11.2,4.7c4.7,0,8.4-1.9,11.2-4.7c6.5-6.5,6.5-16.8,0-22.3L75.4,52.6c-2.8-2.8-7.4-4.7-11.2-4.7
c-4.7,0-8.4,1.9-11.2,4.7c-6.5,6.5-6.5,16.8,0,22.3L81,102.9z M92.2,235.1c-4.7,0-8.4,1.9-11.2,4.7l-27.9,27.9
c-2.8,2.8-4.7,7.4-4.7,11.2c0,4.7,1.9,8.4,4.7,11.2s7.4,4.7,11.2,4.7c4.7,0,8.4-1.9,11.2-4.7l27.9-27.9c6.5-6.5,6.5-16.8,0-22.3
C100.5,236.9,96.8,235.1,92.2,235.1z M251.3,107.5c4.7,0,8.4-1.9,11.2-4.7l27.9-27.9c6.5-6.5,6.5-16.8,0-22.3
c-2.8-2.8-7.4-4.7-11.2-4.7c-3.7,0-8.4,1.9-11.2,4.7l-27.9,27.9c-2.8,2.8-4.7,7.4-4.7,11.2c0,3.7,1.9,8.4,4.7,11.2
C243,105.7,246.7,107.5,251.3,107.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -14,6 +14,7 @@ import CursorControl from './cursor-control';
import MoveControl from './move-control';
import FitControl from './fit-control';
import ResizeControl from './resize-control';
import ToolsControl from './tools-control';
import DrawRectangleControl from './draw-rectangle-control';
import DrawPolygonControl from './draw-polygon-control';
import DrawPolylineControl from './draw-polyline-control';
@ -84,7 +85,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
preventDefault(event);
const drawing = [ActiveControl.DRAW_POINTS, ActiveControl.DRAW_POLYGON,
ActiveControl.DRAW_POLYLINE, ActiveControl.DRAW_RECTANGLE,
ActiveControl.DRAW_CUBOID].includes(activeControl);
ActiveControl.DRAW_CUBOID, ActiveControl.INTERACTION].includes(activeControl);
if (!drawing) {
canvasInstance.cancel();
@ -97,6 +98,12 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
repeatDrawShape();
}
} else {
if (activeControl === ActiveControl.INTERACTION) {
// separated API method
canvasInstance.interact({ enabled: false });
return;
}
canvasInstance.draw({ enabled: false });
}
},
@ -178,7 +185,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
<ResizeControl canvasInstance={canvasInstance} activeControl={activeControl} />
<hr />
<ToolsControl />
<DrawRectangleControl
canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_RECTANGLE}

@ -1,90 +0,0 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState } from 'react';
import { connect } from 'react-redux';
import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox';
import Tooltip from 'antd/lib/tooltip';
import { Canvas } from 'cvat-canvas-wrapper';
import { CombinedState } from 'reducers/interfaces';
import { activate as activatePlugin, deactivate as deactivatePlugin } from 'utils/dextr-utils';
interface StateToProps {
pluginEnabled: boolean;
canvasInstance: Canvas;
}
interface DispatchToProps {
activate(canvasInstance: Canvas): void;
deactivate(canvasInstance: Canvas): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
plugins: {
list,
},
annotation: {
canvas: {
instance: canvasInstance,
},
},
} = state;
return {
canvasInstance,
pluginEnabled: list.DEXTR_SEGMENTATION,
};
}
function mapDispatchToProps(): DispatchToProps {
return {
activate(canvasInstance: Canvas): void {
activatePlugin(canvasInstance);
},
deactivate(canvasInstance: Canvas): void {
deactivatePlugin(canvasInstance);
},
};
}
function DEXTRPlugin(props: StateToProps & DispatchToProps): JSX.Element | null {
const {
pluginEnabled,
canvasInstance,
activate,
deactivate,
} = props;
const [pluginActivated, setActivated] = useState(false);
return (
pluginEnabled ? (
<Tooltip title='Make AI polygon from at least 4 extreme points using deep extreme cut' mouseLeaveDelay={0}>
<Checkbox
style={{ marginTop: 5 }}
checked={pluginActivated}
onChange={(event: CheckboxChangeEvent): void => {
setActivated(event.target.checked);
if (event.target.checked) {
activate(canvasInstance);
} else {
deactivate(canvasInstance);
}
}}
>
Make AI polygon
</Checkbox>
</Tooltip>
) : null
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(DEXTRPlugin);
// TODO: Add dialog window with cancel button

@ -14,7 +14,6 @@ import Text from 'antd/lib/typography/Text';
import { RectDrawingMethod, CuboidDrawingMethod } from 'cvat-canvas-wrapper';
import { ShapeType } from 'reducers/interfaces';
import { clamp } from 'utils/math';
import DEXTRPlugin from './dextr-plugin';
interface Props {
shapeType: ShapeType;
@ -91,7 +90,6 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
</Select>
</Col>
</Row>
{ shapeType === ShapeType.POLYGON && <DEXTRPlugin /> }
{
shapeType === ShapeType.RECTANGLE && (
<>

@ -0,0 +1,472 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux';
import Icon from 'antd/lib/icon';
import Popover from 'antd/lib/popover';
import Select, { OptionProps } from 'antd/lib/select';
import Button from 'antd/lib/button';
import Modal from 'antd/lib/modal';
import Text from 'antd/lib/typography/Text';
import { Row, Col } from 'antd/lib/grid';
import notification from 'antd/lib/notification';
import { AIToolsIcon } from 'icons';
import { Canvas } from 'cvat-canvas-wrapper';
import getCore from 'cvat-core-wrapper';
import {
CombinedState,
ActiveControl,
Model,
ObjectType,
ShapeType,
} from 'reducers/interfaces';
import { interactWithCanvas, fetchAnnotationsAsync, updateAnnotationsAsync } from 'actions/annotation-actions';
import { InteractionResult } from 'cvat-canvas/src/typescript/canvas';
interface StateToProps {
canvasInstance: Canvas;
labels: any[];
states: any[];
activeLabelID: number;
jobInstance: any;
isInteraction: boolean;
frame: number;
interactors: Model[];
}
interface DispatchToProps {
onInteractionStart(activeInteractor: Model, activeLabelID: number): void;
updateAnnotations(statesToUpdate: any[]): void;
fetchAnnotations(): void;
}
const core = getCore();
function mapStateToProps(state: CombinedState): StateToProps {
const { annotation } = state;
const { number: frame } = annotation.player.frame;
const { instance: jobInstance } = annotation.job;
const { instance: canvasInstance, activeControl } = annotation.canvas;
const { models } = state;
const { interactors } = models;
return {
interactors,
isInteraction: activeControl === ActiveControl.INTERACTION,
activeLabelID: annotation.drawing.activeLabelID,
labels: annotation.job.labels,
states: annotation.annotations.states,
canvasInstance,
jobInstance,
frame,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onInteractionStart(activeInteractor: Model, activeLabelID: number): void {
dispatch(interactWithCanvas(activeInteractor, activeLabelID));
},
updateAnnotations(statesToUpdate: any[]): void {
dispatch(updateAnnotationsAsync(statesToUpdate));
},
fetchAnnotations(): void {
dispatch(fetchAnnotationsAsync());
},
};
}
function convertShapesForInteractor(shapes: InteractionResult[]): number[][] {
const reducer = (acc: number[][], _: number, index: number, array: number[]): number[][] => {
if (!(index % 2)) { // 0, 2, 4
acc.push([
array[index],
array[index + 1],
]);
}
return acc;
};
return shapes.filter((shape: InteractionResult): boolean => shape.shapeType === 'points' && shape.button === 0)
.map((shape: InteractionResult): number[] => shape.points)
.flat().reduce(reducer, []);
}
type Props = StateToProps & DispatchToProps;
interface State {
activeInteractor: Model | null;
activeLabelID: number;
interactiveStateID: number | null;
fetching: boolean;
}
class ToolsControlComponent extends React.PureComponent<Props, State> {
private interactionIsAborted: boolean;
private interactionIsDone: boolean;
public constructor(props: Props) {
super(props);
this.state = {
activeInteractor: props.interactors.length ? props.interactors[0] : null,
activeLabelID: props.labels[0].id,
interactiveStateID: null,
fetching: false,
};
this.interactionIsAborted = false;
this.interactionIsDone = false;
}
public componentDidMount(): void {
const { canvasInstance } = this.props;
canvasInstance.html().addEventListener('canvas.interacted', this.interactionListener);
canvasInstance.html().addEventListener('canvas.canceled', this.cancelListener);
}
public componentDidUpdate(prevProps: Props): void {
const { isInteraction } = this.props;
if (prevProps.isInteraction && !isInteraction) {
window.removeEventListener('contextmenu', this.contextmenuDisabler);
} else if (!prevProps.isInteraction && isInteraction) {
this.interactionIsDone = false;
this.interactionIsAborted = false;
window.addEventListener('contextmenu', this.contextmenuDisabler);
}
}
public componentWillUnmount(): void {
const { canvasInstance } = this.props;
canvasInstance.html().removeEventListener('canvas.interacted', this.interactionListener);
canvasInstance.html().removeEventListener('canvas.canceled', this.cancelListener);
}
private getInteractiveState(): any | null {
const { states } = this.props;
const { interactiveStateID } = this.state;
return states
.filter((_state: any): boolean => _state.clientID === interactiveStateID)[0] || null;
}
private contextmenuDisabler = (e: MouseEvent): void => {
if (e.target && (e.target as Element).classList
&& (e.target as Element).classList.toString().includes('ant-modal')) {
e.preventDefault();
}
};
private cancelListener = async (): Promise<void> => {
const {
isInteraction,
jobInstance,
frame,
fetchAnnotations,
} = this.props;
const { interactiveStateID, fetching } = this.state;
if (isInteraction) {
if (fetching && !this.interactionIsDone) {
// user pressed ESC
this.setState({ fetching: false });
this.interactionIsAborted = true;
}
if (interactiveStateID !== null) {
const state = this.getInteractiveState();
this.setState({ interactiveStateID: null });
await state.delete(frame);
fetchAnnotations();
}
await jobInstance.actions.freeze(false);
}
};
private interactionListener = async (e: Event): Promise<void> => {
const {
frame,
labels,
jobInstance,
isInteraction,
activeLabelID,
fetchAnnotations,
updateAnnotations,
} = this.props;
const { activeInteractor, interactiveStateID, fetching } = this.state;
try {
if (!isInteraction) {
throw Error('Canvas raises event "canvas.interacted" when interaction is off');
}
if (fetching) {
this.interactionIsDone = (e as CustomEvent).detail.isDone;
return;
}
const interactor = activeInteractor as Model;
let result = [];
if ((e as CustomEvent).detail.shapesUpdated) {
this.setState({ fetching: true });
try {
result = await core.lambda.call(jobInstance.task, interactor, {
task: jobInstance.task,
frame,
points: convertShapesForInteractor((e as CustomEvent).detail.shapes),
});
if (this.interactionIsAborted) {
// while the server request
// user has cancelled interaction (for example pressed ESC)
return;
}
} finally {
this.setState({ fetching: false });
}
}
if (this.interactionIsDone) {
// while the server request, user has done interaction (for example pressed N)
const object = new core.classes.ObjectState({
frame,
objectType: ObjectType.SHAPE,
label: labels
.filter((label: any) => label.id === activeLabelID)[0],
shapeType: ShapeType.POLYGON,
points: result.flat(),
occluded: false,
zOrder: (e as CustomEvent).detail.zOrder,
});
await jobInstance.annotations.put([object]);
fetchAnnotations();
} else {
// no shape yet, then create it and save to collection
if (interactiveStateID === null) {
// freeze history for interaction time
// (points updating shouldn't cause adding new actions to history)
await jobInstance.actions.freeze(true);
const object = new core.classes.ObjectState({
frame,
objectType: ObjectType.SHAPE,
label: labels
.filter((label: any) => label.id === activeLabelID)[0],
shapeType: ShapeType.POLYGON,
points: result.flat(),
occluded: false,
zOrder: (e as CustomEvent).detail.zOrder,
});
// need a clientID of a created object to interact with it further
// so, we do not use createAnnotationAction
const [clientID] = await jobInstance.annotations.put([object]);
// update annotations on a canvas
fetchAnnotations();
this.setState({ interactiveStateID: clientID });
return;
}
const state = this.getInteractiveState();
if ((e as CustomEvent).detail.isDone) {
const finalObject = new core.classes.ObjectState({
frame: state.frame,
objectType: state.objectType,
label: state.label,
shapeType: state.shapeType,
points: result.length ? result.flat() : state.points,
occluded: state.occluded,
zOrder: state.zOrder,
});
this.setState({ interactiveStateID: null });
await state.delete(frame);
await jobInstance.actions.freeze(false);
await jobInstance.annotations.put([finalObject]);
fetchAnnotations();
} else {
state.points = result.flat();
updateAnnotations([state]);
fetchAnnotations();
}
}
} catch (err) {
notification.error({
description: err.toString(),
message: 'Interaction error occured',
});
}
};
private setActiveInteractor = (key: string): void => {
const { interactors } = this.props;
this.setState({
activeInteractor: interactors.filter(
(interactor: Model) => interactor.id === key,
)[0],
});
};
private renderLabelBlock(): JSX.Element {
const { labels } = this.props;
const { activeLabelID } = this.state;
return (
<>
<Row type='flex' justify='start'>
<Col>
<Text className='cvat-text-color'>Label</Text>
</Col>
</Row>
<Row type='flex' justify='center'>
<Col span={24}>
<Select
style={{ width: '100%' }}
showSearch
filterOption={
(input: string, option: React.ReactElement<OptionProps>) => {
const { children } = option.props;
if (typeof (children) === 'string') {
return children.toLowerCase().includes(input.toLowerCase());
}
return false;
}
}
value={`${activeLabelID}`}
onChange={(value: string) => {
this.setState({ activeLabelID: +value });
}}
>
{
labels.map((label: any) => (
<Select.Option key={label.id} value={`${label.id}`}>
{label.name}
</Select.Option>
))
}
</Select>
</Col>
</Row>
</>
);
}
private renderInteractorBlock(): JSX.Element {
const { interactors, canvasInstance, onInteractionStart } = this.props;
const { activeInteractor, activeLabelID, fetching } = this.state;
return (
<>
<Row type='flex' justify='start'>
<Col>
<Text className='cvat-text-color'>Interactor</Text>
</Col>
</Row>
<Row type='flex' align='middle' justify='center'>
<Col span={24}>
<Select
style={{ width: '100%' }}
defaultValue={interactors[0].name}
onChange={this.setActiveInteractor}
>
{interactors.map((interactor: Model): JSX.Element => (
<Select.Option title={interactor.description} key={interactor.id}>
{interactor.name}
</Select.Option>
))}
</Select>
</Col>
</Row>
<Row type='flex' align='middle' justify='center'>
<Col offset={4} span={16}>
<Button
loading={fetching}
className='cvat-tools-interact-button'
disabled={!activeInteractor || fetching}
onClick={() => {
if (activeInteractor) {
canvasInstance.cancel();
canvasInstance.interact({
shapeType: 'points',
minPosVertices: 4, // TODO: Add parameter to interactor
enabled: true,
});
onInteractionStart(activeInteractor, activeLabelID);
}
}}
>
Interact
</Button>
</Col>
</Row>
</>
);
}
private renderPopoverContent(): JSX.Element {
return (
<div className='cvat-tools-control-popover-content'>
<Row type='flex' justify='start'>
<Col>
<Text className='cvat-text-color' strong>AI Tools</Text>
</Col>
</Row>
{ this.renderLabelBlock() }
{ this.renderInteractorBlock() }
</div>
);
}
public render(): JSX.Element | null {
const { interactors, isInteraction, canvasInstance } = this.props;
const { fetching } = this.state;
if (!interactors.length) return null;
const dynamcPopoverPros = isInteraction ? {
overlayStyle: {
display: 'none',
},
} : {};
const dynamicIconProps = isInteraction ? {
className: 'cvat-active-canvas-control cvat-tools-control',
onClick: (): void => {
canvasInstance.interact({ enabled: false });
},
} : {
className: 'cvat-tools-control',
};
return (
<>
<Modal
title='Interaction request'
zIndex={Number.MAX_SAFE_INTEGER}
visible={fetching}
closable={false}
footer={[]}
>
<Text>Waiting for a server response..</Text>
<Icon style={{ marginLeft: '10px' }} type='loading' />
</Modal>
<Popover
{...dynamcPopoverPros}
placement='right'
overlayClassName='cvat-tools-control-popover'
content={interactors.length && this.renderPopoverContent()}
>
<Icon {...dynamicIconProps} component={AIToolsIcon} />
</Popover>
</>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(ToolsControlComponent);

@ -83,17 +83,31 @@
padding: 0;
}
.cvat-draw-shape-popover >
.ant-popover-content >
.ant-popover-inner > div >
.ant-popover-inner-content {
padding: 0;
.cvat-draw-shape-popover,
.cvat-tools-control-popover {
> .ant-popover-content >
.ant-popover-inner > div >
.ant-popover-inner-content {
padding: 0;
}
}
.cvat-tools-interact-button {
width: 100%;
margin-top: 10px;
}
.cvat-draw-shape-popover-points-selector {
width: 100%;
}
.cvat-tools-control-popover-content {
padding: 10px;
border-radius: 5px;
background: $background-color-2;
width: 270px;
}
.cvat-draw-shape-popover-content {
padding: 10px;
border-radius: 5px;

@ -21,7 +21,8 @@ import {
} from 'reducers/interfaces';
interface Props {
models: Model[];
reid: Model[];
detectors: Model[];
activeProcesses: StringObject;
visible: boolean;
taskInstance: any;
@ -88,14 +89,14 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
public componentDidUpdate(prevProps: Props, prevState: State): void {
const {
reid,
detectors,
taskInstance,
models,
visible,
} = this.props;
const {
selectedModel,
} = this.state;
const { selectedModel } = this.state;
const models = [...reid, ...detectors];
if (!prevProps.visible && visible) {
this.setState({
@ -140,7 +141,8 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
}
private renderModelSelector(): JSX.Element {
const { models } = this.props;
const { reid, detectors } = this.props;
const models = [...reid, ...detectors];
return (
<Row type='flex' align='middle'>
@ -166,10 +168,7 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
}
private renderMappingTag(modelLabel: string, taskLabel: string): JSX.Element {
const {
colors,
mapping,
} = this.state;
const { colors, mapping } = this.state;
return (
<Row key={`${modelLabel}-${taskLabel}`} type='flex' justify='start' align='middle'>
@ -203,11 +202,7 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
current: string,
options: string[],
): JSX.Element {
const {
matching,
mapping,
colors,
} = this.state;
const { matching, mapping, colors } = this.state;
return (
<Select
@ -291,10 +286,7 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
}
private renderReidContent(): JSX.Element {
const {
threshold,
maxDistance,
} = this.state;
const { threshold, maxDistance } = this.state;
return (
<div>
@ -346,16 +338,10 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
}
private renderContent(): JSX.Element {
const {
selectedModel,
cleanup,
mapping,
} = this.state;
const {
models,
taskInstance,
} = this.props;
const { selectedModel, cleanup, mapping } = this.state;
const { reid, detectors, taskInstance } = this.props;
const models = [...reid, ...detectors];
const model = selectedModel && models
.filter((_model): boolean => _model.name === selectedModel)[0];
@ -414,13 +400,15 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
} = this.state;
const {
models,
reid,
detectors,
visible,
taskInstance,
runInference,
closeDialog,
} = this.props;
const models = [...reid, ...detectors];
const activeModel = models.filter(
(model): boolean => model.name === selectedModel,
)[0];

@ -1,49 +0,0 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { Row, Col } from 'antd/lib/grid';
import Tag from 'antd/lib/tag';
import Select from 'antd/lib/select';
import Text from 'antd/lib/typography/Text';
import { Model } from 'reducers/interfaces';
interface Props {
model: Model;
}
export default function BuiltModelItemComponent(props: Props): JSX.Element {
const { model } = props;
return (
<Row className='cvat-models-list-item' type='flex'>
<Col span={4} xxl={3}>
<Tag color='orange'>{model.framework}</Tag>
</Col>
<Col span={6} xxl={7}>
<Text className='cvat-text-color'>
{model.name}
</Text>
</Col>
<Col span={5} offset={7}>
<Select
showSearch
placeholder='Supported labels'
style={{ width: '90%' }}
value='Supported labels'
>
{model.labels.map(
(label): JSX.Element => (
<Select.Option key={label}>
{label}
</Select.Option>
),
)}
</Select>
</Col>
<Col span={2} />
</Row>
);
}

@ -9,7 +9,6 @@ import Text from 'antd/lib/typography/Text';
import { Model } from 'reducers/interfaces';
import DeployedModelItem from './deployed-model-item';
interface Props {
models: Model[];
}

@ -12,11 +12,21 @@ import FeedbackComponent from '../feedback/feedback';
import { Model } from '../../reducers/interfaces';
interface Props {
deployedModels: Model[];
interactors: Model[];
detectors: Model[];
trackers: Model[];
reid: Model[];
}
export default function ModelsPageComponent(props: Props): JSX.Element {
const { deployedModels } = props;
const {
interactors,
detectors,
trackers,
reid,
} = props;
const deployedModels = [...detectors, ...interactors, ...trackers, ...reid];
return (
<div className='cvat-models-page'>

@ -9,7 +9,8 @@ import { Model, CombinedState } from 'reducers/interfaces';
import { startInferenceAsync, modelsActions } from 'actions/models-actions';
interface StateToProps {
models: Model[];
reid: Model[];
detectors: Model[];
activeProcesses: {
[index: string]: string;
};
@ -30,7 +31,8 @@ function mapStateToProps(state: CombinedState): StateToProps {
const { models } = state;
return {
models: models.models,
reid: models.reid,
detectors: models.detectors,
activeProcesses: {},
taskInstance: models.activeRunTask,
visible: models.visibleRunWindows,

@ -11,14 +11,26 @@ import {
} from 'reducers/interfaces';
interface StateToProps {
deployedModels: Model[];
interactors: Model[];
detectors: Model[];
trackers: Model[];
reid: Model[];
}
function mapStateToProps(state: CombinedState): StateToProps {
const { models } = state;
const {
interactors,
detectors,
trackers,
reid,
} = models;
return {
deployedModels: models.models,
interactors,
detectors,
trackers,
reid,
};
}

@ -8,8 +8,13 @@ import {
CanvasVersion,
RectDrawingMethod,
CuboidDrawingMethod,
InteractionData as InteractionDataType,
InteractionResult as InteractionResultType,
} from 'cvat-canvas/src/typescript/canvas';
export type InteractionData = InteractionDataType;
export type InteractionResult = InteractionResultType;
export {
Canvas,
CanvasMode,

@ -42,6 +42,8 @@ import SVGForegroundIcon from './assets/foreground-icon.svg';
import SVGCubeIcon from './assets/cube-icon.svg';
import SVGResetPerspectiveIcon from './assets/reset-perspective.svg';
import SVGColorizeIcon from './assets/colorize-icon.svg';
import SVGAITools from './assets/ai-tools-icon.svg';
export const CVATLogo = React.memo(
(): JSX.Element => <SVGCVATLogo />,
@ -154,6 +156,9 @@ export const CubeIcon = React.memo(
export const ResetPerspectiveIcon = React.memo(
(): JSX.Element => <SVGResetPerspectiveIcon />,
);
export const AIToolsIcon = React.memo(
(): JSX.Element => <SVGAITools />,
);
export const ColorizeIcon = React.memo(
(): JSX.Element => <SVGColorizeIcon />,
);

@ -428,6 +428,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
activeControl,
},
drawing: {
activeInteractor: undefined,
activeLabelID: labelID,
activeNumOfPoints: points,
activeObjectType: objectType,
@ -1039,8 +1040,30 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.INTERACT_WITH_CANVAS: {
return {
...state,
annotations: {
...state.annotations,
activatedStateID: null,
},
drawing: {
...state.drawing,
activeInteractor: action.payload.activeInteractor,
activeLabelID: action.payload.activeLabelID,
},
canvas: {
...state.canvas,
activeControl: ActiveControl.INTERACTION,
},
};
}
case AnnotationActionTypes.CHANGE_WORKSPACE: {
const { workspace } = action.payload;
if (state.canvas.activeControl !== ActiveControl.CURSOR) {
return state;
}
return {
...state,
workspace,

@ -76,7 +76,6 @@ export interface FormatsState {
// eslint-disable-next-line import/prefer-default-export
export enum SupportedPlugins {
GIT_INTEGRATION = 'GIT_INTEGRATION',
DEXTR_SEGMENTATION = 'DEXTR_SEGMENTATION',
ANALYTICS = 'ANALYTICS',
}
@ -161,7 +160,10 @@ export interface ModelsState {
initialized: boolean;
fetching: boolean;
creatingStatus: string;
models: Model[];
interactors: Model[];
detectors: Model[];
trackers: Model[];
reid: Model[];
inferences: {
[index: number]: ActiveInference;
};
@ -206,9 +208,7 @@ export interface NotificationsState {
fetching: null | ErrorState;
};
models: {
creating: null | ErrorState;
starting: null | ErrorState;
deleting: null | ErrorState;
fetching: null | ErrorState;
canceling: null | ErrorState;
metaFetching: null | ErrorState;
@ -270,6 +270,7 @@ export enum ActiveControl {
GROUP = 'group',
SPLIT = 'split',
EDIT = 'edit',
INTERACTION = 'interaction',
}
export enum ShapeType {
@ -342,6 +343,7 @@ export interface AnnotationState {
frameAngles: number[];
};
drawing: {
activeInteractor?: Model;
activeShapeType: ShapeType;
activeRectDrawingMethod?: RectDrawingMethod;
activeNumOfPoints?: number;

@ -5,13 +5,16 @@
import { boundariesActions, BoundariesActionTypes } from 'actions/boundaries-actions';
import { ModelsActionTypes, ModelsActions } from 'actions/models-actions';
import { AuthActionTypes, AuthActions } from 'actions/auth-actions';
import { ModelsState } from './interfaces';
import { ModelsState, Model } from './interfaces';
const defaultState: ModelsState = {
initialized: false,
fetching: false,
creatingStatus: '',
models: [],
interactors: [],
detectors: [],
trackers: [],
reid: [],
visibleRunWindows: false,
activeRunTask: null,
inferences: {},
@ -32,7 +35,10 @@ export default function (
case ModelsActionTypes.GET_MODELS_SUCCESS: {
return {
...state,
models: action.payload.models,
interactors: action.payload.models.filter((model: Model) => ['interactor'].includes(model.type)),
detectors: action.payload.models.filter((model: Model) => ['detector'].includes(model.type)),
trackers: action.payload.models.filter((model: Model) => ['tracker'].includes(model.type)),
reid: action.payload.models.filter((model: Model) => ['reid'].includes(model.type)),
initialized: true,
fetching: false,
};

@ -51,9 +51,7 @@ const defaultState: NotificationsState = {
fetching: null,
},
models: {
creating: null,
starting: null,
deleting: null,
fetching: null,
canceling: null,
metaFetching: null,
@ -414,21 +412,6 @@ export default function (state = defaultState, action: AnyAction): Notifications
},
};
}
case ModelsActionTypes.CREATE_MODEL_FAILED: {
return {
...state,
errors: {
...state.errors,
models: {
...state.errors.models,
creating: {
message: 'Could not create the model',
reason: action.payload.error.toString(),
},
},
},
};
}
case ModelsActionTypes.GET_INFERENCE_STATUS_SUCCESS: {
if (action.payload.activeInference.status === 'finished') {
const { taskID } = action.payload;

@ -4,7 +4,6 @@
import { PluginsActionTypes, PluginActions } from 'actions/plugins-actions';
import { registerGitPlugin } from 'utils/git-utils';
import { registerDEXTRPlugin } from 'utils/dextr-utils';
import { PluginsState } from './interfaces';
const defaultState: PluginsState = {
@ -12,7 +11,6 @@ const defaultState: PluginsState = {
initialized: false,
list: {
GIT_INTEGRATION: false,
DEXTR_SEGMENTATION: false,
ANALYTICS: false,
},
};
@ -36,10 +34,6 @@ export default function (
registerGitPlugin();
}
if (!state.list.DEXTR_SEGMENTATION && list.DEXTR_SEGMENTATION) {
registerDEXTRPlugin();
}
return {
...state,
initialized: true,

@ -1,214 +0,0 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import getCore from 'cvat-core-wrapper';
import { Canvas } from 'cvat-canvas-wrapper';
import { ShapeType, CombinedState } from 'reducers/interfaces';
import { getCVATStore } from 'cvat-store';
const core = getCore();
interface DEXTRPlugin {
name: string;
description: string;
cvat: {
classes: {
Job: {
prototype: {
annotations: {
put: {
enter(self: any, objects: any[]): Promise<void>;
};
};
};
};
};
};
data: {
canceled: boolean;
enabled: boolean;
};
}
interface Point {
x: number;
y: number;
}
const antModalRoot = document.createElement('div');
const antModalMask = document.createElement('div');
antModalMask.classList.add('ant-modal-mask');
const antModalWrap = document.createElement('div');
antModalWrap.classList.add('ant-modal-wrap');
antModalWrap.setAttribute('role', 'dialog');
const antModal = document.createElement('div');
antModal.classList.add('ant-modal');
antModal.style.width = '300px';
antModal.style.top = '40%';
antModal.setAttribute('role', 'document');
const antModalContent = document.createElement('div');
antModalContent.classList.add('ant-modal-content');
const antModalBody = document.createElement('div');
antModalBody.classList.add('ant-modal-body');
antModalBody.style.textAlign = 'center';
const antModalSpan = document.createElement('span');
antModalSpan.innerText = 'Segmentation request is being processed';
antModalSpan.style.display = 'block';
const antModalButton = document.createElement('button');
antModalButton.disabled = true;
antModalButton.classList.add('ant-btn', 'ant-btn-primary');
antModalButton.style.width = '100px';
antModalButton.style.margin = '10px auto';
const antModalButtonSpan = document.createElement('span');
antModalButtonSpan.innerText = 'Cancel';
antModalBody.append(antModalSpan, antModalButton);
antModalButton.append(antModalButtonSpan);
antModalContent.append(antModalBody);
antModal.append(antModalContent);
antModalWrap.append(antModal);
antModalRoot.append(antModalMask, antModalWrap);
async function serverRequest(
taskInstance: any,
frame: number,
points: number[],
): Promise<number[]> {
const reducer = (acc: number[][],
_: number, index: number,
array: number[]): number[][] => {
if (!(index % 2)) { // 0, 2, 4
acc.push([
array[index],
array[index + 1],
]);
}
return acc;
};
const reducedPoints = points.reduce(reducer, []);
const models = await core.lambda.list();
const model = models.filter((func: any): boolean => func.id === 'openvino.dextr')[0];
const result = await core.lambda.call(taskInstance, model, {
task: taskInstance,
frame,
points: reducedPoints,
});
return result.flat();
}
async function enter(this: any, self: DEXTRPlugin, objects: any[]): Promise<void> {
try {
if (self.data.enabled && objects.length === 1) {
const state = (getCVATStore().getState() as CombinedState);
const isPolygon = state.annotation
.drawing.activeShapeType === ShapeType.POLYGON;
if (!isPolygon) return;
document.body.append(antModalRoot);
const promises: Record<number, Promise<number[]>> = {};
for (let i = 0; i < objects.length; i++) {
if (objects[i].points.length >= 8) {
promises[i] = serverRequest(
this.task,
objects[i].frame,
objects[i].points,
);
} else {
promises[i] = new Promise((resolve) => {
resolve(objects[i].points);
});
}
}
const transformed = await Promise
.all(Object.values(promises));
for (let i = 0; i < objects.length; i++) {
// eslint-disable-next-line no-param-reassign
objects[i] = new core.classes.ObjectState({
frame: objects[i].frame,
objectType: objects[i].objectType,
label: objects[i].label,
shapeType: ShapeType.POLYGON,
points: transformed[i],
occluded: objects[i].occluded,
zOrder: objects[i].zOrder,
});
}
}
return;
} catch (error) {
throw new core.exceptions.PluginError(error.toString());
} finally {
// eslint-disable-next-line no-param-reassign
self.data.canceled = false;
antModalButton.disabled = true;
if (antModalRoot.parentElement === document.body) {
document.body.removeChild(antModalRoot);
}
}
}
const plugin: DEXTRPlugin = {
name: 'Deep extreme cut',
description: 'Plugin allows to get a polygon from extreme points using AI',
cvat: {
classes: {
Job: {
prototype: {
annotations: {
put: {
enter,
},
},
},
},
},
},
data: {
canceled: false,
enabled: false,
},
};
antModalButton.onclick = () => {
plugin.data.canceled = true;
};
export function activate(canvasInstance: Canvas): void {
if (!plugin.data.enabled) {
// eslint-disable-next-line no-param-reassign
canvasInstance.draw = (drawData: any): void => {
if (drawData.enabled && drawData.shapeType === ShapeType.POLYGON
&& (typeof (drawData.numberOfPoints) === 'undefined' || drawData.numberOfPoints >= 4)
&& (typeof (drawData.initialState) === 'undefined')
) {
const patchedData = { ...drawData };
patchedData.shapeType = ShapeType.POINTS;
patchedData.crosshair = true;
Object.getPrototypeOf(canvasInstance)
.draw.call(canvasInstance, patchedData);
} else {
Object.getPrototypeOf(canvasInstance)
.draw.call(canvasInstance, drawData);
}
};
plugin.data.enabled = true;
}
}
export function deactivate(canvasInstance: Canvas): void {
if (plugin.data.enabled) {
// eslint-disable-next-line no-param-reassign
canvasInstance.draw = Object.getPrototypeOf(canvasInstance).draw;
plugin.data.enabled = false;
}
}
export function registerDEXTRPlugin(): void {
core.plugins.register(plugin);
}

@ -17,14 +17,6 @@ class PluginChecker {
case SupportedPlugins.GIT_INTEGRATION: {
return isReachable(`${serverHost}/git/repository/meta/get`, 'OPTIONS');
}
case SupportedPlugins.DEXTR_SEGMENTATION: {
try {
const list = await core.lambda.list();
return list.map((func: any): boolean => func.id).includes('openvino.dextr');
} catch (_) {
return false;
}
}
case SupportedPlugins.ANALYTICS: {
return isReachable(`${serverHost}/analytics/app/kibana`, 'GET');
}

Loading…
Cancel
Save