Skip to content

Commit dec5b4f

Browse files
committed
feat: add the matmul operator on Affines and leverage it collapsing
1 parent 50f3f70 commit dec5b4f

File tree

2 files changed

+55
-9
lines changed

2 files changed

+55
-9
lines changed

nitransforms/linear.py

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
class Affine(TransformBase):
2525
"""Represents linear transforms on image data."""
2626

27-
__slots__ = ("_matrix", )
27+
__slots__ = ("_matrix", "_inverse")
2828

2929
def __init__(self, matrix=None, reference=None):
3030
"""
@@ -57,6 +57,7 @@ def __init__(self, matrix=None, reference=None):
5757
"""
5858
super().__init__(reference=reference)
5959
self._matrix = np.eye(4)
60+
self._inverse = np.eye(4)
6061

6162
if matrix is not None:
6263
matrix = np.array(matrix)
@@ -72,6 +73,7 @@ def __init__(self, matrix=None, reference=None):
7273

7374
# Normalize last row
7475
self._matrix[3, :] = (0, 0, 0, 1)
76+
self._inverse = np.linalg.inv(self._matrix)
7577

7678
def __eq__(self, other):
7779
"""
@@ -90,6 +92,44 @@ def __eq__(self, other):
9092
warnings.warn("Affines are equal, but references do not match.")
9193
return _eq
9294

95+
def __invert__(self):
96+
"""
97+
Get the inverse of this transform.
98+
99+
Example
100+
-------
101+
>>> matrix = [[1, 0, 0, 4], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
102+
>>> Affine(np.linalg.inv(matrix)) == ~Affine(matrix)
103+
True
104+
105+
"""
106+
return self.__class__(self._inverse)
107+
108+
def __matmul__(self, b):
109+
"""
110+
Compose two Affines.
111+
112+
Example
113+
-------
114+
>>> xfm1 = Affine([[1, 0, 0, 4], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
115+
>>> xfm1 @ ~xfm1 == Affine()
116+
True
117+
118+
>>> xfm1 = Affine([[1, 0, 0, 4], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
119+
>>> xfm1 @ np.eye(4) == xfm1
120+
True
121+
122+
"""
123+
if not isinstance(b, self.__class__):
124+
_b = self.__class__(b)
125+
else:
126+
_b = b
127+
128+
retval = self.__class__(self.matrix.dot(_b.matrix))
129+
if _b.reference:
130+
retval.reference = _b.reference
131+
return retval
132+
93133
@property
94134
def matrix(self):
95135
"""Access the internal representation of this affine."""
@@ -124,14 +164,14 @@ def map(self, x, inverse=False):
124164
affine = self._matrix
125165
coords = _as_homogeneous(x, dim=affine.shape[0] - 1).T
126166
if inverse is True:
127-
affine = np.linalg.inv(self._matrix)
167+
affine = self._inverse
128168
return affine.dot(coords).T[..., :-1]
129169

130170
def _to_hdf5(self, x5_root):
131171
"""Serialize this object into the x5 file format."""
132172
xform = x5_root.create_dataset("Transform", data=[self._matrix])
133173
xform.attrs["Type"] = "affine"
134-
x5_root.create_dataset("Inverse", data=[np.linalg.inv(self._matrix)])
174+
x5_root.create_dataset("Inverse", data=[(~self).matrix])
135175

136176
if self._reference:
137177
self.reference._to_hdf5(x5_root.create_group("Reference"))
@@ -175,7 +215,7 @@ def to_filename(self, filename, fmt="X5", moving=None):
175215
lt["dst"] = io.VolumeGeometry.from_image(moving)
176216
# However, the affine needs to be inverted
177217
# (i.e., it is not a pure "points" convention).
178-
lt["m_L"] = np.linalg.inv(self.matrix)
218+
lt["m_L"] = (~self).matrix
179219
# to make LTA file format
180220
lta = io.LinearTransformArray()
181221
lta["type"] = 1 # RAS2RAS
@@ -234,6 +274,11 @@ def __init__(self, transforms, reference=None):
234274
[0., 1., 0., 2.],
235275
[0., 0., 1., 3.],
236276
[0., 0., 0., 1.]])
277+
>>> (~xfm)[0].matrix # doctest: +NORMALIZE_WHITESPACE
278+
array([[ 1., 0., 0., -1.],
279+
[ 0., 1., 0., -2.],
280+
[ 0., 0., 1., -3.],
281+
[ 0., 0., 0., 1.]])
237282
238283
"""
239284
super().__init__(reference=reference)
@@ -245,6 +290,7 @@ def __init__(self, transforms, reference=None):
245290
).matrix
246291
for xfm in transforms
247292
], axis=0)
293+
self._inverse = np.linalg.inv(self._matrix)
248294

249295
def __getitem__(self, i):
250296
"""Enable indexed access to the series of matrices."""
@@ -304,7 +350,7 @@ def map(self, x, inverse=False):
304350
affine = self.matrix
305351
coords = _as_homogeneous(x, dim=affine.shape[-1] - 1).T
306352
if inverse is True:
307-
affine = np.linalg.inv(affine)
353+
affine = self._inverse
308354
return np.swapaxes(affine.dot(coords), 1, 2)
309355

310356
def to_filename(self, filename, fmt="X5", moving=None):

nitransforms/manip.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,10 @@ def map(self, x, inverse=False):
142142

143143
def asaffine(self):
144144
"""Combine a succession of linear transforms into one."""
145-
matrix = np.eye(4)
146-
for xfm in self.transforms:
147-
matrix = xfm.matrix.dot(matrix)
148-
return Affine(matrix, reference=self.reference)
145+
retval = self.transforms[-1]
146+
for xfm in self.transforms[:-1][::-1]:
147+
retval @= xfm
148+
return retval
149149

150150
@classmethod
151151
def from_filename(cls, filename, fmt="X5",

0 commit comments

Comments
 (0)