Skip to content

Commit 98c6fdf

Browse files
committed
Merge pull request #128 from matthew-brett/fix-orientation-hangs
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.
2 parents 6b9955c + b72c49d commit 98c6fdf

File tree

3 files changed

+101
-168
lines changed

3 files changed

+101
-168
lines changed

nibabel/funcs.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
''' Processor functions for images '''
1111
import numpy as np
1212

13-
from .orientations import (io_orientation, orientation_affine, flip_axis,
13+
from .orientations import (io_orientation, inv_ornt_aff, flip_axis,
1414
apply_orientation, OrientationError)
1515
from .loadsave import load
1616

@@ -189,7 +189,7 @@ def as_closest_canonical(img, enforce_diag=False):
189189
raise OrientationError('Transformed affine is not diagonal')
190190
return img
191191
shape = img.shape
192-
t_aff = orientation_affine(ornt, shape)
192+
t_aff = inv_ornt_aff(ornt, shape)
193193
out_aff = np.dot(aff, t_aff)
194194
# check if we are going to end up with something diagonal
195195
if enforce_diag and not _aff_is_diag(aff):

nibabel/orientations.py

Lines changed: 62 additions & 93 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))
@@ -88,51 +89,6 @@ def io_orientation(affine, tol=None):
8889
return ornt
8990

9091

91-
def _ornt_to_affine(orientations):
92-
''' Create affine transformation matrix determined by orientations.
93-
94-
This transformation will simply flip, transpose, and possibly drop some
95-
coordinates.
96-
97-
Parameters
98-
----------
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.
106-
107-
Returns
108-
-------
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
112-
'''
113-
ornt = np.asarray(orientations)
114-
n = ornt.shape[0]
115-
keep = ~np.isnan(ornt[:, 1])
116-
# These are the input coordinate axes that do have a matching output
117-
# column in the orientation. That is, if the 2nd row is [np.nan,
118-
# np.nan] then the orientation indicates that no output axes of an
119-
# affine with this orientation matches the 2nd input coordinate
120-
# axis. This would happen if the 2nd row of the affine that
121-
# generated ornt was [0,0,0,*]
122-
axes_kept = np.arange(n)[keep]
123-
m = keep.sum(0)
124-
# the matrix P represents the affine transform impled by ornt. If
125-
# all entries of ornt are not np.nan, then P is square otherwise it
126-
# has more columns than rows indicating some coordinates were
127-
# dropped
128-
P = np.zeros((m + 1, n + 1))
129-
P[-1, -1] = 1
130-
for idx in range(m):
131-
axs, flip = ornt[axes_kept[idx]]
132-
P[idx, axs] = flip
133-
return P
134-
135-
13692
def apply_orientation(arr, ornt):
13793
''' Apply transformations implied by `ornt` to the first
13894
n axes of the array `arr`
@@ -164,7 +120,6 @@ def apply_orientation(arr, ornt):
164120
if np.any(np.isnan(ornt[:, 0])):
165121
raise OrientationError('Cannot drop coordinates when '
166122
'applying orientation to data')
167-
shape = t_arr.shape
168123
# apply ornt transformations
169124
for ax, flip in enumerate(ornt[:, 1]):
170125
if flip == -1:
@@ -176,8 +131,8 @@ def apply_orientation(arr, ornt):
176131
return t_arr
177132

178133

179-
def orientation_affine(ornt, shape):
180-
''' Affine transform resulting from transforms implied in `ornt`
134+
def inv_ornt_aff(ornt, shape):
135+
''' Affine transform reversing transforms implied in `ornt`
181136
182137
Imagine you have an array ``arr`` of shape `shape`, and you apply the
183138
transforms implied by `ornt` (more below), to get ``tarr``.
@@ -187,41 +142,55 @@ def orientation_affine(ornt, shape):
187142
188143
Parameters
189144
----------
190-
ornt : (n,2) ndarray
191-
orientation transform. ``ornt[N,1]` is flip of axis N of the
192-
array implied by `shape`, where 1 means no flip and -1 means
193-
flip. For example, if ``N==0`` and ``ornt[0,1] == -1``, and
194-
there's an array ``arr`` of shape `shape`, the flip would
195-
correspond to the effect of ``np.flipud(arr)``. ``ornt[:,0]`` is
196-
the transpose that needs to be done to the implied array, as in
197-
``arr.transpose(ornt[:,0])``
198-
199-
shape : length n sequence
145+
ornt : (p, 2) ndarray
146+
orientation transform. ``ornt[P, 1]` is flip of axis N of the array
147+
implied by `shape`, where 1 means no flip and -1 means flip. For
148+
example, if ``P==0`` and ``ornt[0, 1] == -1``, and there's an array
149+
``arr`` of shape `shape`, the flip would correspond to the effect of
150+
``np.flipud(arr)``. ``ornt[:,0]`` gives us the (reverse of the)
151+
transpose that has been done to ``arr``. If there are any NaNs in
152+
`ornt`, we raise an ``OrientationError`` (see notes)
153+
shape : length p sequence
200154
shape of array you may transform with `ornt`
201155
202156
Returns
203157
-------
204-
transformed_affine : (n+1,n+1) ndarray
205-
An array ``arr`` (shape `shape`) might be transformed according
206-
to `ornt`, resulting in a transformed array ``tarr``.
207-
`transformed_affine` is the transform that takes you from array
208-
coordinates in ``tarr`` to array coordinates in ``arr``.
158+
transform_affine : (p + 1, p + 1) ndarray
159+
An array ``arr`` (shape `shape`) might be transformed according to
160+
`ornt`, resulting in a transformed array ``tarr``. `transformed_affine`
161+
is the transform that takes you from array coordinates in ``tarr`` to
162+
array coordinates in ``arr``.
163+
164+
Notes
165+
-----
166+
If a row in `ornt` contains NaN, this means that the input row does not
167+
influence the output space, and is thus effectively dropped from the output
168+
space. In that case one ``tarr`` coordinate maps to many ``arr``
169+
coordinates, we can't invert the transform, and we raise an error
209170
'''
210171
ornt = np.asarray(ornt)
211-
n = ornt.shape[0]
212-
shape = np.array(shape)[:n]
172+
if np.any(np.isnan(ornt)):
173+
raise OrientationError("We cannot invert orientation transform")
174+
p = ornt.shape[0]
175+
shape = np.array(shape)[:p]
213176
# ornt implies a flip, followed by a transpose. We need the affine
214177
# that inverts these. Thus we need the affine that first undoes the
215178
# effect of the transpose, then undoes the effects of the flip.
216179
# ornt indicates the transpose that has occurred to get the current
217-
# ordering, relative to canonical, so we just use that
218-
undo_reorder = np.eye(n + 1)[list(ornt[:, 0]) + [n], :]
180+
# ordering, relative to canonical, so we just use that.
181+
# undo_reorder is a row permutatation matrix
182+
undo_reorder = np.eye(p + 1)[list(ornt[:, 0]) + [p], :]
219183
undo_flip = np.diag(list(ornt[:, 1]) + [1.0])
220184
center_trans = -(shape - 1) / 2.0
221-
undo_flip[:n, n] = (ornt[:, 1] * center_trans) - center_trans
185+
undo_flip[:p, p] = (ornt[:, 1] * center_trans) - center_trans
222186
return np.dot(undo_flip, undo_reorder)
223187

224188

189+
@np.deprecate_with_doc("Please use inv_ornt_aff instead")
190+
def orientation_affine(ornt, shape):
191+
return inv_ornt_aff(ornt, shape)
192+
193+
225194
def flip_axis(arr, axis=0):
226195
''' Flip contents of `axis` in array `arr`
227196

nibabel/tests/test_orientations.py

Lines changed: 37 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414

1515
from numpy.testing import assert_array_equal, assert_array_almost_equal
1616

17-
from ..orientations import (io_orientation, orientation_affine,
18-
flip_axis, _ornt_to_affine,
19-
apply_orientation, OrientationError,
20-
ornt2axcodes, aff2axcodes)
17+
from ..orientations import (io_orientation, inv_ornt_aff, flip_axis,
18+
apply_orientation, OrientationError, ornt2axcodes,
19+
aff2axcodes)
20+
21+
from ..affines import from_matvec, to_matvec
2122

2223

2324
IN_ARRS = [np.eye(4),
@@ -147,23 +148,34 @@ def test_flip_axis():
147148

148149

149150
def test_io_orientation():
150-
shape = (2,3,4)
151-
for in_arr, out_ornt in zip(IN_ARRS, OUT_ORNTS):
152-
ornt = io_orientation(in_arr)
153-
assert_array_equal(ornt, out_ornt)
154-
taff = orientation_affine(ornt, shape)
155-
assert_true(same_transform(taff, ornt, shape))
156-
for axno in range(3):
157-
arr = in_arr.copy()
158-
ex_ornt = out_ornt.copy()
159-
# flip the input axis in affine
160-
arr[:,axno] *= -1
161-
# check that result shows flip
162-
ex_ornt[axno, 1] *= -1
163-
ornt = io_orientation(arr)
164-
assert_array_equal(ornt, ex_ornt)
165-
taff = orientation_affine(ornt, shape)
151+
for shape in ((2,3,4), (20, 15, 7)):
152+
for in_arr, out_ornt in zip(IN_ARRS, OUT_ORNTS):
153+
ornt = io_orientation(in_arr)
154+
assert_array_equal(ornt, out_ornt)
155+
taff = inv_ornt_aff(ornt, shape)
166156
assert_true(same_transform(taff, ornt, shape))
157+
for axno in range(3):
158+
arr = in_arr.copy()
159+
ex_ornt = out_ornt.copy()
160+
# flip the input axis in affine
161+
arr[:,axno] *= -1
162+
# check that result shows flip
163+
ex_ornt[axno, 1] *= -1
164+
ornt = io_orientation(arr)
165+
assert_array_equal(ornt, ex_ornt)
166+
taff = inv_ornt_aff(ornt, shape)
167+
assert_true(same_transform(taff, ornt, shape))
168+
# Test nasty hang for zero columns
169+
rzs = np.c_[np.diag([2, 3, 4, 5]), np.zeros((4,3))]
170+
arr = from_matvec(rzs, [15,16,17,18])
171+
ornt = io_orientation(arr)
172+
assert_array_equal(ornt, [[0, 1],
173+
[1, 1],
174+
[2, 1],
175+
[3, 1],
176+
[np.nan, np.nan],
177+
[np.nan, np.nan],
178+
[np.nan, np.nan]])
167179

168180

169181
def test_ornt2axcodes():
@@ -205,56 +217,8 @@ def test_aff2axcodes():
205217
('B', 'R', 'U'))
206218

207219

208-
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-
213-
# this affine has output coordinates '-y','z','x' and is at t=16
214-
sliced_fmri_affine = np.array([[0,-1,0,3],
215-
[0,0,2,5],
216-
[3,0,0,4],
217-
[0,0,0,16],
218-
[0,0,0,1]])
219-
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
225-
assert_array_equal(final_affine,
226-
np.array([[3,0,0,4],
227-
[0,1,0,-3],
228-
[0,0,2,5],
229-
[0,0,0,1]]))
230-
231-
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
237-
ornt = [[2,-1],
238-
[np.nan,np.nan],
239-
[0,1]]
240-
# the reordering/flipping is represented by an affine that
241-
# takes the 3rd output coordinate and maps it to the
242-
# first, takes the 3rd, maps it to first and flips it
243-
A = np.array([[0,0,-1,0],
244-
[1,0,0,0],
245-
[0,0,0,1]])
246-
assert_array_equal(A, _ornt_to_affine(ornt))
247-
# a more complicated example. only the 1st, 3rd and 6th
248-
# coordinates appear in the output
249-
ornt = [[3,-1],
250-
[np.nan,np.nan],
251-
[0,1],
252-
[np.nan,np.nan],
253-
[np.nan,np.nan],
254-
[1,-1]]
255-
B = np.array([[0,0,0,-1,0,0,0],
256-
[1,0,0,0,0,0,0],
257-
[0,-1,0,0,0,0,0],
258-
[0,0,0,0,0,0,1]])
259-
assert_array_equal(B, _ornt_to_affine(ornt))
260-
220+
def test_inv_ornt_aff():
221+
# Extra tests for inv_ornt_aff routines (also tested in
222+
# io_orientations test)
223+
assert_raises(OrientationError, inv_ornt_aff,
224+
[[0, 1], [1, -1], [np.nan, np.nan]], (3, 4, 5))

0 commit comments

Comments
 (0)