Skip to content

Commit 054e20c

Browse files
committed
TST: add tests against SPM resampling
Build some test images by resampling with SPM, and test our resampling against SPM's resampling.
1 parent 412fe6d commit 054e20c

9 files changed

+166
-9
lines changed

nibabel/tests/data/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
anat_moved.nii
2+
resampled_functional.nii

nibabel/tests/data/anatomical.nii

66.4 KB
Binary file not shown.

nibabel/tests/data/functional.nii

42.2 KB
Binary file not shown.

nibabel/tests/data/make_moved_anat.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
""" Make anatomical image with altered affine
2+
3+
* Add some rotations and translations to affine;
4+
* Save as ``.nii`` file so SPM can read it.
5+
6+
See ``resample_using_spm.m`` for processing of this generated image by SPM.
7+
"""
8+
9+
import numpy as np
10+
11+
import nibabel as nib
12+
from nibabel.eulerangles import euler2mat
13+
from nibabel.affines import from_matvec
14+
15+
img = nib.load('anatomical.nii')
16+
some_rotations = euler2mat(0.1, 0.2, 0.3)
17+
extra_affine = from_matvec(some_rotations, [3, 4, 5])
18+
moved_anat = nib.Nifti1Image(img.dataobj,
19+
extra_affine.dot(img.affine),
20+
img.header)
21+
moved_anat.set_data_dtype(np.float32)
22+
nib.save(moved_anat, 'anat_moved.nii')
47.3 KB
Binary file not shown.
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
% Script uses SPM to resample moved anatomical image.
2+
%
3+
% Run `python make_moved_anat.py` to generate file to work on.
4+
%
5+
% Run from the directory containing this file.
6+
% Works with Octave or MATLAB.
7+
% Needs SPM (5, 8 or 12) on the MATLAB path.
8+
P = {'functional.nii', 'anat_moved.nii'};
9+
% Resample without masking
10+
flags = struct('mask', false, 'mean', false, ...
11+
'interp', 1, 'which', 1, ...
12+
'prefix', 'resampled_');
13+
spm_reslice(P, flags);
14+
% Reorient to canonical orientation at 4mm resolution, polynomial interpolation
15+
to_canonical({'anat_moved.nii'}, 4, 'reoriented_', 1);
4.53 KB
Binary file not shown.

nibabel/tests/data/to_canonical.m

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
function to_canonical(imgs, vox_sizes, prefix, hold)
2+
% Resample images to canonical (transverse) orientation with given voxel sizes
3+
%
4+
% Inspired by ``reorient.m`` by John Ashburner:
5+
% http://blogs.warwick.ac.uk/files/nichols/reorient.m
6+
%
7+
% Parameters
8+
% ----------
9+
% imgs : char or cell array or struct array
10+
% Images to resample to canonical orientation.
11+
% vox_sizes : vector (3, 1), optional
12+
% Voxel sizes for output image.
13+
% prefix : char, optional
14+
% Prefix for output resampled images, default = 'r'
15+
% hold : float, optional
16+
% Hold (resampling method) value, default = 3.
17+
18+
if ~isstruct(imgs)
19+
imgs = spm_vol(imgs);
20+
end
21+
if nargin < 2
22+
vox_sizes = [1 1 1];
23+
elseif numel(vox_sizes) == 1
24+
vox_sizes = [vox_sizes vox_sizes vox_sizes];
25+
end
26+
vox_sizes = vox_sizes(:);
27+
if nargin < 3
28+
prefix = 'r';
29+
end
30+
if nargin < 4
31+
hold = 3;
32+
end
33+
34+
for vol_no = 1:numel(imgs)
35+
vol = imgs{vol_no}(1);
36+
% From:
37+
% http://stackoverflow.com/questions/4165859/generate-all-possible-combinations-of-the-elements-of-some-vectors-cartesian-pr
38+
sets = {[1, vol.dim(1)], [1, vol.dim(2)], [1, vol.dim(3)]};
39+
[x y z] = ndgrid(sets{:});
40+
corners = [x(:) y(:) z(:)];
41+
corner_coords = [corners ones(length(corners), 1)]';
42+
corner_mm = vol.mat * corner_coords;
43+
min_xyz = min(corner_mm(1:3, :), [], 2);
44+
max_xyz = max(corner_mm(1:3, :), [], 2);
45+
% Make output volume
46+
out_vol = vol;
47+
out_vol.private = [];
48+
out_vol.mat = diag([vox_sizes' 1]);
49+
out_vol.mat(1:3, 4) = min_xyz - vox_sizes;
50+
out_vol.dim(1:3) = ceil((max_xyz - min_xyz) ./ vox_sizes) + 1;
51+
[dpath, froot, ext] = fileparts(vol.fname);
52+
out_vol.fname = fullfile(dpath, [prefix froot ext]);
53+
out_vol = spm_create_vol(out_vol);
54+
% Resample original volume at output volume grid
55+
plane_size = out_vol.dim(1:2);
56+
for slice_no = 1:out_vol.dim(3)
57+
resamp_affine = inv(spm_matrix([0 0 -slice_no]) * inv(out_vol.mat) * vol.mat);
58+
slice_vals = spm_slice_vol(vol, resamp_affine, plane_size, hold);
59+
out_vol = spm_write_plane(out_vol, slice_vals, slice_no);
60+
end
61+
end

nibabel/tests/test_processing.py

+66-9
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,22 @@
1010
"""
1111
from __future__ import division, print_function
1212

13+
from os.path import dirname, join as pjoin
14+
1315
import numpy as np
1416
import numpy.linalg as npl
1517

16-
from ..optpkg import optional_package
18+
from nibabel.optpkg import optional_package
1719
spnd, have_scipy, _ = optional_package('scipy.ndimage')
1820

19-
from ..processing import (sigma2fwhm, fwhm2sigma, adapt_affine,
20-
resample_from_to, resample_to_output, smooth_image)
21-
from ..nifti1 import Nifti1Image
22-
from ..nifti2 import Nifti2Image
23-
from ..orientations import flip_axis, inv_ornt_aff
24-
from ..affines import AffineError, from_matvec, to_matvec
25-
from ..eulerangles import euler2mat
21+
import nibabel as nib
22+
from nibabel.processing import (sigma2fwhm, fwhm2sigma, adapt_affine,
23+
resample_from_to, resample_to_output, smooth_image)
24+
from nibabel.nifti1 import Nifti1Image
25+
from nibabel.nifti2 import Nifti2Image
26+
from nibabel.orientations import flip_axis, inv_ornt_aff
27+
from nibabel.affines import AffineError, from_matvec, to_matvec, apply_affine
28+
from nibabel.eulerangles import euler2mat
2629

2730
from numpy.testing import (assert_almost_equal,
2831
assert_array_equal)
@@ -31,10 +34,12 @@
3134
from nose.tools import (assert_true, assert_false, assert_raises,
3235
assert_equal, assert_not_equal)
3336

34-
from .test_spaces import assert_all_in, get_outspace_params
37+
from nibabel.tests.test_spaces import assert_all_in, get_outspace_params
38+
from nibabel.testing import assert_allclose_safely
3539

3640
needs_scipy = skipif(not have_scipy, 'These tests need scipy')
3741

42+
DATA_DIR = pjoin(dirname(__file__), 'data')
3843

3944
def test_sigma2fwhm():
4045
# Test from constant
@@ -339,3 +344,55 @@ def test_smooth_image():
339344
assert_equal(
340345
smooth_image(img_ni2, 0, out_class=None).__class__,
341346
Nifti2Image)
347+
348+
349+
def assert_spm_resampling_close(from_img, our_resampled, spm_resampled):
350+
""" Assert our resampling is close to SPM's, allowing for edge effects
351+
"""
352+
# To allow for differences in the way SPM and scipy.ndimage handle off-edge
353+
# interpolation, mask out voxels off edge
354+
to_img_shape = spm_resampled.shape
355+
to_img_affine = spm_resampled.affine
356+
to_vox_coords = np.indices(to_img_shape).transpose((1, 2, 3, 0))
357+
# Coordinates of to_img mapped to from_img
358+
to_to_from = npl.inv(from_img.affine).dot(to_img_affine)
359+
resamp_coords = apply_affine(to_to_from, to_vox_coords)
360+
# Places where SPM may not return default value but scipy.ndimage will (SPM
361+
# does not return zeros <0.05 from image edges).
362+
# See: https://github.com/nipy/nibabel/pull/255#issuecomment-186774173
363+
outside_vol = np.any((resamp_coords < 0) |
364+
(np.subtract(resamp_coords, from_img.shape) > -1),
365+
axis=-1)
366+
spm_res = np.where(outside_vol, np.nan, np.array(spm_resampled.dataobj))
367+
assert_allclose_safely(our_resampled.dataobj, spm_res)
368+
assert_almost_equal(our_resampled.affine, spm_resampled.affine, 5)
369+
370+
371+
@needs_scipy
372+
def test_against_spm_resample():
373+
# Test resampling against images resampled with SPM12
374+
# anatomical.nii has a diagonal -2, 2 2 affine;
375+
# functional.nii has a diagonal -4, 4 4 affine;
376+
# These are a bit boring, so first add some rotations and translations to
377+
# the anatomical image affine, and then resample to the first volume in the
378+
# functional, and compare to the same thing in SPM.
379+
# See ``make_moved_anat.py`` script in this directory for input to SPM.
380+
anat = nib.load(pjoin(DATA_DIR, 'anatomical.nii'))
381+
func = nib.load(pjoin(DATA_DIR, 'functional.nii'))
382+
some_rotations = euler2mat(0.1, 0.2, 0.3)
383+
extra_affine = from_matvec(some_rotations, [3, 4, 5])
384+
moved_anat = nib.Nifti1Image(anat.get_data().astype(float),
385+
extra_affine.dot(anat.affine),
386+
anat.header)
387+
one_func = nib.Nifti1Image(func.dataobj[..., 0],
388+
func.affine,
389+
func.header)
390+
moved2func = resample_from_to(moved_anat, one_func, order=1, cval=np.nan)
391+
spm_moved = nib.load(pjoin(DATA_DIR, 'resampled_anat_moved.nii'))
392+
assert_spm_resampling_close(moved_anat, moved2func, spm_moved)
393+
# Next we resample the rotated anatomical image to output space, and compare
394+
# to the same operation done with SPM (our own version of 'reorient.m' by
395+
# John Ashburner).
396+
moved2output = resample_to_output(moved_anat, 4, order=1, cval=np.nan)
397+
spm2output = nib.load(pjoin(DATA_DIR, 'reoriented_anat_moved.nii'))
398+
assert_spm_resampling_close(moved_anat, moved2output, spm2output);

0 commit comments

Comments
 (0)