Skip to content

ENH: Load ITK's .mat files with Affine's loaders #179

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions nitransforms/io/fsl.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,7 @@ def to_filename(self, filename):
output_dir = Path(filename).parent
output_dir.mkdir(exist_ok=True, parents=True)
for i, xfm in enumerate(self.xforms):
(output_dir / ".".join((str(filename), "%03d" % i))).write_text(
xfm.to_string()
)
(output_dir / f"{filename}.{i:03d}").write_text(str(xfm))

def to_ras(self, moving=None, reference=None):
"""Return a nitransforms' internal RAS matrix."""
Expand Down
11 changes: 9 additions & 2 deletions nitransforms/io/itk.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from h5py import File as H5File
from nibabel import Nifti1Header, Nifti1Image
from nibabel.affines import from_matvec
from .base import (
from nitransforms.io.base import (
BaseLinearTransformList,
DisplacementsField,
LinearParameters,
Expand Down Expand Up @@ -204,7 +204,14 @@ def from_string(cls, string):
parameters[:3, :3] = vals[:-3].reshape((3, 3))
parameters[:3, 3] = vals[-3:]
sa["parameters"] = parameters
return tf

# Try to double-dip and see if there are more transforms
try:
cls.from_string("\n".join(lines[4:8]))
except TransformFileError:
return tf
else:
raise TransformFileError("More than one linear transform found.")


class ITKLinearTransformArray(BaseLinearTransformList):
Expand Down
39 changes: 29 additions & 10 deletions nitransforms/linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,18 +203,35 @@ def from_filename(cls, filename, fmt=None, reference=None, moving=None):
"""Create an affine from a transform file."""
fmtlist = [fmt] if fmt is not None else ("itk", "lta", "afni", "fsl")

if fmt is not None and not Path(filename).exists():
if fmt != "fsl":
raise FileNotFoundError(
f"[Errno 2] No such file or directory: '{filename}'"
)
elif not Path(f"{filename}.000").exists():
raise FileNotFoundError(
f"[Errno 2] No such file or directory: '{filename}[.000]'"
)

is_array = cls != Affine
errors = []
for potential_fmt in fmtlist:
if (potential_fmt == "itk" and Path(filename).suffix == ".mat"):
is_array = False
cls = Affine

try:
struct = get_linear_factory(potential_fmt).from_filename(filename)
matrix = struct.to_ras(reference=reference, moving=moving)
if cls == Affine:
if np.shape(matrix)[0] != 1:
raise TypeError("Cannot load transform array '%s'" % filename)
matrix = matrix[0]
return cls(matrix, reference=reference)
except (TransformFileError, FileNotFoundError):
struct = get_linear_factory(
potential_fmt,
is_array=is_array
).from_filename(filename)
except (TransformFileError, FileNotFoundError) as err:
errors.append((potential_fmt, err))
continue

matrix = struct.to_ras(reference=reference, moving=moving)
return cls(matrix, reference=reference)

raise TransformFileError(
f"Could not open <{filename}> (formats tried: {', '.join(fmtlist)})."
)
Expand Down Expand Up @@ -499,6 +516,8 @@ def load(filename, fmt=None, reference=None, moving=None):
xfm = LinearTransformsMapping.from_filename(
filename, fmt=fmt, reference=reference, moving=moving
)
if len(xfm) == 1:
return xfm[0]

if isinstance(xfm, LinearTransformsMapping) and len(xfm) == 1:
xfm = xfm[0]

return xfm
46 changes: 45 additions & 1 deletion nitransforms/tests/test_linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,22 @@ def test_linear_typeerrors1(matrix):

def test_linear_typeerrors2(data_path):
"""Exercise errors in Affine creation."""
with pytest.raises(TypeError):
with pytest.raises(io.TransformFileError):
nitl.Affine.from_filename(data_path / "itktflist.tfm", fmt="itk")


def test_linear_filenotfound(data_path):
"""Exercise errors in Affine creation."""
with pytest.raises(FileNotFoundError):
nitl.Affine.from_filename("doesnotexist.tfm", fmt="itk")

with pytest.raises(FileNotFoundError):
nitl.LinearTransformsMapping.from_filename("doesnotexist.tfm", fmt="itk")

with pytest.raises(FileNotFoundError):
nitl.LinearTransformsMapping.from_filename("doesnotexist.mat", fmt="fsl")


def test_linear_valueerror():
"""Exercise errors in Affine creation."""
with pytest.raises(ValueError):
Expand Down Expand Up @@ -85,6 +97,38 @@ def test_loadsave_itk(tmp_path, data_path, testdata_path):
)


@pytest.mark.parametrize(
"image_orientation",
[
"RAS",
"LAS",
"LPS",
"oblique",
],
)
def test_itkmat_loadsave(tmpdir, data_path, image_orientation):
tmpdir.chdir()

io.itk.ITKLinearTransform.from_filename(
data_path / f"affine-{image_orientation}.itk.tfm"
).to_filename(f"affine-{image_orientation}.itk.mat")

xfm = nitl.load(data_path / f"affine-{image_orientation}.itk.tfm", fmt="itk")
mat1 = nitl.load(f"affine-{image_orientation}.itk.mat", fmt="itk")

assert xfm == mat1

mat2 = nitl.Affine.from_filename(f"affine-{image_orientation}.itk.mat", fmt="itk")

assert xfm == mat2

mat3 = nitl.LinearTransformsMapping.from_filename(
f"affine-{image_orientation}.itk.mat", fmt="itk"
)

assert xfm == mat3


@pytest.mark.parametrize("autofmt", (False, True))
@pytest.mark.parametrize("fmt", ["itk", "fsl", "afni", "lta"])
def test_loadsave(tmp_path, data_path, testdata_path, autofmt, fmt):
Expand Down