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:
parent
27ba0aa92f
commit
7239af5048
|
|
@ -1616,7 +1616,6 @@ class RichLabelAnnotator(_BaseLabelAnnotator):
|
|||
|
||||
```
|
||||
"""
|
||||
assert isinstance(scene, Image.Image)
|
||||
_validate_labels(labels, detections)
|
||||
|
||||
draw = ImageDraw.Draw(scene)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
|||
|
||||
{ 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(
|
|||
|
||||
{ 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(
|
|||
|
||||
{ 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(
|
|||
|
||||
{ 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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue