22
33from pathlib import Path
44
5- import h5py
6- import nibabel as nb
75import nitransforms as nt
8- import numpy as np
9- from nitransforms .io .itk import ITKCompositeH5
10- from transforms3d .affines import compose as compose_affine
116
127
138def load_transforms (xfm_paths : list [Path ], inverse : list [bool ]) -> nt .base .TransformBase :
@@ -24,7 +19,8 @@ def load_transforms(xfm_paths: list[Path], inverse: list[bool]) -> nt.base.Trans
2419 for path , inv in zip (xfm_paths [::- 1 ], inverse [::- 1 ], strict = False ):
2520 path = Path (path )
2621 if path .suffix == '.h5' :
27- xfm = load_ants_h5 (path )
22+ # Load as a TransformChain
23+ xfm = nt .manip .load (path )
2824 else :
2925 xfm = nt .linear .load (path )
3026 if inv :
@@ -34,72 +30,5 @@ def load_transforms(xfm_paths: list[Path], inverse: list[bool]) -> nt.base.Trans
3430 else :
3531 chain += xfm
3632 if chain is None :
37- chain = nt .base . TransformBase ()
33+ chain = nt .Affine () # Identity
3834 return chain
39-
40-
41- def load_ants_h5 (filename : Path ) -> nt .base .TransformBase :
42- """Load ANTs H5 files as a nitransforms TransformChain"""
43- # Borrowed from https://github.com/feilong/process
44- # process.resample.parse_combined_hdf5()
45- #
46- # Changes:
47- # * Tolerate a missing displacement field
48- # * Return the original affine without a round-trip
49- # * Always return a nitransforms TransformBase
50- # * Construct warp affine from fixed parameters
51- #
52- # This should be upstreamed into nitransforms
53- h = h5py .File (filename )
54- xform = ITKCompositeH5 .from_h5obj (h )
55-
56- # nt.Affine
57- transforms = [nt .Affine (xform [0 ].to_ras ())]
58-
59- if '2' not in h ['TransformGroup' ]:
60- return transforms [0 ]
61-
62- transform2 = h ['TransformGroup' ]['2' ]
63-
64- # Confirm these transformations are applicable
65- if transform2 ['TransformType' ][:][0 ] not in (
66- b'DisplacementFieldTransform_float_3_3' ,
67- b'DisplacementFieldTransform_double_3_3' ,
68- ):
69- msg = 'Unknown transform type [2]\n '
70- for i in h ['TransformGroup' ].keys ():
71- msg += f'[{ i } ]: { h ["TransformGroup" ][i ]["TransformType" ][:][0 ]} \n '
72- raise ValueError (msg )
73-
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 )))
105- return nt .TransformChain (transforms )
0 commit comments