Skip to content

Commit 22cef70

Browse files
Merge pull request #464 from matthew-brett/minc-defense
MRG: check for MINC using processing routines The processing routines assume that image have spatial axes before time etc axes, but this may well not be true for MINC images. Barf on MINC images > 3D for now. Later maybe we can investigate the image axis names to work out which axes are spatial.
2 parents a4724b7 + 3294e29 commit 22cef70

File tree

4 files changed

+120
-7
lines changed

4 files changed

+120
-7
lines changed

nibabel/imageclasses.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,29 @@ def __getitem__(self, *args, **kwargs):
104104
('mgz', '.mgz'),
105105
('par', '.par'),
106106
))
107+
108+
# Image classes known to require spatial axes to be first in index ordering.
109+
# When adding an image class, consider whether the new class should be listed
110+
# here.
111+
KNOWN_SPATIAL_FIRST = (Nifti1Pair, Nifti1Image, Nifti2Pair, Nifti2Image,
112+
Spm2AnalyzeImage, Spm99AnalyzeImage, AnalyzeImage,
113+
MGHImage, PARRECImage)
114+
115+
116+
def spatial_axes_first(img):
117+
""" True if spatial image axes for `img` always preceed other axes
118+
119+
Parameters
120+
----------
121+
img : object
122+
Image object implementing at least ``shape`` attribute.
123+
124+
Returns
125+
-------
126+
spatial_axes_first : bool
127+
True if image only has spatial axes (number of axes < 4) or image type
128+
known to have spatial axes preceeding other axes.
129+
"""
130+
if len(img.shape) < 4:
131+
return True
132+
return type(img) in KNOWN_SPATIAL_FIRST

nibabel/processing.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .affines import AffineError, to_matvec, from_matvec, append_diag
2626
from .spaces import vox2out_vox
2727
from .nifti1 import Nifti1Image
28+
from .imageclasses import spatial_axes_first
2829

2930
SIGMA2FWHM = np.sqrt(8 * np.log(2))
3031

@@ -124,9 +125,9 @@ def resample_from_to(from_img,
124125
Parameters
125126
----------
126127
from_img : object
127-
Object having attributes ``dataobj``, ``affine``, ``header``. If
128-
`out_class` is not None, ``img.__class__`` should be able to construct
129-
an image from data, affine and header.
128+
Object having attributes ``dataobj``, ``affine``, ``header`` and
129+
``shape``. If `out_class` is not None, ``img.__class__`` should be able
130+
to construct an image from data, affine and header.
130131
to_vox_map : image object or length 2 sequence
131132
If object, has attributes ``shape`` giving input voxel shape, and
132133
``affine`` giving mapping of input voxels to output space. If length 2
@@ -153,6 +154,10 @@ def resample_from_to(from_img,
153154
resampling `from_img` into axes aligned to the output space of
154155
``from_img.affine``
155156
"""
157+
# This check requires `shape` attribute of image
158+
if not spatial_axes_first(from_img):
159+
raise ValueError('Cannot predict position of spatial axes for Image '
160+
'type ' + str(type(from_img)))
156161
try:
157162
to_shape, to_affine = to_vox_map.shape, to_vox_map.affine
158163
except AttributeError:
@@ -248,9 +253,9 @@ def smooth_image(img,
248253
Parameters
249254
----------
250255
img : object
251-
Object having attributes ``dataobj``, ``affine``, ``header``. If
252-
`out_class` is not None, ``img.__class__`` should be able to construct
253-
an image from data, affine and header.
256+
Object having attributes ``dataobj``, ``affine``, ``header`` and
257+
``shape``. If `out_class` is not None, ``img.__class__`` should be able
258+
to construct an image from data, affine and header.
254259
fwhm : scalar or length 3 sequence
255260
FWHM *in mm* over which to smooth. The smoothing applies to the voxel
256261
axes, not to the output axes, but is in millimeters. The function
@@ -280,6 +285,10 @@ def smooth_image(img,
280285
Image of instance specified by `out_class`, containing data output from
281286
smoothing `img` data by given FWHM kernel.
282287
"""
288+
# This check requires `shape` attribute of image
289+
if not spatial_axes_first(img):
290+
raise ValueError('Cannot predict position of spatial axes for Image '
291+
'type ' + str(type(img)))
283292
if out_class is None:
284293
out_class = img.__class__
285294
n_dim = len(img.shape)

nibabel/tests/test_imageclasses.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
""" Testing imageclasses module
2+
"""
3+
4+
from os.path import dirname, join as pjoin
5+
6+
import numpy as np
7+
8+
from nibabel.optpkg import optional_package
9+
10+
import nibabel as nib
11+
from nibabel.analyze import AnalyzeImage
12+
from nibabel.nifti1 import Nifti1Image
13+
from nibabel.nifti2 import Nifti2Image
14+
15+
from nibabel.imageclasses import spatial_axes_first
16+
17+
from nose.tools import (assert_true, assert_false)
18+
19+
DATA_DIR = pjoin(dirname(__file__), 'data')
20+
21+
have_h5py = optional_package('h5py')[1]
22+
23+
MINC_3DS = ('minc1_1_scale.mnc',)
24+
MINC_4DS = ('minc1_4d.mnc',)
25+
if have_h5py:
26+
MINC_3DS = MINC_3DS + ('minc2_1_scale.mnc',)
27+
MINC_4DS = MINC_4DS + ('minc2_4d.mnc',)
28+
29+
30+
def test_spatial_axes_first():
31+
# Function tests is spatial axes are first three axes in image
32+
# Always True for Nifti and friends
33+
affine = np.eye(4)
34+
for shape in ((2, 3), (4, 3, 2), (5, 4, 1, 2), (2, 3, 5, 2, 1)):
35+
for img_class in (AnalyzeImage, Nifti1Image, Nifti2Image):
36+
data = np.zeros(shape)
37+
img = img_class(data, affine)
38+
assert_true(spatial_axes_first(img))
39+
# True for MINC images < 4D
40+
for fname in MINC_3DS:
41+
img = nib.load(pjoin(DATA_DIR, fname))
42+
assert_true(len(img.shape) == 3)
43+
assert_true(spatial_axes_first(img))
44+
# False for MINC images < 4D
45+
for fname in MINC_4DS:
46+
img = nib.load(pjoin(DATA_DIR, fname))
47+
assert_true(len(img.shape) == 4)
48+
assert_false(spatial_axes_first(img))

nibabel/tests/test_processing.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
from nibabel.nifti1 import Nifti1Image
2525
from nibabel.nifti2 import Nifti2Image
2626
from nibabel.orientations import flip_axis, inv_ornt_aff
27-
from nibabel.affines import AffineError, from_matvec, to_matvec, apply_affine
27+
from nibabel.affines import (AffineError, from_matvec, to_matvec, apply_affine,
28+
voxel_sizes)
2829
from nibabel.eulerangles import euler2mat
2930

3031
from numpy.testing import (assert_almost_equal,
@@ -41,6 +42,15 @@
4142

4243
DATA_DIR = pjoin(dirname(__file__), 'data')
4344

45+
# 3D MINC work correctly with processing, but not 4D MINC
46+
from .test_imageclasses import MINC_3DS, MINC_4DS
47+
48+
# Filenames of other images that should work correctly with processing
49+
OTHER_IMGS = ('anatomical.nii', 'functional.nii',
50+
'example4d.nii.gz', 'example_nifti2.nii.gz',
51+
'phantom_EPI_asc_CLEAR_2_1.PAR')
52+
53+
4454
def test_sigma2fwhm():
4555
# Test from constant
4656
assert_almost_equal(sigma2fwhm(1), 2.3548200)
@@ -346,6 +356,26 @@ def test_smooth_image():
346356
Nifti2Image)
347357

348358

359+
@needs_scipy
360+
def test_spatial_axes_check():
361+
for fname in MINC_3DS + OTHER_IMGS:
362+
img = nib.load(pjoin(DATA_DIR, fname))
363+
s_img = smooth_image(img, 0)
364+
assert_array_equal(img.dataobj, s_img.dataobj)
365+
out = resample_from_to(img, img, mode='nearest')
366+
assert_almost_equal(img.dataobj, out.dataobj)
367+
if len(img.shape) > 3:
368+
continue
369+
# Resample to output does not raise an error
370+
out = resample_to_output(img, voxel_sizes(img.affine))
371+
for fname in MINC_4DS:
372+
img = nib.load(pjoin(DATA_DIR, fname))
373+
assert_raises(ValueError, smooth_image, img, 0)
374+
assert_raises(ValueError, resample_from_to, img, img, mode='nearest')
375+
assert_raises(ValueError,
376+
resample_to_output, img, voxel_sizes(img.affine))
377+
378+
349379
def assert_spm_resampling_close(from_img, our_resampled, spm_resampled):
350380
""" Assert our resampling is close to SPM's, allowing for edge effects
351381
"""

0 commit comments

Comments
 (0)