Skip to content

Commit

Permalink
Hide mask during editing (#8554)
Browse files Browse the repository at this point in the history
<!-- Raise an issue to propose your change
(https://github.com/cvat-ai/cvat/issues).
It helps to avoid duplication of efforts from multiple independent
contributors.
Discuss your ideas with maintainers to be sure that changes will be
approved and merged.
Read the [Contribution guide](https://docs.cvat.ai/docs/contributing/).
-->

<!-- Provide a general summary of your changes in the Title above -->

### Motivation and context
<!-- Why is this change required? What problem does it solve? If it
fixes an open
issue, please link to the issue here. Describe your changes in detail,
add
screenshots. -->
Added ability to hide a mask during editing

![hide-mask](https://github.com/user-attachments/assets/d8261e8e-4e83-47c3-9620-937e409ce2ba)


### How has this been tested?
<!-- Please describe in detail how you tested your changes.
Include details of your testing environment, and the tests you ran to
see how your change affects other areas of the code, etc. -->

### Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply.
If an item isn't applicable for some reason, then ~~explicitly
strikethrough~~ the whole
line. If you don't do that, GitHub will show incorrect progress for the
pull request.
If you're unsure about any of these, don't hesitate to ask. We're here
to help! -->
- [x] I submit my changes into the `develop` branch
- [x] I have created a changelog fragment <!-- see top comment in
CHANGELOG.md -->
- ~~[ ] I have updated the documentation accordingly~~
- ~~[ ] I have added tests to cover my changes~~
- ~~[ ] I have linked related issues (see [GitHub docs](

https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword))~~
- [x] I have increased versions of npm packages if it is necessary

([cvat-canvas](https://github.com/cvat-ai/cvat/tree/develop/cvat-canvas#versioning),

[cvat-core](https://github.com/cvat-ai/cvat/tree/develop/cvat-core#versioning),

[cvat-data](https://github.com/cvat-ai/cvat/tree/develop/cvat-data#versioning)
and

[cvat-ui](https://github.com/cvat-ai/cvat/tree/develop/cvat-ui#versioning))

### License

- [x] I submit _my code changes_ under the same [MIT License](
https://github.com/cvat-ai/cvat/blob/develop/LICENSE) that covers the
project.
  Feel free to contact the maintainers if that's a concern.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

- **New Features**
- Added the ability to hide edited objects in the canvas configuration.
  - Enhanced drawing functionality with visibility controls for shapes.
  - Introduced new methods for managing the state of edited annotations.

- **Bug Fixes**
  - Improved handling of drawing actions based on visibility states.

- **Documentation**
- Updated interfaces and action types to reflect new features and state
management capabilities.

- **Chores**
  - Refined component logic to support new visibility state management.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Boris Sekachev <[email protected]>
  • Loading branch information
klakhov and bsekachev authored Oct 31, 2024
1 parent 23e1e0a commit 7f5bc4b
Show file tree
Hide file tree
Showing 17 changed files with 301 additions and 41 deletions.
3 changes: 3 additions & 0 deletions changelog.d/20241018_142148_klakhov_hide_mask.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Added

- Feature to hide a mask during editing (<https://github.com/cvat-ai/cvat/pull/8554>)
2 changes: 1 addition & 1 deletion cvat-canvas/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-canvas",
"version": "2.20.9",
"version": "2.20.10",
"type": "module",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts",
Expand Down
6 changes: 6 additions & 0 deletions cvat-canvas/src/typescript/canvasModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export interface Configuration {
controlPointsSize?: number;
outlinedBorders?: string | false;
resetZoom?: boolean;
hideEditedObject?: boolean;
}

export interface BrushTool {
Expand Down Expand Up @@ -416,6 +417,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
textPosition: consts.DEFAULT_SHAPE_TEXT_POSITION,
textContent: consts.DEFAULT_SHAPE_TEXT_CONTENT,
undefinedAttrValue: consts.DEFAULT_UNDEFINED_ATTR_VALUE,
hideEditedObject: false,
},
imageBitmap: false,
image: null,
Expand Down Expand Up @@ -981,6 +983,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.configuration.CSSImageFilter = configuration.CSSImageFilter;
}

if (typeof configuration.hideEditedObject === 'boolean') {
this.data.configuration.hideEditedObject = configuration.hideEditedObject;
}

this.notify(UpdateReasons.CONFIG_UPDATED);
}

Expand Down
40 changes: 36 additions & 4 deletions cvat-canvas/src/typescript/drawHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import * as SVG from 'svg.js';
import 'svg.draw.js';
import './svg.patch';
import { CIRCLE_STROKE } from './svg.patch';

import { AutoborderHandler } from './autoborderHandler';
import {
Expand Down Expand Up @@ -104,6 +104,7 @@ export class DrawHandlerImpl implements DrawHandler {
private controlPointsSize: number;
private selectedShapeOpacity: number;
private outlinedBorders: string;
private isHidden: boolean;

// we should use any instead of SVG.Shape because svg plugins cannot change declared interface
// so, methods like draw() just undefined for SVG.Shape, but nevertheless they exist
Expand Down Expand Up @@ -1276,6 +1277,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.selectedShapeOpacity = configuration.selectedShapeOpacity;
this.outlinedBorders = configuration.outlinedBorders || 'black';
this.autobordersEnabled = false;
this.isHidden = false;
this.startTimestamp = Date.now();
this.onDrawDoneDefault = onDrawDone;
this.canvas = canvas;
Expand All @@ -1301,10 +1303,28 @@ export class DrawHandlerImpl implements DrawHandler {
});
}

private strokePoint(point: SVG.Element): void {
point.attr('stroke', this.isHidden ? 'none' : CIRCLE_STROKE);
point.fill({ opacity: this.isHidden ? 0 : 1 });
}

private updateHidden(value: boolean) {
this.isHidden = value;

if (value) {
this.canvas.attr('pointer-events', 'none');
} else {
this.canvas.attr('pointer-events', 'all');
}
}

public configurate(configuration: Configuration): void {
this.controlPointsSize = configuration.controlPointsSize;
this.selectedShapeOpacity = configuration.selectedShapeOpacity;
this.outlinedBorders = configuration.outlinedBorders || 'black';
if (this.isHidden !== configuration.hideEditedObject) {
this.updateHidden(configuration.hideEditedObject);
}

const isFillableRect = this.drawData &&
this.drawData.shapeType === 'rectangle' &&
Expand All @@ -1315,15 +1335,26 @@ export class DrawHandlerImpl implements DrawHandler {
const isFilalblePolygon = this.drawData && this.drawData.shapeType === 'polygon';

if (this.drawInstance && (isFillableRect || isFillableCuboid || isFilalblePolygon)) {
this.drawInstance.fill({ opacity: configuration.selectedShapeOpacity });
this.drawInstance.fill({
opacity: configuration.hideEditedObject ? 0 : configuration.selectedShapeOpacity,
});
}

if (this.drawInstance && (isFilalblePolygon)) {
const paintHandler = this.drawInstance.remember('_paintHandler');
if (paintHandler) {
for (const point of (paintHandler as any).set.members) {
this.strokePoint(point);
}
}
}

if (this.drawInstance && this.drawInstance.attr('stroke')) {
this.drawInstance.attr('stroke', this.outlinedBorders);
this.drawInstance.attr('stroke', configuration.hideEditedObject ? 'none' : this.outlinedBorders);
}

if (this.pointsGroup && this.pointsGroup.attr('stroke')) {
this.pointsGroup.attr('stroke', this.outlinedBorders);
this.pointsGroup.attr('stroke', configuration.hideEditedObject ? 'none' : this.outlinedBorders);
}

this.autobordersEnabled = configuration.autoborders;
Expand Down Expand Up @@ -1369,6 +1400,7 @@ export class DrawHandlerImpl implements DrawHandler {
const paintHandler = this.drawInstance.remember('_paintHandler');

for (const point of (paintHandler as any).set.members) {
this.strokePoint(point);
point.attr('stroke-width', `${consts.POINTS_STROKE_WIDTH / geometry.scale}`);
point.attr('r', `${this.controlPointsSize / geometry.scale}`);
}
Expand Down
2 changes: 1 addition & 1 deletion cvat-canvas/src/typescript/editHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ export class EditHandlerImpl implements EditHandler {

const paintHandler = this.editLine.remember('_paintHandler');

for (const point of (paintHandler as any).set.members) {
for (const point of paintHandler.set.members) {
point.attr('stroke-width', `${consts.POINTS_STROKE_WIDTH / geometry.scale}`);
point.attr('r', `${this.controlPointsSize / geometry.scale}`);
}
Expand Down
43 changes: 36 additions & 7 deletions cvat-canvas/src/typescript/masksHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { fabric } from 'fabric';
import debounce from 'lodash/debounce';

import {
DrawData, MasksEditData, Geometry, Configuration, BrushTool, ColorBy,
DrawData, MasksEditData, Geometry, Configuration, BrushTool, ColorBy, Position,
} from './canvasModel';
import consts from './consts';
import { DrawHandler } from './drawHandler';
Expand Down Expand Up @@ -61,10 +61,11 @@ export class MasksHandlerImpl implements MasksHandler {
private editData: MasksEditData | null;

private colorBy: ColorBy;
private latestMousePos: { x: number; y: number; };
private latestMousePos: Position;
private startTimestamp: number;
private geometry: Geometry;
private drawingOpacity: number;
private isHidden: boolean;

private keepDrawnPolygon(): void {
const canvasWrapper = this.canvas.getElement().parentElement;
Expand Down Expand Up @@ -217,12 +218,29 @@ export class MasksHandlerImpl implements MasksHandler {
private imageDataFromCanvas(wrappingBBox: WrappingBBox): Uint8ClampedArray {
const imageData = this.canvas.toCanvasElement()
.getContext('2d').getImageData(
wrappingBBox.left, wrappingBBox.top,
wrappingBBox.right - wrappingBBox.left + 1, wrappingBBox.bottom - wrappingBBox.top + 1,
wrappingBBox.left,
wrappingBBox.top,
wrappingBBox.right - wrappingBBox.left + 1,
wrappingBBox.bottom - wrappingBBox.top + 1,
).data;
return imageData;
}

private updateHidden(value: boolean) {
this.isHidden = value;

// Need to update style of upper canvas explicitly because update of default cursor is not applied immediately
// https://github.com/fabricjs/fabric.js/issues/1456
const newOpacity = value ? '0' : '';
const newCursor = value ? 'inherit' : 'none';
this.canvas.getElement().parentElement.style.opacity = newOpacity;
const upperCanvas = this.canvas.getElement().parentElement.querySelector('.upper-canvas') as HTMLElement;
if (upperCanvas) {
upperCanvas.style.cursor = newCursor;
}
this.canvas.defaultCursor = newCursor;
}

private updateBrushTools(brushTool?: BrushTool, opts: Partial<BrushTool> = {}): void {
if (this.isPolygonDrawing) {
// tool was switched from polygon to brush for example
Expand Down Expand Up @@ -350,6 +368,7 @@ export class MasksHandlerImpl implements MasksHandler {
this.editData = null;
this.drawingOpacity = 0.5;
this.brushMarker = null;
this.isHidden = false;
this.colorBy = ColorBy.LABEL;
this.onDrawDone = onDrawDone;
this.onDrawRepeat = onDrawRepeat;
Expand Down Expand Up @@ -452,7 +471,7 @@ export class MasksHandlerImpl implements MasksHandler {
this.canvas.renderAll();
}

if (isMouseDown && !isBrushSizeChanging && ['brush', 'eraser'].includes(tool?.type)) {
if (isMouseDown && !this.isHidden && !isBrushSizeChanging && ['brush', 'eraser'].includes(tool?.type)) {
const color = fabric.Color.fromHex(tool.color);
color.setAlpha(tool.type === 'eraser' ? 1 : 0.5);

Expand Down Expand Up @@ -530,6 +549,10 @@ export class MasksHandlerImpl implements MasksHandler {

public configurate(configuration: Configuration): void {
this.colorBy = configuration.colorBy;

if (this.isHidden !== configuration.hideEditedObject) {
this.updateHidden(configuration.hideEditedObject);
}
}

public transform(geometry: Geometry): void {
Expand Down Expand Up @@ -563,7 +586,10 @@ export class MasksHandlerImpl implements MasksHandler {
const color = fabric.Color.fromHex(this.getStateColor(drawData.initialState)).getSource();
const [left, top, right, bottom] = points.slice(-4);
const imageBitmap = expandChannels(color[0], color[1], color[2], points);
imageDataToDataURL(imageBitmap, right - left + 1, bottom - top + 1,
imageDataToDataURL(
imageBitmap,
right - left + 1,
bottom - top + 1,
(dataURL: string) => new Promise((resolve) => {
fabric.Image.fromURL(dataURL, (image: fabric.Image) => {
try {
Expand Down Expand Up @@ -654,7 +680,10 @@ export class MasksHandlerImpl implements MasksHandler {
const color = fabric.Color.fromHex(this.getStateColor(editData.state)).getSource();
const [left, top, right, bottom] = points.slice(-4);
const imageBitmap = expandChannels(color[0], color[1], color[2], points);
imageDataToDataURL(imageBitmap, right - left + 1, bottom - top + 1,
imageDataToDataURL(
imageBitmap,
right - left + 1,
bottom - top + 1,
(dataURL: string) => new Promise((resolve) => {
fabric.Image.fromURL(dataURL, (image: fabric.Image) => {
try {
Expand Down
2 changes: 2 additions & 0 deletions cvat-canvas/src/typescript/svg.patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ SVG.Element.prototype.draw.extend(
}),
);

export const CIRCLE_STROKE = '#000';
// Fix method drawCircles
function drawCircles(): void {
const array = this.el.array().valueOf();
Expand All @@ -109,6 +110,7 @@ function drawCircles(): void {
.circle(5)
.stroke({
width: 1,
color: CIRCLE_STROKE,
})
.fill('#ccc')
.center(p.x, p.y),
Expand Down
2 changes: 1 addition & 1 deletion cvat-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.66.2",
"version": "1.66.3",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
Expand Down
51 changes: 50 additions & 1 deletion cvat-ui/src/actions/annotation-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ export enum AnnotationActionTypes {
COLLAPSE_APPEARANCE = 'COLLAPSE_APPEARANCE',
COLLAPSE_OBJECT_ITEMS = 'COLLAPSE_OBJECT_ITEMS',
ACTIVATE_OBJECT = 'ACTIVATE_OBJECT',
UPDATE_EDITED_STATE = 'UPDATE_EDITED_STATE',
HIDE_ACTIVE_OBJECT = 'HIDE_ACTIVE_OBJECT',
REMOVE_OBJECT = 'REMOVE_OBJECT',
REMOVE_OBJECT_SUCCESS = 'REMOVE_OBJECT_SUCCESS',
REMOVE_OBJECT_FAILED = 'REMOVE_OBJECT_FAILED',
Expand Down Expand Up @@ -1320,7 +1322,7 @@ export function searchAnnotationsAsync(
};
}

const ShapeTypeToControl: Record<ShapeType, ActiveControl> = {
export const ShapeTypeToControl: Record<ShapeType, ActiveControl> = {
[ShapeType.RECTANGLE]: ActiveControl.DRAW_RECTANGLE,
[ShapeType.POLYLINE]: ActiveControl.DRAW_POLYLINE,
[ShapeType.POLYGON]: ActiveControl.DRAW_POLYGON,
Expand Down Expand Up @@ -1608,3 +1610,50 @@ export function restoreFrameAsync(frame: number): ThunkAction {
}
};
}

export function changeHideActiveObjectAsync(hide: boolean): ThunkAction {
return async (dispatch: ThunkDispatch, getState): Promise<void> => {
const state = getState();
const { instance: canvas } = state.annotation.canvas;
if (canvas) {
(canvas as Canvas).configure({
hideEditedObject: hide,
});

const { objectState } = state.annotation.editing;
if (objectState) {
objectState.hidden = hide;
await dispatch(updateAnnotationsAsync([objectState]));
}

dispatch({
type: AnnotationActionTypes.HIDE_ACTIVE_OBJECT,
payload: {
hide,
},
});
}
};
}

export function updateEditedStateAsync(objectState: ObjectState | null): ThunkAction {
return async (dispatch: ThunkDispatch, getState): Promise<void> => {
let newActiveObjectHidden = false;
if (objectState) {
newActiveObjectHidden = objectState.hidden;
}

dispatch({
type: AnnotationActionTypes.UPDATE_EDITED_STATE,
payload: {
objectState,
},
});

const state = getState();
const { activeObjectHidden } = state.annotation.canvas;
if (activeObjectHidden !== newActiveObjectHidden) {
dispatch(changeHideActiveObjectAsync(newActiveObjectHidden));
}
};
}
Loading

0 comments on commit 7f5bc4b

Please sign in to comment.