Skip to content

Commit 6704d32

Browse files
authored
Annotation hole: Mask rendering order (#1129)
1 parent eedbc3b commit 6704d32

File tree

5 files changed

+48
-17
lines changed

5 files changed

+48
-17
lines changed

web_ui/src/pages/annotator/annotation/layers/background-masks.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ import { ShapeFactory } from '../shapes/factory.component';
66

77
interface BackgroundMasksProps {
88
id: string;
9-
annotations: Annotation[];
9+
masks: { idx: number; annotation: Annotation }[];
1010
}
1111

12-
export const BackgroundMasks = ({ id, annotations }: BackgroundMasksProps) => {
12+
export const BackgroundMasks = ({ id, masks }: BackgroundMasksProps) => {
1313
return (
1414
<defs xmlns='http://www.w3.org/2000/svg'>
1515
<mask id={id} aria-label='background-mask'>
1616
<rect width='100%' height='100%' fill='white' />
1717
<g fill='black' fillOpacity='1'>
18-
{annotations.map((annotation) => (
18+
{masks.map(({ annotation }) => (
1919
<ShapeFactory key={annotation.id} annotation={annotation} />
2020
))}
2121
</g>

web_ui/src/pages/annotator/annotation/layers/layer.component.tsx

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
// Copyright (C) 2022-2025 Intel Corporation
22
// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE
33

4-
import { Annotation as AnnotationInterface } from '../../../../core/annotations/annotation.interface';
5-
import { isBackgroundLabel } from '../../../../core/labels/utils';
64
import { hasEqualId, isNonEmptyArray } from '../../../../shared/utils';
75
import { DEFAULT_ANNOTATION_STYLES } from '../../tools/utils';
86
import { Annotation } from '../annotation.component';
97
import { BackgroundMasks } from './background-masks';
10-
import { LayerProps } from './utils';
8+
import { getBackgroundMaskAnnotations, LayerProps } from './utils';
119

1210
import classes from '../../annotator-canvas.module.scss';
1311

14-
const isBackgroundMask = (annotation: AnnotationInterface) => annotation.labels.some(isBackgroundLabel);
15-
1612
export const Layer = ({
1713
width,
1814
height,
@@ -25,24 +21,22 @@ export const Layer = ({
2521
removeBackground = false,
2622
renderLabel,
2723
}: LayerProps) => {
24+
const maskAnnotations = getBackgroundMaskAnnotations(annotations);
2825
const overwriteAnnotationFill = removeBackground ? { '--annotation-fill-opacity': 0 } : {};
2926
// We render each annotation as two layers: one where we draw its shape and another
3027
// where we draw its labels.
3128
// This is done so that we can use HTML inside the canvas (which gets tricky if you
3229
// try to do this inside of a svg element instead)
3330

34-
let savedMasks: AnnotationInterface[] = [];
35-
3631
return (
3732
<div aria-label='annotations'>
38-
{annotations.map((annotation) => {
33+
{annotations.map((annotation, index) => {
3934
const hideAnnotationShape = globalAnnotations.some(hasEqualId(annotation.id));
4035
// Show labels if the annotation's shape is hidden (i.e. global empty annotations),
4136
// otherwise use the user's settings
4237
const showLabel = hideLabels === false || hideAnnotationShape;
4338
const maskId = `${annotation.id}-mask`;
44-
45-
savedMasks = isBackgroundMask(annotation) ? [...savedMasks, annotation] : savedMasks;
39+
const savedMasks = maskAnnotations.filter((mask) => mask.idx >= index);
4640

4741
return (
4842
<div key={annotation.id} className={classes.disabledLayer}>
@@ -54,9 +48,7 @@ export const Layer = ({
5448
id={`annotations-canvas-${annotation.id}-shape`}
5549
aria-label={`annotations-canvas-${annotation.id}-shape`}
5650
>
57-
{isNonEmptyArray(savedMasks) && (
58-
<BackgroundMasks id={maskId} annotations={savedMasks} />
59-
)}
51+
{isNonEmptyArray(savedMasks) && <BackgroundMasks id={maskId} masks={savedMasks} />}
6052

6153
<Annotation
6254
key={annotation.id}

web_ui/src/pages/annotator/annotation/layers/layer.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ describe('Layer Component', () => {
5151
/>
5252
);
5353

54-
expect(screen.getAllByLabelText('background-mask')).toHaveLength(mockedBackgroundAnnotations.length);
54+
// It generates a background mask for each background annotation, plus one for the mask annotation itself
55+
expect(screen.getAllByLabelText('background-mask')).toHaveLength(mockedBackgroundAnnotations.length + 1);
5556
});
5657
});

web_ui/src/pages/annotator/annotation/layers/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { isNil } from 'lodash-es';
77

88
import { Annotation as AnnotationInterface } from '../../../../core/annotations/annotation.interface';
99
import { Explanation } from '../../../../core/annotations/prediction.interface';
10+
import { isBackgroundLabel } from '../../../../core/labels/utils';
1011
import { Task } from '../../../../core/projects/task.interface';
1112
import { hasEqualId } from '../../../../shared/utils';
1213
import { AnnotationToolContext } from '../../core/annotation-tool-context.interface';
@@ -60,3 +61,11 @@ export const filterByExplanationSelection = (
6061
})
6162
.filter(hasValidLabels);
6263
};
64+
65+
export const isBackgroundMask = (annotation: AnnotationInterface) => annotation.labels.some(isBackgroundLabel);
66+
67+
export const getBackgroundMaskAnnotations = (annotations: AnnotationInterface[]) =>
68+
annotations.reduce<{ idx: number; annotation: AnnotationInterface }[]>(
69+
(acc, annotation, idx) => (isBackgroundMask(annotation) ? [...acc, { idx, annotation }] : acc),
70+
[]
71+
);

web_ui/src/pages/media/utils.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (C) 2022-2025 Intel Corporation
22
// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE
33

4+
import { labelFromUser } from '../../core/annotations/utils';
5+
import { LABEL_BEHAVIOUR } from '../../core/labels/label.interface';
46
import { MEDIA_TYPE } from '../../core/media/base-media.interface';
57
import {
68
AdvancedFilterOptions,
@@ -10,8 +12,10 @@ import {
1012
SearchRuleShapeType,
1113
} from '../../core/media/media-filter.interface';
1214
import { KeyMap } from '../../shared/keyboard-events/keyboard.interface';
15+
import { getMockedAnnotation } from '../../test-utils/mocked-items-factory/mocked-annotations';
1316
import { getMockedLabel } from '../../test-utils/mocked-items-factory/mocked-labels';
1417
import { getMockedImageMediaItem } from '../../test-utils/mocked-items-factory/mocked-media';
18+
import { getBackgroundMaskAnnotations, isBackgroundMask } from '../annotator/annotation/layers/utils';
1519
import {
1620
addOrUpdateFilterRule,
1721
concatByProperty,
@@ -338,4 +342,29 @@ describe('media util', () => {
338342
);
339343
});
340344
});
345+
346+
it('isBackgroundMask', () => {
347+
const backgroundAnnotation = getMockedAnnotation({
348+
labels: [labelFromUser(getMockedLabel({ behaviour: LABEL_BEHAVIOUR.BACKGROUND }))],
349+
});
350+
351+
expect(isBackgroundMask(backgroundAnnotation)).toBe(true);
352+
expect(isBackgroundMask(getMockedAnnotation({}))).toBe(false);
353+
});
354+
});
355+
describe('getBackgroundMaskAnnotations', () => {
356+
it('returns array with background mask annotations and their indices', () => {
357+
const defaultLabel = labelFromUser(getMockedLabel({}));
358+
const backgroundLabel = labelFromUser(getMockedLabel({ behaviour: LABEL_BEHAVIOUR.BACKGROUND }));
359+
360+
const annotation1 = getMockedAnnotation({ labels: [defaultLabel] });
361+
const annotation2 = getMockedAnnotation({ labels: [backgroundLabel] });
362+
const annotation3 = getMockedAnnotation({ labels: [defaultLabel, backgroundLabel] });
363+
const annotations = [annotation1, annotation2, annotation3];
364+
365+
expect(getBackgroundMaskAnnotations(annotations)).toEqual([
366+
{ idx: 1, annotation: annotation2 },
367+
{ idx: 2, annotation: annotation3 },
368+
]);
369+
});
341370
});

0 commit comments

Comments
 (0)