Skip to content

Commit fbc9228

Browse files
committed
ENH: Collapse linear and nonlinear transforms chains
Very undertested, but currently there is a test that uses a "collapsed" transform on an ITK's .h5 file with one affine and one nonlinear. BSpline transforms not currently supported. Resolves #89.
1 parent 54ad1ea commit fbc9228

File tree

4 files changed

+24
-22
lines changed

4 files changed

+24
-22
lines changed

nitransforms/linear.py

+7-9
Original file line numberDiff line numberDiff line change
@@ -123,19 +123,17 @@ def __matmul__(self, b):
123123
True
124124
125125
>>> xfm1 = Affine([[1, 0, 0, 4], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
126-
>>> xfm1 @ np.eye(4) == xfm1
126+
>>> xfm1 @ Affine() == xfm1
127127
True
128128
129129
"""
130-
if not isinstance(b, self.__class__):
131-
_b = self.__class__(b)
132-
else:
133-
_b = b
130+
if isinstance(b, self.__class__):
131+
return self.__class__(
132+
b.matrix @ self.matrix,
133+
reference=b.reference,
134+
)
134135

135-
retval = self.__class__(self.matrix.dot(_b.matrix))
136-
if _b.reference:
137-
retval.reference = _b.reference
138-
return retval
136+
return b @ self
139137

140138
@property
141139
def matrix(self):

nitransforms/manip.py

+7-9
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
99
"""Common interface for transforms."""
1010
from collections.abc import Iterable
11-
import numpy as np
1211

1312
from .base import (
1413
TransformBase,
@@ -140,17 +139,17 @@ def map(self, x, inverse=False):
140139

141140
return x
142141

143-
def asaffine(self, indices=None):
142+
def collapse(self):
144143
"""
145-
Combine a succession of linear transforms into one.
144+
Combine a succession of transforms into one.
146145
147146
Example
148147
------
149148
>>> chain = TransformChain(transforms=[
150149
... Affine.from_matvec(vec=(2, -10, 3)),
151150
... Affine.from_matvec(vec=(-2, 10, -3)),
152151
... ])
153-
>>> chain.asaffine()
152+
>>> chain.collapse()
154153
array([[1., 0., 0., 0.],
155154
[0., 1., 0., 0.],
156155
[0., 0., 1., 0.],
@@ -160,15 +159,15 @@ def asaffine(self, indices=None):
160159
... Affine.from_matvec(vec=(1, 2, 3)),
161160
... Affine.from_matvec(mat=[[0, 1, 0], [0, 0, 1], [1, 0, 0]]),
162161
... ])
163-
>>> chain.asaffine()
162+
>>> chain.collapse()
164163
array([[0., 1., 0., 2.],
165164
[0., 0., 1., 3.],
166165
[1., 0., 0., 1.],
167166
[0., 0., 0., 1.]])
168167
169168
>>> np.allclose(
170169
... chain.map((4, -2, 1)),
171-
... chain.asaffine().map((4, -2, 1)),
170+
... chain.collapse().map((4, -2, 1)),
172171
... )
173172
True
174173
@@ -178,9 +177,8 @@ def asaffine(self, indices=None):
178177
The indices of the values to extract.
179178
180179
"""
181-
affines = self.transforms if indices is None else np.take(self.transforms, indices)
182-
retval = affines[0]
183-
for xfm in affines[1:]:
180+
retval = self.transforms[-1]
181+
for xfm in reversed(self.transforms[:-1]):
184182
retval = xfm @ retval
185183
return retval
186184

nitransforms/tests/test_linear.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -372,10 +372,10 @@ def test_mulmat_operator(testdata_path):
372372
mat2 = from_matvec(np.eye(3), (4, 2, -1))
373373
aff = nitl.Affine(mat1, reference=ref)
374374

375-
composed = aff @ mat2
375+
composed = aff @ nitl.Affine(mat2)
376376
assert composed.reference is None
377-
assert composed == nitl.Affine(mat1.dot(mat2))
377+
assert composed == nitl.Affine(mat2 @ mat1)
378378

379379
composed = nitl.Affine(mat2) @ aff
380380
assert composed.reference == aff.reference
381-
assert composed == nitl.Affine(mat2.dot(mat1), reference=ref)
381+
assert composed == nitl.Affine(mat1 @ mat2, reference=ref)

nitransforms/tests/test_manip.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ def test_itk_h5(tmp_path, testdata_path):
6060
# A certain tolerance is necessary because of resampling at borders
6161
assert (np.abs(diff) > 1e-3).sum() / diff.size < RMSE_TOL
6262

63+
col_moved = xfm.collapse().apply(img_fname, order=0)
64+
col_moved.to_filename("nt_collapse_resampled.nii.gz")
65+
diff = sw_moved.get_fdata() - col_moved.get_fdata()
66+
# A certain tolerance is necessary because of resampling at borders
67+
assert (np.abs(diff) > 1e-3).sum() / diff.size < RMSE_TOL
68+
6369

6470
@pytest.mark.parametrize("ext0", ["lta", "tfm"])
6571
@pytest.mark.parametrize("ext1", ["lta", "tfm"])
@@ -81,7 +87,7 @@ def test_collapse_affines(tmp_path, data_path, ext0, ext1, ext2):
8187
]
8288
)
8389
assert np.allclose(
84-
chain.asaffine().matrix,
90+
chain.collapse().matrix,
8591
Affine.from_filename(
8692
data_path / "regressions" / f"from-fsnative_to-bold_mode-image.{ext2}",
8793
fmt=f"{FMT[ext2]}",

0 commit comments

Comments
 (0)