Repository: Project-MONAI/MONAI (Python)

Issue:
Add sharpen transform based on GaussianFilter
**Is your feature request related to a problem? Please describe.**
We already have Gaussian smooth in MONAI, need also develop a sharpen transform referring to:
http://scipy-lectures.org/advanced/image_processing/auto_examples/plot_sharpen.html



Discussion:

Patch:
diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst
index dc0ca8bb7f..c08c31c600 100644
--- a/docs/source/transforms.rst
+++ b/docs/source/transforms.rst
@@ -180,6 +180,12 @@ Intensity
     :members:
     :special-members: __call__
 
+`GaussianSharpen`
+"""""""""""""""""
+.. autoclass:: GaussianSharpen
+    :members:
+    :special-members: __call__
+
 IO
 ^^
 
@@ -597,6 +603,12 @@ Instensity (Dict)
     :members:
     :special-members: __call__
 
+`GaussianSharpend`
+""""""""""""""""""
+.. autoclass:: GaussianSharpend
+    :members:
+    :special-members: __call__
+
 IO (Dict)
 ^^^^^^^^^
 
diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py
index ed3b9ec180..23607578f7 100644
--- a/monai/transforms/intensity/array.py
+++ b/monai/transforms/intensity/array.py
@@ -493,6 +493,7 @@ def __call__(self, img: np.ndarray, mask_data: Optional[np.ndarray] = None) -> n
 class GaussianSmooth(Transform):
     """
     Apply Gaussian smooth to the input data based on specified `sigma` parameter.
+    A default value `sigma=1.0` is provided for reference.
 
     Args:
         sigma: if a list of values, must match the count of spatial dimensions of input data,
@@ -501,7 +502,7 @@ class GaussianSmooth(Transform):
 
     """
 
-    def __init__(self, sigma: Union[Sequence[float], float]) -> None:
+    def __init__(self, sigma: Union[Sequence[float], float] = 1.0) -> None:
         self.sigma = sigma
 
     def __call__(self, img: np.ndarray) -> np.ndarray:
@@ -547,3 +548,47 @@ def __call__(self, img: np.ndarray) -> np.ndarray:
             return img
         sigma = ensure_tuple_size(tup=(self.x, self.y, self.z), dim=img.ndim - 1)
         return GaussianSmooth(sigma=sigma)(img)
+
+
+class GaussianSharpen(Transform):
+    """
+    Sharpen images using the Gaussian Blur filter.
+    Referring to: http://scipy-lectures.org/advanced/image_processing/auto_examples/plot_sharpen.html.
+    The algorithm is shown as below
+
+    .. code-block:: python
+
+        blurred_f = gaussian_filter(img, sigma1)
+        filter_blurred_f = gaussian_filter(blurred_f, sigma2)
+        img = blurred_f + alpha * (blurred_f - filter_blurred_f)
+
+    A set of default values `sigma1=3.0`, `sigma2=1.0` and `alpha=30.0` is provide for reference.
+
+    Args:
+        sigma1: sigma parameter for the first gaussian kernel. if a list of values, must match the count
+            of spatial dimensions of input data, and apply every value in the list to 1 spatial dimension.
+            if only 1 value provided, use it for all spatial dimensions.
+        sigma2: sigma parameter for the second gaussian kernel. if a list of values, must match the count
+            of spatial dimensions of input data, and apply every value in the list to 1 spatial dimension.
+            if only 1 value provided, use it for all spatial dimensions.
+        alpha: weight parameter to compute the final result.
+
+    """
+
+    def __init__(
+        self,
+        sigma1: Union[Sequence[float], float] = 3.0,
+        sigma2: Union[Sequence[float], float] = 1.0,
+        alpha: float = 30.0,
+    ) -> None:
+        self.sigma1 = sigma1
+        self.sigma2 = sigma2
+        self.alpha = alpha
+
+    def __call__(self, img: np.ndarray) -> np.ndarray:
+        gaussian_filter1 = GaussianFilter(img.ndim - 1, self.sigma1)
+        gaussian_filter2 = GaussianFilter(img.ndim - 1, self.sigma2)
+        input_data = torch.as_tensor(np.ascontiguousarray(img), dtype=torch.float).unsqueeze(0)
+        blurred_f = gaussian_filter1(input_data)
+        filter_blurred_f = gaussian_filter2(blurred_f)
+        return (blurred_f + self.alpha * (blurred_f - filter_blurred_f)).squeeze(0).detach().numpy()
diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py
index a84de77866..7776b94d2b 100644
--- a/monai/transforms/intensity/dictionary.py
+++ b/monai/transforms/intensity/dictionary.py
@@ -24,6 +24,7 @@
 from monai.transforms.intensity.array import (
     AdjustContrast,
     GaussianSmooth,
+    GaussianSharpen,
     MaskIntensity,
     NormalizeIntensity,
     ScaleIntensity,
@@ -492,6 +493,40 @@ def __call__(self, data):
         return d
 
 
+class GaussianSharpend(MapTransform):
+    """
+    Dictionary-based wrapper of :py:class:`monai.transforms.GaussianSharpen`.
+
+    Args:
+        keys: keys of the corresponding items to be transformed.
+            See also: :py:class:`monai.transforms.compose.MapTransform`
+        sigma1: sigma parameter for the first gaussian kernel. if a list of values, must match the count
+            of spatial dimensions of input data, and apply every value in the list to 1 spatial dimension.
+            if only 1 value provided, use it for all spatial dimensions.
+        sigma2: sigma parameter for the second gaussian kernel. if a list of values, must match the count
+            of spatial dimensions of input data, and apply every value in the list to 1 spatial dimension.
+            if only 1 value provided, use it for all spatial dimensions.
+        alpha: weight parameter to compute the final result.
+
+    """
+
+    def __init__(
+        self,
+        keys: KeysCollection,
+        sigma1: Union[Sequence[float], float] = 3.0,
+        sigma2: Union[Sequence[float], float] = 1.0,
+        alpha: float = 30.0,
+    ):
+        super().__init__(keys)
+        self.converter = GaussianSharpen(sigma1, sigma2, alpha)
+
+    def __call__(self, data):
+        d = dict(data)
+        for key in self.keys:
+            d[key] = self.converter(d[key])
+        return d
+
+
 RandGaussianNoiseD = RandGaussianNoiseDict = RandGaussianNoised
 ShiftIntensityD = ShiftIntensityDict = ShiftIntensityd
 RandShiftIntensityD = RandShiftIntensityDict = RandShiftIntensityd
@@ -506,3 +541,4 @@ def __call__(self, data):
 MaskIntensityD = MaskIntensityDict = MaskIntensityd
 GaussianSmoothD = GaussianSmoothDict = GaussianSmoothd
 RandGaussianSmoothD = RandGaussianSmoothDict = RandGaussianSmoothd
+GaussianSharpenD = GaussianSharpenDict = GaussianSharpend


Test patch:
diff --git a/tests/test_gaussian_sharpen.py b/tests/test_gaussian_sharpen.py
new file mode 100644
index 0000000000..99193208ce
--- /dev/null
+++ b/tests/test_gaussian_sharpen.py
@@ -0,0 +1,61 @@
+# Copyright 2020 MONAI Consortium
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+
+import numpy as np
+from parameterized import parameterized
+
+from monai.transforms import GaussianSharpen
+
+TEST_CASE_1 = [
+    {},
+    np.array([[[1, 1, 1], [2, 2, 2], [3, 3, 3]], [[4, 4, 4], [5, 5, 5], [6, 6, 6]]]),
+    np.array(
+        [
+            [[4.0335875, 3.362756, 4.0335875], [3.588128, 2.628216, 3.588128], [4.491922, 3.8134987, 4.491922]],
+            [[10.427719, 8.744948, 10.427719], [8.97032, 6.5705404, 8.970321], [10.886056, 9.195692, 10.886056]],
+        ]
+    ),
+]
+
+TEST_CASE_2 = [
+    {"sigma1": 1.0, "sigma2": 0.75, "alpha": 20},
+    np.array([[[1, 1, 1], [2, 2, 2], [3, 3, 3]], [[4, 4, 4], [5, 5, 5], [6, 6, 6]]]),
+    np.array(
+        [
+            [[4.146659, 4.392873, 4.146659], [8.031006, 8.804623, 8.031005], [10.127394, 11.669131, 10.127394]],
+            [[14.852196, 16.439377, 14.852201], [20.077503, 22.011555, 20.077507], [20.832941, 23.715641, 20.832935]],
+        ]
+    ),
+]
+
+TEST_CASE_3 = [
+    {"sigma1": (0.5, 1.0), "sigma2": (0.5, 0.75), "alpha": 20},
+    np.array([[[1, 1, 1], [2, 2, 2], [3, 3, 3]], [[4, 4, 4], [5, 5, 5], [6, 6, 6]]]),
+    np.array(
+        [
+            [[3.129089, 3.0711129, 3.129089], [6.783306, 6.8526435, 6.7833037], [11.901203, 13.098082, 11.901203]],
+            [[14.401806, 15.198004, 14.401809], [16.958261, 17.131605, 16.958261], [23.17392, 25.224974, 23.17392]],
+        ]
+    ),
+]
+
+
+class TestGaussianSharpen(unittest.TestCase):
+    @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3])
+    def test_value(self, argments, image, expected_data):
+        result = GaussianSharpen(**argments)(image)
+        np.testing.assert_allclose(result, expected_data, rtol=1e-4)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_gaussian_sharpend.py b/tests/test_gaussian_sharpend.py
new file mode 100644
index 0000000000..90139c9122
--- /dev/null
+++ b/tests/test_gaussian_sharpend.py
@@ -0,0 +1,61 @@
+# Copyright 2020 MONAI Consortium
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+
+import numpy as np
+from parameterized import parameterized
+
+from monai.transforms import GaussianSharpend
+
+TEST_CASE_1 = [
+    {"keys": "img"},
+    {"img": np.array([[[1, 1, 1], [2, 2, 2], [3, 3, 3]], [[4, 4, 4], [5, 5, 5], [6, 6, 6]]])},
+    np.array(
+        [
+            [[4.0335875, 3.362756, 4.0335875], [3.588128, 2.628216, 3.588128], [4.491922, 3.8134987, 4.491922]],
+            [[10.427719, 8.744948, 10.427719], [8.97032, 6.5705404, 8.970321], [10.886056, 9.195692, 10.886056]],
+        ]
+    ),
+]
+
+TEST_CASE_2 = [
+    {"keys": "img", "sigma1": 1.0, "sigma2": 0.75, "alpha": 20},
+    {"img": np.array([[[1, 1, 1], [2, 2, 2], [3, 3, 3]], [[4, 4, 4], [5, 5, 5], [6, 6, 6]]])},
+    np.array(
+        [
+            [[4.146659, 4.392873, 4.146659], [8.031006, 8.804623, 8.031005], [10.127394, 11.669131, 10.127394]],
+            [[14.852196, 16.439377, 14.852201], [20.077503, 22.011555, 20.077507], [20.832941, 23.715641, 20.832935]],
+        ]
+    ),
+]
+
+TEST_CASE_3 = [
+    {"keys": "img", "sigma1": (0.5, 1.0), "sigma2": (0.5, 0.75), "alpha": 20},
+    {"img": np.array([[[1, 1, 1], [2, 2, 2], [3, 3, 3]], [[4, 4, 4], [5, 5, 5], [6, 6, 6]]])},
+    np.array(
+        [
+            [[3.129089, 3.0711129, 3.129089], [6.783306, 6.8526435, 6.7833037], [11.901203, 13.098082, 11.901203]],
+            [[14.401806, 15.198004, 14.401809], [16.958261, 17.131605, 16.958261], [23.17392, 25.224974, 23.17392]],
+        ]
+    ),
+]
+
+
+class TestGaussianSharpend(unittest.TestCase):
+    @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3])
+    def test_value(self, argments, image, expected_data):
+        result = GaussianSharpend(**argments)(image)
+        np.testing.assert_allclose(result["img"], expected_data, rtol=1e-4)
+
+
+if __name__ == "__main__":
+    unittest.main()