Skip to content

Commit

Permalink
Remove empty masks (#7295)
Browse files Browse the repository at this point in the history
  • Loading branch information
klakhov committed Jan 18, 2024
1 parent f688908 commit 209826c
Show file tree
Hide file tree
Showing 16 changed files with 258 additions and 64 deletions.
4 changes: 4 additions & 0 deletions changelog.d/20240112_123610_klakhov_remove_empty_masks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Fixed

- Empty masks might be created with `polygon-minus` tool (<https://github.com/opencv/cvat/pull/7295>)
- Empty masks might be created as a result of removing underlying pixels (<https://github.com/opencv/cvat/pull/7295>)
3 changes: 2 additions & 1 deletion cvat-canvas/src/typescript/canvasModel.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022-2023 CVAT.ai Corporation
// Copyright (C) 2022-2024 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

Expand Down Expand Up @@ -103,6 +103,7 @@ export interface BrushTool {
color: string;
form: 'circle' | 'square';
size: number;
onBlockUpdated: (blockedTools: Record<'eraser' | 'polygon-minus', boolean>) => void;
}

export interface DrawData {
Expand Down
77 changes: 64 additions & 13 deletions cvat-canvas/src/typescript/masksHandler.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// Copyright (C) 2022-2023 CVAT.ai Corporation
// Copyright (C) 2022-2024 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

import { fabric } from 'fabric';
import debounce from 'lodash/debounce';

import {
DrawData, MasksEditData, Geometry, Configuration, BrushTool, ColorBy,
Expand Down Expand Up @@ -120,7 +121,7 @@ export class MasksHandlerImpl implements MasksHandler {
this.canvas.clear();
this.canvas.renderAll();
this.isInsertion = false;
this.drawnObjects = [];
this.drawnObjects = this.createDrawnObjectsArray();
this.onDrawDone(null);
}

Expand All @@ -136,7 +137,7 @@ export class MasksHandlerImpl implements MasksHandler {
this.isDrawing = false;
this.isInsertion = false;
this.redraw = null;
this.drawnObjects = [];
this.drawnObjects = this.createDrawnObjectsArray();
this.onDrawDone(null);
}

Expand All @@ -150,7 +151,7 @@ export class MasksHandlerImpl implements MasksHandler {
this.canvas.clear();
this.canvas.renderAll();
this.isEditing = false;
this.drawnObjects = [];
this.drawnObjects = this.createDrawnObjectsArray();
this.onEditDone(null, null);
}

Expand Down Expand Up @@ -255,6 +256,8 @@ export class MasksHandlerImpl implements MasksHandler {
if (this.isDrawing || this.isEditing) {
this.setupBrushMarker();
}

this.updateBlockedTools();
}

if (this.tool?.type?.startsWith('polygon-')) {
Expand Down Expand Up @@ -294,6 +297,42 @@ export class MasksHandlerImpl implements MasksHandler {
}
}

private updateBlockedTools(): void {
if (this.drawnObjects.length === 0) {
this.tool.onBlockUpdated({
eraser: true,
'polygon-minus': true,
});
return;
}
const wrappingBbox = this.getDrawnObjectsWrappingBox();
if (this.brushMarker) {
this.canvas.remove(this.brushMarker);
}
const imageData = this.imageDataFromCanvas(wrappingBbox);
if (this.brushMarker) {
this.canvas.add(this.brushMarker);
}
const rle = zipChannels(imageData);
const emptyMask = rle.length < 2;
this.tool.onBlockUpdated({
eraser: emptyMask,
'polygon-minus': emptyMask,
});
}

private createDrawnObjectsArray(): MasksHandlerImpl['drawnObjects'] {
const drawnObjects = [];
const updateBlockedToolsDebounced = debounce(this.updateBlockedTools.bind(this), 250);
return new Proxy(drawnObjects, {
set(target, property, value) {
target[property] = value;
updateBlockedToolsDebounced();
return true;
},
});
}

public constructor(
onDrawDone: MasksHandlerImpl['onDrawDone'],
onDrawRepeat: MasksHandlerImpl['onDrawRepeat'],
Expand All @@ -310,7 +349,6 @@ export class MasksHandlerImpl implements MasksHandler {
this.isPolygonDrawing = false;
this.drawData = null;
this.editData = null;
this.drawnObjects = [];
this.drawingOpacity = 0.5;
this.brushMarker = null;
this.colorBy = ColorBy.LABEL;
Expand All @@ -326,6 +364,7 @@ export class MasksHandlerImpl implements MasksHandler {
defaultCursor: 'inherit',
});
this.canvas.imageSmoothingEnabled = false;
this.drawnObjects = this.createDrawnObjectsArray();

this.canvas.getElement().parentElement.addEventListener('contextmenu', (e: MouseEvent) => e.preventDefault());
this.latestMousePos = { x: -1, y: -1 };
Expand All @@ -349,6 +388,7 @@ export class MasksHandlerImpl implements MasksHandler {
this.onDrawDone({
shapeType: this.drawData.shapeType,
points: rle,
label: this.drawData.initialState.label,
}, Date.now() - this.startTimestamp, continueInserting, this.drawData);

if (!continueInserting) {
Expand Down Expand Up @@ -445,7 +485,7 @@ export class MasksHandlerImpl implements MasksHandler {
}

this.canvas.add(shape);
if (tool.type === 'brush') {
if (['brush', 'eraser'].includes(tool?.type)) {
this.drawnObjects.push(shape);
}

Expand All @@ -467,7 +507,7 @@ export class MasksHandlerImpl implements MasksHandler {
});

this.canvas.add(line);
if (tool.type === 'brush') {
if (['brush', 'eraser'].includes(tool?.type)) {
this.drawnObjects.push(line);
}
}
Expand Down Expand Up @@ -563,11 +603,17 @@ export class MasksHandlerImpl implements MasksHandler {
const imageData = this.imageDataFromCanvas(wrappingBbox);
const rle = zipChannels(imageData);
rle.push(wrappingBbox.left, wrappingBbox.top, wrappingBbox.right, wrappingBbox.bottom);
this.onDrawDone({
shapeType: this.drawData.shapeType,
points: rle,
...(Number.isInteger(this.redraw) ? { clientID: this.redraw } : {}),
}, Date.now() - this.startTimestamp, drawData.continue, this.drawData);

const isEmptyMask = rle.length < 6;
if (isEmptyMask) {
this.onDrawDone(null);
} else {
this.onDrawDone({
shapeType: this.drawData.shapeType,
points: rle,
...(Number.isInteger(this.redraw) ? { clientID: this.redraw } : {}),
}, Date.now() - this.startTimestamp, drawData.continue, this.drawData);
}
}
} finally {
this.releaseDraw();
Expand Down Expand Up @@ -634,7 +680,12 @@ export class MasksHandlerImpl implements MasksHandler {
const imageData = this.imageDataFromCanvas(wrappingBbox);
const rle = zipChannels(imageData);
rle.push(wrappingBbox.left, wrappingBbox.top, wrappingBbox.right, wrappingBbox.bottom);
this.onEditDone(this.editData.state, rle);
const isEmptyMask = rle.length < 6;
if (isEmptyMask) {
this.onEditDone(null, null);
} else {
this.onEditDone(this.editData.state, rle);
}
}
} finally {
this.releaseEdit();
Expand Down
12 changes: 11 additions & 1 deletion cvat-core/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Copyright (C) 2018-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// Copyright (C) 2023-2024 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

const { join } = require('path');

module.exports = {
ignorePatterns: [
'.eslintrc.cjs',
Expand All @@ -16,4 +18,12 @@ module.exports = {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
rules: {
'import/no-extraneous-dependencies': [
'error',
{
packageDir: [__dirname, join(__dirname, '../')]
},
],
}
};
4 changes: 1 addition & 3 deletions cvat-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "14.0.4",
"version": "14.1.0",
"type": "module",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "src/api.ts",
Expand Down Expand Up @@ -28,7 +28,6 @@
"jest-junit": "^6.4.0"
},
"dependencies": {
"@types/lodash": "^4.14.191",
"axios": "^1.6.0",
"axios-retry": "^4.0.0",
"cvat-data": "link:./../cvat-data",
Expand All @@ -37,7 +36,6 @@
"form-data": "^4.0.0",
"js-cookie": "^3.0.1",
"json-logic-js": "^2.0.1",
"lodash": "^4.17.21",
"platform": "^1.3.5",
"quickhull": "^1.0.3",
"store": "^2.0.12",
Expand Down
36 changes: 31 additions & 5 deletions cvat-core/src/annotations-collection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022-2023 CVAT.ai Corporation
// Copyright (C) 2022-2024 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

Expand Down Expand Up @@ -1006,6 +1006,10 @@ export default class Collection {
);
}

if (state.shapeType === 'mask' && state.points.length < 6) {
throw new ArgumentError('Could not create empty mask');
}

if (state.objectType === 'shape') {
constructed.shapes.push({
attributes,
Expand Down Expand Up @@ -1095,27 +1099,49 @@ export default class Collection {
// eslint-disable-next-line no-unsanitized/method
const imported = this.import(constructed);
const importedArray = imported.tags.concat(imported.tracks).concat(imported.shapes);
const additionalUndo = [];
const additionalRedo = [];
const additionalClientIDs = [];
let globalEmptyMaskOccurred = false;
for (const object of importedArray) {
if (object.shapeType === ShapeType.MASK && config.removeUnderlyingMaskPixels) {
(object as MaskShape).removeUnderlyingPixels(object.frame);
if (object.shapeType === ShapeType.MASK && config.removeUnderlyingMaskPixels.enabled) {
const {
clientIDs,
emptyMaskOccurred,
undo: undoWithUnderlyingPixels,
redo: redoWithUnderlyingPixels,
} = (object as MaskShape).removeUnderlyingPixels(object.frame);
additionalUndo.push(undoWithUnderlyingPixels);
additionalRedo.push(redoWithUnderlyingPixels);
additionalClientIDs.push(clientIDs);
globalEmptyMaskOccurred = emptyMaskOccurred || globalEmptyMaskOccurred;
}
}

if (config.removeUnderlyingMaskPixels.enabled && globalEmptyMaskOccurred) {
config.removeUnderlyingMaskPixels?.onEmptyMaskOccurrence();
}
if (objectStates.length) {
this.history.do(
HistoryActions.CREATED_OBJECTS,
() => {
importedArray.forEach((object) => {
object.removed = true;
});
additionalUndo.forEach((undo) => {
undo();
});
},
() => {
importedArray.forEach((object) => {
object.removed = false;
object.serverID = undefined;
});

additionalRedo.forEach((redo) => {
redo();
});
},
importedArray.map((object) => object.clientID),
[...importedArray.map((object) => object.clientID), ...additionalClientIDs.flat()],
objectStates[0].frame,
);
}
Expand Down
Loading

0 comments on commit 209826c

Please sign in to comment.