Skip to content

Commit aaced9e

Browse files
committed
feat(paint): add fill holes segmentation process
Fill Holes fills enclosed background regions in a segment group on a slice. Options select current-slice vs whole-volume and all-segments vs the selected segment. Only background voxels are filled, so existing segments are never overwritten. Fill Holes is the default process.
1 parent b805c9c commit aaced9e

20 files changed

Lines changed: 1353 additions & 105 deletions
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<template>
2+
<div class="d-flex align-center">
3+
<mini-expansion-panel>
4+
<template #title
5+
>Fill enclosed holes in the segmentation on the axis of the selected
6+
view.</template
7+
>
8+
<ul>
9+
<li>Finds background regions fully enclosed by segments on a slice.</li>
10+
<li>
11+
Fills holes on the current slice by default, or every slice of the
12+
active view's axis.
13+
</li>
14+
<li>
15+
All-segments mode fills each hole with the surrounding segment's
16+
label; selected-segment mode fills only the active segment's holes.
17+
</li>
18+
</ul>
19+
</mini-expansion-panel>
20+
</div>
21+
22+
<div
23+
v-for="scope in scopes"
24+
:key="scope.label"
25+
class="w-100 px-4 mb-4 d-flex flex-column align-center"
26+
>
27+
<div class="text-caption mb-1">{{ scope.label }}</div>
28+
<v-btn-toggle
29+
:model-value="scope.value"
30+
density="compact"
31+
variant="outlined"
32+
divided
33+
mandatory
34+
:disabled="isDisabled"
35+
@update:model-value="scope.onChange"
36+
>
37+
<v-btn
38+
v-for="option in scope.options"
39+
:key="option.value"
40+
:value="option.value"
41+
size="small"
42+
>
43+
{{ option.label }}
44+
</v-btn>
45+
</v-btn-toggle>
46+
</div>
47+
</template>
48+
49+
<script setup lang="ts">
50+
import { computed } from 'vue';
51+
import {
52+
useFillHolesStore,
53+
FillHolesSliceScope,
54+
FillHolesSegmentScope,
55+
} from '@/src/store/tools/fillHoles';
56+
import { usePaintProcessStore } from '@/src/store/tools/paintProcess';
57+
import MiniExpansionPanel from './MiniExpansionPanel.vue';
58+
59+
const fillHolesStore = useFillHolesStore();
60+
const processStore = usePaintProcessStore();
61+
62+
const isDisabled = computed(() => processStore.processStep !== 'start');
63+
64+
type ScopeControl = {
65+
label: string;
66+
value: string;
67+
onChange: (value: string) => void;
68+
options: { value: string; label: string }[];
69+
};
70+
71+
const scopes = computed<ScopeControl[]>(() => [
72+
{
73+
label: 'Slices',
74+
value: fillHolesStore.sliceScope,
75+
onChange: (value) =>
76+
fillHolesStore.setSliceScope(value as FillHolesSliceScope),
77+
options: [
78+
{ value: FillHolesSliceScope.CurrentSlice, label: 'Current slice' },
79+
{ value: FillHolesSliceScope.WholeVolume, label: 'All slices' },
80+
],
81+
},
82+
{
83+
label: 'Segments',
84+
value: fillHolesStore.segmentScope,
85+
onChange: (value) =>
86+
fillHolesStore.setSegmentScope(value as FillHolesSegmentScope),
87+
options: [
88+
{ value: FillHolesSegmentScope.AllSegments, label: 'All segments' },
89+
{
90+
value: FillHolesSegmentScope.SelectedSegment,
91+
label: 'Selected segment',
92+
},
93+
],
94+
},
95+
]);
96+
</script>

src/components/GaussianSmoothParameterControls.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,6 @@ const gaussianSmoothStore = useGaussianSmoothStore();
4646
const processStore = usePaintProcessStore();
4747
4848
const sigma = computed(() => gaussianSmoothStore.sigma);
49-
const isDisabled = computed(() => processStore.processStep === 'previewing');
49+
const isDisabled = computed(() => processStore.processStep !== 'start');
5050
const { setSigma } = gaussianSmoothStore;
5151
</script>

src/components/PaintControls.vue

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<v-container>
33
<v-row no-gutters align="center" justify="center" class="mb-4">
44
<v-item-group
5-
v-model="mode"
5+
v-model="controlsMode"
66
mandatory
77
selected-class="selected"
88
class="d-flex align-center justify-center"
@@ -48,7 +48,12 @@
4848
</v-item>
4949
</v-item-group>
5050
</v-row>
51-
<template v-if="mode === PaintMode.CirclePaint || mode === PaintMode.Erase">
51+
<template
52+
v-if="
53+
controlsMode === PaintMode.CirclePaint ||
54+
controlsMode === PaintMode.Erase
55+
"
56+
>
5257
<v-row no-gutters align="center" class="mb-2">
5358
<span class="mr-2">Sync Views</span>
5459
<v-switch
@@ -126,7 +131,7 @@
126131
</v-range-slider>
127132
</v-row>
128133
</template>
129-
<template v-if="mode === PaintMode.Process">
134+
<template v-if="controlsMode === PaintMode.Process">
130135
<v-row no-gutters align="center">
131136
<ProcessControls />
132137
</v-row>
@@ -145,7 +150,7 @@ import { useImageStatsStore } from '@/src/store/image-stats';
145150
146151
const paintStore = usePaintToolStore();
147152
const imageStatsStore = useImageStatsStore();
148-
const { brushSize, activeMode, thresholdRange, crossPlaneSync } =
153+
const { brushSize, activeControlsMode, thresholdRange, crossPlaneSync } =
149154
storeToRefs(paintStore);
150155
const { currentImageID } = useCurrentImage();
151156
@@ -177,10 +182,10 @@ const setBrushSize = (size: number) => {
177182
paintStore.setBrushSize(Number(size));
178183
};
179184
180-
const mode = computed({
181-
get: () => activeMode.value,
185+
const controlsMode = computed({
186+
get: () => activeControlsMode.value,
182187
set: (m) => {
183-
paintStore.setMode(m);
188+
paintStore.setControlsMode(m);
184189
},
185190
});
186191
</script>

src/components/ProcessControls.vue

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,28 @@
22
<div class="d-flex flex-column align-center w-100">
33
<ProcessTypeSelector />
44

5-
<template v-if="activeProcessType === ProcessType.FillBetween">
6-
<FillBetweenParameterControls />
7-
<ProcessWorkflow :algorithm="fillBetweenAlgorithm" />
8-
</template>
9-
10-
<template v-if="activeProcessType === ProcessType.GaussianSmooth">
11-
<GaussianSmoothParameterControls />
12-
<ProcessWorkflow :algorithm="gaussianSmoothAlgorithm" />
5+
<template v-if="activeDefinition">
6+
<component :is="activeDefinition.controls" />
7+
<ProcessWorkflow
8+
:algorithm="activeDefinition.getAlgorithm()"
9+
:requires-active-segment="
10+
activeDefinition.requiresActiveSegment?.() ?? true
11+
"
12+
/>
1313
</template>
1414
</div>
1515
</template>
1616

1717
<script setup lang="ts">
1818
import { computed } from 'vue';
19-
import {
20-
ProcessType,
21-
usePaintProcessStore,
22-
} from '@/src/store/tools/paintProcess';
19+
import { usePaintProcessStore } from '@/src/store/tools/paintProcess';
2320
import ProcessTypeSelector from './ProcessTypeSelector.vue';
2421
import ProcessWorkflow from './ProcessWorkflow.vue';
25-
import FillBetweenParameterControls from './FillBetweenParameterControls.vue';
26-
import GaussianSmoothParameterControls from './GaussianSmoothParameterControls.vue';
27-
import { useFillBetweenStore } from '../store/tools/fillBetween';
28-
import { useGaussianSmoothStore } from '../store/tools/gaussianSmooth';
22+
import { PROCESS_DEFINITIONS } from './processes';
2923
3024
const processStore = usePaintProcessStore();
31-
const fillBetweenStore = useFillBetweenStore();
32-
const gaussianSmoothStore = useGaussianSmoothStore();
33-
34-
const activeProcessType = computed(() => processStore.activeProcessType);
3525
36-
const fillBetweenAlgorithm = fillBetweenStore.computeAlgorithm;
37-
const gaussianSmoothAlgorithm = gaussianSmoothStore.computeAlgorithm;
26+
const activeDefinition = computed(() =>
27+
PROCESS_DEFINITIONS.find((def) => def.type === processStore.activeProcessType)
28+
);
3829
</script>

src/components/ProcessTypeSelector.vue

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
v-model="activeProcessType"
55
mandatory
66
selected-class="selected"
7-
class="d-flex align-center justify-center"
7+
class="d-flex align-center justify-center flex-wrap"
88
>
99
<v-item
10-
:value="ProcessType.FillBetween"
10+
v-for="definition in PROCESS_DEFINITIONS"
11+
:key="definition.type"
12+
:value="definition.type"
1113
v-slot="{ selectedClass, toggle }"
1214
>
1315
<v-btn
@@ -17,23 +19,8 @@
1719
:class="['process-button', 'mx-2', selectedClass]"
1820
@click.stop="toggle"
1921
>
20-
<v-icon>mdi-layers-triple</v-icon>
21-
<span class="text-caption">Fill Between</span>
22-
</v-btn>
23-
</v-item>
24-
<v-item
25-
:value="ProcessType.GaussianSmooth"
26-
v-slot="{ selectedClass, toggle }"
27-
>
28-
<v-btn
29-
variant="tonal"
30-
rounded="8"
31-
stacked
32-
:class="['process-button', 'mx-2', selectedClass]"
33-
@click.stop="toggle"
34-
>
35-
<v-icon>mdi-blur</v-icon>
36-
<span class="text-caption">Smooth</span>
22+
<v-icon>{{ definition.icon }}</v-icon>
23+
<span class="text-caption">{{ definition.label }}</span>
3724
</v-btn>
3825
</v-item>
3926
</v-item-group>
@@ -42,10 +29,8 @@
4229

4330
<script setup lang="ts">
4431
import { computed } from 'vue';
45-
import {
46-
ProcessType,
47-
usePaintProcessStore,
48-
} from '@/src/store/tools/paintProcess';
32+
import { usePaintProcessStore } from '@/src/store/tools/paintProcess';
33+
import { PROCESS_DEFINITIONS } from './processes';
4934
5035
const processStore = usePaintProcessStore();
5136

src/components/ProcessWorkflow.vue

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,16 @@
1616
<v-btn-toggle
1717
v-if="processStep === 'previewing'"
1818
:model-value="showingOriginal ? 0 : 1"
19-
@update:model-value="handleToggleChange"
2019
mandatory
2120
variant="outlined"
2221
divided
2322
density="compact"
2423
>
25-
<v-btn :value="0" size="small">
24+
<v-btn :value="0" size="small" @click="processStore.togglePreview()">
2625
<v-icon start size="small">mdi-eye-outline</v-icon>
2726
Original
2827
</v-btn>
29-
<v-btn :value="1" size="small">
28+
<v-btn :value="1" size="small" @click="processStore.togglePreview()">
3029
<v-icon start size="small">mdi-eye-settings</v-icon>
3130
Processed
3231
</v-btn>
@@ -60,9 +59,12 @@ import {
6059
6160
interface Props {
6261
algorithm: ProcessAlgorithm;
62+
requiresActiveSegment?: boolean;
6363
}
6464
65-
const props = defineProps<Props>();
65+
const props = withDefaults(defineProps<Props>(), {
66+
requiresActiveSegment: true,
67+
});
6668
6769
const processStore = usePaintProcessStore();
6870
const paintStore = usePaintToolStore();
@@ -73,14 +75,9 @@ const showingOriginal = computed(() => processStore.showingOriginal);
7375
function startCompute() {
7476
const id = paintStore.activeSegmentGroupID;
7577
if (!id) return;
76-
processStore.startProcess(id, props.algorithm);
77-
}
78-
79-
function handleToggleChange(value: number) {
80-
const shouldShowOriginal = value === 0;
81-
if (shouldShowOriginal !== showingOriginal.value) {
82-
processStore.togglePreview();
83-
}
78+
processStore.startProcess(id, props.algorithm, {
79+
requiresActiveSegment: props.requiresActiveSegment,
80+
});
8481
}
8582
8683
function handleCancel() {

src/components/processes.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { Component } from 'vue';
2+
import {
3+
ProcessType,
4+
type ProcessAlgorithm,
5+
} from '@/src/store/tools/paintProcess';
6+
import {
7+
useFillHolesStore,
8+
FillHolesSegmentScope,
9+
} from '@/src/store/tools/fillHoles';
10+
import { useFillBetweenStore } from '@/src/store/tools/fillBetween';
11+
import { useGaussianSmoothStore } from '@/src/store/tools/gaussianSmooth';
12+
import FillHolesParameterControls from './FillHolesParameterControls.vue';
13+
import FillBetweenParameterControls from './FillBetweenParameterControls.vue';
14+
import GaussianSmoothParameterControls from './GaussianSmoothParameterControls.vue';
15+
16+
export type ProcessDefinition = {
17+
type: ProcessType;
18+
label: string;
19+
icon: string;
20+
controls: Component;
21+
// Resolved lazily so each call reads the live store (Pinia singletons).
22+
getAlgorithm: () => ProcessAlgorithm;
23+
// Whether the process needs the active segment. Defaults to true; processes
24+
// that act on every segment opt out. Resolved lazily for reactivity.
25+
requiresActiveSegment?: () => boolean;
26+
};
27+
28+
// Single source of truth for the paint processes. Adding a process is one
29+
// entry here instead of synchronized edits across the selector, the controls,
30+
// and the type enum.
31+
export const PROCESS_DEFINITIONS: ProcessDefinition[] = [
32+
{
33+
type: ProcessType.FillHoles,
34+
label: 'Fill Holes',
35+
icon: 'mdi-format-color-fill',
36+
controls: FillHolesParameterControls,
37+
getAlgorithm: () => useFillHolesStore().computeAlgorithm,
38+
requiresActiveSegment: () =>
39+
useFillHolesStore().segmentScope ===
40+
FillHolesSegmentScope.SelectedSegment,
41+
},
42+
{
43+
type: ProcessType.FillBetween,
44+
label: 'Fill Between',
45+
icon: 'mdi-layers-triple',
46+
controls: FillBetweenParameterControls,
47+
getAlgorithm: () => useFillBetweenStore().computeAlgorithm,
48+
},
49+
{
50+
type: ProcessType.GaussianSmooth,
51+
label: 'Smooth',
52+
icon: 'mdi-blur',
53+
controls: GaussianSmoothParameterControls,
54+
getAlgorithm: () => useGaussianSmoothStore().computeAlgorithm,
55+
},
56+
];

src/components/tools/paint/PaintTool.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
<script lang="ts">
1313
import { computed, defineComponent, PropType } from 'vue';
1414
import { usePaintToolStore } from '@/src/store/tools/paint';
15-
import { PaintMode } from '@/src/core/tools/paint';
1615
import { LPSAxisDir } from '@/src/types/lps';
1716
import { Maybe } from '@/src/types';
1817
import PaintWidget2D from './PaintWidget2D.vue';
@@ -36,7 +35,7 @@ export default defineComponent({
3635
setup() {
3736
const paintStore = usePaintToolStore();
3837
const active = computed(
39-
() => paintStore.isActive && paintStore.activeMode !== PaintMode.Process
38+
() => paintStore.isActive && paintStore.isPaintingModeActive
4039
);
4140
4241
return {

0 commit comments

Comments
 (0)