diff --git a/CHANGES.rst b/CHANGES.rst index d3cb594b..f118e261 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,14 @@ +23.0.1 (July 10, 2023) +====================== +Hotfix release addressing two issues. + +CHANGES +------- + +* FIX: Load ITK's ``.mat`` files with ``Affine``'s loaders (#179) +* FIX: numpy deprecation errors after 1.22 (#180) + + 23.0.0 (June 13, 2023) ====================== A new major release preparing for the finalization of the package and migration into diff --git a/nitransforms/io/fsl.py b/nitransforms/io/fsl.py index 3bd4deb1..8e4c8264 100644 --- a/nitransforms/io/fsl.py +++ b/nitransforms/io/fsl.py @@ -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.""" diff --git a/nitransforms/io/itk.py b/nitransforms/io/itk.py index 62cc2ee8..d7a093eb 100644 --- a/nitransforms/io/itk.py +++ b/nitransforms/io/itk.py @@ -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, @@ -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): diff --git a/nitransforms/linear.py b/nitransforms/linear.py index 84c9126b..9c430d3b 100644 --- a/nitransforms/linear.py +++ b/nitransforms/linear.py @@ -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)})." ) @@ -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 diff --git a/nitransforms/nonlinear.py b/nitransforms/nonlinear.py index ef92eb6c..c0cdc92e 100644 --- a/nitransforms/nonlinear.py +++ b/nitransforms/nonlinear.py @@ -165,7 +165,7 @@ def map(self, x, inverse=False): indexes = tuple(tuple(i) for i in indexes.T) return self._field[indexes] - return np.vstack(( + return np.vstack(tuple( map_coordinates( self._field[..., i], ijk.T, diff --git a/nitransforms/tests/test_linear.py b/nitransforms/tests/test_linear.py index aed4a148..eea77b7f 100644 --- a/nitransforms/tests/test_linear.py +++ b/nitransforms/tests/test_linear.py @@ -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): @@ -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):