Skip to content

Commit b8445fc

Browse files
authored
Merge pull request #1000 from MarcCote/fix_943
ENH: Speed up tractogram.apply_affine
2 parents 2937a3a + cb037d0 commit b8445fc

File tree

4 files changed

+34
-6
lines changed

4 files changed

+34
-6
lines changed

nibabel/affines.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class AffineError(ValueError):
1414
pass
1515

1616

17-
def apply_affine(aff, pts):
17+
def apply_affine(aff, pts, inplace=False):
1818
""" Apply affine matrix `aff` to points `pts`
1919
2020
Returns result of application of `aff` to the *right* of `pts`. The
@@ -39,6 +39,10 @@ def apply_affine(aff, pts):
3939
pts : (..., N-1) array-like
4040
Points, where the last dimension contains the coordinates of each
4141
point. For 3D, the last dimension will be length 3.
42+
inplace : bool, optional
43+
If True, attempt to apply the affine directly to ``pts``.
44+
If False, or in-place application fails, a freshly allocated
45+
array will be returned.
4246
4347
Returns
4448
-------
@@ -80,8 +84,18 @@ def apply_affine(aff, pts):
8084
# rzs == rotations, zooms, shears
8185
rzs = aff[:-1, :-1]
8286
trans = aff[:-1, -1]
83-
res = np.dot(pts, rzs.T) + trans[None, :]
84-
return res.reshape(shape)
87+
88+
if inplace:
89+
try:
90+
np.dot(pts, rzs.T, out=pts)
91+
except ValueError:
92+
inplace = False
93+
else:
94+
pts += trans[None, :]
95+
if not inplace:
96+
pts = pts @ rzs.T + trans[None, :]
97+
98+
return pts.reshape(shape)
8599

86100

87101
def to_matvec(transform):

nibabel/streamlines/array_sequence.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ def __init__(self, iterable=None, buffer_size=4):
146146

147147
self.extend(iterable)
148148

149+
@property
150+
def is_sliced_view(self):
151+
return self._lengths.sum() != self._data.shape[0]
152+
149153
@property
150154
def is_array_sequence(self):
151155
return True

nibabel/streamlines/tractogram.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,8 +429,12 @@ def apply_affine(self, affine, lazy=False):
429429
if np.all(affine == np.eye(4)):
430430
return self # No transformation.
431431

432-
for i in range(len(self.streamlines)):
433-
self.streamlines[i] = apply_affine(affine, self.streamlines[i])
432+
if self.streamlines.is_sliced_view:
433+
# Apply affine only on the selected streamlines.
434+
for i in range(len(self.streamlines)):
435+
self.streamlines[i] = apply_affine(affine, self.streamlines[i])
436+
else:
437+
self.streamlines._data = apply_affine(affine, self.streamlines._data, inplace=True)
434438

435439
if self.affine_to_rasmm is not None:
436440
# Update the affine that brings back the streamlines to RASmm.

nibabel/tests/test_affines.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ def test_apply_affine():
5555
pts = pts.reshape((2, 2, 3))
5656
exp_res = exp_res.reshape((2, 2, 3))
5757
assert_array_equal(apply_affine(aff, pts), exp_res)
58+
59+
# Check inplace modification.
60+
res = apply_affine(aff, pts, inplace=True)
61+
assert_array_equal(res, exp_res)
62+
assert np.shares_memory(res, pts)
63+
5864
# That ND also works
5965
for N in range(2, 6):
6066
aff = np.eye(N)
@@ -203,7 +209,7 @@ def test_rescale_affine():
203209
orig_zooms = voxel_sizes(orig_aff)
204210
orig_axcodes = aff2axcodes(orig_aff)
205211
orig_centroid = apply_affine(orig_aff, (orig_shape - 1) // 2)
206-
212+
207213
for new_shape in (None, tuple(orig_shape), (256, 256, 256), (64, 64, 40)):
208214
for new_zooms in ((1, 1, 1), (2, 2, 3), (0.5, 0.5, 0.5)):
209215
new_aff = rescale_affine(orig_aff, orig_shape, new_zooms, new_shape)

0 commit comments

Comments
 (0)