Skip to content

Commit 241f58f

Browse files
committed
NF: Add nibabel.affines.rescale_affine function
1 parent eb097f4 commit 241f58f

File tree

2 files changed

+61
-1
lines changed

2 files changed

+61
-1
lines changed

nibabel/affines.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,3 +323,43 @@ def obliquity(affine):
323323
vs = voxel_sizes(affine)
324324
best_cosines = np.abs(affine[:-1, :-1] / vs).max(axis=1)
325325
return np.arccos(best_cosines)
326+
327+
328+
def rescale_affine(affine, shape, zooms, new_shape=None):
329+
""" Return a new affine matrix with updated voxel sizes (zooms)
330+
331+
This function preserves the rotations and shears of the original
332+
affine, as well as the RAS location of the central voxel of the
333+
image.
334+
335+
Parameters
336+
----------
337+
affine : (N, N) array-like
338+
NxN transform matrix in homogeneous coordinates representing an affine
339+
transformation from an (N-1)-dimensional space to an (N-1)-dimensional
340+
space. An example is a 4x4 transform representing rotations and
341+
translations in 3 dimensions.
342+
shape : (N-1,) array-like
343+
The extent of the (N-1) dimensions of the original space
344+
zooms : (N-1,) array-like
345+
The size of voxels of the output affine
346+
new_shape : (N-1,) array-like, optional
347+
The extent of the (N-1) dimensions of the space described by the
348+
new affine. If ``None``, use ``shape``.
349+
350+
Returns
351+
-------
352+
affine : (N, N) array
353+
A new affine transform with the specified voxel sizes
354+
355+
"""
356+
shape = np.array(shape, copy=False)
357+
new_shape = np.array(new_shape if new_shape is not None else shape)
358+
359+
s = voxel_sizes(affine)
360+
rzs_out = affine[:3, :3] * zooms / s
361+
362+
# Using xyz = A @ ijk, determine translation
363+
centroid = apply_affine(affine, (shape - 1) // 2)
364+
t_out = centroid - rzs_out @ ((new_shape - 1) // 2)
365+
return from_matvec(rzs_out, t_out)

nibabel/tests/test_affines.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
from ..eulerangles import euler2mat
99
from ..affines import (AffineError, apply_affine, append_diag, to_matvec,
10-
from_matvec, dot_reduce, voxel_sizes, obliquity)
10+
from_matvec, dot_reduce, voxel_sizes, obliquity, rescale_affine)
11+
from ..orientations import aff2axcodes
1112

1213

1314
import pytest
@@ -192,3 +193,22 @@ def test_obliquity():
192193
assert_almost_equal(obliquity(aligned), [0.0, 0.0, 0.0])
193194
assert_almost_equal(obliquity(oblique) * 180 / pi,
194195
[0.0810285, 5.1569949, 5.1569376])
196+
197+
198+
def test_rescale_affine():
199+
rng = np.random.RandomState(20200415)
200+
orig_shape = rng.randint(low=20, high=512, size=(3,))
201+
orig_aff = np.eye(4)
202+
orig_aff[:3, :] = rng.normal(size=(3, 4))
203+
orig_zooms = voxel_sizes(orig_aff)
204+
orig_axcodes = aff2axcodes(orig_aff)
205+
orig_centroid = apply_affine(orig_aff, (orig_shape - 1) // 2)
206+
207+
for new_shape in (None, tuple(orig_shape), (256, 256, 256), (64, 64, 40)):
208+
for new_zooms in ((1, 1, 1), (2, 2, 3), (0.5, 0.5, 0.5)):
209+
new_aff = rescale_affine(orig_aff, orig_shape, new_zooms, new_shape)
210+
assert aff2axcodes(new_aff) == orig_axcodes
211+
if new_shape is None:
212+
new_shape = tuple(orig_shape)
213+
new_centroid = apply_affine(new_aff, (np.array(new_shape) - 1) // 2)
214+
assert_almost_equal(new_centroid, orig_centroid)

0 commit comments

Comments
 (0)