Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

909 Update LoadImage to use Nibabel as default #1307

Merged
merged 20 commits into from
Dec 2, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions monai/transforms/io/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@
https://github.com/Project-MONAI/MONAI/wiki/MONAI_Design
"""

import warnings
from pathlib import Path
from typing import Dict, List, Optional, Sequence, Union

import numpy as np
from torch.utils.data._utils.collate import np_str_obj_array_pattern

from monai.config import KeysCollection
from monai.data.image_reader import ImageReader, ITKReader
from monai.data.image_reader import ImageReader, ITKReader, NibabelReader, NumpyReader, PILReader
from monai.data.utils import correct_nifti_header_if_necessary
from monai.transforms.compose import Transform
from monai.utils import ensure_tuple, optional_import
Expand All @@ -31,38 +32,53 @@

class LoadImage(Transform):
"""
Load image file or files from provided path based on reader, default reader is ITK.
All the supported image formats of ITK:
https://github.com/InsightSoftwareConsortium/ITK/tree/master/Modules/IO
Load image file or files from provided path based on reader, default reader is Nibabel.
Automatically choose readers based on the supported suffixes and in below order:
- User specified reader at runtime when call this loader.
- Registered readers from the first to the last in list.
- Default ITK reader.
- Default Nibabel reader.

"""

def __init__(
self,
reader: Optional[ImageReader] = None,
reader: Optional[Union[ImageReader, str]] = None,
image_only: bool = False,
dtype: np.dtype = np.float32,
*args,
**kwargs,
) -> None:
"""
Args:
reader: register reader to load image file and meta data, if None, still can register readers
at runtime or use the default ITK reader.
at runtime or use the default NIbabel reader. If a string of reader name provided, will construct
a reader object with the `*args` and `**kwargs` parameters, supported reader name: "NibabelReader",
"PILReader", "ITKReader", "NumpyReader"
image_only: if True return only the image volume, otherwise return image data array and header dict.
dtype: if not None convert the loaded image to this data type.
args: additional parameters for reader if providing a reader name.
kwargs: additional parameters for reader if providing a reader name.

Note:
The transform returns image data array if `image_only` is True,
or a tuple of two elements containing the data array, and the meta data in a dict format otherwise.

"""
self.default_reader: ITKReader = ITKReader()
self.default_reader: NibabelReader = NibabelReader()
self.readers: List[ImageReader] = list()
if reader is not None:
if isinstance(reader, str):
supported_readers = {
Nic-Ma marked this conversation as resolved.
Show resolved Hide resolved
"NibabelReader": NibabelReader,
"PILReader": PILReader,
"ITKReader": ITKReader,
"NumpyReader": NumpyReader,
}
if reader not in supported_readers:
raise ValueError(f"unsupported reader type: {reader}.")
reader = supported_readers[reader](*args, **kwargs)
self.readers.append(reader)

self.image_only = image_only
self.dtype = dtype

Expand All @@ -73,7 +89,7 @@ def register(self, reader: ImageReader) -> List[ImageReader]:

Args:
reader: registered reader to load image file and meta data based on suffix,
if all registered readers can't match suffix at runtime, use the default ITK reader.
if all registered readers can't match suffix at runtime, use the default Nibabel reader.

"""
Nic-Ma marked this conversation as resolved.
Show resolved Hide resolved
self.readers.append(reader)
Expand Down Expand Up @@ -136,6 +152,7 @@ def __init__(
- header['affine'] stores the affine of the image.
- header['original_affine'] will be additionally created to store the original affine.
"""
warnings.warn("LoadNifti will be deprecated in v0.5, please use LoadImage instead.", DeprecationWarning)
self.as_closest_canonical = as_closest_canonical
self.image_only = image_only
self.dtype = dtype
Expand Down Expand Up @@ -205,6 +222,7 @@ def __init__(self, image_only: bool = False, dtype: Optional[np.dtype] = np.floa
image_only: if True return only the image volume, otherwise return image data array and metadata.
dtype: if not None convert the loaded image to this data type.
"""
warnings.warn("LoadPNG will be deprecated in v0.5, please use LoadImage instead.", DeprecationWarning)
self.image_only = image_only
self.dtype = dtype

Expand Down Expand Up @@ -267,6 +285,7 @@ def __init__(
stack the loaded items together to construct a new first dimension.

"""
warnings.warn("LoadNumpy will be deprecated in v0.5, please use LoadImage instead.", DeprecationWarning)
self.data_only = data_only
self.dtype = dtype
if npz_keys is not None:
Expand Down
14 changes: 10 additions & 4 deletions monai/transforms/io/dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
Class names are ended with 'd' to denote dictionary-based transforms.
"""

from typing import Callable, Optional
from typing import Callable, Optional, Union

import numpy as np

Expand All @@ -38,26 +38,32 @@ class LoadImaged(MapTransform):
def __init__(
self,
keys: KeysCollection,
reader: Optional[ImageReader] = None,
reader: Optional[Union[ImageReader, str]] = None,
dtype: Optional[np.dtype] = np.float32,
meta_key_postfix: str = "meta_dict",
overwriting: bool = False,
*args,
**kwargs,
) -> None:
"""
Args:
keys: keys of the corresponding items to be transformed.
See also: :py:class:`monai.transforms.compose.MapTransform`
reader: register reader to load image file and meta data, if None, still can register readers
at runtime or use the default ITK reader.
at runtime or use the default Nibabel reader. If a string of reader name provided, will construct
a reader object with the `*args` and `**kwargs` parameters, supported reader name: "NibabelReader",
"PILReader", "ITKReader", "NumpyReader"
dtype: if not None convert the loaded image data to this data type.
meta_key_postfix: use `key_{postfix}` to store the metadata of the nifti image,
default is `meta_dict`. The meta data is a dictionary object.
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.
args: additional parameters for reader if providing a reader name.
kwargs: additional parameters for reader if providing a reader name.
"""
super().__init__(keys)
self._loader = LoadImage(reader, False, dtype)
self._loader = LoadImage(reader, False, 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
Expand Down
36 changes: 17 additions & 19 deletions tests/test_load_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,18 @@
from monai.transforms import LoadImage
from tests.utils import SkipIfNoModule

TEST_CASE_1 = [
{"reader": NibabelReader(), "image_only": True},
["test_image.nii.gz"],
(128, 128, 128),
]
TEST_CASE_1 = [{"image_only": True}, ["test_image.nii.gz"], (128, 128, 128)]

TEST_CASE_2 = [
{"reader": NibabelReader(), "image_only": False},
["test_image.nii.gz"],
(128, 128, 128),
]
TEST_CASE_2 = [{"image_only": False}, ["test_image.nii.gz"], (128, 128, 128)]

TEST_CASE_3 = [
{"reader": NibabelReader(), "image_only": True},
{"image_only": True},
["test_image.nii.gz", "test_image2.nii.gz", "test_image3.nii.gz"],
(3, 128, 128, 128),
]

TEST_CASE_4 = [
{"reader": NibabelReader(), "image_only": False},
{"image_only": False},
["test_image.nii.gz", "test_image2.nii.gz", "test_image3.nii.gz"],
(3, 128, 128, 128),
]
Expand All @@ -53,18 +45,18 @@
(128, 128, 128),
]

TEST_CASE_6 = [{"image_only": True}, ["test_image.nii.gz"], (128, 128, 128)]
TEST_CASE_6 = [{"reader": ITKReader(), "image_only": True}, ["test_image.nii.gz"], (128, 128, 128)]

TEST_CASE_7 = [{"image_only": False}, ["test_image.nii.gz"], (128, 128, 128)]
TEST_CASE_7 = [{"reader": ITKReader(), "image_only": False}, ["test_image.nii.gz"], (128, 128, 128)]

TEST_CASE_8 = [
{"image_only": True},
{"reader": ITKReader(), "image_only": True},
["test_image.nii.gz", "test_image2.nii.gz", "test_image3.nii.gz"],
(3, 128, 128, 128),
]

TEST_CASE_9 = [
{"image_only": False},
{"reader": ITKReader(), "image_only": False},
["test_image.nii.gz", "test_image2.nii.gz", "test_image3.nii.gz"],
(3, 128, 128, 128),
]
Expand All @@ -75,6 +67,12 @@
(4, 16, 16),
]

TEST_CASE_11 = [
{"image_only": False, "reader": "ITKReader", "pixel_type": itk.UC},
"tests/testing_data/CT_DICOM",
(4, 16, 16),
]


class TestLoadImage(unittest.TestCase):
@parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5])
Expand Down Expand Up @@ -112,7 +110,7 @@ def test_itk_reader(self, input_param, filenames, expected_shape):
np.testing.assert_allclose(header["original_affine"], np.eye(4))
self.assertTupleEqual(result.shape, expected_shape)

@parameterized.expand([TEST_CASE_10])
@parameterized.expand([TEST_CASE_10, TEST_CASE_11])
@SkipIfNoModule("itk")
def test_itk_dicom_series_reader(self, input_param, filenames, expected_shape):
result, header = LoadImage(**input_param)(filenames)
Expand All @@ -138,7 +136,7 @@ def test_itk_reader_multichannel(self):
filename = os.path.join(tempdir, "test_image.png")
itk_np_view = itk.image_view_from_array(test_image, is_vector=True)
itk.imwrite(itk_np_view, filename)
result, header = LoadImage()(filename)
result, header = LoadImage(reader=ITKReader())(filename)

self.assertTupleEqual(tuple(header["spatial_shape"]), (256, 256))
np.testing.assert_allclose(result[0, :, :], test_image[:, :, 0])
Expand All @@ -151,7 +149,7 @@ def test_load_png(self):
with tempfile.TemporaryDirectory() as tempdir:
filename = os.path.join(tempdir, "test_image.png")
Image.fromarray(test_image.astype("uint8")).save(filename)
result, header = LoadImage(image_only=False)(filename)
result, header = LoadImage(reader=ITKReader(), image_only=False)(filename)
self.assertTupleEqual(tuple(header["spatial_shape"]), spatial_size)
self.assertTupleEqual(result.shape, spatial_size)
np.testing.assert_allclose(header["affine"], np.eye(3))
Expand Down
4 changes: 3 additions & 1 deletion tests/test_load_imaged.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@

TEST_CASE_1 = [{"keys": KEYS}, (128, 128, 128)]

TEST_CASE_2 = [{"keys": KEYS, "reader": "ITKReader", "fallback_only": False}, (128, 128, 128)]


class TestLoadImaged(unittest.TestCase):
@parameterized.expand([TEST_CASE_1])
@parameterized.expand([TEST_CASE_1, TEST_CASE_2])
def test_shape(self, input_param, expected_shape):
test_image = nib.Nifti1Image(np.random.rand(128, 128, 128), np.eye(4))
test_data = dict()
Expand Down