Skip to content

Commit cec8579

Browse files
authored
fix: πŸ› Correctly orient non-axial volumes (#69)
* fix: πŸ› Correctly orient non-axial volumes * Sort data if positions are mixed.
1 parent 1463664 commit cec8579

13 files changed

+310
-89
lines changed

β€Žexamples/VTKLoadImageDataExample.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ function createActorMapper(imageData) {
4444
};
4545
}
4646

47+
function shuffleImageNames(imageNames) {
48+
for (let index = imageNames.length - 1; index > 0; index--) {
49+
const randomIndex = Math.floor(Math.random() * index);
50+
const temporaryValue = imageNames[index];
51+
imageNames[index] = imageNames[randomIndex];
52+
imageNames[randomIndex] = temporaryValue;
53+
}
54+
}
55+
4756
function getImageIds() {
4857
const ROOT_URL =
4958
'https://s3.amazonaws.com/IsomicsPublic/SampleData/QIN-H%2BN-0139/PET-sorted/';
@@ -54,6 +63,9 @@ function getImageIds() {
5463
imageNames.push('PET_HeadNeck_0-' + i + '.dcm');
5564
}
5665

66+
// Shuffle the image names to test that src/lib/data/sortDatasetsByImagePosition.js works.
67+
shuffleImageNames(imageNames);
68+
5769
return imageNames.map(name => `dicomweb:${ROOT_URL}${name}`);
5870
}
5971

β€Žsrc/lib/data/getSliceIndex.js

Lines changed: 0 additions & 19 deletions
This file was deleted.

β€Žsrc/lib/data/insertSlice.js

Lines changed: 171 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,159 @@
1-
import cornerstone from 'cornerstone-core';
2-
3-
// insert the slice at the z index location.
1+
/**
2+
*
3+
* @param {Object} imageData - The vtkImageData
4+
* @param {*} sliceIndex - The index of the slice you are inserting.
5+
* @param {*} acquistionDirection - The acquistion direction of the slice.
6+
* @param {*} image The cornerstone image to pull pixel data data from.
7+
* @param {*} modality The modality of the image.
8+
* @param {*} modalitySpecificScalingParameters Specific scaling paramaters for this modality. E.g. Patient weight.
9+
*/
410
export default function insertSlice(
511
imageData,
612
sliceIndex,
13+
acquistionDirection,
714
image,
815
modality,
916
modalitySpecificScalingParameters
1017
) {
11-
const pixels = image.getPixelData();
12-
const { rows, columns } = image;
1318
const scalars = imageData.getPointData().getScalars();
1419
const scalarData = scalars.getData();
15-
const sliceLength = pixels.length;
16-
let pixelIndex = 0;
1720

1821
const scalingFunction = _getScalingFunction(
1922
modality,
2023
image,
2124
modalitySpecificScalingParameters
2225
);
2326

27+
const vtkImageDimensions = imageData.getDimensions();
28+
29+
let minAndMax;
30+
31+
switch (acquistionDirection) {
32+
case 'coronal':
33+
minAndMax = insertCoronalSlice(
34+
image,
35+
sliceIndex,
36+
vtkImageDimensions,
37+
scalarData,
38+
scalingFunction
39+
);
40+
break;
41+
case 'sagittal':
42+
minAndMax = insertSagittalSlice(
43+
image,
44+
sliceIndex,
45+
vtkImageDimensions,
46+
scalarData,
47+
scalingFunction
48+
);
49+
break;
50+
case 'axial':
51+
minAndMax = insertAxialSlice(
52+
image,
53+
sliceIndex,
54+
scalarData,
55+
scalingFunction
56+
);
57+
break;
58+
}
59+
60+
return minAndMax;
61+
}
62+
63+
/**
64+
*
65+
* @param {object} image The cornerstone image to pull pixel data data from.
66+
* @param {number} xIndex The x index of axially oriented vtk volume to put the sagital slice.
67+
* @param {number[]} vtkImageDimensions The dimensions of the axially oriented vtk volume.
68+
* @param {number[]} scalarData The data array for the axially oriented vtk volume.
69+
* @param {function} scalingFunction The modality specific scaling function.
70+
*
71+
* @returns {object} The min and max pixel values in the inserted slice.
72+
*/
73+
function insertSagittalSlice(
74+
image,
75+
xIndex,
76+
vtkImageDimensions,
77+
scalarData,
78+
scalingFunction
79+
) {
80+
const pixels = image.getPixelData();
81+
const { rows, columns } = image;
82+
83+
let pixelIndex = 0;
84+
let max = scalingFunction(pixels[pixelIndex]);
85+
let min = max;
86+
87+
const vtkImageDimensionsX = vtkImageDimensions[0];
88+
const vtkImageDimensionsY = vtkImageDimensions[1];
89+
const vtkImageDimensionsZ = vtkImageDimensions[2];
90+
91+
const axialSliceLength = vtkImageDimensionsX * vtkImageDimensionsY;
92+
93+
for (let row = 0; row < rows; row++) {
94+
for (let col = 0; col < columns; col++) {
95+
const yPos = vtkImageDimensionsY - col;
96+
const zPos = vtkImageDimensionsZ - row;
97+
98+
const destIdx =
99+
zPos * axialSliceLength + yPos * vtkImageDimensionsX + xIndex;
100+
101+
const pixel = pixels[pixelIndex];
102+
const pixelValue = scalingFunction(pixel);
103+
104+
if (pixelValue > max) {
105+
max = pixelValue;
106+
} else if (pixelValue < min) {
107+
min = pixelValue;
108+
}
109+
110+
scalarData[destIdx] = pixelValue;
111+
pixelIndex++;
112+
}
113+
}
114+
115+
return { min, max };
116+
}
117+
118+
/**
119+
*
120+
* @param {object} image The cornerstone image to pull pixel data data from.
121+
* @param {number} yIndex The y index of axially oriented vtk volume to put the coronal slice.
122+
* @param {number[]} vtkImageDimensions The dimensions of the axially oriented vtk volume.
123+
* @param {number[]} scalarData The data array for the axially oriented vtk volume.
124+
* @param {function} scalingFunction The modality specific scaling function.
125+
*
126+
* @returns {object} The min and max pixel values in the inserted slice.
127+
*/
128+
function insertCoronalSlice(
129+
image,
130+
yIndex,
131+
vtkImageDimensions,
132+
scalarData,
133+
scalingFunction
134+
) {
135+
const pixels = image.getPixelData();
136+
const { rows, columns } = image;
137+
138+
let pixelIndex = 0;
24139
let max = scalingFunction(pixels[pixelIndex]);
25140
let min = max;
26141

27-
for (let row = 0; row <= rows; row++) {
28-
for (let col = 0; col <= columns; col++) {
29-
const destIdx = pixelIndex + sliceIndex * sliceLength;
142+
const vtkImageDimensionsX = vtkImageDimensions[0];
143+
const vtkImageDimensionsY = vtkImageDimensions[1];
144+
const vtkImageDimensionsZ = vtkImageDimensions[2];
145+
146+
const axialSliceLength = vtkImageDimensionsX * vtkImageDimensionsY;
147+
148+
for (let row = 0; row < rows; row++) {
149+
for (let col = 0; col < columns; col++) {
150+
const xPos = col;
151+
const yPos = yIndex;
152+
const zPos = vtkImageDimensionsZ - row;
153+
154+
const destIdx =
155+
zPos * axialSliceLength + yPos * vtkImageDimensionsX + xPos;
156+
30157
const pixel = pixels[pixelIndex];
31158
const pixelValue = scalingFunction(pixel);
32159

@@ -44,6 +171,40 @@ export default function insertSlice(
44171
return { min, max };
45172
}
46173

174+
/**
175+
*
176+
* @param {object} image The cornerstone image to pull pixel data data from.
177+
* @param {number} zIndex The z index of axially oriented vtk volume to put the axial slice.
178+
* @param {number[]} scalarData The data array for the axially oriented vtk volume.
179+
* @param {function} scalingFunction The modality specific scaling function.
180+
*
181+
* @returns {object} The min and max pixel values in the inserted slice.
182+
*/
183+
function insertAxialSlice(image, zIndex, scalarData, scalingFunction) {
184+
const pixels = image.getPixelData();
185+
const sliceLength = pixels.length;
186+
187+
let pixelIndex = 0;
188+
let max = scalingFunction(pixels[pixelIndex]);
189+
let min = max;
190+
191+
for (let pixelIndex = 0; pixelIndex < pixels.length; pixelIndex++) {
192+
const destIdx = pixelIndex + zIndex * sliceLength;
193+
const pixel = pixels[pixelIndex];
194+
const pixelValue = scalingFunction(pixel);
195+
196+
if (pixelValue > max) {
197+
max = pixelValue;
198+
} else if (pixelValue < min) {
199+
min = pixelValue;
200+
}
201+
202+
scalarData[destIdx] = pixelValue;
203+
}
204+
205+
return { min, max };
206+
}
207+
47208
function _getScalingFunction(
48209
modality,
49210
image,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Vector3 } from 'cornerstone-math';
2+
3+
export default function sortDatasetsByImagePosition(
4+
scanAxisNormal,
5+
imageMetaDataMap
6+
) {
7+
// See https://github.com/dcmjs-org/dcmjs/blob/4849ed50db8788741c2773b3d9c75cc52441dbcb/src/normalizers.js#L167
8+
// TODO: Find a way to make this code generic?
9+
10+
const datasets = Array.from(imageMetaDataMap.values());
11+
const referenceDataset = datasets[0];
12+
13+
const refIppVec = new Vector3(...referenceDataset.imagePositionPatient);
14+
15+
const distanceDatasetPairs = datasets.map(function(dataset) {
16+
const ippVec = new Vector3(...dataset.imagePositionPatient);
17+
const positionVector = refIppVec.clone().sub(ippVec);
18+
const distance = positionVector.dot(scanAxisNormal);
19+
20+
return {
21+
distance,
22+
dataset,
23+
};
24+
});
25+
26+
distanceDatasetPairs.sort(function(a, b) {
27+
return b.distance - a.distance;
28+
});
29+
30+
const sortedDatasets = distanceDatasetPairs.map(a => a.dataset);
31+
const distances = distanceDatasetPairs.map(a => a.distance);
32+
33+
// TODO: The way we calculate spacing determines how the volume shows up if
34+
// we have missing slices.
35+
// - Should we just bail out for now if missing slices are present?
36+
//const spacing = mean(diff(distances));
37+
const spacing = Math.abs(distances[1] - distances[0]);
38+
39+
return {
40+
spacing,
41+
origin: distanceDatasetPairs[0].dataset.imagePositionPatient,
42+
sortedDatasets,
43+
};
44+
}

0 commit comments

Comments
Β (0)