Added outlines to interactor mask previews to make mask boundaries easier to distinguish (#10966)
### Motivation and context Interactor preview masks are rendered in white by default, making them difficult to distinguish on bright or grayscale images. This change adds a color picker to the interactor menu and passes the selected color through the canvas interaction payload. The selected color is applied to temporary mask and polygon previews without affecting the color of the final annotation. Existing callers that do not provide a color continue to use white as a fallback. Closes #6111. ### How has this been tested? The following checks were run: - ESLint for the modified `cvat-ui` and `cvat-canvas` files. - Full TypeScript type checking with `yarn type-check`. Both checks passed. ### Checklist - [x] I submit my changes into the `develop` branch - [x] I have created a changelog fragment - [ ] ~~I have updated the documentation accordingly~~ - [ ] I have added tests to cover my changes - [x] 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-using-a-keyword)) ### 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 commit is contained in:
@@ -0,0 +1,4 @@
|
||||
### Added
|
||||
|
||||
- Added outlines to interactor mask previews to make mask boundaries easier to distinguish.
|
||||
(<https://github.com/cvat-ai/cvat/pull/10966>)
|
||||
@@ -250,6 +250,10 @@ g.cvat_canvas_shape_occluded {
|
||||
@extend .cvat_canvas_shape;
|
||||
}
|
||||
|
||||
.cvat_canvas_interact_mask_outline {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cvat_interaction_delete_button {
|
||||
opacity: 0.25;
|
||||
|
||||
|
||||
@@ -138,6 +138,7 @@ export interface InteractionData {
|
||||
shapes: {
|
||||
shapeType: string;
|
||||
points: ArrayLike<number>;
|
||||
maskOutlines?: ArrayLike<number>[];
|
||||
}[];
|
||||
};
|
||||
settings?: {
|
||||
|
||||
@@ -62,6 +62,7 @@ export class InteractionHandlerImpl implements InteractionHandler {
|
||||
private allPrompts: SupportedShapes[];
|
||||
private deletionButtons: Map<SupportedShapes, SVG.G>;
|
||||
private intermediateShapes: (SVG.Image | SVG.Polygon)[];
|
||||
private intermediateMaskOutlines: SVG.Polygon[];
|
||||
private onInteraction: (interactionResult: InteractionResult[], finished?: boolean) => void;
|
||||
private onMessage: (messages: CanvasHint[] | null, topic: string) => void;
|
||||
private geometry: Geometry;
|
||||
@@ -101,6 +102,7 @@ export class InteractionHandlerImpl implements InteractionHandler {
|
||||
this.pointPrompts = [];
|
||||
this.allPrompts = [];
|
||||
this.intermediateShapes = [];
|
||||
this.intermediateMaskOutlines = [];
|
||||
this.deletionButtons = new Map();
|
||||
this.effectiveStrokeWidth = consts.BASE_STROKE_WIDTH / this.geometry.scale;
|
||||
this.effectivePointSize = (configuration.controlPointsSize ?? consts.BASE_POINT_SIZE) / this.geometry.scale;
|
||||
@@ -150,7 +152,9 @@ export class InteractionHandlerImpl implements InteractionHandler {
|
||||
}
|
||||
shape.remove();
|
||||
});
|
||||
this.intermediateMaskOutlines.forEach((outline) => outline.remove());
|
||||
this.intermediateShapes = [];
|
||||
this.intermediateMaskOutlines = [];
|
||||
}
|
||||
|
||||
private release(): void {
|
||||
@@ -278,7 +282,9 @@ export class InteractionHandlerImpl implements InteractionHandler {
|
||||
this.clearIntermediateShapes();
|
||||
|
||||
for (const shape of shapes) {
|
||||
const { points, shapeType } = shape;
|
||||
const {
|
||||
points, shapeType, maskOutlines,
|
||||
} = shape;
|
||||
if (shapeType === 'polygon') {
|
||||
const isInvalidShape = points.length < 3 * 2;
|
||||
const polygon = this.container
|
||||
@@ -309,6 +315,23 @@ export class InteractionHandlerImpl implements InteractionHandler {
|
||||
this.container.node.prepend(image.node);
|
||||
this.intermediateShapes.push(image);
|
||||
|
||||
let insertionPoint = image.node;
|
||||
for (const outline of maskOutlines ?? []) {
|
||||
if (outline.length >= 3 * 2) {
|
||||
const outlinePoints = stringifyPoints(translateToCanvas(this.geometry.offset, outline));
|
||||
const strokeWidth = consts.BASE_STROKE_WIDTH / this.geometry.scale;
|
||||
const maskOutline = this.container
|
||||
.polygon(outlinePoints)
|
||||
.fill('none')
|
||||
.stroke({ color: '#000000', width: strokeWidth })
|
||||
.addClass('cvat_canvas_interact_mask_outline');
|
||||
|
||||
insertionPoint.after(maskOutline.node);
|
||||
insertionPoint = maskOutline.node;
|
||||
this.intermediateMaskOutlines.push(maskOutline);
|
||||
}
|
||||
}
|
||||
|
||||
imageDataToDataURL(
|
||||
imageBitmap,
|
||||
right - left + 1,
|
||||
@@ -554,6 +577,10 @@ export class InteractionHandlerImpl implements InteractionHandler {
|
||||
this.intermediateShapes.forEach((shape) => {
|
||||
shape.fill({ opacity: this.effectiveShapeOpacity });
|
||||
});
|
||||
|
||||
this.intermediateMaskOutlines.forEach((outline) => {
|
||||
outline.stroke({ width: this.effectiveStrokeWidth });
|
||||
});
|
||||
}
|
||||
|
||||
public interact(interactData: InteractionData): void {
|
||||
|
||||
+17
-11
@@ -244,6 +244,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
||||
latestResponse: {
|
||||
rle: Int32Array;
|
||||
points: [number, number][];
|
||||
contours: [number, number][][];
|
||||
approximatedPoints: [number, number][];
|
||||
confidence: number;
|
||||
}[];
|
||||
@@ -544,18 +545,20 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
||||
if (item.type !== ShapeType.MASK) continue;
|
||||
|
||||
const points = Int32Array.from(item.points);
|
||||
const polygonPoints = this.receivePointsFromMask(points);
|
||||
const contours = this.receiveContoursFromMask(points);
|
||||
const polygonPoints = this.receivePointsFromMask(contours);
|
||||
if (polygonPoints.length < 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const approximated = this.approximateResponsePoints(polygonPoints!);
|
||||
const approximated = this.approximateResponsePoints(polygonPoints);
|
||||
const confidenceAttr = item.attributes.find((attr) => attr.spec_id === 0);
|
||||
const confidence = confidenceAttr ? +confidenceAttr.value : 1;
|
||||
showConfidenceControl = showConfidenceControl || !!confidenceAttr;
|
||||
latestResponse.push({
|
||||
rle: points,
|
||||
points: polygonPoints,
|
||||
contours,
|
||||
approximatedPoints: approximated,
|
||||
confidence,
|
||||
});
|
||||
@@ -783,9 +786,10 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
||||
const shapesToBeDrawn = this.interaction.latestResponse
|
||||
.filter(({ confidence }) => typeof confidence !== 'number' || confidence >= thresholdValue)
|
||||
.filter(({ approximatedPoints }) => !convertMasksToPolygons || approximatedPoints.length >= 3)
|
||||
.map(({ rle, approximatedPoints }) => ({
|
||||
.map(({ rle, contours, approximatedPoints }) => ({
|
||||
shapeType: convertMasksToPolygons ? ShapeType.POLYGON : ShapeType.MASK,
|
||||
points: convertMasksToPolygons ? approximatedPoints.flat() : rle,
|
||||
maskOutlines: contours.map((contour) => contour.flat()),
|
||||
}));
|
||||
|
||||
canvasInstance.interact({
|
||||
@@ -1142,7 +1146,15 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
private receivePointsFromMask(mask: Int32Array): [number, number][] {
|
||||
private receivePointsFromMask(contours: [number, number][][]): [number, number][] {
|
||||
if (contours.length) {
|
||||
return contours[0].map<[number, number]>((val) => [val[0], val[1]]);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private receiveContoursFromMask(mask: Int32Array): [number, number][][] {
|
||||
if (!openCVWrapper.isInitialized) {
|
||||
throw new Error('OpenCV was not initialized');
|
||||
}
|
||||
@@ -1152,12 +1164,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
||||
return [];
|
||||
}
|
||||
|
||||
const polygons = openCVWrapper.getContoursFromStateSync({ points: mask, shapeType: ShapeType.MASK });
|
||||
if (polygons.length) {
|
||||
return polygons[0].map<[number, number]>((val) => [val[0], val[1]]);
|
||||
}
|
||||
|
||||
return [];
|
||||
return openCVWrapper.getContoursFromStateSync({ points: mask, shapeType: ShapeType.MASK });
|
||||
}
|
||||
|
||||
private approximateResponsePoints(points: [number, number][]): [number, number][] {
|
||||
@@ -1353,7 +1360,6 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
|
||||
/>
|
||||
<Text>Convert masks to polygons</Text>
|
||||
</div>
|
||||
|
||||
{renderStartWithBox && (
|
||||
<div>
|
||||
<Switch
|
||||
|
||||
Reference in New Issue
Block a user