Skip to content

Commit 8bf7bf5

Browse files
committed
RF: change to mapped_voxels as vox2out_vox input
Also refactor tests to be a little less repetive.
1 parent c2b0ff6 commit 8bf7bf5

File tree

2 files changed

+78
-73
lines changed

2 files changed

+78
-73
lines changed

nibabel/spaces.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
1313
A voxel space can be expressed by a shape implying an array, where the axes are
1414
the axes of the array.
15+
16+
A mapped voxel space (mapped voxels) is either:
17+
18+
* an image, with attributes ``shape`` (the voxel space) and ``affine`` (the
19+
mapping), or
20+
* a length 2 sequence with the same information (shape, affine).
1521
"""
1622

1723
from itertools import product
@@ -21,12 +27,11 @@
2127
from .affines import apply_affine
2228

2329

24-
def vox2out_vox(in_shape, in_affine, voxel_sizes=None):
25-
""" output-aligned shape, affine for input voxels implied by `in_shape`
26-
27-
The input (voxel) space is given by `in_shape`
30+
def vox2out_vox(mapped_voxels, voxel_sizes=None):
31+
""" output-aligned shape, affine for input implied by `mapped_voxels`
2832
29-
The mapping between input space and output space is `in_affine`
33+
The input (voxel) space, and the affine mapping to output space, are given
34+
in `mapped_voxels`.
3035
3136
The output space is implied by the affine, we don't need to know what that
3237
is, we just return something with the same (implied) output space.
@@ -39,10 +44,11 @@ def vox2out_vox(in_shape, in_affine, voxel_sizes=None):
3944
4045
Parameters
4146
----------
42-
in_shape : sequence
43-
shape of implied input image voxel block. Up to length 3.
44-
in_affine : (4, 4) array-like
45-
affine mapping voxel coordinates in `in_shape` to output coordinates.
47+
mapped_voxels : object or length 2 sequence
48+
If object, has attributes ``shape`` giving input voxel shape, and
49+
``affine`` giving mapping of input voxels to output space. If length 2
50+
sequence, elements are (shape, affine) with same meaning as above. The
51+
affine is a (4, 4) array-like.
4652
voxel_sizes : None or sequence
4753
Gives the diagonal entries of `output_affine` (except the trailing 1
4854
for the homogenous coordinates) (``output_affine == np.diag(voxel_sizes
@@ -53,14 +59,18 @@ def vox2out_vox(in_shape, in_affine, voxel_sizes=None):
5359
output_shape : sequence
5460
Shape of output image that has voxel axes aligned to original image
5561
output space axes, and encloses all the voxel data from the original
56-
image implied by `in_shape`.
62+
image implied by input shape.
5763
output_affine : (4, 4) array
5864
Affine of output image that has voxel axes aligned to the output axes
59-
implied by `in_affine`. Top-left 3 x 3 part of affine is diagonal with
65+
implied by input affine. Top-left 3 x 3 part of affine is diagonal with
6066
all positive entries. The entries come from `voxel_sizes` if
6167
specified, or are all 1. If the image is < 3D, then the missing
6268
dimensions will have a 1 in the matching diagonal.
6369
"""
70+
try:
71+
in_shape, in_affine = mapped_voxels.shape, mapped_voxels.affine
72+
except AttributeError:
73+
in_shape, in_affine = mapped_voxels
6474
n_axes = len(in_shape)
6575
if n_axes > 3:
6676
raise ValueError('This function can only deal with 3D images')

nibabel/tests/test_spaces.py

Lines changed: 57 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import numpy.linalg as npl
66

77
from ..spaces import vox2out_vox, slice2volume
8-
from ..affines import apply_affine
8+
from ..affines import apply_affine, from_matvec
9+
from ..nifti1 import Nifti1Image
910
from ..eulerangles import euler2mat
1011

1112

@@ -36,71 +37,65 @@ def assert_all_in(in_shape, in_affine, out_shape, out_affine):
3637

3738
def test_vox2out_vox():
3839
# Test world space bounding box
39-
shape, aff = vox2out_vox((2, 3, 4), np.eye(4))
40+
# Test basic case, identity, no voxel sizes passed
41+
shape, aff = vox2out_vox(((2, 3, 4), np.eye(4)))
4042
assert_array_equal(shape, (2, 3, 4))
41-
assert_true(isinstance(shape, tuple))
42-
assert_true(isinstance(shape[0], int))
4343
assert_array_equal(aff, np.eye(4))
44-
assert_all_in((2, 3, 4), np.eye(4), shape, aff)
45-
shape, aff = vox2out_vox((2, 3, 4), np.diag([-1, 1, 1, 1]))
46-
assert_array_equal(shape, (2, 3, 4))
47-
assert_array_equal(aff, [[1, 0, 0, -1], # axis reversed -> -ve offset
48-
[0, 1, 0, 0],
49-
[0, 0, 1, 0],
50-
[0, 0, 0, 1]])
51-
assert_all_in((2, 3, 4), np.diag([-1, 1, 1, 1]), shape, aff)
52-
# zooms for affine > 1 -> larger grid with default 1mm output voxels
53-
shape, aff = vox2out_vox((2, 3, 4), np.diag([4, 5, 6, 1]))
54-
assert_array_equal(shape, (5, 11, 19))
55-
assert_array_equal(aff, np.eye(4))
56-
assert_all_in((2, 3, 4), np.diag([4, 5, 6, 1]), shape, aff)
57-
# set output voxels to be same size as input. back to original shape
58-
shape, aff = vox2out_vox((2, 3, 4), np.diag([4, 5, 6, 1]), (4, 5, 6))
59-
assert_array_equal(shape, (2, 3, 4))
60-
assert_array_equal(aff, np.diag([4, 5, 6, 1]))
61-
assert_all_in((2, 3, 4), np.diag([4, 5, 6, 1]), shape, aff)
62-
# zero point preserved
63-
in_aff = [[1, 0, 0, 1], [0, 1, 0, 2], [0, 0, 1, 3], [0, 0, 0, 1]]
64-
shape, aff = vox2out_vox((2, 3, 4), in_aff)
65-
assert_array_equal(shape, (2, 3, 4))
66-
assert_array_equal(aff, in_aff)
67-
assert_all_in((2, 3, 4), in_aff, shape, aff)
68-
in_aff = [[1, 0, 0, -1], [0, 1, 0, -2], [0, 0, 1, -3], [0, 0, 0, 1]]
69-
shape, aff = vox2out_vox((2, 3, 4), in_aff)
70-
assert_array_equal(shape, (2, 3, 4))
71-
assert_array_equal(aff, in_aff)
72-
assert_all_in((2, 3, 4), in_aff, shape, aff)
73-
# rotation around third axis
74-
in_aff = np.eye(4)
75-
in_aff[:3, :3] = euler2mat(np.pi / 4)
76-
shape, aff = vox2out_vox((2, 3, 4), in_aff)
77-
# x diff, y diff now 3 cos pi / 4 == 2.12, ceil to 3, add 1
78-
assert_array_equal(shape, (4, 4, 4))
79-
# most negative x now 2 cos pi / 4
80-
assert_almost_equal(aff, [[1, 0, 0, -2 * np.cos(np.pi / 4)],
81-
[0, 1, 0, 0],
82-
[0, 0, 1, 0],
83-
[0, 0, 0, 1]])
84-
assert_all_in((2, 3, 4), in_aff, shape, aff)
44+
# Some affines as input to the tests
45+
trans_123 = [[1, 0, 0, 1], [0, 1, 0, 2], [0, 0, 1, 3], [0, 0, 0, 1]]
46+
trans_m123 = [[1, 0, 0, -1], [0, 1, 0, -2], [0, 0, 1, -3], [0, 0, 0, 1]]
47+
rot_3 = from_matvec(euler2mat(np.pi / 4), [0, 0, 0])
48+
for in_shape, in_aff, vox, out_shape, out_aff in (
49+
# Identity
50+
((2, 3, 4), np.eye(4), None, (2, 3, 4), np.eye(4)),
51+
# Flip first axis
52+
((2, 3, 4), np.diag([-1, 1, 1, 1]), None,
53+
(2, 3, 4), [[1, 0, 0, -1], # axis reversed -> -ve offset
54+
[0, 1, 0, 0],
55+
[0, 0, 1, 0],
56+
[0, 0, 0, 1]]),
57+
# zooms for affine > 1 -> larger grid with default 1mm output voxels
58+
((2, 3, 4), np.diag([4, 5, 6, 1]), None,
59+
(5, 11, 19), np.eye(4)),
60+
# set output voxels to be same size as input. back to original shape
61+
((2, 3, 4), np.diag([4, 5, 6, 1]), (4, 5, 6),
62+
(2, 3, 4), np.diag([4, 5, 6, 1])),
63+
# Translation preserved in output
64+
((2, 3, 4), trans_123, None,
65+
(2, 3, 4), trans_123),
66+
((2, 3, 4), trans_m123, None,
67+
(2, 3, 4), trans_m123),
68+
# rotation around 3rd axis
69+
((2, 3, 4), rot_3, None,
70+
# x diff, y diff now 3 cos pi / 4 == 2.12, ceil to 3, add 1
71+
# most negative x now 2 cos pi / 4
72+
(4, 4, 4), [[1, 0, 0, -2 * np.cos(np.pi / 4)],
73+
[0, 1, 0, 0],
74+
[0, 0, 1, 0],
75+
[0, 0, 0, 1]]),
76+
# Less than 3 axes
77+
((2, 3), np.eye(4), None,
78+
(2, 3), np.eye(4)),
79+
((2,), np.eye(4), None,
80+
(2,), np.eye(4)),
81+
# Number of voxel sizes matches length
82+
((2, 3), np.diag([4, 5, 6, 1]), (4, 5),
83+
(2, 3), np.diag([4, 5, 1, 1])),
84+
):
85+
img = Nifti1Image(np.ones(in_shape), in_aff)
86+
for input in ((in_shape, in_aff), img):
87+
shape, aff = vox2out_vox(input, vox)
88+
assert_all_in(in_shape, in_aff, shape, aff)
89+
assert_equal(shape, out_shape)
90+
assert_almost_equal(aff, out_aff)
91+
assert_true(isinstance(shape, tuple))
92+
assert_true(isinstance(shape[0], int))
8593
# Enforce number of axes
86-
assert_raises(ValueError, vox2out_vox, (2, 3, 4, 5), np.eye(4))
87-
assert_raises(ValueError, vox2out_vox, (2, 3, 4, 5, 6), np.eye(4))
88-
# Less than 3 is OK
89-
shape, aff = vox2out_vox((2, 3), np.eye(4))
90-
assert_array_equal(shape, (2, 3))
91-
assert_array_equal(aff, np.eye(4))
92-
assert_all_in((2, 3), np.eye(4), shape, aff)
93-
shape, aff = vox2out_vox((2,), np.eye(4))
94-
assert_array_equal(shape, (2,))
95-
assert_array_equal(aff, np.eye(4))
96-
assert_all_in((2,), np.eye(4), shape, aff)
97-
# Number of voxel sizes matches length
98-
shape, aff = vox2out_vox((2, 3), np.diag([4, 5, 6, 1]), (4, 5))
99-
assert_array_equal(shape, (2, 3))
100-
assert_array_equal(aff, np.diag([4, 5, 1, 1]))
94+
assert_raises(ValueError, vox2out_vox, ((2, 3, 4, 5), np.eye(4)))
95+
assert_raises(ValueError, vox2out_vox, ((2, 3, 4, 5, 6), np.eye(4)))
10196
# Voxel sizes must be positive
102-
assert_raises(ValueError, vox2out_vox, (2, 3, 4), np.eye(4), [-1, 1, 1])
103-
assert_raises(ValueError, vox2out_vox, (2, 3, 4), np.eye(4), [1, 0, 1])
97+
assert_raises(ValueError, vox2out_vox, ((2, 3, 4), np.eye(4), [-1, 1, 1]))
98+
assert_raises(ValueError, vox2out_vox, ((2, 3, 4), np.eye(4), [1, 0, 1]))
10499

105100

106101
def test_slice2volume():

0 commit comments

Comments
 (0)