11# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*-
22# vi: set ft=python sts=4 ts=4 sw=4 et:
33"""I/O test cases."""
4+
45import os
56from subprocess import check_call
67from io import StringIO
1516from nibabel .affines import from_matvec
1617from scipy .io import loadmat
1718from nitransforms .linear import Affine
19+ from nitransforms .nonlinear import DenseFieldTransform , BSplineFieldTransform
1820from nitransforms .io import (
1921 afni ,
2022 fsl ,
2123 lta as fs ,
2224 itk ,
25+ x5 ,
2326)
2427from nitransforms .io .lta import (
2528 VolumeGeometry as VG ,
@@ -68,11 +71,13 @@ def test_volume_group_voxel_ordering():
6871def test_VG_from_LTA (data_path ):
6972 """Check the affine interpolation from volume geometries."""
7073 # affine manually clipped after running mri_info on the image
71- oracle = np .loadtxt (StringIO ("""\
74+ oracle = np .loadtxt (
75+ StringIO ("""\
7276 -3.0000 0.0000 -0.0000 91.3027
7377-0.0000 2.0575 -2.9111 -25.5251
7478 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+ )
7681
7782 lta_text = "\n " .join (
7883 (data_path / "bold-to-t1w.lta" ).read_text ().splitlines ()[13 :21 ]
@@ -419,10 +424,17 @@ def test_afni_Displacements():
419424
420425
421426@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+ )
426438def test_itk_h5 (tmpdir , only_linear , h5_path , nxforms ):
427439 """Test displacements fields."""
428440 assert (
@@ -434,7 +446,9 @@ def test_itk_h5(tmpdir, only_linear, h5_path, nxforms):
434446 )
435447 )
436448 )
437- == nxforms if not only_linear else 1
449+ == nxforms
450+ if not only_linear
451+ else 1
438452 )
439453
440454 with pytest .raises (TransformFileError ):
@@ -465,24 +479,33 @@ def test_regressions(file_type, test_file, data_path):
465479 file_type .from_filename (data_path / "regressions" / test_file )
466480
467481
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+ )
473490@pytest .mark .parametrize ("dir_x" , (- 1 , 1 ))
474491@pytest .mark .parametrize ("dir_y" , (- 1 , 1 ))
475492@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+ )
479502def test_afni_oblique (tmpdir , parameters , swapaxes , testdata_path , dir_x , dir_y , dir_z ):
480503 tmpdir .chdir ()
481504 img , R = _generate_reoriented (
482505 testdata_path / "someones_anatomy.nii.gz" ,
483506 (dir_x , dir_y , dir_z ),
484507 swapaxes ,
485- parameters
508+ parameters ,
486509 )
487510 img .to_filename ("orig.nii.gz" )
488511
@@ -507,9 +530,8 @@ def test_afni_oblique(tmpdir, parameters, swapaxes, testdata_path, dir_x, dir_y,
507530 "orig.nii.gz" ,
508531 )
509532
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"
513535 )
514536 assert np .sqrt ((diff [10 :- 10 , 10 :- 10 , 10 :- 10 ] ** 2 ).mean ()) < 0.1
515537
@@ -522,14 +544,15 @@ def test_afni_oblique(tmpdir, parameters, swapaxes, testdata_path, dir_x, dir_y,
522544 "deob_3drefit.nii.gz" ,
523545 )
524546
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"
528549 )
529550 assert np .sqrt ((diff [10 :- 10 , 10 :- 10 , 10 :- 10 ] ** 2 ).mean ()) < 0.1
530551
531552 # 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+ )
533556 assert check_call ([cmd ], shell = True ) == 0
534557
535558 deobnii = nb .load ("deob.nii.gz" )
@@ -540,11 +563,12 @@ def test_afni_oblique(tmpdir, parameters, swapaxes, testdata_path, dir_x, dir_y,
540563
541564 # Check resampling in deobliqued grid
542565 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+ ),
548572 img ,
549573 order = 0 ,
550574 )
@@ -559,9 +583,8 @@ def test_afni_oblique(tmpdir, parameters, swapaxes, testdata_path, dir_x, dir_y,
559583 )
560584 mask = np .asanyarray (ntdeobmask .dataobj , dtype = bool )
561585
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"
565588 )
566589 assert np .sqrt ((diff [mask ] ** 2 ).mean ()) < 0.1
567590
@@ -591,7 +614,7 @@ def _generate_reoriented(path, directions, swapaxes, parameters):
591614 aff = np .diag ((* directions , 1 )) @ aff
592615
593616 for ax in range (3 ):
594- if ( directions [ax ] == - 1 ) :
617+ if directions [ax ] == - 1 :
595618 aff [ax , 3 ] = last_xyz [ax ]
596619 data = np .flip (data , ax )
597620
@@ -621,16 +644,15 @@ def test_itk_linear_h5(tmpdir, data_path, testdata_path):
621644 assert len (h5xfm .xforms ) == 1
622645
623646 # 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" )
627648
628649 with open (data_path / "affine-antsComposite.h5" , "rb" ) as f :
629650 itk .ITKLinearTransform .from_fileobj (f )
630651
631652 # Exercise only_linear
632653 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" ,
634656 only_linear = True ,
635657 )
636658
@@ -673,9 +695,54 @@ def test_itk_linear_h5(tmpdir, data_path, testdata_path):
673695 with pytest .raises (TransformIOError ):
674696 itk .ITKLinearTransform .from_filename ("test.h5" )
675697
676- # Added tests for h5 orientation bug
677698
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 ))
678728
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
679746@pytest .mark .xfail (
680747 reason = "GH-137/GH-171: displacement field dimension order is wrong" ,
681748 strict = False ,
@@ -687,11 +754,15 @@ def test_itk_h5_field_order(tmp_path):
687754 field = np .stack ([vals , vals + 100 , vals + 200 ], axis = 0 )
688755
689756 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+ )
691760 fname = tmp_path / "field.h5"
692761 with H5File (fname , "w" ) as f :
693762 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+ )
695766 g1 = grp .create_group ("1" )
696767 g1 ["TransformType" ] = np .array ([b"DisplacementFieldTransform_float_3_3" ])
697768 g1 ["TransformFixedParameters" ] = fixed
@@ -709,8 +780,10 @@ def _load_composite_testdata(data_path):
709780 # Generated using
710781 # CompositeTransformUtil --disassemble ants_t1_to_mniComposite.h5 \
711782 # 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" )
714787 )
715788 if not (h5file .exists () and warpfile .exists ()):
716789 pytest .skip ("Composite transform test data not available" )
@@ -764,11 +837,15 @@ def test_itk_h5_field_order_fortran(tmp_path):
764837 field = np .stack ([vals , vals + 100 , vals + 200 ], axis = 0 )
765838
766839 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+ )
768843 fname = tmp_path / "field_f.h5"
769844 with H5File (fname , "w" ) as f :
770845 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+ )
772849 g1 = grp .create_group ("1" )
773850 g1 ["TransformType" ] = np .array ([b"DisplacementFieldTransform_float_3_3" ])
774851 g1 ["TransformFixedParameters" ] = fixed
0 commit comments