|
27 | 27 | from nitransforms.conftest import _datadir, _testdir
|
28 | 28 | from nitransforms.tests.utils import get_points
|
29 | 29 |
|
30 |
| -rng = np.random.default_rng() |
31 |
| - |
| 30 | +RNG_SEED = 202508140819 |
32 | 31 | LPS = np.diag([-1, -1, 1, 1])
|
33 | 32 | ITK_MAT = LPS.dot(np.ones((4, 4)).dot(LPS))
|
34 | 33 |
|
@@ -386,6 +385,8 @@ def test_itk_h5_field_order_fortran(tmp_path):
|
386 | 385 | @pytest.mark.parametrize("ongrid", [True, False])
|
387 | 386 | def test_densefield_map_vs_ants(testdata_path, tmp_path, ongrid):
|
388 | 387 | """Map points with DenseFieldTransform and compare to ANTs."""
|
| 388 | + |
| 389 | + rng = np.random.default_rng(RNG_SEED) |
389 | 390 | warpfile = (
|
390 | 391 | testdata_path
|
391 | 392 | / "regressions"
|
@@ -451,3 +452,104 @@ def test_densefield_map_vs_ants(testdata_path, tmp_path, ongrid):
|
451 | 452 | atol = 0 if ongrid else 1e-1
|
452 | 453 | rtol = 1e-4 if ongrid else 1e-3
|
453 | 454 | 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