Skip to content

Commit 2177a59

Browse files
authored
Merge pull request #1 from effigies/add/fs-conform
RF: Update conformation to reorient, rescale and resample
2 parents eb097f4 + f77fbb5 commit 2177a59

File tree

3 files changed

+72
-13
lines changed

3 files changed

+72
-13
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/processing.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from .optpkg import optional_package
2222
spnd, _, _ = optional_package('scipy.ndimage')
2323

24-
from .affines import AffineError, to_matvec, from_matvec, append_diag
24+
from .affines import AffineError, to_matvec, from_matvec, append_diag, rescale_affine
2525
from .spaces import vox2out_vox
2626
from .nifti1 import Nifti1Image
2727
from .orientations import axcodes2ornt, io_orientation, ornt_transform
@@ -373,19 +373,18 @@ def conform(from_img,
373373
elif len(voxel_size) != required_ndim:
374374
raise ValueError("`voxel_size` must have {} values".format(required_ndim))
375375

376-
# Create template image to which input is resampled.
377-
tmpl_hdr = from_img.header_class().from_header(from_img.header)
378-
tmpl_hdr.set_data_shape(out_shape)
379-
tmpl_hdr.set_zooms(voxel_size)
380-
tmpl = from_img.__class__(np.empty(out_shape), affine=np.eye(4), header=tmpl_hdr)
376+
start_ornt = io_orientation(from_img.affine)
377+
end_ornt = axcodes2ornt(orientation)
378+
transform = ornt_transform(start_ornt, end_ornt)
379+
380+
# Reorient first to ensure shape matches expectations
381+
reoriented = from_img.as_reoriented(transform)
382+
383+
out_aff = rescale_affine(reoriented.affine, reoriented.shape, voxel_size, out_shape)
381384

382385
# Resample input image.
383386
out_img = resample_from_to(
384-
from_img=from_img, to_vox_map=tmpl, order=order, mode="constant",
387+
from_img=from_img, to_vox_map=(out_shape, out_aff), order=order, mode="constant",
385388
cval=cval, out_class=out_class)
386389

387-
# Reorient to desired orientation.
388-
start_ornt = io_orientation(out_img.affine)
389-
end_ornt = axcodes2ornt(orientation)
390-
transform = ornt_transform(start_ornt, end_ornt)
391-
return out_img.as_reoriented(transform)
390+
return out_img

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)