Skip to content

Commit 613bf75

Browse files
authored
Merge pull request Kitware#715 from PaulHax/probe
feat: add scalar probe tool
2 parents c8e52d9 + 3339556 commit 613bf75

File tree

12 files changed

+292
-23
lines changed

12 files changed

+292
-23
lines changed

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"@aws-sdk/client-s3": "^3.435.0",
2929
"@itk-wasm/dicom": "7.2.2",
3030
"@itk-wasm/image-io": "^1.3.0",
31-
"@kitware/vtk.js": "^32.9.1",
31+
"@kitware/vtk.js": "^32.12.1",
3232
"@netlify/edge-functions": "^2.0.0",
3333
"@sentry/vue": "^7.54.0",
3434
"@velipso/polybool": "^2.0.11",

src/components/ImageDataBrowser.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ export default defineComponent({
293293
<v-icon v-if="!image.layerable" class="mr-1">
294294
mdi-alert
295295
</v-icon>
296-
Convert to Segment Group
296+
Add as Segment Group
297297
<v-tooltip
298298
activator="parent"
299299
location="end"

src/components/ModulePanel.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
</v-window-item>
3636
</v-window>
3737
</div>
38+
<ProbeView />
3839
</div>
3940
</template>
4041

@@ -46,6 +47,7 @@ import DataBrowser from './DataBrowser.vue';
4647
import RenderingModule from './RenderingModule.vue';
4748
import AnnotationsModule from './AnnotationsModule.vue';
4849
import ServerModule from './ServerModule.vue';
50+
import ProbeView from './ProbeView.vue';
4951
import { useToolStore } from '../store/tools';
5052
import { Tools } from '../store/tools/types';
5153
@@ -88,6 +90,7 @@ const autoSwitchToAnnotationsTools = [
8890
8991
export default defineComponent({
9092
name: 'ModulePanel',
93+
components: { ProbeView },
9194
setup() {
9295
const selectedModuleIndex = ref(0);
9396

src/components/ProbeView.vue

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue';
3+
import { storeToRefs } from 'pinia';
4+
import { useProbeStore } from '@/src/store/probe';
5+
import { shortenNumber } from '@/src/utils';
6+
7+
const probeStore = useProbeStore();
8+
const { probeData } = storeToRefs(probeStore);
9+
10+
const formattedProbeItems = computed(() => {
11+
if (!probeData.value) return [];
12+
const sampleItems = probeData.value.samples.map((sample) => ({
13+
label: sample.name,
14+
value: sample.displayValues
15+
.map((item) => (typeof item === 'number' ? shortenNumber(item) : item))
16+
.join(', '),
17+
}));
18+
19+
// Add additional item for Position
20+
const positionItem = {
21+
label: 'Position',
22+
value: Array.from(probeData.value.pos).map(Math.round).join(', '),
23+
};
24+
25+
return [...sampleItems, positionItem];
26+
});
27+
</script>
28+
29+
<template>
30+
<v-card v-if="probeData" class="probe-value-display">
31+
<v-card-text>
32+
<div
33+
v-for="(item, index) in formattedProbeItems"
34+
:key="index"
35+
class="d-flex"
36+
style="max-width: 100%"
37+
>
38+
<span
39+
class="text-left text-truncate mr-2 flex-grow-0 flex-shrink-1"
40+
style="min-width: 6rem; max-width: 50%"
41+
>
42+
{{ item.label }}
43+
</span>
44+
<span
45+
class="text-right font-weight-bold text-truncate flex-grow-1 flex-shrink-1"
46+
>
47+
{{ item.value }}
48+
</span>
49+
</div>
50+
</v-card-text>
51+
</v-card>
52+
</template>
53+
54+
<style scoped>
55+
.probe-value-display {
56+
position: absolute;
57+
bottom: 0;
58+
right: 0;
59+
width: 100%;
60+
pointer-events: none;
61+
z-index: 1000;
62+
text-align: right;
63+
}
64+
</style>

src/components/SegmentGroupControls.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ function deleteSelected() {
251251
>
252252
{{ item.name }}
253253
<v-tooltip activator="parent" location="end" max-width="200px">
254-
Convert to segment group
254+
Add as segment group
255255
</v-tooltip>
256256
</v-list-item>
257257
</v-list>

src/components/SliceViewer.vue

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
:image-id="currentImageID"
8686
></slice-viewer-overlay>
8787
<vtk-base-slice-representation
88+
ref="baseSliceRep"
8889
:view-id="id"
8990
:image-id="currentImageID"
9091
:axis="viewAxis"
@@ -95,11 +96,13 @@
9596
:view-id="id"
9697
:segmentation-id="segId"
9798
:axis="viewAxis"
99+
ref="segSliceReps"
98100
></vtk-segmentation-slice-representation>
99101
<template v-if="currentImageID">
100102
<vtk-layer-slice-representation
101103
v-for="layer in currentLayers"
102104
:key="`layer-${layer.id}`"
105+
ref="layerSliceReps"
103106
:view-id="id"
104107
:layer-id="layer.id"
105108
:parent-id="currentImageID"
@@ -136,6 +139,11 @@
136139
<svg class="overlay-no-events">
137140
<bounding-rectangle :points="selectionPoints" />
138141
</svg>
142+
<scalar-probe
143+
:base-rep="baseSliceRep"
144+
:layer-reps="layerSliceReps"
145+
:segment-groups-reps="segSliceReps"
146+
></scalar-probe>
139147
<slot></slot>
140148
</vtk-slice-view>
141149
</div>
@@ -173,6 +181,7 @@ import PolygonTool from '@/src/components/tools/polygon/PolygonTool.vue';
173181
import RulerTool from '@/src/components/tools/ruler/RulerTool.vue';
174182
import RectangleTool from '@/src/components/tools/rectangle/RectangleTool.vue';
175183
import SelectTool from '@/src/components/tools/SelectTool.vue';
184+
import ScalarProbe from '@/src/components/tools/ScalarProbe.vue';
176185
import BoundingRectangle from '@/src/components/tools/BoundingRectangle.vue';
177186
import SliceSlider from '@/src/components/SliceSlider.vue';
178187
import SliceViewerOverlay from '@/src/components/SliceViewerOverlay.vue';
@@ -197,6 +206,9 @@ interface Props extends LayoutViewProps {
197206
}
198207
199208
const vtkView = ref<VtkViewApi>();
209+
const baseSliceRep = ref();
210+
const layerSliceReps = ref([]);
211+
const segSliceReps = ref([]);
200212
201213
const props = defineProps<Props>();
202214
@@ -238,7 +250,6 @@ whenever(
238250
}
239251
);
240252
241-
// segmentations
242253
const segmentations = computed(() => {
243254
if (!currentImageID.value) return [];
244255
const store = useSegmentGroupStore();
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<script setup lang="ts">
2+
import { inject, watch, computed, toRefs } from 'vue';
3+
import type { ReadonlyVec3 } from 'gl-matrix';
4+
import { onVTKEvent } from '@/src/composables/onVTKEvent';
5+
import { VtkViewContext } from '@/src/components/vtk/context';
6+
import { useCurrentImage } from '@/src/composables/useCurrentImage';
7+
import vtkPointPicker from '@kitware/vtk.js/Rendering/Core/PointPicker';
8+
import { useSliceRepresentation } from '@/src/core/vtk/useSliceRepresentation';
9+
import { useImageStore } from '@/src/store/datasets-images';
10+
import { useLayersStore } from '@/src/store/datasets-layers';
11+
import { useSegmentGroupStore } from '@/src/store/segmentGroups';
12+
import { useProbeStore } from '@/src/store/probe';
13+
14+
type SliceRepresentationType = ReturnType<typeof useSliceRepresentation>;
15+
16+
const props = defineProps<{
17+
baseRep: SliceRepresentationType;
18+
layerReps: SliceRepresentationType[];
19+
segmentGroupsReps: SliceRepresentationType[];
20+
}>();
21+
22+
const { baseRep, layerReps, segmentGroupsReps } = toRefs(props);
23+
const view = inject(VtkViewContext);
24+
if (!view) throw new Error('No VtkView');
25+
26+
const {
27+
currentImageID,
28+
currentImageData,
29+
currentImageMetadata,
30+
currentLayers,
31+
} = useCurrentImage();
32+
const imageStore = useImageStore();
33+
const layersStore = useLayersStore();
34+
const segmentGroupStore = useSegmentGroupStore();
35+
const probeStore = useProbeStore();
36+
37+
// Helper functions to build a unified sample set
38+
const getBaseSlice = () => {
39+
if (!currentImageData.value || !currentImageID.value) return null;
40+
return {
41+
type: 'layer',
42+
id: currentImageID.value,
43+
name: currentImageMetadata.value.name,
44+
rep: baseRep.value,
45+
image: currentImageData.value,
46+
};
47+
};
48+
49+
const getLayers = () =>
50+
layerReps.value
51+
.map((rep, index) => {
52+
const layer = currentLayers.value[index];
53+
if (!layer) return null;
54+
return {
55+
type: 'layer',
56+
id: layer.id,
57+
name: imageStore.metadata[layer.selection].name,
58+
rep,
59+
image: layersStore.layerImages[layer.id],
60+
};
61+
})
62+
.filter(Boolean);
63+
64+
const getSegments = () => {
65+
if (!currentImageID.value) return [];
66+
const parentGroups = segmentGroupStore.orderByParent[currentImageID.value];
67+
if (!parentGroups) return [];
68+
return segmentGroupsReps.value
69+
.map((rep, index) => {
70+
const groupId = parentGroups[index];
71+
if (!groupId) return null;
72+
const meta = segmentGroupStore.metadataByID[groupId];
73+
return {
74+
type: 'segmentGroup',
75+
id: groupId,
76+
name: meta.name,
77+
rep,
78+
segments: meta.segments,
79+
image: segmentGroupStore.dataIndex[groupId],
80+
};
81+
})
82+
.filter(Boolean);
83+
};
84+
85+
const sampleSet = computed(() => {
86+
const base = getBaseSlice();
87+
if (!base) return [];
88+
return [...getSegments(), ...getLayers(), base];
89+
});
90+
91+
const pointPicker = vtkPointPicker.newInstance();
92+
pointPicker.setPickFromList(true);
93+
94+
watch(
95+
sampleSet,
96+
(samples) => {
97+
pointPicker.setPickList(
98+
samples.length > 0 && samples[0] ? [samples[0].rep.actor] : []
99+
);
100+
},
101+
{ immediate: true }
102+
);
103+
104+
const getImageSamples = (x: number, y: number) => {
105+
const firstToSample = sampleSet.value[0];
106+
if (!firstToSample) return undefined;
107+
108+
pointPicker.pick([x, y, 1.0], view.renderer);
109+
if (pointPicker.getActors().length === 0) return undefined;
110+
111+
const ijk = pointPicker.getPointIJK() as unknown as ReadonlyVec3;
112+
const samples = sampleSet.value.map((item: any) => {
113+
const dims = item.image.getDimensions();
114+
const scalarData = item.image.getPointData().getScalars();
115+
const index = dims[0] * dims[1] * ijk[2] + dims[0] * ijk[1] + ijk[0];
116+
const scalars = scalarData.getTuple(index) as number[];
117+
const baseInfo = { id: item.id, name: item.name };
118+
119+
if (item.type === 'segmentGroup') {
120+
return {
121+
...baseInfo,
122+
displayValues: scalars.map(
123+
(v) => item.segments.byValue[v]?.name || 'Background'
124+
),
125+
};
126+
}
127+
return { ...baseInfo, displayValues: scalars };
128+
});
129+
130+
const position = firstToSample.image.indexToWorld(ijk);
131+
132+
return {
133+
pos: position,
134+
samples,
135+
};
136+
};
137+
138+
onVTKEvent(view.interactor, 'onMouseMove', (event: any) => {
139+
const samples = getImageSamples(event.position.x, event.position.y);
140+
probeStore.updateProbeData(samples);
141+
});
142+
143+
onVTKEvent(view.interactor, 'onPointerLeave', () => {
144+
probeStore.clearProbeData();
145+
});
146+
147+
watch([currentImageID, sampleSet], () => {
148+
probeStore.clearProbeData();
149+
});
150+
</script>

src/store/datasets-images.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type { Bounds } from '@kitware/vtk.js/types';
55

66
import { useIdStore } from '@/src/store/id';
77
import { defaultLPSDirections, getLPSDirections } from '../utils/lps';
8-
import { removeFromArray } from '../utils';
98
import { StateFile, DatasetType } from '../io/state-file/schema';
109
import { serializeData } from '../io/state-file/utils';
1110
import { useFileStore } from './datasets-files';
@@ -74,9 +73,13 @@ export const useImageStore = defineStore('images', {
7473

7574
deleteData(id: string) {
7675
if (id in this.dataIndex) {
77-
delete this.dataIndex[id];
78-
delete this.metadata[id];
79-
removeFromArray(this.idList, id);
76+
this.dataIndex = Object.fromEntries(
77+
Object.entries(this.dataIndex).filter(([key]) => key !== id)
78+
);
79+
this.metadata = Object.fromEntries(
80+
Object.entries(this.metadata).filter(([key]) => key !== id)
81+
);
82+
this.idList = this.idList.filter((i) => i !== id);
8083
}
8184
},
8285

0 commit comments

Comments
 (0)