Refactor/remove asserts annotators image (#2354)

- ValueError → TypeError in all three ensure_*_image_for_* decorators (conversion.py): the decorator intercepts non-ndarray/PIL inputs before the wrapped body runs, so the inner raises were unreachable; fixing at the decorator level fixes all annotators at once
- Remove 5 dead isinstance guards from key_points/annotators.py and 4 from utils/image.py (all now covered by the decorator fix)
- Remove dead assert isinstance(scene, Image.Image) from RichLabelAnnotator.annotate (ensure_pil_image_for_class_method guarantees PIL.Image before inner body)
- Add @ensure_cv2_image_for_class_method to VertexLabelAnnotator.annotate for PIL parity (only decorated annotator missing it)
- Rewrite tests to call public API directly (no __wrapped__ bypass); replace with TestAnnotatorInputValidation class (parametrized, IDs, AAA) + parametrized test_image_utils_wrong_type_raises
- Add Raises: TypeError sections to all 6 annotator .annotate() docstrings, 4 image util docstrings, and 3 decorator docstrings

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: jirka <6035284+Borda@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
This commit is contained in:
Vikas saini 2026-06-26 21:21:01 +05:30 committed by GitHub
parent 27ba0aa92f
commit 7239af5048
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 85 additions and 14 deletions

View File

@ -1616,7 +1616,6 @@ class RichLabelAnnotator(_BaseLabelAnnotator):
```
"""
assert isinstance(scene, Image.Image)
_validate_labels(labels, detections)
draw = ImageDraw.Draw(scene)

View File

@ -64,6 +64,9 @@ class VertexAnnotator(BaseKeyPointAnnotator):
The annotated image, matching the type of `scene` (`numpy.ndarray`
or `PIL.Image.Image`)
Raises:
TypeError: If `scene` is not a `numpy.ndarray` or `PIL.Image.Image`.
Example:
```pycon
>>> import numpy as np
@ -84,7 +87,6 @@ class VertexAnnotator(BaseKeyPointAnnotator):
```
"""
assert isinstance(scene, np.ndarray)
if len(key_points) == 0:
return scene
@ -154,6 +156,9 @@ class EdgeAnnotator(BaseKeyPointAnnotator):
The annotated image, matching the type of `scene` (`numpy.ndarray`
or `PIL.Image.Image`)
Raises:
TypeError: If `scene` is not a `numpy.ndarray` or `PIL.Image.Image`.
Example:
Single-skeleton example:
@ -205,7 +210,6 @@ class EdgeAnnotator(BaseKeyPointAnnotator):
```
"""
assert isinstance(scene, np.ndarray)
if len(key_points) == 0:
return scene
@ -416,6 +420,9 @@ class VertexEllipseAreaAnnotator(_BaseVertexEllipseAnnotator):
Returns:
The annotated image, matching the type of ``scene``.
Raises:
TypeError: If `scene` is not a `numpy.ndarray` or `PIL.Image.Image`.
Example:
```pycon
>>> import numpy as np
@ -445,7 +452,6 @@ class VertexEllipseAreaAnnotator(_BaseVertexEllipseAnnotator):
```
"""
assert isinstance(scene, np.ndarray)
if len(key_points) == 0:
return scene
@ -514,6 +520,9 @@ class VertexEllipseOutlineAnnotator(_BaseVertexEllipseAnnotator):
Returns:
The annotated image, matching the type of ``scene``.
Raises:
TypeError: If `scene` is not a `numpy.ndarray` or `PIL.Image.Image`.
Example:
```pycon
>>> import numpy as np
@ -544,7 +553,6 @@ class VertexEllipseOutlineAnnotator(_BaseVertexEllipseAnnotator):
```
"""
assert isinstance(scene, np.ndarray)
if len(key_points) == 0:
return scene
@ -616,6 +624,9 @@ class VertexEllipseHaloAnnotator(_BaseVertexEllipseAnnotator):
Returns:
The annotated image, matching the type of ``scene``.
Raises:
TypeError: If `scene` is not a `numpy.ndarray` or `PIL.Image.Image`.
Example:
```pycon
>>> import numpy as np
@ -645,7 +656,6 @@ class VertexEllipseHaloAnnotator(_BaseVertexEllipseAnnotator):
```
"""
assert isinstance(scene, np.ndarray)
if len(key_points) == 0:
return scene
@ -737,6 +747,7 @@ class VertexLabelAnnotator:
self.text_padding: int = text_padding
self.smart_position = smart_position
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType,
@ -762,6 +773,9 @@ class VertexLabelAnnotator:
The annotated image, matching the type of `scene` (`numpy.ndarray`
or `PIL.Image.Image`)
Raises:
TypeError: If `scene` is not a `numpy.ndarray` or `PIL.Image.Image`.
Example:
Single-skeleton example:
@ -824,7 +838,6 @@ class VertexLabelAnnotator:
```
"""
assert isinstance(scene, np.ndarray)
font = cv2.FONT_HERSHEY_SIMPLEX
skeletons_count, points_count, _ = key_points.xy.shape

View File

@ -22,6 +22,9 @@ def ensure_cv2_image_for_class_method(
is complete.
Assumes the annotators modify the scene in-place.
Raises:
TypeError: If `scene` is not a `numpy.ndarray` or `PIL.Image.Image`.
"""
@functools.wraps(annotate_func)
@ -35,7 +38,7 @@ def ensure_cv2_image_for_class_method(
scene.paste(cv2_to_pillow(annotated_np))
return scene
raise ValueError(f"Unsupported image type: {type(scene)}")
raise TypeError(f"Unsupported image type: {type(scene)}")
return cast(F, wrapper)
@ -59,6 +62,9 @@ def ensure_cv2_image_for_standalone_function(
np.ndarray, converts back when processing is complete.
Assumes the annotators do NOT modify the scene in-place.
Raises:
TypeError: If `image` is not a `numpy.ndarray` or `PIL.Image.Image`.
"""
@functools.wraps(image_processing_fun)
@ -71,7 +77,7 @@ def ensure_cv2_image_for_standalone_function(
annotated = image_processing_fun(scene, *args, **kwargs)
return cv2_to_pillow(annotated)
raise ValueError(f"Unsupported image type: {type(image)}")
raise TypeError(f"Unsupported image type: {type(image)}")
return cast(F, wrapper)
@ -84,6 +90,9 @@ def ensure_pil_image_for_class_method(
PIL image, converts back when processing is complete.
Assumes the annotators modify the scene in-place.
Raises:
TypeError: If `scene` is not a `numpy.ndarray` or `PIL.Image.Image`.
"""
@functools.wraps(annotate_func)
@ -97,7 +106,7 @@ def ensure_pil_image_for_class_method(
if isinstance(scene, Image.Image):
return cast(ImageType, annotate_func(self, scene, *args, **kwargs))
raise ValueError(f"Unsupported image type: {type(scene)}")
raise TypeError(f"Unsupported image type: {type(scene)}")
return cast(F, wrapper)

View File

@ -105,6 +105,7 @@ def scale_image(image: ImageType, scale_factor: float) -> ImageType:
type.
Raises:
TypeError: If `image` is not a `numpy.ndarray` or `PIL.Image.Image`.
ValueError: If scale factor is non-positive.
Examples:
@ -132,7 +133,6 @@ def scale_image(image: ImageType, scale_factor: float) -> ImageType:
![scale-image](https://media.roboflow.com/supervision-docs/supervision-docs-scale-image-2.png){ align=center width="1000" }
""" # noqa E501 // docs
assert isinstance(image, np.ndarray)
if scale_factor <= 0:
raise ValueError("Scale factor must be positive.")
@ -161,6 +161,9 @@ def resize_image(
Resized image matching input
type.
Raises:
TypeError: If `image` is not a `numpy.ndarray` or `PIL.Image.Image`.
Examples:
```pycon
>>> import numpy as np
@ -190,7 +193,6 @@ def resize_image(
![resize-image](https://media.roboflow.com/supervision-docs/supervision-docs-resize-image-2.png){ align=center width="1000" }
""" # noqa E501 // docs
assert isinstance(image, np.ndarray)
if keep_aspect_ratio:
image_ratio = image.shape[1] / image.shape[0]
target_ratio = resolution_wh[0] / resolution_wh[1]
@ -227,6 +229,9 @@ def letterbox_image(
Returns:
Letterboxed image matching input type.
Raises:
TypeError: If `image` is not a `numpy.ndarray` or `PIL.Image.Image`.
Note:
For BGRA inputs, the alpha channel in the padding region is set to
0 (fully transparent). Grayscale inputs receive scalar padding
@ -252,7 +257,6 @@ def letterbox_image(
![letterbox-image](https://media.roboflow.com/supervision-docs/supervision-docs-letterbox-image-2.png){ align=center width="1000" }
""" # noqa E501 // docs
assert isinstance(image, np.ndarray)
color = unify_to_bgr(color=color)
resized_image = resize_image(
image=image, resolution_wh=resolution_wh, keep_aspect_ratio=True
@ -372,6 +376,7 @@ def tint_image(
type.
Raises:
TypeError: If `image` is not a `numpy.ndarray` or `PIL.Image.Image`.
ValueError: If opacity is outside range [0.0, 1.0].
Examples:
@ -389,7 +394,6 @@ def tint_image(
![tint-image](https://media.roboflow.com/supervision-docs/supervision-docs-tint-image-2.png){ align=center width="1000" }
""" # noqa E501 // docs
assert isinstance(image, np.ndarray)
if not 0.0 <= opacity <= 1.0:
raise ValueError("opacity must be between 0.0 and 1.0")

View File

@ -502,3 +502,30 @@ class TestVertexLabelAnnotator:
def test_resolve_color_list_wrong_length_raises(self, colors, points_count):
with pytest.raises(ValueError, match="Number of colors"):
sv.VertexLabelAnnotator._resolve_color_list(colors, points_count)
class TestAnnotatorInputValidation:
"""Verify that all keypoint annotators reject invalid scene types."""
@pytest.mark.parametrize(
("annotator_class", "kwargs"),
[
pytest.param(sv.VertexAnnotator, {}, id="VertexAnnotator"),
pytest.param(sv.EdgeAnnotator, {}, id="EdgeAnnotator"),
pytest.param(sv.VertexEllipseAnnotator, {}, id="VertexEllipseAnnotator"),
pytest.param(
sv.VertexEllipseOutlineAnnotator, {}, id="VertexEllipseOutlineAnnotator"
),
pytest.param(
sv.VertexEllipseHaloAnnotator, {}, id="VertexEllipseHaloAnnotator"
),
pytest.param(sv.VertexLabelAnnotator, {}, id="VertexLabelAnnotator"),
],
)
def test_annotate_wrong_scene_type_raises(
self, annotator_class, kwargs, sample_key_points
):
"""Wrong scene type raises TypeError."""
annotator = annotator_class(**kwargs)
with pytest.raises(TypeError, match="Unsupported image type"):
annotator.annotate(scene="not_an_image", key_points=sample_key_points)

View File

@ -7,6 +7,8 @@ from supervision.utils.image import (
get_image_resolution_wh,
letterbox_image,
resize_image,
scale_image,
tint_image,
)
@ -193,3 +195,20 @@ def test_crop_image(image, xyxy, expected_size):
def test_get_image_resolution_wh(image, expected):
resolution = get_image_resolution_wh(image)
assert resolution == expected
@pytest.mark.parametrize(
("func", "kwargs"),
[
pytest.param(scale_image, {"scale_factor": 1.0}, id="scale_image"),
pytest.param(resize_image, {"resolution_wh": (10, 10)}, id="resize_image"),
pytest.param(
letterbox_image, {"resolution_wh": (10, 10)}, id="letterbox_image"
),
pytest.param(tint_image, {}, id="tint_image"),
],
)
def test_image_utils_wrong_type_raises(func, kwargs):
"""Wrong image type raises TypeError via decorator."""
with pytest.raises(TypeError, match="Unsupported image type"):
func(image="not_an_image", **kwargs)