@niivue/niivue
Version:
minimal webgl2 nifti image viewer
162 lines (145 loc) • 5.12 kB
text/typescript
import { NIFTI1 } from 'nifti-reader-js'
import { mat4, vec4, vec3 } from 'gl-matrix'
import { log } from '../../logger.js'
import { NVUtilities } from '../../nvutilities.js'
import type { NVImage } from '../index.js'
import { NiiDataType } from '../utils.js'
/**
* Reads FreeSurfer MGH/MGZ format image, modifying the provided NVImage header
* and returning the raw image data buffer.
* @param nvImage - The NVImage instance whose header will be modified.
* @param buffer - ArrayBuffer containing the MGH/MGZ file data.
* @returns Promise resolving to the imgRaw ArrayBuffer or null on critical error.
*/
export async function readMgh(nvImage: NVImage, buffer: ArrayBuffer): Promise<ArrayBuffer | null> {
if (!nvImage.hdr) {
log.warn('readMgh called before nvImage.hdr was initialized. Creating default.')
nvImage.hdr = new NIFTI1() // Ensure header object exists
}
const hdr = nvImage.hdr
hdr.littleEndian = false
let raw = buffer
let reader = new DataView(raw)
// Decompression logic
if (raw.byteLength >= 2 && reader.getUint8(0) === 31 && reader.getUint8(1) === 139) {
try {
raw = await NVUtilities.decompressToBuffer(new Uint8Array(buffer))
reader = new DataView(raw)
} catch (err) {
log.error('Failed to decompress MGZ file.', err)
return null
}
}
if (raw.byteLength < 284) {
log.error('File too small to be a valid MGH/MGZ header.')
return null
}
// --- Read MGH Header Fields ---
const version = reader.getInt32(0, false)
const width = reader.getInt32(4, false)
const height = reader.getInt32(8, false)
const depth = reader.getInt32(12, false)
const nframes = reader.getInt32(16, false)
const mtype = reader.getInt32(20, false)
const spacingX = reader.getFloat32(30, false)
const spacingY = reader.getFloat32(34, false)
const spacingZ = reader.getFloat32(38, false)
const xr = reader.getFloat32(42, false)
const xa = reader.getFloat32(46, false)
const xs = reader.getFloat32(50, false)
const yr = reader.getFloat32(54, false)
const ya = reader.getFloat32(58, false)
const ys = reader.getFloat32(62, false)
const zr = reader.getFloat32(66, false)
const za = reader.getFloat32(70, false)
const zs = reader.getFloat32(74, false)
const cr = reader.getFloat32(78, false)
const ca = reader.getFloat32(82, false)
const cs = reader.getFloat32(86, false)
if (version !== 1) {
log.warn(`Unexpected MGH version: ${version}.`)
}
if (width <= 0 || height <= 0 || depth <= 0) {
log.error(`Invalid MGH dimensions: ${width}x${height}x${depth}`)
return null
}
// Map MGH data type directly onto nvImage.hdr
switch (mtype) {
case 0:
hdr.numBitsPerVoxel = 8
hdr.datatypeCode = NiiDataType.DT_UINT8
break
case 4:
hdr.numBitsPerVoxel = 16
hdr.datatypeCode = NiiDataType.DT_INT16
break
case 1:
hdr.numBitsPerVoxel = 32
hdr.datatypeCode = NiiDataType.DT_INT32
break
case 3:
hdr.numBitsPerVoxel = 32
hdr.datatypeCode = NiiDataType.DT_FLOAT32
break
default:
log.error(`Unsupported MGH data type: ${mtype}`)
return null
}
// Set dimensions directly onto nvImage.hdr
hdr.dims[1] = width
hdr.dims[2] = height
hdr.dims[3] = depth
hdr.dims[4] = Math.max(1, nframes)
hdr.dims[0] = hdr.dims[4] > 1 ? 4 : 3
// Set pixel dimensions directly onto nvImage.hdr (using abs)
hdr.pixDims[1] = Math.abs(spacingX)
hdr.pixDims[2] = Math.abs(spacingY)
hdr.pixDims[3] = Math.abs(spacingZ)
hdr.pixDims[4] = 0
hdr.sform_code = 1
hdr.qform_code = 0
const 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
)
const PcrsVec = vec4.fromValues(hdr.dims[1] / 2.0, hdr.dims[2] / 2.0, hdr.dims[3] / 2.0, 1)
const PxyzOffsetVec = vec4.create()
vec4.transformMat4(PxyzOffsetVec, PcrsVec, rot44)
const translation = vec3.fromValues(cr - PxyzOffsetVec[0], ca - PxyzOffsetVec[1], cs - PxyzOffsetVec[2])
hdr.affine = [
[rot44[0], rot44[1], rot44[2], translation[0]],
[rot44[4], rot44[5], rot44[6], translation[1]],
[rot44[8], rot44[9], rot44[10], translation[2]],
[0, 0, 0, 1]
]
hdr.vox_offset = 284
hdr.magic = 'n+1'
// Check data size
const nBytesPerVoxel = hdr.numBitsPerVoxel / 8
const nVoxels = width * height * depth * hdr.dims[4]
const expectedBytes = nVoxels * nBytesPerVoxel
const remainingBytes = raw.byteLength - hdr.vox_offset
if (remainingBytes < expectedBytes) {
log.error(`MGH image data size mismatch: expected ${expectedBytes}, found ${remainingBytes}`)
return null
} else if (remainingBytes > expectedBytes) {
log.warn(`MGH file has extra ${remainingBytes - expectedBytes} bytes after image data. Truncating.`)
}
// Return only the raw image data buffer
const imgRaw = raw.slice(hdr.vox_offset, hdr.vox_offset + expectedBytes)
return imgRaw
}