From 3abff0dbf2539143451b0ff21ed7f3227afe6a71 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Thu, 28 Jul 2016 16:10:53 -0700 Subject: [PATCH 1/3] NF: add function to check spatial axes are first For the processing and the visualization routines, we need to know if the spatial axes are first, followed by time etc. Make a crude check that we can depend on spatial axes being first. --- nibabel/imageclasses.py | 19 ++++++++++++ nibabel/tests/test_imageclasses.py | 48 ++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 nibabel/tests/test_imageclasses.py diff --git a/nibabel/imageclasses.py b/nibabel/imageclasses.py index 239b4fea56..14591eaeda 100644 --- a/nibabel/imageclasses.py +++ b/nibabel/imageclasses.py @@ -104,3 +104,22 @@ def __getitem__(self, *args, **kwargs): ('mgz', '.mgz'), ('par', '.par'), )) + + +def spatial_axes_first(img): + """ True if spatial image axes for `img` always preceed other axes + + Parameters + ---------- + img : object + Image object implementing at least ``shape`` attribute. + + Returns + ------- + spatial_axes_first : bool + True if image only has spatial axes (number of axes < 4) or image type + known to have spatial axes preceeding other axes. + """ + if len(img.shape) < 4: + return True + return not isinstance(img, Minc1Image) diff --git a/nibabel/tests/test_imageclasses.py b/nibabel/tests/test_imageclasses.py new file mode 100644 index 0000000000..ddad56e1f9 --- /dev/null +++ b/nibabel/tests/test_imageclasses.py @@ -0,0 +1,48 @@ +""" Testing imageclasses module +""" + +from os.path import dirname, join as pjoin + +import numpy as np + +from nibabel.optpkg import optional_package + +import nibabel as nib +from nibabel.analyze import AnalyzeImage +from nibabel.nifti1 import Nifti1Image +from nibabel.nifti2 import Nifti2Image + +from nibabel.imageclasses import spatial_axes_first + +from nose.tools import (assert_true, assert_false) + +DATA_DIR = pjoin(dirname(__file__), 'data') + +have_h5py = optional_package('h5py')[1] + +MINC_3DS = ('minc1_1_scale.mnc',) +MINC_4DS = ('minc1_4d.mnc',) +if have_h5py: + MINC_3DS = MINC_3DS + ('minc2_1_scale.mnc',) + MINC_4DS = MINC_4DS + ('minc2_4d.mnc',) + + +def test_spatial_axes_first(): + # Function tests is spatial axes are first three axes in image + # Always True for Nifti and friends + affine = np.eye(4) + for shape in ((2, 3), (4, 3, 2), (5, 4, 1, 2), (2, 3, 5, 2, 1)): + for img_class in (AnalyzeImage, Nifti1Image, Nifti2Image): + data = np.zeros(shape) + img = img_class(data, affine) + assert_true(spatial_axes_first(img)) + # True for MINC images < 4D + for fname in MINC_3DS: + img = nib.load(pjoin(DATA_DIR, fname)) + assert_true(len(img.shape) == 3) + assert_true(spatial_axes_first(img)) + # False for MINC images < 4D + for fname in MINC_4DS: + img = nib.load(pjoin(DATA_DIR, fname)) + assert_true(len(img.shape) == 4) + assert_false(spatial_axes_first(img)) From 214dd3ba3b3c0f2e7861dfb78f341c6b7b8126e9 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Thu, 28 Jul 2016 16:55:52 -0700 Subject: [PATCH 2/3] RF+TST: add test for 4D MINC to processing + test Processing routines should barf on 4D MINC because they do not know how to work out the spatial axes. Use new `spatial_axes_first` function to check for problem images, raise. Test errors raised. --- nibabel/processing.py | 21 +++++++++++++++------ nibabel/tests/test_processing.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/nibabel/processing.py b/nibabel/processing.py index b1cc867496..cf9f60c76c 100644 --- a/nibabel/processing.py +++ b/nibabel/processing.py @@ -25,6 +25,7 @@ from .affines import AffineError, to_matvec, from_matvec, append_diag from .spaces import vox2out_vox from .nifti1 import Nifti1Image +from .imageclasses import spatial_axes_first SIGMA2FWHM = np.sqrt(8 * np.log(2)) @@ -124,9 +125,9 @@ def resample_from_to(from_img, Parameters ---------- from_img : object - Object having attributes ``dataobj``, ``affine``, ``header``. If - `out_class` is not None, ``img.__class__`` should be able to construct - an image from data, affine and header. + Object having attributes ``dataobj``, ``affine``, ``header`` and + ``shape``. If `out_class` is not None, ``img.__class__`` should be able + to construct an image from data, affine and header. to_vox_map : image object or length 2 sequence If object, has attributes ``shape`` giving input voxel shape, and ``affine`` giving mapping of input voxels to output space. If length 2 @@ -153,6 +154,10 @@ def resample_from_to(from_img, resampling `from_img` into axes aligned to the output space of ``from_img.affine`` """ + # This check requires `shape` attribute of image + if not spatial_axes_first(from_img): + raise ValueError('Cannot predict position of spatial axes for Image ' + 'type ' + str(type(from_img))) try: to_shape, to_affine = to_vox_map.shape, to_vox_map.affine except AttributeError: @@ -248,9 +253,9 @@ def smooth_image(img, Parameters ---------- img : object - Object having attributes ``dataobj``, ``affine``, ``header``. If - `out_class` is not None, ``img.__class__`` should be able to construct - an image from data, affine and header. + Object having attributes ``dataobj``, ``affine``, ``header`` and + ``shape``. If `out_class` is not None, ``img.__class__`` should be able + to construct an image from data, affine and header. fwhm : scalar or length 3 sequence FWHM *in mm* over which to smooth. The smoothing applies to the voxel axes, not to the output axes, but is in millimeters. The function @@ -280,6 +285,10 @@ def smooth_image(img, Image of instance specified by `out_class`, containing data output from smoothing `img` data by given FWHM kernel. """ + # This check requires `shape` attribute of image + if not spatial_axes_first(img): + raise ValueError('Cannot predict position of spatial axes for Image ' + 'type ' + str(type(img))) if out_class is None: out_class = img.__class__ n_dim = len(img.shape) diff --git a/nibabel/tests/test_processing.py b/nibabel/tests/test_processing.py index 72076918a4..c0f29c64c9 100644 --- a/nibabel/tests/test_processing.py +++ b/nibabel/tests/test_processing.py @@ -24,7 +24,8 @@ from nibabel.nifti1 import Nifti1Image from nibabel.nifti2 import Nifti2Image from nibabel.orientations import flip_axis, inv_ornt_aff -from nibabel.affines import AffineError, from_matvec, to_matvec, apply_affine +from nibabel.affines import (AffineError, from_matvec, to_matvec, apply_affine, + voxel_sizes) from nibabel.eulerangles import euler2mat from numpy.testing import (assert_almost_equal, @@ -41,6 +42,15 @@ DATA_DIR = pjoin(dirname(__file__), 'data') +# 3D MINC work correctly with processing, but not 4D MINC +from .test_imageclasses import MINC_3DS, MINC_4DS + +# Filenames of other images that should work correctly with processing +OTHER_IMGS = ('anatomical.nii', 'functional.nii', + 'example4d.nii.gz', 'example_nifti2.nii.gz', + 'phantom_EPI_asc_CLEAR_2_1.PAR') + + def test_sigma2fwhm(): # Test from constant assert_almost_equal(sigma2fwhm(1), 2.3548200) @@ -346,6 +356,26 @@ def test_smooth_image(): Nifti2Image) +@needs_scipy +def test_spatial_axes_check(): + for fname in MINC_3DS + OTHER_IMGS: + img = nib.load(pjoin(DATA_DIR, fname)) + s_img = smooth_image(img, 0) + assert_array_equal(img.dataobj, s_img.dataobj) + out = resample_from_to(img, img, mode='nearest') + assert_almost_equal(img.dataobj, out.dataobj) + if len(img.shape) > 3: + continue + # Resample to output does not raise an error + out = resample_to_output(img, voxel_sizes(img.affine)) + for fname in MINC_4DS: + img = nib.load(pjoin(DATA_DIR, fname)) + assert_raises(ValueError, smooth_image, img, 0) + assert_raises(ValueError, resample_from_to, img, img, mode='nearest') + assert_raises(ValueError, + resample_to_output, img, voxel_sizes(img.affine)) + + def assert_spm_resampling_close(from_img, our_resampled, spm_resampled): """ Assert our resampling is close to SPM's, allowing for edge effects """ From 3294e293bc2d917f67681ef37e9ad691ec13d35c Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Thu, 28 Jul 2016 20:45:09 -0700 Subject: [PATCH 3/3] RF: make opt-in list of spatial-first image types From suggestion by Chris M, make an opt-in list of image types known to be spatial axes first, so that new image types don't accidentally imply they are spatial axes first. --- nibabel/imageclasses.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/nibabel/imageclasses.py b/nibabel/imageclasses.py index 14591eaeda..3bd2911336 100644 --- a/nibabel/imageclasses.py +++ b/nibabel/imageclasses.py @@ -105,6 +105,13 @@ def __getitem__(self, *args, **kwargs): ('par', '.par'), )) +# Image classes known to require spatial axes to be first in index ordering. +# When adding an image class, consider whether the new class should be listed +# here. +KNOWN_SPATIAL_FIRST = (Nifti1Pair, Nifti1Image, Nifti2Pair, Nifti2Image, + Spm2AnalyzeImage, Spm99AnalyzeImage, AnalyzeImage, + MGHImage, PARRECImage) + def spatial_axes_first(img): """ True if spatial image axes for `img` always preceed other axes @@ -122,4 +129,4 @@ def spatial_axes_first(img): """ if len(img.shape) < 4: return True - return not isinstance(img, Minc1Image) + return type(img) in KNOWN_SPATIAL_FIRST