Skip to content

Commit b31367f

Browse files
authored
6985 metatensor_to_itk_image compatible space (#7000)
Fixes #6985 ### Description - update `metatensor_to_itk_image` to accept RAS metatensor - nrrdreader default 'space' from empty/"left-posterior-superior" to `SpaceKeys.LPS` - more tests ### Types of changes <!--- Put an `x` in all the boxes that apply, and remove the not applicable items --> - [x] Non-breaking change (fix or new feature that would not break existing functionality). - [ ] Breaking change (fix or new feature that would cause existing functionality to change). - [x] New tests added to cover the changes. - [ ] Integration tests passed locally by running `./runtests.sh -f -u --net --coverage`. - [x] Quick tests passed locally by running `./runtests.sh --quick --unittests --disttests`. - [x] In-line docstrings updated. - [ ] Documentation updated, tested `make html` command in the `docs/` folder. --------- Signed-off-by: Wenqi Li <wenqil@nvidia.com>
1 parent 5a644e4 commit b31367f

File tree

4 files changed

+54
-4
lines changed

4 files changed

+54
-4
lines changed

monai/data/image_reader.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1316,6 +1316,8 @@ def get_data(self, img: NrrdImage | list[NrrdImage]) -> tuple[np.ndarray, dict]:
13161316

13171317
if self.affine_lps_to_ras:
13181318
header = self._switch_lps_ras(header)
1319+
if header.get(MetaKeys.SPACE, "left-posterior-superior") == "left-posterior-superior":
1320+
header[MetaKeys.SPACE] = SpaceKeys.LPS # assuming LPS if not specified
13191321

13201322
header[MetaKeys.AFFINE] = header[MetaKeys.ORIGINAL_AFFINE].copy()
13211323
header[MetaKeys.SPATIAL_SHAPE] = header["sizes"]

monai/data/itk_torch_bridge.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919
from monai.config.type_definitions import DtypeLike
2020
from monai.data import ITKReader, ITKWriter
2121
from monai.data.meta_tensor import MetaTensor
22+
from monai.data.utils import orientation_ras_lps
2223
from monai.transforms import EnsureChannelFirst
23-
from monai.utils import convert_to_dst_type, optional_import
24+
from monai.utils import MetaKeys, SpaceKeys, convert_to_dst_type, optional_import
2425

2526
if TYPE_CHECKING:
2627
import itk
@@ -83,12 +84,18 @@ def metatensor_to_itk_image(
8384
8485
See also: :py:func:`ITKWriter.create_backend_obj`
8586
"""
87+
if meta_tensor.meta.get(MetaKeys.SPACE, SpaceKeys.LPS) == SpaceKeys.RAS:
88+
_meta_tensor = meta_tensor.clone()
89+
_meta_tensor.affine = orientation_ras_lps(meta_tensor.affine)
90+
_meta_tensor.meta[MetaKeys.SPACE] = SpaceKeys.LPS
91+
else:
92+
_meta_tensor = meta_tensor
8693
writer = ITKWriter(output_dtype=dtype, affine_lps_to_ras=False)
8794
writer.set_data_array(data_array=meta_tensor.data, channel_dim=channel_dim, squeeze_end_dims=True)
8895
return writer.create_backend_obj(
8996
writer.data_obj,
9097
channel_dim=writer.channel_dim,
91-
affine=meta_tensor.affine,
98+
affine=_meta_tensor.affine,
9299
affine_lps_to_ras=False, # False if the affine is in itk convention
93100
dtype=writer.output_dtype,
94101
kwargs=kwargs,

tests/test_itk_torch_bridge.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@
1111

1212
from __future__ import annotations
1313

14+
import itertools
1415
import os
16+
import tempfile
1517
import unittest
1618

1719
import numpy as np
1820
import torch
1921
from parameterized import parameterized
2022

23+
import monai
24+
import monai.transforms as mt
2125
from monai.apps import download_url
2226
from monai.data import ITKReader
2327
from monai.data.itk_torch_bridge import (
@@ -31,14 +35,17 @@
3135
from monai.networks.blocks import Warp
3236
from monai.transforms import Affine
3337
from monai.utils import optional_import, set_determinism
34-
from tests.utils import skip_if_downloading_fails, skip_if_quick, test_is_quick, testing_data_config
38+
from tests.utils import assert_allclose, skip_if_downloading_fails, skip_if_quick, test_is_quick, testing_data_config
3539

3640
itk, has_itk = optional_import("itk")
41+
_, has_nib = optional_import("nibabel")
3742

3843
TESTS = ["CT_2D_head_fixed.mha", "CT_2D_head_moving.mha"]
3944
if not test_is_quick():
4045
TESTS += ["copd1_highres_INSP_STD_COPD_img.nii.gz", "copd1_highres_EXP_STD_COPD_img.nii.gz"]
4146

47+
RW_TESTS = TESTS + ["nrrd_example.nrrd"]
48+
4249

4350
@unittest.skipUnless(has_itk, "Requires `itk` package.")
4451
class TestITKTorchAffineMatrixBridge(unittest.TestCase):
@@ -47,7 +54,7 @@ def setUp(self):
4754
self.data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "testing_data")
4855
self.reader = ITKReader(pixel_type=itk.F)
4956

50-
for file_name in TESTS:
57+
for file_name in RW_TESTS:
5158
path = os.path.join(self.data_dir, file_name)
5259
if not os.path.exists(path):
5360
with skip_if_downloading_fails():
@@ -482,5 +489,34 @@ def test_use_reference_space(self, ref_filepath, filepath):
482489
np.testing.assert_allclose(output_array_monai, output_array_itk, rtol=1e-3, atol=1e-3)
483490

484491

492+
@unittest.skipUnless(has_itk, "Requires `itk` package.")
493+
@unittest.skipUnless(has_nib, "Requires `nibabel` package.")
494+
@skip_if_quick
495+
class TestITKTorchRW(unittest.TestCase):
496+
def setUp(self):
497+
TestITKTorchAffineMatrixBridge.setUp(self)
498+
499+
def tearDown(self):
500+
TestITKTorchAffineMatrixBridge.setUp(self)
501+
502+
@parameterized.expand(list(itertools.product(RW_TESTS, ["ITKReader", "NrrdReader"], [True, False])))
503+
def test_rw_itk(self, filepath, reader, flip):
504+
"""reading and convert: filepath, reader, flip"""
505+
print(filepath, reader, flip)
506+
fname = os.path.join(self.data_dir, filepath)
507+
xform = mt.LoadImageD("img", image_only=True, ensure_channel_first=True, affine_lps_to_ras=flip, reader=reader)
508+
out = xform({"img": fname})["img"]
509+
itk_image = metatensor_to_itk_image(out, channel_dim=0, dtype=float)
510+
with tempfile.TemporaryDirectory() as tempdir:
511+
tname = os.path.join(tempdir, filepath) + (".nii.gz" if not filepath.endswith(".nii.gz") else "")
512+
itk.imwrite(itk_image, tname, True)
513+
ref = mt.LoadImage(image_only=True, ensure_channel_first=True, reader="NibabelReader")(tname)
514+
if out.meta["space"] != ref.meta["space"]:
515+
ref.affine = monai.data.utils.orientation_ras_lps(ref.affine)
516+
assert_allclose(
517+
out.affine, monai.data.utils.to_affine_nd(len(out.affine) - 1, ref.affine), rtol=1e-3, atol=1e-3
518+
)
519+
520+
485521
if __name__ == "__main__":
486522
unittest.main()

tests/testing_data/data_config.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@
8484
"url": "https://github.com/Project-MONAI/MONAI-extra-test-data/releases/download/0.8.1/CT_DICOM_SINGLE.zip",
8585
"hash_type": "sha256",
8686
"hash_val": "a41f6e93d2e3d68956144f9a847273041d36441da12377d6a1d5ae610e0a7023"
87+
},
88+
"nrrd_example": {
89+
"url": "https://github.com/Project-MONAI/MONAI-extra-test-data/releases/download/0.8.1/CT_IMAGE_cropped.nrrd",
90+
"hash_type": "sha256",
91+
"hash_val": "66971ad17f0bac50e6082ed6a4dc1ae7093c30517137e53327b15a752327a1c0"
8792
}
8893
},
8994
"videos": {

0 commit comments

Comments
 (0)