Skip to content

Commit b66d63b

Browse files
committed
Merge pull request #413 from matthew-brett/add-voxel-size
MRG: function to return voxel size from affine Voxel sizes are the Euclidean lengths of the columns of the affine (excluding the last column, and assuming zeros in the last row).
2 parents d96945e + cf82e40 commit b66d63b

File tree

2 files changed

+77
-1
lines changed

2 files changed

+77
-1
lines changed

nibabel/affines.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,3 +254,45 @@ def dot_reduce(*args):
254254
... arg[N-2].dot(arg[N-1])))...``
255255
"""
256256
return reduce(lambda x, y: np.dot(y, x), args[::-1])
257+
258+
259+
def voxel_sizes(affine):
260+
r""" Return voxel size for each input axis given `affine`
261+
262+
The `affine` is the mapping between array (voxel) coordinates and mm
263+
(world) coordinates.
264+
265+
The voxel size for the first voxel (array) axis is the distance moved in
266+
world coordinates when moving one unit along the first voxel (array) axis.
267+
This is the distance between the world coordinate of voxel (0, 0, 0) and
268+
the world coordinate of voxel (1, 0, 0). The world coordinate vector of
269+
voxel coordinate vector (0, 0, 0) is given by ``v0 = affine.dot((0, 0, 0,
270+
1)[:3]``. The world coordinate vector of voxel vector (1, 0, 0) is
271+
``v1_ax1 = affine.dot((1, 0, 0, 1))[:3]``. The final 1 in the voxel
272+
vectors and the ``[:3]`` at the end are because the affine works on
273+
homogenous coodinates. The translations part of the affine is ``trans =
274+
affine[:3, 3]``, and the rotations, zooms and shearing part of the affine
275+
is ``rzs = affine[:3, :3]``. Because of the final 1 in the input voxel
276+
vector, ``v0 == rzs.dot((0, 0, 0)) + trans``, and ``v1_ax1 == rzs.dot((1,
277+
0, 0)) + trans``, and the difference vector is ``rzs.dot((0, 0, 0)) -
278+
rzs.dot((1, 0, 0)) == rzs.dot((1, 0, 0)) == rzs[:, 0]``. The distance
279+
vectors in world coordinates between (0, 0, 0) and (1, 0, 0), (0, 1, 0),
280+
(0, 0, 1) are given by ``rzs.dot(np.eye(3)) = rzs``. The voxel sizes are
281+
the Euclidean lengths of the distance vectors. So, the voxel sizes are
282+
the Euclidean lengths of the columns of the affine (excluding the last row
283+
and column of the affine).
284+
285+
Parameters
286+
----------
287+
affine : 2D array-like
288+
Affine transformation array. Usually shape (4, 4), but can be any 2D
289+
array.
290+
291+
Returns
292+
-------
293+
vox_sizes : 1D array
294+
Voxel sizes for each input axis of affine. Usually 1D array length 3,
295+
but in general has length (N-1) where input `affine` is shape (M, N).
296+
"""
297+
top_left = affine[:-1, :-1]
298+
return np.sqrt(np.sum(top_left ** 2, axis=0))

nibabel/tests/test_affines.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
22
# vi: set ft=python sts=4 ts=4 sw=4 et:
33

4+
from itertools import product
5+
46
import numpy as np
57

8+
from ..eulerangles import euler2mat
69
from ..affines import (AffineError, apply_affine, append_diag, to_matvec,
7-
from_matvec, dot_reduce)
10+
from_matvec, dot_reduce, voxel_sizes)
811

912

1013
from nose.tools import assert_equal, assert_raises
@@ -144,3 +147,34 @@ def test_dot_reduce():
144147
np.dot(mat2, np.dot(vec, mat)))
145148
assert_array_equal(dot_reduce(mat, vec, mat2, ),
146149
np.dot(mat, np.dot(vec, mat2)))
150+
151+
152+
def test_voxel_sizes():
153+
affine = np.diag([2, 3, 4, 1])
154+
assert_almost_equal(voxel_sizes(affine), [2, 3, 4])
155+
# Some example rotations
156+
rotations = []
157+
for x_rot, y_rot, z_rot in product((0, 0.4), (0, 0.6), (0, 0.8)):
158+
rotations.append(euler2mat(z_rot, y_rot, x_rot))
159+
# Works on any size of array
160+
for n in range(2, 10):
161+
vox_sizes = np.arange(n) + 4.1
162+
aff = np.diag(list(vox_sizes) + [1])
163+
assert_almost_equal(voxel_sizes(aff), vox_sizes)
164+
# Translations make no difference
165+
aff[:-1, -1] = np.arange(n) + 10
166+
assert_almost_equal(voxel_sizes(aff), vox_sizes)
167+
# Does not have to be square
168+
new_row = np.vstack((np.zeros(n + 1), aff))
169+
assert_almost_equal(voxel_sizes(new_row), vox_sizes)
170+
new_col = np.c_[np.zeros(n + 1), aff]
171+
assert_almost_equal(voxel_sizes(new_col),
172+
[0] + list(vox_sizes))
173+
if n < 3:
174+
continue
175+
# Rotations do not change the voxel size
176+
for rotation in rotations:
177+
rot_affine = np.eye(n + 1)
178+
rot_affine[:3, :3] = rotation
179+
full_aff = rot_affine.dot(aff)
180+
assert_almost_equal(voxel_sizes(full_aff), vox_sizes)

0 commit comments

Comments
 (0)