Improved interface of interactors on UI (#2054)
parent
174fe1690b
commit
908e0569d8
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 |
@ -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
|
|
||||||
@ -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);
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue