Skip to content

Commit d8b28d7

Browse files
committed
fix(segmentation): handle labelmaps with different orientations
Add coordinate transformation utilities and fix tools to use labelmap's own coordinate system instead of assuming it matches the parent image. Fixes paint, polygon rasterization, slice rendering, and scalar probe when segment groups have different direction matrices than the base image.
1 parent 97f32b0 commit d8b28d7

6 files changed

Lines changed: 179 additions & 60 deletions

File tree

src/components/tools/ScalarProbe.vue

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
<script setup lang="ts">
22
import { inject, watch, computed, toRefs } from 'vue';
33
import type { ReadonlyVec3 } from 'gl-matrix';
4+
import { vec3 } from 'gl-matrix';
45
import { onVTKEvent } from '@/src/composables/onVTKEvent';
6+
import { worldPointToIndex } from '@/src/utils/imageSpace';
57
import { VtkViewContext } from '@/src/components/vtk/context';
68
import { useCurrentImage } from '@/src/composables/useCurrentImage';
79
import vtkPointPicker from '@kitware/vtk.js/Rendering/Core/PointPicker';
@@ -110,11 +112,36 @@ const getImageSamples = (x: number, y: number) => {
110112
pointPicker.pick([x, y, 1.0], view.renderer);
111113
if (pointPicker.getActors().length === 0) return undefined;
112114
113-
const ijk = pointPicker.getPointIJK() as unknown as ReadonlyVec3;
115+
// Get world position from the picked point
116+
const pickedIjk = pointPicker.getPointIJK() as unknown as ReadonlyVec3;
117+
const worldPosition = vec3.clone(
118+
firstToSample.image.indexToWorld(pickedIjk) as vec3
119+
);
120+
114121
const samples = sampleSet.value.map((item: any) => {
122+
// Convert world position to this specific image's IJK
123+
const itemIjk = worldPointToIndex(item.image, worldPosition);
115124
const dims = item.image.getDimensions();
116125
const scalarData = item.image.getPointData().getScalars();
117-
const index = dims[0] * dims[1] * ijk[2] + dims[0] * ijk[1] + ijk[0];
126+
127+
// Round to nearest integer indices
128+
const i = Math.round(itemIjk[0]);
129+
const j = Math.round(itemIjk[1]);
130+
const k = Math.round(itemIjk[2]);
131+
132+
// Check bounds
133+
if (
134+
i < 0 ||
135+
j < 0 ||
136+
k < 0 ||
137+
i >= dims[0] ||
138+
j >= dims[1] ||
139+
k >= dims[2]
140+
) {
141+
return null;
142+
}
143+
144+
const index = dims[0] * dims[1] * k + dims[0] * j + i;
118145
const scalars = scalarData.getTuple(index) as number[];
119146
const baseInfo = { id: item.id, name: item.name };
120147
@@ -129,11 +156,9 @@ const getImageSamples = (x: number, y: number) => {
129156
return { ...baseInfo, displayValues: scalars };
130157
});
131158
132-
const position = firstToSample.image.indexToWorld(ijk);
133-
134159
return {
135-
pos: position,
136-
samples,
160+
pos: worldPosition,
161+
samples: samples.filter((s): s is NonNullable<typeof s> => s !== null),
137162
};
138163
};
139164

src/components/tools/paint/PaintWidget2D.vue

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ import { getLPSAxisFromDir } from '@/src/utils/lps';
1616
import { useImage } from '@/src/composables/useCurrentImage';
1717
import { updatePlaneManipulatorFor2DView } from '@/src/utils/manipulators';
1818
import { usePaintToolStore } from '@/src/store/tools/paint';
19+
import { useSegmentGroupStore } from '@/src/store/segmentGroups';
1920
import { vtkPaintViewWidget } from '@/src/vtk/PaintWidget';
2021
import { LPSAxisDir } from '@/src/types/lps';
22+
import { getLPSDirections } from '@/src/utils/lps';
2123
import { onVTKEvent } from '@/src/composables/onVTKEvent';
2224
import { useSliceInfo } from '@/src/composables/useSliceInfo';
2325
import { VtkViewContext } from '@/src/components/vtk/context';
@@ -48,6 +50,7 @@ export default defineComponent({
4850
const slice = computed(() => sliceInfo.value?.slice);
4951
5052
const paintStore = usePaintToolStore();
53+
const segmentGroupStore = useSegmentGroupStore();
5154
const widgetFactory = paintStore.getWidgetFactory();
5255
const widgetState = widgetFactory.getWidgetState();
5356
@@ -57,29 +60,34 @@ export default defineComponent({
5760
() => imageMetadata.value.lpsOrientation[viewAxis.value]
5861
);
5962
60-
const cloneWorldPoint = (worldPoint: vec3) => {
61-
return vec3.clone(worldPoint);
62-
};
63+
// Get the active labelmap for coordinate transforms
64+
const activeLabelmap = computed(() => {
65+
const groupId = paintStore.activeSegmentGroupID;
66+
if (!groupId) return null;
67+
return segmentGroupStore.dataIndex[groupId] ?? null;
68+
});
6369
6470
const widget = view.widgetManager.addWidget(
6571
widgetFactory
6672
) as vtkPaintViewWidget;
6773
68-
onMounted(() => {
69-
view.widgetManager.renderWidgets();
70-
view.widgetManager.grabFocus(widget);
71-
});
72-
73-
onUnmounted(() => {
74-
view.widgetManager.removeWidget(widgetFactory);
75-
});
76-
7774
// --- widget representation config --- //
7875
7976
watchEffect(() => {
80-
const metadata = imageMetadata.value;
81-
const slicingIndex = metadata.lpsOrientation[viewAxis.value];
82-
if (widget) {
77+
if (!widget) return;
78+
79+
const labelmap = activeLabelmap.value;
80+
if (labelmap) {
81+
// Use labelmap's transforms so brush preview matches where paint appears
82+
const labelmapLps = getLPSDirections(labelmap.getDirection());
83+
const slicingIndex = labelmapLps[viewAxis.value];
84+
widget.setSlicingIndex(slicingIndex);
85+
widget.setIndexToWorld(labelmap.getIndexToWorld());
86+
widget.setWorldToIndex(labelmap.getWorldToIndex());
87+
} else {
88+
// Fall back to parent image transforms
89+
const metadata = imageMetadata.value;
90+
const slicingIndex = metadata.lpsOrientation[viewAxis.value];
8391
widget.setSlicingIndex(slicingIndex);
8492
widget.setIndexToWorld(metadata.indexToWorld);
8593
widget.setWorldToIndex(metadata.worldToIndex);
@@ -92,17 +100,19 @@ export default defineComponent({
92100
if (!imageId.value) return;
93101
paintStore.setSliceAxis(viewAxisIndex.value, imageId.value);
94102
const origin = widgetState.getBrush().getOrigin()!;
95-
const worldPoint = cloneWorldPoint(origin);
96-
paintStore.startStroke(worldPoint, viewAxisIndex.value, imageId.value);
103+
paintStore.startStroke(
104+
vec3.clone(origin),
105+
viewAxisIndex.value,
106+
imageId.value
107+
);
97108
paintStore.updatePaintPosition(origin, viewId.value);
98109
});
99110
100111
onVTKEvent(widget, 'onInteractionEvent', () => {
101112
if (!imageId.value) return;
102113
const origin = widgetState.getBrush().getOrigin()!;
103-
const worldPoint = cloneWorldPoint(origin);
104114
paintStore.placeStrokePoint(
105-
worldPoint,
115+
vec3.clone(origin),
106116
viewAxisIndex.value,
107117
imageId.value
108118
);
@@ -111,8 +121,11 @@ export default defineComponent({
111121
112122
onVTKEvent(widget, 'onEndInteractionEvent', () => {
113123
if (!imageId.value) return;
114-
const worldPoint = cloneWorldPoint(widgetState.getBrush().getOrigin()!);
115-
paintStore.endStroke(worldPoint, viewAxisIndex.value, imageId.value);
124+
paintStore.endStroke(
125+
vec3.clone(widgetState.getBrush().getOrigin()!),
126+
viewAxisIndex.value,
127+
imageId.value
128+
);
116129
});
117130
118131
// --- manipulator --- //
@@ -134,19 +147,10 @@ export default defineComponent({
134147
135148
let checkIfPointerInView = false;
136149
137-
onMounted(() => {
138-
widget.setVisibility(false);
139-
checkIfPointerInView = true;
140-
});
141-
142-
// Turn on widget visibility and update stencil
143-
// if mouse starts within view
150+
// Turn on widget visibility and update stencil if mouse starts within view
144151
onVTKEvent(view.interactor, 'onMouseMove', () => {
145-
if (!checkIfPointerInView) {
146-
return;
147-
}
152+
if (!checkIfPointerInView) return;
148153
checkIfPointerInView = false;
149-
150154
widget.setVisibility(true);
151155
if (imageId.value) {
152156
paintStore.setSliceAxis(viewAxisIndex.value, imageId.value);
@@ -183,12 +187,17 @@ export default defineComponent({
183187
};
184188
185189
onMounted(() => {
190+
view.widgetManager.renderWidgets();
191+
view.widgetManager.grabFocus(widget);
192+
widget.setVisibility(false);
193+
checkIfPointerInView = true;
186194
view.renderWindowView
187195
.getContainer()
188196
?.addEventListener('wheel', handleWheelEvent, { passive: false });
189197
});
190198
191199
onUnmounted(() => {
200+
view.widgetManager.removeWidget(widgetFactory);
192201
view.renderWindowView
193202
.getContainer()
194203
?.removeEventListener('wheel', handleWheelEvent);

src/components/tools/polygon/PolygonTool.vue

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ import type { IGrid2D } from '@thi.ng/api';
110110
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
111111
import type { Vector2, Vector3 } from '@kitware/vtk.js/types';
112112
import { containsPoint } from '@kitware/vtk.js/Common/DataModel/BoundingBox';
113+
import { convertSliceIndex } from '@/src/utils/imageSpace';
114+
import { getLPSDirections } from '@/src/utils/lps';
113115
import { type ToolID } from '@/src/types/annotation-tool';
114116
import PolygonWidget2D from '@/src/components/tools/polygon/PolygonWidget2D.vue';
115117
import { usePaintToolStore } from '@/src/store/tools/paint';
@@ -177,7 +179,6 @@ export default defineComponent({
177179
178180
const sliceInfo = useSliceInfo(viewId, imageId);
179181
const slice = computed(() => sliceInfo.value?.slice ?? 0);
180-
const sliceAxis = computed(() => sliceInfo.value?.axisIndex ?? 0);
181182
182183
const { metadata: imageMetadata } = useImage(imageId);
183184
const isToolActive = computed(() => toolStore.currentTool === toolType);
@@ -279,25 +280,41 @@ export default defineComponent({
279280
paintStore.setActiveSegment(segment.value);
280281
}
281282
282-
const image = segmentGroupStore.dataIndex[segmentGroupID];
283-
if (!image) {
283+
const labelmap = segmentGroupStore.dataIndex[segmentGroupID];
284+
if (!labelmap) {
284285
throw new Error(
285286
`Failed to get labelmap for segment group ${segmentGroupID}`
286287
);
287288
}
288289
290+
// Convert parent slice index to labelmap slice index
291+
const parentMeta = imageMetadata.value;
292+
const labelmapSlice = convertSliceIndex(
293+
slice.value,
294+
parentMeta.lpsOrientation,
295+
parentMeta.indexToWorld,
296+
labelmap,
297+
viewAxis.value
298+
);
299+
289300
const points = activeToolStore.getPoints(toolId);
290-
const axis = sliceAxis.value;
301+
const labelmapIjkIndex = getLPSDirections(labelmap.getDirection())[
302+
viewAxis.value
303+
];
291304
292305
const indexSpacePoints2D = points.map((pt) => {
293-
const output = [...image.worldToIndex(pt)];
294-
output.splice(axis, 1);
306+
const output = [...labelmap.worldToIndex(pt)];
307+
output.splice(labelmapIjkIndex, 1);
295308
return output as Vector2;
296309
});
297310
298-
const grid = createGridAccessor(image, slice.value, axis);
311+
const grid = createGridAccessor(
312+
labelmap,
313+
labelmapSlice,
314+
labelmapIjkIndex
315+
);
299316
fillPoly(grid, indexSpacePoints2D, segment.value);
300-
image.modified();
317+
labelmap.modified();
301318
}
302319
303320
return {

src/components/vtk/VtkSegmentationSliceRepresentation.vue

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import { InterpolationType } from '@kitware/vtk.js/Rendering/Core/ImageProperty/
1414
import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction';
1515
import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction';
1616
import { vtkFieldRef } from '@/src/core/vtk/vtkFieldRef';
17-
import { syncRef } from '@vueuse/core';
17+
import { watchImmediate } from '@vueuse/core';
18+
import { convertSliceIndex } from '@/src/utils/imageSpace';
19+
import { getLPSDirections } from '@/src/utils/lps';
1820
import { useSliceConfig } from '@/src/composables/useSliceConfig';
1921
import useLayerColoringStore from '@/src/store/view-configs/layers';
2022
import { useSegmentGroupConfigStore } from '@/src/store/view-configs/segmentGroups';
@@ -86,17 +88,42 @@ watchEffect(() => {
8688
const parentImageId = computed(() => metadata.value?.parentImage);
8789
const { metadata: parentMetadata } = useImage(parentImageId);
8890
91+
// Compute labelmap's LPS orientation from its direction matrix
92+
const labelmapLpsOrientation = computed(() => {
93+
const labelmap = imageData.value;
94+
if (!labelmap) return null;
95+
return getLPSDirections(labelmap.getDirection());
96+
});
97+
98+
// Set slicing mode based on labelmap's own orientation
8999
watchEffect(() => {
90-
const { lpsOrientation } = parentMetadata.value;
100+
const lpsOrientation = labelmapLpsOrientation.value;
101+
if (!lpsOrientation) return;
91102
const ijkIndex = lpsOrientation[axis.value];
92103
const mode = [SlicingMode.I, SlicingMode.J, SlicingMode.K][ijkIndex];
93104
sliceRep.mapper.setSlicingMode(mode);
94105
});
95106
96-
// sync slicing
107+
// sync slicing - convert parent slice to labelmap slice via world coordinates
97108
const slice = vtkFieldRef(sliceRep.mapper, 'slice');
98109
const { slice: storedSlice } = useSliceConfig(viewId, parentImageId);
99-
syncRef(storedSlice, slice, { immediate: true });
110+
111+
watchImmediate(
112+
[storedSlice, labelmapLpsOrientation, () => parentMetadata.value],
113+
() => {
114+
const parentImage = parentMetadata.value;
115+
const labelmap = imageData.value;
116+
if (!parentImage || !labelmap || storedSlice.value == null) return;
117+
118+
slice.value = convertSliceIndex(
119+
storedSlice.value,
120+
parentImage.lpsOrientation,
121+
parentImage.indexToWorld,
122+
labelmap,
123+
axis.value
124+
);
125+
}
126+
);
100127
101128
// set coloring properties
102129
const applySegmentColoring = () => {

src/store/tools/paint.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { defineStore } from 'pinia';
1010
import { PaintMode } from '@/src/core/tools/paint';
1111
import { getLPSAxisFromDir } from '@/src/utils/lps';
1212
import { get2DViewingVectors } from '@/src/utils/getViewingVectors';
13+
import { worldPointToIndex } from '@/src/utils/imageSpace';
1314
import { Tools } from './types';
1415
import { useSegmentGroupStore } from '../segmentGroups';
1516
import useViewSliceStore from '../view-configs/slicing';
@@ -196,20 +197,13 @@ export const usePaintToolStore = defineStore('paint', () => {
196197

197198
const lastIndex = strokePoints.value.length - 1;
198199
if (lastIndex >= 0) {
199-
const labelmapWorldToIndex = labelmap.getWorldToIndex();
200-
const worldToLabelmapIndex = (worldPoint: vec3) => {
201-
const indexPoint = vec3.create();
202-
vec3.transformMat4(indexPoint, worldPoint, labelmapWorldToIndex);
203-
return indexPoint;
204-
};
205-
206200
const lastWorldPoint = strokePoints.value[lastIndex];
207201
const prevWorldPoint =
208202
lastIndex >= 1 ? strokePoints.value[lastIndex - 1] : undefined;
209203

210-
const lastIndexPoint = worldToLabelmapIndex(lastWorldPoint);
204+
const lastIndexPoint = worldPointToIndex(labelmap, lastWorldPoint);
211205
const prevIndexPoint = prevWorldPoint
212-
? worldToLabelmapIndex(prevWorldPoint)
206+
? worldPointToIndex(labelmap, prevWorldPoint)
213207
: undefined;
214208

215209
this.$paint.paintLabelmap(

0 commit comments

Comments
 (0)