Repository: pytorch/vision
Pull request: perform out of bounds check for single values and two tuples in ColorJitter
Closes #7132.

cc @vfdev-5

Problem statement:
ColorJitter doesn't check bounds for single value inputs
The parameters passed to `ColorJitter` can be passed either as single number or as a two-tuple. Doing the former internally creates a two tuple as well using the passed value as offset from a hardcoded center. For example, the center for `hue` is `0` and so we have

```py
from torchvision.prototype import transforms

transform1 = transforms.ColorJitter(hue=0.3)
transform2 = transforms.ColorJitter(hue=(-0.3, 0.3))

assert transform1.hue == transform2.hue
```

However, when it comes to check for out of bounds values, passing a single value goes unchecked, while passing the corresponding two-tuple fails

```python
transform1 = transforms.ColorJitter(hue=1.0)

assert transform1.hue == (-1.0, 1.0)

transforms.ColorJitter(hue=(-1.0, 1.0))
```

```
ValueError: hue values should be between (-0.5, 0.5)
```

We should apply the same check to the single value as well.

cc @vfdev-5

Hints from issue discussion:

Commit message: apply fix to prototype as well

Patch to review:
diff --git a/test/test_prototype_transforms_consistency.py b/test/test_prototype_transforms_consistency.py
index 3b69b72dd4f..a77f42930bc 100644
--- a/test/test_prototype_transforms_consistency.py
+++ b/test/test_prototype_transforms_consistency.py
@@ -312,7 +312,7 @@ def __init__(
             ArgsKwargs(saturation=(0.8, 0.9)),
             ArgsKwargs(hue=0.3),
             ArgsKwargs(hue=(-0.1, 0.2)),
-            ArgsKwargs(brightness=0.1, contrast=0.4, saturation=0.5, hue=0.6),
+            ArgsKwargs(brightness=0.1, contrast=0.4, saturation=0.5, hue=0.3),
         ],
         closeness_kwargs={"atol": 1e-5, "rtol": 1e-5},
     ),
diff --git a/test/test_transforms.py b/test/test_transforms.py
index 0c388dbb519..0340f9f3f15 100644
--- a/test/test_transforms.py
+++ b/test/test_transforms.py
@@ -1798,6 +1798,12 @@ def test_color_jitter():
     color_jitter.__repr__()
 
 
+@pytest.mark.parametrize("hue", [1, (-1, 1)])
+def test_color_jitter_hue_out_of_bounds(hue):
+    with pytest.raises(ValueError, match=re.escape("hue values should be between (-0.5, 0.5)")):
+        transforms.ColorJitter(hue=hue)
+
+
 @pytest.mark.parametrize("seed", range(10))
 @pytest.mark.skipif(stats is None, reason="scipy.stats not available")
 def test_random_erasing(seed):
diff --git a/torchvision/prototype/transforms/_color.py b/torchvision/prototype/transforms/_color.py
index 0eb20e57764..e439e666678 100644
--- a/torchvision/prototype/transforms/_color.py
+++ b/torchvision/prototype/transforms/_color.py
@@ -42,12 +42,12 @@ def _check_input(
             value = [center - value, center + value]
             if clip_first_on_zero:
                 value[0] = max(value[0], 0.0)
-        elif isinstance(value, collections.abc.Sequence) and len(value) == 2:
-            if not bound[0] <= value[0] <= value[1] <= bound[1]:
-                raise ValueError(f"{name} values should be between {bound}")
-        else:
+        elif not (isinstance(value, collections.abc.Sequence) and len(value) == 2):
             raise TypeError(f"{name} should be a single number or a sequence with length 2.")
 
+        if not bound[0] <= value[0] <= value[1] <= bound[1]:
+            raise ValueError(f"{name} values should be between {bound}")
+
         return None if value[0] == value[1] == center else (float(value[0]), float(value[1]))
 
     @staticmethod
diff --git a/torchvision/transforms/transforms.py b/torchvision/transforms/transforms.py
index cb2bfdb92a8..88f709ee477 100644
--- a/torchvision/transforms/transforms.py
+++ b/torchvision/transforms/transforms.py
@@ -1195,16 +1195,19 @@ def _check_input(self, value, name, center=1, bound=(0, float("inf")), clip_firs
             if clip_first_on_zero:
                 value[0] = max(value[0], 0.0)
         elif isinstance(value, (tuple, list)) and len(value) == 2:
-            if not bound[0] <= value[0] <= value[1] <= bound[1]:
-                raise ValueError(f"{name} values should be between {bound}")
+            value = [float(value[0]), float(value[1])]
         else:
             raise TypeError(f"{name} should be a single number or a list/tuple with length 2.")
 
+        if not bound[0] <= value[0] <= value[1] <= bound[1]:
+            raise ValueError(f"{name} values should be between {bound}")
+
         # if value is 0 or (1., 1.) for brightness/contrast/saturation
         # or (0., 0.) for hue, do nothing
         if value[0] == value[1] == center:
-            value = None
-        return value
+            return None
+        else:
+            return tuple(value)
 
     @staticmethod
     def get_params(


Code review comments:
diff_hunk: @@ -42,12 +42,12 @@ def _check_input(
             value = [center - value, center + value]
             if clip_first_on_zero:
                 value[0] = max(value[0], 0.0)
-        elif isinstance(value, collections.abc.Sequence) and len(value) == 2:
-            if not bound[0] <= value[0] <= value[1] <= bound[1]:
-                raise ValueError(f"{name} values should be between {bound}")
-        else:
+        elif not (isinstance(value, collections.abc.Sequence) and len(value) == 2):
             raise TypeError(f"{name} should be a single number or a sequence with length 2.")
 
+        if not bound[0] <= value[0] <= value[1] <= bound[1]:
+            raise ValueError(f"{name} values should be between {bound}")
path: torchvision/prototype/transforms/_color.py
text: @user1:
indicate what we got in the error message?