Skip to content

Commit 6c4cb04

Browse files
effigiesmgxd
andauthored
RF: Fix ITK warp conversion to nitransforms format (#3300)
Alternative to (and builds on) #3296. Closes #3296. --------- Co-authored-by: mathiasg <[email protected]> Co-authored-by: Mathias Goncalves <[email protected]>
1 parent 604eeef commit 6c4cb04

File tree

2 files changed

+35
-32
lines changed

2 files changed

+35
-32
lines changed

fmriprep/utils/transforms.py

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
"""Utilities for loading transforms for resampling"""
22

3-
import warnings
43
from pathlib import Path
54

65
import h5py
76
import nibabel as nb
87
import nitransforms as nt
98
import numpy as np
109
from nitransforms.io.itk import ITKCompositeH5
10+
from transforms3d.affines import compose as compose_affine
1111

1212

1313
def load_transforms(xfm_paths: list[Path], inverse: list[bool]) -> nt.base.TransformBase:
@@ -38,16 +38,6 @@ def load_transforms(xfm_paths: list[Path], inverse: list[bool]) -> nt.base.Trans
3838
return chain
3939

4040

41-
FIXED_PARAMS = np.array([
42-
193.0, 229.0, 193.0, # Size
43-
96.0, 132.0, -78.0, # Origin
44-
1.0, 1.0, 1.0, # Spacing
45-
-1.0, 0.0, 0.0, # Directions
46-
0.0, -1.0, 0.0,
47-
0.0, 0.0, 1.0,
48-
]) # fmt:skip
49-
50-
5141
def load_ants_h5(filename: Path) -> nt.base.TransformBase:
5242
"""Load ANTs H5 files as a nitransforms TransformChain"""
5343
# Borrowed from https://github.com/feilong/process
@@ -56,7 +46,8 @@ def load_ants_h5(filename: Path) -> nt.base.TransformBase:
5646
# Changes:
5747
# * Tolerate a missing displacement field
5848
# * Return the original affine without a round-trip
59-
# * Always return a nitransforms TransformChain
49+
# * Always return a nitransforms TransformBase
50+
# * Construct warp affine from fixed parameters
6051
#
6152
# This should be upstreamed into nitransforms
6253
h = h5py.File(filename)
@@ -80,24 +71,35 @@ def load_ants_h5(filename: Path) -> nt.base.TransformBase:
8071
msg += f'[{i}]: {h["TransformGroup"][i]["TransformType"][:][0]}\n'
8172
raise ValueError(msg)
8273

83-
fixed_params = transform2['TransformFixedParameters'][:]
84-
if not np.array_equal(fixed_params, FIXED_PARAMS):
85-
msg = 'Unexpected fixed parameters\n'
86-
msg += f'Expected: {FIXED_PARAMS}\n'
87-
msg += f'Found: {fixed_params}'
88-
if not np.array_equal(fixed_params[6:], FIXED_PARAMS[6:]):
89-
raise ValueError(msg)
90-
warnings.warn(msg, stacklevel=1)
91-
92-
shape = tuple(fixed_params[:3].astype(int))
93-
warp = h['TransformGroup']['2']['TransformParameters'][:]
94-
warp = warp.reshape((*shape, 3)).transpose(2, 1, 0, 3)
95-
warp *= np.array([-1, -1, 1])
96-
97-
warp_affine = np.eye(4)
98-
warp_affine[:3, :3] = fixed_params[9:].reshape((3, 3))
99-
warp_affine[:3, 3] = fixed_params[3:6]
100-
lps_to_ras = np.eye(4) * np.array([-1, -1, 1, 1])
101-
warp_affine = lps_to_ras @ warp_affine
102-
transforms.insert(0, nt.DenseFieldTransform(nb.Nifti1Image(warp, warp_affine)))
74+
# Warp field fixed parameters as defined in
75+
# https://itk.org/Doxygen/html/classitk_1_1DisplacementFieldTransform.html
76+
shape = transform2['TransformFixedParameters'][:3]
77+
origin = transform2['TransformFixedParameters'][3:6]
78+
spacing = transform2['TransformFixedParameters'][6:9]
79+
direction = transform2['TransformFixedParameters'][9:].reshape((3, 3))
80+
81+
# We are not yet confident that we handle non-unit spacing
82+
# or direction cosine ordering correctly.
83+
# If we confirm or fix, we can remove these checks.
84+
if not np.allclose(spacing, 1):
85+
raise ValueError(f'Unexpected spacing: {spacing}')
86+
if not np.allclose(direction, direction.T):
87+
raise ValueError(f'Asymmetric direction matrix: {direction}')
88+
89+
# ITK uses LPS affines
90+
lps_affine = compose_affine(T=origin, R=direction, Z=spacing)
91+
ras_affine = np.diag([-1, -1, 1, 1]) @ lps_affine
92+
93+
# ITK stores warps in Fortran-order, where the vector components change fastest
94+
# Vectors are in mm LPS
95+
itk_warp = np.reshape(
96+
transform2['TransformParameters'],
97+
(3, *shape.astype(int)),
98+
order='F',
99+
)
100+
101+
# Nitransforms warps are in RAS, with the vector components changing slowest
102+
nt_warp = itk_warp.transpose(1, 2, 3, 0) * np.array([-1, -1, 1])
103+
104+
transforms.insert(0, nt.DenseFieldTransform(nb.Nifti1Image(nt_warp, ras_affine)))
103105
return nt.TransformChain(transforms)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ dependencies = [
3636
"smriprep @ git+https://github.com/nipreps/smriprep.git@master",
3737
"tedana >= 23.0.2",
3838
"templateflow >= 24.1.0",
39+
"transforms3d",
3940
"toml",
4041
"codecarbon",
4142
"APScheduler",

0 commit comments

Comments
 (0)