Repository: Project-MONAI/MONAI

Issue:
TorchVisiond does not support randomized transforms
**Describe the bug**
`TorchVisiond` is a wrapper for any torchvision transform. However, it is not a `RandomizableTransform`, so when used to wrap randomized transforms from torchvision, it fails to acknowledge the randomness nature of those transforms. It is particularly troubling in data augmentation and situations like `SmartCache` that caches non-random transformation.



Discussion:

Resolving patch:
diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py
--- a/monai/transforms/__init__.py
+++ b/monai/transforms/__init__.py
@@ -340,6 +340,9 @@
     RandLambdad,
     RandLambdaD,
     RandLambdaDict,
+    RandTorchVisiond,
+    RandTorchVisionD,
+    RandTorchVisionDict,
     RemoveRepeatedChanneld,
     RemoveRepeatedChannelD,
     RemoveRepeatedChannelDict,
diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py
--- a/monai/transforms/utility/dictionary.py
+++ b/monai/transforms/utility/dictionary.py
@@ -25,7 +25,7 @@
 
 from monai.config import DtypeLike, KeysCollection, NdarrayTensor
 from monai.transforms.inverse import InvertibleTransform
-from monai.transforms.transform import MapTransform, Randomizable
+from monai.transforms.transform import MapTransform, Randomizable, RandomizableTransform
 from monai.transforms.utility.array import (
     AddChannel,
     AsChannelFirst,
@@ -79,6 +79,7 @@
     "ConvertToMultiChannelBasedOnBratsClassesd",
     "AddExtremePointsChanneld",
     "TorchVisiond",
+    "RandTorchVisiond",
     "MapLabelValued",
     "IdentityD",
     "IdentityDict",
@@ -126,6 +127,8 @@
     "AddExtremePointsChannelDict",
     "TorchVisionD",
     "TorchVisionDict",
+    "RandTorchVisionD",
+    "RandTorchVisionDict",
     "MapLabelValueD",
     "MapLabelValueDict",
 ]
@@ -933,12 +936,22 @@ def __call__(self, data):
 
 class TorchVisiond(MapTransform):
     """
-    Dictionary-based wrapper of :py:class:`monai.transforms.TorchVision`.
-    As most of the TorchVision transforms only work for PIL image and PyTorch Tensor, this transform expects input
-    data to be dict of PyTorch Tensors, users can easily call `ToTensord` transform to convert Numpy to Tensor.
+    Dictionary-based wrapper of :py:class:`monai.transforms.TorchVision` for non-randomized transforms.
+    For randomized transforms of TorchVision use :py:class:`monai.transforms.RandTorchVisiond`.
+
+    Note:
+        As most of the TorchVision transforms only work for PIL image and PyTorch Tensor, this transform expects input
+        data to be dict of PyTorch Tensors, users can easily call `ToTensord` transform to convert Numpy to Tensor.
     """
 
-    def __init__(self, keys: KeysCollection, name: str, allow_missing_keys: bool = False, *args, **kwargs) -> None:
+    def __init__(
+        self,
+        keys: KeysCollection,
+        name: str,
+        allow_missing_keys: bool = False,
+        *args,
+        **kwargs,
+    ) -> None:
         """
         Args:
             keys: keys of the corresponding items to be transformed.
@@ -959,6 +972,44 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc
         return d
 
 
+class RandTorchVisiond(RandomizableTransform, MapTransform):
+    """
+    Dictionary-based wrapper of :py:class:`monai.transforms.TorchVision` for randomized transoforms.
+    For deterministic non-randomized transforms of TorchVision use :py:class:`monai.transforms.TorchVisiond`.
+
+    Note:
+        As most of the TorchVision transforms only work for PIL image and PyTorch Tensor, this transform expects input
+        data to be dict of PyTorch Tensors, users can easily call `ToTensord` transform to convert Numpy to Tensor.
+    """
+
+    def __init__(
+        self,
+        keys: KeysCollection,
+        name: str,
+        allow_missing_keys: bool = False,
+        *args,
+        **kwargs,
+    ) -> None:
+        """
+        Args:
+            keys: keys of the corresponding items to be transformed.
+                See also: :py:class:`monai.transforms.compose.MapTransform`
+            name: The transform name in TorchVision package.
+            allow_missing_keys: don't raise exception if key is missing.
+            args: parameters for the TorchVision transform.
+            kwargs: parameters for the TorchVision transform.
+
+        """
+        MapTransform.__init__(self, keys, allow_missing_keys)
+        self.trans = TorchVision(name, *args, **kwargs)
+
+    def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]:
+        d = dict(data)
+        for key in self.key_iterator(d):
+            d[key] = self.trans(d[key])
+        return d
+
+
 class MapLabelValued(MapTransform):
     """
     Dictionary-based wrapper of :py:class:`monai.transforms.MapLabelValue`.
@@ -1018,5 +1069,6 @@ def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.nda
 ) = ConvertToMultiChannelBasedOnBratsClassesd
 AddExtremePointsChannelD = AddExtremePointsChannelDict = AddExtremePointsChanneld
 TorchVisionD = TorchVisionDict = TorchVisiond
+RandTorchVisionD = RandTorchVisionDict = RandTorchVisiond
 RandLambdaD = RandLambdaDict = RandLambdad
 MapLabelValueD = MapLabelValueDict = MapLabelValued


Test patch:
diff --git a/tests/min_tests.py b/tests/min_tests.py
--- a/tests/min_tests.py
+++ b/tests/min_tests.py
@@ -105,6 +105,7 @@ def run_testsuit():
         "test_occlusion_sensitivity",
         "test_torchvision",
         "test_torchvisiond",
+        "test_randtorchvisiond",
         "test_handler_metrics_saver",
         "test_handler_metrics_saver_dist",
         "test_evenly_divisible_all_gather_dist",
diff --git a/tests/test_randtorchvisiond.py b/tests/test_randtorchvisiond.py
new file mode 100644
--- /dev/null
+++ b/tests/test_randtorchvisiond.py
@@ -0,0 +1,88 @@
+# Copyright 2020 - 2021 MONAI Consortium
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+
+import torch
+from parameterized import parameterized
+
+from monai.transforms import Randomizable, RandTorchVisiond
+from monai.utils import set_determinism
+from tests.utils import SkipIfBeforePyTorchVersion
+
+TEST_CASE_1 = [
+    {"keys": "img", "name": "ColorJitter"},
+    {"img": torch.tensor([[[0.0, 1.0], [1.0, 2.0]], [[0.0, 1.0], [1.0, 2.0]], [[0.0, 1.0], [1.0, 2.0]]])},
+    torch.tensor([[[0.0, 1.0], [1.0, 2.0]], [[0.0, 1.0], [1.0, 2.0]], [[0.0, 1.0], [1.0, 2.0]]]),
+]
+
+TEST_CASE_2 = [
+    {"keys": "img", "name": "ColorJitter", "brightness": 0.5, "contrast": 0.5, "saturation": [0.1, 0.8], "hue": 0.5},
+    {"img": torch.tensor([[[0.0, 1.0], [1.0, 2.0]], [[0.0, 1.0], [1.0, 2.0]], [[0.0, 1.0], [1.0, 2.0]]])},
+    torch.tensor(
+        [
+            [
+                [0.1090, 0.6193],
+                [0.6193, 0.9164],
+            ],
+            [
+                [0.1090, 0.6193],
+                [0.6193, 0.9164],
+            ],
+            [
+                [0.1090, 0.6193],
+                [0.6193, 0.9164],
+            ],
+        ],
+    ),
+]
+
+TEST_CASE_3 = [
+    {"keys": "img", "name": "Pad", "padding": [1, 1, 1, 1]},
+    {"img": torch.tensor([[[0.0, 1.0], [1.0, 2.0]], [[0.0, 1.0], [1.0, 2.0]], [[0.0, 1.0], [1.0, 2.0]]])},
+    torch.tensor(
+        [
+            [
+                [0.0, 0.0, 0.0, 0.0],
+                [0.0, 0.0, 1.0, 0.0],
+                [0.0, 1.0, 2.0, 0.0],
+                [0.0, 0.0, 0.0, 0.0],
+            ],
+            [
+                [0.0, 0.0, 0.0, 0.0],
+                [0.0, 0.0, 1.0, 0.0],
+                [0.0, 1.0, 2.0, 0.0],
+                [0.0, 0.0, 0.0, 0.0],
+            ],
+            [
+                [0.0, 0.0, 0.0, 0.0],
+                [0.0, 0.0, 1.0, 0.0],
+                [0.0, 1.0, 2.0, 0.0],
+                [0.0, 0.0, 0.0, 0.0],
+            ],
+        ]
+    ),
+]
+
+
+@SkipIfBeforePyTorchVersion((1, 7))
+class TestRandTorchVisiond(unittest.TestCase):
+    @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3])
+    def test_value(self, input_param, input_data, expected_value):
+        set_determinism(seed=0)
+        transform = RandTorchVisiond(**input_param)
+        result = transform(input_data)
+        self.assertTrue(isinstance(transform, Randomizable))
+        torch.testing.assert_allclose(result["img"], expected_value)
+
+
+if __name__ == "__main__":
+    unittest.main()