diff --git a/docs/changelog.md b/docs/changelog.md index c9b55991..e7dbb18e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -97,7 +97,7 @@ detections = sv.Detections.from_sam(sam_result=sam_result) - Added [#1409](https://github.com/roboflow/supervision/pull/1409): `text_color` option for [`VertexLabelAnnotator`](https://supervision.roboflow.com/0.23.0/keypoint/annotators/#supervision.keypoint.annotators.VertexLabelAnnotator) keypoint annotator. -- Changed [#1434](https://github.com/roboflow/supervision/pull/1434): [`InferenceSlicer`](https://supervision.roboflow.com/0.23.0/detection/tools/inference_slicer/) now features an `overlap_ratio_wh` parameter, making it easier to compute slice sizes when handling overlapping slices. +- Changed [#1434](https://github.com/roboflow/supervision/pull/1434): [`InferenceSlicer`](https://supervision.roboflow.com/0.23.0/detection/tools/inference_slicer/) now features an `overlap_wh` parameter, making it easier to compute slice sizes when handling overlapping slices. - Fix [#1448](https://github.com/roboflow/supervision/pull/1448): Various annotator type issues have been resolved, supporting expanded error handling. diff --git a/supervision/detection/tools/inference_slicer.py b/supervision/detection/tools/inference_slicer.py index 52f45140..05469dd6 100644 --- a/supervision/detection/tools/inference_slicer.py +++ b/supervision/detection/tools/inference_slicer.py @@ -11,7 +11,6 @@ from supervision.detection.utils import move_boxes, move_masks, move_oriented_bo from supervision.utils.image import crop_image from supervision.utils.internal import ( SupervisionWarnings, - deprecated_parameter, warn_deprecated, ) @@ -60,13 +59,15 @@ class InferenceSlicer: Args: slice_wh (Tuple[int, int]): Dimensions of each slice measured in pixels. The tuple should be in the format `(width, height)`. - overlap_ratio_wh (Optional[Tuple[float, float]]): A tuple representing the + overlap_ratio_wh (Optional[Tuple[float, float]]): [⚠️ Deprecated: please set + to `None` and use `overlap_wh`] A tuple representing the desired overlap ratio for width and height between consecutive slices. Each value should be in the range [0, 1), where 0 means no overlap and a value close to 1 means high overlap. overlap_wh (Optional[Tuple[int, int]]): A tuple representing the desired overlap for width and height between consecutive slices measured in pixels. - Each value should be greater than or equal to 0. + Each value should be greater than or equal to 0. Takes precedence over + `overlap_ratio_wh`. overlap_filter (Union[OverlapFilter, str]): Strategy for filtering or merging overlapping detections in slices. iou_threshold (float): Intersection over Union (IoU) threshold @@ -82,14 +83,6 @@ class InferenceSlicer: not a multiple of the slice's width or height minus the overlap. """ - @deprecated_parameter( - old_parameter="overlap_filter_strategy", - new_parameter="overlap_filter", - map_function=lambda x: x, - warning_message="`{old_parameter}` in `{function_name}` is deprecated and will " - "be removed in `supervision-0.27.0`. Use '{new_parameter}' " - "instead.", - ) def __init__( self, callback: Callable[[np.ndarray], Detections], @@ -103,7 +96,8 @@ class InferenceSlicer: if overlap_ratio_wh is not None: warn_deprecated( "`overlap_ratio_wh` in `InferenceSlicer.__init__` is deprecated and " - "will be removed in `supervision-0.27.0`. Use `overlap_wh` instead." + "will be removed in `supervision-0.27.0`. Please manually set it to " + "`None` and use `overlap_wh` instead." ) self._validate_overlap(overlap_ratio_wh, overlap_wh) diff --git a/test/detection/tools/test_inference_slicer.py b/test/detection/tools/test_inference_slicer.py new file mode 100644 index 00000000..812e7941 --- /dev/null +++ b/test/detection/tools/test_inference_slicer.py @@ -0,0 +1,192 @@ +from contextlib import ExitStack as DoesNotRaise +from typing import Optional, Tuple + +import numpy as np +import pytest + +from supervision.detection.core import Detections +from supervision.detection.overlap_filter import OverlapFilter +from supervision.detection.tools.inference_slicer import InferenceSlicer + + +@pytest.fixture +def mock_callback(): + """Mock callback function for testing.""" + + def callback(_: np.ndarray) -> Detections: + return Detections(xyxy=np.array([[0, 0, 10, 10]])) + + return callback + + +@pytest.mark.parametrize( + "slice_wh, overlap_ratio_wh, overlap_wh, expected_overlap, exception", + [ + # Valid case: overlap_ratio_wh provided, overlap calculated from the ratio + ((128, 128), (0.2, 0.2), None, None, DoesNotRaise()), + # Valid case: overlap_wh in pixels, no ratio provided + ((128, 128), None, (20, 20), (20, 20), DoesNotRaise()), + # Invalid case: overlap_ratio_wh greater than 1, should raise ValueError + ((128, 128), (1.1, 0.5), None, None, pytest.raises(ValueError)), + # Invalid case: negative overlap_wh, should raise ValueError + ((128, 128), None, (-10, 20), None, pytest.raises(ValueError)), + # Invalid case: + # overlap_ratio_wh and overlap_wh provided, should raise ValueError + ((128, 128), (0.5, 0.5), (20, 20), (20, 20), pytest.raises(ValueError)), + # Valid case: no overlap_ratio_wh, overlap_wh = 50 pixels + ((256, 256), None, (50, 50), (50, 50), DoesNotRaise()), + # Valid case: overlap_ratio_wh provided, overlap calculated from (0.3, 0.3) + ((200, 200), (0.3, 0.3), None, None, DoesNotRaise()), + # Valid case: small overlap_ratio_wh values + ((100, 100), (0.1, 0.1), None, None, DoesNotRaise()), + # Invalid case: negative overlap_ratio_wh value, should raise ValueError + ((128, 128), (-0.1, 0.2), None, None, pytest.raises(ValueError)), + # Invalid case: negative overlap_ratio_wh with overlap_wh provided + ((128, 128), (-0.1, 0.2), (30, 30), None, pytest.raises(ValueError)), + # Invalid case: overlap_wh greater than slice size, should raise ValueError + ((128, 128), None, (150, 150), (150, 150), DoesNotRaise()), + # Valid case: overlap_ratio_wh is 0, no overlap + ((128, 128), (0.0, 0.0), None, None, DoesNotRaise()), + # Invalid case: no overlaps defined, no overlap + ((128, 128), None, None, None, pytest.raises(ValueError)), + ], +) +def test_inference_slicer_overlap( + mock_callback, + slice_wh: Tuple[int, int], + overlap_ratio_wh: Optional[Tuple[float, float]], + overlap_wh: Optional[Tuple[int, int]], + expected_overlap: Optional[Tuple[int, int]], + exception: Exception, +) -> None: + with exception: + slicer = InferenceSlicer( + callback=mock_callback, + slice_wh=slice_wh, + overlap_ratio_wh=overlap_ratio_wh, + overlap_wh=overlap_wh, + overlap_filter=OverlapFilter.NONE, + ) + assert slicer.overlap_wh == expected_overlap + + +@pytest.mark.parametrize( + "resolution_wh, slice_wh, overlap_wh, expected_offsets", + [ + # Case 1: No overlap, exact slices fit within image dimensions + ( + (256, 256), + (128, 128), + (0, 0), + np.array( + [ + [0, 0, 128, 128], + [128, 0, 256, 128], + [0, 128, 128, 256], + [128, 128, 256, 256], + ] + ), + ), + # Case 2: Overlap of 64 pixels in both directions + ( + (256, 256), + (128, 128), + (64, 64), + np.array( + [ + [0, 0, 128, 128], + [64, 0, 192, 128], + [128, 0, 256, 128], + [192, 0, 256, 128], + [0, 64, 128, 192], + [64, 64, 192, 192], + [128, 64, 256, 192], + [192, 64, 256, 192], + [0, 128, 128, 256], + [64, 128, 192, 256], + [128, 128, 256, 256], + [192, 128, 256, 256], + [0, 192, 128, 256], + [64, 192, 192, 256], + [128, 192, 256, 256], + [192, 192, 256, 256], + ] + ), + ), + # Case 3: Image not perfectly divisible by slice size (no overlap) + ( + (300, 300), + (128, 128), + (0, 0), + np.array( + [ + [0, 0, 128, 128], + [128, 0, 256, 128], + [256, 0, 300, 128], + [0, 128, 128, 256], + [128, 128, 256, 256], + [256, 128, 300, 256], + [0, 256, 128, 300], + [128, 256, 256, 300], + [256, 256, 300, 300], + ] + ), + ), + # Case 4: Overlap of 32 pixels, image not perfectly divisible by slice size + ( + (300, 300), + (128, 128), + (32, 32), + np.array( + [ + [0, 0, 128, 128], + [96, 0, 224, 128], + [192, 0, 300, 128], + [288, 0, 300, 128], + [0, 96, 128, 224], + [96, 96, 224, 224], + [192, 96, 300, 224], + [288, 96, 300, 224], + [0, 192, 128, 300], + [96, 192, 224, 300], + [192, 192, 300, 300], + [288, 192, 300, 300], + [0, 288, 128, 300], + [96, 288, 224, 300], + [192, 288, 300, 300], + [288, 288, 300, 300], + ] + ), + ), + # Case 5: Image smaller than slice size (no overlap) + ( + (100, 100), + (128, 128), + (0, 0), + np.array( + [ + [0, 0, 100, 100], + ] + ), + ), + # Case 6: Overlap_wh is greater than the slice size + ((256, 256), (128, 128), (150, 150), np.array([]).reshape(0, 4)), + ], +) +def test_generate_offset( + resolution_wh: Tuple[int, int], + slice_wh: Tuple[int, int], + overlap_wh: Optional[Tuple[int, int]], + expected_offsets: np.ndarray, +) -> None: + offsets = InferenceSlicer._generate_offset( + resolution_wh=resolution_wh, + slice_wh=slice_wh, + overlap_ratio_wh=None, + overlap_wh=overlap_wh, + ) + + # Verify that the generated offsets match the expected offsets + assert np.array_equal( + offsets, expected_offsets + ), f"Expected {expected_offsets}, got {offsets}"