Skip to content

Commit

Permalink
6985 metatensor_to_itk_image compatible space (#7000)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
wyli committed Sep 18, 2023
1 parent 5a644e4 commit b31367f
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 4 deletions.
2 changes: 2 additions & 0 deletions monai/data/image_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
11 changes: 9 additions & 2 deletions monai/data/itk_torch_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
40 changes: 38 additions & 2 deletions tests/test_itk_torch_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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):
Expand All @@ -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():
Expand Down Expand Up @@ -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()
5 changes: 5 additions & 0 deletions tests/testing_data/data_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down

0 comments on commit b31367f

Please sign in to comment.