Skip to content

Commit 330b8a8

Browse files
author
Boris Sekachev
authored
Semi-automatic tools enhancements (Stage 0) (#3417)
* Improved removable points, added a button to finish current object * Code refactoring for ai tools and opencv * Fixed style for workspace selector * IOG UX fix * Updated version & changelog * Added 'min_neg_points' parameter to serverless interactors * Return 'min_neg_points' from the server
1 parent bab3366 commit 330b8a8

File tree

31 files changed

+296
-264
lines changed

31 files changed

+296
-264
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Support of cloud storage without copying data into CVAT: server part (<https://github.com/openvinotoolkit/cvat/pull/2620>)
1414
- Filter `is_active` for user list (<https://github.com/openvinotoolkit/cvat/pull/3235>)
1515
- Ability to export/import tasks (<https://github.com/openvinotoolkit/cvat/pull/3056>)
16+
- Explicit "Done" button when drawing any polyshapes (<https://github.com/openvinotoolkit/cvat/pull/3417>)
1617

1718
### Changed
1819

1920
- Updated manifest format, added meta with related images (<https://github.com/openvinotoolkit/cvat/pull/3122>)
2021
- Update of COCO format documentation (<https://github.com/openvinotoolkit/cvat/pull/3197>)
2122
- Updated Webpack Dev Server config to add proxxy (<https://github.com/openvinotoolkit/cvat/pull/3368>)
2223
- Update to Django 3.1.12 (<https://github.com/openvinotoolkit/cvat/pull/3378>)
24+
- Updated visibility for removable points in AI tools (<https://github.com/openvinotoolkit/cvat/pull/3417>)
25+
- Updated UI handling for IOG serverless function (<https://github.com/openvinotoolkit/cvat/pull/3417>)
2326

2427
### Deprecated
2528

cvat-canvas/package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cvat-canvas/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cvat-canvas",
3-
"version": "2.4.5",
3+
"version": "2.5.0",
44
"description": "Part of Computer Vision Annotation Tool which presents its canvas library",
55
"main": "src/canvas.ts",
66
"scripts": {

cvat-canvas/src/scss/canvas.scss

+11
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,17 @@ polyline.cvat_canvas_shape_splitting {
155155
fill: blueviolet;
156156
}
157157

158+
.cvat_canvas_interact_intermediate_shape {
159+
@extend .cvat_canvas_shape;
160+
}
161+
162+
.cvat_canvas_removable_interaction_point {
163+
cursor:
164+
url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K')
165+
10 10,
166+
auto;
167+
}
168+
158169
.svg_select_boundingRect {
159170
opacity: 0;
160171
pointer-events: none;

cvat-canvas/src/typescript/canvasModel.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,14 @@ export interface InteractionData {
7777
crosshair?: boolean;
7878
minPosVertices?: number;
7979
minNegVertices?: number;
80-
enableNegVertices?: boolean;
80+
startWithBox?: boolean;
8181
enableThreshold?: boolean;
8282
enableSliding?: boolean;
8383
allowRemoveOnlyLast?: boolean;
84+
intermediateShape?: {
85+
shapeType: string;
86+
points: number[];
87+
};
8488
}
8589

8690
export interface InteractionResult {
@@ -551,7 +555,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
551555
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
552556
}
553557

554-
if (interactionData.enabled) {
558+
if (interactionData.enabled && !interactionData.intermediateShape) {
555559
if (this.data.interactionData.enabled) {
556560
throw new Error('Interaction has been already started');
557561
} else if (!interactionData.shapeType) {

cvat-canvas/src/typescript/canvasView.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import consts from './consts';
2323
import {
2424
translateToSVG,
2525
translateFromSVG,
26+
translateToCanvas,
2627
pointsToNumberArray,
2728
parsePoints,
2829
displayShapeSize,
@@ -103,7 +104,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
103104

104105
private translateToCanvas(points: number[]): number[] {
105106
const { offset } = this.controller.geometry;
106-
return points.map((coord: number): number => coord + offset);
107+
return translateToCanvas(offset, points);
107108
}
108109

109110
private translateFromCanvas(points: number[]): number[] {
@@ -1267,9 +1268,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
12671268
}
12681269
} else if (reason === UpdateReasons.INTERACT) {
12691270
const data: InteractionData = this.controller.interactionData;
1270-
if (data.enabled && this.mode === Mode.IDLE) {
1271-
this.canvas.style.cursor = 'crosshair';
1272-
this.mode = Mode.INTERACT;
1271+
if (data.enabled && (this.mode === Mode.IDLE || data.intermediateShape)) {
1272+
if (!data.intermediateShape) {
1273+
this.canvas.style.cursor = 'crosshair';
1274+
this.mode = Mode.INTERACT;
1275+
}
12731276
this.interactionHandler.interact(data);
12741277
} else {
12751278
this.canvas.style.cursor = '';

cvat-canvas/src/typescript/interactionHandler.ts

+76-12
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import * as SVG from 'svg.js';
66
import consts from './consts';
77
import Crosshair from './crosshair';
8-
import { translateToSVG } from './shared';
8+
import {
9+
translateToSVG, PropType, stringifyPoints, translateToCanvas,
10+
} from './shared';
911
import { InteractionData, InteractionResult, Geometry } from './canvasModel';
1012

1113
export interface InteractionHandler {
@@ -26,6 +28,8 @@ export class InteractionHandlerImpl implements InteractionHandler {
2628
private crosshair: Crosshair;
2729
private threshold: SVG.Rect | null;
2830
private thresholdRectSize: number;
31+
private intermediateShape: PropType<InteractionData, 'intermediateShape'>;
32+
private drawnIntermediateShape: SVG.Shape;
2933

3034
private prepareResult(): InteractionResult[] {
3135
return this.interactionShapes.map(
@@ -65,8 +69,10 @@ export class InteractionHandlerImpl implements InteractionHandler {
6569
return enabled && !ctrlKey && !!interactionShapes.length;
6670
}
6771

68-
const minPosVerticesAchieved = typeof minPosVertices === 'undefined' || minPosVertices <= positiveShapes.length;
69-
const minNegVerticesAchieved = typeof minNegVertices === 'undefined' || minPosVertices <= negativeShapes.length;
72+
const minPosVerticesDefined = Number.isInteger(minPosVertices);
73+
const minNegVerticesDefined = Number.isInteger(minNegVertices) && minNegVertices >= 0;
74+
const minPosVerticesAchieved = !minPosVerticesDefined || minPosVertices <= positiveShapes.length;
75+
const minNegVerticesAchieved = !minNegVerticesDefined || minNegVertices <= negativeShapes.length;
7076
const minimumVerticesAchieved = minPosVerticesAchieved && minNegVerticesAchieved;
7177
return enabled && !ctrlKey && minimumVerticesAchieved && shapesWereUpdated;
7278
}
@@ -91,7 +97,7 @@ export class InteractionHandlerImpl implements InteractionHandler {
9197

9298
private interactPoints(): void {
9399
const eventListener = (e: MouseEvent): void => {
94-
if ((e.button === 0 || (e.button === 2 && this.interactionData.enableNegVertices)) && !e.altKey) {
100+
if ((e.button === 0 || (e.button === 2 && this.interactionData.minNegVertices >= 0)) && !e.altKey) {
95101
e.preventDefault();
96102
const [cx, cy] = translateToSVG((this.canvas.node as any) as SVGSVGElement, [e.clientX, e.clientY]);
97103
if (!this.isWithinFrame(cx, cy)) return;
@@ -121,8 +127,10 @@ export class InteractionHandlerImpl implements InteractionHandler {
121127
}
122128
}
123129

130+
self.addClass('cvat_canvas_removable_interaction_point');
124131
self.attr({
125132
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale,
133+
r: (consts.BASE_POINT_SIZE * 1.5) / this.geometry.scale,
126134
});
127135

128136
self.on('mousedown', (_e: MouseEvent): void => {
@@ -132,6 +140,9 @@ export class InteractionHandlerImpl implements InteractionHandler {
132140
this.interactionShapes = this.interactionShapes.filter(
133141
(shape: SVG.Shape): boolean => shape !== self,
134142
);
143+
if (this.interactionData.startWithBox && this.interactionShapes.length === 1) {
144+
this.interactionShapes[0].style({ visibility: '' });
145+
}
135146
this.shapesWereUpdated = true;
136147
if (this.shouldRaiseEvent(_e.ctrlKey)) {
137148
this.onInteraction(this.prepareResult(), true, false);
@@ -140,8 +151,10 @@ export class InteractionHandlerImpl implements InteractionHandler {
140151
});
141152

142153
self.on('mouseleave', (): void => {
154+
self.removeClass('cvat_canvas_removable_interaction_point');
143155
self.attr({
144156
'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale,
157+
r: consts.BASE_POINT_SIZE / this.geometry.scale,
145158
});
146159

147160
self.off('mousedown');
@@ -153,7 +166,7 @@ export class InteractionHandlerImpl implements InteractionHandler {
153166
this.canvas.on('mousedown.interaction', eventListener);
154167
}
155168

156-
private interactRectangle(): void {
169+
private interactRectangle(shouldFinish: boolean, onContinue?: () => void): void {
157170
let initialized = false;
158171
const eventListener = (e: MouseEvent): void => {
159172
if (e.button === 0 && !e.altKey) {
@@ -170,11 +183,15 @@ export class InteractionHandlerImpl implements InteractionHandler {
170183
this.canvas.on('mousedown.interaction', eventListener);
171184
this.currentInteractionShape
172185
.on('drawstop', (): void => {
186+
this.canvas.off('mousedown.interaction', eventListener);
173187
this.interactionShapes.push(this.currentInteractionShape);
174188
this.shapesWereUpdated = true;
175189

176-
this.canvas.off('mousedown.interaction', eventListener);
177-
this.interact({ enabled: false });
190+
if (shouldFinish) {
191+
this.interact({ enabled: false });
192+
} else if (onContinue) {
193+
onContinue();
194+
}
178195
})
179196
.addClass('cvat_canvas_shape_drawing')
180197
.attr({
@@ -194,15 +211,24 @@ export class InteractionHandlerImpl implements InteractionHandler {
194211

195212
private startInteraction(): void {
196213
if (this.interactionData.shapeType === 'rectangle') {
197-
this.interactRectangle();
214+
this.interactRectangle(true);
198215
} else if (this.interactionData.shapeType === 'points') {
199-
this.interactPoints();
216+
if (this.interactionData.startWithBox) {
217+
this.interactRectangle(false, (): void => this.interactPoints());
218+
} else {
219+
this.interactPoints();
220+
}
200221
} else {
201222
throw new Error('Interactor implementation supports only rectangle and points');
202223
}
203224
}
204225

205226
private release(): void {
227+
if (this.drawnIntermediateShape) {
228+
this.drawnIntermediateShape.remove();
229+
this.drawnIntermediateShape = null;
230+
}
231+
206232
if (this.crosshair) {
207233
this.removeCrosshair();
208234
}
@@ -241,6 +267,31 @@ export class InteractionHandlerImpl implements InteractionHandler {
241267
return imageX >= 0 && imageX < width && imageY >= 0 && imageY < height;
242268
}
243269

270+
private updateIntermediateShape(): void {
271+
const { intermediateShape, geometry } = this;
272+
if (this.drawnIntermediateShape) {
273+
this.drawnIntermediateShape.remove();
274+
}
275+
276+
if (!intermediateShape) return;
277+
const { shapeType, points } = intermediateShape;
278+
if (shapeType === 'polygon') {
279+
this.drawnIntermediateShape = this.canvas
280+
.polygon(stringifyPoints(translateToCanvas(geometry.offset, points)))
281+
.attr({
282+
'color-rendering': 'optimizeQuality',
283+
'shape-rendering': 'geometricprecision',
284+
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
285+
fill: 'none',
286+
})
287+
.addClass('cvat_canvas_interact_intermediate_shape');
288+
} else {
289+
throw new Error(
290+
`Shape type "${shapeType}" was not implemented at interactionHandler::updateIntermediateShape`,
291+
);
292+
}
293+
}
294+
244295
public constructor(
245296
onInteraction: (
246297
shapes: InteractionResult[] | null,
@@ -264,6 +315,8 @@ export class InteractionHandlerImpl implements InteractionHandler {
264315
this.crosshair = new Crosshair();
265316
this.threshold = null;
266317
this.thresholdRectSize = 300;
318+
this.intermediateShape = null;
319+
this.drawnIntermediateShape = null;
267320
this.cursorPosition = {
268321
x: 0,
269322
y: 0,
@@ -334,16 +387,27 @@ export class InteractionHandlerImpl implements InteractionHandler {
334387
: [...this.interactionShapes];
335388
for (const shape of shapesToBeScaled) {
336389
if (shape.type === 'circle') {
337-
(shape as SVG.Circle).radius(consts.BASE_POINT_SIZE / this.geometry.scale);
338-
shape.attr('stroke-width', consts.POINTS_STROKE_WIDTH / this.geometry.scale);
390+
if (shape.hasClass('cvat_canvas_removable_interaction_point')) {
391+
(shape as SVG.Circle).radius((consts.BASE_POINT_SIZE * 1.5) / this.geometry.scale);
392+
shape.attr('stroke-width', consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale);
393+
} else {
394+
(shape as SVG.Circle).radius(consts.BASE_POINT_SIZE / this.geometry.scale);
395+
shape.attr('stroke-width', consts.POINTS_STROKE_WIDTH / this.geometry.scale);
396+
}
339397
} else {
340398
shape.attr('stroke-width', consts.BASE_STROKE_WIDTH / this.geometry.scale);
341399
}
342400
}
343401
}
344402

345403
public interact(interactionData: InteractionData): void {
346-
if (interactionData.enabled) {
404+
if (interactionData.intermediateShape) {
405+
this.intermediateShape = interactionData.intermediateShape;
406+
this.updateIntermediateShape();
407+
if (this.interactionData.startWithBox) {
408+
this.interactionShapes[0].style({ visibility: 'hidden' });
409+
}
410+
} else if (interactionData.enabled) {
347411
this.interactionData = interactionData;
348412
this.initInteraction();
349413
this.startInteraction();

cvat-canvas/src/typescript/shared.ts

+6
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,9 @@ export function vectorLength(vector: Vector2D): number {
181181
const sqrJ = vector.j ** 2;
182182
return Math.sqrt(sqrI + sqrJ);
183183
}
184+
185+
export function translateToCanvas(offset: number, points: number[]): number[] {
186+
return points.map((coord: number): number => coord + offset);
187+
}
188+
189+
export type PropType<T, Prop extends keyof T> = T[Prop];

cvat-core/src/ml-model.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ class MLModel {
1717
this._params = {
1818
canvas: {
1919
minPosVertices: data.min_pos_points,
20-
enableNegVertices: true,
20+
minNegVertices: data.min_neg_points,
21+
startWithBox: data.startswith_box,
2122
},
2223
};
2324
}

cvat-ui/package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cvat-ui/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cvat-ui",
3-
"version": "1.20.7",
3+
"version": "1.21.0",
44
"description": "CVAT single-page application",
55
"main": "src/index.tsx",
66
"scripts": {
+1-1
Loading

cvat-ui/src/assets/info-icon.svg

+1-1
Loading

cvat-ui/src/assets/main-menu-icon.svg

+1-1
Loading
+1-1
Loading

cvat-ui/src/assets/redo-icon.svg

+1-1
Loading

0 commit comments

Comments
 (0)