Skip to content

Commit 50f3f70

Browse files
committed
feat: add an .asaffine() member to TransformChain
This PR starts the implementation of "collapsing" series of transforms, for the linear case. It is tested on real data added in the context of nipy#66, nipy#74 and nipy#75. For the moment does not cover unintended use-cases such as collapsing transform sequences containing one or more nonlinear transforms. Resolves: nipy#88
1 parent 1e4aa00 commit 50f3f70

File tree

2 files changed

+31
-1
lines changed

2 files changed

+31
-1
lines changed

nitransforms/manip.py

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

1213
from .base import (
1314
TransformBase,
@@ -139,6 +140,13 @@ def map(self, x, inverse=False):
139140

140141
return x
141142

143+
def asaffine(self):
144+
"""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)
149+
142150
@classmethod
143151
def from_filename(cls, filename, fmt="X5",
144152
reference=None, moving=None):

nitransforms/tests/test_manip.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88

99
import numpy as np
1010
import nibabel as nb
11-
from ..manip import load as _load
11+
from ..manip import load as _load, TransformChain
12+
from ..linear import Affine
1213
from .test_nonlinear import (
1314
TESTS_BORDER_TOLERANCE,
1415
APPLY_NONLINEAR_CMD,
1516
)
1617

18+
FMT = {"lta": "fs", "tfm": "itk"}
19+
1720

1821
def test_itk_h5(tmp_path, testdata_path):
1922
"""Check a translation-only field on one or more axes, different image orientations."""
@@ -51,3 +54,22 @@ def test_itk_h5(tmp_path, testdata_path):
5154
diff = sw_moved.get_fdata() - nt_moved.get_fdata()
5255
# A certain tolerance is necessary because of resampling at borders
5356
assert (np.abs(diff) > 1e-3).sum() / diff.size < TESTS_BORDER_TOLERANCE
57+
58+
59+
@pytest.mark.parametrize("ext0", ["lta", "tfm"])
60+
@pytest.mark.parametrize("ext1", ["lta", "tfm"])
61+
@pytest.mark.parametrize("ext2", ["lta", "tfm"])
62+
def test_collapse_affines(tmp_path, data_path, ext0, ext1, ext2):
63+
"""Check whether affines are correctly collapsed."""
64+
chain = TransformChain([
65+
Affine.from_filename(data_path / "regressions"
66+
/ f"from-fsnative_to-scanner_mode-image.{ext0}", fmt=f"{FMT[ext0]}"),
67+
Affine.from_filename(data_path / "regressions"
68+
/ f"from-scanner_to-bold_mode-image.{ext1}", fmt=f"{FMT[ext1]}"),
69+
])
70+
assert np.allclose(
71+
chain.asaffine().matrix,
72+
Affine.from_filename(
73+
data_path / "regressions" / f"from-fsnative_to-bold_mode-image.{ext2}",
74+
fmt=f"{FMT[ext2]}").matrix,
75+
)

0 commit comments

Comments
 (0)