|
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