@niivue/niivue
Version:
minimal webgl2 nifti image viewer
109 lines (105 loc) • 4.06 kB
text/typescript
/**
* TensorProcessing module
*
* Handles diffusion tensor and vector field processing.
* This module contains functions for:
* - Converting vector fields to RGBA representation
* - Loading and processing V1 vector data with optional flips
*/
import type { NVImage } from './index'
import { log } from '@/logger'
import { NiiDataType } from '@/nvimage/utils'
/**
* Convert vector field from Float32 to RGBA representation.
* Note: We use RGBA rather than RGB and use least significant bits to store vector polarity.
* This allows a single bitmap to store BOTH (unsigned) color magnitude and signed vector direction.
*
* @param nvImage - The NVImage instance
* @param inImg - Input Float32Array containing vector field data
* @returns Uint8Array with RGBA encoded vector data
*/
export function float32V1asRGBA(nvImage: NVImage, inImg: Float32Array): Uint8Array {
if (inImg.length !== nvImage.nVox3D * 3) {
log.warn('float32V1asRGBA() expects ' + nvImage.nVox3D * 3 + 'voxels, got ', +inImg.length)
}
const f32 = inImg.slice()
// Note we will use RGBA rather than RGB and use least significant bits to store vector polarity
// this allows a single bitmap to store BOTH (unsigned) color magnitude and signed vector direction
nvImage.hdr.datatypeCode = NiiDataType.DT_RGBA32
nvImage.nFrame4D = 1
for (let i = 4; i < 7; i++) {
nvImage.hdr.dims[i] = 1
}
nvImage.hdr.dims[0] = 3 // 3D
const imgRaw = new Uint8Array(nvImage.nVox3D * 4) //* 3 for RGB
let mx = 1.0
for (let i = 0; i < nvImage.nVox3D * 3; i++) {
// n.b. NaN values created by dwi2tensor and tensor2metric tensors.mif -vector v1.mif
if (isNaN(f32[i])) {
continue
}
mx = Math.max(mx, Math.abs(f32[i]))
}
const slope = 255 / mx
const nVox3D2 = nvImage.nVox3D * 2
let j = 0
for (let i = 0; i < nvImage.nVox3D; i++) {
// n.b. it is really necessary to overwrite imgRaw with a new datatype mid-method
const x = f32[i]
const y = f32[i + nvImage.nVox3D]
const z = f32[i + nVox3D2]
;(imgRaw as Uint8Array)[j] = Math.abs(x * slope)
;(imgRaw as Uint8Array)[j + 1] = Math.abs(y * slope)
;(imgRaw as Uint8Array)[j + 2] = Math.abs(z * slope)
const xNeg = Number(x > 0) * 1
const yNeg = Number(y > 0) * 2
const zNeg = Number(z > 0) * 4
let alpha = 248 + xNeg + yNeg + zNeg
if (Math.abs(x) + Math.abs(y) + Math.abs(z) < 0.1) {
alpha = 0
}
;(imgRaw as Uint8Array)[j + 3] = alpha
j += 4
}
return imgRaw
}
/**
* Load and process diffusion tensor vector (V1) data with optional flips.
* The vectors must be of unit length.
* Modifies the nvImage.img property with the processed RGBA data.
*
* @param nvImage - The NVImage instance
* @param isFlipX - Flip X component (default: false)
* @param isFlipY - Flip Y component (default: false)
* @param isFlipZ - Flip Z component (default: false)
* @example nv1.loadVolumes(volumeList); nv1.volumes[1].loadImgV1();
* @returns true if successful, false if V1 data is not available
* @see {@link https://niivue.com/demos/features/modulate.html | live demo usage}
*/
export function loadImgV1(nvImage: NVImage, isFlipX: boolean = false, isFlipY: boolean = false, isFlipZ: boolean = false): boolean {
let v1 = nvImage.v1
if (!v1 && nvImage.nFrame4D === 3 && nvImage.img.constructor === Float32Array) {
v1 = nvImage.img.slice()
}
if (!v1) {
log.warn('Image does not have V1 data')
return false
}
if (isFlipX) {
for (let i = 0; i < nvImage.nVox3D; i++) {
v1[i] = -v1[i]
}
}
if (isFlipY) {
for (let i = nvImage.nVox3D; i < 2 * nvImage.nVox3D; i++) {
v1[i] = -v1[i]
}
}
if (isFlipZ) {
for (let i = 2 * nvImage.nVox3D; i < 3 * nvImage.nVox3D; i++) {
v1[i] = -v1[i]
}
}
nvImage.img = float32V1asRGBA(nvImage, v1)
return true
}