1
1
"""Utilities for loading transforms for resampling"""
2
2
3
- import warnings
4
3
from pathlib import Path
5
4
6
5
import h5py
7
6
import nibabel as nb
8
7
import nitransforms as nt
9
8
import numpy as np
10
9
from nitransforms .io .itk import ITKCompositeH5
10
+ from transforms3d .affines import compose as compose_affine
11
11
12
12
13
13
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
38
38
return chain
39
39
40
40
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
-
51
41
def load_ants_h5 (filename : Path ) -> nt .base .TransformBase :
52
42
"""Load ANTs H5 files as a nitransforms TransformChain"""
53
43
# Borrowed from https://github.com/feilong/process
@@ -56,7 +46,8 @@ def load_ants_h5(filename: Path) -> nt.base.TransformBase:
56
46
# Changes:
57
47
# * Tolerate a missing displacement field
58
48
# * 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
60
51
#
61
52
# This should be upstreamed into nitransforms
62
53
h = h5py .File (filename )
@@ -80,24 +71,35 @@ def load_ants_h5(filename: Path) -> nt.base.TransformBase:
80
71
msg += f'[{ i } ]: { h ["TransformGroup" ][i ]["TransformType" ][:][0 ]} \n '
81
72
raise ValueError (msg )
82
73
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 )))
103
105
return nt .TransformChain (transforms )
0 commit comments