diff --git a/test/test_transforms_v2.py b/test/test_transforms_v2.py index 1030032b980..06d6514770d 100644 --- a/test/test_transforms_v2.py +++ b/test/test_transforms_v2.py @@ -5526,7 +5526,7 @@ def test_correctness_image(self, mean, std, dtype, fn): class TestClampBoundingBoxes: @pytest.mark.parametrize("format", list(tv_tensors.BoundingBoxFormat)) - @pytest.mark.parametrize("clamping_mode", ("hard", None)) # TODOBB add soft + @pytest.mark.parametrize("clamping_mode", ("soft", "hard", None)) @pytest.mark.parametrize("dtype", [torch.int64, torch.float32]) @pytest.mark.parametrize("device", cpu_and_cuda()) def test_kernel(self, format, clamping_mode, dtype, device): @@ -5542,7 +5542,7 @@ def test_kernel(self, format, clamping_mode, dtype, device): ) @pytest.mark.parametrize("format", list(tv_tensors.BoundingBoxFormat)) - @pytest.mark.parametrize("clamping_mode", ("hard", None)) # TODOBB add soft + @pytest.mark.parametrize("clamping_mode", ("soft", "hard", None)) def test_functional(self, format, clamping_mode): check_functional(F.clamp_bounding_boxes, make_bounding_boxes(format=format, clamping_mode=clamping_mode)) @@ -5566,12 +5566,17 @@ def test_errors(self): ): F.clamp_bounding_boxes(input_tv_tensor, format=format_, canvas_size=canvas_size_) + with pytest.raises(ValueError, match="clamping_mode must be soft,"): + F.clamp_bounding_boxes(input_tv_tensor, clamping_mode="bad") + with pytest.raises(ValueError, match="clamping_mode must be soft,"): + transforms.ClampBoundingBoxes(clamping_mode="bad")(input_tv_tensor) + def test_transform(self): check_transform(transforms.ClampBoundingBoxes(), make_bounding_boxes()) @pytest.mark.parametrize("rotated", (True, False)) - @pytest.mark.parametrize("constructor_clamping_mode", ("hard", None)) - @pytest.mark.parametrize("clamping_mode", ("hard", None, "auto")) # TODOBB add soft here. + @pytest.mark.parametrize("constructor_clamping_mode", ("soft", "hard", None)) + @pytest.mark.parametrize("clamping_mode", ("soft", "hard", None, "auto")) @pytest.mark.parametrize("pass_pure_tensor", (True, False)) @pytest.mark.parametrize("fn", [F.clamp_bounding_boxes, transform_cls_to_functional(transforms.ClampBoundingBoxes)]) def test_clamping_mode(self, rotated, constructor_clamping_mode, clamping_mode, pass_pure_tensor, fn): @@ -5624,8 +5629,8 @@ def test_clamping_mode(self, rotated, constructor_clamping_mode, clamping_mode, class TestSetClampingMode: @pytest.mark.parametrize("format", list(tv_tensors.BoundingBoxFormat)) - @pytest.mark.parametrize("constructor_clamping_mode", ("hard", None)) # TODOBB add soft - @pytest.mark.parametrize("desired_clamping_mode", ("hard", None)) # TODOBB add soft + @pytest.mark.parametrize("constructor_clamping_mode", ("soft", "hard", None)) + @pytest.mark.parametrize("desired_clamping_mode", ("soft", "hard", None)) def test_setter(self, format, constructor_clamping_mode, desired_clamping_mode): in_boxes = make_bounding_boxes(format=format, clamping_mode=constructor_clamping_mode) @@ -5635,7 +5640,7 @@ def test_setter(self, format, constructor_clamping_mode, desired_clamping_mode): assert out_boxes.clamping_mode == desired_clamping_mode @pytest.mark.parametrize("format", list(tv_tensors.BoundingBoxFormat)) - @pytest.mark.parametrize("constructor_clamping_mode", ("hard", None)) # TODOBB add soft + @pytest.mark.parametrize("constructor_clamping_mode", ("soft", "hard", None)) def test_pipeline_no_leak(self, format, constructor_clamping_mode): class AssertClampingMode(transforms.Transform): def __init__(self, expected_clamping_mode): @@ -5669,6 +5674,10 @@ def transform(self, inpt, _): # ClampBoundingBoxes doesn't set clamping_mode. assert out_boxes.clamping_mode is None + def test_error(self): + with pytest.raises(ValueError, match="clamping_mode must be"): + transforms.SetClampingMode("bad") + class TestClampKeyPoints: @pytest.mark.parametrize("dtype", [torch.int64, torch.float32]) diff --git a/test/test_tv_tensors.py b/test/test_tv_tensors.py index 9fb1b9fd7ec..f9d545eb9c9 100644 --- a/test/test_tv_tensors.py +++ b/test/test_tv_tensors.py @@ -432,7 +432,7 @@ def test_return_type_input(): tv_tensors.set_return_type("tensor") -def test_box_clamping_mode_default(): +def test_box_clamping_mode_default_and_error(): assert ( tv_tensors.BoundingBoxes([0.0, 0.0, 10.0, 10.0], format="XYXY", canvas_size=(100, 100)).clamping_mode == "soft" ) @@ -440,3 +440,6 @@ def test_box_clamping_mode_default(): tv_tensors.BoundingBoxes([0.0, 0.0, 10.0, 10.0, 0.0], format="XYWHR", canvas_size=(100, 100)).clamping_mode == "soft" ) + + with pytest.raises(ValueError, match="clamping_mode must be"): + tv_tensors.BoundingBoxes([0, 0, 10, 10], format="XYXY", canvas_size=(100, 100), clamping_mode="bad") diff --git a/torchvision/transforms/v2/_meta.py b/torchvision/transforms/v2/_meta.py index 2b40f21392f..68395b468ba 100644 --- a/torchvision/transforms/v2/_meta.py +++ b/torchvision/transforms/v2/_meta.py @@ -34,8 +34,6 @@ class ClampBoundingBoxes(Transform): """ - # TODOBB consider "auto" to be a Literal, make sur torchscript is still happy - # TODOBB validate clamping_mode def __init__(self, clamping_mode: Union[CLAMPING_MODE_TYPE, str] = "auto") -> None: super().__init__() self.clamping_mode = clamping_mode @@ -63,9 +61,11 @@ class SetClampingMode(Transform): def __init__(self, clamping_mode: CLAMPING_MODE_TYPE) -> None: super().__init__() - # TODOBB validate mode self.clamping_mode = clamping_mode + if self.clamping_mode not in (None, "soft", "hard"): + raise ValueError(f"clamping_mode must be soft, hard or None, got {clamping_mode}") + _transformed_types = (tv_tensors.BoundingBoxes,) def transform(self, inpt: tv_tensors.BoundingBoxes, params: dict[str, Any]) -> tv_tensors.BoundingBoxes: diff --git a/torchvision/transforms/v2/functional/_meta.py b/torchvision/transforms/v2/functional/_meta.py index 434a0c79b15..6256a288203 100644 --- a/torchvision/transforms/v2/functional/_meta.py +++ b/torchvision/transforms/v2/functional/_meta.py @@ -640,6 +640,9 @@ def clamp_bounding_boxes( if not torch.jit.is_scripting(): _log_api_usage_once(clamp_bounding_boxes) + if clamping_mode is not None and clamping_mode not in ("soft", "hard", "auto"): + raise ValueError(f"clamping_mode must be soft, hard, auto or None, got {clamping_mode}") + if torch.jit.is_scripting() or is_pure_tensor(inpt): if format is None or canvas_size is None or (clamping_mode is not None and clamping_mode == "auto"): diff --git a/torchvision/tv_tensors/_bounding_boxes.py b/torchvision/tv_tensors/_bounding_boxes.py index e3c1032d0de..e4963192671 100644 --- a/torchvision/tv_tensors/_bounding_boxes.py +++ b/torchvision/tv_tensors/_bounding_boxes.py @@ -53,8 +53,7 @@ def is_rotated_bounding_format(format: BoundingBoxFormat | str) -> bool: raise ValueError(f"format should be str or BoundingBoxFormat, got {type(format)}") -# TODOBB consider making this a Literal instead. Tried briefly and got -# torchscript errors, leaving to str for now. +# This should ideally be a Literal, but torchscript fails. CLAMPING_MODE_TYPE = Optional[str] # TODOBB All docs. Add any new API to rst files, add tutorial[s]. @@ -96,12 +95,15 @@ def _wrap(cls, tensor: torch.Tensor, *, format: BoundingBoxFormat | str, canvas_ tensor = tensor.unsqueeze(0) elif tensor.ndim != 2: raise ValueError(f"Expected a 1D or 2D tensor, got {tensor.ndim}D") + if clamping_mode is not None and clamping_mode not in ("hard", "soft"): + raise ValueError(f"clamping_mode must be None, hard or soft, got {clamping_mode}.") + if isinstance(format, str): format = BoundingBoxFormat[format.upper()] + bounding_boxes = tensor.as_subclass(cls) bounding_boxes.format = format bounding_boxes.canvas_size = canvas_size - # TODOBB validate values bounding_boxes.clamping_mode = clamping_mode return bounding_boxes