|
7 | 7 | #
|
8 | 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
|
9 | 9 | """Linear transforms."""
|
| 10 | + |
10 | 11 | import warnings
|
11 | 12 | import numpy as np
|
12 | 13 | from pathlib import Path
|
13 | 14 |
|
14 | 15 | from nibabel.affines import from_matvec
|
15 | 16 |
|
| 17 | +from nitransforms import __version__ |
16 | 18 | from nitransforms.base import (
|
17 | 19 | ImageGrid,
|
18 | 20 | TransformBase,
|
@@ -80,6 +82,23 @@ def __init__(self, matrix=None, reference=None):
|
80 | 82 | self._matrix[3, :] = (0, 0, 0, 1)
|
81 | 83 | self._inverse = np.linalg.inv(self._matrix)
|
82 | 84 |
|
| 85 | + def __repr__(self): |
| 86 | + """ |
| 87 | + Change representation to the internal matrix. |
| 88 | +
|
| 89 | + Example |
| 90 | + ------- |
| 91 | + >>> Affine([ |
| 92 | + ... [1, 0, 0, 4], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] |
| 93 | + ... ]) # doctest: +NORMALIZE_WHITESPACE |
| 94 | + array([[1, 0, 0, 4], |
| 95 | + [0, 1, 0, 0], |
| 96 | + [0, 0, 1, 0], |
| 97 | + [0, 0, 0, 1]]) |
| 98 | +
|
| 99 | + """ |
| 100 | + return repr(self.matrix) |
| 101 | + |
83 | 102 | def __eq__(self, other):
|
84 | 103 | """
|
85 | 104 | Overload equals operator.
|
@@ -149,62 +168,6 @@ def ndim(self):
|
149 | 168 | """Access the internal representation of this affine."""
|
150 | 169 | return self._matrix.ndim + 1
|
151 | 170 |
|
152 |
| - def map(self, x, inverse=False): |
153 |
| - r""" |
154 |
| - Apply :math:`y = f(x)`. |
155 |
| -
|
156 |
| - Parameters |
157 |
| - ---------- |
158 |
| - x : N x D numpy.ndarray |
159 |
| - Input RAS+ coordinates (i.e., physical coordinates). |
160 |
| - inverse : bool |
161 |
| - If ``True``, apply the inverse transform :math:`x = f^{-1}(y)`. |
162 |
| -
|
163 |
| - Returns |
164 |
| - ------- |
165 |
| - y : N x D numpy.ndarray |
166 |
| - Transformed (mapped) RAS+ coordinates (i.e., physical coordinates). |
167 |
| -
|
168 |
| - Examples |
169 |
| - -------- |
170 |
| - >>> xfm = Affine([[1, 0, 0, 1], [0, 1, 0, 2], [0, 0, 1, 3], [0, 0, 0, 1]]) |
171 |
| - >>> xfm.map((0,0,0)) |
172 |
| - array([[1., 2., 3.]]) |
173 |
| -
|
174 |
| - >>> xfm.map((0,0,0), inverse=True) |
175 |
| - array([[-1., -2., -3.]]) |
176 |
| -
|
177 |
| - """ |
178 |
| - affine = self._matrix |
179 |
| - coords = _as_homogeneous(x, dim=affine.shape[0] - 1).T |
180 |
| - if inverse is True: |
181 |
| - affine = self._inverse |
182 |
| - return affine.dot(coords).T[..., :-1] |
183 |
| - |
184 |
| - def _to_hdf5(self, x5_root): |
185 |
| - """Serialize this object into the x5 file format.""" |
186 |
| - xform = x5_root.create_dataset("Transform", data=[self._matrix]) |
187 |
| - xform.attrs["Type"] = "affine" |
188 |
| - x5_root.create_dataset("Inverse", data=[(~self).matrix]) |
189 |
| - |
190 |
| - if self._reference: |
191 |
| - self.reference._to_hdf5(x5_root.create_group("Reference")) |
192 |
| - |
193 |
| - def to_filename(self, filename, fmt="X5", moving=None): |
194 |
| - """Store the transform in the requested output format.""" |
195 |
| - writer = get_linear_factory(fmt, is_array=False) |
196 |
| - |
197 |
| - if fmt.lower() in ("itk", "ants", "elastix"): |
198 |
| - writer.from_ras(self.matrix).to_filename(filename) |
199 |
| - else: |
200 |
| - # Rest of the formats peek into moving and reference image grids |
201 |
| - writer.from_ras( |
202 |
| - self.matrix, |
203 |
| - reference=self.reference, |
204 |
| - moving=ImageGrid(moving) if moving is not None else self.reference, |
205 |
| - ).to_filename(filename) |
206 |
| - return filename |
207 |
| - |
208 | 171 | @classmethod
|
209 | 172 | def from_filename(cls, filename, fmt=None, reference=None, moving=None):
|
210 | 173 | """Create an affine from a transform file."""
|
@@ -260,40 +223,75 @@ def from_matvec(cls, mat=None, vec=None, reference=None):
|
260 | 223 | vec = vec if vec is not None else np.zeros((3,))
|
261 | 224 | return cls(from_matvec(mat, vector=vec), reference=reference)
|
262 | 225 |
|
263 |
| - def __repr__(self): |
264 |
| - """ |
265 |
| - Change representation to the internal matrix. |
| 226 | + def map(self, x, inverse=False): |
| 227 | + r""" |
| 228 | + Apply :math:`y = f(x)`. |
266 | 229 |
|
267 |
| - Example |
| 230 | + Parameters |
| 231 | + ---------- |
| 232 | + x : N x D numpy.ndarray |
| 233 | + Input RAS+ coordinates (i.e., physical coordinates). |
| 234 | + inverse : bool |
| 235 | + If ``True``, apply the inverse transform :math:`x = f^{-1}(y)`. |
| 236 | +
|
| 237 | + Returns |
268 | 238 | -------
|
269 |
| - >>> Affine([ |
270 |
| - ... [1, 0, 0, 4], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] |
271 |
| - ... ]) # doctest: +NORMALIZE_WHITESPACE |
272 |
| - array([[1, 0, 0, 4], |
273 |
| - [0, 1, 0, 0], |
274 |
| - [0, 0, 1, 0], |
275 |
| - [0, 0, 0, 1]]) |
| 239 | + y : N x D numpy.ndarray |
| 240 | + Transformed (mapped) RAS+ coordinates (i.e., physical coordinates). |
| 241 | +
|
| 242 | + Examples |
| 243 | + -------- |
| 244 | + >>> xfm = Affine([[1, 0, 0, 1], [0, 1, 0, 2], [0, 0, 1, 3], [0, 0, 0, 1]]) |
| 245 | + >>> xfm.map((0,0,0)) |
| 246 | + array([[1., 2., 3.]]) |
| 247 | +
|
| 248 | + >>> xfm.map((0,0,0), inverse=True) |
| 249 | + array([[-1., -2., -3.]]) |
276 | 250 |
|
277 | 251 | """
|
278 |
| - return repr(self.matrix) |
| 252 | + affine = self._matrix |
| 253 | + coords = _as_homogeneous(x, dim=affine.shape[0] - 1).T |
| 254 | + if inverse is True: |
| 255 | + affine = self._inverse |
| 256 | + return affine.dot(coords).T[..., :-1] |
| 257 | + |
| 258 | + def to_filename(self, filename, fmt="X5", moving=None): |
| 259 | + """Store the transform in the requested output format.""" |
| 260 | + writer = get_linear_factory(fmt, is_array=False) |
| 261 | + |
| 262 | + if fmt.lower() in ("itk", "ants", "elastix"): |
| 263 | + writer.from_ras(self.matrix).to_filename(filename) |
| 264 | + else: |
| 265 | + # Rest of the formats peek into moving and reference image grids |
| 266 | + writer.from_ras( |
| 267 | + self.matrix, |
| 268 | + reference=self.reference, |
| 269 | + moving=ImageGrid(moving) if moving is not None else self.reference, |
| 270 | + ).to_filename(filename) |
| 271 | + return filename |
279 | 272 |
|
280 |
| - def to_x5(self): |
| 273 | + def to_x5(self, store_inverse=False, metadata=None): |
281 | 274 | """Return an :class:`~nitransforms.io.x5.X5Transform` representation."""
|
| 275 | + metadata = {"WrittenBy": f"NiTransforms {__version__}"} | (metadata or {}) |
| 276 | + |
282 | 277 | domain = None
|
283 |
| - if self._reference is not None: |
| 278 | + if (reference := self.reference) is not None: |
284 | 279 | domain = X5Domain(
|
285 | 280 | grid=True,
|
286 |
| - size=self.reference.shape, |
287 |
| - mapping=self.reference.affine, |
| 281 | + size=getattr(reference or {}, "shape", (0, 0, 0)), |
| 282 | + mapping=reference.affine, |
288 | 283 | )
|
289 | 284 | kinds = tuple("space" for _ in range(self.ndim)) + ("vector",)
|
290 | 285 | return X5Transform(
|
291 | 286 | type="linear",
|
292 | 287 | subtype="affine",
|
| 288 | + representation="matrix", |
| 289 | + metadata=metadata, |
293 | 290 | transform=self.matrix,
|
294 | 291 | dimension_kinds=kinds,
|
295 | 292 | domain=domain,
|
296 |
| - inverse=(~self).matrix, |
| 293 | + inverse=(~self).matrix if store_inverse else None, |
| 294 | + array_length=len(self), |
297 | 295 | )
|
298 | 296 |
|
299 | 297 |
|
@@ -350,26 +348,6 @@ def __getitem__(self, i):
|
350 | 348 | """Enable indexed access to the series of matrices."""
|
351 | 349 | return Affine(self.matrix[i, ...], reference=self._reference)
|
352 | 350 |
|
353 |
| - def to_x5(self): |
354 |
| - """Return an :class:`~nitransforms.io.x5.X5Transform` object.""" |
355 |
| - domain = None |
356 |
| - if self._reference is not None: |
357 |
| - domain = X5Domain( |
358 |
| - grid=True, |
359 |
| - size=self.reference.shape, |
360 |
| - mapping=self.reference.affine, |
361 |
| - ) |
362 |
| - kinds = tuple("space" for _ in range(self.ndim - 1)) + ("vector",) |
363 |
| - return X5Transform( |
364 |
| - type="linear", |
365 |
| - subtype="affine", |
366 |
| - transform=self.matrix, |
367 |
| - dimension_kinds=kinds, |
368 |
| - domain=domain, |
369 |
| - inverse=self._inverse, |
370 |
| - array_length=len(self), |
371 |
| - ) |
372 |
| - |
373 | 351 | def map(self, x, inverse=False):
|
374 | 352 | r"""
|
375 | 353 | Apply :math:`y = f(x)`.
|
|
0 commit comments