From d96960bb86f1c7e45d5e145a64b07f756f84cb8e Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+behxyz@users.noreply.github.com> Date: Wed, 24 Feb 2021 15:13:54 -0500 Subject: [PATCH 01/12] Add ToPIL transformation Signed-off-by: Behrooz <3968947+behxyz@users.noreply.github.com> --- monai/transforms/utility/array.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index c0ae40de59..3cc051ca91 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -15,7 +15,7 @@ import logging import time -from typing import Callable, List, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING, Callable, List, Optional, Sequence, Tuple, Union import numpy as np import torch @@ -25,6 +25,13 @@ from monai.transforms.utils import extreme_points_to_image, get_extreme_points, map_binary_to_indices from monai.utils import ensure_tuple, min_version, optional_import +if TYPE_CHECKING: + from PIL import Image as PILImage + + has_pil = True +else: + PILImage, has_pil = optional_import("PIL.Image") + __all__ = [ "Identity", "AsChannelFirst", @@ -261,6 +268,21 @@ def __call__(self, img: Union[List, Tuple, np.ndarray, torch.Tensor]) -> np.ndar return np.ascontiguousarray(img) +class ToPIL(Transform): + """ + Converts the input image (in the form of NumPy array or PyTorch Tensor) to PIL image + """ + + def __call__(self, img: Union[np.ndarray, torch.Tensor]) -> PILImage.Image: + """ + Apply the transform to `img` and make it contiguous. + """ + if isinstance(img, torch.Tensor): + img = img.detach().cpu().numpy() + img = np.ascontiguousarray(img) + return PILImage.fromarray(img) + + class Transpose(Transform): """ Transposes the input image based on the given `indices` dimension ordering. From 92a9eb3e398ad898ec0d5db60ec77b939f84a3bb Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+behxyz@users.noreply.github.com> Date: Wed, 24 Feb 2021 15:17:13 -0500 Subject: [PATCH 02/12] Add ToPILd, ToPILD, ToPILDict Signed-off-by: Behrooz <3968947+behxyz@users.noreply.github.com> --- monai/transforms/utility/dictionary.py | 40 ++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index f374b82d76..cb6ac6a21e 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -17,7 +17,7 @@ import copy import logging -from typing import Any, Callable, Dict, Hashable, List, Mapping, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Hashable, List, Mapping, Optional, Sequence, Tuple, Union import numpy as np import torch @@ -40,12 +40,16 @@ SplitChannel, SqueezeDim, ToNumpy, + ToPIL, TorchVision, ToTensor, ) from monai.transforms.utils import extreme_points_to_image, get_extreme_points from monai.utils import ensure_tuple, ensure_tuple_rep +if TYPE_CHECKING: + from PIL import Image as PILImage + __all__ = [ "Identityd", "AsChannelFirstd", @@ -56,6 +60,7 @@ "CastToTyped", "ToTensord", "ToNumpyd", + "ToPILd", "DeleteItemsd", "SelectItemsd", "SqueezeDimd", @@ -322,8 +327,8 @@ def __init__(self, keys: KeysCollection) -> None: self.converter = ToTensor() def __call__( - self, data: Mapping[Hashable, Union[np.ndarray, torch.Tensor]] - ) -> Dict[Hashable, Union[np.ndarray, torch.Tensor]]: + self, data: Mapping[Hashable, Union[np.ndarray, torch.Tensor, PILImage]] + ) -> Dict[Hashable, Union[np.ndarray, torch.Tensor, PILImage]]: d = dict(data) for key in self.keys: d[key] = self.converter(d[key]) @@ -345,8 +350,31 @@ def __init__(self, keys: KeysCollection) -> None: self.converter = ToNumpy() def __call__( - self, data: Mapping[Hashable, Union[np.ndarray, torch.Tensor]] - ) -> Dict[Hashable, Union[np.ndarray, torch.Tensor]]: + self, data: Mapping[Hashable, Union[np.ndarray, torch.Tensor, PILImage]] + ) -> Dict[Hashable, Union[np.ndarray, torch.Tensor, PILImage]]: + d = dict(data) + for key in self.keys: + d[key] = self.converter(d[key]) + return d + + +class ToPILd(MapTransform): + """ + Dictionary-based wrapper of :py:class:`monai.transforms.ToNumpy`. + """ + + def __init__(self, keys: KeysCollection) -> None: + """ + Args: + keys: keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + """ + super().__init__(keys) + self.converter = ToPIL() + + def __call__( + self, data: Mapping[Hashable, Union[np.ndarray, torch.Tensor, PILImage]] + ) -> Dict[Hashable, Union[np.ndarray, torch.Tensor, PILImage]]: d = dict(data) for key in self.keys: d[key] = self.converter(d[key]) @@ -840,6 +868,8 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc SplitChannelD = SplitChannelDict = SplitChanneld CastToTypeD = CastToTypeDict = CastToTyped ToTensorD = ToTensorDict = ToTensord +ToNumpyD = ToNumpyDict = ToNumpyd +ToPILD = ToPILDict = ToPILd DeleteItemsD = DeleteItemsDict = DeleteItemsd SqueezeDimD = SqueezeDimDict = SqueezeDimd DataStatsD = DataStatsDict = DataStatsd From d3673296e6cda9f429df14fca91103ee7546a003 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+behxyz@users.noreply.github.com> Date: Wed, 24 Feb 2021 15:18:19 -0500 Subject: [PATCH 03/12] Remove has_pil Signed-off-by: Behrooz <3968947+behxyz@users.noreply.github.com> --- monai/transforms/utility/array.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index 3cc051ca91..fad82cff9d 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -27,10 +27,8 @@ if TYPE_CHECKING: from PIL import Image as PILImage - - has_pil = True else: - PILImage, has_pil = optional_import("PIL.Image") + PILImage, _ = optional_import("PIL.Image") __all__ = [ "Identity", From 99f5de4684e13a8a03e8ba8861000d4a25ca1de4 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+behxyz@users.noreply.github.com> Date: Wed, 24 Feb 2021 15:19:48 -0500 Subject: [PATCH 04/12] Include ToPIL, ToPILd, ToPILD, and ToPILDict Also include ToNumpyD, ToNumpyDict, TorchVisionD, and TorchVisionDict Signed-off-by: Behrooz <3968947+behxyz@users.noreply.github.com> --- monai/transforms/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index 4dc7744755..cd6a78ad09 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -253,6 +253,7 @@ ToNumpy, TorchVision, ToTensor, + ToPIL, Transpose, ) from .utility.dictionary import ( @@ -315,7 +316,14 @@ SqueezeDimD, SqueezeDimDict, ToNumpyd, + ToNumpyD, + ToNumpyDict, + ToPILd, + ToPILD, + ToPILDict, TorchVisiond, + TorchVisionD, + TorchVisionDict, ToTensord, ToTensorD, ToTensorDict, From 0d32f8ff5f5c5cba1a5e03861c101e09edd73570 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+behxyz@users.noreply.github.com> Date: Wed, 24 Feb 2021 15:23:13 -0500 Subject: [PATCH 05/12] Fix a typing issue Signed-off-by: Behrooz <3968947+behxyz@users.noreply.github.com> --- monai/transforms/utility/dictionary.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index cb6ac6a21e..ac1871bb72 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -327,8 +327,8 @@ def __init__(self, keys: KeysCollection) -> None: self.converter = ToTensor() def __call__( - self, data: Mapping[Hashable, Union[np.ndarray, torch.Tensor, PILImage]] - ) -> Dict[Hashable, Union[np.ndarray, torch.Tensor, PILImage]]: + self, data: Mapping[Hashable, Union[np.ndarray, torch.Tensor, PILImage.Image]] + ) -> Dict[Hashable, Union[np.ndarray, torch.Tensor, PILImage.Image]]: d = dict(data) for key in self.keys: d[key] = self.converter(d[key]) @@ -350,8 +350,8 @@ def __init__(self, keys: KeysCollection) -> None: self.converter = ToNumpy() def __call__( - self, data: Mapping[Hashable, Union[np.ndarray, torch.Tensor, PILImage]] - ) -> Dict[Hashable, Union[np.ndarray, torch.Tensor, PILImage]]: + self, data: Mapping[Hashable, Union[np.ndarray, torch.Tensor, PILImage.Image]] + ) -> Dict[Hashable, Union[np.ndarray, torch.Tensor, PILImage.Image]]: d = dict(data) for key in self.keys: d[key] = self.converter(d[key]) @@ -373,8 +373,8 @@ def __init__(self, keys: KeysCollection) -> None: self.converter = ToPIL() def __call__( - self, data: Mapping[Hashable, Union[np.ndarray, torch.Tensor, PILImage]] - ) -> Dict[Hashable, Union[np.ndarray, torch.Tensor, PILImage]]: + self, data: Mapping[Hashable, Union[np.ndarray, torch.Tensor, PILImage.Image]] + ) -> Dict[Hashable, Union[np.ndarray, torch.Tensor, PILImage.Image]]: d = dict(data) for key in self.keys: d[key] = self.converter(d[key]) From 6a8e0a6112423f980b413f82e24d70df54e67309 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+behxyz@users.noreply.github.com> Date: Thu, 25 Feb 2021 09:27:05 -0500 Subject: [PATCH 06/12] Fix PIL optional import Signed-off-by: Behrooz <3968947+behxyz@users.noreply.github.com> --- monai/transforms/utility/array.py | 3 ++- monai/transforms/utility/dictionary.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index fad82cff9d..ecc329a7fe 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -275,9 +275,10 @@ def __call__(self, img: Union[np.ndarray, torch.Tensor]) -> PILImage.Image: """ Apply the transform to `img` and make it contiguous. """ + if isinstance(img, PILImage.Image): + return img if isinstance(img, torch.Tensor): img = img.detach().cpu().numpy() - img = np.ascontiguousarray(img) return PILImage.fromarray(img) diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index ac1871bb72..48a5c847c6 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -45,10 +45,12 @@ ToTensor, ) from monai.transforms.utils import extreme_points_to_image, get_extreme_points -from monai.utils import ensure_tuple, ensure_tuple_rep +from monai.utils import ensure_tuple, ensure_tuple_rep, optional_import if TYPE_CHECKING: from PIL import Image as PILImage +else: + PILImage, _ = optional_import("PIL.Image") __all__ = [ "Identityd", From 396547b76b04dce848cd217e5a39257e2b7f28cd Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+behxyz@users.noreply.github.com> Date: Thu, 25 Feb 2021 09:27:38 -0500 Subject: [PATCH 07/12] Add unittests for ToPIL and ToPILD Signed-off-by: Behrooz <3968947+behxyz@users.noreply.github.com> --- tests/test_to_pil.py | 50 ++++++++++++++++++++++++++++++++++++++++++ tests/test_to_pild.py | 51 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 tests/test_to_pil.py create mode 100644 tests/test_to_pild.py diff --git a/tests/test_to_pil.py b/tests/test_to_pil.py new file mode 100644 index 0000000000..2ffb29557a --- /dev/null +++ b/tests/test_to_pil.py @@ -0,0 +1,50 @@ +# Copyright 2020 - 2021 MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import numpy as np +import torch +import PIL.Image as PILImage + +from parameterized import parameterized +from monai.transforms import ToPIL + +TEST_CASE_ARRAY_1 = [np.array([[1.0, 2.0], [3.0, 4.0]])] +TEST_CASE_TENSOR_1 = [torch.tensor([[1.0, 2.0], [3.0, 4.0]])] + + +class TestToPIL(unittest.TestCase): + @parameterized.expand([TEST_CASE_ARRAY_1]) + def test_numpy_input(self, test_data): + self.assertTrue(isinstance(test_data, np.ndarray)) + result = ToPIL()(test_data) + self.assertTrue(isinstance(result, PILImage.Image)) + np.testing.assert_allclose(np.array(result), test_data) + + @parameterized.expand([TEST_CASE_TENSOR_1]) + def test_tensor_input(self, test_data): + self.assertTrue(isinstance(test_data, torch.Tensor)) + result = ToPIL()(test_data) + self.assertTrue(isinstance(result, PILImage.Image)) + np.testing.assert_allclose(np.array(result), test_data.numpy()) + + @parameterized.expand([TEST_CASE_ARRAY_1]) + def test_pil_input(self, test_data): + test_data_pil = PILImage.fromarray(test_data) + self.assertTrue(isinstance(test_data_pil, PILImage.Image)) + result = ToPIL()(test_data_pil) + self.assertTrue(isinstance(result, PILImage.Image)) + np.testing.assert_allclose(np.array(result), test_data) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_to_pild.py b/tests/test_to_pild.py new file mode 100644 index 0000000000..6fe47f1a95 --- /dev/null +++ b/tests/test_to_pild.py @@ -0,0 +1,51 @@ +# Copyright 2020 - 2021 MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import numpy as np +import torch +import PIL.Image as PILImage + +from parameterized import parameterized +from monai.transforms import ToPILD, ToNumpyd + +TEST_CASE_ARRAY_1 = [{"keys": "image"}, {"image": np.array([[1.0, 2.0], [3.0, 4.0]])}] +TEST_CASE__TENSOR_1 = [{"keys": "image"}, {"image": torch.tensor([[1.0, 2.0], [3.0, 4.0]])}] + + +class TestToPIL(unittest.TestCase): + @parameterized.expand([TEST_CASE_ARRAY_1]) + def test_numpy_input(self, input_param, test_data): + self.assertTrue(isinstance(test_data[input_param['keys']], np.ndarray)) + result = ToPILD(**input_param)(test_data)[input_param['keys']] + self.assertTrue(isinstance(result, PILImage.Image)) + np.testing.assert_allclose(np.array(result), test_data[input_param['keys']]) + + @parameterized.expand([TEST_CASE__TENSOR_1]) + def test_tensor_input(self, input_param, test_data): + self.assertTrue(isinstance(test_data[input_param['keys']], torch.Tensor)) + result = ToPILD(**input_param)(test_data)[input_param['keys']] + self.assertTrue(isinstance(result, PILImage.Image)) + np.testing.assert_allclose(np.array(result), test_data[input_param['keys']].numpy()) + + @parameterized.expand([TEST_CASE_ARRAY_1]) + def test_pil_input(self, input_param, test_data): + input_array = test_data[input_param['keys']] + test_data[input_param['keys']] = PILImage.fromarray(input_array) + self.assertTrue(isinstance(test_data[input_param['keys']], PILImage.Image)) + result = ToPILD(**input_param)(test_data)[input_param['keys']] + self.assertTrue(isinstance(result, PILImage.Image)) + np.testing.assert_allclose(np.array(result), test_data[input_param['keys']]) + + +if __name__ == "__main__": + unittest.main() From cfb5519faa5e8ebf461889bb18b51471a940a7ec Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+behxyz@users.noreply.github.com> Date: Thu, 25 Feb 2021 10:09:05 -0500 Subject: [PATCH 08/12] Fix formatting Signed-off-by: Behrooz <3968947+behxyz@users.noreply.github.com> --- monai/transforms/__init__.py | 2 +- tests/test_to_pild.py | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index cd6a78ad09..6b85d5e983 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -251,9 +251,9 @@ SplitChannel, SqueezeDim, ToNumpy, + ToPIL, TorchVision, ToTensor, - ToPIL, Transpose, ) from .utility.dictionary import ( diff --git a/tests/test_to_pild.py b/tests/test_to_pild.py index 6fe47f1a95..bb4f2e8a71 100644 --- a/tests/test_to_pild.py +++ b/tests/test_to_pild.py @@ -16,7 +16,7 @@ import PIL.Image as PILImage from parameterized import parameterized -from monai.transforms import ToPILD, ToNumpyd +from monai.transforms import ToPILD TEST_CASE_ARRAY_1 = [{"keys": "image"}, {"image": np.array([[1.0, 2.0], [3.0, 4.0]])}] TEST_CASE__TENSOR_1 = [{"keys": "image"}, {"image": torch.tensor([[1.0, 2.0], [3.0, 4.0]])}] @@ -25,26 +25,26 @@ class TestToPIL(unittest.TestCase): @parameterized.expand([TEST_CASE_ARRAY_1]) def test_numpy_input(self, input_param, test_data): - self.assertTrue(isinstance(test_data[input_param['keys']], np.ndarray)) - result = ToPILD(**input_param)(test_data)[input_param['keys']] + self.assertTrue(isinstance(test_data[input_param["keys"]], np.ndarray)) + result = ToPILD(**input_param)(test_data)[input_param["keys"]] self.assertTrue(isinstance(result, PILImage.Image)) - np.testing.assert_allclose(np.array(result), test_data[input_param['keys']]) + np.testing.assert_allclose(np.array(result), test_data[input_param["keys"]]) @parameterized.expand([TEST_CASE__TENSOR_1]) def test_tensor_input(self, input_param, test_data): - self.assertTrue(isinstance(test_data[input_param['keys']], torch.Tensor)) - result = ToPILD(**input_param)(test_data)[input_param['keys']] + self.assertTrue(isinstance(test_data[input_param["keys"]], torch.Tensor)) + result = ToPILD(**input_param)(test_data)[input_param["keys"]] self.assertTrue(isinstance(result, PILImage.Image)) - np.testing.assert_allclose(np.array(result), test_data[input_param['keys']].numpy()) + np.testing.assert_allclose(np.array(result), test_data[input_param["keys"]].numpy()) @parameterized.expand([TEST_CASE_ARRAY_1]) def test_pil_input(self, input_param, test_data): - input_array = test_data[input_param['keys']] - test_data[input_param['keys']] = PILImage.fromarray(input_array) - self.assertTrue(isinstance(test_data[input_param['keys']], PILImage.Image)) - result = ToPILD(**input_param)(test_data)[input_param['keys']] + input_array = test_data[input_param["keys"]] + test_data[input_param["keys"]] = PILImage.fromarray(input_array) + self.assertTrue(isinstance(test_data[input_param["keys"]], PILImage.Image)) + result = ToPILD(**input_param)(test_data)[input_param["keys"]] self.assertTrue(isinstance(result, PILImage.Image)) - np.testing.assert_allclose(np.array(result), test_data[input_param['keys']]) + np.testing.assert_allclose(np.array(result), test_data[input_param["keys"]]) if __name__ == "__main__": From 96bcfdc10f9b9365133e69b8d1fc9342c4b5c1bb Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+behxyz@users.noreply.github.com> Date: Thu, 25 Feb 2021 10:34:03 -0500 Subject: [PATCH 09/12] Fix formatting Signed-off-by: Behrooz <3968947+behxyz@users.noreply.github.com> --- tests/test_to_pil.py | 4 ++-- tests/test_to_pild.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_to_pil.py b/tests/test_to_pil.py index 2ffb29557a..9c6e6a2dcb 100644 --- a/tests/test_to_pil.py +++ b/tests/test_to_pil.py @@ -12,10 +12,10 @@ import unittest import numpy as np -import torch import PIL.Image as PILImage - +import torch from parameterized import parameterized + from monai.transforms import ToPIL TEST_CASE_ARRAY_1 = [np.array([[1.0, 2.0], [3.0, 4.0]])] diff --git a/tests/test_to_pild.py b/tests/test_to_pild.py index bb4f2e8a71..49a62c48d8 100644 --- a/tests/test_to_pild.py +++ b/tests/test_to_pild.py @@ -12,10 +12,10 @@ import unittest import numpy as np -import torch import PIL.Image as PILImage - +import torch from parameterized import parameterized + from monai.transforms import ToPILD TEST_CASE_ARRAY_1 = [{"keys": "image"}, {"image": np.array([[1.0, 2.0], [3.0, 4.0]])}] From 85704538d971daddc40a4eda86c7a6278c30196a Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+behxyz@users.noreply.github.com> Date: Thu, 25 Feb 2021 10:37:17 -0500 Subject: [PATCH 10/12] Add PILImage.Image as the input for ToNumpy and ToTensor Signed-off-by: Behrooz <3968947+behxyz@users.noreply.github.com> --- monai/transforms/utility/array.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index ecc329a7fe..4795d24c6c 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -243,7 +243,7 @@ class ToTensor(Transform): Converts the input image to a tensor without applying any other transformations. """ - def __call__(self, img: Union[np.ndarray, torch.Tensor]) -> torch.Tensor: + def __call__(self, img: Union[np.ndarray, torch.Tensor, PILImage.Image]) -> torch.Tensor: """ Apply the transform to `img` and make it contiguous. """ @@ -257,7 +257,7 @@ class ToNumpy(Transform): Converts the input data to numpy array, can support list or tuple of numbers and PyTorch Tensor. """ - def __call__(self, img: Union[List, Tuple, np.ndarray, torch.Tensor]) -> np.ndarray: + def __call__(self, img: Union[List, Tuple, np.ndarray, torch.Tensor, PILImage.Image]) -> np.ndarray: """ Apply the transform to `img` and make it contiguous. """ @@ -271,7 +271,7 @@ class ToPIL(Transform): Converts the input image (in the form of NumPy array or PyTorch Tensor) to PIL image """ - def __call__(self, img: Union[np.ndarray, torch.Tensor]) -> PILImage.Image: + def __call__(self, img: Union[np.ndarray, torch.Tensor, PILImage.Image]) -> PILImage.Image: """ Apply the transform to `img` and make it contiguous. """ From 302284a7525315bae827837ce5ea07307048821b Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+behxyz@users.noreply.github.com> Date: Thu, 25 Feb 2021 17:03:30 -0500 Subject: [PATCH 11/12] Fix type checking issue for PIL.Image.Image Signed-off-by: Behrooz <3968947+behxyz@users.noreply.github.com> --- monai/transforms/utility/array.py | 18 ++++++++------ monai/transforms/utility/dictionary.py | 18 ++++++++------ tests/test_to_pil.py | 26 +++++++++++++++----- tests/test_to_pild.py | 34 ++++++++++++++++++-------- 4 files changed, 65 insertions(+), 31 deletions(-) diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index 4795d24c6c..74f2f82ad5 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -26,9 +26,13 @@ from monai.utils import ensure_tuple, min_version, optional_import if TYPE_CHECKING: - from PIL import Image as PILImage + from PIL.Image import Image as PILImageImage + from PIL.Image import fromarray as PILImage_fromarray + + has_pil = True else: - PILImage, _ = optional_import("PIL.Image") + PILImage_fromarray, has_pil = optional_import("PIL.Image", name="fromarray") + PILImageImage, _ = optional_import("PIL.Image", name="Image") __all__ = [ "Identity", @@ -243,7 +247,7 @@ class ToTensor(Transform): Converts the input image to a tensor without applying any other transformations. """ - def __call__(self, img: Union[np.ndarray, torch.Tensor, PILImage.Image]) -> torch.Tensor: + def __call__(self, img: Union[np.ndarray, torch.Tensor, PILImageImage]) -> torch.Tensor: """ Apply the transform to `img` and make it contiguous. """ @@ -257,7 +261,7 @@ class ToNumpy(Transform): Converts the input data to numpy array, can support list or tuple of numbers and PyTorch Tensor. """ - def __call__(self, img: Union[List, Tuple, np.ndarray, torch.Tensor, PILImage.Image]) -> np.ndarray: + def __call__(self, img: Union[List, Tuple, np.ndarray, torch.Tensor, PILImageImage]) -> np.ndarray: """ Apply the transform to `img` and make it contiguous. """ @@ -271,15 +275,15 @@ class ToPIL(Transform): Converts the input image (in the form of NumPy array or PyTorch Tensor) to PIL image """ - def __call__(self, img: Union[np.ndarray, torch.Tensor, PILImage.Image]) -> PILImage.Image: + def __call__(self, img: Union[np.ndarray, torch.Tensor, PILImageImage]) -> PILImageImage: """ Apply the transform to `img` and make it contiguous. """ - if isinstance(img, PILImage.Image): + if isinstance(img, PILImageImage): return img if isinstance(img, torch.Tensor): img = img.detach().cpu().numpy() - return PILImage.fromarray(img) + return PILImage_fromarray(img) class Transpose(Transform): diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index 48a5c847c6..ed1c61f665 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -48,9 +48,11 @@ from monai.utils import ensure_tuple, ensure_tuple_rep, optional_import if TYPE_CHECKING: - from PIL import Image as PILImage + from PIL.Image import Image as PILImageImage + + has_pil = True else: - PILImage, _ = optional_import("PIL.Image") + PILImageImage, has_pil = optional_import("PIL.Image", name="Image") __all__ = [ "Identityd", @@ -329,8 +331,8 @@ def __init__(self, keys: KeysCollection) -> None: self.converter = ToTensor() def __call__( - self, data: Mapping[Hashable, Union[np.ndarray, torch.Tensor, PILImage.Image]] - ) -> Dict[Hashable, Union[np.ndarray, torch.Tensor, PILImage.Image]]: + self, data: Mapping[Hashable, Union[np.ndarray, torch.Tensor, PILImageImage]] + ) -> Dict[Hashable, Union[np.ndarray, torch.Tensor, PILImageImage]]: d = dict(data) for key in self.keys: d[key] = self.converter(d[key]) @@ -352,8 +354,8 @@ def __init__(self, keys: KeysCollection) -> None: self.converter = ToNumpy() def __call__( - self, data: Mapping[Hashable, Union[np.ndarray, torch.Tensor, PILImage.Image]] - ) -> Dict[Hashable, Union[np.ndarray, torch.Tensor, PILImage.Image]]: + self, data: Mapping[Hashable, Union[np.ndarray, torch.Tensor, PILImageImage]] + ) -> Dict[Hashable, Union[np.ndarray, torch.Tensor, PILImageImage]]: d = dict(data) for key in self.keys: d[key] = self.converter(d[key]) @@ -375,8 +377,8 @@ def __init__(self, keys: KeysCollection) -> None: self.converter = ToPIL() def __call__( - self, data: Mapping[Hashable, Union[np.ndarray, torch.Tensor, PILImage.Image]] - ) -> Dict[Hashable, Union[np.ndarray, torch.Tensor, PILImage.Image]]: + self, data: Mapping[Hashable, Union[np.ndarray, torch.Tensor, PILImageImage]] + ) -> Dict[Hashable, Union[np.ndarray, torch.Tensor, PILImageImage]]: d = dict(data) for key in self.keys: d[key] = self.converter(d[key]) diff --git a/tests/test_to_pil.py b/tests/test_to_pil.py index 9c6e6a2dcb..7ceb2170d7 100644 --- a/tests/test_to_pil.py +++ b/tests/test_to_pil.py @@ -10,13 +10,24 @@ # limitations under the License. import unittest +from typing import TYPE_CHECKING +from unittest import skipUnless import numpy as np -import PIL.Image as PILImage import torch from parameterized import parameterized from monai.transforms import ToPIL +from monai.utils import optional_import + +if TYPE_CHECKING: + from PIL.Image import Image as PILImageImage + from PIL.Image import fromarray as PILImage_fromarray + + has_pil = True +else: + PILImage_fromarray, has_pil = optional_import("PIL.Image", name="fromarray") + PILImageImage, _ = optional_import("PIL.Image", name="Image") TEST_CASE_ARRAY_1 = [np.array([[1.0, 2.0], [3.0, 4.0]])] TEST_CASE_TENSOR_1 = [torch.tensor([[1.0, 2.0], [3.0, 4.0]])] @@ -24,25 +35,28 @@ class TestToPIL(unittest.TestCase): @parameterized.expand([TEST_CASE_ARRAY_1]) + @skipUnless(has_pil, "Requires `pillow` package.") def test_numpy_input(self, test_data): self.assertTrue(isinstance(test_data, np.ndarray)) result = ToPIL()(test_data) - self.assertTrue(isinstance(result, PILImage.Image)) + self.assertTrue(isinstance(result, PILImageImage)) np.testing.assert_allclose(np.array(result), test_data) @parameterized.expand([TEST_CASE_TENSOR_1]) + @skipUnless(has_pil, "Requires `pillow` package.") def test_tensor_input(self, test_data): self.assertTrue(isinstance(test_data, torch.Tensor)) result = ToPIL()(test_data) - self.assertTrue(isinstance(result, PILImage.Image)) + self.assertTrue(isinstance(result, PILImageImage)) np.testing.assert_allclose(np.array(result), test_data.numpy()) @parameterized.expand([TEST_CASE_ARRAY_1]) + @skipUnless(has_pil, "Requires `pillow` package.") def test_pil_input(self, test_data): - test_data_pil = PILImage.fromarray(test_data) - self.assertTrue(isinstance(test_data_pil, PILImage.Image)) + test_data_pil = PILImage_fromarray(test_data) + self.assertTrue(isinstance(test_data_pil, PILImageImage)) result = ToPIL()(test_data_pil) - self.assertTrue(isinstance(result, PILImage.Image)) + self.assertTrue(isinstance(result, PILImageImage)) np.testing.assert_allclose(np.array(result), test_data) diff --git a/tests/test_to_pild.py b/tests/test_to_pild.py index 49a62c48d8..7ddac93b47 100644 --- a/tests/test_to_pild.py +++ b/tests/test_to_pild.py @@ -10,13 +10,24 @@ # limitations under the License. import unittest +from typing import TYPE_CHECKING +from unittest import skipUnless import numpy as np -import PIL.Image as PILImage import torch from parameterized import parameterized -from monai.transforms import ToPILD +from monai.transforms import ToPILd +from monai.utils import optional_import + +if TYPE_CHECKING: + from PIL.Image import Image as PILImageImage + from PIL.Image import fromarray as PILImage_fromarray + + has_pil = True +else: + PILImage_fromarray, has_pil = optional_import("PIL.Image", name="fromarray") + PILImageImage, _ = optional_import("PIL.Image", name="Image") TEST_CASE_ARRAY_1 = [{"keys": "image"}, {"image": np.array([[1.0, 2.0], [3.0, 4.0]])}] TEST_CASE__TENSOR_1 = [{"keys": "image"}, {"image": torch.tensor([[1.0, 2.0], [3.0, 4.0]])}] @@ -24,26 +35,29 @@ class TestToPIL(unittest.TestCase): @parameterized.expand([TEST_CASE_ARRAY_1]) + @skipUnless(has_pil, "Requires `pillow` package.") def test_numpy_input(self, input_param, test_data): self.assertTrue(isinstance(test_data[input_param["keys"]], np.ndarray)) - result = ToPILD(**input_param)(test_data)[input_param["keys"]] - self.assertTrue(isinstance(result, PILImage.Image)) + result = ToPILd(**input_param)(test_data)[input_param["keys"]] + self.assertTrue(isinstance(result, PILImageImage)) np.testing.assert_allclose(np.array(result), test_data[input_param["keys"]]) @parameterized.expand([TEST_CASE__TENSOR_1]) + @skipUnless(has_pil, "Requires `pillow` package.") def test_tensor_input(self, input_param, test_data): self.assertTrue(isinstance(test_data[input_param["keys"]], torch.Tensor)) - result = ToPILD(**input_param)(test_data)[input_param["keys"]] - self.assertTrue(isinstance(result, PILImage.Image)) + result = ToPILd(**input_param)(test_data)[input_param["keys"]] + self.assertTrue(isinstance(result, PILImageImage)) np.testing.assert_allclose(np.array(result), test_data[input_param["keys"]].numpy()) @parameterized.expand([TEST_CASE_ARRAY_1]) + @skipUnless(has_pil, "Requires `pillow` package.") def test_pil_input(self, input_param, test_data): input_array = test_data[input_param["keys"]] - test_data[input_param["keys"]] = PILImage.fromarray(input_array) - self.assertTrue(isinstance(test_data[input_param["keys"]], PILImage.Image)) - result = ToPILD(**input_param)(test_data)[input_param["keys"]] - self.assertTrue(isinstance(result, PILImage.Image)) + test_data[input_param["keys"]] = PILImage_fromarray(input_array) + self.assertTrue(isinstance(test_data[input_param["keys"]], PILImageImage)) + result = ToPILd(**input_param)(test_data)[input_param["keys"]] + self.assertTrue(isinstance(result, PILImageImage)) np.testing.assert_allclose(np.array(result), test_data[input_param["keys"]]) From 32c9f1a1895eb9298cdd4343523e2b5578495d0f Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+behxyz@users.noreply.github.com> Date: Thu, 25 Feb 2021 17:12:03 -0500 Subject: [PATCH 12/12] Change PILImage_fromarray to lower case Signed-off-by: Behrooz <3968947+behxyz@users.noreply.github.com> --- monai/transforms/utility/array.py | 8 ++++---- tests/test_to_pil.py | 6 +++--- tests/test_to_pild.py | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index 74f2f82ad5..077a126162 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -27,12 +27,12 @@ if TYPE_CHECKING: from PIL.Image import Image as PILImageImage - from PIL.Image import fromarray as PILImage_fromarray + from PIL.Image import fromarray as pil_image_fromarray has_pil = True else: - PILImage_fromarray, has_pil = optional_import("PIL.Image", name="fromarray") - PILImageImage, _ = optional_import("PIL.Image", name="Image") + PILImageImage, has_pil = optional_import("PIL.Image", name="Image") + pil_image_fromarray, _ = optional_import("PIL.Image", name="fromarray") __all__ = [ "Identity", @@ -283,7 +283,7 @@ def __call__(self, img: Union[np.ndarray, torch.Tensor, PILImageImage]) -> PILIm return img if isinstance(img, torch.Tensor): img = img.detach().cpu().numpy() - return PILImage_fromarray(img) + return pil_image_fromarray(img) class Transpose(Transform): diff --git a/tests/test_to_pil.py b/tests/test_to_pil.py index 7ceb2170d7..ec63750ce4 100644 --- a/tests/test_to_pil.py +++ b/tests/test_to_pil.py @@ -22,11 +22,11 @@ if TYPE_CHECKING: from PIL.Image import Image as PILImageImage - from PIL.Image import fromarray as PILImage_fromarray + from PIL.Image import fromarray as pil_image_fromarray has_pil = True else: - PILImage_fromarray, has_pil = optional_import("PIL.Image", name="fromarray") + pil_image_fromarray, has_pil = optional_import("PIL.Image", name="fromarray") PILImageImage, _ = optional_import("PIL.Image", name="Image") TEST_CASE_ARRAY_1 = [np.array([[1.0, 2.0], [3.0, 4.0]])] @@ -53,7 +53,7 @@ def test_tensor_input(self, test_data): @parameterized.expand([TEST_CASE_ARRAY_1]) @skipUnless(has_pil, "Requires `pillow` package.") def test_pil_input(self, test_data): - test_data_pil = PILImage_fromarray(test_data) + test_data_pil = pil_image_fromarray(test_data) self.assertTrue(isinstance(test_data_pil, PILImageImage)) result = ToPIL()(test_data_pil) self.assertTrue(isinstance(result, PILImageImage)) diff --git a/tests/test_to_pild.py b/tests/test_to_pild.py index 7ddac93b47..43778022ee 100644 --- a/tests/test_to_pild.py +++ b/tests/test_to_pild.py @@ -22,11 +22,11 @@ if TYPE_CHECKING: from PIL.Image import Image as PILImageImage - from PIL.Image import fromarray as PILImage_fromarray + from PIL.Image import fromarray as pil_image_fromarray has_pil = True else: - PILImage_fromarray, has_pil = optional_import("PIL.Image", name="fromarray") + pil_image_fromarray, has_pil = optional_import("PIL.Image", name="fromarray") PILImageImage, _ = optional_import("PIL.Image", name="Image") TEST_CASE_ARRAY_1 = [{"keys": "image"}, {"image": np.array([[1.0, 2.0], [3.0, 4.0]])}] @@ -54,7 +54,7 @@ def test_tensor_input(self, input_param, test_data): @skipUnless(has_pil, "Requires `pillow` package.") def test_pil_input(self, input_param, test_data): input_array = test_data[input_param["keys"]] - test_data[input_param["keys"]] = PILImage_fromarray(input_array) + test_data[input_param["keys"]] = pil_image_fromarray(input_array) self.assertTrue(isinstance(test_data[input_param["keys"]], PILImageImage)) result = ToPILd(**input_param)(test_data)[input_param["keys"]] self.assertTrue(isinstance(result, PILImageImage))