2
2
3
3
from pathlib import Path
4
4
5
- import h5py
6
- import nibabel as nb
7
5
import nitransforms as nt
8
- import numpy as np
9
- from nitransforms .io .itk import ITKCompositeH5
10
- from transforms3d .affines import compose as compose_affine
11
6
12
7
13
8
def 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
24
19
for path , inv in zip (xfm_paths [::- 1 ], inverse [::- 1 ], strict = False ):
25
20
path = Path (path )
26
21
if path .suffix == '.h5' :
27
- xfm = load_ants_h5 (path )
22
+ # Load as a TransformChain
23
+ xfm = nt .manip .load (path )
28
24
else :
29
25
xfm = nt .linear .load (path )
30
26
if inv :
@@ -34,72 +30,5 @@ def load_transforms(xfm_paths: list[Path], inverse: list[bool]) -> nt.base.Trans
34
30
else :
35
31
chain += xfm
36
32
if chain is None :
37
- chain = nt .base . TransformBase ()
33
+ chain = nt .Affine () # Identity
38
34
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