Skip to content

Commit 649acf6

Browse files
committed
feat(nifti): adaptive setDefaultVolumeVOI, update "nifti-reader-js"
1 parent 332e529 commit 649acf6

File tree

3 files changed

+159
-141
lines changed

3 files changed

+159
-141
lines changed

packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts

Lines changed: 117 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
IImageVolume,
44
VOIRange,
55
ScalingParameters,
6-
VOI,
76
} from '../../types';
87
import { loadAndCacheImage } from '../../loaders/imageLoader';
98
import * as metaData from '../../metaData';
@@ -20,7 +19,6 @@ const REQUEST_TYPE = RequestType.Prefetch;
2019
* loads the middle slice image (middle imageId) and based on its min
2120
* and max pixel values, it calculates the VOI.
2221
* Finally it sets the VOI on the volumeActor transferFunction
23-
* For nifti image which imageIds is empty, try to look for voiLut from its metadata
2422
* @param volumeActor - The volume actor
2523
* @param imageVolume - The image volume that we want to set the VOI for.
2624
* @param useNativeDataType - The image data type is native or Float32Array
@@ -30,41 +28,29 @@ async function setDefaultVolumeVOI(
3028
imageVolume: IImageVolume,
3129
useNativeDataType: boolean
3230
): Promise<void> {
33-
let voi;
34-
if (imageVolume.imageIds?.length) {
35-
// If the volume is composed of imageIds, we can apply a default VOI based
36-
// on either the metadata or the min/max of the middle slice.
37-
voi = getVOIFromMetadata(imageVolume);
31+
let voi = getVOIFromMetadata(imageVolume);
3832

39-
if (!voi) {
40-
voi = await getVOIFromMinMax(imageVolume, useNativeDataType);
41-
}
33+
if (!voi) {
34+
voi = await getVOIFromMinMax(imageVolume, useNativeDataType);
35+
}
4236

43-
if (!voi || voi.lower === undefined || voi.upper === undefined) {
44-
throw new Error(
45-
'Could not get VOI from metadata, nor from the min max of the image middle slice'
46-
);
47-
}
48-
voi = handlePreScaledVolume(imageVolume, voi);
49-
} else if (imageVolume.metadata?.voiLut?.length > 0) {
50-
// case: .nitfi, .nrrd
51-
const index = Math.floor(imageVolume.metadata?.voiLut?.length / 2);
52-
const voiLut: VOI = imageVolume.metadata.voiLut[index];
53-
voi = windowLevel.toLowHighRange(
54-
Number(voiLut.windowWidth),
55-
Number(voiLut.windowCenter)
37+
if (!voi || voi.lower === undefined || voi.upper === undefined) {
38+
throw new Error(
39+
'Could not get VOI from metadata, nor from the min max of the image middle slice'
5640
);
5741
}
58-
if (voi?.lower && voi?.upper) {
59-
const { lower, upper } = voi;
60-
if (lower === 0 && upper === 0) {
61-
return;
62-
}
63-
volumeActor
64-
.getProperty()
65-
.getRGBTransferFunction(0)
66-
.setMappingRange(lower, upper);
42+
43+
voi = handlePreScaledVolume(imageVolume, voi);
44+
const { lower, upper } = voi;
45+
46+
if (lower === 0 && upper === 0) {
47+
return;
6748
}
49+
50+
volumeActor
51+
.getProperty()
52+
.getRGBTransferFunction(0)
53+
.setMappingRange(lower, upper);
6854
}
6955

7056
function handlePreScaledVolume(imageVolume: IImageVolume, voi: VOIRange) {
@@ -92,35 +78,36 @@ function handlePreScaledVolume(imageVolume: IImageVolume, voi: VOIRange) {
9278
}
9379

9480
/**
95-
* Get the VOI from the metadata of the middle slice of the image volume. It checks
96-
* the metadata for the VOI and if it is not found, it returns null
81+
* Get the VOI from the metadata of the middle slice of the image volume or the metadata of the image volume
82+
* It checks the metadata for the VOI and if it is not found, it returns null
9783
*
9884
* @param imageVolume - The image volume that we want to get the VOI from.
9985
* @returns VOIRange with lower and upper values
10086
*/
10187
function getVOIFromMetadata(imageVolume: IImageVolume): VOIRange {
102-
const { imageIds } = imageVolume;
103-
104-
const imageIdIndex = Math.floor(imageIds.length / 2);
105-
const imageId = imageIds[imageIdIndex];
106-
107-
const voiLutModule = metaData.get('voiLutModule', imageId);
108-
109-
if (voiLutModule && voiLutModule.windowWidth && voiLutModule.windowCenter) {
110-
const { windowWidth, windowCenter } = voiLutModule;
111-
112-
const voi = {
113-
windowWidth: Array.isArray(windowWidth) ? windowWidth[0] : windowWidth,
114-
windowCenter: Array.isArray(windowCenter)
115-
? windowCenter[0]
116-
: windowCenter,
117-
};
118-
88+
const { imageIds, metadata } = imageVolume;
89+
let voi;
90+
if (imageIds.length) {
91+
const imageIdIndex = Math.floor(imageIds.length / 2);
92+
const imageId = imageIds[imageIdIndex];
93+
const voiLutModule = metaData.get('voiLutModule', imageId);
94+
if (voiLutModule && voiLutModule.windowWidth && voiLutModule.windowCenter) {
95+
const { windowWidth, windowCenter } = voiLutModule;
96+
voi = {
97+
windowWidth: Array.isArray(windowWidth) ? windowWidth[0] : windowWidth,
98+
windowCenter: Array.isArray(windowCenter)
99+
? windowCenter[0]
100+
: windowCenter,
101+
};
102+
}
103+
} else {
104+
voi = metadata?.voiLut?.[0];
105+
}
106+
if (voi) {
119107
const { lower, upper } = windowLevel.toLowHighRange(
120108
Number(voi.windowWidth),
121109
Number(voi.windowCenter)
122110
);
123-
124111
return {
125112
lower,
126113
upper,
@@ -133,90 +120,92 @@ function getVOIFromMetadata(imageVolume: IImageVolume): VOIRange {
133120
* and max pixel values, it calculates the VOI.
134121
*
135122
* @param imageVolume - The image volume that we want to get the VOI from.
123+
* @param useNativeDataType - The image data type is native or Float32Array
136124
* @returns The VOIRange with lower and upper values
137125
*/
138126
async function getVOIFromMinMax(
139127
imageVolume: IImageVolume,
140128
useNativeDataType: boolean
141129
): Promise<VOIRange> {
142130
const { imageIds } = imageVolume;
143-
const scalarData = imageVolume.getScalarData();
144-
145-
// Get the middle image from the list of imageIds
146-
const imageIdIndex = Math.floor(imageIds.length / 2);
147-
const imageId = imageVolume.imageIds[imageIdIndex];
148-
const generalSeriesModule =
149-
metaData.get('generalSeriesModule', imageId) || {};
150-
const { modality } = generalSeriesModule;
151-
const modalityLutModule = metaData.get('modalityLutModule', imageId) || {};
152-
153-
const numImages = imageIds.length;
154-
const bytesPerImage = scalarData.byteLength / numImages;
155-
const voxelsPerImage = scalarData.length / numImages;
156-
const bytePerPixel = scalarData.BYTES_PER_ELEMENT;
157-
158-
const scalingParameters: ScalingParameters = {
159-
rescaleSlope: modalityLutModule.rescaleSlope,
160-
rescaleIntercept: modalityLutModule.rescaleIntercept,
161-
modality,
162-
};
163-
164-
let scalingParametersToUse;
165-
if (modality === 'PT') {
166-
const suvFactor = metaData.get('scalingModule', imageId);
167-
168-
if (suvFactor) {
169-
scalingParametersToUse = {
170-
...scalingParameters,
171-
suvbw: suvFactor.suvbw,
172-
};
131+
const numImages = imageIds?.length || imageVolume.dimensions[2];
132+
let image;
133+
if (imageIds?.length) {
134+
// Get index of the middle image
135+
const imageIdIndex = Math.floor(numImages / 2);
136+
const imageId = imageVolume.imageIds[imageIdIndex];
137+
const generalSeriesModule =
138+
metaData.get('generalSeriesModule', imageId) || {};
139+
const { modality } = generalSeriesModule;
140+
const modalityLutModule = metaData.get('modalityLutModule', imageId) || {};
141+
const scalingParameters: ScalingParameters = {
142+
rescaleSlope: modalityLutModule.rescaleSlope,
143+
rescaleIntercept: modalityLutModule.rescaleIntercept,
144+
modality,
145+
};
146+
let scalingParametersToUse;
147+
if (modality === 'PT') {
148+
const suvFactor = metaData.get('scalingModule', imageId);
149+
if (suvFactor) {
150+
scalingParametersToUse = {
151+
...scalingParameters,
152+
suvbw: suvFactor.suvbw,
153+
};
154+
}
155+
}
156+
const options = {
157+
targetBuffer: {
158+
type: useNativeDataType ? undefined : 'Float32Array',
159+
},
160+
priority: PRIORITY,
161+
requestType: REQUEST_TYPE,
162+
useNativeDataType,
163+
preScale: {
164+
enabled: true,
165+
scalingParameters: scalingParametersToUse,
166+
},
167+
};
168+
// Loading the middle slice image for a volume has two scenarios, the first one is that
169+
// uses the same volumeLoader which might not resolve to an image (since for performance
170+
// reasons volumes' pixelData is set via offset and length on the volume arrayBuffer
171+
// when each slice is loaded). The second scenario is that the image might not reach
172+
// to the volumeLoader, and an already cached image (with Image object) is used
173+
// instead. For the first scenario, we use the arrayBuffer of the volume to get the correct
174+
// slice for the imageScalarData, and for the second scenario we use the getPixelData
175+
// on the Cornerstone IImage object to get the pixel data.
176+
// Note: we don't want to use the derived or generated images for setting the
177+
// default VOI, because they are not the original. This is ugly but don't
178+
// know how to do it better.
179+
image = cache.getImage(imageId);
180+
if (!imageVolume.referencedImageIds?.length) {
181+
// we should ignore the cache here,
182+
// since we want to load the image from with the most
183+
// recent prescale settings
184+
image = await loadAndCacheImage(imageId, {
185+
...options,
186+
ignoreCache: true,
187+
});
173188
}
174189
}
175-
176-
const byteOffset = imageIdIndex * bytesPerImage;
177-
178-
const options = {
179-
targetBuffer: {
180-
type: useNativeDataType ? undefined : 'Float32Array',
181-
},
182-
priority: PRIORITY,
183-
requestType: REQUEST_TYPE,
184-
useNativeDataType,
185-
preScale: {
186-
enabled: true,
187-
scalingParameters: scalingParametersToUse,
188-
},
189-
};
190-
191-
// Loading the middle slice image for a volume has two scenarios, the first one is that
192-
// uses the same volumeLoader which might not resolve to an image (since for performance
193-
// reasons volumes' pixelData is set via offset and length on the volume arrayBuffer
194-
// when each slice is loaded). The second scenario is that the image might not reach
195-
// to the volumeLoader, and an already cached image (with Image object) is used
196-
// instead. For the first scenario, we use the arrayBuffer of the volume to get the correct
197-
// slice for the imageScalarData, and for the second scenario we use the getPixelData
198-
// on the Cornerstone IImage object to get the pixel data.
199-
// Note: we don't want to use the derived or generated images for setting the
200-
// default VOI, because they are not the original. This is ugly but don't
201-
// know how to do it better.
202-
let image = cache.getImage(imageId);
203-
204-
if (!imageVolume.referencedImageIds?.length) {
205-
// we should ignore the cache here,
206-
// since we want to load the image from with the most
207-
// recent prescale settings
208-
image = await loadAndCacheImage(imageId, { ...options, ignoreCache: true });
190+
let imageScalarData;
191+
if (image) {
192+
imageScalarData = image.getPixelData();
193+
} else {
194+
// If image data is missing such as .nifti and .nrrd image
195+
// calculate offset of the middle slice
196+
const scalarData = imageVolume.getScalarData();
197+
const imageIdIndex = Math.floor(numImages / 2);
198+
const bytesPerImage = scalarData.byteLength / numImages;
199+
const voxelsPerImage = scalarData.length / numImages;
200+
const bytePerPixel = scalarData.BYTES_PER_ELEMENT;
201+
const byteOffset = imageIdIndex * bytesPerImage;
202+
imageScalarData = _getImageScalarDataFromImageVolume(
203+
imageVolume,
204+
byteOffset,
205+
bytePerPixel,
206+
voxelsPerImage
207+
);
209208
}
210-
211-
const imageScalarData = image
212-
? image.getPixelData()
213-
: _getImageScalarDataFromImageVolume(
214-
imageVolume,
215-
byteOffset,
216-
bytePerPixel,
217-
voxelsPerImage
218-
);
219-
220209
// Get the min and max pixel values of the middle slice
221210
const { min, max } = getMinMax(imageScalarData);
222211

@@ -233,19 +222,15 @@ function _getImageScalarDataFromImageVolume(
233222
voxelsPerImage
234223
) {
235224
const { scalarData } = imageVolume;
236-
const { volumeBuffer } = scalarData;
225+
const { buffer } = scalarData;
237226
if (scalarData.BYTES_PER_ELEMENT !== bytePerPixel) {
238227
byteOffset *= scalarData.BYTES_PER_ELEMENT / bytePerPixel;
239228
}
240229

241230
const TypedArray = scalarData.constructor;
242231
const imageScalarData = new TypedArray(voxelsPerImage);
243232

244-
const volumeBufferView = new TypedArray(
245-
volumeBuffer,
246-
byteOffset,
247-
voxelsPerImage
248-
);
233+
const volumeBufferView = new TypedArray(buffer, byteOffset, voxelsPerImage);
249234

250235
imageScalarData.set(volumeBufferView);
251236

packages/nifti-volume-loader/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
},
3131
"dependencies": {
3232
"@cornerstonejs/core": "^1.70.10",
33-
"nifti-reader-js": "^0.6.6"
33+
"nifti-reader-js": "^0.6.8"
3434
},
3535
"contributors": [
3636
{

0 commit comments

Comments
 (0)