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