UNPKG

@chinhui/niivue

Version:

minimal webgl2 nifti image viewer

1,373 lines (1,348 loc) 90 kB
import nifti from "nifti-reader-js"; import daikon from "daikon"; import { v4 as uuidv4 } from "uuid"; import { mat3, mat4, vec3, vec4 } from "gl-matrix"; import * as cmaps from "./cmaps"; import * as fflate from "fflate"; import { NiivueObject3D } from "./niivue-object3D"; import { Log } from "./logger"; const log = new Log(); function isPlatformLittleEndian() { //inspired by https://github.com/rii-mango/Papaya var buffer = new ArrayBuffer(2); new DataView(buffer).setInt16(0, 256, true); return new Int16Array(buffer)[0] === 256; } /** * query all available color maps that can be applied to volumes * @param {boolean} [sort=true] whether or not to sort the returned array * @returns {array} an array of colormap strings * @example * niivue = new Niivue() * colormaps = niivue.colorMaps() */ /** * @class NVImage * @type NVImage * @description * a NVImage encapsulates some images data and provides methods to query and operate on images * @constructor * @param {array} dataBuffer an array buffer of image data to load (there are also methods that abstract this more. See loadFromUrl, and loadFromFile) * @param {string} [name=''] a name for this image. Default is an empty string * @param {string} [colorMap='gray'] a color map to use. default is gray * @param {number} [opacity=1.0] the opacity for this image. default is 1 * @param {boolean} [trustCalMinMax=true] whether or not to trust cal_min and cal_max from the nifti header (trusting results in faster loading) * @param {number} [percentileFrac=0.02] the percentile to use for setting the robust range of the display values (smart intensity setting for images with large ranges) * @param {boolean} [ignoreZeroVoxels=false] whether or not to ignore zero voxels in setting the robust range of display values * @param {boolean} [visible=true] whether or not this image is to be visible */ export function NVImage( dataBuffer, // can be an array of Typed arrays or just a typed array. If an array of Typed arrays then it is assumed you are loading DICOM (perhaps the only real use case?) name = "", colorMap = "gray", opacity = 1.0, pairedImgData = null, trustCalMinMax = true, percentileFrac = 0.02, ignoreZeroVoxels = false, visible = true, isDICOMDIR = false, useQFormNotSForm = false ) { // https://nifti.nimh.nih.gov/pub/dist/src/niftilib/nifti1.h this.DT_NONE = 0; this.DT_UNKNOWN = 0; /* what it says, dude */ this.DT_BINARY = 1; /* binary (1 bit/voxel) */ this.DT_UNSIGNED_CHAR = 2; /* unsigned char (8 bits/voxel) */ this.DT_SIGNED_SHORT = 4; /* signed short (16 bits/voxel) */ this.DT_SIGNED_INT = 8; /* signed int (32 bits/voxel) */ this.DT_FLOAT = 16; /* float (32 bits/voxel) */ this.DT_COMPLEX = 32; /* complex (64 bits/voxel) */ this.DT_DOUBLE = 64; /* double (64 bits/voxel) */ this.DT_RGB = 128; /* RGB triple (24 bits/voxel) */ this.DT_ALL = 255; /* not very useful (?) */ this.DT_INT8 = 256; /* signed char (8 bits) */ this.DT_UINT16 = 512; /* unsigned short (16 bits) */ this.DT_UINT32 = 768; /* unsigned int (32 bits) */ this.DT_INT64 = 1024; /* long long (64 bits) */ this.DT_UINT64 = 1280; /* unsigned long long (64 bits) */ this.DT_FLOAT128 = 1536; /* long double (128 bits) */ this.DT_COMPLEX128 = 1792; /* double pair (128 bits) */ this.DT_COMPLEX256 = 2048; /* long double pair (256 bits) */ this.DT_RGBA32 = 2304; /* 4 byte RGBA (32 bits/voxel) */ this.name = name; this.id = uuidv4(); this.colorMap = colorMap; this.frame4D = 0; //indexed from 0! this.opacity = opacity > 1.0 ? 1.0 : opacity; //make sure opacity can't be initialized greater than 1 see: #107 and #117 on github this.percentileFrac = percentileFrac; this.ignoreZeroVoxels = ignoreZeroVoxels; this.trustCalMinMax = trustCalMinMax; this.visible = visible; this.series = []; // for concatenating dicom images // Added to support zerosLike if (!dataBuffer) { return; } var re = /(?:\.([^.]+))?$/; let ext = re.exec(name)[1] || ""; ext = ext.toUpperCase(); if (ext === "GZ") { ext = re.exec(name.slice(0, -3))[1]; //img.trk.gz -> img.trk ext = ext.toUpperCase(); } let imgRaw = null; this.hdr = null; if (ext === "" && isDICOMDIR && Array.isArray(dataBuffer)) { imgRaw = this.readDICOM(dataBuffer); } else if (ext === "MIH" || ext === "MIF") { imgRaw = this.readMIF(dataBuffer, pairedImgData); //detached } else if (ext === "NHDR" || ext === "NRRD") { imgRaw = this.readNRRD(dataBuffer, pairedImgData); //detached } else if (ext === "MHD" || ext === "MHA") { imgRaw = this.readMHA(dataBuffer); //to do: pairedImgData } else if (ext === "MGH" || ext === "MGZ") { imgRaw = this.readMGH(dataBuffer); } else if (ext === "V") { imgRaw = this.readECAT(dataBuffer); } else if (ext === "V16") { imgRaw = this.readV16(dataBuffer); } else if (ext === "VMR") { imgRaw = this.readVMR(dataBuffer); } else if (ext === "HEAD") { imgRaw = this.readHEAD(dataBuffer, pairedImgData); //paired = .BRIK } else if (ext === "NII") { this.hdr = nifti.readHeader(dataBuffer); if (this.hdr.cal_min === 0 && this.hdr.cal_max === 255) this.hdr.cal_max = 0.0; if (nifti.isCompressed(dataBuffer)) { imgRaw = nifti.readImage(this.hdr, nifti.decompress(dataBuffer)); } else { imgRaw = nifti.readImage(this.hdr, dataBuffer); } } else { //DICOMs do not always end .dcm, so DICOM is our format of last resort imgRaw = this.readDICOM(dataBuffer); // if loading a DICOM directory } this.nFrame4D = 1; for (let i = 4; i < 7; i++) if (this.hdr.dims[i] > 1) this.nFrame4D *= this.hdr.dims[i]; this.nVox3D = this.hdr.dims[1] * this.hdr.dims[2] * this.hdr.dims[3]; let nVol4D = imgRaw.byteLength / this.nVox3D / (this.hdr.numBitsPerVoxel / 8); if (nVol4D !== this.nFrame4D) console.log( "This header does not match voxel data", this.hdr, imgRaw.byteLength ); if ( this.hdr.intent_code === 1007 && this.nFrame4D === 3 && this.hdr.datatypeCode === this.DT_FLOAT ) { let tmp = new Float32Array(imgRaw); let f32 = tmp.slice(); this.hdr.datatypeCode = this.DT_RGB; this.nFrame4D = 1; for (let i = 4; i < 7; i++) this.hdr.dims[i] = 1; this.hdr.dims[0] = 3; //3D imgRaw = new Uint8Array(this.nVox3D * 3); //*3 for RGB let mx = Math.abs(f32[0]); for (let i = 0; i < this.nVox3D * 3; i++) mx = Math.max(mx, Math.abs(f32[i])); let slope = 1.0; if (mx > 0) slope = 1.0 / mx; let nVox3D2 = this.nVox3D * 2; let j = 0; for (let i = 0; i < this.nVox3D; i++) { imgRaw[j] = 255.0 * Math.abs(f32[i] * slope); imgRaw[j + 1] = 255.0 * Math.abs(f32[i + this.nVox3D] * slope); imgRaw[j + 2] = 255.0 * Math.abs(f32[i + nVox3D2] * slope); j += 3; } } //NIFTI_INTENT_VECTOR: this is a RGB tensor if ( this.hdr.pixDims[1] === 0.0 || this.hdr.pixDims[2] === 0.0 || this.hdr.pixDims[3] === 0.0 ) console.log("pixDims not plausible", this.hdr); function isAffineOK(mtx) { //A good matrix should not have any components that are not a number //A good spatial transformation matrix should not have a row or column that is all zeros let iOK = [false, false, false, false]; let jOK = [false, false, false, false]; for (let i = 0; i < 4; i++) { for (let j = 0; j < 4; j++) { if (isNaN(mtx[i][j])) return false; } } for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { if (mtx[i][j] === 0.0) continue; iOK[i] = true; jOK[j] = true; } } for (let i = 0; i < 3; i++) { if (!iOK[i]) return false; if (!jOK[i]) return false; } return true; } // if (isNaN(this.hdr.scl_slope) || this.hdr.scl_slope === 0.0) this.hdr.scl_slope = 1.0; //https://github.com/nipreps/fmriprep/issues/2507 if (isNaN(this.hdr.scl_inter)) this.hdr.scl_inter = 0.0; let affineOK = isAffineOK(this.hdr.affine); if ( useQFormNotSForm || !affineOK || this.hdr.qform_code > this.hdr.sform_code ) { log.debug("spatial transform based on QForm"); //https://github.com/rii-mango/NIFTI-Reader-JS/blob/6908287bf99eb3bc4795c1591d3e80129da1e2f6/src/nifti1.js#L238 // Define a, b, c, d for coding covenience const b = this.hdr.quatern_b; const c = this.hdr.quatern_c; const d = this.hdr.quatern_d; // quatern_a is a parameter in quaternion [a, b, c, d], which is required in affine calculation (METHOD 2) // mentioned in the nifti1.h file // It can be calculated by a = sqrt(1.0-(b*b+c*c+d*d)) const a = Math.sqrt( 1.0 - (Math.pow(b, 2) + Math.pow(c, 2) + Math.pow(d, 2)) ); const qfac = this.hdr.pixDims[0] === 0 ? 1 : this.hdr.pixDims[0]; const quatern_R = [ [ a * a + b * b - c * c - d * d, 2 * b * c - 2 * a * d, 2 * b * d + 2 * a * c, ], [ 2 * b * c + 2 * a * d, a * a + c * c - b * b - d * d, 2 * c * d - 2 * a * b, ], [ 2 * b * d - 2 * a * c, 2 * c * d + 2 * a * b, a * a + d * d - c * c - b * b, ], ]; const affine = this.hdr.affine; for (let ctrOut = 0; ctrOut < 3; ctrOut += 1) { for (let ctrIn = 0; ctrIn < 3; ctrIn += 1) { affine[ctrOut][ctrIn] = quatern_R[ctrOut][ctrIn] * this.hdr.pixDims[ctrIn + 1]; if (ctrIn === 2) { affine[ctrOut][ctrIn] *= qfac; } } } // The last row of affine matrix is the offset vector affine[0][3] = this.hdr.qoffset_x; affine[1][3] = this.hdr.qoffset_y; affine[2][3] = this.hdr.qoffset_z; this.hdr.affine = affine; } affineOK = isAffineOK(this.hdr.affine); if (!affineOK) { log.debug("Defective NIfTI: spatial transform does not make sense"); let x = this.hdr.pixDims[1]; let y = this.hdr.pixDims[2]; let z = this.hdr.pixDims[3]; if (isNaN(x) || x === 0.0) x = 1.0; if (isNaN(y) || y === 0.0) y = 1.0; if (isNaN(z) || z === 0.0) z = 1.0; this.hdr.pixDims[1] = x; this.hdr.pixDims[2] = y; this.hdr.pixDims[3] = z; const affine = [ [x, 0, 0, 0], [0, y, 0, 0], [0, 0, z, 0], [0, 0, 0, 1], ]; this.hdr.affine = affine; } //defective affine //swap data if foreign endian: if ( this.hdr.datatypeCode !== this.DT_RGB && this.hdr.datatypeCode !== this.DT_RGBA32 && this.hdr.littleEndian !== isPlatformLittleEndian() && this.hdr.numBitsPerVoxel > 8 ) { if (this.hdr.numBitsPerVoxel === 16) { //inspired by https://github.com/rii-mango/Papaya var u16 = new Uint16Array(imgRaw); for (let i = 0; i < u16.length; i++) { let val = u16[i]; u16[i] = ((((val & 0xff) << 8) | ((val >> 8) & 0xff)) << 16) >> 16; // since JS uses 32-bit when bit shifting } } else if (this.hdr.numBitsPerVoxel === 32) { //inspired by https://github.com/rii-mango/Papaya var u32 = new Uint32Array(imgRaw); for (let i = 0; i < u32.length; i++) { let val = u32[i]; u32[i] = ((val & 0xff) << 24) | ((val & 0xff00) << 8) | ((val >> 8) & 0xff00) | ((val >> 24) & 0xff); } } else if (this.hdr.numBitsPerVoxel === 64) { //inspired by MIT licensed code: https://github.com/rochars/endianness let numBytesPerVoxel = this.hdr.numBitsPerVoxel / 8; var u8 = new Uint8Array(imgRaw); for (let index = 0; index < u8.length; index += numBytesPerVoxel) { let offset = bytesPer - 1; for (let x = 0; x < offset; x++) { let theByte = u8[index + x]; u8[index + x] = u8[index + offset]; u8[index + offset] = theByte; offset--; } } } //if 64-bits } //swap byte order switch (this.hdr.datatypeCode) { case this.DT_UNSIGNED_CHAR: this.img = new Uint8Array(imgRaw); break; case this.DT_SIGNED_SHORT: this.img = new Int16Array(imgRaw); break; case this.DT_FLOAT: this.img = new Float32Array(imgRaw); break; case this.DT_DOUBLE: this.img = new Float64Array(imgRaw); break; case this.DT_RGB: this.img = new Uint8Array(imgRaw); break; case this.DT_UINT16: this.img = new Uint16Array(imgRaw); break; case this.DT_RGBA32: this.img = new Uint8Array(imgRaw); break; case this.DT_INT8: { let i8 = new Int8Array(imgRaw); var vx8 = i8.length; this.img = new Int16Array(vx8); for (let i = 0; i < vx8 - 1; i++) this.img[i] = i8[i]; this.hdr.datatypeCode = this.DT_SIGNED_SHORT; break; } case this.DT_UINT32: { let u32 = new Uint32Array(imgRaw); var vx32 = u32.length; this.img = new Float64Array(vx32); for (let i = 0; i < vx32 - 1; i++) this.img[i] = u32[i]; this.hdr.datatypeCode = this.DT_DOUBLE; break; } case this.DT_SIGNED_INT: { let i32 = new Int32Array(imgRaw); var vxi32 = i32.length; this.img = new Float64Array(vxi32); for (let i = 0; i < vxi32 - 1; i++) this.img[i] = i32[i]; this.hdr.datatypeCode = this.DT_DOUBLE; break; } case this.DT_INT64: { // eslint-disable-next-line no-undef let i64 = new BigInt64Array(imgRaw); let vx = i64.length; this.img = new Float64Array(vx); for (let i = 0; i < vx - 1; i++) this.img[i] = Number(i64[i]); this.hdr.datatypeCode = this.DT_DOUBLE; break; } default: throw "datatype " + this.hdr.datatypeCode + " not supported"; } console.log(this.hdr.datatypeCode); this.calculateRAS(); this.calMinMax(); } NVImage.prototype.calculateOblique = function () { let LPI = this.vox2mm([0.0, 0.0, 0.0], this.matRAS); let X1mm = this.vox2mm([1.0 / this.pixDimsRAS[1], 0.0, 0.0], this.matRAS); let Y1mm = this.vox2mm([0.0, 1.0 / this.pixDimsRAS[2], 0.0], this.matRAS); let Z1mm = this.vox2mm([0.0, 0.0, 1.0 / this.pixDimsRAS[3]], this.matRAS); vec3.subtract(X1mm, X1mm, LPI); vec3.subtract(Y1mm, Y1mm, LPI); vec3.subtract(Z1mm, Z1mm, LPI); let oblique = mat4.fromValues( X1mm[0], X1mm[1], X1mm[2], 0, Y1mm[0], Y1mm[1], Y1mm[2], 0, Z1mm[0], Z1mm[1], Z1mm[2], 0, 0, 0, 0, 1 ); this.obliqueRAS = mat4.clone(oblique); let XY = Math.abs(90 - vec3.angle(X1mm, Y1mm) * (180 / Math.PI)); let XZ = Math.abs(90 - vec3.angle(X1mm, Z1mm) * (180 / Math.PI)); let YZ = Math.abs(90 - vec3.angle(Y1mm, Z1mm) * (180 / Math.PI)); let maxShear = Math.max(Math.max(XY, XZ), YZ); if (maxShear > 0.1) log.debug("Warning: shear detected (gantry tilt) of %f degrees", maxShear); }; NVImage.prototype.THD_daxes_to_NIFTI = function ( xyzDelta, xyzOrigin, orientSpecific ) { //https://github.com/afni/afni/blob/d6997e71f2b625ac1199460576d48f3136dac62c/src/thd_niftiwrite.c#L315 let hdr = this.hdr; hdr.sform_code = 2; const ORIENT_xyz = "xxyyzzg"; //note strings indexed from 0! let nif_x_axnum = -1; let nif_y_axnum = -1; let nif_z_axnum = -1; let axcode = ["x", "y", "z"]; axcode[0] = ORIENT_xyz[orientSpecific[0]]; axcode[1] = ORIENT_xyz[orientSpecific[1]]; axcode[2] = ORIENT_xyz[orientSpecific[2]]; let axstep = xyzDelta.slice(0, 3); let axstart = xyzOrigin.slice(0, 3); for (var ii = 0; ii < 3; ii++) { if (axcode[ii] === "x") nif_x_axnum = ii; else if (axcode[ii] === "y") nif_y_axnum = ii; else nif_z_axnum = ii; } if (nif_x_axnum < 0 || nif_y_axnum < 0 || nif_z_axnum < 0) return; //not assigned if ( nif_x_axnum === nif_y_axnum || nif_x_axnum === nif_z_axnum || nif_y_axnum === nif_z_axnum ) return; //not assigned hdr.pixDims[1] = Math.abs(axstep[0]); hdr.pixDims[2] = Math.abs(axstep[1]); hdr.pixDims[3] = Math.abs(axstep[2]); hdr.affine = [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1], ]; hdr.affine[0][nif_x_axnum] = -axstep[nif_x_axnum]; hdr.affine[1][nif_y_axnum] = -axstep[nif_y_axnum]; hdr.affine[2][nif_z_axnum] = axstep[nif_z_axnum]; hdr.affine[0][3] = -axstart[nif_x_axnum]; hdr.affine[1][3] = -axstart[nif_y_axnum]; hdr.affine[2][3] = axstart[nif_z_axnum]; }; NVImage.prototype.SetPixDimFromSForm = function () { let m = this.hdr.affine; let mat = mat4.fromValues( m[0][0], m[0][1], m[0][2], m[0][3], m[1][0], m[1][1], m[1][2], m[1][3], m[2][0], m[2][1], m[2][2], m[2][3], m[3][0], m[3][1], m[3][2], m[3][3] ); let mm000 = this.vox2mm([0, 0, 0], mat); let mm100 = this.vox2mm([1, 0, 0], mat); vec3.subtract(mm100, mm100, mm000); let mm010 = this.vox2mm([0, 1, 0], mat); vec3.subtract(mm010, mm010, mm000); let mm001 = this.vox2mm([0, 0, 1], mat); vec3.subtract(mm001, mm001, mm000); this.hdr.pixDims[1] = vec3.length(mm100); this.hdr.pixDims[2] = vec3.length(mm010); this.hdr.pixDims[3] = vec3.length(mm001); }; function getBestTransform(imageDirections, voxelDimensions, imagePosition) { //https://github.com/rii-mango/Papaya/blob/782a19341af77a510d674c777b6da46afb8c65f1/src/js/volume/dicom/header-dicom.js#L605 /*Copyright (c) 2012-2015, RII-UTHSCSA All rights reserved. THIS PRODUCT IS NOT FOR CLINICAL USE. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name of the RII-UTHSCSA nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ var cosines = imageDirections, m = null; if (cosines) { var vs = { colSize: voxelDimensions[0], rowSize: voxelDimensions[1], sliceSize: voxelDimensions[2], }; var coord = imagePosition; var cosx = [cosines[0], cosines[1], cosines[2]]; var cosy = [cosines[3], cosines[4], cosines[5]]; var cosz = [ cosx[1] * cosy[2] - cosx[2] * cosy[1], cosx[2] * cosy[0] - cosx[0] * cosy[2], cosx[0] * cosy[1] - cosx[1] * cosy[0], ]; m = [ [ cosx[0] * vs.colSize * -1, cosy[0] * vs.rowSize * -1, cosz[0] * vs.sliceSize * -1, -1 * coord[0], ], [ cosx[1] * vs.colSize * -1, cosy[1] * vs.rowSize * -1, cosz[1] * vs.sliceSize * -1, -1 * coord[1], ], [ cosx[2] * vs.colSize, cosy[2] * vs.rowSize, cosz[2] * vs.sliceSize, coord[2], ], [0, 0, 0, 1], ]; } return m; } // getBestTransform() NVImage.prototype.readDICOM = function (buf) { this.series = new daikon.Series(); // parse DICOM file if (Array.isArray(buf)) { for (let i = 0; i < buf.length; i++) { let image = daikon.Series.parseImage(new DataView(buf[i])); if (image === null) { console.error(daikon.Series.parserError); } else if (image.hasPixelData()) { // if it's part of the same series, add it if ( this.series.images.length === 0 || image.getSeriesId() === this.series.images[0].getSeriesId() ) { this.series.addImage(image); } } // if hasPixelData } // for i } else { // Array.isArray var image = daikon.Series.parseImage(new DataView(buf)); if (image === null) { console.error(daikon.Series.parserError); } else if (image.hasPixelData()) { // if it's part of the same series, add it if ( this.series.images.length === 0 || image.getSeriesId() === this.series.images[0].getSeriesId() ) { this.series.addImage(image); } } } // order the image files, determines number of frames, etc. this.series.buildSeries(); // output some header info this.hdr = new nifti.NIFTI1(); let hdr = this.hdr; hdr.scl_inter = 0; hdr.scl_slope = 1; if (this.series.images[0].getDataScaleIntercept()) hdr.scl_inter = this.series.images[0].getDataScaleIntercept(); if (this.series.images[0].getDataScaleSlope()) hdr.scl_slope = this.series.images[0].getDataScaleSlope(); if (hdr.scl_slope === 0) hdr.scl_slope; hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]; hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]; hdr.dims[1] = this.series.images[0].getCols(); hdr.dims[2] = this.series.images[0].getRows(); hdr.dims[3] = this.series.images[0].getNumberOfFrames(); if (this.series.images.length > 1) { if (hdr.dims[3] > 1) console.log( "To Do: multiple slices per file and multiple files (XA30 DWI)" ); hdr.dims[3] = this.series.images.length; } let rc = this.series.images[0].getPixelSpacing(); //TODO: order? hdr.pixDims[1] = rc[0]; hdr.pixDims[2] = rc[1]; hdr.pixDims[3] = Math.max( this.series.images[0].getSliceGap(), this.series.images[0].getSliceThickness() ); hdr.pixDims[4] = this.series.images[0].getTR() / 1000.0; //msec -> sec let dt = this.series.images[0].getDataType(); //2=int,3=uint,4=float, let bpv = this.series.images[0].getBitsAllocated(); hdr.numBitsPerVoxel = bpv; this.hdr.littleEndian = this.series.images[0].littleEndian; if (bpv === 8 && dt === 2) hdr.datatypeCode = this.DT_INT8; else if (bpv === 8 && dt === 3) hdr.datatypeCode = this.DT_UNSIGNED_CHAR; else if (bpv === 16 && dt === 2) hdr.datatypeCode = this.DT_SIGNED_SHORT; else if (bpv === 16 && dt === 3) hdr.datatypeCode = this.DT_UINT16; else if (bpv === 32 && dt === 2) hdr.datatypeCode = this.DT_SIGNED_INT; else if (bpv === 32 && dt === 3) hdr.datatypeCode = this.DT_UINT32; else if (bpv === 32 && dt === 4) hdr.datatypeCode = this.DT_FLOAT; else if (bpv === 64 && dt === 4) hdr.datatypeCode = this.DT_DOUBLE; else console.log("Unsupported DICOM format: " + dt + " " + bpv); let voxelDimensions = hdr.pixDims.slice(1, 4); //console.log("dir", this.series.images[0].getImageDirections()); //console.log("pos", this.series.images[0].getImagePosition()); //console.log("dims", voxelDimensions); let m = getBestTransform( this.series.images[0].getImageDirections(), voxelDimensions, this.series.images[0].getImagePosition() ); if (m) { hdr.sform_code = 1; hdr.affine = [ [m[0][0], m[0][1], m[0][2], m[0][3]], [m[1][0], m[1][1], m[1][2], m[1][3]], [m[2][0], m[2][1], m[2][2], m[2][3]], [0, 0, 0, 1], ]; } console.log("DICOM", this.series.images[0]); console.log("NIfTI", hdr); let imgRaw = []; //let byteLength = hdr.dims[1] * hdr.dims[2] * hdr.dims[3] * (bpv / 8); let data; let length = this.series.validatePixelDataLength(this.series.images[0]); let buffer = new Uint8Array( new ArrayBuffer(length * this.series.images.length) ); // implementation copied from: // https://github.com/rii-mango/Daikon/blob/bbe08bad9758dfbdf31ca22fb79048c7bad85706/src/series.js#L496 for (let i = 0; i < this.series.images.length; i++) { if (this.series.isMosaic) { data = this.series.getMosaicData( this.series.images[i], this.series.images[i].getPixelDataBytes() ); } else { data = this.series.images[i].getPixelDataBytes(); } length = this.series.validatePixelDataLength(this.series.images[i]); this.series.images[i].clearPixelData(); buffer.set(new Uint8Array(data, 0, length), length * i); } // for images.length imgRaw = buffer.buffer; return imgRaw; }; // readDICOM() NVImage.prototype.readECAT = function (buffer) { //https://github.com/openneuropet/PET2BIDS/tree/28aae3fab22309047d36d867c624cd629c921ca6/ecat_validation/ecat_info this.hdr = new nifti.NIFTI1(); let hdr = this.hdr; hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]; hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]; var reader = new DataView(buffer); var raw = buffer; let signature = reader.getInt32(0, false); //"MATR" let filetype = reader.getInt16(50, false); if (signature !== 1296127058 || filetype < 1 || filetype > 14) { console.log("Not a valid ECAT file"); return; } //list header, starts at 512 bytes: int32_t hdr[4], r[31][4]; let pos = 512; //512=main header, 4*32-bit hdr let vols = 0; let frame_duration = []; let rawImg = []; while (true) { //read 512 block lists let hdr0 = reader.getInt32(pos, false); let hdr3 = reader.getInt32(pos + 12, false); if (hdr0 + hdr3 !== 31) break; let lpos = pos + 20; //skip hdr and read slice offset (r[0][1]) let r = 0; let voloffset = 0; while (r < 31) { //r[0][1]...r[30][1] voloffset = reader.getInt32(lpos, false); lpos += 16; //e.g. r[0][1] to r[1][1] if (voloffset === 0) break; r++; let ipos = voloffset * 512; //image start position let spos = ipos - 512; //subheader for matrix image, immediately before image let data_type = reader.getUint16(spos, false); hdr.dims[1] = reader.getUint16(spos + 4, false); hdr.dims[2] = reader.getUint16(spos + 6, false); hdr.dims[3] = reader.getUint16(spos + 8, false); let scale_factor = reader.getFloat32(spos + 26, false); hdr.pixDims[1] = reader.getFloat32(spos + 34, false) * 10.0; //cm -> mm hdr.pixDims[2] = reader.getFloat32(spos + 38, false) * 10.0; //cm -> mm hdr.pixDims[3] = reader.getFloat32(spos + 42, false) * 10.0; //cm -> mm hdr.pixDims[4] = reader.getUint32(spos + 46, false) / 1000.0; //ms -> sec frame_duration.push(hdr.pixDims[4]); let nvox3D = hdr.dims[1] * hdr.dims[2] * hdr.dims[3]; var newImg = new Float32Array(nvox3D); //convert to float32 as scale varies if (data_type == 1) //uint8 for (var i = 0; i < nvox3D; i++) { newImg[i] = reader.getUint8(ipos) * scale_factor; ipos++; } else if (data_type == 6) { //uint16 for (var i = 0; i < nvox3D; i++) { newImg[i] = reader.getUint16(ipos, false) * scale_factor; ipos += 2; } } else if (ihdr.data_type == 7) { //uint32 for (var i = 0; i < nvox3D; i++) { newImg[i] = reader.getUint32(ipos, false) * scale_factor; ipos += 4; } } else console.log("Unknown ECAT data type " + data_type); let prevImg = rawImg.slice(); rawImg = new Float32Array(prevImg.length + newImg.length); rawImg.set(prevImg); rawImg.set(newImg, prevImg.length); vols++; } if (voloffset === 0) break; pos += 512; //possible to have multiple 512-byte lists of images } hdr.dims[4] = vols; hdr.pixDims[4] = frame_duration[0]; if (vols > 1) { hdr.dims[0] = 4; let isFDvaries = false; for (var i = 0; i < vols; i++) if (frame_duration[i] !== frame_duration[0]) isFDvaries = true; if (isFDvaries) console.log("Frame durations vary"); } hdr.sform_code = 1; hdr.affine = [ [-hdr.pixDims[1], 0, 0, (hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]], [0, -hdr.pixDims[2], 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]], [0, 0, -hdr.pixDims[3], (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]], [0, 0, 0, 1], ]; hdr.numBitsPerVoxel = 32; hdr.datatypeCode = this.DT_FLOAT; return rawImg; }; // readECAT() NVImage.prototype.readV16 = function (buffer) { this.hdr = new nifti.NIFTI1(); let hdr = this.hdr; hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]; hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]; var reader = new DataView(buffer); hdr.dims[1] = reader.getUint16(0, true); hdr.dims[2] = reader.getUint16(2, true); hdr.dims[3] = reader.getUint16(4, true); let nBytes = 2 * hdr.dims[1] * hdr.dims[2] * hdr.dims[3]; if (nBytes + 6 !== buffer.byteLength) console.log("This does not look like a valid BrainVoyager V16 file"); hdr.numBitsPerVoxel = 16; hdr.datatypeCode = this.DT_UINT16; console.log("Warning: V16 files have no spatial transforms"); hdr.affine = [ [0, 0, -hdr.pixDims[1], (hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]], [-hdr.pixDims[2], 0, 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]], [0, -hdr.pixDims[3], 0, (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]], [0, 0, 0, 1], ]; hdr.littleEndian = true; return buffer.slice(6); }; // readV16() NVImage.prototype.readVMR = function (buffer) { //https://support.brainvoyager.com/brainvoyager/automation-development/84-file-formats/343-developer-guide-2-6-the-format-of-vmr-files this.hdr = new nifti.NIFTI1(); let hdr = this.hdr; hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]; hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]; var reader = new DataView(buffer); let version = reader.getUint16(0, true); if (version !== 4) console.log("Not a valid version 4 VMR image"); hdr.dims[1] = reader.getUint16(2, true); hdr.dims[2] = reader.getUint16(4, true); hdr.dims[3] = reader.getUint16(6, true); let nBytes = hdr.dims[1] * hdr.dims[2] * hdr.dims[3]; if (version >= 4) { let pos = 8 + nBytes; //offset to post header let xoff = reader.getUint16(pos, true); let yoff = reader.getUint16(pos + 2, true); let zoff = reader.getUint16(pos + 4, true); let framingCube = reader.getUint16(pos + 6, true); let posInfo = reader.getUint32(pos + 8, true); let coordSys = reader.getUint32(pos + 12, true); let XmmStart = reader.getFloat32(pos + 16, true); let YmmStart = reader.getFloat32(pos + 20, true); let ZmmStart = reader.getFloat32(pos + 24, true); let XmmEnd = reader.getFloat32(pos + 28, true); let YmmEnd = reader.getFloat32(pos + 32, true); let ZmmEnd = reader.getFloat32(pos + 36, true); let Xsl = reader.getFloat32(pos + 40, true); let Ysl = reader.getFloat32(pos + 44, true); let Zsl = reader.getFloat32(pos + 48, true); let colDirX = reader.getFloat32(pos + 52, true); let colDirY = reader.getFloat32(pos + 56, true); let colDirZ = reader.getFloat32(pos + 60, true); let nRow = reader.getUint32(pos + 64, true); let nCol = reader.getUint32(pos + 68, true); let FOVrow = reader.getFloat32(pos + 72, true); let FOVcol = reader.getFloat32(pos + 76, true); let sliceThickness = reader.getFloat32(pos + 80, true); let gapThickness = reader.getFloat32(pos + 84, true); let nSpatialTransforms = reader.getUint32(pos + 88, true); pos = pos + 92; if (nSpatialTransforms > 0) { let len = buffer.byteLength; for (let i = 0; i < nSpatialTransforms; i++) { //read variable length name name... while (pos < len && reader.getUint8(pos) !== 0) pos++; pos++; let typ = reader.getUint32(pos, true); pos += 4; //read variable length name name... while (pos < len && reader.getUint8(pos) !== 0) pos++; pos++; let nValues = reader.getUint32(pos, true); pos += 4; for (let j = 0; j < nValues; j++) pos += 4; } } let LRconv = reader.getUint8(pos); let ref = reader.getUint8(pos + 1); hdr.pixDims[1] = reader.getFloat32(pos + 2, true); hdr.pixDims[2] = reader.getFloat32(pos + 6, true); hdr.pixDims[3] = reader.getFloat32(pos + 10, true); let isVer = reader.getUint8(pos + 14); let isTal = reader.getUint8(pos + 15); let minInten = reader.getInt32(pos + 16, true); let meanInten = reader.getInt32(pos + 20, true); let maxInten = reader.getInt32(pos + 24, true); } console.log("Warning: VMR spatial transform not implemented"); //if (XmmStart === XmmEnd) { // https://brainvoyager.com/bv/sampledata/index.html?? hdr.affine = [ [0, 0, -hdr.pixDims[1], (hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]], [-hdr.pixDims[2], 0, 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]], [0, -hdr.pixDims[3], 0, (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]], [0, 0, 0, 1], ]; //} console.log(hdr); hdr.numBitsPerVoxel = 8; hdr.datatypeCode = this.DT_UNSIGNED_CHAR; return buffer.slice(8, 8 + nBytes); }; // readVMR() NVImage.prototype.readMGH = function (buffer) { this.hdr = new nifti.NIFTI1(); let hdr = this.hdr; hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]; hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]; var reader = new DataView(buffer); var raw = buffer; if (reader.getUint8(0) === 31 && reader.getUint8(1) === 139) { var raw = fflate.decompressSync(new Uint8Array(buffer)); reader = new DataView(raw.buffer); } let version = reader.getInt32(0, false); let width = reader.getInt32(4, false); let height = reader.getInt32(8, false); let depth = reader.getInt32(12, false); let nframes = reader.getInt32(16, false); let mtype = reader.getInt32(20, false); let dof = reader.getInt32(24, false); let goodRASFlag = reader.getInt16(28, false); let spacingX = reader.getFloat32(30, false); let spacingY = reader.getFloat32(34, false); let spacingZ = reader.getFloat32(38, false); let xr = reader.getFloat32(42, false); let xa = reader.getFloat32(46, false); let xs = reader.getFloat32(50, false); let yr = reader.getFloat32(54, false); let ya = reader.getFloat32(58, false); let ys = reader.getFloat32(62, false); let zr = reader.getFloat32(66, false); let za = reader.getFloat32(70, false); let zs = reader.getFloat32(74, false); let cr = reader.getFloat32(78, false); let ca = reader.getFloat32(82, false); let cs = reader.getFloat32(86, false); if (version !== 1 || mtype < 0 || mtype > 4) console.log("Not a valid MGH file"); if (mtype === 0) { hdr.numBitsPerVoxel = 8; hdr.datatypeCode = this.DT_UNSIGNED_CHAR; } else if (mtype === 4) { hdr.numBitsPerVoxel = 16; hdr.datatypeCode = this.DT_SIGNED_SHORT; } else if (mtype === 1) { hdr.numBitsPerVoxel = 32; hdr.datatypeCode = this.DT_SIGNED_INT; } else if (mtype === 3) { hdr.numBitsPerVoxel = 32; hdr.datatypeCode = this.DT_FLOAT; } hdr.dims[1] = width; hdr.dims[2] = height; hdr.dims[3] = depth; hdr.dims[4] = nframes; if (nframes > 1) hdr.dims[0] = 4; hdr.pixDims[1] = spacingX; hdr.pixDims[2] = spacingY; hdr.pixDims[3] = spacingZ; hdr.vox_offset = 284; hdr.sform_code = 1; let rot44 = mat4.fromValues( xr * hdr.pixDims[1], yr * hdr.pixDims[2], zr * hdr.pixDims[3], 0, xa * hdr.pixDims[1], ya * hdr.pixDims[2], za * hdr.pixDims[3], 0, xs * hdr.pixDims[1], ys * hdr.pixDims[2], zs * hdr.pixDims[3], 0, 0, 0, 0, 1 ); let base = 0.0; //0 or 1: are voxels indexed from 0 or 1? let Pcrs = [ hdr.dims[1] / 2.0 + base, hdr.dims[2] / 2.0 + base, hdr.dims[3] / 2.0 + base, 1, ]; let PxyzOffset = [0, 0, 0, 0]; for (var i = 0; i < 3; i++) { //multiply Pcrs * m for (var j = 0; j < 3; j++) { PxyzOffset[i] = PxyzOffset[i] + rot44[i + j * 4] * Pcrs[j]; } } hdr.affine = [ [rot44[0], rot44[1], rot44[2], PxyzOffset[0]], [rot44[4], rot44[5], rot44[6], PxyzOffset[1]], [rot44[8], rot44[9], rot44[10], PxyzOffset[2]], [0, 0, 0, 1], ]; let nBytes = hdr.dims[1] * hdr.dims[2] * hdr.dims[3] * hdr.dims[4] * (hdr.numBitsPerVoxel / 8); return raw.slice(hdr.vox_offset, hdr.vox_offset + nBytes); }; // readMGH() NVImage.prototype.readHEAD = function (dataBuffer, pairedImgData) { this.hdr = new nifti.NIFTI1(); let hdr = this.hdr; hdr.dims[0] = 3; hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]; let orientSpecific = [0, 0, 0]; let xyzOrigin = [0, 0, 0]; let xyzDelta = [1, 1, 1]; let txt = new TextDecoder().decode(dataBuffer); var lines = txt.split("\n"); let nlines = lines.length; let i = 0; let hasIJK_TO_DICOM_REAL = false; while (i < nlines) { let line = lines[i]; //e.g. 'type = string-attribute' i++; if (!line.startsWith("type")) continue; //n.b. white space varies, "type =" vs "type =" let isInt = line.includes("integer-attribute"); let isFloat = line.includes("float-attribute"); line = lines[i]; //e.g. 'name = IDCODE_DATE' i++; if (!line.startsWith("name")) continue; let items = line.split("= "); let key = items[1]; //e.g. 'IDCODE_DATE' line = lines[i]; //e.g. 'count = 5' i++; items = line.split("= "); let count = parseInt(items[1]); //e.g. '5' if (count < 1) continue; line = lines[i]; //e.g. ''LSB_FIRST~' i++; items = line.trim().split(/\s+/); if (isFloat || isInt) { //read arrays written on multiple lines while (items.length < count) { line = lines[i]; //e.g. ''LSB_FIRST~' i++; let items2 = line.trim().split(/\s+/); items.push(...items2); } for (var j = 0; j < count; j++) items[j] = parseFloat(items[j]); } switch (key) { case "BYTEORDER_STRING": if (items[0].includes("LSB_FIRST")) hdr.littleEndian = true; else if (items[0].includes("MSB_FIRST")) hdr.littleEndian = false; break; case "BRICK_TYPES": hdr.dims[4] = count; let datatype = parseInt(items[0]); if (datatype === 0) { hdr.numBitsPerVoxel = 8; hdr.datatypeCode = this.DT_UNSIGNED_CHAR; } else if (datatype === 1) { hdr.numBitsPerVoxel = 16; hdr.datatypeCode = this.DT_SIGNED_SHORT; } else if (datatype === 1) { hdr.numBitsPerVoxel = 32; hdr.datatypeCode = this.DT_FLOAT; } else console.log("Unknown BRICK_TYPES ", datatype); break; case "IJK_TO_DICOM_REAL": if (count < 12) break; hasIJK_TO_DICOM_REAL = true; hdr.sform_code = 2; //note DICOM space is LPS while NIfTI is RAS hdr.affine = [ [-items[0], -items[1], -items[2], -items[3]], [-items[4], -items[5], -items[6], -items[7]], [items[8], items[9], items[10], items[11]], [0, 0, 0, 1], ]; break; case "DATASET_DIMENSIONS": count = Math.max(count, 3); for (var j = 0; j < count; j++) hdr.dims[j + 1] = items[j]; break; case "ORIENT_SPECIFIC": orientSpecific = items; break; case "ORIGIN": xyzOrigin = items; break; case "DELTA": xyzDelta = items; break; case "TAXIS_FLOATS": hdr.pixDims[4] = items[0]; break; default: //console.log('Unknown:',key); } //read item } //read all lines if (!hasIJK_TO_DICOM_REAL) this.THD_daxes_to_NIFTI(xyzDelta, xyzOrigin, orientSpecific); else this.SetPixDimFromSForm(); let nBytes = (hdr.numBitsPerVoxel / 8) * hdr.dims[1] * hdr.dims[2] * hdr.dims[3] * hdr.dims[4]; if (pairedImgData.byteLength < nBytes) { //n.b. npm run dev implicitly extracts gz, npm run demo does not! //assume gz compressed var raw = fflate.decompressSync(new Uint8Array(pairedImgData)); return raw.buffer; } return pairedImgData.slice(); }; NVImage.prototype.readMHA = function (buffer, pairedImgData) { //https://itk.org/Wiki/ITK/MetaIO/Documentation#Reading_a_Brick-of-Bytes_.28an_N-Dimensional_volume_in_a_single_file.29 let len = buffer.byteLength; if (len < 20) throw new Error("File too small to be VTK: bytes = " + buffer.byteLength); var bytes = new Uint8Array(buffer); let pos = 0; function readStr() { while (pos < len && bytes[pos] === 10) pos++; //skip blank lines let startPos = pos; while (pos < len && bytes[pos] !== 10) pos++; pos++; //skip EOLN if (pos - startPos < 1) return ""; return new TextDecoder().decode(buffer.slice(startPos, pos - 1)); } let line = readStr(); //1st line: signature this.hdr = new nifti.NIFTI1(); let hdr = this.hdr; hdr.littleEndian = true; let isGz = false; let isDetached = false; let compressedDataSize = 0; let mat33 = mat3.fromValues(NaN, 0, 0, 0, 1, 0, 0, 0, 1); let offset = vec3.fromValues(0, 0, 0); while (line !== "") { let items = line.split(" "); if (items.length > 2); items = items.slice(2); if (line.startsWith("BinaryDataByteOrderMSB") && items[0].includes("False")) hdr.littleEndian = true; if (line.startsWith("BinaryDataByteOrderMSB") && items[0].includes("True")) hdr.littleEndian = false; if (line.startsWith("CompressedData") && items[0].includes("True")) isGz = true; if (line.startsWith("CompressedDataSize")) compressedDataSize = parseInt(items[0]); if (line.startsWith("TransformMatrix")) { for (var d = 0; d < 9; d++) mat33[d] = parseFloat(items[d]); } if (line.startsWith("Offset")) { offset[0] = parseFloat(items[0]); offset[1] = parseFloat(items[1]); offset[2] = parseFloat(items[2]); } //if (line.startsWith("AnatomicalOrientation")) //we can ignore, tested with Slicer3D converting NIfTIspace images if (line.startsWith("ElementSpacing")) { hdr.pixDims[1] = parseFloat(items[0]); hdr.pixDims[2] = parseFloat(items[1]); hdr.pixDims[3] = parseFloat(items[2]); if (items.length > 3) hdr.pixDims[4] = parseFloat(items[3]); } if (line.startsWith("DimSize")) { hdr.dims[0] = items.length; for (var d = 0; d < items.length; d++) hdr.dims[d + 1] = parseInt(items[d]); } if (line.startsWith("ElementType")) { switch (items[0]) { case "MET_UCHAR": hdr.numBitsPerVoxel = 8; hdr.datatypeCode = this.DT_UNSIGNED_CHAR; break; case "MET_CHAR": hdr.numBitsPerVoxel = 8; hdr.datatypeCode = this.DT_INT8; break; case "MET_SHORT": hdr.numBitsPerVoxel = 16; hdr.datatypeCode = this.DT_SIGNED_SHORT; break; case "MET_USHORT": hdr.numBitsPerVoxel = 16; hdr.datatypeCode = this.DT_UINT16; break; case "MET_INT": hdr.numBitsPerVoxel = 32; hdr.datatypeCode = this.DT_SIGNED_INT; break; case "MET_UINT": hdr.numBitsPerVoxel = 32; hdr.datatypeCode = this.DT_UINT32; break; case "MET_FLOAT": hdr.numBitsPerVoxel = 32; hdr.datatypeCode = this.DT_FLOAT; break; case "MET_DOUBLE": hdr.numBitsPerVoxel = 64; hdr.datatypeCode = this.DT_DOUBLE; break; default: throw new Error("Unsupported NRRD data type: " + value); } } if (line.startsWith("ObjectType") && !items[0].includes("Image")) console.log("Only able to read ObjectType = Image, not " + line); if (line.startsWith("ElementDataFile")) { if (items[0] !== "LOCAL") isDetached = true; break; } line = readStr(); } let mmMat = mat3.fromValues( hdr.pixDims[1], 0, 0, 0, hdr.pixDims[2], 0, 0, 0, hdr.pixDims[3] ); mat3.multiply(mat33, mmMat, mat33); hdr.affine = [ [-mat33[0], -mat33[3], -mat33[6], -offset[0]], [-mat33[1], -mat33[4], -mat33[7], -offset[1]], [mat33[2], mat33[5], mat33[8], offset[2]], [0, 0, 0, 1], ]; hdr.vox_offset = pos; if (isDetached && pairedImgData) { if (isGz) return fflate.decompressSync(new Uint8Array(buffer.slice(hdr.vox_offset))) .buffer; return pairedImgData.slice(); } if (isGz) return fflate.decompressSync(new Uint8Array(buffer.slice(hdr.vox_offset))) .buffer; return buffer.slice(hdr.vox_offset); }; //readMHA() NVImage.prototype.readMIF = function (buffer, pairedImgData) { //https://mrtrix.readthedocs.io/en/latest/getting_started/image_data.html#mrtrix-image-formats //MIF files typically 3D (e.g. anatomical), 4D (fMRI, DWI). 5D rarely seen //This read currently supports up to 5D. To create test: "mrcat -axis 4 a4d.mif b4d.mif out5d.mif" this.hdr = new nifti.NIFTI1(); let hdr = this.hdr; hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]; hdr.dims = [1, 1, 1, 1, 1, 1, 1, 1]; let len = buffer.byteLength; if (len < 20) throw new Error("File too small to be MIF: bytes = " + len); var bytes = new Uint8Array(buffer); if (bytes[0] === 31 && bytes[1] === 139) { console.log("MIF with GZ decompression"); var raw = fflate.decompressSync(new Uint8Array(buffer)); buffer = raw.buffer; len = buffer.byteLength; } let pos = 0; function readStr() { while (pos < len && bytes[pos] === 10) pos++; //skip blank lines let startPos = pos; while (pos < len && bytes[pos] !== 10) pos++; pos++; //skip EOLN if (pos - startPos < 1) return ""; return new TextDecoder().decode(buffer.slice(startPos, pos - 1)); } let line = readStr(); //1st line: signature 'mrtrix tracks' if (!line.startsWith("mrtrix image")) { console.log("Not a valid MIF file"); return; } let layout = []; let nTransform = 0; let TR = 0; let isDetached = false; line = readStr(); while (pos < len && !line.startsWith("END")) { let items = line.split(":"); // "vox: 1,1,1" -> "vox", " 1,1,1" line = readStr(); if (items.length < 2) break; // let tag = items[0]; // "datatype", "dim" items = items[1].split(","); // " 1,1,1" -> " 1", "1", "1" for (let i = 0; i < items.length; i++) items[i] = items[i].trim(); // " 1", "1", "1" -> "1", "1", "1" switch (tag) { case "dim": hdr.dims[0] = items.length; for (let i = 0; i < items.length; i++) hdr.dims[i + 1] = parseInt(items[i]); break; case "vox": for (let i = 0; i < items.length; i++) { hdr.pixDims[i + 1] = parseFloat(items[i]); if (isNaN(hdr.pixDims[i + 1])) hdr.pixDims[i + 1] = 0.0; } break; case "layout": for (let i = 0; i < items.length; i++) layout.push(parseInt(items[i])); //n.b. JavaScript preserves sign for -0 break; case "datatype": let dt = items[0]; if (dt.startsWith("Int8")) hdr.datatypeCode = this.DT_INT8; else if (dt.startsWith("UInt8")) hdr.datatypeCode = this.DT_UNSIGNED_CHAR; else if (dt.startsWith("Int16")) hdr.datatypeCode = this.DT_SIGNED_SHORT; else if (dt.startsWith("UInt16")) hdr.datatypeCode = this.DT_UINT16; else if (dt.startsWith("Int32")) hdr.datatypeCode = this.DT_SIGNED_INT; else if (dt.startsWith("UInt32")) hdr.datatypeCode = this.DT_UINT32; else if (dt.startsWith("Float32")) hdr.datatypeCode = this.DT_FLOAT; else if (dt.startsWith("Float64")) hdr.datatypeCode = this.DT_DOUBLE; else console.log("Unsupported datatype " + dt); if (dt.includes("8")) hdr.numBitsPerVoxel = 8; else if (dt.includes("16")) hdr.numBitsPerVoxel = 16; else if (dt.includes("32")) hdr.numBitsPerVoxel = 32; else if (dt.includes("64")) hdr.numBitsPerVoxel = 64; hdr.littleEndian = true; //native, to do support big endian readers if (dt.endsWith("LE")) hdr.littleEndian = true; if (dt.endsWith("BE")) hdr.littleEndian = false; break; case "transform": if (nTransform > 2 || items.length !== 4) break; hdr.affine[nTransform][0] = parseFloat(items[0]); hdr.affine[nTransform][1] = parseFloat(items[1]); hdr.affine[nTransform][2] = parseFloat(items[2]); hdr.affine[nTransform][3] = parseFloat(items[3]); nTransform++; break; case "RepetitionTime": TR = parseFloat(items[0]); break; case "file": isDetached = !items[0].startsWith(". "); if (!isDetached) { items = items[0].split(" "); //". 2336" -> ". ", "2336" hdr.vox_offset = parseInt(items[1]); } break; } } let ndim = hdr.dims[0]; if (ndim > 5) console.log("reader only designed for a maximum of 5 dimensions (XYZTD)"); let nvox = 1; for (let i = 0; i < ndim; i++) nvox *= Math.max(hdr.dims[i + 1], 1); console.log(nvox); //let nvox = hdr.dims[1] * hdr.dims[2] * hdr.dims[3] * hdr.dims[4]; for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { //hdr.affine[i][j] *= hdr.pixDims[i + 1]; hdr.affine[i][j] *= hdr.pixDims[j + 1]; } } console.log("mif affine:" + hdr.affine[0]); if (TR > 0) hdr.pixDims[4] = TR; if (isDetached && !pairedImgData) console.log("MIH header provided without paired image data"); let rawImg = []; if (isDetached) rawImg = pairedImgData.slice(); //n.b. mrconvert can pad files? See dtitest_Siemens_SC 4_dti_nopf_x2_pitch else rawImg = buffer.slice( hdr.vox_offset, hdr.vox_offset + nvox * (hdr.numBitsPerVoxel / 8) ); if (layout.length != hdr.dims[0]) console.log("dims does not match layout"); //estimate strides: let stride = 1; let instride = [1, 1, 1, 1, 1]; let inflip = [false, false, false, false, false]; for (let i = 0; i < layout.length; i++) { for (let j = 0; j < layout.length; j++) { let a = Math.abs(layout[j]); if (a != i) continue; instride[j] = stride; //detect -0: https