From 3503cc2c00ddd63bdbaf8ee73041239f4612ffc3 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 21 Sep 2022 14:15:14 +0000 Subject: [PATCH 01/25] Update SobelGradients to include gradient direction Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/post/array.py | 56 ++++++++++++++++++++++++++++++---- monai/utils/__init__.py | 1 + monai/utils/enums.py | 10 ++++++ 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/monai/transforms/post/array.py b/monai/transforms/post/array.py index 2ecf6b6566..0baf0339c1 100644 --- a/monai/transforms/post/array.py +++ b/monai/transforms/post/array.py @@ -13,6 +13,7 @@ https://github.com/Project-MONAI/MONAI/wiki/MONAI_Design """ +from multiprocessing.sharedctypes import Value import warnings from typing import Callable, Iterable, Optional, Sequence, Union @@ -36,6 +37,7 @@ from monai.transforms.utils_pytorch_numpy_unification import unravel_index from monai.utils import TransformBackends, convert_data_type, convert_to_tensor, ensure_tuple, look_up_option from monai.utils.type_conversion import convert_to_dst_type +from monai.utils.enums import Direction __all__ = [ "Activations", @@ -810,12 +812,13 @@ def __call__(self, data): class SobelGradients(Transform): - """Calculate Sobel horizontal and vertical gradients + """Calculate Sobel horizontal and vertical gradients of a binary image. Args: kernel_size: the size of the Sobel kernel. Defaults to 3. padding: the padding for the convolution to apply the kernel. Defaults to `"same"`. dtype: kernel data type (torch.dtype). Defaults to `torch.float32`. + direction: the gradient direction to be calculated. "horizontal" or "vertical" device: the device to create the kernel on. Defaults to `"cpu"`. """ @@ -827,12 +830,35 @@ def __init__( kernel_size: int = 3, padding: Union[int, str] = "same", dtype: torch.dtype = torch.float32, + direction: Optional[str] = None, device: Union[torch.device, int, str] = "cpu", ) -> None: super().__init__() self.kernel: torch.Tensor = self._get_kernel(kernel_size, dtype, device) self.padding = padding + if direction is None: + self.direction = [Direction.HORIZONTAL, Direction.VERTICAL] + elif isinstance(direction, str): + if direction == Direction.HORIZONTAL: + self.direction = [Direction.HORIZONTAL] + elif direction == Direction.VERTICAL: + self.direction = [Direction.VERTICAL] + else: + raise ValueError( + f"`direction` should be either {Direction.HORIZONTAL.value} or {Direction.VERTICAL.value}" + f"but {direction} is given." + ) + elif isinstance(direction, list): + for d in direction: + if d not in [Direction.HORIZONTAL, Direction.VERTICAL]: + raise ValueError( + f"`direction` should be either {Direction.HORIZONTAL.value} or {Direction.VERTICAL.value}" + f"but {direction} is given." + ) + else: + raise ValueError(f"`direction` should be either str or list but {type(direction)} was given.") + def _get_kernel(self, size, dtype, device) -> torch.Tensor: if size % 2 == 0: raise ValueError(f"Sobel kernel size should be an odd number. {size} was given.") @@ -850,11 +876,29 @@ def _get_kernel(self, size, dtype, device) -> torch.Tensor: def __call__(self, image: NdarrayOrTensor) -> torch.Tensor: image_tensor = convert_to_tensor(image, track_meta=get_track_meta()) + # Get the Sobel kernels kernel_v = self.kernel.to(image_tensor.device) kernel_h = kernel_v.T - image_tensor = image_tensor.unsqueeze(0) # adds a batch dim - grad_v = apply_filter(image_tensor, kernel_v, padding=self.padding) - grad_h = apply_filter(image_tensor, kernel_h, padding=self.padding) - grad = torch.cat([grad_h, grad_v], dim=1) - grad, *_ = convert_to_dst_type(grad.squeeze(0), image_tensor) + + # Add batch dimension to be able to use apply_filter + image_tensor = image_tensor.unsqueeze(0) + + # Calculate gradients + grad_h = None + grad_v = None + if Direction.HORIZONTAL in self.direction: + grad_h = apply_filter(image_tensor, kernel_h, padding=self.padding) + if Direction.VERTICAL in self.direction: + grad_v = apply_filter(image_tensor, kernel_v, padding=self.padding) + + # Check gradient direction + if grad_h is None: + grad = grad_v + elif grad_v is None: + grad = grad_h + else: + grad = torch.cat([grad_h, grad_v], dim=1) + + # Remove batch dimension and convert the gradient type to be the same as input image + grad = convert_to_dst_type(grad.squeeze(0), image_tensor)[0] return grad diff --git a/monai/utils/__init__.py b/monai/utils/__init__.py index 2428da88a2..9deb4f5311 100644 --- a/monai/utils/__init__.py +++ b/monai/utils/__init__.py @@ -22,6 +22,7 @@ ColorOrder, CommonKeys, DiceCEReduction, + Direction, EngineStatsKeys, FastMRIKeys, ForwardMode, diff --git a/monai/utils/enums.py b/monai/utils/enums.py index d69c184dae..1e93522086 100644 --- a/monai/utils/enums.py +++ b/monai/utils/enums.py @@ -52,6 +52,7 @@ "ImageStatsKeys", "LabelStatsKeys", "AlgoEnsembleKeys", + "Direction", ] @@ -587,3 +588,12 @@ class AlgoEnsembleKeys(StrEnum): ID = "identifier" ALGO = "infer_algo" SCORE = "best_metric" + + +class Direction(StrEnum): + """ + Default keys for axis directions of an image + """ + + HORIZONTAL = "horizontal" + VERTICAL = "vertical" From c3dcb0db3035a55df7cb265d755b22f1b80acdaf Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 21 Sep 2022 14:51:03 +0000 Subject: [PATCH 02/25] Add more checking to raise errors Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/post/array.py | 48 ++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/monai/transforms/post/array.py b/monai/transforms/post/array.py index 0baf0339c1..57eafec70f 100644 --- a/monai/transforms/post/array.py +++ b/monai/transforms/post/array.py @@ -13,7 +13,6 @@ https://github.com/Project-MONAI/MONAI/wiki/MONAI_Design """ -from multiprocessing.sharedctypes import Value import warnings from typing import Callable, Iterable, Optional, Sequence, Union @@ -36,8 +35,8 @@ ) from monai.transforms.utils_pytorch_numpy_unification import unravel_index from monai.utils import TransformBackends, convert_data_type, convert_to_tensor, ensure_tuple, look_up_option -from monai.utils.type_conversion import convert_to_dst_type from monai.utils.enums import Direction +from monai.utils.type_conversion import convert_to_dst_type __all__ = [ "Activations", @@ -818,7 +817,8 @@ class SobelGradients(Transform): kernel_size: the size of the Sobel kernel. Defaults to 3. padding: the padding for the convolution to apply the kernel. Defaults to `"same"`. dtype: kernel data type (torch.dtype). Defaults to `torch.float32`. - direction: the gradient direction to be calculated. "horizontal" or "vertical" + direction: the direction in which the gradient to be calculated. It can be string "horizontal" or "vertical", + or list of strings ["horizontal", "vertical"]. By default it calculate the gradient in both directions. device: the device to create the kernel on. Defaults to `"cpu"`. """ @@ -846,20 +846,27 @@ def __init__( self.direction = [Direction.VERTICAL] else: raise ValueError( - f"`direction` should be either {Direction.HORIZONTAL.value} or {Direction.VERTICAL.value}" + f"`direction` should be either {Direction.HORIZONTAL.value} or {Direction.VERTICAL.value} " f"but {direction} is given." ) elif isinstance(direction, list): + self.direction = [] for d in direction: - if d not in [Direction.HORIZONTAL, Direction.VERTICAL]: + if d == Direction.HORIZONTAL: + self.direction.append(Direction.HORIZONTAL) + elif d == Direction.VERTICAL: + self.direction.append(Direction.VERTICAL) + else: raise ValueError( f"`direction` should be either {Direction.HORIZONTAL.value} or {Direction.VERTICAL.value}" - f"but {direction} is given." + f"but {d} is given." ) else: raise ValueError(f"`direction` should be either str or list but {type(direction)} was given.") def _get_kernel(self, size, dtype, device) -> torch.Tensor: + if size < 3: + raise ValueError(f"Sobel kernel size should be at least three. {size} was given.") if size % 2 == 0: raise ValueError(f"Sobel kernel size should be an odd number. {size} was given.") if not dtype.is_floating_point: @@ -876,13 +883,21 @@ def _get_kernel(self, size, dtype, device) -> torch.Tensor: def __call__(self, image: NdarrayOrTensor) -> torch.Tensor: image_tensor = convert_to_tensor(image, track_meta=get_track_meta()) - # Get the Sobel kernels - kernel_v = self.kernel.to(image_tensor.device) - kernel_h = kernel_v.T + + # Check if the image has only one channel + if image_tensor.shape[0] > 1: + raise ValueError( + f"Input image has more than one channel dimension num_channels={image_tensor.shape[0]}. " + "Sobel gradients are intended to be used on intensities so the images should have only one channel." + ) # Add batch dimension to be able to use apply_filter image_tensor = image_tensor.unsqueeze(0) + # Get the Sobel kernels + kernel_v = self.kernel.to(image_tensor.device) + kernel_h = kernel_v.T + # Calculate gradients grad_h = None grad_v = None @@ -891,14 +906,15 @@ def __call__(self, image: NdarrayOrTensor) -> torch.Tensor: if Direction.VERTICAL in self.direction: grad_v = apply_filter(image_tensor, kernel_v, padding=self.padding) - # Check gradient direction - if grad_h is None: - grad = grad_v - elif grad_v is None: - grad = grad_h - else: - grad = torch.cat([grad_h, grad_v], dim=1) + # Check gradient directions + grad_list = [] + if grad_h is not None: + grad_list.append(grad_h) + if grad_v is not None: + grad_list.append(grad_v) + grad = torch.cat(grad_list, dim=1) # Remove batch dimension and convert the gradient type to be the same as input image grad = convert_to_dst_type(grad.squeeze(0), image_tensor)[0] + return grad From 7b26dd5884abe1c3b60c20ff752043c739489c8f Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 21 Sep 2022 14:51:23 +0000 Subject: [PATCH 03/25] Add test cases Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_sobel_gradient.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/tests/test_sobel_gradient.py b/tests/test_sobel_gradient.py index 17f6bffdfb..a579e8ef0a 100644 --- a/tests/test_sobel_gradient.py +++ b/tests/test_sobel_gradient.py @@ -31,6 +31,10 @@ TEST_CASE_0 = [IMAGE, {"kernel_size": 3, "dtype": torch.float32}, OUTPUT_3x3] TEST_CASE_1 = [IMAGE, {"kernel_size": 3, "dtype": torch.float64}, OUTPUT_3x3] +TEST_CASE_2 = [IMAGE, {"kernel_size": 3, "direction": "horizontal", "dtype": torch.float64}, OUTPUT_3x3[0][None, ...]] +TEST_CASE_3 = [IMAGE, {"kernel_size": 3, "direction": "vertical", "dtype": torch.float64}, OUTPUT_3x3[1][None, ...]] +TEST_CASE_4 = [IMAGE, {"kernel_size": 3, "direction": ["vertical"], "dtype": torch.float64}, OUTPUT_3x3[1][None, ...]] +TEST_CASE_5 = [IMAGE, {"kernel_size": 3, "direction": ["horizontal", "vertical"], "dtype": torch.float64}, OUTPUT_3x3] TEST_CASE_KERNEL_0 = [ {"kernel_size": 3, "dtype": torch.float64}, @@ -64,13 +68,20 @@ dtype=torch.float64, ), ] -TEST_CASE_ERROR_0 = [{"kernel_size": 2, "dtype": torch.float32}] +TEST_CASE_ERROR_0 = [{"kernel_size": 1}] # kernel size on one +TEST_CASE_ERROR_1 = [{"kernel_size": 4}] # even kernel size +TEST_CASE_ERROR_2 = [{"direction": 1}] # wrong type direction +TEST_CASE_ERROR_3 = [{"direction": "not_exist_direction"}] # wrong direction +TEST_CASE_ERROR_4 = [{"direction": ["not_exist_direction"]}] # wrong direction in a list +TEST_CASE_ERROR_5 = [{"direction": ["horizontal", "not_exist_direction"]}] # correct and wrong direction in a list + +TEST_CASE_IMAGE_ERROR_0 = [torch.cat([IMAGE, IMAGE], dim=0), {"kernel_size": 3, "dtype": torch.float32}] class SobelGradientTests(unittest.TestCase): backend = None - @parameterized.expand([TEST_CASE_0]) + @parameterized.expand([TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5]) def test_sobel_gradients(self, image, arguments, expected_grad): sobel = SobelGradients(**arguments) grad = sobel(image) @@ -82,11 +93,26 @@ def test_sobel_kernels(self, arguments, expected_kernel): self.assertTrue(sobel.kernel.dtype == expected_kernel.dtype) assert_allclose(sobel.kernel, expected_kernel) - @parameterized.expand([TEST_CASE_ERROR_0]) + @parameterized.expand( + [ + TEST_CASE_ERROR_0, + TEST_CASE_ERROR_1, + TEST_CASE_ERROR_2, + TEST_CASE_ERROR_3, + TEST_CASE_ERROR_4, + TEST_CASE_ERROR_5, + ] + ) def test_sobel_gradients_error(self, arguments): with self.assertRaises(ValueError): SobelGradients(**arguments) + @parameterized.expand([TEST_CASE_IMAGE_ERROR_0]) + def test_sobel_gradients_image_error(self, image, arguments): + sobel = SobelGradients(**arguments) + with self.assertRaises(ValueError): + sobel(image) + if __name__ == "__main__": unittest.main() From 852eba88991cd4d9fd26726e8f6e17f4866ec503 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 21 Sep 2022 14:59:34 +0000 Subject: [PATCH 04/25] Update SobelGradientsd Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/post/array.py | 4 ++-- monai/transforms/post/dictionary.py | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/monai/transforms/post/array.py b/monai/transforms/post/array.py index 57eafec70f..96b3a50ff6 100644 --- a/monai/transforms/post/array.py +++ b/monai/transforms/post/array.py @@ -816,9 +816,9 @@ class SobelGradients(Transform): Args: kernel_size: the size of the Sobel kernel. Defaults to 3. padding: the padding for the convolution to apply the kernel. Defaults to `"same"`. - dtype: kernel data type (torch.dtype). Defaults to `torch.float32`. direction: the direction in which the gradient to be calculated. It can be string "horizontal" or "vertical", or list of strings ["horizontal", "vertical"]. By default it calculate the gradient in both directions. + dtype: kernel data type (torch.dtype). Defaults to `torch.float32`. device: the device to create the kernel on. Defaults to `"cpu"`. """ @@ -829,8 +829,8 @@ def __init__( self, kernel_size: int = 3, padding: Union[int, str] = "same", - dtype: torch.dtype = torch.float32, direction: Optional[str] = None, + dtype: torch.dtype = torch.float32, device: Union[torch.device, int, str] = "cpu", ) -> None: super().__init__() diff --git a/monai/transforms/post/dictionary.py b/monai/transforms/post/dictionary.py index b5439d0c1b..4b095a2b2b 100644 --- a/monai/transforms/post/dictionary.py +++ b/monai/transforms/post/dictionary.py @@ -785,6 +785,8 @@ class SobelGradientsd(MapTransform): keys: keys of the corresponding items to model output. kernel_size: the size of the Sobel kernel. Defaults to 3. padding: the padding for the convolution to apply the kernel. Defaults to `"same"`. + direction: the direction in which the gradient to be calculated. It can be string "horizontal" or "vertical", + or list of strings ["horizontal", "vertical"]. By default it calculate the gradient in both directions. dtype: kernel data type (torch.dtype). Defaults to `torch.float32`. device: the device to create the kernel on. Defaults to `"cpu"`. new_key_prefix: this prefix be prepended to the key to create a new key for the output and keep the value of @@ -800,13 +802,16 @@ def __init__( keys: KeysCollection, kernel_size: int = 3, padding: Union[int, str] = "same", + direction: Optional[str] = None, dtype: torch.dtype = torch.float32, device: Union[torch.device, int, str] = "cpu", new_key_prefix: Optional[str] = None, allow_missing_keys: bool = False, ) -> None: super().__init__(keys, allow_missing_keys) - self.transform = SobelGradients(kernel_size=kernel_size, padding=padding, dtype=dtype, device=device) + self.transform = SobelGradients( + kernel_size=kernel_size, padding=padding, direction=direction, dtype=dtype, device=device + ) self.new_key_prefix = new_key_prefix def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: From a1483dcb518922f23a9ce857b3cd6146b3c87268 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 21 Sep 2022 14:59:50 +0000 Subject: [PATCH 05/25] Add test cases for SobelGradientsD Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_sobel_gradient.py | 2 +- tests/test_sobel_gradientd.py | 55 +++++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/tests/test_sobel_gradient.py b/tests/test_sobel_gradient.py index a579e8ef0a..eb8f4d9bf7 100644 --- a/tests/test_sobel_gradient.py +++ b/tests/test_sobel_gradient.py @@ -68,7 +68,7 @@ dtype=torch.float64, ), ] -TEST_CASE_ERROR_0 = [{"kernel_size": 1}] # kernel size on one +TEST_CASE_ERROR_0 = [{"kernel_size": 1}] # kernel size less than 3 TEST_CASE_ERROR_1 = [{"kernel_size": 4}] # even kernel size TEST_CASE_ERROR_2 = [{"direction": 1}] # wrong type direction TEST_CASE_ERROR_3 = [{"direction": "not_exist_direction"}] # wrong direction diff --git a/tests/test_sobel_gradientd.py b/tests/test_sobel_gradientd.py index b3e04da0bf..bdc441940d 100644 --- a/tests/test_sobel_gradientd.py +++ b/tests/test_sobel_gradientd.py @@ -36,6 +36,26 @@ {"keys": "image", "kernel_size": 3, "dtype": torch.float32, "new_key_prefix": "sobel_"}, {"sobel_image": OUTPUT_3x3}, ] +TEST_CASE_3 = [ + {"image": IMAGE}, + {"keys": "image", "kernel_size": 3, "direction": "horizontal", "dtype": torch.float32}, + {"image": OUTPUT_3x3[0][None, ...]}, +] +TEST_CASE_4 = [ + {"image": IMAGE}, + {"keys": "image", "kernel_size": 3, "direction": "vertical", "dtype": torch.float32}, + {"image": OUTPUT_3x3[1][None, ...]}, +] +TEST_CASE_5 = [ + {"image": IMAGE}, + {"keys": "image", "kernel_size": 3, "direction": ["vertical"], "dtype": torch.float32}, + {"image": OUTPUT_3x3[1][None, ...]}, +] +TEST_CASE_6 = [ + {"image": IMAGE}, + {"keys": "image", "kernel_size": 3, "direction": ["horizontal", "vertical"], "dtype": torch.float32}, + {"image": OUTPUT_3x3}, +] TEST_CASE_KERNEL_0 = [ {"keys": "image", "kernel_size": 3, "dtype": torch.float64}, @@ -69,13 +89,27 @@ dtype=torch.float64, ), ] -TEST_CASE_ERROR_0 = [{"keys": "image", "kernel_size": 2, "dtype": torch.float32}] +TEST_CASE_ERROR_0 = [{"keys": "image", "kernel_size": 1}] # kernel size less than 3 +TEST_CASE_ERROR_1 = [{"keys": "image", "kernel_size": 4}] # even kernel size +TEST_CASE_ERROR_2 = [{"keys": "image", "kernel_size": 2, "direction": 1}] # wrong type direction +TEST_CASE_ERROR_3 = [{"keys": "image", "kernel_size": 2, "direction": "not_exist_direction"}] # wrong direction +TEST_CASE_ERROR_4 = [ + {"keys": "image", "kernel_size": 2, "direction": ["not_exist_direction"]} +] # wrong direction in a list +TEST_CASE_ERROR_5 = [ + {"keys": "image", "kernel_size": 2, "direction": ["horizontal", "not_exist_direction"]} +] # correct and wrong direction in a list + +TEST_CASE_IMAGE_ERROR_0 = [ + {"image": torch.cat([IMAGE, IMAGE], dim=0)}, + {"keys": "image", "kernel_size": 3, "dtype": torch.float32}, +] class SobelGradientTests(unittest.TestCase): backend = None - @parameterized.expand([TEST_CASE_0]) + @parameterized.expand([TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5, TEST_CASE_6]) def test_sobel_gradients(self, image_dict, arguments, expected_grad): sobel = SobelGradientsd(**arguments) grad = sobel(image_dict) @@ -88,11 +122,26 @@ def test_sobel_kernels(self, arguments, expected_kernel): self.assertTrue(sobel.transform.kernel.dtype == expected_kernel.dtype) assert_allclose(sobel.transform.kernel, expected_kernel) - @parameterized.expand([TEST_CASE_ERROR_0]) + @parameterized.expand( + [ + TEST_CASE_ERROR_0, + TEST_CASE_ERROR_1, + TEST_CASE_ERROR_2, + TEST_CASE_ERROR_3, + TEST_CASE_ERROR_4, + TEST_CASE_ERROR_5, + ] + ) def test_sobel_gradients_error(self, arguments): with self.assertRaises(ValueError): SobelGradientsd(**arguments) + @parameterized.expand([TEST_CASE_IMAGE_ERROR_0]) + def test_sobel_gradients_image_error(self, image_dict, arguments): + sobel = SobelGradientsd(**arguments) + with self.assertRaises(ValueError): + sobel(image_dict) + if __name__ == "__main__": unittest.main() From 2b2b28e98696e2a2dec65017bfac1738557ad6e9 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 21 Sep 2022 17:00:18 +0000 Subject: [PATCH 06/25] Update docstring Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/post/array.py | 2 +- monai/transforms/post/dictionary.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monai/transforms/post/array.py b/monai/transforms/post/array.py index 96b3a50ff6..181d0f9836 100644 --- a/monai/transforms/post/array.py +++ b/monai/transforms/post/array.py @@ -811,7 +811,7 @@ def __call__(self, data): class SobelGradients(Transform): - """Calculate Sobel horizontal and vertical gradients of a binary image. + """Calculate Sobel horizontal and vertical gradients of a grayscale image. Args: kernel_size: the size of the Sobel kernel. Defaults to 3. diff --git a/monai/transforms/post/dictionary.py b/monai/transforms/post/dictionary.py index 4b095a2b2b..656c4e3652 100644 --- a/monai/transforms/post/dictionary.py +++ b/monai/transforms/post/dictionary.py @@ -779,7 +779,7 @@ def get_saver(self): class SobelGradientsd(MapTransform): - """Calculate Sobel horizontal and vertical gradients. + """Calculate Sobel horizontal and vertical gradients of a grayscale image. Args: keys: keys of the corresponding items to model output. From 48125e699ec2e59302b0194d8f9474bd8a795aea Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 22 Sep 2022 13:48:24 +0000 Subject: [PATCH 07/25] Address comments Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/post/array.py | 32 ++++++++------------------------ tests/test_sobel_gradient.py | 3 ++- tests/test_sobel_gradientd.py | 9 ++++++++- 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/monai/transforms/post/array.py b/monai/transforms/post/array.py index 181d0f9836..6df50688fa 100644 --- a/monai/transforms/post/array.py +++ b/monai/transforms/post/array.py @@ -817,7 +817,7 @@ class SobelGradients(Transform): kernel_size: the size of the Sobel kernel. Defaults to 3. padding: the padding for the convolution to apply the kernel. Defaults to `"same"`. direction: the direction in which the gradient to be calculated. It can be string "horizontal" or "vertical", - or list of strings ["horizontal", "vertical"]. By default it calculate the gradient in both directions. + or list/tuple of strings ["horizontal", "vertical"]. By default it calculate the gradient in both directions. dtype: kernel data type (torch.dtype). Defaults to `torch.float32`. device: the device to create the kernel on. Defaults to `"cpu"`. @@ -829,7 +829,7 @@ def __init__( self, kernel_size: int = 3, padding: Union[int, str] = "same", - direction: Optional[str] = None, + direction: Optional[Union[str, Sequence]] = None, dtype: torch.dtype = torch.float32, device: Union[torch.device, int, str] = "cpu", ) -> None: @@ -838,31 +838,15 @@ def __init__( self.padding = padding if direction is None: - self.direction = [Direction.HORIZONTAL, Direction.VERTICAL] - elif isinstance(direction, str): - if direction == Direction.HORIZONTAL: - self.direction = [Direction.HORIZONTAL] - elif direction == Direction.VERTICAL: - self.direction = [Direction.VERTICAL] - else: - raise ValueError( - f"`direction` should be either {Direction.HORIZONTAL.value} or {Direction.VERTICAL.value} " - f"but {direction} is given." - ) - elif isinstance(direction, list): - self.direction = [] - for d in direction: - if d == Direction.HORIZONTAL: - self.direction.append(Direction.HORIZONTAL) - elif d == Direction.VERTICAL: - self.direction.append(Direction.VERTICAL) - else: + self.direction = (Direction.HORIZONTAL, Direction.VERTICAL) + else: + self.direction = ensure_tuple(direction) + for d in self.direction: + if d not in (Direction.HORIZONTAL, Direction.VERTICAL): raise ValueError( - f"`direction` should be either {Direction.HORIZONTAL.value} or {Direction.VERTICAL.value}" + f"`direction` should only contain {Direction.HORIZONTAL.value} or {Direction.VERTICAL.value}, " f"but {d} is given." ) - else: - raise ValueError(f"`direction` should be either str or list but {type(direction)} was given.") def _get_kernel(self, size, dtype, device) -> torch.Tensor: if size < 3: diff --git a/tests/test_sobel_gradient.py b/tests/test_sobel_gradient.py index eb8f4d9bf7..440878724b 100644 --- a/tests/test_sobel_gradient.py +++ b/tests/test_sobel_gradient.py @@ -35,6 +35,7 @@ TEST_CASE_3 = [IMAGE, {"kernel_size": 3, "direction": "vertical", "dtype": torch.float64}, OUTPUT_3x3[1][None, ...]] TEST_CASE_4 = [IMAGE, {"kernel_size": 3, "direction": ["vertical"], "dtype": torch.float64}, OUTPUT_3x3[1][None, ...]] TEST_CASE_5 = [IMAGE, {"kernel_size": 3, "direction": ["horizontal", "vertical"], "dtype": torch.float64}, OUTPUT_3x3] +TEST_CASE_6 = [IMAGE, {"kernel_size": 3, "direction": ("horizontal", "vertical"), "dtype": torch.float64}, OUTPUT_3x3] TEST_CASE_KERNEL_0 = [ {"kernel_size": 3, "dtype": torch.float64}, @@ -81,7 +82,7 @@ class SobelGradientTests(unittest.TestCase): backend = None - @parameterized.expand([TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5]) + @parameterized.expand([TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5, TEST_CASE_6]) def test_sobel_gradients(self, image, arguments, expected_grad): sobel = SobelGradients(**arguments) grad = sobel(image) diff --git a/tests/test_sobel_gradientd.py b/tests/test_sobel_gradientd.py index bdc441940d..ae030a6e9b 100644 --- a/tests/test_sobel_gradientd.py +++ b/tests/test_sobel_gradientd.py @@ -56,6 +56,11 @@ {"keys": "image", "kernel_size": 3, "direction": ["horizontal", "vertical"], "dtype": torch.float32}, {"image": OUTPUT_3x3}, ] +TEST_CASE_7 = [ + {"image": IMAGE}, + {"keys": "image", "kernel_size": 3, "direction": ("horizontal", "vertical"), "dtype": torch.float32}, + {"image": OUTPUT_3x3}, +] TEST_CASE_KERNEL_0 = [ {"keys": "image", "kernel_size": 3, "dtype": torch.float64}, @@ -109,7 +114,9 @@ class SobelGradientTests(unittest.TestCase): backend = None - @parameterized.expand([TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5, TEST_CASE_6]) + @parameterized.expand( + [TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5, TEST_CASE_6, TEST_CASE_7] + ) def test_sobel_gradients(self, image_dict, arguments, expected_grad): sobel = SobelGradientsd(**arguments) grad = sobel(image_dict) From 54694b42d34cc568567730ad84dd1b7f60a3e88d Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Fri, 23 Sep 2022 20:39:45 +0000 Subject: [PATCH 08/25] Type checking Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/post/array.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monai/transforms/post/array.py b/monai/transforms/post/array.py index 6df50688fa..6c7c085dd0 100644 --- a/monai/transforms/post/array.py +++ b/monai/transforms/post/array.py @@ -829,7 +829,7 @@ def __init__( self, kernel_size: int = 3, padding: Union[int, str] = "same", - direction: Optional[Union[str, Sequence]] = None, + direction: Optional[Union[str, Sequence[str]]] = None, dtype: torch.dtype = torch.float32, device: Union[torch.device, int, str] = "cpu", ) -> None: @@ -837,6 +837,7 @@ def __init__( self.kernel: torch.Tensor = self._get_kernel(kernel_size, dtype, device) self.padding = padding + self.direction: tuple if direction is None: self.direction = (Direction.HORIZONTAL, Direction.VERTICAL) else: From 0c16ceb7c5c07f575502387e18aae8fdca12e29d Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 26 Oct 2022 17:20:02 -0400 Subject: [PATCH 09/25] Reimplementation of sobel with separable kernels Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/post/array.py | 105 ++++++++++++++++----------------- 1 file changed, 50 insertions(+), 55 deletions(-) diff --git a/monai/transforms/post/array.py b/monai/transforms/post/array.py index 7410b07f68..edcc079bc7 100644 --- a/monai/transforms/post/array.py +++ b/monai/transforms/post/array.py @@ -14,16 +14,17 @@ """ import warnings -from typing import Callable, Iterable, Optional, Sequence, Union +from typing import Callable, Iterable, Optional, Sequence, Tuple, Union import numpy as np import torch +import torch.nn.functional as F from monai.config.type_definitions import NdarrayOrTensor from monai.data.meta_obj import get_track_meta from monai.data.meta_tensor import MetaTensor from monai.networks import one_hot -from monai.networks.layers import GaussianFilter, apply_filter +from monai.networks.layers import GaussianFilter, apply_filter, separable_filtering, spatial_transforms from monai.transforms.inverse import InvertibleTransform from monai.transforms.transform import Transform from monai.transforms.utils import ( @@ -31,11 +32,11 @@ fill_holes, get_largest_connected_component_mask, get_unique_labels, + map_spatial_axes, remove_small_objects, ) from monai.transforms.utils_pytorch_numpy_unification import unravel_index from monai.utils import TransformBackends, convert_data_type, convert_to_tensor, ensure_tuple, look_up_option -from monai.utils.enums import Direction from monai.utils.type_conversion import convert_to_dst_type __all__ = [ @@ -827,8 +828,7 @@ class SobelGradients(Transform): Args: kernel_size: the size of the Sobel kernel. Defaults to 3. padding: the padding for the convolution to apply the kernel. Defaults to `"same"`. - direction: the direction in which the gradient to be calculated. It can be string "horizontal" or "vertical", - or list/tuple of strings ["horizontal", "vertical"]. By default it calculate the gradient in both directions. + axis: the axis on which the gradient to be calculated. By default it calculate the gradient for all axes. dtype: kernel data type (torch.dtype). Defaults to `torch.float32`. device: the device to create the kernel on. Defaults to `"cpu"`. @@ -839,28 +839,17 @@ class SobelGradients(Transform): def __init__( self, kernel_size: int = 3, - padding: Union[int, str] = "same", - direction: Optional[Union[str, Sequence[str]]] = None, + padding: str = "reflect", + spatial_axes: Optional[Union[Sequence[int], int]] = None, dtype: torch.dtype = torch.float32, device: Union[torch.device, int, str] = "cpu", ) -> None: super().__init__() - self.kernel: torch.Tensor = self._get_kernel(kernel_size, dtype, device) self.padding = padding + self.spatial_axes = spatial_axes + self.kernel_diff, self.kernel_smooth = self._get_kernel(kernel_size, dtype, device) - self.direction: tuple - if direction is None: - self.direction = (Direction.HORIZONTAL, Direction.VERTICAL) - else: - self.direction = ensure_tuple(direction) - for d in self.direction: - if d not in (Direction.HORIZONTAL, Direction.VERTICAL): - raise ValueError( - f"`direction` should only contain {Direction.HORIZONTAL.value} or {Direction.VERTICAL.value}, " - f"but {d} is given." - ) - - def _get_kernel(self, size, dtype, device) -> torch.Tensor: + def _get_kernel(self, size, dtype, device) -> Tuple[torch.Tensor, torch.Tensor]: if size < 3: raise ValueError(f"Sobel kernel size should be at least three. {size} was given.") if size % 2 == 0: @@ -868,49 +857,55 @@ def _get_kernel(self, size, dtype, device) -> torch.Tensor: if not dtype.is_floating_point: raise ValueError(f"`dtype` for Sobel kernel should be floating point. {dtype} was given.") - numerator: torch.Tensor = torch.arange( - -size // 2 + 1, size // 2 + 1, dtype=dtype, device=device, requires_grad=False - ).expand(size, size) - denominator = numerator * numerator - denominator = denominator + denominator.T - denominator[:, size // 2] = 1.0 # to avoid division by zero - kernel = numerator / denominator - return kernel + expand_kernel = torch.tensor([[[1, 2, 1]]], dtype=dtype, device=device) + kernel_diff = torch.tensor([[[1, 0, -1]]], dtype=dtype, device=device) + kernel_smooth = torch.tensor([[[1, 2, 1]]], dtype=dtype, device=device) + + # Expand the kernel to larger size than 3 + expand = (size - 3) // 2 + for _ in range(expand): + kernel_diff = F.conv1d(kernel_diff, expand_kernel, padding=2) + kernel_smooth = F.conv1d(kernel_smooth, expand_kernel, padding=2) + + return kernel_diff.squeeze(), kernel_smooth.squeeze() def __call__(self, image: NdarrayOrTensor) -> torch.Tensor: image_tensor = convert_to_tensor(image, track_meta=get_track_meta()) - # Check if the image has only one channel - if image_tensor.shape[0] > 1: - raise ValueError( - f"Input image has more than one channel dimension num_channels={image_tensor.shape[0]}. " - "Sobel gradients are intended to be used on intensities so the images should have only one channel." - ) + # Check/set spatial axes + n_spatial_dims = image_tensor.ndim - 1 # excluding the channel dimension + valid_spatial_axes = list(range(n_spatial_dims)) + list(range(-n_spatial_dims, 0)) - # Add batch dimension to be able to use apply_filter + # Check gradient axes to be valid + if self.spatial_axes is None: + spatial_axes = list(range(n_spatial_dims)) + else: + invalid_axis = set(ensure_tuple(self.spatial_axes)) - set(valid_spatial_axes) + if invalid_axis: + raise ValueError( + f"The provide axes to calculate gradient is not valid: {invalid_axis}. " + f"The image has {n_spatial_dims} spatial dimensions so it should be: {valid_spatial_axes}." + ) + spatial_axes = [ax % n_spatial_dims if ax < 0 else ax for ax in ensure_tuple(self.spatial_axes)] + + # Add batch dimension for separable_filtering image_tensor = image_tensor.unsqueeze(0) # Get the Sobel kernels - kernel_v = self.kernel.to(image_tensor.device) - kernel_h = kernel_v.T - - # Calculate gradients - grad_h = None - grad_v = None - if Direction.HORIZONTAL in self.direction: - grad_h = apply_filter(image_tensor, kernel_h, padding=self.padding) - if Direction.VERTICAL in self.direction: - grad_v = apply_filter(image_tensor, kernel_v, padding=self.padding) - - # Check gradient directions + kernel_diff = self.kernel_diff.to(image_tensor.device) + kernel_smooth = self.kernel_smooth.to(image_tensor.device) + + # Calculate gradient grad_list = [] - if grad_h is not None: - grad_list.append(grad_h) - if grad_v is not None: - grad_list.append(grad_v) - grad = torch.cat(grad_list, dim=1) + for ax in spatial_axes: + kernels = [kernel_smooth] * n_spatial_dims + kernels[ax - 1] = kernel_diff + grad = separable_filtering(image_tensor, kernels, mode=self.padding) + grad_list.append(grad) + + grads = torch.cat(grad_list, dim=1) # Remove batch dimension and convert the gradient type to be the same as input image - grad = convert_to_dst_type(grad.squeeze(0), image_tensor)[0] + grads = convert_to_dst_type(grads.squeeze(0), image_tensor)[0] - return grad + return grads From 5bfa852b2bc113a1b168c4857e50385960231831 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 26 Oct 2022 17:20:27 -0400 Subject: [PATCH 10/25] Remove Direction from init Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/utils/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monai/utils/__init__.py b/monai/utils/__init__.py index 81878081fb..c5419cb9af 100644 --- a/monai/utils/__init__.py +++ b/monai/utils/__init__.py @@ -22,7 +22,6 @@ ColorOrder, CommonKeys, DiceCEReduction, - Direction, EngineStatsKeys, FastMRIKeys, ForwardMode, From a1cf946461b829d21829eea2f409e272dc8df409 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 26 Oct 2022 17:20:49 -0400 Subject: [PATCH 11/25] Update unittests for sobel Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/post/dictionary.py | 2 +- tests/test_sobel_gradient.py | 92 ++++++++++++----------------- 2 files changed, 40 insertions(+), 54 deletions(-) diff --git a/monai/transforms/post/dictionary.py b/monai/transforms/post/dictionary.py index 2060fb7000..4e39c1d5ee 100644 --- a/monai/transforms/post/dictionary.py +++ b/monai/transforms/post/dictionary.py @@ -825,7 +825,7 @@ def __init__( ) -> None: super().__init__(keys, allow_missing_keys) self.transform = SobelGradients( - kernel_size=kernel_size, padding=padding, direction=direction, dtype=dtype, device=device + kernel_size=kernel_size, padding=padding, spatial_axes=direction, dtype=dtype, device=device ) self.new_key_prefix = new_key_prefix diff --git a/tests/test_sobel_gradient.py b/tests/test_sobel_gradient.py index 440878724b..d890789169 100644 --- a/tests/test_sobel_gradient.py +++ b/tests/test_sobel_gradient.py @@ -9,6 +9,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from ast import arg import unittest import torch @@ -20,79 +21,69 @@ IMAGE = torch.zeros(1, 16, 16, dtype=torch.float32) IMAGE[0, 8, :] = 1 OUTPUT_3x3 = torch.zeros(2, 16, 16, dtype=torch.float32) -OUTPUT_3x3[0, 7, :] = 2.0 -OUTPUT_3x3[0, 9, :] = -2.0 -OUTPUT_3x3[0, 7, 0] = OUTPUT_3x3[0, 7, -1] = 1.5 -OUTPUT_3x3[0, 9, 0] = OUTPUT_3x3[0, 9, -1] = -1.5 -OUTPUT_3x3[1, 7, 0] = OUTPUT_3x3[1, 9, 0] = 0.5 -OUTPUT_3x3[1, 8, 0] = 1.0 -OUTPUT_3x3[1, 8, -1] = -1.0 -OUTPUT_3x3[1, 7, -1] = OUTPUT_3x3[1, 9, -1] = -0.5 +OUTPUT_3x3[1, 7, :] = -4.0 +OUTPUT_3x3[1, 9, :] = 4.0 TEST_CASE_0 = [IMAGE, {"kernel_size": 3, "dtype": torch.float32}, OUTPUT_3x3] TEST_CASE_1 = [IMAGE, {"kernel_size": 3, "dtype": torch.float64}, OUTPUT_3x3] -TEST_CASE_2 = [IMAGE, {"kernel_size": 3, "direction": "horizontal", "dtype": torch.float64}, OUTPUT_3x3[0][None, ...]] -TEST_CASE_3 = [IMAGE, {"kernel_size": 3, "direction": "vertical", "dtype": torch.float64}, OUTPUT_3x3[1][None, ...]] -TEST_CASE_4 = [IMAGE, {"kernel_size": 3, "direction": ["vertical"], "dtype": torch.float64}, OUTPUT_3x3[1][None, ...]] -TEST_CASE_5 = [IMAGE, {"kernel_size": 3, "direction": ["horizontal", "vertical"], "dtype": torch.float64}, OUTPUT_3x3] -TEST_CASE_6 = [IMAGE, {"kernel_size": 3, "direction": ("horizontal", "vertical"), "dtype": torch.float64}, OUTPUT_3x3] +TEST_CASE_2 = [IMAGE, {"kernel_size": 3, "spatial_axes": 0, "dtype": torch.float64}, OUTPUT_3x3[0][None, ...]] +TEST_CASE_3 = [IMAGE, {"kernel_size": 3, "spatial_axes": 1, "dtype": torch.float64}, OUTPUT_3x3[1][None, ...]] +TEST_CASE_4 = [IMAGE, {"kernel_size": 3, "spatial_axes": [1], "dtype": torch.float64}, OUTPUT_3x3[1][None, ...]] +TEST_CASE_5 = [IMAGE, {"kernel_size": 3, "spatial_axes": [0, 1], "dtype": torch.float64}, OUTPUT_3x3] +TEST_CASE_6 = [IMAGE, {"kernel_size": 3, "spatial_axes": (0, 1), "dtype": torch.float64}, OUTPUT_3x3] TEST_CASE_KERNEL_0 = [ {"kernel_size": 3, "dtype": torch.float64}, - torch.tensor([[-0.5, 0.0, 0.5], [-1.0, 0.0, 1.0], [-0.5, 0.0, 0.5]], dtype=torch.float64), + (torch.tensor([1.0, 0.0, -1.0], dtype=torch.float64), torch.tensor([1.0, 2.0, 1.0], dtype=torch.float64)), ] TEST_CASE_KERNEL_1 = [ {"kernel_size": 5, "dtype": torch.float64}, - torch.tensor( - [ - [-0.25, -0.2, 0.0, 0.2, 0.25], - [-0.4, -0.5, 0.0, 0.5, 0.4], - [-0.5, -1.0, 0.0, 1.0, 0.5], - [-0.4, -0.5, 0.0, 0.5, 0.4], - [-0.25, -0.2, 0.0, 0.2, 0.25], - ], - dtype=torch.float64, + ( + torch.tensor([1.0, 2.0, 0.0, -2.0, -1.0], dtype=torch.float64), + torch.tensor([1.0, 4.0, 6.0, 4.0, 1.0], dtype=torch.float64), ), ] TEST_CASE_KERNEL_2 = [ {"kernel_size": 7, "dtype": torch.float64}, - torch.tensor( - [ - [-3.0 / 18.0, -2.0 / 13.0, -1.0 / 10.0, 0.0, 1.0 / 10.0, 2.0 / 13.0, 3.0 / 18.0], - [-3.0 / 13.0, -2.0 / 8.0, -1.0 / 5.0, 0.0, 1.0 / 5.0, 2.0 / 8.0, 3.0 / 13.0], - [-3.0 / 10.0, -2.0 / 5.0, -1.0 / 2.0, 0.0, 1.0 / 2.0, 2.0 / 5.0, 3.0 / 10.0], - [-3.0 / 9.0, -2.0 / 4.0, -1.0 / 1.0, 0.0, 1.0 / 1.0, 2.0 / 4.0, 3.0 / 9.0], - [-3.0 / 10.0, -2.0 / 5.0, -1.0 / 2.0, 0.0, 1.0 / 2.0, 2.0 / 5.0, 3.0 / 10.0], - [-3.0 / 13.0, -2.0 / 8.0, -1.0 / 5.0, 0.0, 1.0 / 5.0, 2.0 / 8.0, 3.0 / 13.0], - [-3.0 / 18.0, -2.0 / 13.0, -1.0 / 10.0, 0.0, 1.0 / 10.0, 2.0 / 13.0, 3.0 / 18.0], - ], - dtype=torch.float64, + ( + torch.tensor([1.0, 4.0, 5.0, 0.0, -5.0, -4.0, -1.0], dtype=torch.float64), + torch.tensor([1.0, 6.0, 15.0, 20.0, 15.0, 6.0, 1.0], dtype=torch.float64), ), ] -TEST_CASE_ERROR_0 = [{"kernel_size": 1}] # kernel size less than 3 -TEST_CASE_ERROR_1 = [{"kernel_size": 4}] # even kernel size -TEST_CASE_ERROR_2 = [{"direction": 1}] # wrong type direction -TEST_CASE_ERROR_3 = [{"direction": "not_exist_direction"}] # wrong direction -TEST_CASE_ERROR_4 = [{"direction": ["not_exist_direction"]}] # wrong direction in a list -TEST_CASE_ERROR_5 = [{"direction": ["horizontal", "not_exist_direction"]}] # correct and wrong direction in a list - -TEST_CASE_IMAGE_ERROR_0 = [torch.cat([IMAGE, IMAGE], dim=0), {"kernel_size": 3, "dtype": torch.float32}] +TEST_CASE_ERROR_0 = [IMAGE, {"kernel_size": 1}] # kernel size less than 3 +TEST_CASE_ERROR_1 = [IMAGE, {"kernel_size": 4}] # even kernel size +TEST_CASE_ERROR_2 = [IMAGE, {"spatial_axes": "horizontal"}] # wrong type direction +TEST_CASE_ERROR_3 = [IMAGE, {"spatial_axes": 3}] # wrong direction +TEST_CASE_ERROR_4 = [IMAGE, {"spatial_axes": [3]}] # wrong direction in a list +TEST_CASE_ERROR_5 = [IMAGE, {"spatial_axes": [0, 4]}] # correct and wrong direction in a list class SobelGradientTests(unittest.TestCase): backend = None - @parameterized.expand([TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5, TEST_CASE_6]) + @parameterized.expand( + [ + TEST_CASE_0, + TEST_CASE_1, + TEST_CASE_2, + TEST_CASE_3, + TEST_CASE_4, + TEST_CASE_5, + TEST_CASE_6, + ] + ) def test_sobel_gradients(self, image, arguments, expected_grad): sobel = SobelGradients(**arguments) grad = sobel(image) assert_allclose(grad, expected_grad) @parameterized.expand([TEST_CASE_KERNEL_0, TEST_CASE_KERNEL_1, TEST_CASE_KERNEL_2]) - def test_sobel_kernels(self, arguments, expected_kernel): + def test_sobel_kernels(self, arguments, expected_kernels): sobel = SobelGradients(**arguments) - self.assertTrue(sobel.kernel.dtype == expected_kernel.dtype) - assert_allclose(sobel.kernel, expected_kernel) + self.assertTrue(sobel.kernel_diff.dtype == expected_kernels[0].dtype) + self.assertTrue(sobel.kernel_smooth.dtype == expected_kernels[0].dtype) + assert_allclose(sobel.kernel_diff, expected_kernels[0]) + assert_allclose(sobel.kernel_smooth, expected_kernels[1]) @parameterized.expand( [ @@ -104,14 +95,9 @@ def test_sobel_kernels(self, arguments, expected_kernel): TEST_CASE_ERROR_5, ] ) - def test_sobel_gradients_error(self, arguments): - with self.assertRaises(ValueError): - SobelGradients(**arguments) - - @parameterized.expand([TEST_CASE_IMAGE_ERROR_0]) - def test_sobel_gradients_image_error(self, image, arguments): - sobel = SobelGradients(**arguments) + def test_sobel_gradients_error(self, image, arguments): with self.assertRaises(ValueError): + sobel = SobelGradients(**arguments) sobel(image) From 43e845abe44c85ba29868b65cd1b7d97e8ba18d8 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 26 Oct 2022 17:49:15 -0400 Subject: [PATCH 12/25] Change arguments and add additional test case Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/post/array.py | 25 +++++++++++++------------ monai/transforms/post/dictionary.py | 2 +- tests/test_sobel_gradient.py | 23 ++++++++++++++++++++++- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/monai/transforms/post/array.py b/monai/transforms/post/array.py index edcc079bc7..aa479f5bdd 100644 --- a/monai/transforms/post/array.py +++ b/monai/transforms/post/array.py @@ -823,14 +823,16 @@ def __call__(self, data): class SobelGradients(Transform): - """Calculate Sobel horizontal and vertical gradients of a grayscale image. + """Calculate Sobel gradients of a grayscale image with the shape of (CxH[xWxDx...]). Args: kernel_size: the size of the Sobel kernel. Defaults to 3. - padding: the padding for the convolution to apply the kernel. Defaults to `"same"`. - axis: the axis on which the gradient to be calculated. By default it calculate the gradient for all axes. + spatial_axes: the axes that define the direction of the gradient to be calculated. It calculate the gradient + along each of the provide axis. By default it calculate the gradient for all spatial axes. + padding_mode: the padding mode of the image when convolving with Sobel kernels. Defaults to `"reflect"`. + Acceptable values are ``'zeros'``, ``'reflect'``, ``'replicate'`` or ``'circular'``. + See ``torch.nn.Conv1d()`` for more information. dtype: kernel data type (torch.dtype). Defaults to `torch.float32`. - device: the device to create the kernel on. Defaults to `"cpu"`. """ @@ -839,17 +841,16 @@ class SobelGradients(Transform): def __init__( self, kernel_size: int = 3, - padding: str = "reflect", spatial_axes: Optional[Union[Sequence[int], int]] = None, + padding_mode: str = "reflect", dtype: torch.dtype = torch.float32, - device: Union[torch.device, int, str] = "cpu", ) -> None: super().__init__() - self.padding = padding + self.padding = padding_mode self.spatial_axes = spatial_axes - self.kernel_diff, self.kernel_smooth = self._get_kernel(kernel_size, dtype, device) + self.kernel_diff, self.kernel_smooth = self._get_kernel(kernel_size, dtype) - def _get_kernel(self, size, dtype, device) -> Tuple[torch.Tensor, torch.Tensor]: + def _get_kernel(self, size, dtype) -> Tuple[torch.Tensor, torch.Tensor]: if size < 3: raise ValueError(f"Sobel kernel size should be at least three. {size} was given.") if size % 2 == 0: @@ -857,9 +858,9 @@ def _get_kernel(self, size, dtype, device) -> Tuple[torch.Tensor, torch.Tensor]: if not dtype.is_floating_point: raise ValueError(f"`dtype` for Sobel kernel should be floating point. {dtype} was given.") - expand_kernel = torch.tensor([[[1, 2, 1]]], dtype=dtype, device=device) - kernel_diff = torch.tensor([[[1, 0, -1]]], dtype=dtype, device=device) - kernel_smooth = torch.tensor([[[1, 2, 1]]], dtype=dtype, device=device) + expand_kernel = torch.tensor([[[1, 2, 1]]], dtype=dtype) + kernel_diff = torch.tensor([[[1, 0, -1]]], dtype=dtype) + kernel_smooth = torch.tensor([[[1, 2, 1]]], dtype=dtype) # Expand the kernel to larger size than 3 expand = (size - 3) // 2 diff --git a/monai/transforms/post/dictionary.py b/monai/transforms/post/dictionary.py index 4e39c1d5ee..e43157c164 100644 --- a/monai/transforms/post/dictionary.py +++ b/monai/transforms/post/dictionary.py @@ -825,7 +825,7 @@ def __init__( ) -> None: super().__init__(keys, allow_missing_keys) self.transform = SobelGradients( - kernel_size=kernel_size, padding=padding, spatial_axes=direction, dtype=dtype, device=device + kernel_size=kernel_size, padding_mode=padding, spatial_axes=direction, dtype=dtype, device=device ) self.new_key_prefix = new_key_prefix diff --git a/tests/test_sobel_gradient.py b/tests/test_sobel_gradient.py index d890789169..10f68a1ba0 100644 --- a/tests/test_sobel_gradient.py +++ b/tests/test_sobel_gradient.py @@ -20,17 +20,37 @@ IMAGE = torch.zeros(1, 16, 16, dtype=torch.float32) IMAGE[0, 8, :] = 1 + +# Output with reflect padding OUTPUT_3x3 = torch.zeros(2, 16, 16, dtype=torch.float32) OUTPUT_3x3[1, 7, :] = -4.0 OUTPUT_3x3[1, 9, :] = 4.0 +# Output with zero padding +OUTPUT_3x3_ZERO_PAD = OUTPUT_3x3.clone() +OUTPUT_3x3_ZERO_PAD[0, 7, 0] = OUTPUT_3x3_ZERO_PAD[0, 9, 0] = -1.0 +OUTPUT_3x3_ZERO_PAD[0, 8, 0] = -2.0 +OUTPUT_3x3_ZERO_PAD[0, 7, -1] = OUTPUT_3x3_ZERO_PAD[0, 9, -1] = 1.0 +OUTPUT_3x3_ZERO_PAD[0, 8, -1] = 2.0 +OUTPUT_3x3_ZERO_PAD[1, 7, 0] = OUTPUT_3x3_ZERO_PAD[1, 7, -1] = -3.0 +OUTPUT_3x3_ZERO_PAD[1, 9, 0] = OUTPUT_3x3_ZERO_PAD[1, 9, -1] = 3.0 + TEST_CASE_0 = [IMAGE, {"kernel_size": 3, "dtype": torch.float32}, OUTPUT_3x3] TEST_CASE_1 = [IMAGE, {"kernel_size": 3, "dtype": torch.float64}, OUTPUT_3x3] TEST_CASE_2 = [IMAGE, {"kernel_size": 3, "spatial_axes": 0, "dtype": torch.float64}, OUTPUT_3x3[0][None, ...]] TEST_CASE_3 = [IMAGE, {"kernel_size": 3, "spatial_axes": 1, "dtype": torch.float64}, OUTPUT_3x3[1][None, ...]] TEST_CASE_4 = [IMAGE, {"kernel_size": 3, "spatial_axes": [1], "dtype": torch.float64}, OUTPUT_3x3[1][None, ...]] TEST_CASE_5 = [IMAGE, {"kernel_size": 3, "spatial_axes": [0, 1], "dtype": torch.float64}, OUTPUT_3x3] -TEST_CASE_6 = [IMAGE, {"kernel_size": 3, "spatial_axes": (0, 1), "dtype": torch.float64}, OUTPUT_3x3] +TEST_CASE_6 = [ + IMAGE, + {"kernel_size": 3, "spatial_axes": (0, 1), "padding_mode": "reflect", "dtype": torch.float64}, + OUTPUT_3x3, +] +TEST_CASE_7 = [ + IMAGE, + {"kernel_size": 3, "spatial_axes": (0, 1), "padding_mode": "zeros", "dtype": torch.float64}, + OUTPUT_3x3_ZERO_PAD, +] TEST_CASE_KERNEL_0 = [ {"kernel_size": 3, "dtype": torch.float64}, @@ -70,6 +90,7 @@ class SobelGradientTests(unittest.TestCase): TEST_CASE_4, TEST_CASE_5, TEST_CASE_6, + TEST_CASE_7, ] ) def test_sobel_gradients(self, image, arguments, expected_grad): From 6f29bf3495db29ab5ab233055c4651f6231cde25 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 26 Oct 2022 18:01:10 -0400 Subject: [PATCH 13/25] Update sobel grad dict and related unittests Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/post/dictionary.py | 18 +++-- tests/test_sobel_gradientd.py | 113 ++++++++++++++-------------- 2 files changed, 65 insertions(+), 66 deletions(-) diff --git a/monai/transforms/post/dictionary.py b/monai/transforms/post/dictionary.py index e43157c164..ba63803de8 100644 --- a/monai/transforms/post/dictionary.py +++ b/monai/transforms/post/dictionary.py @@ -799,11 +799,12 @@ class SobelGradientsd(MapTransform): Args: keys: keys of the corresponding items to model output. kernel_size: the size of the Sobel kernel. Defaults to 3. - padding: the padding for the convolution to apply the kernel. Defaults to `"same"`. - direction: the direction in which the gradient to be calculated. It can be string "horizontal" or "vertical", - or list of strings ["horizontal", "vertical"]. By default it calculate the gradient in both directions. + spatial_axes: the axes that define the direction of the gradient to be calculated. It calculate the gradient + along each of the provide axis. By default it calculate the gradient for all spatial axes. + padding_mode: the padding mode of the image when convolving with Sobel kernels. Defaults to `"reflect"`. + Acceptable values are ``'zeros'``, ``'reflect'``, ``'replicate'`` or ``'circular'``. + See ``torch.nn.Conv1d()`` for more information. dtype: kernel data type (torch.dtype). Defaults to `torch.float32`. - device: the device to create the kernel on. Defaults to `"cpu"`. new_key_prefix: this prefix be prepended to the key to create a new key for the output and keep the value of key intact. By default not prefix is set and the corresponding array to the key will be replaced. allow_missing_keys: don't raise exception if key is missing. @@ -816,18 +817,19 @@ def __init__( self, keys: KeysCollection, kernel_size: int = 3, - padding: Union[int, str] = "same", - direction: Optional[str] = None, + spatial_axes: Optional[Union[Sequence[int], int]] = None, + padding_mode: str = "reflect", dtype: torch.dtype = torch.float32, - device: Union[torch.device, int, str] = "cpu", new_key_prefix: Optional[str] = None, allow_missing_keys: bool = False, ) -> None: super().__init__(keys, allow_missing_keys) self.transform = SobelGradients( - kernel_size=kernel_size, padding_mode=padding, spatial_axes=direction, dtype=dtype, device=device + kernel_size=kernel_size, spatial_axes=spatial_axes, padding_mode=padding_mode, dtype=dtype ) self.new_key_prefix = new_key_prefix + self.kernel_diff = self.transform.kernel_diff + self.kernel_smooth = self.transform.kernel_smooth def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: d = dict(data) diff --git a/tests/test_sobel_gradientd.py b/tests/test_sobel_gradientd.py index ae030a6e9b..d80a17f3bd 100644 --- a/tests/test_sobel_gradientd.py +++ b/tests/test_sobel_gradientd.py @@ -19,15 +19,20 @@ IMAGE = torch.zeros(1, 16, 16, dtype=torch.float32) IMAGE[0, 8, :] = 1 +# Output with reflect padding OUTPUT_3x3 = torch.zeros(2, 16, 16, dtype=torch.float32) -OUTPUT_3x3[0, 7, :] = 2.0 -OUTPUT_3x3[0, 9, :] = -2.0 -OUTPUT_3x3[0, 7, 0] = OUTPUT_3x3[0, 7, -1] = 1.5 -OUTPUT_3x3[0, 9, 0] = OUTPUT_3x3[0, 9, -1] = -1.5 -OUTPUT_3x3[1, 7, 0] = OUTPUT_3x3[1, 9, 0] = 0.5 -OUTPUT_3x3[1, 8, 0] = 1.0 -OUTPUT_3x3[1, 8, -1] = -1.0 -OUTPUT_3x3[1, 7, -1] = OUTPUT_3x3[1, 9, -1] = -0.5 +OUTPUT_3x3[1, 7, :] = -4.0 +OUTPUT_3x3[1, 9, :] = 4.0 + +# Output with zero padding +OUTPUT_3x3_ZERO_PAD = OUTPUT_3x3.clone() +OUTPUT_3x3_ZERO_PAD[0, 7, 0] = OUTPUT_3x3_ZERO_PAD[0, 9, 0] = -1.0 +OUTPUT_3x3_ZERO_PAD[0, 8, 0] = -2.0 +OUTPUT_3x3_ZERO_PAD[0, 7, -1] = OUTPUT_3x3_ZERO_PAD[0, 9, -1] = 1.0 +OUTPUT_3x3_ZERO_PAD[0, 8, -1] = 2.0 +OUTPUT_3x3_ZERO_PAD[1, 7, 0] = OUTPUT_3x3_ZERO_PAD[1, 7, -1] = -3.0 +OUTPUT_3x3_ZERO_PAD[1, 9, 0] = OUTPUT_3x3_ZERO_PAD[1, 9, -1] = 3.0 + TEST_CASE_0 = [{"image": IMAGE}, {"keys": "image", "kernel_size": 3, "dtype": torch.float32}, {"image": OUTPUT_3x3}] TEST_CASE_1 = [{"image": IMAGE}, {"keys": "image", "kernel_size": 3, "dtype": torch.float64}, {"image": OUTPUT_3x3}] @@ -38,84 +43,79 @@ ] TEST_CASE_3 = [ {"image": IMAGE}, - {"keys": "image", "kernel_size": 3, "direction": "horizontal", "dtype": torch.float32}, + {"keys": "image", "kernel_size": 3, "spatial_axes": 0, "dtype": torch.float32}, {"image": OUTPUT_3x3[0][None, ...]}, ] TEST_CASE_4 = [ {"image": IMAGE}, - {"keys": "image", "kernel_size": 3, "direction": "vertical", "dtype": torch.float32}, + {"keys": "image", "kernel_size": 3, "spatial_axes": 1, "dtype": torch.float32}, {"image": OUTPUT_3x3[1][None, ...]}, ] TEST_CASE_5 = [ {"image": IMAGE}, - {"keys": "image", "kernel_size": 3, "direction": ["vertical"], "dtype": torch.float32}, + {"keys": "image", "kernel_size": 3, "spatial_axes": [1], "dtype": torch.float32}, {"image": OUTPUT_3x3[1][None, ...]}, ] TEST_CASE_6 = [ {"image": IMAGE}, - {"keys": "image", "kernel_size": 3, "direction": ["horizontal", "vertical"], "dtype": torch.float32}, + {"keys": "image", "kernel_size": 3, "spatial_axes": [0, 1], "dtype": torch.float32}, {"image": OUTPUT_3x3}, ] TEST_CASE_7 = [ {"image": IMAGE}, - {"keys": "image", "kernel_size": 3, "direction": ("horizontal", "vertical"), "dtype": torch.float32}, + {"keys": "image", "kernel_size": 3, "spatial_axes": (0, 1), "padding_mode": "reflect", "dtype": torch.float32}, {"image": OUTPUT_3x3}, ] +TEST_CASE_8 = [ + {"image": IMAGE}, + {"keys": "image", "kernel_size": 3, "spatial_axes": (0, 1), "padding_mode": "zeros", "dtype": torch.float32}, + {"image": OUTPUT_3x3_ZERO_PAD}, +] TEST_CASE_KERNEL_0 = [ {"keys": "image", "kernel_size": 3, "dtype": torch.float64}, - torch.tensor([[-0.5, 0.0, 0.5], [-1.0, 0.0, 1.0], [-0.5, 0.0, 0.5]], dtype=torch.float64), + (torch.tensor([1.0, 0.0, -1.0], dtype=torch.float64), torch.tensor([1.0, 2.0, 1.0], dtype=torch.float64)), ] TEST_CASE_KERNEL_1 = [ {"keys": "image", "kernel_size": 5, "dtype": torch.float64}, - torch.tensor( - [ - [-0.25, -0.2, 0.0, 0.2, 0.25], - [-0.4, -0.5, 0.0, 0.5, 0.4], - [-0.5, -1.0, 0.0, 1.0, 0.5], - [-0.4, -0.5, 0.0, 0.5, 0.4], - [-0.25, -0.2, 0.0, 0.2, 0.25], - ], - dtype=torch.float64, + ( + torch.tensor([1.0, 2.0, 0.0, -2.0, -1.0], dtype=torch.float64), + torch.tensor([1.0, 4.0, 6.0, 4.0, 1.0], dtype=torch.float64), ), ] TEST_CASE_KERNEL_2 = [ {"keys": "image", "kernel_size": 7, "dtype": torch.float64}, - torch.tensor( - [ - [-3.0 / 18.0, -2.0 / 13.0, -1.0 / 10.0, 0.0, 1.0 / 10.0, 2.0 / 13.0, 3.0 / 18.0], - [-3.0 / 13.0, -2.0 / 8.0, -1.0 / 5.0, 0.0, 1.0 / 5.0, 2.0 / 8.0, 3.0 / 13.0], - [-3.0 / 10.0, -2.0 / 5.0, -1.0 / 2.0, 0.0, 1.0 / 2.0, 2.0 / 5.0, 3.0 / 10.0], - [-3.0 / 9.0, -2.0 / 4.0, -1.0 / 1.0, 0.0, 1.0 / 1.0, 2.0 / 4.0, 3.0 / 9.0], - [-3.0 / 10.0, -2.0 / 5.0, -1.0 / 2.0, 0.0, 1.0 / 2.0, 2.0 / 5.0, 3.0 / 10.0], - [-3.0 / 13.0, -2.0 / 8.0, -1.0 / 5.0, 0.0, 1.0 / 5.0, 2.0 / 8.0, 3.0 / 13.0], - [-3.0 / 18.0, -2.0 / 13.0, -1.0 / 10.0, 0.0, 1.0 / 10.0, 2.0 / 13.0, 3.0 / 18.0], - ], - dtype=torch.float64, + ( + torch.tensor([1.0, 4.0, 5.0, 0.0, -5.0, -4.0, -1.0], dtype=torch.float64), + torch.tensor([1.0, 6.0, 15.0, 20.0, 15.0, 6.0, 1.0], dtype=torch.float64), ), ] -TEST_CASE_ERROR_0 = [{"keys": "image", "kernel_size": 1}] # kernel size less than 3 -TEST_CASE_ERROR_1 = [{"keys": "image", "kernel_size": 4}] # even kernel size -TEST_CASE_ERROR_2 = [{"keys": "image", "kernel_size": 2, "direction": 1}] # wrong type direction -TEST_CASE_ERROR_3 = [{"keys": "image", "kernel_size": 2, "direction": "not_exist_direction"}] # wrong direction -TEST_CASE_ERROR_4 = [ - {"keys": "image", "kernel_size": 2, "direction": ["not_exist_direction"]} -] # wrong direction in a list +TEST_CASE_ERROR_0 = [{"image": IMAGE}, {"keys": "image", "kernel_size": 1}] # kernel size less than 3 +TEST_CASE_ERROR_1 = [{"image": IMAGE}, {"keys": "image", "kernel_size": 4}] # even kernel size +TEST_CASE_ERROR_2 = [{"image": IMAGE}, {"keys": "image", "spatial_axes": "horizontal"}] # wrong type direction +TEST_CASE_ERROR_3 = [{"image": IMAGE}, {"keys": "image", "spatial_axes": 3}] # wrong direction +TEST_CASE_ERROR_4 = [{"image": IMAGE}, {"keys": "image", "spatial_axes": [3]}] # wrong direction in a list TEST_CASE_ERROR_5 = [ - {"keys": "image", "kernel_size": 2, "direction": ["horizontal", "not_exist_direction"]} + {"image": IMAGE}, + {"keys": "image", "spatial_axes": [0, 4]}, ] # correct and wrong direction in a list -TEST_CASE_IMAGE_ERROR_0 = [ - {"image": torch.cat([IMAGE, IMAGE], dim=0)}, - {"keys": "image", "kernel_size": 3, "dtype": torch.float32}, -] - class SobelGradientTests(unittest.TestCase): backend = None @parameterized.expand( - [TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5, TEST_CASE_6, TEST_CASE_7] + [ + TEST_CASE_0, + TEST_CASE_1, + TEST_CASE_2, + TEST_CASE_3, + TEST_CASE_4, + TEST_CASE_5, + TEST_CASE_6, + TEST_CASE_7, + TEST_CASE_8, + ] ) def test_sobel_gradients(self, image_dict, arguments, expected_grad): sobel = SobelGradientsd(**arguments) @@ -124,10 +124,12 @@ def test_sobel_gradients(self, image_dict, arguments, expected_grad): assert_allclose(grad[key], expected_grad[key]) @parameterized.expand([TEST_CASE_KERNEL_0, TEST_CASE_KERNEL_1, TEST_CASE_KERNEL_2]) - def test_sobel_kernels(self, arguments, expected_kernel): + def test_sobel_kernels(self, arguments, expected_kernels): sobel = SobelGradientsd(**arguments) - self.assertTrue(sobel.transform.kernel.dtype == expected_kernel.dtype) - assert_allclose(sobel.transform.kernel, expected_kernel) + self.assertTrue(sobel.kernel_diff.dtype == expected_kernels[0].dtype) + self.assertTrue(sobel.kernel_smooth.dtype == expected_kernels[0].dtype) + assert_allclose(sobel.kernel_diff, expected_kernels[0]) + assert_allclose(sobel.kernel_smooth, expected_kernels[1]) @parameterized.expand( [ @@ -139,14 +141,9 @@ def test_sobel_kernels(self, arguments, expected_kernel): TEST_CASE_ERROR_5, ] ) - def test_sobel_gradients_error(self, arguments): - with self.assertRaises(ValueError): - SobelGradientsd(**arguments) - - @parameterized.expand([TEST_CASE_IMAGE_ERROR_0]) - def test_sobel_gradients_image_error(self, image_dict, arguments): - sobel = SobelGradientsd(**arguments) + def test_sobel_gradients_error(self, image_dict, arguments): with self.assertRaises(ValueError): + sobel = SobelGradientsd(**arguments) sobel(image_dict) From a26e340fc42c4002315bfa297a8050a8a9e27f5d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 26 Oct 2022 22:01:42 +0000 Subject: [PATCH 14/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- monai/transforms/post/array.py | 3 +-- tests/test_sobel_gradient.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/monai/transforms/post/array.py b/monai/transforms/post/array.py index aa479f5bdd..aff1f923f6 100644 --- a/monai/transforms/post/array.py +++ b/monai/transforms/post/array.py @@ -24,7 +24,7 @@ from monai.data.meta_obj import get_track_meta from monai.data.meta_tensor import MetaTensor from monai.networks import one_hot -from monai.networks.layers import GaussianFilter, apply_filter, separable_filtering, spatial_transforms +from monai.networks.layers import GaussianFilter, apply_filter, separable_filtering from monai.transforms.inverse import InvertibleTransform from monai.transforms.transform import Transform from monai.transforms.utils import ( @@ -32,7 +32,6 @@ fill_holes, get_largest_connected_component_mask, get_unique_labels, - map_spatial_axes, remove_small_objects, ) from monai.transforms.utils_pytorch_numpy_unification import unravel_index diff --git a/tests/test_sobel_gradient.py b/tests/test_sobel_gradient.py index 10f68a1ba0..0848ef6ee2 100644 --- a/tests/test_sobel_gradient.py +++ b/tests/test_sobel_gradient.py @@ -9,7 +9,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ast import arg import unittest import torch From feedaf500d7d035575816433023f085b1244c21e Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 26 Oct 2022 18:04:39 -0400 Subject: [PATCH 15/25] Remove unused imports Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/post/array.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/monai/transforms/post/array.py b/monai/transforms/post/array.py index aa479f5bdd..aff1f923f6 100644 --- a/monai/transforms/post/array.py +++ b/monai/transforms/post/array.py @@ -24,7 +24,7 @@ from monai.data.meta_obj import get_track_meta from monai.data.meta_tensor import MetaTensor from monai.networks import one_hot -from monai.networks.layers import GaussianFilter, apply_filter, separable_filtering, spatial_transforms +from monai.networks.layers import GaussianFilter, apply_filter, separable_filtering from monai.transforms.inverse import InvertibleTransform from monai.transforms.transform import Transform from monai.transforms.utils import ( @@ -32,7 +32,6 @@ fill_holes, get_largest_connected_component_mask, get_unique_labels, - map_spatial_axes, remove_small_objects, ) from monai.transforms.utils_pytorch_numpy_unification import unravel_index From ab3b2d2afb506a28be92540065665767a69a5f16 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 26 Oct 2022 18:06:25 -0400 Subject: [PATCH 16/25] Minor renaming Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/post/array.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monai/transforms/post/array.py b/monai/transforms/post/array.py index aff1f923f6..55a8f3d824 100644 --- a/monai/transforms/post/array.py +++ b/monai/transforms/post/array.py @@ -857,15 +857,15 @@ def _get_kernel(self, size, dtype) -> Tuple[torch.Tensor, torch.Tensor]: if not dtype.is_floating_point: raise ValueError(f"`dtype` for Sobel kernel should be floating point. {dtype} was given.") - expand_kernel = torch.tensor([[[1, 2, 1]]], dtype=dtype) + kernel_expansion = torch.tensor([[[1, 2, 1]]], dtype=dtype) kernel_diff = torch.tensor([[[1, 0, -1]]], dtype=dtype) kernel_smooth = torch.tensor([[[1, 2, 1]]], dtype=dtype) # Expand the kernel to larger size than 3 expand = (size - 3) // 2 for _ in range(expand): - kernel_diff = F.conv1d(kernel_diff, expand_kernel, padding=2) - kernel_smooth = F.conv1d(kernel_smooth, expand_kernel, padding=2) + kernel_diff = F.conv1d(kernel_diff, kernel_expansion, padding=2) + kernel_smooth = F.conv1d(kernel_smooth, kernel_expansion, padding=2) return kernel_diff.squeeze(), kernel_smooth.squeeze() From be0570dcdf3bb0272019b4585ab4971a3e249f53 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 26 Oct 2022 18:11:35 -0400 Subject: [PATCH 17/25] formatting Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_sobel_gradient.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/tests/test_sobel_gradient.py b/tests/test_sobel_gradient.py index 0848ef6ee2..16ed8356f5 100644 --- a/tests/test_sobel_gradient.py +++ b/tests/test_sobel_gradient.py @@ -81,16 +81,7 @@ class SobelGradientTests(unittest.TestCase): backend = None @parameterized.expand( - [ - TEST_CASE_0, - TEST_CASE_1, - TEST_CASE_2, - TEST_CASE_3, - TEST_CASE_4, - TEST_CASE_5, - TEST_CASE_6, - TEST_CASE_7, - ] + [TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5, TEST_CASE_6, TEST_CASE_7] ) def test_sobel_gradients(self, image, arguments, expected_grad): sobel = SobelGradients(**arguments) From 69ff3907ca4257abdaca991f014498d24c4c1581 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 26 Oct 2022 18:12:24 -0400 Subject: [PATCH 18/25] formatting Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_sobel_gradientd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_sobel_gradientd.py b/tests/test_sobel_gradientd.py index d80a17f3bd..b84b3acc25 100644 --- a/tests/test_sobel_gradientd.py +++ b/tests/test_sobel_gradientd.py @@ -19,6 +19,7 @@ IMAGE = torch.zeros(1, 16, 16, dtype=torch.float32) IMAGE[0, 8, :] = 1 + # Output with reflect padding OUTPUT_3x3 = torch.zeros(2, 16, 16, dtype=torch.float32) OUTPUT_3x3[1, 7, :] = -4.0 @@ -33,7 +34,6 @@ OUTPUT_3x3_ZERO_PAD[1, 7, 0] = OUTPUT_3x3_ZERO_PAD[1, 7, -1] = -3.0 OUTPUT_3x3_ZERO_PAD[1, 9, 0] = OUTPUT_3x3_ZERO_PAD[1, 9, -1] = 3.0 - TEST_CASE_0 = [{"image": IMAGE}, {"keys": "image", "kernel_size": 3, "dtype": torch.float32}, {"image": OUTPUT_3x3}] TEST_CASE_1 = [{"image": IMAGE}, {"keys": "image", "kernel_size": 3, "dtype": torch.float64}, {"image": OUTPUT_3x3}] TEST_CASE_2 = [ From 2983edb66a53ad64ed4454b55cb06752a8a3bd55 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 27 Oct 2022 09:12:58 -0400 Subject: [PATCH 19/25] Reverse gradient direction Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/post/array.py | 2 +- tests/test_sobel_gradientd.py | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/monai/transforms/post/array.py b/monai/transforms/post/array.py index 55a8f3d824..34e215137e 100644 --- a/monai/transforms/post/array.py +++ b/monai/transforms/post/array.py @@ -858,7 +858,7 @@ def _get_kernel(self, size, dtype) -> Tuple[torch.Tensor, torch.Tensor]: raise ValueError(f"`dtype` for Sobel kernel should be floating point. {dtype} was given.") kernel_expansion = torch.tensor([[[1, 2, 1]]], dtype=dtype) - kernel_diff = torch.tensor([[[1, 0, -1]]], dtype=dtype) + kernel_diff = torch.tensor([[[-1, 0, 1]]], dtype=dtype) kernel_smooth = torch.tensor([[[1, 2, 1]]], dtype=dtype) # Expand the kernel to larger size than 3 diff --git a/tests/test_sobel_gradientd.py b/tests/test_sobel_gradientd.py index b84b3acc25..6fd4b7e2de 100644 --- a/tests/test_sobel_gradientd.py +++ b/tests/test_sobel_gradientd.py @@ -22,17 +22,17 @@ # Output with reflect padding OUTPUT_3x3 = torch.zeros(2, 16, 16, dtype=torch.float32) -OUTPUT_3x3[1, 7, :] = -4.0 -OUTPUT_3x3[1, 9, :] = 4.0 +OUTPUT_3x3[1, 7, :] = 4.0 +OUTPUT_3x3[1, 9, :] = -4.0 # Output with zero padding OUTPUT_3x3_ZERO_PAD = OUTPUT_3x3.clone() -OUTPUT_3x3_ZERO_PAD[0, 7, 0] = OUTPUT_3x3_ZERO_PAD[0, 9, 0] = -1.0 -OUTPUT_3x3_ZERO_PAD[0, 8, 0] = -2.0 -OUTPUT_3x3_ZERO_PAD[0, 7, -1] = OUTPUT_3x3_ZERO_PAD[0, 9, -1] = 1.0 -OUTPUT_3x3_ZERO_PAD[0, 8, -1] = 2.0 -OUTPUT_3x3_ZERO_PAD[1, 7, 0] = OUTPUT_3x3_ZERO_PAD[1, 7, -1] = -3.0 -OUTPUT_3x3_ZERO_PAD[1, 9, 0] = OUTPUT_3x3_ZERO_PAD[1, 9, -1] = 3.0 +OUTPUT_3x3_ZERO_PAD[0, 7, 0] = OUTPUT_3x3_ZERO_PAD[0, 9, 0] = 1.0 +OUTPUT_3x3_ZERO_PAD[0, 8, 0] = 2.0 +OUTPUT_3x3_ZERO_PAD[0, 7, -1] = OUTPUT_3x3_ZERO_PAD[0, 9, -1] = -1.0 +OUTPUT_3x3_ZERO_PAD[0, 8, -1] = -2.0 +OUTPUT_3x3_ZERO_PAD[1, 7, 0] = OUTPUT_3x3_ZERO_PAD[1, 7, -1] = 3.0 +OUTPUT_3x3_ZERO_PAD[1, 9, 0] = OUTPUT_3x3_ZERO_PAD[1, 9, -1] = -3.0 TEST_CASE_0 = [{"image": IMAGE}, {"keys": "image", "kernel_size": 3, "dtype": torch.float32}, {"image": OUTPUT_3x3}] TEST_CASE_1 = [{"image": IMAGE}, {"keys": "image", "kernel_size": 3, "dtype": torch.float64}, {"image": OUTPUT_3x3}] @@ -74,19 +74,19 @@ TEST_CASE_KERNEL_0 = [ {"keys": "image", "kernel_size": 3, "dtype": torch.float64}, - (torch.tensor([1.0, 0.0, -1.0], dtype=torch.float64), torch.tensor([1.0, 2.0, 1.0], dtype=torch.float64)), + (torch.tensor([-1.0, 0.0, 1.0], dtype=torch.float64), torch.tensor([1.0, 2.0, 1.0], dtype=torch.float64)), ] TEST_CASE_KERNEL_1 = [ {"keys": "image", "kernel_size": 5, "dtype": torch.float64}, ( - torch.tensor([1.0, 2.0, 0.0, -2.0, -1.0], dtype=torch.float64), + torch.tensor([-1.0, -2.0, 0.0, 2.0, 1.0], dtype=torch.float64), torch.tensor([1.0, 4.0, 6.0, 4.0, 1.0], dtype=torch.float64), ), ] TEST_CASE_KERNEL_2 = [ {"keys": "image", "kernel_size": 7, "dtype": torch.float64}, ( - torch.tensor([1.0, 4.0, 5.0, 0.0, -5.0, -4.0, -1.0], dtype=torch.float64), + torch.tensor([-1.0, -4.0, -5.0, 0.0, 5.0, 4.0, 1.0], dtype=torch.float64), torch.tensor([1.0, 6.0, 15.0, 20.0, 15.0, 6.0, 1.0], dtype=torch.float64), ), ] From 7eb6ae56381b4b32dcebc82bb236f28263889990 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 27 Oct 2022 10:52:14 -0400 Subject: [PATCH 20/25] Update sobel unittests Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_sobel_gradient.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_sobel_gradient.py b/tests/test_sobel_gradient.py index 16ed8356f5..23a3e7cd05 100644 --- a/tests/test_sobel_gradient.py +++ b/tests/test_sobel_gradient.py @@ -22,17 +22,17 @@ # Output with reflect padding OUTPUT_3x3 = torch.zeros(2, 16, 16, dtype=torch.float32) -OUTPUT_3x3[1, 7, :] = -4.0 -OUTPUT_3x3[1, 9, :] = 4.0 +OUTPUT_3x3[1, 7, :] = 4.0 +OUTPUT_3x3[1, 9, :] = -4.0 # Output with zero padding OUTPUT_3x3_ZERO_PAD = OUTPUT_3x3.clone() -OUTPUT_3x3_ZERO_PAD[0, 7, 0] = OUTPUT_3x3_ZERO_PAD[0, 9, 0] = -1.0 -OUTPUT_3x3_ZERO_PAD[0, 8, 0] = -2.0 -OUTPUT_3x3_ZERO_PAD[0, 7, -1] = OUTPUT_3x3_ZERO_PAD[0, 9, -1] = 1.0 -OUTPUT_3x3_ZERO_PAD[0, 8, -1] = 2.0 -OUTPUT_3x3_ZERO_PAD[1, 7, 0] = OUTPUT_3x3_ZERO_PAD[1, 7, -1] = -3.0 -OUTPUT_3x3_ZERO_PAD[1, 9, 0] = OUTPUT_3x3_ZERO_PAD[1, 9, -1] = 3.0 +OUTPUT_3x3_ZERO_PAD[0, 7, 0] = OUTPUT_3x3_ZERO_PAD[0, 9, 0] = 1.0 +OUTPUT_3x3_ZERO_PAD[0, 8, 0] = 2.0 +OUTPUT_3x3_ZERO_PAD[0, 7, -1] = OUTPUT_3x3_ZERO_PAD[0, 9, -1] = -1.0 +OUTPUT_3x3_ZERO_PAD[0, 8, -1] = -2.0 +OUTPUT_3x3_ZERO_PAD[1, 7, 0] = OUTPUT_3x3_ZERO_PAD[1, 7, -1] = 3.0 +OUTPUT_3x3_ZERO_PAD[1, 9, 0] = OUTPUT_3x3_ZERO_PAD[1, 9, -1] = -3.0 TEST_CASE_0 = [IMAGE, {"kernel_size": 3, "dtype": torch.float32}, OUTPUT_3x3] TEST_CASE_1 = [IMAGE, {"kernel_size": 3, "dtype": torch.float64}, OUTPUT_3x3] @@ -53,19 +53,19 @@ TEST_CASE_KERNEL_0 = [ {"kernel_size": 3, "dtype": torch.float64}, - (torch.tensor([1.0, 0.0, -1.0], dtype=torch.float64), torch.tensor([1.0, 2.0, 1.0], dtype=torch.float64)), + (torch.tensor([-1.0, 0.0, 1.0], dtype=torch.float64), torch.tensor([1.0, 2.0, 1.0], dtype=torch.float64)), ] TEST_CASE_KERNEL_1 = [ {"kernel_size": 5, "dtype": torch.float64}, ( - torch.tensor([1.0, 2.0, 0.0, -2.0, -1.0], dtype=torch.float64), + torch.tensor([-1.0, -2.0, 0.0, 2.0, 1.0], dtype=torch.float64), torch.tensor([1.0, 4.0, 6.0, 4.0, 1.0], dtype=torch.float64), ), ] TEST_CASE_KERNEL_2 = [ {"kernel_size": 7, "dtype": torch.float64}, ( - torch.tensor([1.0, 4.0, 5.0, 0.0, -5.0, -4.0, -1.0], dtype=torch.float64), + torch.tensor([-1.0, -4.0, -5.0, 0.0, 5.0, 4.0, 1.0], dtype=torch.float64), torch.tensor([1.0, 6.0, 15.0, 20.0, 15.0, 6.0, 1.0], dtype=torch.float64), ), ] From a09c2accac8272017f773c5ae4b717f1fee1efe5 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 27 Oct 2022 14:33:28 -0400 Subject: [PATCH 21/25] Add normalize kernels and normalize gradients Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/post/array.py | 24 +++++++++++++++++++++--- monai/transforms/post/dictionary.py | 11 ++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/monai/transforms/post/array.py b/monai/transforms/post/array.py index 34e215137e..0ebc2faa55 100644 --- a/monai/transforms/post/array.py +++ b/monai/transforms/post/array.py @@ -828,6 +828,8 @@ class SobelGradients(Transform): kernel_size: the size of the Sobel kernel. Defaults to 3. spatial_axes: the axes that define the direction of the gradient to be calculated. It calculate the gradient along each of the provide axis. By default it calculate the gradient for all spatial axes. + normalize_kernels: if normalize the Sobel kernel to provide proper gradients. Defaults to True. + normalize_gradients: if normalize the output gradient to 0 and 1. Defaults to False. padding_mode: the padding mode of the image when convolving with Sobel kernels. Defaults to `"reflect"`. Acceptable values are ``'zeros'``, ``'reflect'``, ``'replicate'`` or ``'circular'``. See ``torch.nn.Conv1d()`` for more information. @@ -841,12 +843,16 @@ def __init__( self, kernel_size: int = 3, spatial_axes: Optional[Union[Sequence[int], int]] = None, + normalize_kernels: bool = True, + normalize_gradients: bool = False, padding_mode: str = "reflect", dtype: torch.dtype = torch.float32, ) -> None: super().__init__() self.padding = padding_mode self.spatial_axes = spatial_axes + self.normalize_kernels = normalize_kernels + self.normalize_gradients = normalize_gradients self.kernel_diff, self.kernel_smooth = self._get_kernel(kernel_size, dtype) def _get_kernel(self, size, dtype) -> Tuple[torch.Tensor, torch.Tensor]: @@ -854,12 +860,19 @@ def _get_kernel(self, size, dtype) -> Tuple[torch.Tensor, torch.Tensor]: raise ValueError(f"Sobel kernel size should be at least three. {size} was given.") if size % 2 == 0: raise ValueError(f"Sobel kernel size should be an odd number. {size} was given.") - if not dtype.is_floating_point: - raise ValueError(f"`dtype` for Sobel kernel should be floating point. {dtype} was given.") - kernel_expansion = torch.tensor([[[1, 2, 1]]], dtype=dtype) kernel_diff = torch.tensor([[[-1, 0, 1]]], dtype=dtype) kernel_smooth = torch.tensor([[[1, 2, 1]]], dtype=dtype) + kernel_expansion = torch.tensor([[[1, 2, 1]]], dtype=dtype) + + if self.normalize_kernels: + if not dtype.is_floating_point: + raise ValueError( + f"`dtype` for Sobel kernel should be floating point when `normalize_kernel==True`. {dtype} was given." + ) + kernel_diff /= 2.0 + kernel_smooth /= 4.0 + kernel_expansion /= 4.0 # Expand the kernel to larger size than 3 expand = (size - 3) // 2 @@ -901,6 +914,11 @@ def __call__(self, image: NdarrayOrTensor) -> torch.Tensor: kernels = [kernel_smooth] * n_spatial_dims kernels[ax - 1] = kernel_diff grad = separable_filtering(image_tensor, kernels, mode=self.padding) + if self.normalize_gradients: + if grad.min() != grad.max(): + grad -= grad.min() + if grad.max() > 0: + grad /= grad.max() grad_list.append(grad) grads = torch.cat(grad_list, dim=1) diff --git a/monai/transforms/post/dictionary.py b/monai/transforms/post/dictionary.py index ba63803de8..78d84a0bd1 100644 --- a/monai/transforms/post/dictionary.py +++ b/monai/transforms/post/dictionary.py @@ -801,6 +801,8 @@ class SobelGradientsd(MapTransform): kernel_size: the size of the Sobel kernel. Defaults to 3. spatial_axes: the axes that define the direction of the gradient to be calculated. It calculate the gradient along each of the provide axis. By default it calculate the gradient for all spatial axes. + normalize_kernels: if normalize the Sobel kernel to provide proper gradients. Defaults to True. + normalize_gradients: if normalize the output gradient to 0 and 1. Defaults to False. padding_mode: the padding mode of the image when convolving with Sobel kernels. Defaults to `"reflect"`. Acceptable values are ``'zeros'``, ``'reflect'``, ``'replicate'`` or ``'circular'``. See ``torch.nn.Conv1d()`` for more information. @@ -818,6 +820,8 @@ def __init__( keys: KeysCollection, kernel_size: int = 3, spatial_axes: Optional[Union[Sequence[int], int]] = None, + normalize_kernels: bool = True, + normalize_gradients: bool = False, padding_mode: str = "reflect", dtype: torch.dtype = torch.float32, new_key_prefix: Optional[str] = None, @@ -825,7 +829,12 @@ def __init__( ) -> None: super().__init__(keys, allow_missing_keys) self.transform = SobelGradients( - kernel_size=kernel_size, spatial_axes=spatial_axes, padding_mode=padding_mode, dtype=dtype + kernel_size=kernel_size, + spatial_axes=spatial_axes, + normalize_kernels=normalize_kernels, + normalize_gradients=normalize_gradients, + padding_mode=padding_mode, + dtype=dtype, ) self.new_key_prefix = new_key_prefix self.kernel_diff = self.transform.kernel_diff From 6848a3810c97271e3be70639ed895ce13b4b1bb1 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 27 Oct 2022 14:33:53 -0400 Subject: [PATCH 22/25] Update unitests and add new test cases Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_sobel_gradient.py | 106 ++++++++++++++++++++++++++++------ tests/test_sobel_gradientd.py | 86 ++++++++++++++++++++++----- 2 files changed, 162 insertions(+), 30 deletions(-) diff --git a/tests/test_sobel_gradient.py b/tests/test_sobel_gradient.py index 23a3e7cd05..d8366f576a 100644 --- a/tests/test_sobel_gradient.py +++ b/tests/test_sobel_gradient.py @@ -22,24 +22,28 @@ # Output with reflect padding OUTPUT_3x3 = torch.zeros(2, 16, 16, dtype=torch.float32) -OUTPUT_3x3[1, 7, :] = 4.0 -OUTPUT_3x3[1, 9, :] = -4.0 +OUTPUT_3x3[1, 7, :] = 0.5 +OUTPUT_3x3[1, 9, :] = -0.5 # Output with zero padding OUTPUT_3x3_ZERO_PAD = OUTPUT_3x3.clone() -OUTPUT_3x3_ZERO_PAD[0, 7, 0] = OUTPUT_3x3_ZERO_PAD[0, 9, 0] = 1.0 -OUTPUT_3x3_ZERO_PAD[0, 8, 0] = 2.0 -OUTPUT_3x3_ZERO_PAD[0, 7, -1] = OUTPUT_3x3_ZERO_PAD[0, 9, -1] = -1.0 -OUTPUT_3x3_ZERO_PAD[0, 8, -1] = -2.0 -OUTPUT_3x3_ZERO_PAD[1, 7, 0] = OUTPUT_3x3_ZERO_PAD[1, 7, -1] = 3.0 -OUTPUT_3x3_ZERO_PAD[1, 9, 0] = OUTPUT_3x3_ZERO_PAD[1, 9, -1] = -3.0 +OUTPUT_3x3_ZERO_PAD[0, 7, 0] = OUTPUT_3x3_ZERO_PAD[0, 9, 0] = 0.125 +OUTPUT_3x3_ZERO_PAD[0, 8, 0] = 0.25 +OUTPUT_3x3_ZERO_PAD[0, 7, -1] = OUTPUT_3x3_ZERO_PAD[0, 9, -1] = -0.125 +OUTPUT_3x3_ZERO_PAD[0, 8, -1] = -0.25 +OUTPUT_3x3_ZERO_PAD[1, 7, 0] = OUTPUT_3x3_ZERO_PAD[1, 7, -1] = 3.0 / 8.0 +OUTPUT_3x3_ZERO_PAD[1, 9, 0] = OUTPUT_3x3_ZERO_PAD[1, 9, -1] = -3.0 / 8.0 TEST_CASE_0 = [IMAGE, {"kernel_size": 3, "dtype": torch.float32}, OUTPUT_3x3] TEST_CASE_1 = [IMAGE, {"kernel_size": 3, "dtype": torch.float64}, OUTPUT_3x3] -TEST_CASE_2 = [IMAGE, {"kernel_size": 3, "spatial_axes": 0, "dtype": torch.float64}, OUTPUT_3x3[0][None, ...]] -TEST_CASE_3 = [IMAGE, {"kernel_size": 3, "spatial_axes": 1, "dtype": torch.float64}, OUTPUT_3x3[1][None, ...]] -TEST_CASE_4 = [IMAGE, {"kernel_size": 3, "spatial_axes": [1], "dtype": torch.float64}, OUTPUT_3x3[1][None, ...]] -TEST_CASE_5 = [IMAGE, {"kernel_size": 3, "spatial_axes": [0, 1], "dtype": torch.float64}, OUTPUT_3x3] +TEST_CASE_2 = [IMAGE, {"kernel_size": 3, "spatial_axes": 0, "dtype": torch.float64}, OUTPUT_3x3[0:1]] +TEST_CASE_3 = [IMAGE, {"kernel_size": 3, "spatial_axes": 1, "dtype": torch.float64}, OUTPUT_3x3[1:2]] +TEST_CASE_4 = [IMAGE, {"kernel_size": 3, "spatial_axes": [1], "dtype": torch.float64}, OUTPUT_3x3[1:2]] +TEST_CASE_5 = [ + IMAGE, + {"kernel_size": 3, "spatial_axes": [0, 1], "normalize_kernels": True, "dtype": torch.float64}, + OUTPUT_3x3, +] TEST_CASE_6 = [ IMAGE, {"kernel_size": 3, "spatial_axes": (0, 1), "padding_mode": "reflect", "dtype": torch.float64}, @@ -50,25 +54,72 @@ {"kernel_size": 3, "spatial_axes": (0, 1), "padding_mode": "zeros", "dtype": torch.float64}, OUTPUT_3x3_ZERO_PAD, ] +TEST_CASE_8 = [ # Non-normalized kernels + IMAGE, + {"kernel_size": 3, "normalize_kernels": False, "dtype": torch.float32}, + OUTPUT_3x3 * 8.0, +] +TEST_CASE_9 = [ # Normalized gradients and normalized kernels + IMAGE, + { + "kernel_size": 3, + "normalize_kernels": True, + "normalize_gradients": True, + "spatial_axes": (0, 1), + "dtype": torch.float64, + }, + torch.cat([OUTPUT_3x3[0:1], OUTPUT_3x3[1:2] + 0.5]), +] +TEST_CASE_10 = [ # Normalized gradients but non-normalized kernels + IMAGE, + { + "kernel_size": 3, + "normalize_kernels": False, + "normalize_gradients": True, + "spatial_axes": (0, 1), + "dtype": torch.float64, + }, + torch.cat([OUTPUT_3x3[0:1], OUTPUT_3x3[1:2] + 0.5]), +] + TEST_CASE_KERNEL_0 = [ {"kernel_size": 3, "dtype": torch.float64}, - (torch.tensor([-1.0, 0.0, 1.0], dtype=torch.float64), torch.tensor([1.0, 2.0, 1.0], dtype=torch.float64)), + (torch.tensor([-0.5, 0.0, 0.5], dtype=torch.float64), torch.tensor([0.25, 0.5, 0.25], dtype=torch.float64)), ] TEST_CASE_KERNEL_1 = [ {"kernel_size": 5, "dtype": torch.float64}, ( - torch.tensor([-1.0, -2.0, 0.0, 2.0, 1.0], dtype=torch.float64), - torch.tensor([1.0, 4.0, 6.0, 4.0, 1.0], dtype=torch.float64), + torch.tensor([-0.1250, -0.2500, 0.0000, 0.2500, 0.1250], dtype=torch.float64), + torch.tensor([0.0625, 0.2500, 0.3750, 0.2500, 0.0625], dtype=torch.float64), ), ] TEST_CASE_KERNEL_2 = [ {"kernel_size": 7, "dtype": torch.float64}, + ( + torch.tensor([-0.03125, -0.125, -0.15625, 0.0, 0.15625, 0.125, 0.03125], dtype=torch.float64), + torch.tensor([0.015625, 0.09375, 0.234375, 0.3125, 0.234375, 0.09375, 0.015625], dtype=torch.float64), + ), +] +TEST_CASE_KERNEL_NON_NORMALIZED_0 = [ + {"kernel_size": 3, "normalize_kernels": False, "dtype": torch.float64}, + (torch.tensor([-1.0, 0.0, 1.0], dtype=torch.float64), torch.tensor([1.0, 2.0, 1.0], dtype=torch.float64)), +] +TEST_CASE_KERNEL_NON_NORMALIZED_1 = [ + {"kernel_size": 5, "normalize_kernels": False, "dtype": torch.float64}, + ( + torch.tensor([-1.0, -2.0, 0.0, 2.0, 1.0], dtype=torch.float64), + torch.tensor([1.0, 4.0, 6.0, 4.0, 1.0], dtype=torch.float64), + ), +] +TEST_CASE_KERNEL_NON_NORMALIZED_2 = [ + {"kernel_size": 7, "normalize_kernels": False, "dtype": torch.float64}, ( torch.tensor([-1.0, -4.0, -5.0, 0.0, 5.0, 4.0, 1.0], dtype=torch.float64), torch.tensor([1.0, 6.0, 15.0, 20.0, 15.0, 6.0, 1.0], dtype=torch.float64), ), ] + TEST_CASE_ERROR_0 = [IMAGE, {"kernel_size": 1}] # kernel size less than 3 TEST_CASE_ERROR_1 = [IMAGE, {"kernel_size": 4}] # even kernel size TEST_CASE_ERROR_2 = [IMAGE, {"spatial_axes": "horizontal"}] # wrong type direction @@ -81,14 +132,35 @@ class SobelGradientTests(unittest.TestCase): backend = None @parameterized.expand( - [TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5, TEST_CASE_6, TEST_CASE_7] + [ + TEST_CASE_0, + TEST_CASE_1, + TEST_CASE_2, + TEST_CASE_3, + TEST_CASE_4, + TEST_CASE_5, + TEST_CASE_6, + TEST_CASE_7, + TEST_CASE_8, + TEST_CASE_9, + TEST_CASE_10, + ] ) def test_sobel_gradients(self, image, arguments, expected_grad): sobel = SobelGradients(**arguments) grad = sobel(image) assert_allclose(grad, expected_grad) - @parameterized.expand([TEST_CASE_KERNEL_0, TEST_CASE_KERNEL_1, TEST_CASE_KERNEL_2]) + @parameterized.expand( + [ + TEST_CASE_KERNEL_0, + TEST_CASE_KERNEL_1, + TEST_CASE_KERNEL_2, + TEST_CASE_KERNEL_NON_NORMALIZED_0, + TEST_CASE_KERNEL_NON_NORMALIZED_1, + TEST_CASE_KERNEL_NON_NORMALIZED_2, + ] + ) def test_sobel_kernels(self, arguments, expected_kernels): sobel = SobelGradients(**arguments) self.assertTrue(sobel.kernel_diff.dtype == expected_kernels[0].dtype) diff --git a/tests/test_sobel_gradientd.py b/tests/test_sobel_gradientd.py index 6fd4b7e2de..e4de0c4b54 100644 --- a/tests/test_sobel_gradientd.py +++ b/tests/test_sobel_gradientd.py @@ -22,17 +22,18 @@ # Output with reflect padding OUTPUT_3x3 = torch.zeros(2, 16, 16, dtype=torch.float32) -OUTPUT_3x3[1, 7, :] = 4.0 -OUTPUT_3x3[1, 9, :] = -4.0 +OUTPUT_3x3[1, 7, :] = 0.5 +OUTPUT_3x3[1, 9, :] = -0.5 # Output with zero padding OUTPUT_3x3_ZERO_PAD = OUTPUT_3x3.clone() -OUTPUT_3x3_ZERO_PAD[0, 7, 0] = OUTPUT_3x3_ZERO_PAD[0, 9, 0] = 1.0 -OUTPUT_3x3_ZERO_PAD[0, 8, 0] = 2.0 -OUTPUT_3x3_ZERO_PAD[0, 7, -1] = OUTPUT_3x3_ZERO_PAD[0, 9, -1] = -1.0 -OUTPUT_3x3_ZERO_PAD[0, 8, -1] = -2.0 -OUTPUT_3x3_ZERO_PAD[1, 7, 0] = OUTPUT_3x3_ZERO_PAD[1, 7, -1] = 3.0 -OUTPUT_3x3_ZERO_PAD[1, 9, 0] = OUTPUT_3x3_ZERO_PAD[1, 9, -1] = -3.0 +OUTPUT_3x3_ZERO_PAD[0, 7, 0] = OUTPUT_3x3_ZERO_PAD[0, 9, 0] = 0.125 +OUTPUT_3x3_ZERO_PAD[0, 8, 0] = 0.25 +OUTPUT_3x3_ZERO_PAD[0, 7, -1] = OUTPUT_3x3_ZERO_PAD[0, 9, -1] = -0.125 +OUTPUT_3x3_ZERO_PAD[0, 8, -1] = -0.25 +OUTPUT_3x3_ZERO_PAD[1, 7, 0] = OUTPUT_3x3_ZERO_PAD[1, 7, -1] = 3.0 / 8.0 +OUTPUT_3x3_ZERO_PAD[1, 9, 0] = OUTPUT_3x3_ZERO_PAD[1, 9, -1] = -3.0 / 8.0 + TEST_CASE_0 = [{"image": IMAGE}, {"keys": "image", "kernel_size": 3, "dtype": torch.float32}, {"image": OUTPUT_3x3}] TEST_CASE_1 = [{"image": IMAGE}, {"keys": "image", "kernel_size": 3, "dtype": torch.float64}, {"image": OUTPUT_3x3}] @@ -58,7 +59,7 @@ ] TEST_CASE_6 = [ {"image": IMAGE}, - {"keys": "image", "kernel_size": 3, "spatial_axes": [0, 1], "dtype": torch.float32}, + {"keys": "image", "kernel_size": 3, "spatial_axes": [0, 1], "normalize_kernels": True, "dtype": torch.float32}, {"image": OUTPUT_3x3}, ] TEST_CASE_7 = [ @@ -71,20 +72,67 @@ {"keys": "image", "kernel_size": 3, "spatial_axes": (0, 1), "padding_mode": "zeros", "dtype": torch.float32}, {"image": OUTPUT_3x3_ZERO_PAD}, ] +TEST_CASE_9 = [ # Non-normalized kernels + {"image": IMAGE}, + {"keys": "image", "kernel_size": 3, "spatial_axes": (0, 1), "normalize_kernels": False, "dtype": torch.float32}, + {"image": OUTPUT_3x3 * 8.0}, +] +TEST_CASE_10 = [ # Normalized gradients and normalized kernels + {"image": IMAGE}, + { + "keys": "image", + "kernel_size": 3, + "spatial_axes": (0, 1), + "normalize_kernels": True, + "normalize_gradients": True, + "dtype": torch.float32, + }, + {"image": torch.cat([OUTPUT_3x3[0:1], OUTPUT_3x3[1:2] + 0.5])}, +] +TEST_CASE_11 = [ # Normalized gradients but non-normalized kernels + {"image": IMAGE}, + { + "keys": "image", + "kernel_size": 3, + "spatial_axes": (0, 1), + "normalize_kernels": False, + "normalize_gradients": True, + "dtype": torch.float32, + }, + {"image": torch.cat([OUTPUT_3x3[0:1], OUTPUT_3x3[1:2] + 0.5])}, +] TEST_CASE_KERNEL_0 = [ {"keys": "image", "kernel_size": 3, "dtype": torch.float64}, - (torch.tensor([-1.0, 0.0, 1.0], dtype=torch.float64), torch.tensor([1.0, 2.0, 1.0], dtype=torch.float64)), + (torch.tensor([-0.5, 0.0, 0.5], dtype=torch.float64), torch.tensor([0.25, 0.5, 0.25], dtype=torch.float64)), ] TEST_CASE_KERNEL_1 = [ {"keys": "image", "kernel_size": 5, "dtype": torch.float64}, ( - torch.tensor([-1.0, -2.0, 0.0, 2.0, 1.0], dtype=torch.float64), - torch.tensor([1.0, 4.0, 6.0, 4.0, 1.0], dtype=torch.float64), + torch.tensor([-0.1250, -0.2500, 0.0000, 0.2500, 0.1250], dtype=torch.float64), + torch.tensor([0.0625, 0.2500, 0.3750, 0.2500, 0.0625], dtype=torch.float64), ), ] TEST_CASE_KERNEL_2 = [ {"keys": "image", "kernel_size": 7, "dtype": torch.float64}, + ( + torch.tensor([-0.03125, -0.125, -0.15625, 0.0, 0.15625, 0.125, 0.03125], dtype=torch.float64), + torch.tensor([0.015625, 0.09375, 0.234375, 0.3125, 0.234375, 0.09375, 0.015625], dtype=torch.float64), + ), +] +TEST_CASE_KERNEL_NON_NORMALIZED_0 = [ + {"keys": "image", "kernel_size": 3, "normalize_kernels": False, "dtype": torch.float64}, + (torch.tensor([-1.0, 0.0, 1.0], dtype=torch.float64), torch.tensor([1.0, 2.0, 1.0], dtype=torch.float64)), +] +TEST_CASE_KERNEL_NON_NORMALIZED_1 = [ + {"keys": "image", "kernel_size": 5, "normalize_kernels": False, "dtype": torch.float64}, + ( + torch.tensor([-1.0, -2.0, 0.0, 2.0, 1.0], dtype=torch.float64), + torch.tensor([1.0, 4.0, 6.0, 4.0, 1.0], dtype=torch.float64), + ), +] +TEST_CASE_KERNEL_NON_NORMALIZED_2 = [ + {"keys": "image", "kernel_size": 7, "normalize_kernels": False, "dtype": torch.float64}, ( torch.tensor([-1.0, -4.0, -5.0, 0.0, 5.0, 4.0, 1.0], dtype=torch.float64), torch.tensor([1.0, 6.0, 15.0, 20.0, 15.0, 6.0, 1.0], dtype=torch.float64), @@ -115,6 +163,9 @@ class SobelGradientTests(unittest.TestCase): TEST_CASE_6, TEST_CASE_7, TEST_CASE_8, + TEST_CASE_9, + TEST_CASE_10, + TEST_CASE_11, ] ) def test_sobel_gradients(self, image_dict, arguments, expected_grad): @@ -123,7 +174,16 @@ def test_sobel_gradients(self, image_dict, arguments, expected_grad): key = "image" if "new_key_prefix" not in arguments else arguments["new_key_prefix"] + arguments["keys"] assert_allclose(grad[key], expected_grad[key]) - @parameterized.expand([TEST_CASE_KERNEL_0, TEST_CASE_KERNEL_1, TEST_CASE_KERNEL_2]) + @parameterized.expand( + [ + TEST_CASE_KERNEL_0, + TEST_CASE_KERNEL_1, + TEST_CASE_KERNEL_2, + TEST_CASE_KERNEL_NON_NORMALIZED_0, + TEST_CASE_KERNEL_NON_NORMALIZED_1, + TEST_CASE_KERNEL_NON_NORMALIZED_2, + ] + ) def test_sobel_kernels(self, arguments, expected_kernels): sobel = SobelGradientsd(**arguments) self.assertTrue(sobel.kernel_diff.dtype == expected_kernels[0].dtype) From 3977784c35c724159a54c0432389069d91215cb8 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 27 Oct 2022 14:35:39 -0400 Subject: [PATCH 23/25] Update hovernet unittests Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_hovernet_loss.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/test_hovernet_loss.py b/tests/test_hovernet_loss.py index 0bdabdef70..0a3fd574a2 100644 --- a/tests/test_hovernet_loss.py +++ b/tests/test_hovernet_loss.py @@ -141,20 +141,27 @@ def test_shape_generator(num_classes=1, num_objects=3, batch_size=1, height=5, w TEST_CASE_3 = [ # batch size of 2, 3 classes with minor rotation of nuclear prediction {"prediction": inputs_test[3].inputs, "target": inputs_test[3].targets}, - 6.5777, + 3.6169, ] TEST_CASE_4 = [ # batch size of 2, 3 classes with medium rotation of nuclear prediction {"prediction": inputs_test[4].inputs, "target": inputs_test[4].targets}, - 8.5143, + 4.5079, ] TEST_CASE_5 = [ # batch size of 2, 3 classes with medium rotation of nuclear prediction {"prediction": inputs_test[5].inputs, "target": inputs_test[5].targets}, - 10.1705, + 5.4663, ] -CASES = [TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5] +CASES = [ + TEST_CASE_0, + TEST_CASE_1, + TEST_CASE_2, + TEST_CASE_3, + TEST_CASE_4, + TEST_CASE_5, +] ILL_CASES = [ [ From 45c0cea8b297ebf2b2fca5ce5071cadebc6e87f0 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Fri, 28 Oct 2022 08:39:29 -0400 Subject: [PATCH 24/25] formatting Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_hovernet_loss.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/test_hovernet_loss.py b/tests/test_hovernet_loss.py index 0a3fd574a2..c2c888804a 100644 --- a/tests/test_hovernet_loss.py +++ b/tests/test_hovernet_loss.py @@ -154,14 +154,7 @@ def test_shape_generator(num_classes=1, num_objects=3, batch_size=1, height=5, w 5.4663, ] -CASES = [ - TEST_CASE_0, - TEST_CASE_1, - TEST_CASE_2, - TEST_CASE_3, - TEST_CASE_4, - TEST_CASE_5, -] +CASES = [TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5] ILL_CASES = [ [ From 8f23120460884482405f8ac9794e746ee7707592 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Fri, 28 Oct 2022 15:04:30 -0400 Subject: [PATCH 25/25] Make less call to min and max Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/post/array.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/monai/transforms/post/array.py b/monai/transforms/post/array.py index 0ebc2faa55..34709daa42 100644 --- a/monai/transforms/post/array.py +++ b/monai/transforms/post/array.py @@ -915,10 +915,12 @@ def __call__(self, image: NdarrayOrTensor) -> torch.Tensor: kernels[ax - 1] = kernel_diff grad = separable_filtering(image_tensor, kernels, mode=self.padding) if self.normalize_gradients: - if grad.min() != grad.max(): - grad -= grad.min() - if grad.max() > 0: - grad /= grad.max() + grad_min = grad.min() + if grad_min != grad.max(): + grad -= grad_min + grad_max = grad.max() + if grad_max > 0: + grad /= grad_max grad_list.append(grad) grads = torch.cat(grad_list, dim=1)