1
1
# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*-
2
2
# vi: set ft=python sts=4 ts=4 sw=4 et:
3
3
"""I/O test cases."""
4
+
4
5
import os
5
6
from subprocess import check_call
6
7
from io import StringIO
15
16
from nibabel .affines import from_matvec
16
17
from scipy .io import loadmat
17
18
from nitransforms .linear import Affine
18
- from nitransforms import nonlinear as nitnl , linear as nitl
19
+ from nitransforms import nonlinear as nitnl
19
20
from nitransforms .io import (
20
21
afni ,
21
22
fsl ,
@@ -69,11 +70,13 @@ def test_volume_group_voxel_ordering():
69
70
def test_VG_from_LTA (data_path ):
70
71
"""Check the affine interpolation from volume geometries."""
71
72
# affine manually clipped after running mri_info on the image
72
- oracle = np .loadtxt (StringIO ("""\
73
+ oracle = np .loadtxt (
74
+ StringIO ("""\
73
75
-3.0000 0.0000 -0.0000 91.3027
74
76
-0.0000 2.0575 -2.9111 -25.5251
75
77
0.0000 2.1833 2.7433 -105.0820
76
- 0.0000 0.0000 0.0000 1.0000""" ))
78
+ 0.0000 0.0000 0.0000 1.0000""" )
79
+ )
77
80
78
81
lta_text = "\n " .join (
79
82
(data_path / "bold-to-t1w.lta" ).read_text ().splitlines ()[13 :21 ]
@@ -420,10 +423,17 @@ def test_afni_Displacements():
420
423
421
424
422
425
@pytest .mark .parametrize ("only_linear" , [True , False ])
423
- @pytest .mark .parametrize ("h5_path,nxforms" , [
424
- (_datadir / "affine-antsComposite.h5" , 1 ),
425
- (_testdir / "ds-005_sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5" , 2 ),
426
- ])
426
+ @pytest .mark .parametrize (
427
+ "h5_path,nxforms" ,
428
+ [
429
+ (_datadir / "affine-antsComposite.h5" , 1 ),
430
+ (
431
+ _testdir
432
+ / "ds-005_sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5" ,
433
+ 2 ,
434
+ ),
435
+ ],
436
+ )
427
437
def test_itk_h5 (tmpdir , only_linear , h5_path , nxforms ):
428
438
"""Test displacements fields."""
429
439
assert (
@@ -435,7 +445,9 @@ def test_itk_h5(tmpdir, only_linear, h5_path, nxforms):
435
445
)
436
446
)
437
447
)
438
- == nxforms if not only_linear else 1
448
+ == nxforms
449
+ if not only_linear
450
+ else 1
439
451
)
440
452
441
453
with pytest .raises (TransformFileError ):
@@ -466,24 +478,33 @@ def test_regressions(file_type, test_file, data_path):
466
478
file_type .from_filename (data_path / "regressions" / test_file )
467
479
468
480
469
- @pytest .mark .parametrize ("parameters" , [
470
- {"x" : 0.1 , "y" : 0.03 , "z" : 0.002 },
471
- {"x" : 0.001 , "y" : 0.3 , "z" : 0.002 },
472
- {"x" : 0.01 , "y" : 0.03 , "z" : 0.2 },
473
- ])
481
+ @pytest .mark .parametrize (
482
+ "parameters" ,
483
+ [
484
+ {"x" : 0.1 , "y" : 0.03 , "z" : 0.002 },
485
+ {"x" : 0.001 , "y" : 0.3 , "z" : 0.002 },
486
+ {"x" : 0.01 , "y" : 0.03 , "z" : 0.2 },
487
+ ],
488
+ )
474
489
@pytest .mark .parametrize ("dir_x" , (- 1 , 1 ))
475
490
@pytest .mark .parametrize ("dir_y" , (- 1 , 1 ))
476
491
@pytest .mark .parametrize ("dir_z" , (1 , - 1 ))
477
- @pytest .mark .parametrize ("swapaxes" , [
478
- None , (0 , 1 ), (1 , 2 ), (0 , 2 ),
479
- ])
492
+ @pytest .mark .parametrize (
493
+ "swapaxes" ,
494
+ [
495
+ None ,
496
+ (0 , 1 ),
497
+ (1 , 2 ),
498
+ (0 , 2 ),
499
+ ],
500
+ )
480
501
def test_afni_oblique (tmpdir , parameters , swapaxes , testdata_path , dir_x , dir_y , dir_z ):
481
502
tmpdir .chdir ()
482
503
img , R = _generate_reoriented (
483
504
testdata_path / "someones_anatomy.nii.gz" ,
484
505
(dir_x , dir_y , dir_z ),
485
506
swapaxes ,
486
- parameters
507
+ parameters ,
487
508
)
488
509
img .to_filename ("orig.nii.gz" )
489
510
@@ -508,9 +529,8 @@ def test_afni_oblique(tmpdir, parameters, swapaxes, testdata_path, dir_x, dir_y,
508
529
"orig.nii.gz" ,
509
530
)
510
531
511
- diff = (
512
- np .asanyarray (img .dataobj , dtype = "uint8" )
513
- - np .asanyarray (nt3drefit .dataobj , dtype = "uint8" )
532
+ diff = np .asanyarray (img .dataobj , dtype = "uint8" ) - np .asanyarray (
533
+ nt3drefit .dataobj , dtype = "uint8"
514
534
)
515
535
assert np .sqrt ((diff [10 :- 10 , 10 :- 10 , 10 :- 10 ] ** 2 ).mean ()) < 0.1
516
536
@@ -523,14 +543,15 @@ def test_afni_oblique(tmpdir, parameters, swapaxes, testdata_path, dir_x, dir_y,
523
543
"deob_3drefit.nii.gz" ,
524
544
)
525
545
526
- diff = (
527
- np .asanyarray (img .dataobj , dtype = "uint8" )
528
- - np .asanyarray (nt_undo3drefit .dataobj , dtype = "uint8" )
546
+ diff = np .asanyarray (img .dataobj , dtype = "uint8" ) - np .asanyarray (
547
+ nt_undo3drefit .dataobj , dtype = "uint8"
529
548
)
530
549
assert np .sqrt ((diff [10 :- 10 , 10 :- 10 , 10 :- 10 ] ** 2 ).mean ()) < 0.1
531
550
532
551
# Check the target grid by 3dWarp and the affine & size interpolated by NiTransforms
533
- cmd = f"3dWarp -verb -deoblique -NN -prefix { tmpdir } /deob.nii.gz { tmpdir } /orig.nii.gz"
552
+ cmd = (
553
+ f"3dWarp -verb -deoblique -NN -prefix { tmpdir } /deob.nii.gz { tmpdir } /orig.nii.gz"
554
+ )
534
555
assert check_call ([cmd ], shell = True ) == 0
535
556
536
557
deobnii = nb .load ("deob.nii.gz" )
@@ -541,11 +562,12 @@ def test_afni_oblique(tmpdir, parameters, swapaxes, testdata_path, dir_x, dir_y,
541
562
542
563
# Check resampling in deobliqued grid
543
564
ntdeobnii = apply (
544
- Affine (np .eye (4 ), reference = deobnii .__class__ (
545
- np .zeros (deobshape , dtype = "uint8" ),
546
- deobaff ,
547
- deobnii .header
548
- )),
565
+ Affine (
566
+ np .eye (4 ),
567
+ reference = deobnii .__class__ (
568
+ np .zeros (deobshape , dtype = "uint8" ), deobaff , deobnii .header
569
+ ),
570
+ ),
549
571
img ,
550
572
order = 0 ,
551
573
)
@@ -560,9 +582,8 @@ def test_afni_oblique(tmpdir, parameters, swapaxes, testdata_path, dir_x, dir_y,
560
582
)
561
583
mask = np .asanyarray (ntdeobmask .dataobj , dtype = bool )
562
584
563
- diff = (
564
- np .asanyarray (deobnii .dataobj , dtype = "uint8" )
565
- - np .asanyarray (ntdeobnii .dataobj , dtype = "uint8" )
585
+ diff = np .asanyarray (deobnii .dataobj , dtype = "uint8" ) - np .asanyarray (
586
+ ntdeobnii .dataobj , dtype = "uint8"
566
587
)
567
588
assert np .sqrt ((diff [mask ] ** 2 ).mean ()) < 0.1
568
589
@@ -592,7 +613,7 @@ def _generate_reoriented(path, directions, swapaxes, parameters):
592
613
aff = np .diag ((* directions , 1 )) @ aff
593
614
594
615
for ax in range (3 ):
595
- if ( directions [ax ] == - 1 ) :
616
+ if directions [ax ] == - 1 :
596
617
aff [ax , 3 ] = last_xyz [ax ]
597
618
data = np .flip (data , ax )
598
619
@@ -622,16 +643,15 @@ def test_itk_linear_h5(tmpdir, data_path, testdata_path):
622
643
assert len (h5xfm .xforms ) == 1
623
644
624
645
# File loadable with single affine object
625
- itk .ITKLinearTransform .from_filename (
626
- data_path / "affine-antsComposite.h5"
627
- )
646
+ itk .ITKLinearTransform .from_filename (data_path / "affine-antsComposite.h5" )
628
647
629
648
with open (data_path / "affine-antsComposite.h5" , "rb" ) as f :
630
649
itk .ITKLinearTransform .from_fileobj (f )
631
650
632
651
# Exercise only_linear
633
652
itk .ITKCompositeH5 .from_filename (
634
- testdata_path / "ds-005_sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5" ,
653
+ testdata_path
654
+ / "ds-005_sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5" ,
635
655
only_linear = True ,
636
656
)
637
657
@@ -674,55 +694,25 @@ def test_itk_linear_h5(tmpdir, data_path, testdata_path):
674
694
with pytest .raises (TransformIOError ):
675
695
itk .ITKLinearTransform .from_filename ("test.h5" )
676
696
677
- # Added tests for h5 orientation bug
678
-
679
-
680
- @pytest .mark .xfail (
681
- reason = "GH-137/GH-171: displacement field dimension order is wrong" ,
682
- strict = False ,
683
- )
684
- def test_itk_h5_field_order (tmp_path ):
685
- """Displacement fields stored in row-major order should fail to round-trip."""
686
- shape = (3 , 4 , 5 )
687
- vals = np .arange (np .prod (shape ), dtype = float ).reshape (shape )
688
- field = np .stack ([vals , vals + 100 , vals + 200 ], axis = 0 )
689
-
690
- params = field .reshape (- 1 , order = "C" )
691
- fixed = np .array (list (shape ) + [0 , 0 , 0 ] + [1 , 1 , 1 ] + list (np .eye (3 ).ravel ()), dtype = float )
692
- fname = tmp_path / "field.h5"
693
- with H5File (fname , "w" ) as f :
694
- grp = f .create_group ("TransformGroup" )
695
- grp .create_group ("0" )["TransformType" ] = np .array ([b"CompositeTransform_double_3_3" ])
696
- g1 = grp .create_group ("1" )
697
- g1 ["TransformType" ] = np .array ([b"DisplacementFieldTransform_float_3_3" ])
698
- g1 ["TransformFixedParameters" ] = fixed
699
- g1 ["TransformParameters" ] = params
700
-
701
- img = itk .ITKCompositeH5 .from_filename (fname )[0 ]
702
- expected = np .moveaxis (field , 0 , - 1 )
703
- expected [..., (0 , 1 )] *= - 1
704
- assert np .allclose (img .get_fdata (), expected )
705
-
706
697
698
+ # Added tests for h5 orientation bug (#167)
707
699
def _load_composite_testdata (data_path ):
708
700
"""Return the composite HDF5 and displacement field from regressions."""
709
701
h5file = data_path / "regressions" / "ants_t1_to_mniComposite.h5"
710
702
# Generated using
711
703
# CompositeTransformUtil --disassemble ants_t1_to_mniComposite.h5 \
712
704
# ants_t1_to_mniComposite
713
- warpfile = data_path / "regressions" / (
714
- "01_ants_t1_to_mniComposite_DisplacementFieldTransform.nii.gz"
705
+ warpfile = (
706
+ data_path
707
+ / "regressions"
708
+ / ("01_ants_t1_to_mniComposite_DisplacementFieldTransform.nii.gz" )
715
709
)
716
710
if not (h5file .exists () and warpfile .exists ()):
717
711
pytest .skip ("Composite transform test data not available" )
718
712
return h5file , warpfile
719
713
720
714
721
- @pytest .mark .xfail (
722
- reason = "GH-137/GH-171: displacement field dimension order is wrong" ,
723
- strict = False ,
724
- )
725
- def test_itk_h5_displacement_mismatch (testdata_path ):
715
+ def test_itk_h5_and_displacement_equivalence (testdata_path ):
726
716
"""Composite displacements should match the standalone field"""
727
717
h5file , warpfile = _load_composite_testdata (testdata_path )
728
718
xforms = itk .ITKCompositeH5 .from_filename (h5file )
@@ -733,80 +723,21 @@ def test_itk_h5_displacement_mismatch(testdata_path):
733
723
np .asanyarray (field_h5 .dataobj ), np .asanyarray (field_img .dataobj )
734
724
)
735
725
726
+ points = np .array (
727
+ [
728
+ [0.0 , 0.0 , 0.0 ],
729
+ [1.0 , 2.0 , 3.0 ],
730
+ [10.0 , - 10.0 , 5.0 ],
731
+ [- 5.0 , 7.0 , - 2.0 ],
732
+ [12.0 , 0.0 , - 11.0 ],
733
+ ]
734
+ )
736
735
737
- def test_itk_h5_transpose_fix (testdata_path ):
738
- """Check the displacement field orientation explicitly.
739
-
740
- ITK stores displacement fields with the vector dimension leading in
741
- Fortran (column-major) order [1]_. Transposing the parameters from the HDF5
742
- composite file accordingly should match the standalone displacement image.
743
-
744
- References
745
- ----------
746
- .. [1] ITK Software Guide. https://itk.org/ItkSoftwareGuide.pdf
747
- """
748
- h5file , warpfile = _load_composite_testdata (testdata_path )
749
-
750
- with H5File (h5file , "r" ) as f :
751
- group = f ["TransformGroup" ]["2" ]
752
- size = group ["TransformFixedParameters" ][:3 ].astype (int )
753
- params = group ["TransformParameters" ][:].reshape (* size , 3 )
754
-
755
- img = nb .load (warpfile )
756
- ref = np .squeeze (np .asanyarray (img .dataobj ))
757
-
758
- np .testing .assert_array_equal (params .transpose (2 , 1 , 0 , 3 ), ref )
759
-
760
-
761
- def test_itk_h5_field_order_fortran (tmp_path ):
762
- """Verify Fortran-order displacement fields load correctly"""
763
- shape = (3 , 4 , 5 )
764
- vals = np .arange (np .prod (shape ), dtype = float ).reshape (shape )
765
- field = np .stack ([vals , vals + 100 , vals + 200 ], axis = 0 )
766
-
767
- params = field .reshape (- 1 , order = "F" )
768
- fixed = np .array (list (shape ) + [0 , 0 , 0 ] + [1 , 1 , 1 ] + list (np .eye (3 ).ravel ()), dtype = float )
769
- fname = tmp_path / "field_f.h5"
770
- with H5File (fname , "w" ) as f :
771
- grp = f .create_group ("TransformGroup" )
772
- grp .create_group ("0" )["TransformType" ] = np .array ([b"CompositeTransform_double_3_3" ])
773
- g1 = grp .create_group ("1" )
774
- g1 ["TransformType" ] = np .array ([b"DisplacementFieldTransform_float_3_3" ])
775
- g1 ["TransformFixedParameters" ] = fixed
776
- g1 ["TransformParameters" ] = params
777
-
778
- img = itk .ITKCompositeH5 .from_filename (fname )[0 ]
779
- expected = np .moveaxis (field , 0 , - 1 )
780
- expected [..., (0 , 1 )] *= - 1
781
- assert np .allclose (img .get_fdata (), expected )
782
-
783
-
784
- @pytest .mark .xfail (strict = False , reason = "Results may not match ANTs exactly" )
785
- def test_composite_h5_map_against_ants (testdata_path , tmp_path ):
786
- """Map points with NiTransforms and compare to ANTs."""
787
- h5file = testdata_path / "regressions" / "ants_t1_to_mniComposite.h5"
788
- if not h5file .exists ():
789
- pytest .skip ("Composite transform test data not available" )
790
-
791
- points = np .array ([[0.0 , 0.0 , 0.0 ], [1.0 , 2.0 , 3.0 ]])
792
- csvin = tmp_path / "points.csv"
793
- np .savetxt (csvin , points , delimiter = "," , header = "x,y,z" , comments = "" )
794
-
795
- csvout = tmp_path / "out.csv"
796
- cmd = f"antsApplyTransformsToPoints -d 3 -i { csvin } -o { csvout } -t { h5file } "
797
- exe = cmd .split ()[0 ]
798
- if not shutil .which (exe ):
799
- pytest .skip (f"Command { exe } not found on host" )
800
- check_call (cmd , shell = True )
801
-
802
- ants_res = np .genfromtxt (csvout , delimiter = "," , names = True )
803
- ants_pts = np .vstack ([ants_res [n ] for n in ("x" , "y" , "z" )]).T
736
+ # Load the displacements field
737
+ xfm_h5 = nitnl .DenseFieldTransform (xforms [1 ])
738
+ mapped_h5 = xfm_h5 .map (points )
804
739
805
- xforms = itk .ITKCompositeH5 .from_filename (h5file )
806
- chain = (
807
- nitl .Affine (xforms [0 ].to_ras ())
808
- + nitnl .DenseFieldTransform (xforms [1 ])
809
- )
810
- mapped = chain .map (points )
740
+ xfm_warp = nitnl .DenseFieldTransform (field_img )
741
+ mapped_warp = xfm_warp .map (points )
811
742
812
- assert np .allclose ( mapped , ants_pts , atol = 1e-6 )
743
+ np .testing . assert_array_equal ( mapped_h5 , mapped_warp )
0 commit comments