@cornerstonejs/nifti-image-loader
Version:
Cornerstone ImageLoader for NIfTI
265 lines (237 loc) • 8.56 kB
JavaScript
/* eslint import/extensions: off */
import Slice from './Slice.js';
import ndarray from 'ndarray';
const convertToNeurologicalView = Symbol('convertToNeurologicalView');
const ensureVoxelStorageInXYZ = Symbol('ensureVoxelStorageInXYZ');
const changeVoxelStorageOrder = Symbol('changeVoxelStorageOrder');
const convertRAStoLPS = Symbol('convertRAStoLPS');
export default class Volume {
constructor (
imageIdObject,
metaData,
imageDataNDarray,
floatImageDataNDarray,
isSingleTimepoint = false
) {
this.imageIdObject = imageIdObject;
this.metaData = metaData;
this.imageDataNDarray = imageDataNDarray;
this.floatImageDataNDarray = floatImageDataNDarray;
this.isSingleTimepoint = isSingleTimepoint;
this[ensureVoxelStorageInXYZ]();
this[convertToNeurologicalView]();
this[convertRAStoLPS]();
}
/**
* ensureVoxelStorageInXYZ - Changes, if necessary, the order in which
* voxels have been stored in this volume so it matches XYZ.
* If a change is necessary, the voxel ordering is changed, as well as
* the orietantion matrix and other metadata, such as pixel spacing and
* voxel matrix lengths.
*
*/
[ensureVoxelStorageInXYZ] () {
const orientationString = this.metaData.orientationString;
const voxelStorageOrder = orientationString.slice(0, 3); // eg 'XYZ'
const voxelOrientation = new Array(3).fill(0);
// voxel index map of default orientation ([X, Y, Z] or [0,1,2])
const defaultVoxelIndexMap = {
X: 0,
Y: 1,
Z: 2
};
let validVoxelOrientation = true;
// fill voxelOrientation array with index of each dimension
for (
let voxelIndex = 0;
voxelIndex < voxelStorageOrder.length;
voxelIndex++
) {
const voxel = voxelStorageOrder[voxelIndex];
if (voxel in defaultVoxelIndexMap) {
const defaultVoxelIndex = defaultVoxelIndexMap[voxel];
// assign current voxel index to its voxel`s default position
voxelOrientation[defaultVoxelIndex] = voxelIndex;
} else {
validVoxelOrientation = false;
break;
}
}
if (validVoxelOrientation) {
// skip in case is already XYZ
if (voxelStorageOrder !== 'XYZ') {
this[changeVoxelStorageOrder](voxelOrientation);
}
} else {
console.info(
`The NIfTI file ${this.imageIdObject.filePath} has its\n voxel values stored in ${voxelStorageOrder} order in the file,\n which is a rare orientation unsupported by the viewer. Hence,\n the viewer is not doing auto flipping to match the neurological view.`
);
}
}
/**
* changeVoxelStorageOrder - Changes the voxel ordering and the appropriate
* metadata so it matches XYZ ordering. The parameter indicate the index
* of each dimension that will be mapped to x, y and z.
*
* @param {type} [x index of patient's 'x' in the original voxel storage.
* @param {type} y index of patient's 'y'.
* @param {type} z] index of patient's 'z'.
*/
[changeVoxelStorageOrder] ([x, y, z]) {
// changes the order in which voxel data is stored
if (this.hasImageData) {
this.imageDataNDarray = this.imageDataNDarray.transpose(x, y, z, 3);
if (this.floatImageDataNDarray) {
this.floatImageDataNDarray = this.floatImageDataNDarray.transpose(
x,
y,
z,
3
);
}
}
// changes the voxel data length to match new order
this.metaData.voxelLength = [
this.metaData.voxelLength[x],
this.metaData.voxelLength[y],
this.metaData.voxelLength[z]
];
// changes the orientation matrix according to the dimension rearrangement
const matrix = this.metaData.orientationMatrix;
const matrixCopy = JSON.parse(JSON.stringify(matrix));
const matrixTranspose = ndarray([].concat(...matrixCopy), [4, 4]).transpose(
1,
0
);
const matrixTransposeLines = [
matrixTranspose.pick(0, null),
matrixTranspose.pick(1, null),
matrixTranspose.pick(2, null),
matrixTranspose.pick(3, null)
];
matrix[0] = [
matrixTransposeLines[x].get(0),
matrixTransposeLines[x].get(1),
matrixTransposeLines[x].get(2),
matrixTransposeLines[3].get(x)
];
matrix[1] = [
matrixTransposeLines[y].get(0),
matrixTransposeLines[y].get(1),
matrixTransposeLines[y].get(2),
-matrixTransposeLines[3].get(y)
];
matrix[2] = [
matrixTransposeLines[z].get(0),
matrixTransposeLines[z].get(1),
matrixTransposeLines[z].get(2),
-matrixTransposeLines[3].get(z)
];
// changes the pixel spacing according to the new order
[...this.metaData.pixelSpacing] = [
this.metaData.pixelSpacing[x],
this.metaData.pixelSpacing[y],
this.metaData.pixelSpacing[z]
];
// changes the order of the signs of the axes
const orientationString = this.metaData.orientationString;
let senses = orientationString.slice(3, 6); // eg, '-++'
senses = [senses[x], senses[y], senses[z]].join('');
this.metaData.orientationString = `XYZ${senses}`;
}
/**
* convertToNeurologicalView - Changes the data array and the
* orientation matrix to match the neurological view: patient right on the
* right of the screen, anterior on the top, or to the right.
*
*/
[convertToNeurologicalView] () {
// the orientationString is created by NIFTI-Reader-JS and has 6 characters
// (e.g., XYZ+--), in which the first 3 represent the order in
// which the patient dimensions are stored in the
// image data (typically it's XYZ) and also in which sense each direction
// grows positive (compared to RAS). For example, a NIFTI file with the
// image data coded as LAS would have an orientationString of XYZ-++, with
// the negative sign representing the flip of R to L
const matrix = this.metaData.orientationMatrix;
const senses = this.metaData.orientationString.slice(3, 6); // eg, '-++'
const steps = [1, 1, 1];
if (this.metaData.orientationString.slice(0, 3) === 'XYZ') {
// if 'X-', we need to flip x axis so patient's right is
// shown on the right
if (senses[0] === '-') {
matrix[0][0] *= -1;
matrix[0][1] *= -1;
matrix[0][2] *= -1;
matrix[0][3] *= -1;
steps[0] = -1;
}
// if 'Y+' we need to flip y axis so patient's anterior is shown on the
// top
if (senses[1] === '+') {
matrix[1][0] *= -1;
matrix[1][1] *= -1;
matrix[1][2] *= -1;
matrix[1][3] *= -1;
steps[1] = -1;
}
// if 'Z+' we need to flip z axis so patient's head is shown on the top
if (senses[2] === '+') {
matrix[2][0] *= -1;
matrix[2][1] *= -1;
matrix[2][2] *= -1;
matrix[2][3] *= -1;
steps[2] = -1;
}
}
if (this.hasImageData) {
this.imageDataNDarray = this.imageDataNDarray.step(...steps, 1);
if (this.floatImageDataNDarray) {
this.floatImageDataNDarray = this.floatImageDataNDarray.step(
...steps,
1
);
}
}
}
/**
* convertRAStoLPS - converts the orientation matrix from standard nifti
* orientation of RAS to dicom's LPS so it matches cornerstone expectation
* of dicom's image orientation (i.e., row cosines, column cosines). That is
* achieved by doing a 180deg rotation on the z axis, which is equivalent to
* flipping the signs of the first 2 rows.
*
*/
[convertRAStoLPS] () {
const matrix = this.metaData.orientationMatrix;
// flipping the first row is equivalent to doing a 180deg rotation on 'z',
// which achieves going from RAS (nifti orientation) to LPS (dicom's)
matrix[0][0] *= -1;
matrix[0][1] *= -1;
matrix[0][2] *= -1;
matrix[0][3] *= -1;
matrix[1][0] *= -1;
matrix[1][1] *= -1;
matrix[1][2] *= -1;
matrix[1][3] *= -1;
}
slice (imageIdObject) {
return new Slice(this, imageIdObject, this.isSingleTimepoint);
}
get hasImageData () {
return (
this.imageDataNDarray &&
this.imageDataNDarray.data &&
this.imageDataNDarray.data.byteLength > 0
);
}
get sizeInBytes () {
const integerArraySize = this.imageDataNDarray
? this.imageDataNDarray.data.byteLength
: 0;
const floatArraySize = this.floatImageDataView
? this.floatImageDataView.data.byteLength
: 0;
return integerArraySize + floatArraySize;
}
}