Skip to content

Commit 87796d3

Browse files
authored
Segmentation Masks (#240)
* basic endpoint for mask creation * inital loading and displaying of segmentations * setting default opacity * add RLE back-end support * adding logging and desktop Image creation * minor modifications * supporting webworkers for mask generation * initial canvas editing of masks * segmentation mask editing * editing and adding new segmentation masks * deletion, adressing visualization errors * finalize mask editing interface, linting * preload RLE worker version * improving caching performance * testing scripts created * exporting of masks * poetry upgrade * mask zip extraction, zip importing, job complete notificaiton * add mouse size adjustments * update documentation * fix some rounding issues in subviews * updating shortcuts * remove npm package lock and increment version
1 parent b42b5d4 commit 87796d3

52 files changed

Lines changed: 5805 additions & 2569 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

client/dive-common/apispec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ interface DatasetMeta {
130130
// eslint-disable-next-line @typescript-eslint/no-explicit-any
131131
url: string; filename: string; id: string; metadata?: Record<string, any>;
132132
}[] | undefined>;
133+
masks?: Readonly<{
134+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
135+
url: string; filename: string; id: string; metadata?: Record<string, any>;
136+
}[] | undefined>;
133137
type: Readonly<DatasetType | 'multi'>;
134138
fps: Readonly<number>; // this will become mutable in the future.
135139
name: Readonly<string>;

client/dive-common/components/DeleteControls.vue

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<script lang="ts">
2-
import Vue, { PropType } from 'vue';
2+
import { defineComponent, computed, PropType } from 'vue';
33
import { EditAnnotationTypes } from 'vue-media-annotator/layers/';
44
5-
export default Vue.extend({
5+
export default defineComponent({
66
name: 'DeleteControls',
77
88
props: {
@@ -16,35 +16,40 @@ export default Vue.extend({
1616
},
1717
},
1818
19-
computed: {
20-
disabled(): boolean {
21-
if (this.selectedFeatureHandle < 0 && this.editingMode === false) {
19+
setup(props, { emit }) {
20+
const disabled = computed(() => {
21+
if (props.selectedFeatureHandle < 0 && props.editingMode === false) {
2222
return true;
2323
}
24-
if (this.editingMode === 'rectangle') {
24+
if (props.editingMode === 'rectangle' || props.editingMode === 'Mask') {
2525
return true; // deleting rectangle is unsupported
2626
}
2727
return false;
28-
},
29-
},
28+
});
3029
31-
methods: {
32-
deleteSelected() {
33-
if (this.disabled) {
30+
const deleteSelected = () => {
31+
if (disabled.value) {
3432
throw new Error('Cannot delete while disabled!');
3533
}
36-
if (this.selectedFeatureHandle >= 0) {
37-
this.$emit('delete-point');
34+
if (props.selectedFeatureHandle >= 0) {
35+
emit('delete-point');
3836
} else {
39-
this.$emit('delete-annotation');
37+
emit('delete-annotation');
4038
}
41-
},
42-
toggleTime() {
43-
if (this.disabled) {
39+
};
40+
41+
const toggleTime = () => {
42+
if (disabled.value) {
4443
throw new Error('Cannot endTime while disabled!');
4544
}
46-
this.$emit('toggle-time');
47-
},
45+
emit('toggle-time');
46+
};
47+
48+
return {
49+
disabled,
50+
deleteSelected,
51+
toggleTime,
52+
};
4853
},
4954
});
5055
</script>

client/dive-common/components/EditorMenu.vue

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Mousetrap, OverlayPreferences } from 'vue-media-annotator/types';
77
import { EditAnnotationTypes, VisibleAnnotationTypes } from 'vue-media-annotator/layers';
88
import Recipe from 'vue-media-annotator/recipe';
99
import { hexToRgb } from 'vue-media-annotator/utils';
10+
import { useMasks } from 'vue-media-annotator/provides';
1011
1112
interface ButtonData {
1213
id: string;
@@ -60,18 +61,21 @@ export default defineComponent({
6061
setup(props, { emit }) {
6162
const toolTipForce = ref(false);
6263
let toolTimeTimeout: number | undefined;
64+
const { editorOptions, editorFunctions } = useMasks();
6365
const modeToolTips = {
6466
Creating: {
6567
rectangle: 'Drag to draw rectangle. Press ESC to exit.',
6668
Polygon: 'Click to place vertices. Right click to close.',
6769
LineString: 'Click to place head/tail points.',
6870
Time: 'Automatically creating Time',
71+
Mask: 'Create a Segmentation Mask',
6972
},
7073
Editing: {
7174
rectangle: 'Drag vertices to resize the rectangle',
7275
Polygon: 'Drag midpoints to create new vertices. Click vertices to select for deletion.',
7376
LineString: 'Click endpoints to select for deletion.',
7477
Time: 'Use the keyframe indicator to modify the time annotation, Delete to return to rectangle annotations',
78+
Mask: 'Edit the Segmentation Mask',
7579
},
7680
};
7781
@@ -108,6 +112,16 @@ export default defineComponent({
108112
}],
109113
click: () => emit('set-annotation-state', { editing: 'Time' }),
110114
},
115+
{
116+
id: 'Mask',
117+
icon: 'mdi-draw',
118+
active: props.editingTrack && props.editingMode === 'Mask',
119+
mousetrap: [{
120+
bind: '4',
121+
handler: () => emit('set-annotation-state', { editing: 'Mask' }),
122+
}],
123+
click: () => emit('set-annotation-state', { editing: 'Mask' }),
124+
},
111125
];
112126
// Others should be disabled with a reason
113127
for (let i = 0; i < buttons.length; i += 1) {
@@ -116,13 +130,20 @@ export default defineComponent({
116130
button.disabled = true;
117131
button.disabledReason = 'Time Annotation is Active, delete the Time annotation to enable other modes';
118132
button.color = 'error';
133+
} else if (button.id !== 'Mask' && props.editingTrack && props.editingMode === 'Mask') {
134+
button.disabled = true;
135+
button.disabledReason = 'Mask Annotation is Active, delete/save or exit Mask mode to reanble';
136+
button.color = 'error';
119137
} else {
120138
button.disabled = !props.editingMode;
121139
button.disabledReason = undefined;
122140
button.color = button.active ? editingHeader.value.color : '';
123141
}
124142
}
125-
return buttons;
143+
if (props.editingTrack && props.editingMode !== 'Mask') {
144+
return buttons;
145+
}
146+
return [];
126147
});
127148
128149
const viewButtons = computed((): ButtonData[] => {
@@ -152,12 +173,12 @@ export default defineComponent({
152173
click: () => toggleVisible('LineString'),
153174
},
154175
{
155-
id: 'Time',
156-
type: 'Time',
157-
active: isVisible('Time'),
158-
icon: 'mdi-timer-outline',
159-
tooltip: 'Time Annotation Display',
160-
click: () => toggleVisible('Time'),
176+
id: 'Mask',
177+
type: 'Mask',
178+
active: isVisible('Mask'),
179+
icon: 'mdi-draw',
180+
tooltip: 'Segementation Masks',
181+
click: () => toggleVisible('Mask'),
161182
},
162183
{
163184
id: 'text',
@@ -256,6 +277,15 @@ export default defineComponent({
256277
return { text: 'Not editing', icon: 'mdi-pencil-off-outline', color: '' };
257278
});
258279
280+
const updateMaskOpacity = (event: Event) => {
281+
const target = event.target as HTMLInputElement;
282+
const value = Number.parseFloat(target.value);
283+
editorFunctions.setEditorOptions({ opactiy: value });
284+
};
285+
286+
const maskOpacity = computed(() => editorOptions.opacity.value);
287+
const loadingFrame = computed(() => editorOptions.loadingFrame.value);
288+
259289
return {
260290
toolTipForce,
261291
editButtons,
@@ -267,6 +297,9 @@ export default defineComponent({
267297
mousetrap,
268298
editingHeader,
269299
modeToolTips,
300+
updateMaskOpacity,
301+
maskOpacity,
302+
loadingFrame,
270303
};
271304
},
272305
});
@@ -332,6 +365,7 @@ export default defineComponent({
332365
<span v-if="button.disabledReason"> {{ button.disabledReason }}</span>
333366
</v-tooltip></span>
334367
<slot name="delete-controls" />
368+
<slot name="additional-controls" />
335369
<v-spacer />
336370
<span class="pb-1">
337371
<span class="mr-1 px-3 py-1">
@@ -351,7 +385,59 @@ export default defineComponent({
351385
&& (getUISetting('UIVisibility') === true || getUISetting('UIVisibility')[index])"
352386
>
353387
<template #activator="{ on }">
388+
<v-badge
389+
v-if="button.id === 'Mask'"
390+
overlap
391+
bottom
392+
:color="loadingFrame ? 'primary' : undefined"
393+
:content="!loadingFrame ? '' : undefined"
394+
:icon="loadingFrame ? 'mdi-spin mdi-sync' : undefined"
395+
:value="loadingFrame ? true : false"
396+
offset-x="25"
397+
offset-y="18"
398+
>
399+
400+
<v-menu
401+
open-on-hover
402+
bottom
403+
offset-y
404+
:close-on-content-click="false"
405+
>
406+
<template #activator="{ on, attrs }">
407+
<v-btn
408+
v-if="getUISetting('Masks')"
409+
v-bind="attrs"
410+
:color="isVisible('Mask') ? 'grey darken-2' : ''"
411+
class="mx-1 mode-button"
412+
small
413+
v-on="on"
414+
@click="button.click"
415+
>
416+
<v-icon>{{ button.icon }}</v-icon>
417+
</v-btn>
418+
</template>
419+
<v-card
420+
class="pa-4 flex-column d-flex"
421+
outlined
422+
>
423+
<v-card-text>Segementation Masks</v-card-text>
424+
<label for="frames-before">Opacity: {{ maskOpacity }}</label>
425+
<input
426+
id="frames-before"
427+
type="range"
428+
name="frames-before"
429+
class="tail-slider-width"
430+
label
431+
min="0"
432+
max="100"
433+
:value="maskOpacity"
434+
@input="updateMaskOpacity($event)"
435+
>
436+
</v-card>
437+
</v-menu>
438+
</v-badge>
354439
<v-btn
440+
v-else
355441
:color="button.active ? 'grey darken-2' : ''"
356442
class="mx-1 mode-button"
357443
small

client/dive-common/components/ImportAnnotations.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,15 @@ export default defineComponent({
6363
);
6464
}
6565
66+
let reloadAfter = true;
67+
if (path.endsWith('.zip')) {
68+
reloadAfter = false;
69+
}
70+
if (importFile && reloadAfter) {
71+
await reloadAnnotations();
72+
}
6673
if (importFile) {
6774
processing.value = false;
68-
await reloadAnnotations();
6975
}
7076
}
7177
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -145,6 +151,7 @@ export default defineComponent({
145151
<li> DIVE Annotation JSON </li>
146152
<li> DIVE Configuation JSON</li>
147153
<li> KWCOCO JSON files </li>
154+
<li> Masks Zip File </li>
148155
</ul>
149156
<a
150157
href="https://kitware.github.io/dive/DataFormats/"

0 commit comments

Comments
 (0)