From 7239af504831d70cd833510d9cee47fdd89e4039 Mon Sep 17 00:00:00 2001 From: Vikas saini <119332490+vikassaini77@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:21:01 +0530 Subject: [PATCH] Refactor/remove asserts annotators image (#2354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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> --- src/supervision/annotators/core.py | 1 - src/supervision/key_points/annotators.py | 25 ++++++++++++++++------ src/supervision/utils/conversion.py | 15 ++++++++++--- src/supervision/utils/image.py | 12 +++++++---- tests/key_points/test_annotators.py | 27 ++++++++++++++++++++++++ tests/utils/test_image.py | 19 +++++++++++++++++ 6 files changed, 85 insertions(+), 14 deletions(-) diff --git a/src/supervision/annotators/core.py b/src/supervision/annotators/core.py index 8336e11a..ae43fa1c 100644 --- a/src/supervision/annotators/core.py +++ b/src/supervision/annotators/core.py @@ -1616,7 +1616,6 @@ class RichLabelAnnotator(_BaseLabelAnnotator): ``` """ - assert isinstance(scene, Image.Image) _validate_labels(labels, detections) draw = ImageDraw.Draw(scene) diff --git a/src/supervision/key_points/annotators.py b/src/supervision/key_points/annotators.py index 63a2b7d1..02db1c9a 100644 --- a/src/supervision/key_points/annotators.py +++ b/src/supervision/key_points/annotators.py @@ -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 diff --git a/src/supervision/utils/conversion.py b/src/supervision/utils/conversion.py index 1ef2acba..741dc5e7 100644 --- a/src/supervision/utils/conversion.py +++ b/src/supervision/utils/conversion.py @@ -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) diff --git a/src/supervision/utils/image.py b/src/supervision/utils/image.py index a4768e94..e5cc5cfd 100644 --- a/src/supervision/utils/image.py +++ b/src/supervision/utils/image.py @@ -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") diff --git a/tests/key_points/test_annotators.py b/tests/key_points/test_annotators.py index 8121c027..d2eae10b 100644 --- a/tests/key_points/test_annotators.py +++ b/tests/key_points/test_annotators.py @@ -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) diff --git a/tests/utils/test_image.py b/tests/utils/test_image.py index 3ae413f8..1326081e 100644 --- a/tests/utils/test_image.py +++ b/tests/utils/test_image.py @@ -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)