Skip to content

Commit b57987a

Browse files
committed
feat(paint): thresholding support
1 parent 6c6329a commit b57987a

3 files changed

Lines changed: 151 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="max-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="max-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: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
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';
57
import { vec3 } from 'gl-matrix';
68
import { defineStore } from 'pinia';
7-
import { Maybe } from '@/src/types';
89
import { PaintMode } from '@/src/core/tools/paint';
910
import { Tools } from './types';
1011
import { useSegmentGroupStore } from '../segmentGroups';
1112

1213
const DEFAULT_BRUSH_SIZE = 4;
14+
const DEFAULT_THRESHOLD_RANGE: Vector2 = [
15+
Number.NEGATIVE_INFINITY,
16+
Number.POSITIVE_INFINITY,
17+
];
1318

1419
export const usePaintToolStore = defineStore('paint', () => {
1520
type _This = ReturnType<typeof usePaintToolStore>;
@@ -20,8 +25,10 @@ export const usePaintToolStore = defineStore('paint', () => {
2025
const brushSize = ref(DEFAULT_BRUSH_SIZE);
2126
const strokePoints = ref<vec3[]>([]);
2227
const isActive = ref(false);
28+
const thresholdRange = ref<Vector2>([...DEFAULT_THRESHOLD_RANGE]);
2329

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

2633
function getWidgetFactory(this: _This) {
2734
return this.$paint.factory;
@@ -126,6 +133,17 @@ export const usePaintToolStore = defineStore('paint', () => {
126133
return;
127134
}
128135

136+
const underlyingImagePixels = currentImageData.value
137+
?.getPointData()
138+
.getScalars()
139+
.getData();
140+
const [minThreshold, maxThreshold] = thresholdRange.value;
141+
const shouldPaint = (idx: number) => {
142+
if (!underlyingImagePixels) return false;
143+
const pixValue = underlyingImagePixels[idx];
144+
return minThreshold <= pixValue && pixValue <= maxThreshold;
145+
};
146+
129147
const lastIndex = strokePoints.value.length - 1;
130148
if (lastIndex >= 0) {
131149
const lastPoint = strokePoints.value[lastIndex];
@@ -135,7 +153,8 @@ export const usePaintToolStore = defineStore('paint', () => {
135153
activeLabelmap.value,
136154
axisIndex,
137155
lastPoint,
138-
prevPoint
156+
prevPoint,
157+
shouldPaint
139158
);
140159
}
141160
}
@@ -203,6 +222,13 @@ export const usePaintToolStore = defineStore('paint', () => {
203222
ensureActiveSegmentGroupForImage(imageID);
204223
this.$paint.setBrushSize(this.brushSize);
205224

225+
const stats = imageStatsStore.stats[imageID];
226+
if (stats) {
227+
thresholdRange.value = [stats.scalarMin, stats.scalarMax];
228+
} else {
229+
thresholdRange.value = [...DEFAULT_THRESHOLD_RANGE];
230+
}
231+
206232
isActive.value = true;
207233
return true;
208234
}
@@ -211,6 +237,10 @@ export const usePaintToolStore = defineStore('paint', () => {
211237
isActive.value = false;
212238
}
213239

240+
function setThresholdRange(this: _This, range: Vector2) {
241+
thresholdRange.value = range;
242+
}
243+
214244
function serialize(state: StateFile) {
215245
const { paint } = state.manifest.tools;
216246

@@ -243,6 +273,7 @@ export const usePaintToolStore = defineStore('paint', () => {
243273
brushSize,
244274
strokePoints,
245275
isActive,
276+
thresholdRange,
246277

247278
getWidgetFactory,
248279

@@ -254,6 +285,7 @@ export const usePaintToolStore = defineStore('paint', () => {
254285
setActiveSegment,
255286
setBrushSize,
256287
setSliceAxis,
288+
setThresholdRange,
257289
startStroke,
258290
placeStrokePoint,
259291
endStroke,

0 commit comments

Comments
 (0)