@niivue/niivue
Version:
minimal webgl2 nifti image viewer
94 lines (90 loc) • 3.56 kB
text/typescript
import { NIFTI1 } from 'nifti-reader-js'
import type { NVImage } from '@/nvimage'
import { NiiDataType } from '@/nvimage/utils'
/**
* Helper function to convert ZXY RGB data to XYZ format for Zarr arrays.
* @param data - Input data in ZXY order
* @param X - Width dimension
* @param Y - Height dimension
* @param Z - Depth dimension
* @returns Uint8Array in XYZ order
*/
function zxy2xyz(data: any, X: number, Y: number, Z: number): Uint8Array {
const voxelCount = X * Y
const rgb = new Uint8Array(voxelCount * Z * 3)
const offsets = new Array(Z)
for (let s = 0; s < Z; s++) {
offsets[s] = voxelCount * 3 * s
}
let srcIndex = 0
let dstIndex = 0
for (let v = 0; v < voxelCount; v++) {
for (let s = 0; s < Z; s++) {
rgb[offsets[s] + dstIndex] = data[srcIndex++] // R
rgb[offsets[s] + dstIndex + 1] = data[srcIndex++] // G
rgb[offsets[s] + dstIndex + 2] = data[srcIndex++] // B
}
dstIndex += 3
}
return rgb
}
/**
* Reads Zarr multi-dimensional array format, modifying the provided
* NVImage header and returning the raw image data buffer.
*
* Format specification: https://zarrita.dev/get-started.html
*
* Note: RGB channels may be returned as depth dimension.
*
* @param nvImage - The NVImage instance whose header will be modified.
* @param buffer - ArrayBuffer containing the Zarr file data (unused, kept for consistency).
* @param zarrData - Parsed Zarr data object containing width, height, depth, and data.
* @returns Promise resolving to ArrayBuffer containing the image data.
* @throws Error if data dimensions don't match expected size.
*/
export async function readZARR(nvImage: NVImage, buffer: ArrayBuffer, zarrData: unknown): Promise<ArrayBufferLike> {
let { width, height, depth = 1, data } = (zarrData ?? {}) as any
let expectedLength = width * height * depth * 3
let isRGB = expectedLength === data.length
if (!isRGB) {
expectedLength = width * height * depth
if (depth === 3) {
// see https://zarrita.dev/get-started.html R,G,B channels returns as depth!
isRGB = true
depth = 1
}
}
if (expectedLength !== data.length) {
throw new Error(`Expected RGB ${width}×${height}×${depth}×3 = ${expectedLength}, but ZARR length ${data.length}`)
}
nvImage.hdr = new NIFTI1()
const hdr = nvImage.hdr
hdr.dims = [3, width, height, depth, 1, 1, 1, 1]
hdr.pixDims = [1, 1, 1, 1, 0, 0, 0, 0]
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]
]
if (!isRGB) {
hdr.numBitsPerVoxel = 8
hdr.datatypeCode = NiiDataType.DT_UINT8
// if data is a Uint8Array, convert to ArrayBuffer
if (data instanceof Uint8Array) {
const retBuffer = new ArrayBuffer(data.length)
const retView = new Uint8Array(retBuffer)
retView.set(data)
return retBuffer
}
return data
}
hdr.numBitsPerVoxel = 24
hdr.datatypeCode = NiiDataType.DT_RGB24
const retData = zxy2xyz(data, hdr.dims[1], hdr.dims[2], hdr.dims[3])
// convert retData Uint8Array to ArrayBuffer
const retBuffer = new ArrayBuffer(retData.length)
const retView = new Uint8Array(retBuffer)
retView.set(retData)
return retBuffer
}