diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index 936d24cced..3ae4066eae 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -1316,6 +1316,8 @@ def get_data(self, img: NrrdImage | list[NrrdImage]) -> tuple[np.ndarray, dict]: if self.affine_lps_to_ras: header = self._switch_lps_ras(header) + if header.get(MetaKeys.SPACE, "left-posterior-superior") == "left-posterior-superior": + header[MetaKeys.SPACE] = SpaceKeys.LPS # assuming LPS if not specified header[MetaKeys.AFFINE] = header[MetaKeys.ORIGINAL_AFFINE].copy() header[MetaKeys.SPATIAL_SHAPE] = header["sizes"] diff --git a/monai/data/itk_torch_bridge.py b/monai/data/itk_torch_bridge.py index 3dc25ad0bd..4671cd112b 100644 --- a/monai/data/itk_torch_bridge.py +++ b/monai/data/itk_torch_bridge.py @@ -19,8 +19,9 @@ from monai.config.type_definitions import DtypeLike from monai.data import ITKReader, ITKWriter from monai.data.meta_tensor import MetaTensor +from monai.data.utils import orientation_ras_lps from monai.transforms import EnsureChannelFirst -from monai.utils import convert_to_dst_type, optional_import +from monai.utils import MetaKeys, SpaceKeys, convert_to_dst_type, optional_import if TYPE_CHECKING: import itk @@ -83,12 +84,18 @@ def metatensor_to_itk_image( See also: :py:func:`ITKWriter.create_backend_obj` """ + if meta_tensor.meta.get(MetaKeys.SPACE, SpaceKeys.LPS) == SpaceKeys.RAS: + _meta_tensor = meta_tensor.clone() + _meta_tensor.affine = orientation_ras_lps(meta_tensor.affine) + _meta_tensor.meta[MetaKeys.SPACE] = SpaceKeys.LPS + else: + _meta_tensor = meta_tensor writer = ITKWriter(output_dtype=dtype, affine_lps_to_ras=False) writer.set_data_array(data_array=meta_tensor.data, channel_dim=channel_dim, squeeze_end_dims=True) return writer.create_backend_obj( writer.data_obj, channel_dim=writer.channel_dim, - affine=meta_tensor.affine, + affine=_meta_tensor.affine, affine_lps_to_ras=False, # False if the affine is in itk convention dtype=writer.output_dtype, kwargs=kwargs, diff --git a/tests/test_itk_torch_bridge.py b/tests/test_itk_torch_bridge.py index c08db89198..b368230c53 100644 --- a/tests/test_itk_torch_bridge.py +++ b/tests/test_itk_torch_bridge.py @@ -11,13 +11,17 @@ from __future__ import annotations +import itertools import os +import tempfile import unittest import numpy as np import torch from parameterized import parameterized +import monai +import monai.transforms as mt from monai.apps import download_url from monai.data import ITKReader from monai.data.itk_torch_bridge import ( @@ -31,14 +35,17 @@ from monai.networks.blocks import Warp from monai.transforms import Affine from monai.utils import optional_import, set_determinism -from tests.utils import skip_if_downloading_fails, skip_if_quick, test_is_quick, testing_data_config +from tests.utils import assert_allclose, skip_if_downloading_fails, skip_if_quick, test_is_quick, testing_data_config itk, has_itk = optional_import("itk") +_, has_nib = optional_import("nibabel") TESTS = ["CT_2D_head_fixed.mha", "CT_2D_head_moving.mha"] if not test_is_quick(): TESTS += ["copd1_highres_INSP_STD_COPD_img.nii.gz", "copd1_highres_EXP_STD_COPD_img.nii.gz"] +RW_TESTS = TESTS + ["nrrd_example.nrrd"] + @unittest.skipUnless(has_itk, "Requires `itk` package.") class TestITKTorchAffineMatrixBridge(unittest.TestCase): @@ -47,7 +54,7 @@ def setUp(self): self.data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "testing_data") self.reader = ITKReader(pixel_type=itk.F) - for file_name in TESTS: + for file_name in RW_TESTS: path = os.path.join(self.data_dir, file_name) if not os.path.exists(path): with skip_if_downloading_fails(): @@ -482,5 +489,34 @@ def test_use_reference_space(self, ref_filepath, filepath): np.testing.assert_allclose(output_array_monai, output_array_itk, rtol=1e-3, atol=1e-3) +@unittest.skipUnless(has_itk, "Requires `itk` package.") +@unittest.skipUnless(has_nib, "Requires `nibabel` package.") +@skip_if_quick +class TestITKTorchRW(unittest.TestCase): + def setUp(self): + TestITKTorchAffineMatrixBridge.setUp(self) + + def tearDown(self): + TestITKTorchAffineMatrixBridge.setUp(self) + + @parameterized.expand(list(itertools.product(RW_TESTS, ["ITKReader", "NrrdReader"], [True, False]))) + def test_rw_itk(self, filepath, reader, flip): + """reading and convert: filepath, reader, flip""" + print(filepath, reader, flip) + fname = os.path.join(self.data_dir, filepath) + xform = mt.LoadImageD("img", image_only=True, ensure_channel_first=True, affine_lps_to_ras=flip, reader=reader) + out = xform({"img": fname})["img"] + itk_image = metatensor_to_itk_image(out, channel_dim=0, dtype=float) + with tempfile.TemporaryDirectory() as tempdir: + tname = os.path.join(tempdir, filepath) + (".nii.gz" if not filepath.endswith(".nii.gz") else "") + itk.imwrite(itk_image, tname, True) + ref = mt.LoadImage(image_only=True, ensure_channel_first=True, reader="NibabelReader")(tname) + if out.meta["space"] != ref.meta["space"]: + ref.affine = monai.data.utils.orientation_ras_lps(ref.affine) + assert_allclose( + out.affine, monai.data.utils.to_affine_nd(len(out.affine) - 1, ref.affine), rtol=1e-3, atol=1e-3 + ) + + if __name__ == "__main__": unittest.main() diff --git a/tests/testing_data/data_config.json b/tests/testing_data/data_config.json index c0666119d9..a570c787ba 100644 --- a/tests/testing_data/data_config.json +++ b/tests/testing_data/data_config.json @@ -84,6 +84,11 @@ "url": "https://github.com/Project-MONAI/MONAI-extra-test-data/releases/download/0.8.1/CT_DICOM_SINGLE.zip", "hash_type": "sha256", "hash_val": "a41f6e93d2e3d68956144f9a847273041d36441da12377d6a1d5ae610e0a7023" + }, + "nrrd_example": { + "url": "https://github.com/Project-MONAI/MONAI-extra-test-data/releases/download/0.8.1/CT_IMAGE_cropped.nrrd", + "hash_type": "sha256", + "hash_val": "66971ad17f0bac50e6082ed6a4dc1ae7093c30517137e53327b15a752327a1c0" } }, "videos": {