@niivue/niivue
Version:
minimal webgl2 nifti image viewer
164 lines (140 loc) • 5.2 kB
text/typescript
/**
* AffineProcessor module
*
* Handles affine matrix processing for NIfTI images:
* - Validates pixel dimensions
* - Calculates affine matrix from QForm quaternion parameters
* - Repairs defective affine matrices
* - Validates scale/intercept values
*
* The affine matrix defines the spatial transformation from voxel coordinates
* to world (scanner) coordinates.
*/
import { NIFTI1, NIFTI2 } from 'nifti-reader-js'
import { log } from '@/logger'
import { isAffineOK } from '@/nvimage/utils'
/**
* Validate and fix pixel dimensions in NIfTI header.
* Ensures all spatial pixel dimensions are non-zero.
*
* @param hdr - NIfTI header to validate
*/
export function validatePixelDimensions(hdr: NIFTI1 | NIFTI2): void {
if (hdr.pixDims[1] === 0.0 || hdr.pixDims[2] === 0.0 || hdr.pixDims[3] === 0.0) {
log.error('pixDims not plausible', hdr)
}
}
/**
* Validate and fix scale/intercept values in NIfTI header.
* Ensures scl_slope is non-zero and scl_inter is defined.
*
* @param hdr - NIfTI header to validate
*/
export function validateScaleIntercept(hdr: NIFTI1 | NIFTI2): void {
// https://github.com/nipreps/fmriprep/issues/2507
if (isNaN(hdr.scl_slope) || hdr.scl_slope === 0.0) {
hdr.scl_slope = 1.0
}
if (isNaN(hdr.scl_inter)) {
hdr.scl_inter = 0.0
}
}
/**
* Calculate affine matrix from QForm quaternion parameters.
* The QForm method uses quaternion rotation parameters to define the spatial transform.
* This is used when useQFormNotSForm is true, or when the affine is invalid,
* or when qform_code > sform_code.
*
* @param hdr - NIfTI header containing quaternion parameters
* @param useQFormNotSForm - Force use of QForm even if SForm is valid
*/
export function calculateAffineFromQForm(hdr: NIFTI1 | NIFTI2, useQFormNotSForm: boolean): void {
const affineOK = isAffineOK(hdr.affine)
if (!useQFormNotSForm && affineOK && hdr.qform_code <= hdr.sform_code) {
// SForm is valid and preferred, no need to calculate from QForm
return
}
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 convenience
const b = hdr.quatern_b
const c = hdr.quatern_c
const d = 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 = hdr.pixDims[0] === 0 ? 1 : 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 = 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] * hdr.pixDims[ctrIn + 1]
if (ctrIn === 2) {
affine[ctrOut][ctrIn] *= qfac
}
}
}
// The last row of affine matrix is the offset vector
affine[0][3] = hdr.qoffset_x
affine[1][3] = hdr.qoffset_y
affine[2][3] = hdr.qoffset_z
hdr.affine = affine
}
/**
* Repair defective affine matrix by creating a simple diagonal matrix
* from pixel dimensions. This is a fallback when both QForm and SForm
* produce invalid affine matrices.
*
* @param hdr - NIfTI header with defective affine matrix
*/
export function repairDefectiveAffine(hdr: NIFTI1 | NIFTI2): void {
if (isAffineOK(hdr.affine)) {
// Affine is already valid, no repair needed
return
}
log.debug('Defective NIfTI: spatial transform does not make sense')
let x = hdr.pixDims[1]
let y = hdr.pixDims[2]
let z = 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
}
hdr.pixDims[1] = x
hdr.pixDims[2] = y
hdr.pixDims[3] = z
const affine = [
[x, 0, 0, 0],
[0, y, 0, 0],
[0, 0, z, 0],
[0, 0, 0, 1]
]
hdr.affine = affine
}
/**
* Process NIfTI affine matrix: validate, calculate from QForm if needed, and repair if defective.
* This is the main entry point that coordinates all affine processing steps.
*
* @param hdr - NIfTI header to process
* @param useQFormNotSForm - Prefer QForm over SForm for spatial transform
*/
export function processAffine(hdr: NIFTI1 | NIFTI2, useQFormNotSForm: boolean): void {
// Step 1: Validate pixel dimensions
validatePixelDimensions(hdr)
// Step 2: Validate scale/intercept
validateScaleIntercept(hdr)
// Step 3: Calculate affine from QForm if needed
calculateAffineFromQForm(hdr, useQFormNotSForm)
// Step 4: Repair affine if still defective
repairDefectiveAffine(hdr)
}