Skip to content

Commit a943879

Browse files
committed
BF: fix nasty bug for orientations and 0 columns
Getting orientations for columns of all zeros in an affine resulted in numpy svd hanging. I realized in the mean time that the io_orientation function was not returning the correct number of rows. Extend tests, fix, improve docstrings.
1 parent 7460fd7 commit a943879

File tree

2 files changed

+68
-56
lines changed

2 files changed

+68
-56
lines changed

nibabel/orientations.py

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,42 +19,43 @@ class OrientationError(Exception):
1919
def io_orientation(affine, tol=None):
2020
''' Orientation of input axes in terms of output axes for `affine`
2121
22-
Valid for an affine transformation from ``m`` dimensions to ``n``
23-
dimensions (``affine.shape == (n+1, m+1)``).
22+
Valid for an affine transformation from ``p`` dimensions to ``q``
23+
dimensions (``affine.shape == (q + 1, p + 1)``).
2424
2525
The calculated orientations can be used to transform associated
26-
arrays to best match the output orientations. If ``n`` > ``m``, then
26+
arrays to best match the output orientations. If ``p`` > ``q``, then
2727
some of the output axes should be considered dropped in this
2828
orientation.
2929
3030
Parameters
3131
----------
32-
affine : (n+1,m+1) ndarray-like
33-
Transformation affine from ``m`` inputs to ``n`` outputs.
34-
Usually this will be a shape (4,4) matrix, transforming 3 inputs
35-
to 3 outputs, but the code also handles the more general case
32+
affine : (q+1, p+1) ndarray-like
33+
Transformation affine from ``p`` inputs to ``q`` outputs. Usually this
34+
will be a shape (4,4) matrix, transforming 3 inputs to 3 outputs, but the
35+
code also handles the more general case
3636
tol : {None, float}, optional
37-
threshold below which SVD values of the affine are considered
38-
zero. If `tol` is None, and ``S`` is an array with singular
39-
values for `affine`, and ``eps`` is the epsilon value for
40-
datatype of ``S``, then `tol` set to ``S.max() * eps``.
37+
threshold below which SVD values of the affine are considered zero. If
38+
`tol` is None, and ``S`` is an array with singular values for `affine`,
39+
and ``eps`` is the epsilon value for datatype of ``S``, then `tol` set to
40+
``S.max() * eps``.
4141
4242
Returns
4343
-------
44-
orientations : (n,2) ndarray
45-
one row per input axis, where the first value in each row is the
46-
closest corresponding output axis, and the second value is 1 if
47-
the input axis is in the same direction as the corresponding
48-
output axis, , and -1 if it is in the opposite direction, to the
49-
corresponding output axis. If a row is [np.nan, np.nan], which
50-
can happen when n > m, then this row should be considered
51-
dropped.
44+
orientations : (p, 2) ndarray
45+
one row per input axis, where the first value in each row is the closest
46+
corresponding output axis. The second value in each row is 1 if the input
47+
axis is in the same direction as the corresponding output axis and -1 if
48+
it is in the opposite direction. If a row is [np.nan, np.nan], which can
49+
happen when p > q, then this row should be considered dropped.
5250
'''
5351
affine = np.asarray(affine)
54-
n, m = affine.shape[0]-1, affine.shape[1]-1
55-
# extract the underlying rotation matrix
56-
RZS = affine[:n, :m]
52+
q, p = affine.shape[0]-1, affine.shape[1]-1
53+
# extract the underlying rotation, zoom, shear matrix
54+
RZS = affine[:q, :p]
5755
zooms = np.sqrt(np.sum(RZS * RZS, axis=0))
56+
# Zooms can be zero, in which case all elements in the column are zero, and
57+
# we can leave them as they are
58+
zooms[zooms == 0] = 1
5859
RS = RZS / zooms
5960
# Transform below is polar decomposition, returning the closest
6061
# shearless matrix R to RS
@@ -66,13 +67,13 @@ def io_orientation(affine, tol=None):
6667
R = np.dot(P[:, keep], Qs[keep])
6768
# the matrix R is such that np.dot(R,R.T) is projection onto the
6869
# columns of P[:,keep] and np.dot(R.T,R) is projection onto the rows
69-
# of Qs[keep]. R (== np.dot(R, np.eye(m))) gives rotation of the
70+
# of Qs[keep]. R (== np.dot(R, np.eye(p))) gives rotation of the
7071
# unit input vectors to output coordinates. Therefore, the row
7172
# index of abs max R[:,N], is the output axis changing most as input
7273
# axis N changes. In case there are ties, we choose the axes
7374
# iteratively, removing used axes from consideration as we go
74-
ornt = np.ones((n, 2), dtype=np.int8) * np.nan
75-
for in_ax in range(m):
75+
ornt = np.ones((p, 2), dtype=np.int8) * np.nan
76+
for in_ax in range(p):
7677
col = R[:, in_ax]
7778
if not np.alltrue(np.equal(col, 0)):
7879
out_ax = np.argmax(np.abs(col))
@@ -96,38 +97,37 @@ def _ornt_to_affine(orientations):
9697
9798
Parameters
9899
----------
99-
orientations : (n,2) ndarray
100-
one row per input axis, where the first value in each row is the
101-
closest corresponding output axis, and the second value is 1 if
102-
the input axis is in the same direction as the corresponding
103-
output axis, and -1 if it is in the opposite direction, to the
104-
corresponding output axis. If a row has first entry np.nan, then
105-
this axis is dropped from the output.
100+
orientations : (p, 2) ndarray
101+
one row per input axis, where the first value in each row is the closest
102+
corresponding output axis. The second value in each row is 1 if the input
103+
axis is in the same direction as the corresponding output axis and -1 if
104+
it is in the opposite direction. If a row is [np.nan, np.nan], which can
105+
happen when p > q, then this row should be considered dropped.
106106
107107
Returns
108108
-------
109-
affine : (m+1,n+1) ndarray
110-
matrix representing flipping / dropping axes. m is equal to the
111-
number of rows of orientations[:,0] that are not np.nan
109+
affine : (q + 1, p + 1) ndarray
110+
matrix representing flipping / dropping axes. q is equal to the number of
111+
rows of ``orientations[:, 0]`` that are not np.nan
112112
'''
113113
ornt = np.asarray(orientations)
114-
n = ornt.shape[0]
114+
p = ornt.shape[0]
115115
keep = ~np.isnan(ornt[:, 1])
116116
# These are the input coordinate axes that do have a matching output
117117
# column in the orientation. That is, if the 2nd row is [np.nan,
118118
# np.nan] then the orientation indicates that no output axes of an
119119
# affine with this orientation matches the 2nd input coordinate
120120
# axis. This would happen if the 2nd row of the affine that
121121
# generated ornt was [0,0,0,*]
122-
axes_kept = np.arange(n)[keep]
123-
m = keep.sum(0)
122+
axes_kept = np.arange(p)[keep]
123+
q = keep.sum(0)
124124
# the matrix P represents the affine transform impled by ornt. If
125125
# all entries of ornt are not np.nan, then P is square otherwise it
126126
# has more columns than rows indicating some coordinates were
127127
# dropped
128-
P = np.zeros((m + 1, n + 1))
128+
P = np.zeros((q + 1, p + 1))
129129
P[-1, -1] = 1
130-
for idx in range(m):
130+
for idx in range(q):
131131
axs, flip = ornt[axes_kept[idx]]
132132
P[idx, axs] = flip
133133
return P
@@ -164,7 +164,6 @@ def apply_orientation(arr, ornt):
164164
if np.any(np.isnan(ornt[:, 0])):
165165
raise OrientationError('Cannot drop coordinates when '
166166
'applying orientation to data')
167-
shape = t_arr.shape
168167
# apply ornt transformations
169168
for ax, flip in enumerate(ornt[:, 1]):
170169
if flip == -1:

nibabel/tests/test_orientations.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
apply_orientation, OrientationError,
2020
ornt2axcodes, aff2axcodes)
2121

22+
from ..affines import from_matvec, to_matvec
23+
2224

2325
IN_ARRS = [np.eye(4),
2426
[[0,0,1,0],
@@ -164,6 +166,17 @@ def test_io_orientation():
164166
assert_array_equal(ornt, ex_ornt)
165167
taff = orientation_affine(ornt, shape)
166168
assert_true(same_transform(taff, ornt, shape))
169+
# Test nasty hang for zero columns
170+
rzs = np.c_[np.diag([2, 3, 4, 5]), np.zeros((4,3))]
171+
arr = from_matvec(rzs, [15,16,17,18])
172+
ornt = io_orientation(arr)
173+
assert_array_equal(ornt, [[0, 1],
174+
[1, 1],
175+
[2, 1],
176+
[3, 1],
177+
[np.nan, np.nan],
178+
[np.nan, np.nan],
179+
[np.nan, np.nan]])
167180

168181

169182
def test_ornt2axcodes():
@@ -206,34 +219,35 @@ def test_aff2axcodes():
206219

207220

208221
def test_drop_coord():
209-
# given a 5x4 affine from slicing an fmri,
210-
# the orientations code should easily reorder and drop the t
211-
# axis
212-
222+
# given a 5x4 affine from slicing an fmri, the orientations codes should
223+
# reorder and drop the t axis
213224
# this affine has output coordinates '-y','z','x' and is at t=16
214225
sliced_fmri_affine = np.array([[0,-1,0,3],
215226
[0,0,2,5],
216227
[3,0,0,4],
217228
[0,0,0,16],
218229
[0,0,0,1]])
230+
# This gives a 3 by 2 matrix (because there are 3 input axes)
219231
ornt = io_orientation(sliced_fmri_affine)
220-
affine_that_drops_t_reorders_and_flips = _ornt_to_affine(ornt)
221-
final_affine = np.dot(affine_that_drops_t_reorders_and_flips,
222-
sliced_fmri_affine)
223-
# the output will be diagonal
224-
# with the 'y' row having been flipped and the 't' row dropped
232+
# 4 x 4 (3 input dimensions, no input dimension dropped)
233+
flipping_aff = _ornt_to_affine(ornt)
234+
# So we have to do the dropping part ourselves
235+
rzs, trans = to_matvec(flipping_aff)
236+
drop_flip_aff = from_matvec(np.hstack((rzs, np.zeros((3, 1)))), trans)
237+
final_affine = np.dot(drop_flip_aff, sliced_fmri_affine)
238+
# the output will be diagonal with the 'y' row having been flipped and the
239+
# 't' row dropped
225240
assert_array_equal(final_affine,
226241
np.array([[3,0,0,4],
227242
[0,1,0,-3],
228243
[0,0,2,5],
229244
[0,0,0,1]]))
230245

231246

232-
def test_ornt_to_affine():
233-
# this orientation indicates that the first output
234-
# axis of the affine is closest to the vector [0,0,-1],
235-
# the last is closest to [1,0,0] and
236-
# that the y coordinate ([0,1,0]) is dropped
247+
def test__ornt_to_affine():
248+
# this orientation indicates that the first output axis of the affine is
249+
# closest to the vector [0,0,-1], the last is closest to [1,0,0] and that
250+
# the y coordinate ([0,1,0]) is dropped
237251
ornt = [[2,-1],
238252
[np.nan,np.nan],
239253
[0,1]]
@@ -257,4 +271,3 @@ def test_ornt_to_affine():
257271
[0,-1,0,0,0,0,0],
258272
[0,0,0,0,0,0,1]])
259273
assert_array_equal(B, _ornt_to_affine(ornt))
260-

0 commit comments

Comments
 (0)