Skip to content

Commit 241728d

Browse files
authored
Merge pull request #297 from effigies/fix/bad-qform
fix: Set nifti_header.axis_codes to null for bad qforms, rather than error
2 parents c8fcef3 + 272299b commit 241728d

File tree

3 files changed

+38
-23
lines changed

3 files changed

+38
-23
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
### Fixed
2+
3+
- NIfTI files with bad qform matrices, resulting from non-normalized quaternions,
4+
would previously raise a NIFTI_HEADER_UNREADABLE error. Now only the axis codes
5+
are disabled, preventing orientation checks, but not raising errors.

src/files/nifti.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ Deno.test('Test extracting axis codes', async (t) => {
9696
const affine = [[0, 0, 1, 0], [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1]]
9797
assertEquals(axisCodes(affine), ['A', 'S', 'R'])
9898
})
99+
await t.step('Fail gracefully on NaNs', async () => {
100+
const affine = [[Number.NaN, 0, 1, 0], [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1]]
101+
assertEquals(axisCodes(affine), null)
102+
})
99103
})
100104

101105
testAsyncFileAccess('Test file access errors for loadHeader', loadHeader)

src/files/nifti.ts

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ async function extract(buffer: Uint8Array, nbytes: number): Promise<Uint8Array<A
3434

3535
export async function loadHeader(file: BIDSFile): Promise<NiftiHeader> {
3636
const buf = await readBytes(file, 1024)
37+
let header
3738
try {
3839
const data = isCompressed(buf.buffer) ? await extract(buf, 540) : buf.slice(0, 540)
39-
let header
4040
if (isNIFTI1(data.buffer)) {
4141
header = new NIFTI1()
4242
// Truncate to 348 bytes to avoid attempting to parse extensions
@@ -48,29 +48,30 @@ export async function loadHeader(file: BIDSFile): Promise<NiftiHeader> {
4848
if (!header) {
4949
throw { code: 'NIFTI_HEADER_UNREADABLE' }
5050
}
51-
const ndim = header.dims[0]
52-
return {
53-
dim: header.dims,
54-
// Hack: round pixdim to 3 decimal places; schema should add rounding function
55-
pixdim: header.pixDims.map((pixdim) => Math.round(pixdim * 1000) / 1000),
56-
shape: header.dims.slice(1, ndim + 1),
57-
voxel_sizes: header.pixDims.slice(1, ndim + 1),
58-
dim_info: {
59-
freq: header.dim_info & 0x03,
60-
phase: (header.dim_info >> 2) & 0x03,
61-
slice: (header.dim_info >> 4) & 0x03,
62-
},
63-
xyzt_units: {
64-
xyz: ['unknown', 'meter', 'mm', 'um'][header.xyzt_units & 0x03],
65-
t: ['unknown', 'sec', 'msec', 'usec'][(header.xyzt_units >> 3) & 0x03],
66-
},
67-
qform_code: header.qform_code,
68-
sform_code: header.sform_code,
69-
axis_codes: axisCodes(header.affine),
70-
} as NiftiHeader
7151
} catch (err) {
7252
throw { code: 'NIFTI_HEADER_UNREADABLE' }
7353
}
54+
55+
const ndim = header.dims[0]
56+
return {
57+
dim: header.dims,
58+
// Hack: round pixdim to 3 decimal places; schema should add rounding function
59+
pixdim: header.pixDims.map((pixdim) => Math.round(pixdim * 1000) / 1000),
60+
shape: header.dims.slice(1, ndim + 1),
61+
voxel_sizes: header.pixDims.slice(1, ndim + 1),
62+
dim_info: {
63+
freq: header.dim_info & 0x03,
64+
phase: (header.dim_info >> 2) & 0x03,
65+
slice: (header.dim_info >> 4) & 0x03,
66+
},
67+
xyzt_units: {
68+
xyz: ['unknown', 'meter', 'mm', 'um'][header.xyzt_units & 0x03],
69+
t: ['unknown', 'sec', 'msec', 'usec'][(header.xyzt_units >> 3) & 0x03],
70+
},
71+
qform_code: header.qform_code,
72+
sform_code: header.sform_code,
73+
axis_codes: axisCodes(header.affine),
74+
} as NiftiHeader
7475
}
7576

7677
/** Vector addition */
@@ -124,13 +125,18 @@ function argMax(arr: number[]): number {
124125
*
125126
* @returns character codes describing the orientation of an image affine.
126127
*/
127-
export function axisCodes(affine: number[][]): string[] {
128+
export function axisCodes(affine: number[][]): string[] | null {
128129
// This function is an extract of the Python function transforms3d.affines.decompose44
129130
// (https://github.com/matthew-brett/transforms3d/blob/6a43a98/transforms3d/affines.py#L10-L153)
130131
//
131132
// As an optimization, this only orthogonalizes the basis,
132133
// and does not normalize to unit vectors.
133134

135+
// Bad qforms result in NaNs in the rotation matrix
136+
if (affine.some((row) => row.some((val) => !Number.isFinite(val)))) {
137+
return null
138+
}
139+
134140
// Operate on columns, which are the cosines that project input coordinates onto output axes
135141
const [cosX, cosY, cosZ] = [0, 1, 2].map((j) => [0, 1, 2].map((i) => affine[i][j]))
136142

@@ -148,7 +154,7 @@ export function axisCodes(affine: number[][]): string[] {
148154

149155
// Check that indices are 0, 1 and 2 in some order
150156
if (maxIndices.toSorted().some((idx, i) => idx !== i)) {
151-
throw { key: 'AMBIGUOUS_AFFINE' }
157+
throw { code: 'AMBIGUOUS_AFFINE' }
152158
}
153159

154160
// Positive/negative codes for each world axis

0 commit comments

Comments
 (0)