From 8f6a9d116a5295aafc09fac3515e1fc1efcd5136 Mon Sep 17 00:00:00 2001 From: oesteban Date: Mon, 14 Oct 2019 14:42:57 -0700 Subject: [PATCH 1/4] ENH: Setting up a battery of tests This PR will attempt to set up a test-driven development framework for the implementation of functional features of nitransforms. --- nitransforms/conftest.py | 43 +++++++++++ nitransforms/tests/checkaffines.py | 21 ++++++ nitransforms/tests/data/affine-LAS-itk.tfm | 1 - nitransforms/tests/data/affine-LAS.itk.tfm | 1 + ...{affine-LPS-itk.tfm => affine-LPS.itk.tfm} | 0 ...{affine-RAS-itk.tfm => affine-RAS.itk.tfm} | 0 ...oblique-itk.tfm => affine-oblique.itk.tfm} | 0 nitransforms/tests/test_transform.py | 71 +++++-------------- 8 files changed, 82 insertions(+), 55 deletions(-) create mode 100644 nitransforms/tests/checkaffines.py delete mode 120000 nitransforms/tests/data/affine-LAS-itk.tfm create mode 120000 nitransforms/tests/data/affine-LAS.itk.tfm rename nitransforms/tests/data/{affine-LPS-itk.tfm => affine-LPS.itk.tfm} (100%) rename nitransforms/tests/data/{affine-RAS-itk.tfm => affine-RAS.itk.tfm} (100%) rename nitransforms/tests/data/{affine-oblique-itk.tfm => affine-oblique.itk.tfm} (100%) diff --git a/nitransforms/conftest.py b/nitransforms/conftest.py index 4bdbd723..c93daea9 100644 --- a/nitransforms/conftest.py +++ b/nitransforms/conftest.py @@ -5,6 +5,11 @@ import nibabel as nb import pytest import tempfile +import pkg_resources + +SOMEONES_ANATOMY = pkg_resources.resource_filename( + 'nitransforms', 'tests/data/someones_anatomy.nii.gz') +_data = None @pytest.fixture(autouse=True) @@ -25,3 +30,41 @@ def doctest_autoimport(doctest_namespace): doctest_namespace['testfile'] = nifti_fname yield tmpdir.cleanup() + + +@pytest.fixture +def data_path(): + """Return the test data folder.""" + return os.path.join(os.path.dirname(__file__), 'tests/data') + + +@pytest.fixture +def get_data(): + """Generate data in the requested orientation.""" + global _data + + if _data is not None: + return _data + + img = nb.load(SOMEONES_ANATOMY) + imgaff = img.affine + + _data = {'RAS': img} + newaff = imgaff.copy() + newaff[0, 0] *= -1.0 + newaff[0, 3] = imgaff.dot(np.hstack((np.array(img.shape[:3]) - 1, 1.0)))[0] + _data['LAS'] = nb.Nifti1Image(np.flip(img.get_fdata(), 0), newaff, img.header) + newaff = imgaff.copy() + newaff[0, 0] *= -1.0 + newaff[1, 1] *= -1.0 + newaff[:2, 3] = imgaff.dot(np.hstack((np.array(img.shape[:3]) - 1, 1.0)))[:2] + _data['LPS'] = nb.Nifti1Image(np.flip(np.flip(img.get_fdata(), 0), 1), newaff, img.header) + A = nb.volumeutils.shape_zoom_affine(img.shape, img.header.get_zooms(), x_flip=False) + R = nb.affines.from_matvec(nb.eulerangles.euler2mat(x=0.09, y=0.001, z=0.001)) + newaff = R.dot(A) + oblique_img = nb.Nifti1Image(img.get_fdata(), newaff, img.header) + oblique_img.header.set_qform(newaff, 1) + oblique_img.header.set_sform(newaff, 1) + _data['oblique'] = oblique_img + + return _data diff --git a/nitransforms/tests/checkaffines.py b/nitransforms/tests/checkaffines.py new file mode 100644 index 00000000..3bea4357 --- /dev/null +++ b/nitransforms/tests/checkaffines.py @@ -0,0 +1,21 @@ +"""Utilities for testing.""" +from pathlib import Path +import numpy as np + +from .. import linear as nbl + + +def assert_affines_by_filename(affine1, affine2): + """Check affines by filename.""" + affine1 = Path(affine1) + affine2 = Path(affine2) + assert affine1.suffix == affine2.suffix, 'affines of different type' + + if affine1.suffix.endswith('.tfm'): # An ITK transform + xfm1 = nbl.load(str(affine1), fmt='itk') + xfm2 = nbl.load(str(affine2), fmt='itk') + assert xfm1 == xfm2 + else: + xfm1 = np.loadtxt(str(affine1)) + xfm2 = np.loadtxt(str(affine2)) + np.testing.assert_almost_equal(xfm1, xfm2) diff --git a/nitransforms/tests/data/affine-LAS-itk.tfm b/nitransforms/tests/data/affine-LAS-itk.tfm deleted file mode 120000 index 849a5e16..00000000 --- a/nitransforms/tests/data/affine-LAS-itk.tfm +++ /dev/null @@ -1 +0,0 @@ -affine-RAS-itk.tfm \ No newline at end of file diff --git a/nitransforms/tests/data/affine-LAS.itk.tfm b/nitransforms/tests/data/affine-LAS.itk.tfm new file mode 120000 index 00000000..629b9bf2 --- /dev/null +++ b/nitransforms/tests/data/affine-LAS.itk.tfm @@ -0,0 +1 @@ +affine-RAS.itk.tfm \ No newline at end of file diff --git a/nitransforms/tests/data/affine-LPS-itk.tfm b/nitransforms/tests/data/affine-LPS.itk.tfm similarity index 100% rename from nitransforms/tests/data/affine-LPS-itk.tfm rename to nitransforms/tests/data/affine-LPS.itk.tfm diff --git a/nitransforms/tests/data/affine-RAS-itk.tfm b/nitransforms/tests/data/affine-RAS.itk.tfm similarity index 100% rename from nitransforms/tests/data/affine-RAS-itk.tfm rename to nitransforms/tests/data/affine-RAS.itk.tfm diff --git a/nitransforms/tests/data/affine-oblique-itk.tfm b/nitransforms/tests/data/affine-oblique.itk.tfm similarity index 100% rename from nitransforms/tests/data/affine-oblique-itk.tfm rename to nitransforms/tests/data/affine-oblique.itk.tfm diff --git a/nitransforms/tests/test_transform.py b/nitransforms/tests/test_transform.py index 86f404d3..0549ff51 100644 --- a/nitransforms/tests/test_transform.py +++ b/nitransforms/tests/test_transform.py @@ -2,73 +2,36 @@ # vi: set ft=python sts=4 ts=4 sw=4 et: """Tests of the transform module.""" import os -import numpy as np -from numpy.testing import assert_array_equal, assert_almost_equal, \ - assert_array_almost_equal import pytest -from nibabel.loadsave import load as loadimg -from nibabel.nifti1 import Nifti1Image from nibabel.eulerangles import euler2mat from nibabel.affines import from_matvec -from ..patched import shape_zoom_affine -from .. import linear as nbl -from nibabel.testing import (assert_equal, assert_not_equal, assert_true, - assert_false, assert_raises, - suppress_warnings, assert_dt_equal) from nibabel.tmpdirs import InTemporaryDirectory - -data_path = os.path.join(os.path.dirname(__file__), 'data') -SOMEONES_ANATOMY = os.path.join(data_path, 'someones_anatomy.nii.gz') +from .. import linear as nbl +from .checkaffines import assert_affines_by_filename -@pytest.mark.parametrize('image_orientation', ['RAS', 'LAS', 'LPS', 'oblique']) -def test_affines_save(image_orientation): +@pytest.mark.parametrize('image_orientation', [ + 'RAS', 'LAS', 'LPS', + # 'oblique', +]) +@pytest.mark.parametrize('sw_tool', ['itk', 'fsl', 'afni']) +def test_affines_save(data_path, get_data, image_orientation, sw_tool): """Check implementation of exporting affines to formats.""" + img = get_data[image_orientation] # Generate test transform - img = loadimg(SOMEONES_ANATOMY) - imgaff = img.affine - - if image_orientation == 'LAS': - newaff = imgaff.copy() - newaff[0, 0] *= -1.0 - newaff[0, 3] = imgaff.dot(np.hstack((np.array(img.shape[:3]) - 1, 1.0)))[0] - img = Nifti1Image(np.flip(img.get_fdata(), 0), newaff, img.header) - elif image_orientation == 'LPS': - newaff = imgaff.copy() - newaff[0, 0] *= -1.0 - newaff[1, 1] *= -1.0 - newaff[:2, 3] = imgaff.dot(np.hstack((np.array(img.shape[:3]) - 1, 1.0)))[:2] - img = Nifti1Image(np.flip(np.flip(img.get_fdata(), 0), 1), newaff, img.header) - elif image_orientation == 'oblique': - A = shape_zoom_affine(img.shape, img.header.get_zooms(), x_flip=False) - R = from_matvec(euler2mat(x=0.09, y=0.001, z=0.001)) - newaff = R.dot(A) - img = Nifti1Image(img.get_fdata(), newaff, img.header) - img.header.set_qform(newaff, 1) - img.header.set_sform(newaff, 1) - T = from_matvec(euler2mat(x=0.9, y=0.001, z=0.001), [4.0, 2.0, -1.0]) - xfm = nbl.Affine(T) xfm.reference = img - itk = nbl.load(os.path.join(data_path, 'affine-%s-itk.tfm' % image_orientation), - fmt='itk') - fsl = np.loadtxt(os.path.join(data_path, 'affine-%s.fsl' % image_orientation)) - afni = np.loadtxt(os.path.join(data_path, 'affine-%s.afni' % image_orientation)) + ext = '' + if sw_tool == 'itk': + ext = '.tfm' with InTemporaryDirectory(): - xfm.to_filename('M.tfm', fmt='itk') - xfm.to_filename('M.fsl', fmt='fsl') - xfm.to_filename('M.afni', fmt='afni') - - nb_itk = nbl.load('M.tfm', fmt='itk') - nb_fsl = np.loadtxt('M.fsl') - nb_afni = np.loadtxt('M.afni') - - assert_equal(itk, nb_itk) - assert_almost_equal(fsl, nb_fsl) - assert_almost_equal(afni, nb_afni) + xfm_fname1 = 'M.%s%s' % (sw_tool, ext) + xfm.to_filename(xfm_fname1, fmt=sw_tool) -# Create version not aligned to canonical \ No newline at end of file + xfm_fname2 = os.path.join( + data_path, 'affine-%s.%s%s' % (image_orientation, sw_tool, ext)) + assert_affines_by_filename(xfm_fname1, xfm_fname2) From 46e2560a84e802efb525faf34c99dd8ff76f4e99 Mon Sep 17 00:00:00 2001 From: oesteban Date: Mon, 14 Oct 2019 15:32:12 -0700 Subject: [PATCH 2/4] enh: add apply linear transform tests (closes #6) --- nitransforms/tests/test_transform.py | 60 ++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/nitransforms/tests/test_transform.py b/nitransforms/tests/test_transform.py index 0549ff51..f38381f0 100644 --- a/nitransforms/tests/test_transform.py +++ b/nitransforms/tests/test_transform.py @@ -3,20 +3,38 @@ """Tests of the transform module.""" import os import pytest +import numpy as np +from subprocess import check_call +import nibabel as nb from nibabel.eulerangles import euler2mat from nibabel.affines import from_matvec from nibabel.tmpdirs import InTemporaryDirectory from .. import linear as nbl from .checkaffines import assert_affines_by_filename +TESTS_BORDER_TOLERANCE = 0.05 +APPLY_LINEAR_CMD = { + 'fsl': """\ +flirt -setbackground 0 -interp nearestneighbour -in {moving} -ref {reference} \ +-applyxfm -init {transform} -out resampled.nii.gz\ +""".format, + 'itk': """\ +antsApplyTransforms -d 3 -r {reference} -i {moving} \ +-o resampled.nii.gz -n NearestNeighbor -t {transform} --float\ +""".format, + 'afni': """\ +3dAllineate -base {reference} -input {moving} \ +-prefix resampled.nii.gz -1Dmatrix_apply {transform} -final NN\ +""".format, +} + @pytest.mark.parametrize('image_orientation', [ - 'RAS', 'LAS', 'LPS', - # 'oblique', + 'RAS', 'LAS', 'LPS', # 'oblique', ]) @pytest.mark.parametrize('sw_tool', ['itk', 'fsl', 'afni']) -def test_affines_save(data_path, get_data, image_orientation, sw_tool): +def test_linear_save(data_path, get_data, image_orientation, sw_tool): """Check implementation of exporting affines to formats.""" img = get_data[image_orientation] # Generate test transform @@ -35,3 +53,39 @@ def test_affines_save(data_path, get_data, image_orientation, sw_tool): xfm_fname2 = os.path.join( data_path, 'affine-%s.%s%s' % (image_orientation, sw_tool, ext)) 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']) +def test_apply_linear_transform(tmpdir, data_path, get_data, image_orientation, sw_tool): + """Check implementation of exporting affines to formats.""" + tmpdir.chdir() + + img = get_data[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 = nbl.Affine(T) + xfm.reference = img + + ext = '' + if sw_tool == 'itk': + ext = '.tfm' + + img.to_filename('img.nii.gz') + xfm_fname = 'M.%s%s' % (sw_tool, ext) + xfm.to_filename(xfm_fname, fmt=sw_tool) + + 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')) + exit_code = check_call([cmd], shell=True) + assert exit_code == 0 + sw_moved = nb.load('resampled.nii.gz') + + nt_moved = xfm.resample(img, order=0) + 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 < TESTS_BORDER_TOLERANCE From dbecbe870f155ceff105f1e7cd6843af4a6e5952 Mon Sep 17 00:00:00 2001 From: oesteban Date: Mon, 14 Oct 2019 15:45:21 -0700 Subject: [PATCH 3/4] enh: add loading affines test (Closes #2) --- nitransforms/tests/test_transform.py | 45 ++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/nitransforms/tests/test_transform.py b/nitransforms/tests/test_transform.py index f38381f0..b6562a17 100644 --- a/nitransforms/tests/test_transform.py +++ b/nitransforms/tests/test_transform.py @@ -30,6 +30,51 @@ } +@pytest.mark.parametrize('image_orientation', [ + 'RAS', 'LAS', 'LPS', # 'oblique', +]) +@pytest.mark.parametrize('sw_tool', ['itk', 'fsl', 'afni']) +def test_linear_load(tmpdir, data_path, get_data, image_orientation, sw_tool): + """Check implementation of loading affines from formats.""" + tmpdir.chdir() + + img = get_data[image_orientation] + img.to_filename('img.nii.gz') + + # Generate test transform + T = from_matvec(euler2mat(x=0.9, y=0.001, z=0.001), [4.0, 2.0, -1.0]) + xfm = nbl.Affine(T) + xfm.reference = img + + ext = '' + if sw_tool == 'itk': + ext = '.tfm' + + fname = 'affine-%s.%s%s' % (image_orientation, sw_tool, ext) + xfm_fname = os.path.join(data_path, fname) + + if sw_tool == 'fsl': + with pytest.raises("ValueError"): + loaded = nbl.load(xfm_fname, fmt=fname.rsplit('.', 1)[-1]) + with pytest.raises("ValueError"): + loaded = nbl.load(xfm_fname, fmt=fname.rsplit('.', 1)[-1], + reference='img.nii.gz') + with pytest.raises("ValueError"): + loaded = nbl.load(xfm_fname, fmt=fname.rsplit('.', 1)[-1], + moving='img.nii.gz') + + loaded = nbl.load(xfm_fname, fmt=fname.rsplit('.', 1)[-1], + moving='img.nii.gz', reference='img.nii.gz') + if sw_tool == 'afni': + with pytest.raises("ValueError"): + loaded = nbl.load(xfm_fname, fmt=fname.rsplit('.', 1)[-1]) + + loaded = nbl.load(xfm_fname, fmt=fname.rsplit('.', 1)[-1], + reference='img.nii.gz') + + assert loaded == xfm + + @pytest.mark.parametrize('image_orientation', [ 'RAS', 'LAS', 'LPS', # 'oblique', ]) From bb08caacf511ba8e6acc9193f9e3ca1a39f79a75 Mon Sep 17 00:00:00 2001 From: oesteban Date: Tue, 15 Oct 2019 09:56:49 -0700 Subject: [PATCH 4/4] enh: apply @mgxd's review comments --- nitransforms/tests/test_transform.py | 14 +++++++------- nitransforms/tests/{checkaffines.py => utils.py} | 0 2 files changed, 7 insertions(+), 7 deletions(-) rename nitransforms/tests/{checkaffines.py => utils.py} (100%) diff --git a/nitransforms/tests/test_transform.py b/nitransforms/tests/test_transform.py index b6562a17..cdad85d2 100644 --- a/nitransforms/tests/test_transform.py +++ b/nitransforms/tests/test_transform.py @@ -11,7 +11,7 @@ from nibabel.affines import from_matvec from nibabel.tmpdirs import InTemporaryDirectory from .. import linear as nbl -from .checkaffines import assert_affines_by_filename +from .utils import assert_affines_by_filename TESTS_BORDER_TOLERANCE = 0.05 APPLY_LINEAR_CMD = { @@ -55,21 +55,21 @@ def test_linear_load(tmpdir, data_path, get_data, image_orientation, sw_tool): if sw_tool == 'fsl': with pytest.raises("ValueError"): - loaded = nbl.load(xfm_fname, fmt=fname.rsplit('.', 1)[-1]) + loaded = nbl.load(xfm_fname, fmt=fname.split('.')[-1]) with pytest.raises("ValueError"): - loaded = nbl.load(xfm_fname, fmt=fname.rsplit('.', 1)[-1], + loaded = nbl.load(xfm_fname, fmt=fname.split('.')[-1], reference='img.nii.gz') with pytest.raises("ValueError"): - loaded = nbl.load(xfm_fname, fmt=fname.rsplit('.', 1)[-1], + loaded = nbl.load(xfm_fname, fmt=fname.split('.')[-1], moving='img.nii.gz') - loaded = nbl.load(xfm_fname, fmt=fname.rsplit('.', 1)[-1], + loaded = nbl.load(xfm_fname, fmt=fname.split('.')[-1], moving='img.nii.gz', reference='img.nii.gz') if sw_tool == 'afni': with pytest.raises("ValueError"): - loaded = nbl.load(xfm_fname, fmt=fname.rsplit('.', 1)[-1]) + loaded = nbl.load(xfm_fname, fmt=fname.split('.')[-1]) - loaded = nbl.load(xfm_fname, fmt=fname.rsplit('.', 1)[-1], + loaded = nbl.load(xfm_fname, fmt=fname.split('.')[-1], reference='img.nii.gz') assert loaded == xfm diff --git a/nitransforms/tests/checkaffines.py b/nitransforms/tests/utils.py similarity index 100% rename from nitransforms/tests/checkaffines.py rename to nitransforms/tests/utils.py