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
19
+ from nitransforms .nonlinear import DenseFieldTransform , BSplineFieldTransform
18
20
from nitransforms .io import (
19
21
afni ,
20
22
fsl ,
21
23
lta as fs ,
22
24
itk ,
25
+ x5 ,
23
26
)
24
27
from nitransforms .io .lta import (
25
28
VolumeGeometry as VG ,
@@ -68,11 +71,13 @@ def test_volume_group_voxel_ordering():
68
71
def test_VG_from_LTA (data_path ):
69
72
"""Check the affine interpolation from volume geometries."""
70
73
# affine manually clipped after running mri_info on the image
71
- oracle = np .loadtxt (StringIO ("""\
74
+ oracle = np .loadtxt (
75
+ StringIO ("""\
72
76
-3.0000 0.0000 -0.0000 91.3027
73
77
-0.0000 2.0575 -2.9111 -25.5251
74
78
0.0000 2.1833 2.7433 -105.0820
75
- 0.0000 0.0000 0.0000 1.0000""" ))
79
+ 0.0000 0.0000 0.0000 1.0000""" )
80
+ )
76
81
77
82
lta_text = "\n " .join (
78
83
(data_path / "bold-to-t1w.lta" ).read_text ().splitlines ()[13 :21 ]
@@ -419,10 +424,17 @@ def test_afni_Displacements():
419
424
420
425
421
426
@pytest .mark .parametrize ("only_linear" , [True , False ])
422
- @pytest .mark .parametrize ("h5_path,nxforms" , [
423
- (_datadir / "affine-antsComposite.h5" , 1 ),
424
- (_testdir / "ds-005_sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5" , 2 ),
425
- ])
427
+ @pytest .mark .parametrize (
428
+ "h5_path,nxforms" ,
429
+ [
430
+ (_datadir / "affine-antsComposite.h5" , 1 ),
431
+ (
432
+ _testdir
433
+ / "ds-005_sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5" ,
434
+ 2 ,
435
+ ),
436
+ ],
437
+ )
426
438
def test_itk_h5 (tmpdir , only_linear , h5_path , nxforms ):
427
439
"""Test displacements fields."""
428
440
assert (
@@ -434,7 +446,9 @@ def test_itk_h5(tmpdir, only_linear, h5_path, nxforms):
434
446
)
435
447
)
436
448
)
437
- == nxforms if not only_linear else 1
449
+ == nxforms
450
+ if not only_linear
451
+ else 1
438
452
)
439
453
440
454
with pytest .raises (TransformFileError ):
@@ -465,24 +479,33 @@ def test_regressions(file_type, test_file, data_path):
465
479
file_type .from_filename (data_path / "regressions" / test_file )
466
480
467
481
468
- @pytest .mark .parametrize ("parameters" , [
469
- {"x" : 0.1 , "y" : 0.03 , "z" : 0.002 },
470
- {"x" : 0.001 , "y" : 0.3 , "z" : 0.002 },
471
- {"x" : 0.01 , "y" : 0.03 , "z" : 0.2 },
472
- ])
482
+ @pytest .mark .parametrize (
483
+ "parameters" ,
484
+ [
485
+ {"x" : 0.1 , "y" : 0.03 , "z" : 0.002 },
486
+ {"x" : 0.001 , "y" : 0.3 , "z" : 0.002 },
487
+ {"x" : 0.01 , "y" : 0.03 , "z" : 0.2 },
488
+ ],
489
+ )
473
490
@pytest .mark .parametrize ("dir_x" , (- 1 , 1 ))
474
491
@pytest .mark .parametrize ("dir_y" , (- 1 , 1 ))
475
492
@pytest .mark .parametrize ("dir_z" , (1 , - 1 ))
476
- @pytest .mark .parametrize ("swapaxes" , [
477
- None , (0 , 1 ), (1 , 2 ), (0 , 2 ),
478
- ])
493
+ @pytest .mark .parametrize (
494
+ "swapaxes" ,
495
+ [
496
+ None ,
497
+ (0 , 1 ),
498
+ (1 , 2 ),
499
+ (0 , 2 ),
500
+ ],
501
+ )
479
502
def test_afni_oblique (tmpdir , parameters , swapaxes , testdata_path , dir_x , dir_y , dir_z ):
480
503
tmpdir .chdir ()
481
504
img , R = _generate_reoriented (
482
505
testdata_path / "someones_anatomy.nii.gz" ,
483
506
(dir_x , dir_y , dir_z ),
484
507
swapaxes ,
485
- parameters
508
+ parameters ,
486
509
)
487
510
img .to_filename ("orig.nii.gz" )
488
511
@@ -507,9 +530,8 @@ def test_afni_oblique(tmpdir, parameters, swapaxes, testdata_path, dir_x, dir_y,
507
530
"orig.nii.gz" ,
508
531
)
509
532
510
- diff = (
511
- np .asanyarray (img .dataobj , dtype = "uint8" )
512
- - np .asanyarray (nt3drefit .dataobj , dtype = "uint8" )
533
+ diff = np .asanyarray (img .dataobj , dtype = "uint8" ) - np .asanyarray (
534
+ nt3drefit .dataobj , dtype = "uint8"
513
535
)
514
536
assert np .sqrt ((diff [10 :- 10 , 10 :- 10 , 10 :- 10 ] ** 2 ).mean ()) < 0.1
515
537
@@ -522,14 +544,15 @@ def test_afni_oblique(tmpdir, parameters, swapaxes, testdata_path, dir_x, dir_y,
522
544
"deob_3drefit.nii.gz" ,
523
545
)
524
546
525
- diff = (
526
- np .asanyarray (img .dataobj , dtype = "uint8" )
527
- - np .asanyarray (nt_undo3drefit .dataobj , dtype = "uint8" )
547
+ diff = np .asanyarray (img .dataobj , dtype = "uint8" ) - np .asanyarray (
548
+ nt_undo3drefit .dataobj , dtype = "uint8"
528
549
)
529
550
assert np .sqrt ((diff [10 :- 10 , 10 :- 10 , 10 :- 10 ] ** 2 ).mean ()) < 0.1
530
551
531
552
# Check the target grid by 3dWarp and the affine & size interpolated by NiTransforms
532
- cmd = f"3dWarp -verb -deoblique -NN -prefix { tmpdir } /deob.nii.gz { tmpdir } /orig.nii.gz"
553
+ cmd = (
554
+ f"3dWarp -verb -deoblique -NN -prefix { tmpdir } /deob.nii.gz { tmpdir } /orig.nii.gz"
555
+ )
533
556
assert check_call ([cmd ], shell = True ) == 0
534
557
535
558
deobnii = nb .load ("deob.nii.gz" )
@@ -540,11 +563,12 @@ def test_afni_oblique(tmpdir, parameters, swapaxes, testdata_path, dir_x, dir_y,
540
563
541
564
# Check resampling in deobliqued grid
542
565
ntdeobnii = apply (
543
- Affine (np .eye (4 ), reference = deobnii .__class__ (
544
- np .zeros (deobshape , dtype = "uint8" ),
545
- deobaff ,
546
- deobnii .header
547
- )),
566
+ Affine (
567
+ np .eye (4 ),
568
+ reference = deobnii .__class__ (
569
+ np .zeros (deobshape , dtype = "uint8" ), deobaff , deobnii .header
570
+ ),
571
+ ),
548
572
img ,
549
573
order = 0 ,
550
574
)
@@ -559,9 +583,8 @@ def test_afni_oblique(tmpdir, parameters, swapaxes, testdata_path, dir_x, dir_y,
559
583
)
560
584
mask = np .asanyarray (ntdeobmask .dataobj , dtype = bool )
561
585
562
- diff = (
563
- np .asanyarray (deobnii .dataobj , dtype = "uint8" )
564
- - np .asanyarray (ntdeobnii .dataobj , dtype = "uint8" )
586
+ diff = np .asanyarray (deobnii .dataobj , dtype = "uint8" ) - np .asanyarray (
587
+ ntdeobnii .dataobj , dtype = "uint8"
565
588
)
566
589
assert np .sqrt ((diff [mask ] ** 2 ).mean ()) < 0.1
567
590
@@ -591,7 +614,7 @@ def _generate_reoriented(path, directions, swapaxes, parameters):
591
614
aff = np .diag ((* directions , 1 )) @ aff
592
615
593
616
for ax in range (3 ):
594
- if ( directions [ax ] == - 1 ) :
617
+ if directions [ax ] == - 1 :
595
618
aff [ax , 3 ] = last_xyz [ax ]
596
619
data = np .flip (data , ax )
597
620
@@ -621,16 +644,15 @@ def test_itk_linear_h5(tmpdir, data_path, testdata_path):
621
644
assert len (h5xfm .xforms ) == 1
622
645
623
646
# File loadable with single affine object
624
- itk .ITKLinearTransform .from_filename (
625
- data_path / "affine-antsComposite.h5"
626
- )
647
+ itk .ITKLinearTransform .from_filename (data_path / "affine-antsComposite.h5" )
627
648
628
649
with open (data_path / "affine-antsComposite.h5" , "rb" ) as f :
629
650
itk .ITKLinearTransform .from_fileobj (f )
630
651
631
652
# Exercise only_linear
632
653
itk .ITKCompositeH5 .from_filename (
633
- testdata_path / "ds-005_sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5" ,
654
+ testdata_path
655
+ / "ds-005_sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5" ,
634
656
only_linear = True ,
635
657
)
636
658
@@ -673,9 +695,54 @@ def test_itk_linear_h5(tmpdir, data_path, testdata_path):
673
695
with pytest .raises (TransformIOError ):
674
696
itk .ITKLinearTransform .from_filename ("test.h5" )
675
697
676
- # Added tests for h5 orientation bug
677
698
699
+ @pytest .mark .parametrize ("is_deltas" , [True , False ])
700
+ def test_densefield_x5_roundtrip (tmp_path , is_deltas ):
701
+ """Ensure dense field transforms roundtrip via X5."""
702
+ ref = nb .Nifti1Image (np .zeros ((2 , 2 , 2 ), dtype = "uint8" ), np .eye (4 ))
703
+ disp = nb .Nifti1Image (np .random .rand (2 , 2 , 2 , 3 ).astype ("float32" ), np .eye (4 ))
704
+
705
+ xfm = DenseFieldTransform (disp , is_deltas = is_deltas , reference = ref )
706
+
707
+ node = xfm .to_x5 (metadata = {"GeneratedBy" : "pytest" })
708
+ assert node .type == "nonlinear"
709
+ assert node .subtype == "densefield"
710
+ assert node .representation == "displacements" if is_deltas else "deformations"
711
+ assert node .domain .size == ref .shape
712
+ assert node .metadata ["GeneratedBy" ] == "pytest"
713
+
714
+ fname = tmp_path / "test.x5"
715
+ x5 .to_filename (fname , [node ])
716
+
717
+ xfm2 = DenseFieldTransform .from_filename (fname , fmt = "X5" )
718
+
719
+ assert xfm2 .reference .shape == ref .shape
720
+ assert np .allclose (xfm2 .reference .affine , ref .affine )
721
+ assert xfm == xfm2
722
+
723
+
724
+ def test_bspline_to_x5 (tmp_path ):
725
+ """Check BSpline transforms export to X5."""
726
+ coeff = nb .Nifti1Image (np .zeros ((2 , 2 , 2 , 3 ), dtype = "float32" ), np .eye (4 ))
727
+ ref = nb .Nifti1Image (np .zeros ((2 , 2 , 2 ), dtype = "uint8" ), np .eye (4 ))
678
728
729
+ xfm = BSplineFieldTransform (coeff , reference = ref )
730
+ node = xfm .to_x5 (metadata = {"tool" : "pytest" })
731
+ assert node .type == "nonlinear"
732
+ assert node .subtype == "bspline"
733
+ assert node .representation == "coefficients"
734
+ assert node .metadata ["tool" ] == "pytest"
735
+
736
+ fname = tmp_path / "bspline.x5"
737
+ x5 .to_filename (fname , [node ])
738
+
739
+ xfm2 = BSplineFieldTransform .from_filename (fname , fmt = "X5" )
740
+ assert np .allclose (xfm ._coeffs , xfm2 ._coeffs )
741
+ assert xfm2 .reference .shape == ref .shape
742
+ assert np .allclose (xfm2 .reference .affine , ref .affine )
743
+
744
+
745
+ # Added tests for h5 orientation bug
679
746
@pytest .mark .xfail (
680
747
reason = "GH-137/GH-171: displacement field dimension order is wrong" ,
681
748
strict = False ,
@@ -687,11 +754,15 @@ def test_itk_h5_field_order(tmp_path):
687
754
field = np .stack ([vals , vals + 100 , vals + 200 ], axis = 0 )
688
755
689
756
params = field .reshape (- 1 , order = "C" )
690
- fixed = np .array (list (shape ) + [0 , 0 , 0 ] + [1 , 1 , 1 ] + list (np .eye (3 ).ravel ()), dtype = float )
757
+ fixed = np .array (
758
+ list (shape ) + [0 , 0 , 0 ] + [1 , 1 , 1 ] + list (np .eye (3 ).ravel ()), dtype = float
759
+ )
691
760
fname = tmp_path / "field.h5"
692
761
with H5File (fname , "w" ) as f :
693
762
grp = f .create_group ("TransformGroup" )
694
- grp .create_group ("0" )["TransformType" ] = np .array ([b"CompositeTransform_double_3_3" ])
763
+ grp .create_group ("0" )["TransformType" ] = np .array (
764
+ [b"CompositeTransform_double_3_3" ]
765
+ )
695
766
g1 = grp .create_group ("1" )
696
767
g1 ["TransformType" ] = np .array ([b"DisplacementFieldTransform_float_3_3" ])
697
768
g1 ["TransformFixedParameters" ] = fixed
@@ -709,8 +780,10 @@ def _load_composite_testdata(data_path):
709
780
# Generated using
710
781
# CompositeTransformUtil --disassemble ants_t1_to_mniComposite.h5 \
711
782
# ants_t1_to_mniComposite
712
- warpfile = data_path / "regressions" / (
713
- "01_ants_t1_to_mniComposite_DisplacementFieldTransform.nii.gz"
783
+ warpfile = (
784
+ data_path
785
+ / "regressions"
786
+ / ("01_ants_t1_to_mniComposite_DisplacementFieldTransform.nii.gz" )
714
787
)
715
788
if not (h5file .exists () and warpfile .exists ()):
716
789
pytest .skip ("Composite transform test data not available" )
@@ -764,11 +837,15 @@ def test_itk_h5_field_order_fortran(tmp_path):
764
837
field = np .stack ([vals , vals + 100 , vals + 200 ], axis = 0 )
765
838
766
839
params = field .reshape (- 1 , order = "F" )
767
- fixed = np .array (list (shape ) + [0 , 0 , 0 ] + [1 , 1 , 1 ] + list (np .eye (3 ).ravel ()), dtype = float )
840
+ fixed = np .array (
841
+ list (shape ) + [0 , 0 , 0 ] + [1 , 1 , 1 ] + list (np .eye (3 ).ravel ()), dtype = float
842
+ )
768
843
fname = tmp_path / "field_f.h5"
769
844
with H5File (fname , "w" ) as f :
770
845
grp = f .create_group ("TransformGroup" )
771
- grp .create_group ("0" )["TransformType" ] = np .array ([b"CompositeTransform_double_3_3" ])
846
+ grp .create_group ("0" )["TransformType" ] = np .array (
847
+ [b"CompositeTransform_double_3_3" ]
848
+ )
772
849
g1 = grp .create_group ("1" )
773
850
g1 ["TransformType" ] = np .array ([b"DisplacementFieldTransform_float_3_3" ])
774
851
g1 ["TransformFixedParameters" ] = fixed
0 commit comments