Skip to content

Commit d48a739

Browse files
committed
enh: add test with constant field
1 parent 9f5b7eb commit d48a739

File tree

1 file changed

+104
-2
lines changed

1 file changed

+104
-2
lines changed

nitransforms/tests/test_io_itk.py

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@
2727
from nitransforms.conftest import _datadir, _testdir
2828
from nitransforms.tests.utils import get_points
2929

30-
rng = np.random.default_rng()
31-
30+
RNG_SEED = 202508140819
3231
LPS = np.diag([-1, -1, 1, 1])
3332
ITK_MAT = LPS.dot(np.ones((4, 4)).dot(LPS))
3433

@@ -386,6 +385,8 @@ def test_itk_h5_field_order_fortran(tmp_path):
386385
@pytest.mark.parametrize("ongrid", [True, False])
387386
def test_densefield_map_vs_ants(testdata_path, tmp_path, ongrid):
388387
"""Map points with DenseFieldTransform and compare to ANTs."""
388+
389+
rng = np.random.default_rng(RNG_SEED)
389390
warpfile = (
390391
testdata_path
391392
/ "regressions"
@@ -451,3 +452,104 @@ def test_densefield_map_vs_ants(testdata_path, tmp_path, ongrid):
451452
atol = 0 if ongrid else 1e-1
452453
rtol = 1e-4 if ongrid else 1e-3
453454
np.testing.assert_allclose(mapped, ants_pts, atol=atol, rtol=rtol)
455+
456+
457+
@pytest.mark.parametrize("image_orientation", ["RAS", "LAS", "LPS", "oblique"])
458+
@pytest.mark.parametrize("ongrid", [True, False])
459+
def test_constant_field_vs_ants(tmp_path, get_testdata, image_orientation, ongrid):
460+
"""Create a constant displacement field and compare mappings."""
461+
462+
rng = np.random.default_rng(RNG_SEED)
463+
464+
nii = get_testdata[image_orientation]
465+
466+
# Get sampling indices
467+
coords_xyz, points_ijk, grid_xyz, shape, ref_affine, reference, subsample = (
468+
get_points(nii, ongrid, npoints=5, rng=rng)
469+
)
470+
471+
tol = (
472+
{"atol": 0, "rtol": 1e-4}
473+
if image_orientation != "oblique"
474+
else {"atol": 1e-4, "rtol": 1e-2}
475+
)
476+
coords_map = grid_xyz.reshape(*shape, 3)
477+
478+
deltas = np.hstack(
479+
(
480+
np.zeros(np.prod(shape)),
481+
np.linspace(-80, 80, num=np.prod(shape)),
482+
np.linspace(-50, 50, num=np.prod(shape)),
483+
)
484+
).reshape(shape + (3,))
485+
gold_mapped_xyz = coords_map + deltas
486+
487+
fieldnii = nb.Nifti1Image(deltas, ref_affine, None)
488+
warpfile = tmp_path / "itk_transform.nii.gz"
489+
490+
# Ensure direct (xfm) and ITK roundtrip (itk_xfm) are equivalent
491+
xfm = nitnl.DenseFieldTransform(fieldnii)
492+
xfm.to_filename(warpfile, fmt="itk")
493+
itk_xfm = nitnl.DenseFieldTransform(ITKDisplacementsField.from_filename(warpfile))
494+
495+
np.testing.assert_allclose(xfm.reference.affine, itk_xfm.reference.affine)
496+
np.testing.assert_allclose(ref_affine, itk_xfm.reference.affine)
497+
np.testing.assert_allclose(xfm.reference.shape, itk_xfm.reference.shape)
498+
np.testing.assert_allclose(xfm._field, itk_xfm._field, **tol)
499+
if image_orientation != "oblique":
500+
assert xfm == itk_xfm
501+
502+
# Ensure deltas and mapped grid are equivalent
503+
orig_grid_mapped_xyz = xfm.map(grid_xyz).reshape(*shape, -1)
504+
np.testing.assert_allclose(gold_mapped_xyz, orig_grid_mapped_xyz)
505+
506+
# Test ANTs mapping
507+
grid_mapped_xyz = itk_xfm.map(grid_xyz).reshape(*shape, -1)
508+
509+
# Check apparent healthiness of mapping
510+
np.testing.assert_allclose(gold_mapped_xyz, grid_mapped_xyz, **tol)
511+
np.testing.assert_allclose(orig_grid_mapped_xyz, grid_mapped_xyz, **tol)
512+
513+
csvout = tmp_path / "mapped_xyz.csv"
514+
csvin = tmp_path / "coords_xyz.csv"
515+
# antsApplyTransformsToPoints wants LPS coordinates, see last post at
516+
# http://sourceforge.net/p/advants/discussion/840261/thread/2a1e9307/
517+
lps_xyz = coords_xyz.copy() * (-1, -1, 1)
518+
np.savetxt(csvin, lps_xyz, delimiter=",", header="x,y,z", comments="")
519+
520+
cmd = f"antsApplyTransformsToPoints -d 3 -i {csvin} -o {csvout} -t {warpfile}"
521+
exe = cmd.split()[0]
522+
if not shutil.which(exe):
523+
pytest.skip(f"Command {exe} not found on host")
524+
check_call(cmd, shell=True)
525+
526+
ants_res = np.genfromtxt(csvout, delimiter=",", names=True)
527+
# antsApplyTransformsToPoints writes LPS coordinates, see last post at
528+
# http://sourceforge.net/p/advants/discussion/840261/thread/2a1e9307/
529+
ants_pts = np.vstack([ants_res[n] for n in ("x", "y", "z")]).T * (-1, -1, 1)
530+
531+
nb.Nifti1Image(grid_mapped_xyz, ref_affine, None).to_filename(
532+
tmp_path / "grid_mapped.nii.gz"
533+
)
534+
nb.Nifti1Image(coords_map, ref_affine, None).to_filename(
535+
tmp_path / "baseline_field.nii.gz"
536+
)
537+
nb.Nifti1Image(gold_mapped_xyz, ref_affine, None).to_filename(
538+
tmp_path / "gold_mapped_xyz.nii.gz"
539+
)
540+
541+
if ongrid:
542+
ants_pts = ants_pts.reshape(*shape, 3)
543+
544+
nb.Nifti1Image(ants_pts, ref_affine, None).to_filename(
545+
tmp_path / "ants_mapped_xyz.nii.gz"
546+
)
547+
np.testing.assert_allclose(gold_mapped_xyz, ants_pts, rtol=1e-2, atol=1e-3)
548+
np.testing.assert_allclose(deltas, ants_pts - coords_map, rtol=1e-2, atol=1e-3)
549+
else:
550+
# TODO Change test to norms and investigate extreme cases
551+
# We're likely hitting OBB points (see gh-188)
552+
# https://github.com/nipy/nitransforms/pull/188
553+
np.testing.assert_allclose(
554+
xfm.map(coords_xyz) - coords_xyz, ants_pts - coords_xyz, rtol=1, atol=1
555+
)

0 commit comments

Comments
 (0)