From d9e549412202a98c70c0ed4e5122837dbbf20ca2 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Mon, 10 Jul 2023 13:52:28 +0200 Subject: [PATCH 1/6] fix: pacify numpy deprecation that errors with numpy 1.22 --- nitransforms/nonlinear.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, From f31e223d80b644a0e176eacf9f05f00fb03492ea Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Mon, 10 Jul 2023 10:01:25 +0200 Subject: [PATCH 2/6] enh: load ITK's .mat files with Affine's loaders --- nitransforms/io/itk.py | 9 +++++++- nitransforms/linear.py | 37 ++++++++++++++++++++++--------- nitransforms/tests/test_linear.py | 2 +- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/nitransforms/io/itk.py b/nitransforms/io/itk.py index 62cc2ee8..ecdffe61 100644 --- a/nitransforms/io/itk.py +++ b/nitransforms/io/itk.py @@ -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..166c2664 100644 --- a/nitransforms/linear.py +++ b/nitransforms/linear.py @@ -203,18 +203,33 @@ 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") + 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) + + # Process matrix + if not is_array and np.ndim(matrix) == 3: + if np.shape(matrix)[0] != 1: + raise TypeError("Cannot load transform array '%s'" % filename) + matrix = matrix[0] + + return cls(matrix, reference=reference) + raise TransformFileError( f"Could not open <{filename}> (formats tried: {', '.join(fmtlist)})." ) @@ -499,6 +514,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/tests/test_linear.py b/nitransforms/tests/test_linear.py index aed4a148..1f6d87fe 100644 --- a/nitransforms/tests/test_linear.py +++ b/nitransforms/tests/test_linear.py @@ -44,7 +44,7 @@ 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") From 852b02d419d8caced6ace81d467113fafe7aabfb Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Mon, 10 Jul 2023 16:40:47 +0200 Subject: [PATCH 3/6] fix: when adding tests to better cover the new code, a bug with FSL was discovered --- nitransforms/io/fsl.py | 4 +--- nitransforms/io/itk.py | 2 +- nitransforms/linear.py | 11 ++++++++- nitransforms/tests/test_linear.py | 38 +++++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) 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 ecdffe61..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, diff --git a/nitransforms/linear.py b/nitransforms/linear.py index 166c2664..fee9b6d2 100644 --- a/nitransforms/linear.py +++ b/nitransforms/linear.py @@ -203,8 +203,17 @@ 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") - is_array = cls != Affine + 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"): diff --git a/nitransforms/tests/test_linear.py b/nitransforms/tests/test_linear.py index 1f6d87fe..3180b55d 100644 --- a/nitransforms/tests/test_linear.py +++ b/nitransforms/tests/test_linear.py @@ -48,6 +48,12 @@ def test_linear_typeerrors2(data_path): 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") + + def test_linear_valueerror(): """Exercise errors in Affine creation.""" with pytest.raises(ValueError): @@ -85,6 +91,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): From b0599a6fff19cf4d12bd0aa27dcd7a09717bb171 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Mon, 10 Jul 2023 18:13:56 +0200 Subject: [PATCH 4/6] fix: remove unexercised code path with duplicate responsibility After all, it is ``linear.load()`` where the downcast to ``Affine`` happens if only one transform is in the array. --- nitransforms/linear.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/nitransforms/linear.py b/nitransforms/linear.py index fee9b6d2..9c430d3b 100644 --- a/nitransforms/linear.py +++ b/nitransforms/linear.py @@ -230,13 +230,6 @@ def from_filename(cls, filename, fmt=None, reference=None, moving=None): continue matrix = struct.to_ras(reference=reference, moving=moving) - - # Process matrix - if not is_array and np.ndim(matrix) == 3: - if np.shape(matrix)[0] != 1: - raise TypeError("Cannot load transform array '%s'" % filename) - matrix = matrix[0] - return cls(matrix, reference=reference) raise TransformFileError( From 9b8c3631b6ecf712a028b97dc29eed663748f442 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Mon, 10 Jul 2023 20:36:39 +0200 Subject: [PATCH 5/6] enh: add coverage --- nitransforms/tests/test_linear.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nitransforms/tests/test_linear.py b/nitransforms/tests/test_linear.py index 3180b55d..eea77b7f 100644 --- a/nitransforms/tests/test_linear.py +++ b/nitransforms/tests/test_linear.py @@ -53,6 +53,12 @@ def test_linear_filenotfound(data_path): 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.""" From 951119e009a3617d48bb48d8f18649e90175ca17 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Mon, 10 Jul 2023 21:04:11 +0200 Subject: [PATCH 6/6] rel(23.0.1): update CHANGES --- CHANGES.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) 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