Repository: Project-MONAI/MONAI

Issue:
Add random gaussian smooth transform
**Is your feature request related to a problem? Please describe.**
As we already have `GaussianSmooth` transform, need to add random version.



Discussion:

Resolving patch:
diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py
--- a/monai/transforms/intensity/array.py
+++ b/monai/transforms/intensity/array.py
@@ -22,6 +22,7 @@
 from monai.networks.layers import GaussianFilter
 from monai.transforms.compose import Randomizable, Transform
 from monai.transforms.utils import rescale_array
+from monai.utils import ensure_tuple_size
 
 
 class RandGaussianNoise(Randomizable, Transform):
@@ -492,3 +493,42 @@ def __call__(self, img: np.ndarray) -> np.ndarray:
         gaussian_filter = GaussianFilter(img.ndim - 1, self.sigma)
         input_data = torch.as_tensor(np.ascontiguousarray(img), dtype=torch.float).unsqueeze(0)
         return gaussian_filter(input_data).squeeze(0).detach().numpy()
+
+
+class RandGaussianSmooth(Randomizable, Transform):
+    """
+    Apply Gaussian smooth to the input data based on randomly selected `sigma` parameters.
+
+    Args:
+        sigma_x: randomly select sigma value for the first spatial dimension.
+        sigma_y: randomly select sigma value for the second spatial dimension if have.
+        sigma_z: randomly select sigma value for the third spatial dimension if have.
+        prob: probability of Gaussian smooth.
+
+    """
+
+    def __init__(
+        self,
+        sigma_x: Sequence[float] = (0.25, 1.5),
+        sigma_y: Sequence[float] = (0.25, 1.5),
+        sigma_z: Sequence[float] = (0.25, 1.5),
+        prob: float = 0.1,
+    ) -> None:
+        self.sigma_x = sigma_x
+        self.sigma_y = sigma_y
+        self.sigma_z = sigma_z
+        self.prob = prob
+        self._do_transform = False
+
+    def randomize(self, data: Optional[Any] = None) -> None:
+        self._do_transform = self.R.random_sample() < self.prob
+        self.x = self.R.uniform(low=self.sigma_x[0], high=self.sigma_x[1])
+        self.y = self.R.uniform(low=self.sigma_y[0], high=self.sigma_y[1])
+        self.z = self.R.uniform(low=self.sigma_z[0], high=self.sigma_z[1])
+
+    def __call__(self, img: np.ndarray) -> np.ndarray:
+        self.randomize()
+        if not self._do_transform:
+            return img
+        sigma = ensure_tuple_size(tup=(self.x, self.y, self.z), dim=img.ndim - 1)
+        return GaussianSmooth(sigma=sigma)(img)
diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py
--- a/monai/transforms/intensity/dictionary.py
+++ b/monai/transforms/intensity/dictionary.py
@@ -32,6 +32,7 @@
     ShiftIntensity,
     ThresholdIntensity,
 )
+from monai.utils import ensure_tuple_size
 
 
 class RandGaussianNoised(Randomizable, MapTransform):
@@ -443,6 +444,52 @@ def __call__(self, data):
         return d
 
 
+class RandGaussianSmoothd(Randomizable, MapTransform):
+    """
+    Dictionary-based wrapper of :py:class:`monai.transforms.GaussianSmooth`.
+
+    Args:
+        keys: keys of the corresponding items to be transformed.
+            See also: :py:class:`monai.transforms.compose.MapTransform`
+        sigma_x: randomly select sigma value for the first spatial dimension.
+        sigma_y: randomly select sigma value for the second spatial dimension if have.
+        sigma_z: randomly select sigma value for the third spatial dimension if have.
+        prob: probability of Gaussian smooth.
+
+    """
+
+    def __init__(
+        self,
+        keys: KeysCollection,
+        sigma_x: Sequence[float] = (0.25, 1.5),
+        sigma_y: Sequence[float] = (0.25, 1.5),
+        sigma_z: Sequence[float] = (0.25, 1.5),
+        prob: float = 0.1,
+    ):
+        super().__init__(keys)
+        self.sigma_x = sigma_x
+        self.sigma_y = sigma_y
+        self.sigma_z = sigma_z
+        self.prob = prob
+        self._do_transform = False
+
+    def randomize(self, data: Optional[Any] = None) -> None:
+        self._do_transform = self.R.random_sample() < self.prob
+        self.x = self.R.uniform(low=self.sigma_x[0], high=self.sigma_x[1])
+        self.y = self.R.uniform(low=self.sigma_y[0], high=self.sigma_y[1])
+        self.z = self.R.uniform(low=self.sigma_z[0], high=self.sigma_z[1])
+
+    def __call__(self, data):
+        d = dict(data)
+        self.randomize()
+        if not self._do_transform:
+            return d
+        for key in self.keys:
+            sigma = ensure_tuple_size(tup=(self.x, self.y, self.z), dim=d[key].ndim - 1)
+            d[key] = GaussianSmooth(sigma=sigma)(d[key])
+        return d
+
+
 RandGaussianNoiseD = RandGaussianNoiseDict = RandGaussianNoised
 ShiftIntensityD = ShiftIntensityDict = ShiftIntensityd
 RandShiftIntensityD = RandShiftIntensityDict = RandShiftIntensityd
@@ -456,3 +503,4 @@ def __call__(self, data):
 ScaleIntensityRangePercentilesD = ScaleIntensityRangePercentilesDict = ScaleIntensityRangePercentilesd
 MaskIntensityD = MaskIntensityDict = MaskIntensityd
 GaussianSmoothD = GaussianSmoothDict = GaussianSmoothd
+RandGaussianSmoothD = RandGaussianSmoothDict = RandGaussianSmoothd


Test patch:
diff --git a/tests/test_rand_gaussian_smooth.py b/tests/test_rand_gaussian_smooth.py
new file mode 100644
--- /dev/null
+++ b/tests/test_rand_gaussian_smooth.py
@@ -0,0 +1,52 @@
+# 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 RandGaussianSmooth
+
+TEST_CASE_1 = [
+    {"sigma_x": (0.5, 1.5), "prob": 1.0},
+    np.array([[[1, 1, 1], [2, 2, 2], [3, 3, 3]], [[4, 4, 4], [5, 5, 5], [6, 6, 6]]]),
+    np.array(
+        [
+            [[0.7291442, 0.9260285, 0.7291442], [1.1054044, 1.4038869, 1.1054044], [1.0672514, 1.3554319, 1.0672514]],
+            [[2.076441, 2.6371238, 2.076441], [2.763511, 3.5097172, 2.763511], [2.4145484, 3.0665274, 2.4145484]],
+        ]
+    ),
+]
+
+TEST_CASE_2 = [
+    {"sigma_x": (0.5, 1.5), "sigma_y": (0.5, 1.0), "prob": 1.0},
+    np.array([[[1, 1, 1], [2, 2, 2], [3, 3, 3]], [[4, 4, 4], [5, 5, 5], [6, 6, 6]]]),
+    np.array(
+        [
+            [[0.78625894, 1.0031066, 0.78625894], [1.1919919, 1.5207394, 1.191992], [1.1508504, 1.4682512, 1.1508505]],
+            [[2.239091, 2.8566248, 2.239091], [2.97998, 3.8018486, 2.97998], [2.6036828, 3.3217697, 2.6036828]],
+        ]
+    ),
+]
+
+
+class TestRandGaussianSmooth(unittest.TestCase):
+    @parameterized.expand([TEST_CASE_1, TEST_CASE_2])
+    def test_value(self, argments, image, expected_data):
+        converter = RandGaussianSmooth(**argments)
+        converter.set_random_state(seed=0)
+        result = converter(image)
+        np.testing.assert_allclose(result, expected_data, rtol=1e-4)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_rand_gaussian_smoothd.py b/tests/test_rand_gaussian_smoothd.py
new file mode 100644
--- /dev/null
+++ b/tests/test_rand_gaussian_smoothd.py
@@ -0,0 +1,52 @@
+# 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 RandGaussianSmoothd
+
+TEST_CASE_1 = [
+    {"keys": "img", "sigma_x": (0.5, 1.5), "prob": 1.0},
+    {"img": np.array([[[1, 1, 1], [2, 2, 2], [3, 3, 3]], [[4, 4, 4], [5, 5, 5], [6, 6, 6]]])},
+    np.array(
+        [
+            [[0.7291442, 0.9260285, 0.7291442], [1.1054044, 1.4038869, 1.1054044], [1.0672514, 1.3554319, 1.0672514]],
+            [[2.076441, 2.6371238, 2.076441], [2.763511, 3.5097172, 2.763511], [2.4145484, 3.0665274, 2.4145484]],
+        ]
+    ),
+]
+
+TEST_CASE_2 = [
+    {"keys": "img", "sigma_x": (0.5, 1.5), "sigma_y": (0.5, 1.0), "prob": 1.0},
+    {"img": np.array([[[1, 1, 1], [2, 2, 2], [3, 3, 3]], [[4, 4, 4], [5, 5, 5], [6, 6, 6]]])},
+    np.array(
+        [
+            [[0.78625894, 1.0031066, 0.78625894], [1.1919919, 1.5207394, 1.191992], [1.1508504, 1.4682512, 1.1508505]],
+            [[2.239091, 2.8566248, 2.239091], [2.97998, 3.8018486, 2.97998], [2.6036828, 3.3217697, 2.6036828]],
+        ]
+    ),
+]
+
+
+class TestRandGaussianSmoothd(unittest.TestCase):
+    @parameterized.expand([TEST_CASE_1, TEST_CASE_2])
+    def test_value(self, argments, image, expected_data):
+        converter = RandGaussianSmoothd(**argments)
+        converter.set_random_state(seed=0)
+        result = converter(image)
+        np.testing.assert_allclose(result["img"], expected_data, rtol=1e-4)
+
+
+if __name__ == "__main__":
+    unittest.main()