Skip to content

Commit db620f2

Browse files
authored
Merge pull request #755 from Kitware/paint-thresholding
feat(paint): thresholding support
2 parents 6c6329a + 63b5159 commit db620f2

3 files changed

Lines changed: 165 additions & 63 deletions

File tree

src/components/PaintControls.vue

Lines changed: 110 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -51,73 +51,128 @@
5151
</v-item>
5252
</v-item-group>
5353
</v-row>
54-
<v-row no-gutters align="center">
55-
<v-slider
56-
v-if="mode === PaintMode.CirclePaint || mode === PaintMode.Erase"
57-
:model-value="brushSize"
58-
@update:model-value="setBrushSize"
59-
density="compact"
60-
hide-details
61-
label="Size"
62-
min="1"
63-
max="50"
64-
>
65-
<template v-slot:append>
66-
<v-text-field
67-
:model-value="brushSize"
68-
@input="setBrushSize"
69-
variant="plain"
70-
class="mt-n3 pt-0 pl-2"
71-
style="width: 60px"
72-
density="compact"
73-
hide-details
74-
type="number"
75-
min="1"
76-
max="50"
77-
/>
78-
</template>
79-
</v-slider>
80-
81-
<FillBetweenControls v-if="mode === PaintMode.FillBetween" />
82-
</v-row>
54+
<template v-if="mode === PaintMode.CirclePaint || mode === PaintMode.Erase">
55+
<v-row no-gutters>Size (pixels)</v-row>
56+
<v-row no-gutters align="center">
57+
<v-slider
58+
:model-value="brushSize"
59+
@update:model-value="setBrushSize"
60+
density="compact"
61+
hide-details
62+
min="1"
63+
max="50"
64+
>
65+
<template v-slot:append>
66+
<v-text-field
67+
:model-value="brushSize"
68+
@input="setBrushSize"
69+
variant="underlined"
70+
class="mt-n3 pt-0 pl-2 opacity-70"
71+
style="width: 60px"
72+
density="compact"
73+
hide-details
74+
type="number"
75+
min="1"
76+
max="50"
77+
/>
78+
</template>
79+
</v-slider>
80+
</v-row>
81+
<v-row no-gutters class="mb-1">Threshold </v-row>
82+
<v-row v-if="currentImageStats" no-gutters align="center">
83+
<v-range-slider
84+
v-model="threshold"
85+
:min="currentImageStats.scalarMin"
86+
:max="currentImageStats.scalarMax"
87+
:step="thresholdStepGranularity"
88+
>
89+
<template #prepend>
90+
<v-text-field
91+
:model-value="thresholdRange[0].toFixed(2)"
92+
@input="setMinThreshold($event.target.value)"
93+
variant="underlined"
94+
class="mt-n3 pt-0 pl-2 opacity-70"
95+
style="width: 80px"
96+
density="compact"
97+
hide-details
98+
type="number"
99+
precision="2"
100+
:min="currentImageStats.scalarMin"
101+
:max="thresholdRange[1]"
102+
/>
103+
</template>
104+
<template #append>
105+
<v-text-field
106+
:model-value="thresholdRange[1].toFixed(2)"
107+
@input="setMaxThreshold($event.target.value)"
108+
variant="underlined"
109+
class="mt-n3 pt-0 pl-2 opacity-70"
110+
style="width: 80px"
111+
density="compact"
112+
hide-details
113+
type="number"
114+
precision="2"
115+
:min="thresholdRange[0]"
116+
:max="currentImageStats.scalarMax"
117+
/>
118+
</template>
119+
</v-range-slider>
120+
</v-row>
121+
</template>
122+
<template v-if="mode === PaintMode.FillBetween">
123+
<v-row no-gutters align="center">
124+
<FillBetweenControls />
125+
</v-row>
126+
</template>
83127
</v-container>
84128
</template>
85129

86-
<script lang="ts">
87-
import { defineComponent, computed } from 'vue';
130+
<script setup lang="ts">
131+
import { computed } from 'vue';
88132
import { storeToRefs } from 'pinia';
89133
import { PaintMode } from '@/src/core/tools/paint';
90-
import { usePaintToolStore } from '../store/tools/paint';
91-
import FillBetweenControls from './FillBetweenControls.vue';
134+
import { usePaintToolStore } from '@/src/store/tools/paint';
135+
import FillBetweenControls from '@/src/components/FillBetweenControls.vue';
136+
import { useCurrentImage } from '@/src/composables/useCurrentImage';
137+
import { useImageStatsStore } from '@/src/store/image-stats';
92138
93-
export default defineComponent({
94-
name: 'PaintControls',
139+
const paintStore = usePaintToolStore();
140+
const imageStatsStore = useImageStatsStore();
141+
const { brushSize, activeMode, thresholdRange } = storeToRefs(paintStore);
142+
const { currentImageID } = useCurrentImage();
95143
96-
components: {
97-
FillBetweenControls,
144+
const currentImageStats = computed(() => {
145+
if (!currentImageID.value) return null;
146+
return imageStatsStore.stats[currentImageID.value] ?? null;
147+
});
148+
const thresholdStepGranularity = computed(() => {
149+
if (!currentImageStats.value) return 1;
150+
const { scalarMin, scalarMax } = currentImageStats.value;
151+
return Math.min(1, (scalarMax - scalarMin) / 256);
152+
});
153+
const threshold = computed({
154+
get: () => thresholdRange.value,
155+
set: (range) => {
156+
paintStore.setThresholdRange(range);
98157
},
158+
});
99159
100-
setup() {
101-
const paintStore = usePaintToolStore();
102-
const { brushSize, activeMode } = storeToRefs(paintStore);
160+
const setMinThreshold = (n: string) => {
161+
threshold.value = [+n, threshold.value[1]];
162+
};
103163
104-
const setBrushSize = (size: number) => {
105-
paintStore.setBrushSize(Number(size));
106-
};
164+
const setMaxThreshold = (n: string) => {
165+
threshold.value = [threshold.value[0], +n];
166+
};
107167
108-
const mode = computed({
109-
get: () => activeMode.value,
110-
set: (m) => {
111-
paintStore.setMode(m);
112-
},
113-
});
168+
const setBrushSize = (size: number) => {
169+
paintStore.setBrushSize(Number(size));
170+
};
114171
115-
return {
116-
brushSize,
117-
setBrushSize,
118-
mode,
119-
PaintMode,
120-
};
172+
const mode = computed({
173+
get: () => activeMode.value,
174+
set: (m) => {
175+
paintStore.setMode(m);
121176
},
122177
});
123178
</script>

src/core/tools/paint/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ export default class PaintTool {
7575
labelmap: vtkLabelMap,
7676
sliceAxis: 0 | 1 | 2,
7777
startPoint: vec3,
78-
endPoint?: vec3
78+
endPoint?: vec3,
79+
shouldPaint: (offset: number, point: number[]) => boolean = () => true
7980
) {
8081
const inBrushingMode =
8182
this.mode === PaintMode.CirclePaint || this.mode === PaintMode.Erase;
@@ -147,9 +148,9 @@ export default class PaintTool {
147148
rounded[1] = Math.round(curPoint[1]);
148149
rounded[2] = Math.round(curPoint[2]);
149150

150-
if (isInBounds(rounded)) {
151-
const offset =
152-
rounded[0] + rounded[1] * jStride + rounded[2] * kStride;
151+
const offset =
152+
rounded[0] + rounded[1] * jStride + rounded[2] * kStride;
153+
if (isInBounds(rounded) && shouldPaint(offset, rounded)) {
153154
labelmapPixels[offset] = brushValue;
154155
}
155156

src/store/tools/paint.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import type { Vector2 } from '@kitware/vtk.js/types';
22
import { useCurrentImage } from '@/src/composables/useCurrentImage';
3-
import { Manifest, StateFile } from '@/src/io/state-file/schema';
3+
import type { Manifest, StateFile } from '@/src/io/state-file/schema';
4+
import type { Maybe } from '@/src/types';
5+
import { useImageStatsStore } from '@/src/store/image-stats';
46
import { computed, ref } from 'vue';
7+
import { watchImmediate } from '@vueuse/core';
58
import { vec3 } from 'gl-matrix';
69
import { defineStore } from 'pinia';
7-
import { Maybe } from '@/src/types';
810
import { PaintMode } from '@/src/core/tools/paint';
911
import { Tools } from './types';
1012
import { useSegmentGroupStore } from '../segmentGroups';
1113

1214
const DEFAULT_BRUSH_SIZE = 4;
15+
const DEFAULT_THRESHOLD_RANGE: Vector2 = [
16+
Number.NEGATIVE_INFINITY,
17+
Number.POSITIVE_INFINITY,
18+
];
1319

1420
export const usePaintToolStore = defineStore('paint', () => {
1521
type _This = ReturnType<typeof usePaintToolStore>;
@@ -20,8 +26,10 @@ export const usePaintToolStore = defineStore('paint', () => {
2026
const brushSize = ref(DEFAULT_BRUSH_SIZE);
2127
const strokePoints = ref<vec3[]>([]);
2228
const isActive = ref(false);
29+
const thresholdRange = ref<Vector2>([...DEFAULT_THRESHOLD_RANGE]);
2330

24-
const { currentImageID } = useCurrentImage();
31+
const { currentImageID, currentImageData } = useCurrentImage();
32+
const imageStatsStore = useImageStatsStore();
2533

2634
function getWidgetFactory(this: _This) {
2735
return this.$paint.factory;
@@ -126,6 +134,17 @@ export const usePaintToolStore = defineStore('paint', () => {
126134
return;
127135
}
128136

137+
const underlyingImagePixels = currentImageData.value
138+
?.getPointData()
139+
.getScalars()
140+
.getData();
141+
const [minThreshold, maxThreshold] = thresholdRange.value;
142+
const shouldPaint = (idx: number) => {
143+
if (!underlyingImagePixels) return false;
144+
const pixValue = underlyingImagePixels[idx];
145+
return minThreshold <= pixValue && pixValue <= maxThreshold;
146+
};
147+
129148
const lastIndex = strokePoints.value.length - 1;
130149
if (lastIndex >= 0) {
131150
const lastPoint = strokePoints.value[lastIndex];
@@ -135,7 +154,8 @@ export const usePaintToolStore = defineStore('paint', () => {
135154
activeLabelmap.value,
136155
axisIndex,
137156
lastPoint,
138-
prevPoint
157+
prevPoint,
158+
shouldPaint
139159
);
140160
}
141161
}
@@ -193,6 +213,26 @@ export const usePaintToolStore = defineStore('paint', () => {
193213
doPaintStroke.call(this, axisIndex);
194214
}
195215

216+
const currentImageStats = computed(() => {
217+
if (!currentImageID.value) return null;
218+
return imageStatsStore.stats[currentImageID.value];
219+
});
220+
221+
function resetThresholdRange(imageID: Maybe<string>) {
222+
if (imageID) {
223+
const stats = imageStatsStore.stats[imageID];
224+
if (stats) {
225+
thresholdRange.value = [stats.scalarMin, stats.scalarMax];
226+
} else {
227+
thresholdRange.value = [...DEFAULT_THRESHOLD_RANGE];
228+
}
229+
}
230+
}
231+
232+
watchImmediate([currentImageID, currentImageStats], ([id]) => {
233+
resetThresholdRange(id);
234+
});
235+
196236
// --- setup and teardown --- //
197237

198238
function activateTool(this: _This) {
@@ -211,6 +251,10 @@ export const usePaintToolStore = defineStore('paint', () => {
211251
isActive.value = false;
212252
}
213253

254+
function setThresholdRange(this: _This, range: Vector2) {
255+
thresholdRange.value = range;
256+
}
257+
214258
function serialize(state: StateFile) {
215259
const { paint } = state.manifest.tools;
216260

@@ -243,6 +287,7 @@ export const usePaintToolStore = defineStore('paint', () => {
243287
brushSize,
244288
strokePoints,
245289
isActive,
290+
thresholdRange,
246291

247292
getWidgetFactory,
248293

@@ -254,6 +299,7 @@ export const usePaintToolStore = defineStore('paint', () => {
254299
setActiveSegment,
255300
setBrushSize,
256301
setSliceAxis,
302+
setThresholdRange,
257303
startStroke,
258304
placeStrokePoint,
259305
endStroke,

0 commit comments

Comments
 (0)