Skip to content

Commit

Permalink
maint: reorganize tests around the spun-off apply
Browse files Browse the repository at this point in the history
  • Loading branch information
oesteban committed Jul 31, 2024
1 parent 85d03b4 commit 06a1c01
Show file tree
Hide file tree
Showing 4 changed files with 353 additions and 341 deletions.
146 changes: 0 additions & 146 deletions nitransforms/tests/test_linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,15 @@
import os
import pytest
import numpy as np
from subprocess import check_call
import shutil
import h5py

import nibabel as nb
from nibabel.eulerangles import euler2mat
from nibabel.affines import from_matvec
from nitransforms import linear as nitl
from nitransforms import io
from nitransforms.resampling import apply
from .utils import assert_affines_by_filename

RMSE_TOL = 0.1
APPLY_LINEAR_CMD = {
"fsl": """\
flirt -setbackground 0 -interp nearestneighbour -in {moving} -ref {reference} \
-applyxfm -init {transform} -out {resampled}\
""".format,
"itk": """\
antsApplyTransforms -d 3 -r {reference} -i {moving} \
-o {resampled} -n NearestNeighbor -t {transform} --float\
""".format,
"afni": """\
3dAllineate -base {reference} -input {moving} \
-prefix {resampled} -1Dmatrix_apply {transform} -final NN\
""".format,
"fs": """\
mri_vol2vol --mov {moving} --targ {reference} --lta {transform} \
--o {resampled} --nearest""".format,
}


@pytest.mark.parametrize("matrix", [[0.0], np.ones((3, 3, 3)), np.ones((3, 4)), ])
def test_linear_typeerrors1(matrix):
Expand Down Expand Up @@ -234,96 +212,6 @@ def test_linear_save(tmpdir, data_path, get_testdata, image_orientation, sw_tool
assert_affines_by_filename(xfm_fname1, xfm_fname2)


@pytest.mark.parametrize("image_orientation", ["RAS", "LAS", "LPS", 'oblique', ])
@pytest.mark.parametrize("sw_tool", ["itk", "fsl", "afni", "fs"])
def test_apply_linear_transform(tmpdir, get_testdata, get_testmask, image_orientation, sw_tool):
"""Check implementation of exporting affines to formats."""
tmpdir.chdir()

img = get_testdata[image_orientation]
msk = get_testmask[image_orientation]

# Generate test transform
T = from_matvec(euler2mat(x=0.9, y=0.001, z=0.001), [4.0, 2.0, -1.0])
xfm = nitl.Affine(T)
xfm.reference = img

ext = ""
if sw_tool == "itk":
ext = ".tfm"
elif sw_tool == "fs":
ext = ".lta"

img.to_filename("img.nii.gz")
msk.to_filename("mask.nii.gz")

# Write out transform file (software-dependent)
xfm_fname = f"M.{sw_tool}{ext}"
# Change reference dataset for AFNI & oblique
if (sw_tool, image_orientation) == ("afni", "oblique"):
io.afni.AFNILinearTransform.from_ras(
T,
moving=img,
reference=img,
).to_filename(xfm_fname)
else:
xfm.to_filename(xfm_fname, fmt=sw_tool)

cmd = APPLY_LINEAR_CMD[sw_tool](
transform=os.path.abspath(xfm_fname),
reference=os.path.abspath("mask.nii.gz"),
moving=os.path.abspath("mask.nii.gz"),
resampled=os.path.abspath("resampled_brainmask.nii.gz"),
)

# skip test if command is not available on host
exe = cmd.split(" ", 1)[0]
if not shutil.which(exe):
pytest.skip(f"Command {exe} not found on host")

# resample mask
exit_code = check_call([cmd], shell=True)
assert exit_code == 0
sw_moved_mask = nb.load("resampled_brainmask.nii.gz")

nt_moved_mask = apply(xfm, msk, order=0)
nt_moved_mask.set_data_dtype(msk.get_data_dtype())
nt_moved_mask.to_filename("ntmask.nii.gz")
diff = np.asanyarray(sw_moved_mask.dataobj) - np.asanyarray(nt_moved_mask.dataobj)

assert np.sqrt((diff ** 2).mean()) < RMSE_TOL
brainmask = np.asanyarray(nt_moved_mask.dataobj, dtype=bool)

cmd = APPLY_LINEAR_CMD[sw_tool](
transform=os.path.abspath(xfm_fname),
reference=os.path.abspath("img.nii.gz"),
moving=os.path.abspath("img.nii.gz"),
resampled=os.path.abspath("resampled.nii.gz"),
)

exit_code = check_call([cmd], shell=True)
assert exit_code == 0
sw_moved = nb.load("resampled.nii.gz")
sw_moved.set_data_dtype(img.get_data_dtype())

nt_moved = apply(xfm, img, order=0)
diff = (
np.asanyarray(sw_moved.dataobj, dtype=sw_moved.get_data_dtype())
- np.asanyarray(nt_moved.dataobj, dtype=nt_moved.get_data_dtype())
)

# A certain tolerance is necessary because of resampling at borders
assert np.sqrt((diff[brainmask] ** 2).mean()) < RMSE_TOL

nt_moved = apply(xfm, "img.nii.gz", order=0)
diff = (
np.asanyarray(sw_moved.dataobj, dtype=sw_moved.get_data_dtype())
- np.asanyarray(nt_moved.dataobj, dtype=nt_moved.get_data_dtype())
)
# A certain tolerance is necessary because of resampling at borders
assert np.sqrt((diff[brainmask] ** 2).mean()) < RMSE_TOL


def test_Affine_to_x5(tmpdir, testdata_path):
"""Test affine's operations."""
tmpdir.chdir()
Expand All @@ -336,40 +224,6 @@ def test_Affine_to_x5(tmpdir, testdata_path):
aff._to_hdf5(f.create_group("Affine"))


def test_LinearTransformsMapping_apply(tmp_path, data_path, testdata_path):
"""Apply transform mappings."""
hmc = nitl.load(
data_path / "hmc-itk.tfm", fmt="itk", reference=testdata_path / "sbref.nii.gz"
)
assert isinstance(hmc, nitl.LinearTransformsMapping)

# Test-case: realign functional data on to sbref
nii = apply(
hmc, testdata_path / "func.nii.gz", order=1, reference=testdata_path / "sbref.nii.gz"
)
assert nii.dataobj.shape[-1] == len(hmc)

# Test-case: write out a fieldmap moved with head
hmcinv = nitl.LinearTransformsMapping(
np.linalg.inv(hmc.matrix), reference=testdata_path / "func.nii.gz"
)

nii = apply(
hmcinv, testdata_path / "fmap.nii.gz", order=1
)
assert nii.dataobj.shape[-1] == len(hmc)

# Ensure a ValueError is issued when trying to do weird stuff
hmc = nitl.LinearTransformsMapping(hmc.matrix[:1, ...])
with pytest.raises(ValueError):
apply(
hmc,
testdata_path / "func.nii.gz",
order=1,
reference=testdata_path / "sbref.nii.gz",
)


def test_mulmat_operator(testdata_path):
"""Check the @ operator."""
ref = testdata_path / "someones_anatomy.nii.gz"
Expand Down
54 changes: 1 addition & 53 deletions nitransforms/tests/test_manip.py
Original file line number Diff line number Diff line change
@@ -1,67 +1,15 @@
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
"""Tests of nonlinear transforms."""
import os
import shutil
from subprocess import check_call
import pytest

import numpy as np
import nibabel as nb
from ..manip import load as _load, TransformChain
from ..manip import TransformChain
from ..linear import Affine
from .test_nonlinear import (
RMSE_TOL,
APPLY_NONLINEAR_CMD,
)
from nitransforms.resampling import apply

FMT = {"lta": "fs", "tfm": "itk"}


def test_itk_h5(tmp_path, testdata_path):
"""Check a translation-only field on one or more axes, different image orientations."""
os.chdir(str(tmp_path))
img_fname = testdata_path / "T1w_scanner.nii.gz"
xfm_fname = (
testdata_path
/ "ds-005_sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5"
)

xfm = _load(xfm_fname)

assert len(xfm) == 2

ref_fname = tmp_path / "reference.nii.gz"
nb.Nifti1Image(
np.zeros(xfm.reference.shape, dtype="uint16"), xfm.reference.affine,
).to_filename(str(ref_fname))

# Then apply the transform and cross-check with software
cmd = APPLY_NONLINEAR_CMD["itk"](
transform=xfm_fname,
reference=ref_fname,
moving=img_fname,
output="resampled.nii.gz",
extra="",
)

# skip test if command is not available on host
exe = cmd.split(" ", 1)[0]
if not shutil.which(exe):
pytest.skip(f"Command {exe} not found on host")

exit_code = check_call([cmd], shell=True)
assert exit_code == 0
sw_moved = nb.load("resampled.nii.gz")

nt_moved = apply(xfm, img_fname, order=0)
nt_moved.to_filename("nt_resampled.nii.gz")
diff = sw_moved.get_fdata() - nt_moved.get_fdata()
# A certain tolerance is necessary because of resampling at borders
assert (np.abs(diff) > 1e-3).sum() / diff.size < RMSE_TOL


@pytest.mark.parametrize("ext0", ["lta", "tfm"])
@pytest.mark.parametrize("ext1", ["lta", "tfm"])
@pytest.mark.parametrize("ext2", ["lta", "tfm"])
Expand Down
Loading

0 comments on commit 06a1c01

Please sign in to comment.