Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nifti): NIFTI data type enhancement #1219

Merged
merged 11 commits into from
May 28, 2024
11 changes: 9 additions & 2 deletions common/reviews/api/nifti-volume-loader.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,17 @@ declare namespace helpers {
export { helpers }

// @public (undocumented)
function makeVolumeMetadata(niftiHeader: any, orientation: any, scalarData: any): Types.Metadata;
function makeVolumeMetadata(niftiHeader: any, orientation: any, scalarData: any, pixelRepresentation: any): {
volumeMetadata: Types.Metadata;
dimensions: Types.Point3;
direction: Types.Mat3;
};

// @public (undocumented)
function modalityScaleNifti(array: Float32Array | Int16Array | Uint8Array, niftiHeader: any): void;
function modalityScaleNifti(niftiHeader: any, niftiImageBuffer: any): {
scalarData: Types.PixelDataTypedArray;
pixelRepresentation: number;
};

// @public (undocumented)
export class NiftiImageVolume extends ImageVolume {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,7 @@ async function createVolumeActor(
volumeActor.getProperty().setIndependentComponents(false);
}

// If the volume is composed of imageIds, we can apply a default VOI based
// on either the metadata or the min/max of the middle slice. Example of other
// types of volumes which might not be composed of imageIds would be e.g., nrrd, nifti
// format volumes
if (imageVolume.imageIds?.length) {
await setDefaultVolumeVOI(volumeActor, imageVolume, useNativeDataType);
}
await setDefaultVolumeVOI(volumeActor, imageVolume, useNativeDataType);

if (callback) {
callback({ volumeActor, volumeId });
Expand Down
60 changes: 36 additions & 24 deletions packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
IImageVolume,
VOIRange,
ScalingParameters,
VOI,
} from '../../types';
import { loadAndCacheImage } from '../../loaders/imageLoader';
import * as metaData from '../../metaData';
Expand All @@ -19,37 +20,51 @@ const REQUEST_TYPE = RequestType.Prefetch;
* loads the middle slice image (middle imageId) and based on its min
* and max pixel values, it calculates the VOI.
* Finally it sets the VOI on the volumeActor transferFunction
* For nifti image which imageIds is empty, try to look for voiLut from its metadata
* @param volumeActor - The volume actor
* @param imageVolume - The image volume that we want to set the VOI for.
* @param useNativeDataType - The image data type is native or Float32Array
*/
async function setDefaultVolumeVOI(
volumeActor: VolumeActor,
imageVolume: IImageVolume,
useNativeDataType: boolean
): Promise<void> {
let voi = getVOIFromMetadata(imageVolume);

if (!voi) {
voi = await getVOIFromMinMax(imageVolume, useNativeDataType);
}
let voi;
if (imageVolume.imageIds?.length) {
// If the volume is composed of imageIds, we can apply a default VOI based
// on either the metadata or the min/max of the middle slice.
voi = getVOIFromMetadata(imageVolume);

if (!voi) {
voi = await getVOIFromMinMax(imageVolume, useNativeDataType);
}

if (!voi || voi.lower === undefined || voi.upper === undefined) {
throw new Error(
'Could not get VOI from metadata, nor from the min max of the image middle slice'
if (!voi || voi.lower === undefined || voi.upper === undefined) {
throw new Error(
'Could not get VOI from metadata, nor from the min max of the image middle slice'
);
}
voi = handlePreScaledVolume(imageVolume, voi);
} else if (imageVolume.metadata?.voiLut?.length > 0) {
// case: .nitfi, .nrrd
const index = Math.floor(imageVolume.metadata?.voiLut?.length / 2);
const voiLut: VOI = imageVolume.metadata.voiLut[index];
voi = windowLevel.toLowHighRange(
Number(voiLut.windowWidth),
Number(voiLut.windowCenter)
);
}

voi = handlePreScaledVolume(imageVolume, voi);
const { lower, upper } = voi;

if (lower === 0 && upper === 0) {
return;
if (voi?.lower && voi?.upper) {
const { lower, upper } = voi;
if (lower === 0 && upper === 0) {
return;
}
volumeActor
.getProperty()
.getRGBTransferFunction(0)
.setMappingRange(lower, upper);
}

volumeActor
.getProperty()
.getRGBTransferFunction(0)
.setMappingRange(lower, upper);
}

function handlePreScaledVolume(imageVolume: IImageVolume, voi: VOIRange) {
Expand Down Expand Up @@ -118,6 +133,7 @@ function getVOIFromMetadata(imageVolume: IImageVolume): VOIRange {
* and max pixel values, it calculates the VOI.
*
* @param imageVolume - The image volume that we want to get the VOI from.
* @param useNativeDataType - The image data type is native or Float32Array
* @returns The VOIRange with lower and upper values
*/
async function getVOIFromMinMax(
Expand Down Expand Up @@ -166,7 +182,6 @@ async function getVOIFromMinMax(
},
priority: PRIORITY,
requestType: REQUEST_TYPE,
useNativeDataType,
preScale: {
enabled: true,
scalingParameters: scalingParametersToUse,
Expand All @@ -187,10 +202,7 @@ async function getVOIFromMinMax(
let image = cache.getImage(imageId);

if (!imageVolume.referencedImageIds?.length) {
// we should ignore the cache here,
// since we want to load the image from with the most
// recent prescale settings
image = await loadAndCacheImage(imageId, { ...options, ignoreCache: true });
image = await loadAndCacheImage(imageId, options);
}

const imageScalarData = image
Expand Down
5 changes: 1 addition & 4 deletions packages/nifti-volume-loader/examples/niftiBasic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@ import {
RenderingEngine,
Enums,
init as csInit,
Types,
volumeLoader,
setVolumesForViewports,
} from '@cornerstonejs/core';
import { init as csTools3dInit } from '@cornerstonejs/tools';
import { cornerstoneNiftiImageVolumeLoader } from '@cornerstonejs/nifti-volume-loader';

import { setCtTransferFunctionForVolumeActor } from '../../../../utils/demo/helpers';

// This is for debugging purposes
console.warn(
'Click on index.ts to open source code for this example --------->'
Expand Down Expand Up @@ -90,7 +87,7 @@ async function setup() {

setVolumesForViewports(
renderingEngine,
[{ volumeId, callback: setCtTransferFunctionForVolumeActor }],
[{ volumeId }],
viewportInputArray.map((v) => v.viewportId)
);

Expand Down
7 changes: 2 additions & 5 deletions packages/nifti-volume-loader/examples/niftiWithTools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ import {
Enums as NiftiEnums,
} from '@cornerstonejs/nifti-volume-loader';

import {
addDropdownToToolbar,
setCtTransferFunctionForVolumeActor,
} from '../../../../utils/demo/helpers';
import { addDropdownToToolbar } from '../../../../utils/demo/helpers';

const {
LengthTool,
Expand Down Expand Up @@ -276,7 +273,7 @@ async function setup() {

setVolumesForViewports(
renderingEngine,
[{ volumeId, callback: setCtTransferFunctionForVolumeActor }],
[{ volumeId }],
viewportInputArray.map((v) => v.viewportId)
);

Expand Down
154 changes: 13 additions & 141 deletions packages/nifti-volume-loader/src/helpers/fetchAndAllocateNiftiVolume.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
import * as NiftiReader from 'nifti-reader-js';
import {
cache,
utilities,
Enums,
eventTarget,
triggerEvent,
getShouldUseSharedArrayBuffer,
} from '@cornerstonejs/core';
import type { Types } from '@cornerstonejs/core';
import { vec3 } from 'gl-matrix';
import makeVolumeMetadata from './makeVolumeMetadata';
import { eventTarget, triggerEvent } from '@cornerstonejs/core';
import NiftiImageVolume from '../NiftiImageVolume';
import * as NIFTICONSTANTS from './niftiConstants';
import { invertDataPerFrame, rasToLps } from './convert';
import modalityScaleNifti from './modalityScaleNifti';
import { rasToLps } from './convert';
import Events from '../enums/Events';
import { NIFTI_LOADER_SCHEME } from '../constants';

const { createUint8SharedArray, createFloat32SharedArray } = utilities;
import makeVolumeMetadata from './makeVolumeMetadata';
import modalityScaleNifti from './modalityScaleNifti';

export const urlsMap = new Map();

Expand Down Expand Up @@ -68,19 +56,6 @@ function fetchArrayBuffer(url, onProgress, signal, onload) {
});
}

export const getTypedNiftiArray = (datatypeCode, niftiImageBuffer) => {
switch (datatypeCode) {
case NIFTICONSTANTS.NIFTI_TYPE_UINT8:
return new Uint8Array(niftiImageBuffer);
case NIFTICONSTANTS.NIFTI_TYPE_FLOAT32:
return new Float32Array(niftiImageBuffer);
case NIFTICONSTANTS.NIFTI_TYPE_INT16:
return new Int16Array(niftiImageBuffer);
default:
throw new Error(`datatypeCode ${datatypeCode} is not yet supported`);
}
};

export default async function fetchAndAllocateNiftiVolume(
volumeId: string
): Promise<NiftiImageVolume> {
Expand Down Expand Up @@ -127,122 +102,21 @@ export default async function fetchAndAllocateNiftiVolume(
niftiImage = NiftiReader.readImage(niftiHeader, niftiBuffer);
}

const typedNiftiArray = getTypedNiftiArray(
niftiHeader.datatypeCode,
const { scalarData, pixelRepresentation } = modalityScaleNifti(
niftiHeader,
niftiImage
);

// Convert to LPS for display in OHIF.
// TODO: Comment it as no need invert data of each frame
// invertDataPerFrame(niftiHeader.dims.slice(1, 4), scalarData);

const { orientation, origin, spacing } = rasToLps(niftiHeader);
invertDataPerFrame(niftiHeader.dims.slice(1, 4), typedNiftiArray);

modalityScaleNifti(typedNiftiArray, niftiHeader);

const volumeMetadata = makeVolumeMetadata(
const { volumeMetadata, dimensions, direction } = makeVolumeMetadata(
niftiHeader,
orientation,
typedNiftiArray
);

const scanAxisNormal = vec3.create();
vec3.set(scanAxisNormal, orientation[6], orientation[7], orientation[8]);

const {
BitsAllocated,
PixelRepresentation,
PhotometricInterpretation,
ImageOrientationPatient,
Columns,
Rows,
} = volumeMetadata;

const rowCosineVec = vec3.fromValues(
ImageOrientationPatient[0],
ImageOrientationPatient[1],
ImageOrientationPatient[2]
scalarData,
pixelRepresentation
);
const colCosineVec = vec3.fromValues(
ImageOrientationPatient[3],
ImageOrientationPatient[4],
ImageOrientationPatient[5]
);

const { dims } = niftiHeader;

const numFrames = dims[3];

// Spacing goes [1] then [0], as [1] is column spacing (x) and [0] is row spacing (y)
const dimensions = <Types.Point3>[Columns, Rows, numFrames];
const direction = new Float32Array([
rowCosineVec[0],
rowCosineVec[1],
rowCosineVec[2],
colCosineVec[0],
colCosineVec[1],
colCosineVec[2],
scanAxisNormal[0],
scanAxisNormal[1],
scanAxisNormal[2],
]) as Types.Mat3;
const signed = PixelRepresentation === 1;

// Check if it fits in the cache before we allocate data
// TODO Improve this when we have support for more types
// NOTE: We use 4 bytes per voxel as we are using Float32.
let bytesPerVoxel = 1;
if (BitsAllocated === 16 || BitsAllocated === 32) {
bytesPerVoxel = 4;
}
const sizeInBytesPerComponent =
bytesPerVoxel * dimensions[0] * dimensions[1] * dimensions[2];

let numComponents = 1;
if (PhotometricInterpretation === 'RGB') {
numComponents = 3;
}

const sizeInBytes = sizeInBytesPerComponent * numComponents;

// check if there is enough space in unallocated + image Cache
const isCacheable = cache.isCacheable(sizeInBytes);
if (!isCacheable) {
throw new Error(Enums.Events.CACHE_SIZE_EXCEEDED);
}

cache.decacheIfNecessaryUntilBytesAvailable(sizeInBytes);

let scalarData;
const useSharedArrayBuffer = getShouldUseSharedArrayBuffer();
const buffer_size = dimensions[0] * dimensions[1] * dimensions[2];

switch (BitsAllocated) {
case 8:
if (signed) {
throw new Error(
'8 Bit signed images are not yet supported by this plugin.'
);
} else {
scalarData = useSharedArrayBuffer
? createUint8SharedArray(buffer_size)
: new Uint8Array(buffer_size);
}

break;

case 16:
case 32:
scalarData = useSharedArrayBuffer
? createFloat32SharedArray(buffer_size)
: new Float32Array(buffer_size);

break;
}

// Set the scalar data from the nifti typed view into the SAB
scalarData.set(typedNiftiArray);

const niftiImageVolume = new NiftiImageVolume(
return new NiftiImageVolume(
// ImageVolume properties
{
volumeId,
Expand All @@ -252,7 +126,7 @@ export default async function fetchAndAllocateNiftiVolume(
origin,
direction,
scalarData,
sizeInBytes,
sizeInBytes: scalarData.byteLength,
imageIds: [],
},
// Streaming properties
Expand All @@ -265,6 +139,4 @@ export default async function fetchAndAllocateNiftiVolume(
controller,
}
);

return niftiImageVolume;
}
Loading