@niivue/niivue
Version:
minimal webgl2 nifti image viewer
208 lines (189 loc) • 7.38 kB
text/typescript
/**
* ImageDataProcessor module
*
* Handles low-level image data processing:
* - Endian byte swapping for cross-platform compatibility
* - Data type conversion from NIfTI format codes to TypeScript typed arrays
*
* This module contains pure functions for transforming raw image data
* according to NIfTI header specifications.
*/
import { NIFTI1, NIFTI2 } from 'nifti-reader-js'
import type { TypedVoxelArray } from './index'
import { NiiDataType, isPlatformLittleEndian } from '@/nvimage/utils'
/**
* Result of data type conversion, containing the converted image data
* and potentially updated header fields.
*/
export interface DataTypeConversionResult {
img: TypedVoxelArray
imaginary?: Float32Array
updatedDatatypeCode?: number
updatedNumBitsPerVoxel?: number
}
/**
* Swap byte order of multi-byte data if foreign endian.
* Modifies the ArrayBuffer in place.
*
* @param imgRaw - Raw image data buffer
* @param hdr - NIfTI header containing datatype and endianness information
*/
export function swapBytesIfNeeded(imgRaw: ArrayBufferLike, hdr: NIFTI1 | NIFTI2): void {
// No swapping needed for RGB formats or single-byte data
if (hdr.datatypeCode === NiiDataType.DT_RGB24 || hdr.datatypeCode === NiiDataType.DT_RGBA32 || hdr.littleEndian === isPlatformLittleEndian() || hdr.numBitsPerVoxel <= 8) {
return
}
if (hdr.numBitsPerVoxel === 16) {
// inspired by https://github.com/rii-mango/Papaya
const u16 = new Uint16Array(imgRaw)
for (let i = 0; i < u16.length; i++) {
const val = u16[i]
u16[i] = ((((val & 0xff) << 8) | ((val >> 8) & 0xff)) << 16) >> 16 // since JS uses 32-bit when bit shifting
}
} else if (hdr.numBitsPerVoxel === 32) {
// inspired by https://github.com/rii-mango/Papaya
const u32 = new Uint32Array(imgRaw)
for (let i = 0; i < u32.length; i++) {
const val = u32[i]
u32[i] = ((val & 0xff) << 24) | ((val & 0xff00) << 8) | ((val >> 8) & 0xff00) | ((val >> 24) & 0xff)
}
} else if (hdr.numBitsPerVoxel === 64) {
// inspired by MIT licensed code: https://github.com/rochars/endianness
const numBytesPerVoxel = hdr.numBitsPerVoxel / 8
const u8 = new Uint8Array(imgRaw)
for (let index = 0; index < u8.length; index += numBytesPerVoxel) {
let offset = numBytesPerVoxel - 1
for (let x = 0; x < offset; x++) {
const theByte = u8[index + x]
u8[index + x] = u8[index + offset]
u8[index + offset] = theByte
offset--
}
}
}
}
/**
* Convert raw image data to the appropriate TypedArray based on NIfTI datatype code.
* Some data types are converted to simpler representations (e.g., INT8 → INT16).
*
* @param imgRaw - Raw image data buffer (after endian swapping if needed)
* @param hdr - NIfTI header containing datatype information
* @returns Conversion result with typed array and any header updates
* @throws Error if datatype is not supported
*/
export function convertDataType(imgRaw: ArrayBufferLike, hdr: NIFTI1 | NIFTI2): DataTypeConversionResult {
switch (hdr.datatypeCode) {
case NiiDataType.DT_UINT8:
return { img: new Uint8Array(imgRaw) }
case NiiDataType.DT_INT16:
return { img: new Int16Array(imgRaw) }
case NiiDataType.DT_FLOAT32:
return { img: new Float32Array(imgRaw) }
case NiiDataType.DT_FLOAT64:
return { img: new Float64Array(imgRaw) }
case NiiDataType.DT_RGB24:
return { img: new Uint8Array(imgRaw) }
case NiiDataType.DT_UINT16:
return { img: new Uint16Array(imgRaw) }
case NiiDataType.DT_RGBA32:
return { img: new Uint8Array(imgRaw) }
case NiiDataType.DT_INT8: {
// Convert INT8 to INT16 for easier handling
const i8 = new Int8Array(imgRaw)
const vx8 = i8.length
const img = new Int16Array(vx8)
for (let i = 0; i < vx8; i++) {
img[i] = i8[i]
}
return {
img,
updatedDatatypeCode: NiiDataType.DT_INT16,
updatedNumBitsPerVoxel: 16
}
}
case NiiDataType.DT_BINARY: {
// Convert binary (bit-packed) to UINT8
const nvox = hdr.dims[1] * hdr.dims[2] * Math.max(1, hdr.dims[3]) * Math.max(1, hdr.dims[4])
const img1 = new Uint8Array(imgRaw)
const img = new Uint8Array(nvox)
const lut = new Uint8Array(8)
for (let i = 0; i < 8; i++) {
lut[i] = Math.pow(2, i)
}
let i1 = -1
for (let i = 0; i < nvox; i++) {
const bit = i % 8
if (bit === 0) {
i1++
}
if ((img1[i1] & lut[bit]) !== 0) {
img[i] = 1
}
}
return {
img,
updatedDatatypeCode: NiiDataType.DT_UINT8,
updatedNumBitsPerVoxel: 8
}
}
case NiiDataType.DT_UINT32: {
// Convert UINT32 to FLOAT64 (JavaScript number precision)
const u32 = new Uint32Array(imgRaw)
const vx32 = u32.length
const img = new Float64Array(vx32)
for (let i = 0; i < vx32 - 1; i++) {
img[i] = u32[i]
}
return {
img,
updatedDatatypeCode: NiiDataType.DT_FLOAT64
}
}
case NiiDataType.DT_INT32: {
// Convert INT32 to FLOAT64 (JavaScript number precision)
const i32 = new Int32Array(imgRaw)
const vxi32 = i32.length
const img = new Float64Array(vxi32)
for (let i = 0; i < vxi32 - 1; i++) {
img[i] = i32[i]
}
return {
img,
updatedDatatypeCode: NiiDataType.DT_FLOAT64
}
}
case NiiDataType.DT_INT64: {
// Convert INT64 to FLOAT64 (JavaScript number precision)
const i64 = new BigInt64Array(imgRaw)
const vx = i64.length
const img = new Float64Array(vx)
for (let i = 0; i < vx - 1; i++) {
img[i] = Number(i64[i])
}
return {
img,
updatedDatatypeCode: NiiDataType.DT_FLOAT64
}
}
case NiiDataType.DT_COMPLEX64: {
// Saved as real/imaginary pairs: show real following fsleyes/MRIcroGL convention
const f32 = new Float32Array(imgRaw)
const nvx = Math.floor(f32.length / 2)
const imaginary = new Float32Array(nvx)
const img = new Float32Array(nvx)
let r = 0
for (let i = 0; i < nvx - 1; i++) {
img[i] = f32[r]
imaginary[i] = f32[r + 1]
r += 2
}
return {
img,
imaginary,
updatedDatatypeCode: NiiDataType.DT_FLOAT32
}
}
default:
throw new Error('datatype ' + hdr.datatypeCode + ' not supported')
}
}