4
4
5
5
import os
6
6
import shutil
7
+ from subprocess import check_call
7
8
8
9
import pytest
9
10
14
15
from scipy .io import loadmat
15
16
from h5py import File as H5File
16
17
17
- from nitransforms .io .base import TransformIOError , TransformFileError
18
18
19
- from nitransforms .io import itk
19
+ from nitransforms .io .base import TransformIOError , TransformFileError
20
+ from nitransforms .io .itk import (
21
+ ITKLinearTransform ,
22
+ ITKLinearTransformArray ,
23
+ ITKDisplacementsField ,
24
+ ITKCompositeH5 ,
25
+ )
26
+ from nitransforms import nonlinear as nitnl
20
27
from nitransforms .conftest import _datadir , _testdir
28
+ from nitransforms .tests .utils import get_points
29
+
30
+ rng = np .random .default_rng ()
21
31
22
32
LPS = np .diag ([- 1 , - 1 , 1 , 1 ])
23
33
ITK_MAT = LPS .dot (np .ones ((4 , 4 )).dot (LPS ))
@@ -29,14 +39,14 @@ def test_ITKLinearTransform(tmpdir, testdata_path):
29
39
matlabfile = testdata_path / "ds-005_sub-01_from-T1_to-OASIS_affine.mat"
30
40
mat = loadmat (str (matlabfile ))
31
41
with open (str (matlabfile ), "rb" ) as f :
32
- itkxfm = itk . ITKLinearTransform .from_fileobj (f )
42
+ itkxfm = ITKLinearTransform .from_fileobj (f )
33
43
assert np .allclose (
34
44
itkxfm ["parameters" ][:3 , :3 ].flatten (),
35
45
mat ["AffineTransform_float_3_3" ][:- 3 ].flatten (),
36
46
)
37
47
assert np .allclose (itkxfm ["offset" ], mat ["fixed" ].reshape ((3 ,)))
38
48
39
- itkxfm = itk . ITKLinearTransform .from_filename (matlabfile )
49
+ itkxfm = ITKLinearTransform .from_filename (matlabfile )
40
50
assert np .allclose (
41
51
itkxfm ["parameters" ][:3 , :3 ].flatten (),
42
52
mat ["AffineTransform_float_3_3" ][:- 3 ].flatten (),
@@ -46,17 +56,17 @@ def test_ITKLinearTransform(tmpdir, testdata_path):
46
56
# Test to_filename(textfiles)
47
57
itkxfm .to_filename ("textfile.tfm" )
48
58
with open ("textfile.tfm" ) as f :
49
- itkxfm2 = itk . ITKLinearTransform .from_fileobj (f )
59
+ itkxfm2 = ITKLinearTransform .from_fileobj (f )
50
60
assert np .allclose (itkxfm ["parameters" ], itkxfm2 ["parameters" ])
51
61
52
62
# Test to_filename(matlab)
53
63
itkxfm .to_filename ("copy.mat" )
54
64
with open ("copy.mat" , "rb" ) as f :
55
- itkxfm3 = itk . ITKLinearTransform .from_fileobj (f )
65
+ itkxfm3 = ITKLinearTransform .from_fileobj (f )
56
66
assert np .all (itkxfm ["parameters" ] == itkxfm3 ["parameters" ])
57
67
58
68
rasmat = from_matvec (euler2mat (x = 0.9 , y = 0.001 , z = 0.001 ), [4.0 , 2.0 , - 1.0 ])
59
- itkxfm = itk . ITKLinearTransform .from_ras (rasmat )
69
+ itkxfm = ITKLinearTransform .from_ras (rasmat )
60
70
assert np .allclose (itkxfm ["parameters" ], ITK_MAT * rasmat )
61
71
assert np .allclose (itkxfm .to_ras (), rasmat )
62
72
@@ -67,9 +77,9 @@ def test_ITKLinearTransformArray(tmpdir, data_path):
67
77
with open (str (data_path / "itktflist.tfm" )) as f :
68
78
text = f .read ()
69
79
f .seek (0 )
70
- itklist = itk . ITKLinearTransformArray .from_fileobj (f )
80
+ itklist = ITKLinearTransformArray .from_fileobj (f )
71
81
72
- itklistb = itk . ITKLinearTransformArray .from_filename (data_path / "itktflist.tfm" )
82
+ itklistb = ITKLinearTransformArray .from_filename (data_path / "itktflist.tfm" )
73
83
assert itklist ["nxforms" ] == itklistb ["nxforms" ]
74
84
assert all (
75
85
[
@@ -85,14 +95,14 @@ def test_ITKLinearTransformArray(tmpdir, data_path):
85
95
assert itklist ["nxforms" ] == 9
86
96
assert text == itklist .to_string ()
87
97
with pytest .raises (TransformFileError ):
88
- itk . ITKLinearTransformArray .from_string ("\n " .join (text .splitlines ()[1 :]))
98
+ ITKLinearTransformArray .from_string ("\n " .join (text .splitlines ()[1 :]))
89
99
90
100
itklist .to_filename ("copy.tfm" )
91
101
with open ("copy.tfm" ) as f :
92
102
copytext = f .read ()
93
103
assert text == copytext
94
104
95
- itklist = itk . ITKLinearTransformArray (
105
+ itklist = ITKLinearTransformArray (
96
106
xforms = [np .random .normal (size = (4 , 4 )) for _ in range (4 )]
97
107
)
98
108
@@ -108,7 +118,7 @@ def test_ITKLinearTransformArray(tmpdir, data_path):
108
118
f .write (xfm .to_string ())
109
119
110
120
with open ("extracted.tfm" ) as f :
111
- xfm2 = itk . ITKLinearTransform .from_fileobj (f )
121
+ xfm2 = ITKLinearTransform .from_fileobj (f )
112
122
assert np .allclose (
113
123
xfm .structarr ["parameters" ][:3 , ...], xfm2 .structarr ["parameters" ][:3 , ...]
114
124
)
@@ -118,26 +128,26 @@ def test_ITKLinearTransformArray(tmpdir, data_path):
118
128
itklist .to_filename ("matlablist.mat" )
119
129
120
130
with pytest .raises (TransformFileError ):
121
- xfm2 = itk . ITKLinearTransformArray .from_binary ({})
131
+ xfm2 = ITKLinearTransformArray .from_binary ({})
122
132
123
133
with open ("filename.mat" , "ab" ) as f :
124
134
with pytest .raises (TransformFileError ):
125
- xfm2 = itk . ITKLinearTransformArray .from_fileobj (f )
135
+ xfm2 = ITKLinearTransformArray .from_fileobj (f )
126
136
127
137
128
138
@pytest .mark .parametrize ("size" , [(20 , 20 , 20 ), (20 , 20 , 20 , 3 )])
129
139
def test_itk_disp_load (size ):
130
140
"""Checks field sizes."""
131
141
with pytest .raises (TransformFileError ):
132
- itk . ITKDisplacementsField .from_image (
142
+ ITKDisplacementsField .from_image (
133
143
nb .Nifti1Image (np .zeros (size ), np .eye (4 ), None )
134
144
)
135
145
136
146
137
147
def test_itk_disp_load_intent ():
138
148
"""Checks whether the NIfTI intent is fixed."""
139
149
with pytest .warns (UserWarning ):
140
- field = itk . ITKDisplacementsField .from_image (
150
+ field = ITKDisplacementsField .from_image (
141
151
nb .Nifti1Image (np .zeros ((20 , 20 , 20 , 1 , 3 )), np .eye (4 ), None )
142
152
)
143
153
@@ -161,7 +171,7 @@ def test_itk_h5(tmpdir, only_linear, h5_path, nxforms):
161
171
assert (
162
172
len (
163
173
list (
164
- itk . ITKCompositeH5 .from_filename (
174
+ ITKCompositeH5 .from_filename (
165
175
h5_path ,
166
176
only_linear = only_linear ,
167
177
)
@@ -174,7 +184,7 @@ def test_itk_h5(tmpdir, only_linear, h5_path, nxforms):
174
184
175
185
with pytest .raises (TransformFileError ):
176
186
list (
177
- itk . ITKCompositeH5 .from_filename (
187
+ ITKCompositeH5 .from_filename (
178
188
h5_path .absolute ().name .replace (".h5" , ".x5" ),
179
189
only_linear = only_linear ,
180
190
)
@@ -190,30 +200,28 @@ def test_itk_h5(tmpdir, only_linear, h5_path, nxforms):
190
200
xfm ["TransformType" ][0 ] = b"InventTransform"
191
201
192
202
with pytest .raises (TransformIOError ):
193
- itk . ITKCompositeH5 .from_filename ("test.h5" )
203
+ ITKCompositeH5 .from_filename ("test.h5" )
194
204
195
205
196
206
def test_itk_linear_h5 (tmpdir , data_path , testdata_path ):
197
207
"""Check different lower-level loading options."""
198
208
199
209
# File loadable with transform array
200
- h5xfm = itk .ITKLinearTransformArray .from_filename (
201
- data_path / "affine-antsComposite.h5"
202
- )
210
+ h5xfm = ITKLinearTransformArray .from_filename (data_path / "affine-antsComposite.h5" )
203
211
assert len (h5xfm .xforms ) == 1
204
212
205
213
with open (data_path / "affine-antsComposite.h5" , "rb" ) as f :
206
- h5xfm = itk . ITKLinearTransformArray .from_fileobj (f )
214
+ h5xfm = ITKLinearTransformArray .from_fileobj (f )
207
215
assert len (h5xfm .xforms ) == 1
208
216
209
217
# File loadable with single affine object
210
- itk . ITKLinearTransform .from_filename (data_path / "affine-antsComposite.h5" )
218
+ ITKLinearTransform .from_filename (data_path / "affine-antsComposite.h5" )
211
219
212
220
with open (data_path / "affine-antsComposite.h5" , "rb" ) as f :
213
- itk . ITKLinearTransform .from_fileobj (f )
221
+ ITKLinearTransform .from_fileobj (f )
214
222
215
223
# Exercise only_linear
216
- itk . ITKCompositeH5 .from_filename (
224
+ ITKCompositeH5 .from_filename (
217
225
testdata_path
218
226
/ "ds-005_sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5" ,
219
227
only_linear = True ,
@@ -231,16 +239,16 @@ def test_itk_linear_h5(tmpdir, data_path, testdata_path):
231
239
xfm ["TransformFixedParameters" ] = np .zeros (3 , dtype = float )
232
240
233
241
# File loadable with transform array
234
- h5xfm = itk . ITKLinearTransformArray .from_filename ("test.h5" )
242
+ h5xfm = ITKLinearTransformArray .from_filename ("test.h5" )
235
243
assert len (h5xfm .xforms ) == 2
236
244
237
245
# File loadable with generalistic object (NOTE we directly access the list)
238
- h5xfm = itk . ITKCompositeH5 .from_filename ("test.h5" )
246
+ h5xfm = ITKCompositeH5 .from_filename ("test.h5" )
239
247
assert len (h5xfm ) == 2
240
248
241
249
# Error raised if the we try to use the single affine loader
242
250
with pytest .raises (TransformIOError ):
243
- itk . ITKLinearTransform .from_filename ("test.h5" )
251
+ ITKLinearTransform .from_filename ("test.h5" )
244
252
245
253
shutil .copy (data_path / "affine-antsComposite.h5" , "test.h5" )
246
254
os .chmod ("test.h5" , 0o666 )
@@ -251,12 +259,12 @@ def test_itk_linear_h5(tmpdir, data_path, testdata_path):
251
259
del h5group ["1" ]
252
260
253
261
# File loadable with generalistic object
254
- h5xfm = itk . ITKCompositeH5 .from_filename ("test.h5" )
262
+ h5xfm = ITKCompositeH5 .from_filename ("test.h5" )
255
263
assert len (h5xfm ) == 0
256
264
257
265
# Error raised if the we try to use the single affine loader
258
266
with pytest .raises (TransformIOError ):
259
- itk . ITKLinearTransform .from_filename ("test.h5" )
267
+ ITKLinearTransform .from_filename ("test.h5" )
260
268
261
269
262
270
# Added tests for h5 orientation bug
@@ -285,7 +293,7 @@ def test_itk_h5_field_order(tmp_path):
285
293
g1 ["TransformFixedParameters" ] = fixed
286
294
g1 ["TransformParameters" ] = params
287
295
288
- img = itk . ITKCompositeH5 .from_filename (fname )[0 ]
296
+ img = ITKCompositeH5 .from_filename (fname )[0 ]
289
297
expected = np .moveaxis (field , 0 , - 1 )
290
298
expected [..., (0 , 1 )] *= - 1
291
299
assert np .allclose (img .get_fdata (), expected )
@@ -314,9 +322,9 @@ def _load_composite_testdata(data_path):
314
322
def test_itk_h5_displacement_mismatch (testdata_path ):
315
323
"""Composite displacements should match the standalone field"""
316
324
h5file , warpfile = _load_composite_testdata (testdata_path )
317
- xforms = itk . ITKCompositeH5 .from_filename (h5file )
325
+ xforms = ITKCompositeH5 .from_filename (h5file )
318
326
field_h5 = xforms [1 ]
319
- field_img = itk . ITKDisplacementsField .from_filename (warpfile )
327
+ field_img = ITKDisplacementsField .from_filename (warpfile )
320
328
321
329
np .testing .assert_array_equal (
322
330
np .asanyarray (field_h5 .dataobj ), np .asanyarray (field_img .dataobj )
@@ -368,7 +376,78 @@ def test_itk_h5_field_order_fortran(tmp_path):
368
376
g1 ["TransformFixedParameters" ] = fixed
369
377
g1 ["TransformParameters" ] = params
370
378
371
- img = itk . ITKCompositeH5 .from_filename (fname )[0 ]
379
+ img = ITKCompositeH5 .from_filename (fname )[0 ]
372
380
expected = np .moveaxis (field , 0 , - 1 )
373
381
expected [..., (0 , 1 )] *= - 1
374
382
assert np .allclose (img .get_fdata (), expected )
383
+
384
+
385
+ # Tests against ANTs' ``antsApplyTransformsToPoints``
386
+ @pytest .mark .parametrize ("ongrid" , [True , False ])
387
+ def test_densefield_map_vs_ants (testdata_path , tmp_path , ongrid ):
388
+ """Map points with DenseFieldTransform and compare to ANTs."""
389
+ warpfile = (
390
+ testdata_path
391
+ / "regressions"
392
+ / ("01_ants_t1_to_mniComposite_DisplacementFieldTransform.nii.gz" )
393
+ )
394
+ if not warpfile .exists ():
395
+ pytest .skip ("Composite transform test data not available" )
396
+
397
+ nii = ITKDisplacementsField .from_filename (warpfile )
398
+
399
+ # Get sampling indices
400
+ coords_xyz , points_ijk , grid_xyz , shape , ref_affine , reference , subsample = (
401
+ get_points (nii , ongrid , npoints = 5 , rng = rng )
402
+ )
403
+ coords_map = grid_xyz .reshape (* shape , 3 )
404
+
405
+ csvin = tmp_path / "fixed_coords.csv"
406
+ csvout = tmp_path / "moving_coords.csv"
407
+
408
+ # antsApplyTransformsToPoints wants LPS coordinates, see last post at
409
+ # http://sourceforge.net/p/advants/discussion/840261/thread/2a1e9307/
410
+ lps_xyz = coords_xyz .copy () * (- 1 , - 1 , 1 )
411
+ np .savetxt (csvin , lps_xyz , delimiter = "," , header = "x,y,z" , comments = "" )
412
+
413
+ cmd = f"antsApplyTransformsToPoints -d 3 -i { csvin } -o { csvout } -t { warpfile } "
414
+ exe = cmd .split ()[0 ]
415
+ if not shutil .which (exe ):
416
+ pytest .skip (f"Command { exe } not found on host" )
417
+ check_call (cmd , shell = True )
418
+
419
+ ants_res = np .genfromtxt (csvout , delimiter = "," , names = True )
420
+
421
+ # antsApplyTransformsToPoints writes LPS coordinates, see last post at
422
+ # http://sourceforge.net/p/advants/discussion/840261/thread/2a1e9307/
423
+ ants_pts = np .vstack ([ants_res [n ] for n in ("x" , "y" , "z" )]).T * (- 1 , - 1 , 1 )
424
+
425
+ xfm = nitnl .DenseFieldTransform (nii , reference = reference )
426
+ mapped = xfm .map (coords_xyz )
427
+
428
+ if ongrid :
429
+ ants_mapped_xyz = ants_pts .reshape (* shape , 3 )
430
+ nit_mapped_xyz = mapped .reshape (* shape , 3 )
431
+
432
+ nb .load (warpfile ).to_filename (tmp_path / "original_ants_deltas.nii.gz" )
433
+
434
+ nb .Nifti1Image (coords_map , ref_affine , None ).to_filename (
435
+ tmp_path / "baseline_positions.nii.gz"
436
+ )
437
+
438
+ nii .to_filename (tmp_path / "original_interpreted_deltas.nii.gz" )
439
+
440
+ nb .Nifti1Image (nit_mapped_xyz , ref_affine , None ).to_filename (
441
+ tmp_path / "nit_deformation_xyz.nii.gz"
442
+ )
443
+ nb .Nifti1Image (ants_mapped_xyz - coords_map , ref_affine , None ).to_filename (
444
+ tmp_path / "ants_deltas_xyz.nii.gz"
445
+ )
446
+ nb .Nifti1Image (nit_mapped_xyz - coords_map , ref_affine , None ).to_filename (
447
+ tmp_path / "nit_deltas_xyz.nii.gz"
448
+ )
449
+
450
+ # When transforming off-grid points, rounding errors are large
451
+ atol = 0 if ongrid else 1e-1
452
+ rtol = 1e-4 if ongrid else 1e-3
453
+ np .testing .assert_allclose (mapped , ants_pts , atol = atol , rtol = rtol )
0 commit comments