Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0b9371c

Browse files
authoredFeb 15, 2022
Merge pull request #142 from poldracklab/fix/fsl-load-array-xfail
FIX: Load arrays of linear transforms from FSL files
2 parents b40aff7 + 67d5662 commit 0b9371c

File tree

4 files changed

+104
-9
lines changed

4 files changed

+104
-9
lines changed
 

‎nitransforms/io/base.py

+7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Read/write linear transforms."""
2+
from pathlib import Path
23
import numpy as np
34
from nibabel import load as loadimg
45
from scipy.io.matlab.miobase import get_matfile_version
@@ -174,3 +175,9 @@ def _read_mat(byte_stream):
174175
else:
175176
raise TransformFileError("Not a Matlab file.")
176177
return reader.get_variables()
178+
179+
180+
def _ensure_image(img):
181+
if isinstance(img, (str, Path)):
182+
return loadimg(img)
183+
return img

‎nitransforms/io/fsl.py

+47-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
"""Read/write FSL's transforms."""
22
import os
3+
import warnings
34
import numpy as np
5+
from numpy.linalg import inv
46
from pathlib import Path
57
from nibabel.affines import voxel_sizes
68

7-
from .base import BaseLinearTransformList, LinearParameters, TransformFileError
9+
from .base import (
10+
BaseLinearTransformList,
11+
LinearParameters,
12+
TransformFileError,
13+
_ensure_image,
14+
)
815

916

1017
class FSLLinearTransform(LinearParameters):
@@ -24,17 +31,29 @@ def to_string(self):
2431

2532
@classmethod
2633
def from_ras(cls, ras, moving=None, reference=None):
27-
"""Create an ITK affine from a nitransform's RAS+ matrix."""
34+
"""Create an FSL affine from a nitransform's RAS+ matrix."""
35+
if moving is None:
36+
warnings.warn(
37+
"[Converting FSL to RAS] moving not provided, using reference as moving"
38+
)
39+
moving = reference
40+
41+
if reference is None:
42+
raise ValueError("Cannot build FSL linear transform without a reference")
43+
44+
reference = _ensure_image(reference)
45+
moving = _ensure_image(moving)
46+
2847
# Adjust for reference image offset and orientation
2948
refswp, refspc = _fsl_aff_adapt(reference)
30-
pre = reference.affine.dot(np.linalg.inv(refspc).dot(np.linalg.inv(refswp)))
49+
pre = reference.affine.dot(inv(refspc).dot(inv(refswp)))
3150

3251
# Adjust for moving image offset and orientation
3352
movswp, movspc = _fsl_aff_adapt(moving)
34-
post = np.linalg.inv(movswp).dot(movspc.dot(np.linalg.inv(moving.affine)))
53+
post = inv(movswp).dot(movspc.dot(inv(moving.affine)))
3554

3655
# Compose FSL transform
37-
mat = np.linalg.inv(np.swapaxes(post.dot(ras.dot(pre)), 0, 1))
56+
mat = inv(np.swapaxes(post.dot(ras.dot(pre)), 0, 1))
3857

3958
tf = cls()
4059
tf.structarr["parameters"] = mat.T
@@ -55,6 +74,29 @@ def from_string(cls, string):
5574
)
5675
return tf
5776

77+
def to_ras(self, moving=None, reference=None):
78+
"""Return a nitransforms internal RAS+ matrix."""
79+
if reference is None:
80+
raise ValueError("Cannot build FSL linear transform without a reference")
81+
82+
if moving is None:
83+
warnings.warn(
84+
"Converting FSL to RAS: moving image not provided, using reference."
85+
)
86+
moving = reference
87+
88+
reference = _ensure_image(reference)
89+
moving = _ensure_image(moving)
90+
91+
refswp, refspc = _fsl_aff_adapt(reference)
92+
93+
pre = refswp @ refspc @ inv(reference.affine)
94+
# Adjust for moving image offset and orientation
95+
movswp, movspc = _fsl_aff_adapt(moving)
96+
post = moving.affine @ inv(movspc) @ inv(movswp)
97+
mat = self.structarr["parameters"].T
98+
return post @ np.swapaxes(inv(mat), 0, 1) @ pre
99+
58100

59101
class FSLLinearTransformArray(BaseLinearTransformList):
60102
"""A string-based structure for series of FSL linear transforms."""

‎nitransforms/linear.py

+2
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ def from_filename(cls, filename, fmt="X5", reference=None, moving=None):
241241
_factory = io.itk.ITKLinearTransformArray
242242
elif fmt.lower() in ("lta", "fs"):
243243
_factory = io.LinearTransformArray
244+
elif fmt.lower() == "fsl":
245+
_factory = io.fsl.FSLLinearTransformArray
244246
else:
245247
raise NotImplementedError
246248

‎nitransforms/tests/test_linear.py

+48-4
Original file line numberDiff line numberDiff line change
@@ -82,18 +82,62 @@ def test_loadsave(tmp_path, data_path, testdata_path, fmt):
8282

8383
fname = tmp_path / ".".join(("transform-mapping", fmt))
8484
xfm.to_filename(fname, fmt=fmt)
85-
xfm == nitl.load(fname, fmt=fmt, reference=ref_file)
85+
86+
if fmt == "fsl":
87+
# FSL should not read a transform without reference
88+
with pytest.raises(ValueError):
89+
nitl.load(fname, fmt=fmt)
90+
nitl.load(fname, fmt=fmt, moving=ref_file)
91+
92+
with pytest.warns(UserWarning):
93+
assert np.allclose(
94+
xfm.matrix,
95+
nitl.load(fname, fmt=fmt, reference=ref_file).matrix,
96+
rtol=1e-2, # FSL incurs into large errors due to rounding
97+
)
98+
99+
assert np.allclose(
100+
xfm.matrix,
101+
nitl.load(fname, fmt=fmt, reference=ref_file, moving=ref_file).matrix,
102+
rtol=1e-2, # FSL incurs into large errors due to rounding
103+
)
104+
else:
105+
assert xfm == nitl.load(fname, fmt=fmt, reference=ref_file)
106+
86107
xfm.to_filename(fname, fmt=fmt, moving=ref_file)
87-
xfm == nitl.load(fname, fmt=fmt, reference=ref_file)
108+
109+
if fmt == "fsl":
110+
assert np.allclose(
111+
xfm.matrix,
112+
nitl.load(fname, fmt=fmt, reference=ref_file, moving=ref_file).matrix,
113+
rtol=1e-2, # FSL incurs into large errors due to rounding
114+
)
115+
else:
116+
assert xfm == nitl.load(fname, fmt=fmt, reference=ref_file)
88117

89118
ref_file = testdata_path / "someones_anatomy.nii.gz"
90119
xfm = nitl.load(data_path / "affine-LAS.itk.tfm", fmt="itk")
91120
xfm.reference = ref_file
92121
fname = tmp_path / ".".join(("single-transform", fmt))
93122
xfm.to_filename(fname, fmt=fmt)
94-
xfm == nitl.load(fname, fmt=fmt, reference=ref_file)
123+
if fmt == "fsl":
124+
assert np.allclose(
125+
xfm.matrix,
126+
nitl.load(fname, fmt=fmt, reference=ref_file, moving=ref_file).matrix,
127+
rtol=1e-2, # FSL incurs into large errors due to rounding
128+
)
129+
else:
130+
assert xfm == nitl.load(fname, fmt=fmt, reference=ref_file)
131+
95132
xfm.to_filename(fname, fmt=fmt, moving=ref_file)
96-
xfm == nitl.load(fname, fmt=fmt, reference=ref_file)
133+
if fmt == "fsl":
134+
assert np.allclose(
135+
xfm.matrix,
136+
nitl.load(fname, fmt=fmt, reference=ref_file, moving=ref_file).matrix,
137+
rtol=1e-2, # FSL incurs into large errors due to rounding
138+
)
139+
else:
140+
assert xfm == nitl.load(fname, fmt=fmt, reference=ref_file)
97141

98142

99143
@pytest.mark.xfail(reason="Not fully implemented")

0 commit comments

Comments
 (0)
Please sign in to comment.