Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Intelligent scissors with OpenCV javascript #2689

Merged
merged 36 commits into from
Feb 3, 2021
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b1ac4b1
Some UI implementations
bsekachev Oct 2, 2020
b553502
Added opencv wrapper
bsekachev Oct 2, 2020
a058c02
Updated Opencv wrapper
bsekachev Oct 2, 2020
28101b4
Moved initialization stub
bsekachev Oct 2, 2020
5c46aaa
Added threshold
bsekachev Oct 2, 2020
45c7f96
Setup interaction with canvas
bsekachev Oct 7, 2020
516d666
Fixed couple of issues
bsekachev Oct 14, 2020
9c83aed
Added threshold, changing size via ctrl
bsekachev Oct 14, 2020
19d3ce4
tmp
bsekachev Oct 19, 2020
7974c6a
Merged develop
bsekachev Oct 23, 2020
6605cb2
Aborted host change
bsekachev Oct 23, 2020
fd7a767
Fixed threshold
bsekachev Oct 23, 2020
0946e53
Merge branch 'develop' into bs/opencv
bsekachev Oct 24, 2020
29e6121
Aborted host
bsekachev Oct 24, 2020
5bcb8b4
Merge branch 'develop' into bs/opencv
bsekachev Oct 24, 2020
9b5d7d5
Merge branch 'develop' into bs/opencv
bsekachev Nov 18, 2020
c6a99b6
Merged develop
bsekachev Dec 29, 2020
9c4f80b
Some fixes
bsekachev Dec 29, 2020
be77510
Using ready label selector
bsekachev Dec 29, 2020
e296046
Raw implementation
bsekachev Jan 14, 2021
f747197
Merge branch 'develop' into bs/opencv
bsekachev Jan 19, 2021
afebd50
Added additional arguments
bsekachev Jan 20, 2021
9d9c5db
Fixed some minor issues
bsekachev Jan 20, 2021
ba90aa8
Removed unused file
bsekachev Jan 20, 2021
d68cc97
Merged develop
bsekachev Jan 20, 2021
34e0963
Fixed tool reset
Jan 28, 2021
46563ca
Added short instructions to update opencv.js
Jan 28, 2021
2c01a1d
Merge branch 'develop' into bs/opencv
Jan 28, 2021
b20f502
Fixed corner case
Jan 28, 2021
3ced2b4
Merge branch 'develop' into bs/opencv
Feb 1, 2021
5fcf469
Added error handler, opencv version, updated cvat_proxy & mod_wsgi
Feb 1, 2021
d9eadc1
OpenCV returned back
Feb 1, 2021
feb8ad6
Using dinamic function instead of script
Feb 1, 2021
53fe4b5
Merge branch 'develop' into bs/opencv
Feb 2, 2021
2a5e91b
Updated changelog & version
Feb 3, 2021
f9c0a34
Merge branch 'develop' into bs/opencv
Feb 3, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cvat-canvas/src/scss/canvas.scss
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ polyline.cvat_shape_drawing_opacity {
stroke: red;
}

.cvat_canvas_threshold {
stroke: red;
}

.cvat_canvas_shape_grouping {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
Expand Down
6 changes: 5 additions & 1 deletion cvat-canvas/src/typescript/canvasModel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT

Expand Down Expand Up @@ -76,6 +76,10 @@ export interface InteractionData {
crosshair?: boolean;
minPosVertices?: number;
minNegVertices?: number;
enableNegVertices?: boolean;
enableThreshold?: boolean;
enableSliding?: boolean;
allowRemoveOnlyLast?: boolean;
}

export interface InteractionResult {
Expand Down
3 changes: 3 additions & 0 deletions cvat-canvas/src/typescript/canvasView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
shapes: InteractionResult[] | null,
shapesUpdated: boolean = true,
isDone: boolean = false,
threshold: number | null = null,
): void {
const { zLayer } = this.controller;
if (Array.isArray(shapes)) {
Expand All @@ -176,6 +177,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
isDone,
shapes,
zOrder: zLayer || 0,
threshold,
},
});

Expand Down Expand Up @@ -1050,6 +1052,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
});

this.content.addEventListener('wheel', (event): void => {
if (event.ctrlKey) return;
const { offset } = this.controller.geometry;
const point = translateToSVG(this.content, [event.clientX, event.clientY]);
self.controller.zoom(point[0] - offset, point[1] - offset, event.deltaY > 0 ? -1 : 1);
Expand Down
107 changes: 100 additions & 7 deletions cvat-canvas/src/typescript/interactionHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT

Expand All @@ -24,6 +24,8 @@ export class InteractionHandlerImpl implements InteractionHandler {
private interactionShapes: SVG.Shape[];
private currentInteractionShape: SVG.Shape | null;
private crosshair: Crosshair;
private threshold: SVG.Rect | null;
private thresholdRectSize: number;

private prepareResult(): InteractionResult[] {
return this.interactionShapes.map(
Expand Down Expand Up @@ -63,12 +65,21 @@ export class InteractionHandlerImpl implements InteractionHandler {
return enabled && !ctrlKey && !!interactionShapes.length;
}

const minimumVerticesAchieved =
(typeof minPosVertices === 'undefined' || minPosVertices <= positiveShapes.length) &&
(typeof minNegVertices === 'undefined' || minPosVertices <= negativeShapes.length);
const minPosVerticesAchieved = typeof minPosVertices === 'undefined' || minPosVertices <= positiveShapes.length;
const minNegVerticesAchieved = typeof minNegVertices === 'undefined' || minPosVertices <= negativeShapes.length;
const minimumVerticesAchieved = minPosVerticesAchieved && minNegVerticesAchieved;
return enabled && !ctrlKey && minimumVerticesAchieved && shapesWereUpdated;
}

private addThreshold(): void {
const { x, y } = this.cursorPosition;
this.threshold = this.canvas
.rect(this.thresholdRectSize, this.thresholdRectSize)
.fill('none')
.addClass('cvat_canvas_threshold');
this.threshold.center(x, y);
}

private addCrosshair(): void {
const { x, y } = this.cursorPosition;
this.crosshair.show(this.canvas, x, y, this.geometry.scale);
Expand All @@ -80,9 +91,12 @@ export class InteractionHandlerImpl implements InteractionHandler {

private interactPoints(): void {
const eventListener = (e: MouseEvent): void => {
if ((e.button === 0 || e.button === 2) && !e.altKey) {
if ((e.button === 0 || (e.button === 2 && this.interactionData.enableNegVertices)) && !e.altKey) {
e.preventDefault();
const [cx, cy] = translateToSVG((this.canvas.node as any) as SVGSVGElement, [e.clientX, e.clientY]);
if (!this.isWithingFrame(cx, cy)) return;
if (!this.isWithinThreshold(cx, cy)) return;

this.currentInteractionShape = this.canvas
.circle((consts.BASE_POINT_SIZE * 2) / this.geometry.scale)
.center(cx, cy)
Expand All @@ -101,6 +115,12 @@ export class InteractionHandlerImpl implements InteractionHandler {

const self = this.currentInteractionShape;
self.on('mouseenter', (): void => {
if (this.interactionData.allowRemoveOnlyLast) {
if (this.interactionShapes.indexOf(self) !== this.interactionShapes.length - 1) {
return;
}
}

self.attr({
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale,
});
Expand Down Expand Up @@ -166,6 +186,10 @@ export class InteractionHandlerImpl implements InteractionHandler {
if (this.interactionData.crosshair) {
this.addCrosshair();
}

if (this.interactionData.enableThreshold) {
this.addThreshold();
}
}

private startInteraction(): void {
Expand All @@ -183,6 +207,11 @@ export class InteractionHandlerImpl implements InteractionHandler {
this.removeCrosshair();
}

if (this.threshold) {
this.threshold.remove();
this.threshold = null;
}

this.canvas.off('mousedown.interaction');
this.interactionShapes.forEach((shape: SVG.Shape): SVG.Shape => shape.remove());
this.interactionShapes = [];
Expand All @@ -192,14 +221,39 @@ export class InteractionHandlerImpl implements InteractionHandler {
}
}

private isWithinThreshold(x: number, y: number): boolean {
const [prev] = this.interactionShapes.slice(-1);
if (!this.interactionData.enableThreshold || !prev) {
return true;
}

const [prevCx, prevCy] = [(prev as SVG.Circle).cx(), (prev as SVG.Circle).cy()];
const xDiff = Math.abs(prevCx - x);
const yDiff = Math.abs(prevCy - y);

return xDiff < this.thresholdRectSize / 2 && yDiff < this.thresholdRectSize / 2;
}

private isWithingFrame(x: number, y: number): boolean {
const { offset, image } = this.geometry;
const { width, height } = image;
const [imageX, imageY] = [Math.round(x - offset), Math.round(y - offset)];
return imageX >= 0 && imageX < width && imageY >= 0 && imageY < height;
}

public constructor(
onInteraction: (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean) => void,
onInteraction: (
shapes: InteractionResult[] | null,
shapesUpdated?: boolean,
isDone?: boolean,
threshold?: number,
) => void,
canvas: SVG.Container,
geometry: Geometry,
) {
this.onInteraction = (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean): void => {
this.shapesWereUpdated = false;
onInteraction(shapes, shapesUpdated, isDone);
onInteraction(shapes, shapesUpdated, isDone, this.threshold ? this.thresholdRectSize / 2 : null);
};
this.canvas = canvas;
this.geometry = geometry;
Expand All @@ -208,6 +262,8 @@ export class InteractionHandlerImpl implements InteractionHandler {
this.interactionData = { enabled: false };
this.currentInteractionShape = null;
this.crosshair = new Crosshair();
this.threshold = null;
this.thresholdRectSize = 300;
this.cursorPosition = {
x: 0,
y: 0,
Expand All @@ -219,6 +275,43 @@ export class InteractionHandlerImpl implements InteractionHandler {
if (this.crosshair) {
this.crosshair.move(x, y);
}
if (this.threshold) {
this.threshold.center(x, y);
}

if (this.interactionData.enableSliding && this.interactionShapes.length) {
if (this.isWithingFrame(x, y)) {
if (this.interactionData.enableThreshold && !this.isWithinThreshold(x, y)) return;
this.onInteraction(
[
...this.prepareResult(),
{
points: [x - this.geometry.offset, y - this.geometry.offset],
shapeType: 'points',
button: 0,
},
],
true,
false,
);
}
}
});

this.canvas.on('wheel.interaction', (e: WheelEvent): void => {
if (e.ctrlKey) {
if (this.threshold) {
const { x, y } = this.cursorPosition;
e.preventDefault();
if (e.deltaY > 0) {
this.thresholdRectSize *= 6 / 5;
} else {
this.thresholdRectSize *= 5 / 6;
}
this.threshold.size(this.thresholdRectSize, this.thresholdRectSize);
this.threshold.center(x, y);
}
}
});

document.body.addEventListener('keyup', (e: KeyboardEvent): void => {
Expand Down
3 changes: 2 additions & 1 deletion cvat-core/src/ml-model.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT

Expand All @@ -17,6 +17,7 @@ class MLModel {
this._params = {
canvas: {
minPosVertices: data.min_pos_points,
enableNegVertices: true,
},
};
}
Expand Down
6 changes: 5 additions & 1 deletion cvat-ui/src/actions/annotation-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ContextMenuType,
Workspace,
Model,
OpenCVTool,
} from 'reducers/interfaces';

import getCore from 'cvat-core-wrapper';
Expand Down Expand Up @@ -1354,7 +1355,10 @@ export function pasteShapeAsync(): ThunkAction {
};
}

export function interactWithCanvas(activeInteractor: Model, activeLabelID: number): AnyAction {
export function interactWithCanvas(
activeInteractor: Model | OpenCVTool,
activeLabelID: number,
): AnyAction {
return {
type: AnnotationActionTypes.INTERACT_WITH_CANVAS,
payload: {
Expand Down
10 changes: 10 additions & 0 deletions cvat-ui/src/assets/opencv.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import MoveControl from './move-control';
import FitControl from './fit-control';
import ResizeControl from './resize-control';
import ToolsControl from './tools-control';
import OpenCVControl from './opencv-control';
import DrawRectangleControl from './draw-rectangle-control';
import DrawPolygonControl from './draw-polygon-control';
import DrawPolylineControl from './draw-polyline-control';
Expand Down Expand Up @@ -90,6 +91,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
ActiveControl.DRAW_RECTANGLE,
ActiveControl.DRAW_CUBOID,
ActiveControl.AI_TOOLS,
ActiveControl.OPENCV_TOOLS,
].includes(activeControl);

if (!drawing) {
Expand All @@ -103,7 +105,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
repeatDrawShape();
}
} else {
if (activeControl === ActiveControl.AI_TOOLS) {
if ([ActiveControl.AI_TOOLS, ActiveControl.OPENCV_TOOLS].includes(activeControl)) {
// separated API method
canvasInstance.interact({ enabled: false });
return;
Expand Down Expand Up @@ -187,6 +189,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {

<hr />
<ToolsControl />
<OpenCVControl />
<DrawRectangleControl
canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_RECTANGLE}
Expand Down
Loading