From 6559460972b151e9fee5a53d0bb9d8818cbe9a7e Mon Sep 17 00:00:00 2001 From: Rich <33289025+rijobro@users.noreply.github.com> Date: Mon, 1 Mar 2021 11:29:22 +0000 Subject: [PATCH 1/7] allow dictionary image only and test endianness Signed-off-by: Richard Brown <33289025+rijobro@users.noreply.github.com> --- monai/transforms/io/dictionary.py | 26 ++++++++++++-------- tests/test_nifti_endianness.py | 40 +++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 tests/test_nifti_endianness.py diff --git a/monai/transforms/io/dictionary.py b/monai/transforms/io/dictionary.py index 55707f750e..1d498018ae 100644 --- a/monai/transforms/io/dictionary.py +++ b/monai/transforms/io/dictionary.py @@ -59,6 +59,7 @@ def __init__( dtype: DtypeLike = np.float32, meta_key_postfix: str = "meta_dict", overwriting: bool = False, + image_only: bool = False, *args, **kwargs, ) -> None: @@ -80,7 +81,7 @@ def __init__( kwargs: additional parameters for reader if providing a reader name. """ super().__init__(keys) - self._loader = LoadImage(reader, False, dtype, *args, **kwargs) + self._loader = LoadImage(reader, image_only, dtype, *args, **kwargs) if not isinstance(meta_key_postfix, str): raise TypeError(f"meta_key_postfix must be a str but is {type(meta_key_postfix).__name__}.") self.meta_key_postfix = meta_key_postfix @@ -98,15 +99,20 @@ def __call__(self, data, reader: Optional[ImageReader] = None): d = dict(data) for key in self.keys: data = self._loader(d[key], reader) - if not isinstance(data, (tuple, list)): - raise ValueError("loader must return a tuple or list.") - d[key] = data[0] - if not isinstance(data[1], dict): - raise ValueError("metadata must be a dict.") - key_to_add = f"{key}_{self.meta_key_postfix}" - if key_to_add in d and not self.overwriting: - raise KeyError(f"Meta data with key {key_to_add} already exists and overwriting=False.") - d[key_to_add] = data[1] + if self._loader.image_only: + if not isinstance(data, np.ndarray): + raise ValueError("loader must return a numpy array (because image_only=True was used).") + d[key] = data + else: + if not isinstance(data, (tuple, list)): + raise ValueError("loader must return a tuple or list (because image_only=False was used).") + d[key] = data[0] + if not isinstance(data[1], dict): + raise ValueError("metadata must be a dict.") + key_to_add = f"{key}_{self.meta_key_postfix}" + if key_to_add in d and not self.overwriting: + raise KeyError(f"Meta data with key {key_to_add} already exists and overwriting=False.") + d[key_to_add] = data[1] return d diff --git a/tests/test_nifti_endianness.py b/tests/test_nifti_endianness.py new file mode 100644 index 0000000000..71b9501588 --- /dev/null +++ b/tests/test_nifti_endianness.py @@ -0,0 +1,40 @@ +from monai.utils.module import optional_import +from typing import List, Tuple +from unittest.case import skipUnless +from monai.data import Dataset, DataLoader +from monai.transforms import LoadImaged, LoadImage +from monai.data import create_test_image_2d +import numpy as np +import unittest +import tempfile +from parameterized import parameterized + +nib, has_nib = optional_import("nibabel") + +TESTS: List[Tuple] = [] +for endianness in ["<", ">"]: + for use_array in [True, False]: + for image_only in [True, False]: + TESTS.append((endianness, use_array, image_only)) + +class TestNiftiEndianness(unittest.TestCase): + def setUp(self): + self.im, _ = create_test_image_2d(100, 100) + self.fname = tempfile.NamedTemporaryFile(suffix=".nii.gz").name + + @skipUnless(has_nib, "Requires NiBabel") + @parameterized.expand(TESTS) + def test_value(self, endianness, use_array, image_only): + + hdr = nib.Nifti1Header(endianness=endianness) + nii = nib.Nifti1Image(self.im, np.eye(4), header=hdr) + nib.save(nii, self.fname) + + data = [self.fname] if use_array else [{"image": self.fname}] + tr = LoadImage(image_only=image_only) if use_array else LoadImaged("image", image_only=image_only) + check_ds = Dataset(data, tr) + check_loader = DataLoader(check_ds, batch_size=1) + _ = next(iter(check_loader)) + +if __name__ == "__main__": + unittest.main() From 34ebc8a4d1bc0d886d9ec4ec2ce5aa4b20a1478d Mon Sep 17 00:00:00 2001 From: Rich <33289025+rijobro@users.noreply.github.com> Date: Mon, 1 Mar 2021 12:45:58 +0000 Subject: [PATCH 2/7] switch endianness of meta data Signed-off-by: Richard Brown <33289025+rijobro@users.noreply.github.com> --- monai/transforms/io/array.py | 24 ++++++++++++++++++++++++ tests/test_nifti_endianness.py | 17 ++++++++++------- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/monai/transforms/io/array.py b/monai/transforms/io/array.py index 855621e432..cf79ad62fb 100644 --- a/monai/transforms/io/array.py +++ b/monai/transforms/io/array.py @@ -33,6 +33,27 @@ __all__ = ["LoadImage", "SaveImage"] +def switch_endianness(data, old, new): + """ + If any numpy arrays have `old` (e.g., ">"), + replace with `new` (e.g., "<"). + """ + if isinstance(data, np.ndarray): + if data.dtype.byteorder == old: + data = data.newbyteorder(new) + elif isinstance(data, tuple): + data = (switch_endianness(x, old, new) for x in data) + elif isinstance(data, list): + data = [switch_endianness(x, old, new) for x in data] + elif isinstance(data, dict): + data = {k: switch_endianness(v, old, new) for k, v in data.items()} + elif isinstance(data, (bool, str)): + pass + else: + raise AssertionError() + return data + + class LoadImage(Transform): """ Load image file or files from provided path based on reader. @@ -132,6 +153,9 @@ def __call__( if self.image_only: return img_array meta_data[Key.FILENAME_OR_OBJ] = ensure_tuple(filename)[0] + # make sure all elements in metadata are little endian + meta_data = switch_endianness(meta_data, ">", "<") + return img_array, meta_data diff --git a/tests/test_nifti_endianness.py b/tests/test_nifti_endianness.py index 71b9501588..f44199d449 100644 --- a/tests/test_nifti_endianness.py +++ b/tests/test_nifti_endianness.py @@ -1,14 +1,15 @@ -from monai.utils.module import optional_import +import tempfile +import unittest from typing import List, Tuple from unittest.case import skipUnless -from monai.data import Dataset, DataLoader -from monai.transforms import LoadImaged, LoadImage -from monai.data import create_test_image_2d + import numpy as np -import unittest -import tempfile from parameterized import parameterized +from monai.data import DataLoader, Dataset, create_test_image_2d +from monai.transforms import LoadImage, LoadImaged +from monai.utils.module import optional_import + nib, has_nib = optional_import("nibabel") TESTS: List[Tuple] = [] @@ -17,6 +18,7 @@ for image_only in [True, False]: TESTS.append((endianness, use_array, image_only)) + class TestNiftiEndianness(unittest.TestCase): def setUp(self): self.im, _ = create_test_image_2d(100, 100) @@ -24,7 +26,7 @@ def setUp(self): @skipUnless(has_nib, "Requires NiBabel") @parameterized.expand(TESTS) - def test_value(self, endianness, use_array, image_only): + def test_endianness(self, endianness, use_array, image_only): hdr = nib.Nifti1Header(endianness=endianness) nii = nib.Nifti1Image(self.im, np.eye(4), header=hdr) @@ -36,5 +38,6 @@ def test_value(self, endianness, use_array, image_only): check_loader = DataLoader(check_ds, batch_size=1) _ = next(iter(check_loader)) + if __name__ == "__main__": unittest.main() From e0a395c77100d6d6f6c61197ff49ae4f1e35a05f Mon Sep 17 00:00:00 2001 From: Richard Brown <33289025+rijobro@users.noreply.github.com> Date: Mon, 1 Mar 2021 16:06:05 +0000 Subject: [PATCH 3/7] fix test_load_image Signed-off-by: Richard Brown <33289025+rijobro@users.noreply.github.com> --- monai/transforms/io/array.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/transforms/io/array.py b/monai/transforms/io/array.py index cf79ad62fb..9c4f631699 100644 --- a/monai/transforms/io/array.py +++ b/monai/transforms/io/array.py @@ -47,7 +47,7 @@ def switch_endianness(data, old, new): data = [switch_endianness(x, old, new) for x in data] elif isinstance(data, dict): data = {k: switch_endianness(v, old, new) for k, v in data.items()} - elif isinstance(data, (bool, str)): + elif isinstance(data, (bool, str, float, int)): pass else: raise AssertionError() From dcb52bd10e40a7bd2079112bf8135f361a98789d Mon Sep 17 00:00:00 2001 From: Richard Brown <33289025+rijobro@users.noreply.github.com> Date: Mon, 1 Mar 2021 16:50:49 +0000 Subject: [PATCH 4/7] type checking Signed-off-by: Richard Brown <33289025+rijobro@users.noreply.github.com> --- tests/test_nifti_endianness.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_nifti_endianness.py b/tests/test_nifti_endianness.py index f44199d449..a85abb11f2 100644 --- a/tests/test_nifti_endianness.py +++ b/tests/test_nifti_endianness.py @@ -1,6 +1,6 @@ import tempfile import unittest -from typing import List, Tuple +from typing import TYPE_CHECKING, List, Tuple from unittest.case import skipUnless import numpy as np @@ -10,7 +10,12 @@ from monai.transforms import LoadImage, LoadImaged from monai.utils.module import optional_import -nib, has_nib = optional_import("nibabel") +if TYPE_CHECKING: + import nibabel as nib + + has_nib = True +else: + nib, has_nib = optional_import("nibabel") TESTS: List[Tuple] = [] for endianness in ["<", ">"]: From e5a14a6a9c511f6049ffe104f3cf2cabddd78bc3 Mon Sep 17 00:00:00 2001 From: Richard Brown <33289025+rijobro@users.noreply.github.com> Date: Mon, 1 Mar 2021 17:25:55 +0000 Subject: [PATCH 5/7] switch order Signed-off-by: Richard Brown <33289025+rijobro@users.noreply.github.com> --- tests/test_nifti_endianness.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_nifti_endianness.py b/tests/test_nifti_endianness.py index a85abb11f2..14317c0832 100644 --- a/tests/test_nifti_endianness.py +++ b/tests/test_nifti_endianness.py @@ -29,8 +29,8 @@ def setUp(self): self.im, _ = create_test_image_2d(100, 100) self.fname = tempfile.NamedTemporaryFile(suffix=".nii.gz").name - @skipUnless(has_nib, "Requires NiBabel") @parameterized.expand(TESTS) + @skipUnless(has_nib, "Requires NiBabel") def test_endianness(self, endianness, use_array, image_only): hdr = nib.Nifti1Header(endianness=endianness) From ef01a1bf207041e22e7f7d6f7dd7eb116d28092d Mon Sep 17 00:00:00 2001 From: Richard Brown <33289025+rijobro@users.noreply.github.com> Date: Mon, 1 Mar 2021 18:17:23 +0000 Subject: [PATCH 6/7] add docstring Signed-off-by: Richard Brown <33289025+rijobro@users.noreply.github.com> --- monai/transforms/io/dictionary.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monai/transforms/io/dictionary.py b/monai/transforms/io/dictionary.py index 1d498018ae..4dd8209d67 100644 --- a/monai/transforms/io/dictionary.py +++ b/monai/transforms/io/dictionary.py @@ -77,6 +77,8 @@ def __init__( For example, load nifti file for `image`, store the metadata into `image_meta_dict`. overwriting: whether allow to overwrite existing meta data of same key. default is False, which will raise exception if encountering existing key. + image_only: if True return dictionary containing just only the image volumes, otherwise return + dictionary containing image data array and header dict per input key. args: additional parameters for reader if providing a reader name. kwargs: additional parameters for reader if providing a reader name. """ From 3bcb71d5333c238705849d946c886ef5c32aef3f Mon Sep 17 00:00:00 2001 From: Richard Brown <33289025+rijobro@users.noreply.github.com> Date: Tue, 2 Mar 2021 09:48:40 +0000 Subject: [PATCH 7/7] autofix Signed-off-by: Richard Brown <33289025+rijobro@users.noreply.github.com> --- monai/transforms/io/dictionary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/transforms/io/dictionary.py b/monai/transforms/io/dictionary.py index 4dd8209d67..d9b6b5e6ab 100644 --- a/monai/transforms/io/dictionary.py +++ b/monai/transforms/io/dictionary.py @@ -78,7 +78,7 @@ def __init__( overwriting: whether allow to overwrite existing meta data of same key. default is False, which will raise exception if encountering existing key. image_only: if True return dictionary containing just only the image volumes, otherwise return - dictionary containing image data array and header dict per input key. + dictionary containing image data array and header dict per input key. args: additional parameters for reader if providing a reader name. kwargs: additional parameters for reader if providing a reader name. """