Skip to content

Commit c34e0db

Browse files
committed
enh: manual revision
1 parent bcbadda commit c34e0db

File tree

3 files changed

+80
-100
lines changed

3 files changed

+80
-100
lines changed

nitransforms/io/x5.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,7 @@ def to_filename(fname: str, x5_list: List[X5Transform]):
6161
)
6262
if node.domain is not None:
6363
dgrp = g.create_group("Domain")
64-
dgrp.create_dataset(
65-
"Grid", data=np.uint8(1 if node.domain.grid else 0)
66-
)
64+
dgrp.create_dataset("Grid", data=np.uint8(1 if node.domain.grid else 0))
6765
dgrp.create_dataset("Size", data=np.asarray(node.domain.size))
6866
dgrp.create_dataset("Mapping", data=node.domain.mapping)
6967
if node.domain.coordinates is not None:
@@ -77,4 +75,4 @@ def to_filename(fname: str, x5_list: List[X5Transform]):
7775
g.create_dataset(
7876
"AdditionalParameters", data=node.additional_parameters
7977
)
80-
return str(fname)
78+
return fname

nitransforms/linear.py

Lines changed: 71 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
#
88
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
99
"""Linear transforms."""
10+
1011
import warnings
1112
import numpy as np
1213
from pathlib import Path
1314

1415
from nibabel.affines import from_matvec
1516

17+
from nitransforms import __version__
1618
from nitransforms.base import (
1719
ImageGrid,
1820
TransformBase,
@@ -80,6 +82,23 @@ def __init__(self, matrix=None, reference=None):
8082
self._matrix[3, :] = (0, 0, 0, 1)
8183
self._inverse = np.linalg.inv(self._matrix)
8284

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+
83102
def __eq__(self, other):
84103
"""
85104
Overload equals operator.
@@ -149,62 +168,6 @@ def ndim(self):
149168
"""Access the internal representation of this affine."""
150169
return self._matrix.ndim + 1
151170

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-
208171
@classmethod
209172
def from_filename(cls, filename, fmt=None, reference=None, moving=None):
210173
"""Create an affine from a transform file."""
@@ -260,40 +223,75 @@ def from_matvec(cls, mat=None, vec=None, reference=None):
260223
vec = vec if vec is not None else np.zeros((3,))
261224
return cls(from_matvec(mat, vector=vec), reference=reference)
262225

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)`.
266229
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
268238
-------
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.]])
276250
277251
"""
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
279272

280-
def to_x5(self):
273+
def to_x5(self, store_inverse=False, metadata=None):
281274
"""Return an :class:`~nitransforms.io.x5.X5Transform` representation."""
275+
metadata = {"WrittenBy": f"NiTransforms {__version__}"} | (metadata or {})
276+
282277
domain = None
283-
if self._reference is not None:
278+
if (reference := self.reference) is not None:
284279
domain = X5Domain(
285280
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,
288283
)
289284
kinds = tuple("space" for _ in range(self.ndim)) + ("vector",)
290285
return X5Transform(
291286
type="linear",
292287
subtype="affine",
288+
representation="matrix",
289+
metadata=metadata,
293290
transform=self.matrix,
294291
dimension_kinds=kinds,
295292
domain=domain,
296-
inverse=(~self).matrix,
293+
inverse=(~self).matrix if store_inverse else None,
294+
array_length=len(self),
297295
)
298296

299297

@@ -350,26 +348,6 @@ def __getitem__(self, i):
350348
"""Enable indexed access to the series of matrices."""
351349
return Affine(self.matrix[i, ...], reference=self._reference)
352350

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-
373351
def map(self, x, inverse=False):
374352
r"""
375353
Apply :math:`y = f(x)`.

nitransforms/tests/test_linear.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import pytest
66
import numpy as np
7-
import h5py
87

98
from pathlib import Path
109

@@ -246,15 +245,20 @@ def test_linear_save(tmpdir, data_path, get_testdata, image_orientation, sw_tool
246245
assert_affines_by_filename(xfm_fname1, xfm_fname2)
247246

248247

249-
def test_Affine_to_x5(tmpdir):
248+
@pytest.mark.parametrize("store_inverse", [True, False])
249+
def test_Affine_to_x5(tmpdir, store_inverse):
250250
"""Test affine's operations."""
251251
tmpdir.chdir()
252252
aff = nitl.Affine()
253-
node = aff.to_x5()
253+
node = aff.to_x5(
254+
metadata={"GeneratedBy": "FreeSurfer 8"}, store_inverse=store_inverse
255+
)
254256
assert node.type == "linear"
257+
assert node.subtype == "matrix"
255258
assert node.domain is None
256259
assert node.transform.shape == (4, 4)
257260
assert node.array_length == 1
261+
assert (node.metadata or {}).get("GeneratedBy") == "FreeSurfer 8"
258262

259263
img = nb.Nifti1Image(np.zeros((2, 2, 2), dtype="float32"), np.eye(4))
260264
img_path = Path(tmpdir) / "ref.nii.gz"

0 commit comments

Comments
 (0)