Skip to content

Commit 80c3cfe

Browse files
committed
fix: revise tests by a human
1 parent de3b1ac commit 80c3cfe

File tree

5 files changed

+130
-152
lines changed

5 files changed

+130
-152
lines changed

env.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ dependencies:
2424
- nitime=0.10
2525
- scikit-image=0.22
2626
- scikit-learn=1.4
27+
# SimpleITK, so build doesn't complain about building scikit from sources
28+
- simpleitk=2.4
2729
# Utilities
2830
- graphviz=9.0
2931
- pandoc=3.1

nitransforms/tests/test_io.py

Lines changed: 78 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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+
45
import os
56
from subprocess import check_call
67
from io import StringIO
@@ -15,7 +16,7 @@
1516
from nibabel.affines import from_matvec
1617
from scipy.io import loadmat
1718
from nitransforms.linear import Affine
18-
from nitransforms import nonlinear as nitnl, linear as nitl
19+
from nitransforms import nonlinear as nitnl
1920
from nitransforms.io import (
2021
afni,
2122
fsl,
@@ -69,11 +70,13 @@ def test_volume_group_voxel_ordering():
6970
def test_VG_from_LTA(data_path):
7071
"""Check the affine interpolation from volume geometries."""
7172
# affine manually clipped after running mri_info on the image
72-
oracle = np.loadtxt(StringIO("""\
73+
oracle = np.loadtxt(
74+
StringIO("""\
7375
-3.0000 0.0000 -0.0000 91.3027
7476
-0.0000 2.0575 -2.9111 -25.5251
7577
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+
)
7780

7881
lta_text = "\n".join(
7982
(data_path / "bold-to-t1w.lta").read_text().splitlines()[13:21]
@@ -420,10 +423,17 @@ def test_afni_Displacements():
420423

421424

422425
@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+
)
427437
def test_itk_h5(tmpdir, only_linear, h5_path, nxforms):
428438
"""Test displacements fields."""
429439
assert (
@@ -435,7 +445,9 @@ def test_itk_h5(tmpdir, only_linear, h5_path, nxforms):
435445
)
436446
)
437447
)
438-
== nxforms if not only_linear else 1
448+
== nxforms
449+
if not only_linear
450+
else 1
439451
)
440452

441453
with pytest.raises(TransformFileError):
@@ -466,24 +478,33 @@ def test_regressions(file_type, test_file, data_path):
466478
file_type.from_filename(data_path / "regressions" / test_file)
467479

468480

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+
)
474489
@pytest.mark.parametrize("dir_x", (-1, 1))
475490
@pytest.mark.parametrize("dir_y", (-1, 1))
476491
@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+
)
480501
def test_afni_oblique(tmpdir, parameters, swapaxes, testdata_path, dir_x, dir_y, dir_z):
481502
tmpdir.chdir()
482503
img, R = _generate_reoriented(
483504
testdata_path / "someones_anatomy.nii.gz",
484505
(dir_x, dir_y, dir_z),
485506
swapaxes,
486-
parameters
507+
parameters,
487508
)
488509
img.to_filename("orig.nii.gz")
489510

@@ -508,9 +529,8 @@ def test_afni_oblique(tmpdir, parameters, swapaxes, testdata_path, dir_x, dir_y,
508529
"orig.nii.gz",
509530
)
510531

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"
514534
)
515535
assert np.sqrt((diff[10:-10, 10:-10, 10:-10] ** 2).mean()) < 0.1
516536

@@ -523,14 +543,15 @@ def test_afni_oblique(tmpdir, parameters, swapaxes, testdata_path, dir_x, dir_y,
523543
"deob_3drefit.nii.gz",
524544
)
525545

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"
529548
)
530549
assert np.sqrt((diff[10:-10, 10:-10, 10:-10] ** 2).mean()) < 0.1
531550

532551
# 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+
)
534555
assert check_call([cmd], shell=True) == 0
535556

536557
deobnii = nb.load("deob.nii.gz")
@@ -541,11 +562,12 @@ def test_afni_oblique(tmpdir, parameters, swapaxes, testdata_path, dir_x, dir_y,
541562

542563
# Check resampling in deobliqued grid
543564
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+
),
549571
img,
550572
order=0,
551573
)
@@ -560,9 +582,8 @@ def test_afni_oblique(tmpdir, parameters, swapaxes, testdata_path, dir_x, dir_y,
560582
)
561583
mask = np.asanyarray(ntdeobmask.dataobj, dtype=bool)
562584

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"
566587
)
567588
assert np.sqrt((diff[mask] ** 2).mean()) < 0.1
568589

@@ -592,7 +613,7 @@ def _generate_reoriented(path, directions, swapaxes, parameters):
592613
aff = np.diag((*directions, 1)) @ aff
593614

594615
for ax in range(3):
595-
if (directions[ax] == -1):
616+
if directions[ax] == -1:
596617
aff[ax, 3] = last_xyz[ax]
597618
data = np.flip(data, ax)
598619

@@ -622,16 +643,15 @@ def test_itk_linear_h5(tmpdir, data_path, testdata_path):
622643
assert len(h5xfm.xforms) == 1
623644

624645
# 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")
628647

629648
with open(data_path / "affine-antsComposite.h5", "rb") as f:
630649
itk.ITKLinearTransform.from_fileobj(f)
631650

632651
# Exercise only_linear
633652
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",
635655
only_linear=True,
636656
)
637657

@@ -674,55 +694,25 @@ def test_itk_linear_h5(tmpdir, data_path, testdata_path):
674694
with pytest.raises(TransformIOError):
675695
itk.ITKLinearTransform.from_filename("test.h5")
676696

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-
706697

698+
# Added tests for h5 orientation bug (#167)
707699
def _load_composite_testdata(data_path):
708700
"""Return the composite HDF5 and displacement field from regressions."""
709701
h5file = data_path / "regressions" / "ants_t1_to_mniComposite.h5"
710702
# Generated using
711703
# CompositeTransformUtil --disassemble ants_t1_to_mniComposite.h5 \
712704
# 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")
715709
)
716710
if not (h5file.exists() and warpfile.exists()):
717711
pytest.skip("Composite transform test data not available")
718712
return h5file, warpfile
719713

720714

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):
726716
"""Composite displacements should match the standalone field"""
727717
h5file, warpfile = _load_composite_testdata(testdata_path)
728718
xforms = itk.ITKCompositeH5.from_filename(h5file)
@@ -733,80 +723,21 @@ def test_itk_h5_displacement_mismatch(testdata_path):
733723
np.asanyarray(field_h5.dataobj), np.asanyarray(field_img.dataobj)
734724
)
735725

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+
)
736735

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)
804739

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)
811742

812-
assert np.allclose(mapped, ants_pts, atol=1e-6)
743+
np.testing.assert_array_equal(mapped_h5, mapped_warp)

0 commit comments

Comments
 (0)