Repository: roboflow/supervision (python)

Issue:
PixelateAnnotator throws errors if the area is too small, BlurAnnotator is not censoring if area is too large

### Search before asking

- [X] I have searched the Supervision [issues](https://github.com/roboflow/supervision/issues) and found no similar bug report.


### Bug

When using PixelateAnnotator with no additional configuration, it throws an error if the area to pixelate is too small:
```
Traceback (most recent call last):
  File "/home/ultra/clean.py", line 83, in <module>
    annotated_frame = blur_annotator.annotate(
  File "/opt/conda/lib/python3.10/site-packages/supervision/annotators/core.py", line 1253, in annotate
    scaled_up_roi = cv2.resize(
cv2.error: OpenCV(4.8.1) /io/opencv/modules/imgproc/src/resize.cpp:4068: error: (-215:Assertion failed) !dsize.empty() in function 'resize'
```

I added this debug to [this section](https://github.com/roboflow/supervision/blob/develop/supervision/annotators/core.py#L1252)
```py
    print(f"Box: {(x1, y1, x2, y2)}")
    roi = scene[y1:y2, x1:x2]
    print(f"ROI shape: {roi.shape}")
```
The last output before the error is:
Box: (644, 444, 678, 453)
ROI shape: (9, 34, 3)
I left the pixel size at the default (10), so I'm guessing the 9 is too small for it
As seen in the below code, I made this adjustment to automatically pick the largest possible pixel size and half it by 2, this works very well. 
```py
self.pixel_size = min(y2 - y1, x2 - x1) / 2

roi = scene[y1:y2, x1:x2]


scaled_down_roi = cv2.resize(
    src=roi, dsize=None, fx=self.pixel_size, fy=self.pixel_size
)
```

This approach would also resolve an issue with the BlurAnnotator.
If the kernel size set to the default, and the area is large, the resulting area is still very identifiable.

Maybe we can resolve this by having the option to provide a lambda instead of a fixed number, so the user can dynamically decide how large the used kernel/pixel size should be.
If that's something worth implementing in the project, I would be happy to create a PR.





### Environment

- Supervision 0.17.0 & 0.17.1
hardware not relevant

### Minimal Reproducible Example

_No response_

### Additional

_No response_

### Are you willing to submit a PR?

- [X] Yes I'd like to help by submitting a PR!


Patch:
diff --git a/src/supervision/annotators/core.py b/src/supervision/annotators/core.py
index a2f729b31b..52a77a3f54 100644
--- a/src/supervision/annotators/core.py
+++ b/src/supervision/annotators/core.py
@@ -15,6 +15,8 @@
     PENDING_TRACK_ID,
     ColorLookup,
     Trace,
+    calculate_dynamic_kernel_size,
+    calculate_dynamic_pixel_size,
     get_labels_text,
     hex_to_rgba,
     resolve_color,
@@ -1841,12 +1843,16 @@ class BlurAnnotator(BaseAnnotator):
     A class for blurring regions in an image using provided detections.
     """
 
-    def __init__(self, kernel_size: int = 15):
+    def __init__(self, kernel_size: int | None = None):
         """
         Args:
             kernel_size: The size of the average pooling kernel used for blurring.
+                If not set, a dynamic size is computed as one-third of the shorter
+                bounding-box dimension. Must be >= 1 when provided.
         """
-        self.kernel_size: int = kernel_size
+        if kernel_size is not None and kernel_size < 1:
+            raise ValueError(f"kernel_size must be >= 1, got {kernel_size}.")
+        self.kernel_size: int | None = kernel_size
 
     @ensure_cv2_image_for_class_method
     def annotate(
@@ -1895,8 +1901,15 @@ def annotate(
         ).astype(int)
 
         for x1, y1, x2, y2 in clipped_xyxy:
+            if x2 <= x1 or y2 <= y1:
+                continue
             roi = scene[y1:y2, x1:x2]
-            roi = cv2.blur(roi, (self.kernel_size, self.kernel_size))
+            kernel_size = (
+                self.kernel_size
+                if self.kernel_size is not None
+                else calculate_dynamic_kernel_size(x1, y1, x2, y2)
+            )
+            roi = cv2.blur(roi, (kernel_size, kernel_size))
             scene[y1:y2, x1:x2] = roi
 
         return scene
@@ -2145,12 +2158,18 @@ class PixelateAnnotator(BaseAnnotator):
     A class for pixelating regions in an image using provided detections.
     """
 
-    def __init__(self, pixel_size: int = 20):
+    def __init__(self, pixel_size: int | None = None):
         """
         Args:
-            pixel_size: The size of the pixelation.
+            pixel_size: The size of the pixelation. If not set, a dynamic size is
+                computed as one-half of the shorter bounding-box dimension. When set
+                and the detection area is smaller than `pixel_size`, the region is
+                filled with its average colour instead to avoid an OpenCV crash.
+                Must be >= 1 when provided.
         """
-        self.pixel_size: int = pixel_size
+        if pixel_size is not None and pixel_size < 1:
+            raise ValueError(f"pixel_size must be >= 1, got {pixel_size}.")
+        self.pixel_size: int | None = pixel_size
 
     @ensure_cv2_image_for_class_method
     def annotate(
@@ -2197,9 +2216,25 @@ def annotate(
         ).astype(int)
 
         for x1, y1, x2, y2 in clipped_xyxy:
+            if x2 <= x1 or y2 <= y1:
+                continue
             roi = scene[y1:y2, x1:x2]
+
+            pixel_size = (
+                self.pixel_size
+                if self.pixel_size is not None
+                else calculate_dynamic_pixel_size(x1, y1, x2, y2)
+            )
+            if min(y2 - y1, x2 - x1) < pixel_size:
+                if roi.ndim == 2 or (roi.ndim == 3 and roi.shape[2] == 1):
+                    scene[y1:y2, x1:x2] = cv2.mean(roi)[0]
+                else:
+                    num_channels = scene.shape[2]
+                    scene[y1:y2, x1:x2] = cv2.mean(roi)[:num_channels]
+                continue
+
             scaled_up_roi = cv2.resize(
-                src=roi, dsize=None, fx=1 / self.pixel_size, fy=1 / self.pixel_size
+                src=roi, dsize=None, fx=1 / pixel_size, fy=1 / pixel_size
             )
             scaled_down_roi = cv2.resize(
                 src=scaled_up_roi,
diff --git a/src/supervision/annotators/utils.py b/src/supervision/annotators/utils.py
index 2efc053a26..cfeaf98194 100644
--- a/src/supervision/annotators/utils.py
+++ b/src/supervision/annotators/utils.py
@@ -429,3 +429,49 @@ def is_valid_hex(hex_color: str) -> bool:
         True if the string is a valid 6- or 8-digit hex color, otherwise False.
     """
     return bool(re.fullmatch(r"#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?", hex_color.strip()))
+
+
+def calculate_dynamic_kernel_size(x1: int, y1: int, x2: int, y2: int) -> int:
+    """
+    Computes a blur kernel size proportional to the shorter side of a bounding box.
+
+    Args:
+        x1: Left edge of the bounding box.
+        y1: Top edge of the bounding box.
+        x2: Right edge of the bounding box.
+        y2: Bottom edge of the bounding box.
+
+    Returns:
+        Kernel size as one-third of the shorter dimension, minimum 1.
+
+    Examples:
+        ```pycon
+        >>> calculate_dynamic_kernel_size(0, 0, 90, 60)
+        20
+
+        ```
+    """
+    return max(1, min(y2 - y1, x2 - x1) // 3)
+
+
+def calculate_dynamic_pixel_size(x1: int, y1: int, x2: int, y2: int) -> int:
+    """
+    Computes a pixelation size proportional to the shorter side of a bounding box.
+
+    Args:
+        x1: Left edge of the bounding box.
+        y1: Top edge of the bounding box.
+        x2: Right edge of the bounding box.
+        y2: Bottom edge of the bounding box.
+
+    Returns:
+        Pixel size as one-half of the shorter dimension, minimum 1.
+
+    Examples:
+        ```pycon
+        >>> calculate_dynamic_pixel_size(0, 0, 90, 60)
+        30
+
+        ```
+    """
+    return max(1, min(y2 - y1, x2 - x1) // 2)


Test patch:
diff --git a/tests/annotators/test_core.py b/tests/annotators/test_core.py
index 7614943f8b..157213381e 100644
--- a/tests/annotators/test_core.py
+++ b/tests/annotators/test_core.py
@@ -504,6 +504,19 @@ def test_annotate_with_single_detection(self, gradient_image):
         result = annotator.annotate(scene=gradient_image.copy(), detections=detections)
         assert not np.array_equal(gradient_image, result)
 
+    @pytest.mark.parametrize("bad_size", [0, -1, -10])
+    def test_invalid_kernel_size_raises(self, bad_size):
+        """BlurAnnotator must reject kernel_size < 1 at construction time."""
+        with pytest.raises(ValueError, match="kernel_size must be >= 1"):
+            BlurAnnotator(kernel_size=bad_size)
+
+    def test_annotate_zero_area_bbox_is_skipped(self, test_image):
+        """Zero-area bounding boxes must be silently skipped, not crash."""
+        detections = _create_detections(xyxy=[[10, 10, 10, 50]], class_id=[0])
+        annotator = BlurAnnotator(kernel_size=5)
+        result = annotator.annotate(scene=test_image.copy(), detections=detections)
+        assert np.array_equal(test_image, result)
+
 
 class TestPixelateAnnotator:
     """Tests for PixelateAnnotator class"""
@@ -522,6 +535,57 @@ def test_annotate_with_single_detection(self, gradient_image):
         result = annotator.annotate(scene=gradient_image.copy(), detections=detections)
         assert not np.array_equal(gradient_image, result)
 
+    def test_annotate_bbox_smaller_than_pixel_size_does_not_raise(self):
+        """PixelateAnnotator must not crash when the bbox is smaller than pixel_size.
+
+        Regression test for https://github.com/roboflow/supervision/issues/703:
+        a fixed pixel_size larger than the detection dimensions previously caused
+        an OpenCV assertion error in cv2.resize.
+        """
+        image = np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8)
+        # bbox is 5x5; pixel_size=50 is much larger, triggers the avg-fill fallback
+        detections = _create_detections(xyxy=[[10, 10, 15, 15]], class_id=[0])
+        annotator = PixelateAnnotator(pixel_size=50)
+        result = annotator.annotate(scene=image.copy(), detections=detections)
+        assert result.shape == image.shape
+
+    def test_annotate_grayscale_image_does_not_raise(self):
+        """PixelateAnnotator must work on single-channel (grayscale) images.
+
+        The small-ROI avg-fill branch previously sliced cv2.mean()[:3] into a
+        2-D array, causing a NumPy broadcast error on grayscale frames.
+        """
+        gray = np.random.randint(0, 255, (100, 100), dtype=np.uint8)
+        # Normal-size detection — exercises the resize path on a grayscale frame
+        detections = _create_detections(xyxy=[[10, 10, 90, 90]], class_id=[0])
+        annotator = PixelateAnnotator(pixel_size=10)
+        result = annotator.annotate(scene=gray.copy(), detections=detections)
+        assert result.shape == gray.shape
+
+    def test_annotate_grayscale_image_small_roi_does_not_raise(self):
+        """Grayscale image with bbox smaller than pixel_size uses scalar avg fill.
+
+        Exercises the ndim-aware branch added to the small-ROI fallback.
+        """
+        gray = np.random.randint(0, 255, (100, 100), dtype=np.uint8)
+        detections = _create_detections(xyxy=[[10, 10, 15, 15]], class_id=[0])
+        annotator = PixelateAnnotator(pixel_size=50)
+        result = annotator.annotate(scene=gray.copy(), detections=detections)
+        assert result.shape == gray.shape
+
+    @pytest.mark.parametrize("bad_size", [0, -1, -10])
+    def test_invalid_pixel_size_raises(self, bad_size):
+        """PixelateAnnotator must reject pixel_size < 1 at construction time."""
+        with pytest.raises(ValueError, match="pixel_size must be >= 1"):
+            PixelateAnnotator(pixel_size=bad_size)
+
+    def test_annotate_zero_area_bbox_is_skipped(self, test_image):
+        """Zero-area bounding boxes must be silently skipped, not crash."""
+        detections = _create_detections(xyxy=[[10, 10, 10, 50]], class_id=[0])
+        annotator = PixelateAnnotator(pixel_size=5)
+        result = annotator.annotate(scene=test_image.copy(), detections=detections)
+        assert np.array_equal(test_image, result)
+
 
 class TestTriangleAnnotator:
     """Tests for TriangleAnnotator class"""