Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1393691a4 | ||
|
|
90f8582e4f | ||
|
|
0c9fea4d9f | ||
|
|
cdca2e6b51 | ||
|
|
8407d5a692 | ||
|
|
eaf6cdd477 | ||
|
|
f447b6fa95 | ||
|
|
8c6ac2dd29 | ||
|
|
22b6d3d464 | ||
|
|
dda3900943 | ||
|
|
ac0c367e81 | ||
|
|
6f2490cc23 | ||
|
|
4a9d06eb50 | ||
|
|
21e311a319 | ||
|
|
71a14812fe | ||
|
|
35fdcb0a1a | ||
|
|
6d26a9766c | ||
|
|
1cfaef9405 | ||
|
|
063e2092c8 | ||
|
|
cb215d26f3 | ||
|
|
8bbe80e683 | ||
|
|
9a0483446a | ||
|
|
f4561f9ba7 | ||
|
|
beaf89cb22 | ||
|
|
96b58c2860 | ||
|
|
8873da4a30 | ||
|
|
1362850986 | ||
|
|
98a4391677 | ||
|
|
036a956473 | ||
|
|
b381fa198f | ||
|
|
8ca2e9fe8d | ||
|
|
bc1d73a307 | ||
|
|
9679cd5c4e | ||
|
|
00817df9e2 | ||
|
|
ec189bf388 | ||
|
|
58bdce5707 | ||
|
|
8262b6e64b | ||
|
|
72fe0bf47f | ||
|
|
137078602b | ||
|
|
8fa21dd165 | ||
|
|
da70389546 | ||
|
|
cc1525707a | ||
|
|
b3aee43c3b | ||
|
|
932c1aa91e | ||
|
|
76ee05420d | ||
|
|
a470bc2976 | ||
|
|
cf725dd4db | ||
|
|
b1780fbae5 | ||
|
|
1e84351d60 | ||
|
|
535cb2a91b | ||
|
|
8e845caeeb | ||
|
|
9e2a535044 | ||
|
|
f737782ae8 |
@@ -34,7 +34,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🐍 Install uv and set Python ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
activate-environment: true
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
activate-environment: true
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
activate-environment: true
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
activate-environment: true
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🐍 Install uv and set Python ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
activate-environment: true
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
activate-environment: true
|
||||
|
||||
@@ -32,7 +32,7 @@ repos:
|
||||
additional_dependencies: ["bandit[toml]"]
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.12.3
|
||||
rev: v0.12.5
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# VLMs Utils
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.vlms.edit_distance">edit_distance</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.utils.vlms.edit_distance
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.vlms.fuzzy_match_index">fuzzy_match_index</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.utils.vlms.fuzzy_match_index
|
||||
@@ -345,7 +345,7 @@ Supervision is versatile and compatible with various models. Check this [link](/
|
||||
|
||||
We will define a `callback` function, which will process each frame of the video by obtaining model predictions and then annotating the frame based on these predictions.
|
||||
|
||||
Let's immediately visualize the results with our [`EdgeAnnotator`](/latest/keypoint/annotators/#supervision.keypoint.annotators.EdgeAnnotator) and [`VertexAnnotator`](https://supervision.roboflow.com/latest/keypoint/annotators/#supervision.keypoint.annotators.VertexAnnotator).
|
||||
Let's immediately visualize the results with our [`EdgeAnnotator`](/latest/keypoint/annotators/#supervision.key_points.annotators.EdgeAnnotator) and [`VertexAnnotator`](https://supervision.roboflow.com/latest/keypoint/annotators/#supervision.key_points.annotators.VertexAnnotator).
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
@@ -408,7 +408,7 @@ Let's immediately visualize the results with our [`EdgeAnnotator`](/latest/keypo
|
||||
|
||||
### Convert to Detections
|
||||
|
||||
Keypoint tracking is currently supported via the conversion of `KeyPoints` to `Detections`. This is achieved with the [`KeyPoints.as_detections()`](/latest/keypoint/core/#supervision.keypoint.core.KeyPoints.as_detections) function.
|
||||
Keypoint tracking is currently supported via the conversion of `KeyPoints` to `Detections`. This is achieved with the [`KeyPoints.as_detections()`](/latest/keypoint/core/#supervision.key_points.core.KeyPoints.as_detections) function.
|
||||
|
||||
Let's convert to detections and visualize the results with our [`BoxAnnotator`](/latest/detection/annotators/#supervision.annotators.core.BoxAnnotator).
|
||||
|
||||
|
||||
@@ -78,19 +78,19 @@ comments: true
|
||||
</div>
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.keypoint.annotators.VertexAnnotator">VertexAnnotator</a></h2>
|
||||
<h2><a href="#supervision.key_points.annotators.VertexAnnotator">VertexAnnotator</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.keypoint.annotators.VertexAnnotator
|
||||
:::supervision.key_points.annotators.VertexAnnotator
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.keypoint.annotators.EdgeAnnotator">EdgeAnnotator</a></h2>
|
||||
<h2><a href="#supervision.key_points.annotators.EdgeAnnotator">EdgeAnnotator</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.keypoint.annotators.EdgeAnnotator
|
||||
:::supervision.key_points.annotators.EdgeAnnotator
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.keypoint.annotators.VertexLabelAnnotator">VertexLabelAnnotator</a></h2>
|
||||
<h2><a href="#supervision.key_points.annotators.VertexLabelAnnotator">VertexLabelAnnotator</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.keypoint.annotators.VertexLabelAnnotator
|
||||
:::supervision.key_points.annotators.VertexLabelAnnotator
|
||||
|
||||
@@ -5,4 +5,4 @@ status: new
|
||||
|
||||
# Keypoint Detection
|
||||
|
||||
:::supervision.keypoint.core.KeyPoints
|
||||
:::supervision.key_points.core.KeyPoints
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
supervision
|
||||
ultralytics
|
||||
inference
|
||||
pytube
|
||||
# https://github.com/pytube/pytube/issues/2044
|
||||
# pytube
|
||||
pytubefix
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from pytube import YouTube
|
||||
from pytubefix import YouTube
|
||||
|
||||
|
||||
def main(url: str, output_path: str | None, file_name: str | None) -> None:
|
||||
|
||||
@@ -47,6 +47,7 @@ nav:
|
||||
- Boxes: detection/utils/boxes.md
|
||||
- Masks: detection/utils/masks.md
|
||||
- Polygons: detection/utils/polygons.md
|
||||
- VLMs: detection/utils/vlms.md
|
||||
- Keypoint Detection:
|
||||
- Core: keypoint/core.md
|
||||
- Annotators: keypoint/annotators.md
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
name = "supervision"
|
||||
description = "A set of easy-to-use utils that will come in handy in any Computer Vision project"
|
||||
license = { text = "MIT" }
|
||||
version = "0.26.1"
|
||||
version = "0.27.0rc0"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
authors = [
|
||||
|
||||
@@ -92,6 +92,7 @@ from supervision.detection.utils.polygons import (
|
||||
approximate_polygon,
|
||||
filter_polygons_by_area,
|
||||
)
|
||||
from supervision.detection.utils.vlms import edit_distance, fuzzy_match_index
|
||||
from supervision.detection.vlm import LMM, VLM
|
||||
from supervision.draw.color import Color, ColorPalette
|
||||
from supervision.draw.utils import (
|
||||
@@ -107,12 +108,12 @@ from supervision.draw.utils import (
|
||||
)
|
||||
from supervision.geometry.core import Point, Position, Rect
|
||||
from supervision.geometry.utils import get_polygon_center
|
||||
from supervision.keypoint.annotators import (
|
||||
from supervision.key_points.annotators import (
|
||||
EdgeAnnotator,
|
||||
VertexAnnotator,
|
||||
VertexLabelAnnotator,
|
||||
)
|
||||
from supervision.keypoint.core import KeyPoints
|
||||
from supervision.key_points.core import KeyPoints
|
||||
from supervision.metrics.detection import ConfusionMatrix, MeanAveragePrecision
|
||||
from supervision.tracker.byte_tracker.core import ByteTrack
|
||||
from supervision.utils.conversion import cv2_to_pillow, pillow_to_cv2
|
||||
@@ -215,7 +216,9 @@ __all__ = [
|
||||
"draw_polygon",
|
||||
"draw_rectangle",
|
||||
"draw_text",
|
||||
"edit_distance",
|
||||
"filter_polygons_by_area",
|
||||
"fuzzy_match_index",
|
||||
"get_coco_class_index_mapping",
|
||||
"get_polygon_center",
|
||||
"get_video_frames_generator",
|
||||
|
||||
+176
-25
@@ -7,6 +7,7 @@ import cv2
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from scipy.interpolate import splev, splprep
|
||||
|
||||
from supervision.annotators.base import BaseAnnotator, ImageType
|
||||
from supervision.annotators.utils import (
|
||||
@@ -51,25 +52,28 @@ class _BaseLabelAnnotator(BaseAnnotator):
|
||||
|
||||
Attributes:
|
||||
color (Union[Color, ColorPalette]): The color to use for the label background.
|
||||
color_lookup (ColorLookup): The method used to determine the color of the label.
|
||||
text_color (Union[Color, ColorPalette]): The color to use for the label text.
|
||||
text_padding (int): The padding around the label text, in pixels.
|
||||
text_anchor (Position): The position of the text relative to the detection
|
||||
bounding box.
|
||||
color_lookup (ColorLookup): The method used to determine the color of the label.
|
||||
bounding box.
|
||||
text_offset (Tuple[int, int]): A tuple of 2D coordinates `(x, y)` to
|
||||
offset the text position from the anchor point, in pixels.
|
||||
border_radius (int): The radius of the label background corners, in pixels.
|
||||
smart_position (bool): Whether to intelligently adjust the label position to
|
||||
avoid overlapping with other elements.
|
||||
avoid overlapping with other elements.
|
||||
max_line_length (Optional[int]): Maximum number of characters per line before
|
||||
wrapping the text. None means no wrapping.
|
||||
wrapping the text. None means no wrapping.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
color: Color | ColorPalette = ColorPalette.DEFAULT,
|
||||
color_lookup: ColorLookup = ColorLookup.CLASS,
|
||||
text_color: Color | ColorPalette = Color.WHITE,
|
||||
text_padding: int = 10,
|
||||
text_position: Position = Position.TOP_LEFT,
|
||||
color_lookup: ColorLookup = ColorLookup.CLASS,
|
||||
text_offset: tuple[int, int] = (0, 0),
|
||||
border_radius: int = 0,
|
||||
smart_position: bool = False,
|
||||
max_line_length: int | None = None,
|
||||
@@ -79,27 +83,29 @@ class _BaseLabelAnnotator(BaseAnnotator):
|
||||
|
||||
Args:
|
||||
color (Union[Color, ColorPalette], optional): The color to use for the label
|
||||
background.
|
||||
background.
|
||||
color_lookup (ColorLookup, optional): The method used to determine the color
|
||||
of the label
|
||||
text_color (Union[Color, ColorPalette], optional): The color to use for the
|
||||
label text.
|
||||
label text.
|
||||
text_padding (int, optional): The padding around the label text, in pixels.
|
||||
text_position (Position, optional): The position of the text relative to the
|
||||
detection bounding box.
|
||||
color_lookup (ColorLookup, optional): The method used to determine the color
|
||||
of the label
|
||||
detection bounding box.
|
||||
text_offset (Tuple[int, int], optional): A tuple of 2D coordinates
|
||||
`(x, y)` to offset the text position from the anchor point, in pixels.
|
||||
border_radius (int, optional): The radius of the label background corners,
|
||||
in pixels.
|
||||
in pixels.
|
||||
smart_position (bool, optional): Whether to intelligently adjust the label
|
||||
position to avoid overlapping with other elements.
|
||||
position to avoid overlapping with other elements.
|
||||
max_line_length (Optional[int], optional): Maximum number of characters per
|
||||
line before wrapping the text. None means no wrapping.
|
||||
|
||||
line before wrapping the text. None means no wrapping.
|
||||
"""
|
||||
self.color: Color | ColorPalette = color
|
||||
self.color_lookup: ColorLookup = color_lookup
|
||||
self.text_color: Color | ColorPalette = text_color
|
||||
self.text_padding: int = text_padding
|
||||
self.text_anchor: Position = text_position
|
||||
self.color_lookup: ColorLookup = color_lookup
|
||||
self.text_offset: tuple[int, int] = text_offset
|
||||
self.border_radius: int = border_radius
|
||||
self.smart_position = smart_position
|
||||
self.max_line_length: int | None = max_line_length
|
||||
@@ -1076,24 +1082,47 @@ class LabelAnnotator(_BaseLabelAnnotator):
|
||||
def __init__(
|
||||
self,
|
||||
color: Color | ColorPalette = ColorPalette.DEFAULT,
|
||||
color_lookup: ColorLookup = ColorLookup.CLASS,
|
||||
text_color: Color | ColorPalette = Color.WHITE,
|
||||
text_scale: float = 0.5,
|
||||
text_thickness: int = 1,
|
||||
text_padding: int = 10,
|
||||
text_position: Position = Position.TOP_LEFT,
|
||||
color_lookup: ColorLookup = ColorLookup.CLASS,
|
||||
text_offset: tuple[int, int] = (0, 0),
|
||||
border_radius: int = 0,
|
||||
smart_position: bool = False,
|
||||
max_line_length: int | None = None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
color (Union[Color, ColorPalette]): The color or color palette to use for
|
||||
annotating the text background.
|
||||
color_lookup (ColorLookup): Strategy for mapping colors to annotations.
|
||||
Options are `INDEX`, `CLASS`, `TRACK`.
|
||||
text_color (Union[Color, ColorPalette]): The color or color palette to use
|
||||
for the text.
|
||||
text_scale (float): Font scale for the text.
|
||||
text_thickness (int): Thickness of the text characters.
|
||||
text_padding (int): Padding around the text within its background box.
|
||||
text_position (Position): Position of the text relative to the detection.
|
||||
Possible values are defined in the `Position` enum.
|
||||
text_offset (Tuple[int, int]): A tuple of 2D coordinates `(x, y)` to
|
||||
offset the text position from the anchor point, in pixels.
|
||||
border_radius (int): The radius to apply round edges. If the selected
|
||||
value is higher than the lower dimension, width or height, is clipped.
|
||||
smart_position (bool): Spread out the labels to avoid overlapping.
|
||||
max_line_length (Optional[int]): Maximum number of characters per line
|
||||
before wrapping the text. None means no wrapping.
|
||||
"""
|
||||
self.text_scale: float = text_scale
|
||||
self.text_thickness: int = text_thickness
|
||||
super().__init__(
|
||||
color=color,
|
||||
color_lookup=color_lookup,
|
||||
text_color=text_color,
|
||||
text_padding=text_padding,
|
||||
text_position=text_position,
|
||||
color_lookup=color_lookup,
|
||||
text_offset=text_offset,
|
||||
border_radius=border_radius,
|
||||
smart_position=smart_position,
|
||||
max_line_length=max_line_length,
|
||||
@@ -1102,11 +1131,51 @@ class LabelAnnotator(_BaseLabelAnnotator):
|
||||
@ensure_cv2_image_for_annotation
|
||||
def annotate(
|
||||
self,
|
||||
scene: ImageType, # Ensure scene is initially a NumPy array here
|
||||
scene: ImageType,
|
||||
detections: Detections,
|
||||
labels: list[str] | None = None,
|
||||
custom_color_lookup: np.ndarray | None = None,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Annotates the given scene with labels based on the provided detections.
|
||||
|
||||
Args:
|
||||
scene (ImageType): The image where labels will be drawn.
|
||||
`ImageType` is a flexible type, accepting either `numpy.ndarray`
|
||||
or `PIL.Image.Image`.
|
||||
detections (Detections): Object detections to annotate.
|
||||
labels (Optional[List[str]]): Custom labels for each detection.
|
||||
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
|
||||
Allows to override the default color mapping strategy.
|
||||
|
||||
Returns:
|
||||
The annotated image, matching the type of `scene` (`numpy.ndarray`
|
||||
or `PIL.Image.Image`)
|
||||
|
||||
Example:
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
image = ...
|
||||
detections = sv.Detections(...)
|
||||
|
||||
labels = [
|
||||
f"{class_name} {confidence:.2f}"
|
||||
for class_name, confidence
|
||||
in zip(detections['class_name'], detections.confidence)
|
||||
]
|
||||
|
||||
label_annotator = sv.LabelAnnotator(text_position=sv.Position.CENTER)
|
||||
annotated_frame = label_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections=detections,
|
||||
labels=labels
|
||||
)
|
||||
```
|
||||
|
||||

|
||||
"""
|
||||
assert isinstance(scene, np.ndarray)
|
||||
validate_labels(labels, detections)
|
||||
|
||||
@@ -1144,7 +1213,12 @@ class LabelAnnotator(_BaseLabelAnnotator):
|
||||
anchor=self.text_anchor
|
||||
).astype(int)
|
||||
|
||||
for label, center_coords in zip(labels, anchors_coordinates):
|
||||
for label, center_coordinates in zip(labels, anchors_coordinates):
|
||||
center_coordinates = (
|
||||
center_coordinates[0] + self.text_offset[0],
|
||||
center_coordinates[1] + self.text_offset[1],
|
||||
)
|
||||
|
||||
wrapped_lines = wrap_text(label, self.max_line_length)
|
||||
line_heights = []
|
||||
line_widths = []
|
||||
@@ -1170,7 +1244,7 @@ class LabelAnnotator(_BaseLabelAnnotator):
|
||||
height_padded = total_height + 2 * self.text_padding
|
||||
|
||||
text_background_xyxy = resolve_text_background_xyxy(
|
||||
center_coordinates=tuple(center_coords),
|
||||
center_coordinates=center_coordinates,
|
||||
text_wh=(width_padded, height_padded),
|
||||
position=self.text_anchor,
|
||||
)
|
||||
@@ -1317,25 +1391,48 @@ class RichLabelAnnotator(_BaseLabelAnnotator):
|
||||
def __init__(
|
||||
self,
|
||||
color: Color | ColorPalette = ColorPalette.DEFAULT,
|
||||
color_lookup: ColorLookup = ColorLookup.CLASS,
|
||||
text_color: Color | ColorPalette = Color.WHITE,
|
||||
font_path: str | None = None,
|
||||
font_size: int = 10,
|
||||
text_padding: int = 10,
|
||||
text_position: Position = Position.TOP_LEFT,
|
||||
color_lookup: ColorLookup = ColorLookup.CLASS,
|
||||
text_offset: tuple[int, int] = (0, 0),
|
||||
border_radius: int = 0,
|
||||
smart_position: bool = False,
|
||||
max_line_length: int | None = None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
color (Union[Color, ColorPalette]): The color or color palette to use for
|
||||
annotating the text background.
|
||||
color_lookup (ColorLookup): Strategy for mapping colors to annotations.
|
||||
Options are `INDEX`, `CLASS`, `TRACK`.
|
||||
text_color (Union[Color, ColorPalette]): The color to use for the text.
|
||||
font_path (Optional[str]): Path to the font file (e.g., ".ttf" or ".otf")
|
||||
to use for rendering text. If `None`, the default PIL font will be used.
|
||||
font_size (int): Font size for the text.
|
||||
text_padding (int): Padding around the text within its background box.
|
||||
text_position (Position): Position of the text relative to the detection.
|
||||
Possible values are defined in the `Position` enum.
|
||||
text_offset (Tuple[int, int]): A tuple of 2D coordinates `(x, y)` to
|
||||
offset the text position from the anchor point, in pixels.
|
||||
border_radius (int): The radius to apply round edges. If the selected
|
||||
value is higher than the lower dimension, width or height, is clipped.
|
||||
smart_position (bool): Spread out the labels to avoid overlapping.
|
||||
max_line_length (Optional[int]): Maximum number of characters per line
|
||||
before wrapping the text. None means no wrapping.
|
||||
"""
|
||||
self.font_path = font_path
|
||||
self.font_size = font_size
|
||||
self.font = self._load_font(font_size, font_path)
|
||||
super().__init__(
|
||||
color=color,
|
||||
color_lookup=color_lookup,
|
||||
text_color=text_color,
|
||||
text_padding=text_padding,
|
||||
text_position=text_position,
|
||||
color_lookup=color_lookup,
|
||||
text_offset=text_offset,
|
||||
border_radius=border_radius,
|
||||
smart_position=smart_position,
|
||||
max_line_length=max_line_length,
|
||||
@@ -1349,6 +1446,44 @@ class RichLabelAnnotator(_BaseLabelAnnotator):
|
||||
labels: list[str] | None = None,
|
||||
custom_color_lookup: np.ndarray | None = None,
|
||||
) -> ImageType:
|
||||
"""
|
||||
Annotates the given scene with labels based on the provided
|
||||
detections, with support for Unicode characters.
|
||||
|
||||
Args:
|
||||
scene (ImageType): The image where labels will be drawn.
|
||||
`ImageType` is a flexible type, accepting either `numpy.ndarray`
|
||||
or `PIL.Image.Image`.
|
||||
detections (Detections): Object detections to annotate.
|
||||
labels (Optional[List[str]]): Custom labels for each detection.
|
||||
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
|
||||
Allows to override the default color mapping strategy.
|
||||
|
||||
Returns:
|
||||
The annotated image, matching the type of `scene` (`numpy.ndarray`
|
||||
or `PIL.Image.Image`)
|
||||
|
||||
Example:
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
image = ...
|
||||
detections = sv.Detections(...)
|
||||
|
||||
labels = [
|
||||
f"{class_name} {confidence:.2f}"
|
||||
for class_name, confidence
|
||||
in zip(detections['class_name'], detections.confidence)
|
||||
]
|
||||
|
||||
rich_label_annotator = sv.RichLabelAnnotator(font_path="path/to/font.ttf")
|
||||
annotated_frame = label_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections=detections,
|
||||
labels=labels
|
||||
)
|
||||
```
|
||||
"""
|
||||
assert isinstance(scene, Image.Image)
|
||||
validate_labels(labels, detections)
|
||||
|
||||
@@ -1386,7 +1521,12 @@ class RichLabelAnnotator(_BaseLabelAnnotator):
|
||||
anchor=self.text_anchor
|
||||
).astype(int)
|
||||
|
||||
for label, center_coords in zip(labels, anchor_coordinates):
|
||||
for label, center_coordinates in zip(labels, anchor_coordinates):
|
||||
center_coordinates = (
|
||||
center_coordinates[0] + self.text_offset[0],
|
||||
center_coordinates[1] + self.text_offset[1],
|
||||
)
|
||||
|
||||
wrapped_lines = wrap_text(label, self.max_line_length)
|
||||
|
||||
# Calculate the total text height and maximum width
|
||||
@@ -1409,7 +1549,7 @@ class RichLabelAnnotator(_BaseLabelAnnotator):
|
||||
height_padded = int(total_height + 2 * self.text_padding)
|
||||
|
||||
text_background_xyxy = resolve_text_background_xyxy(
|
||||
center_coordinates=tuple(center_coords),
|
||||
center_coordinates=center_coordinates,
|
||||
text_wh=(width_padded, height_padded),
|
||||
position=self.text_anchor,
|
||||
)
|
||||
@@ -1681,6 +1821,7 @@ class TraceAnnotator(BaseAnnotator):
|
||||
position: Position = Position.CENTER,
|
||||
trace_length: int = 30,
|
||||
thickness: int = 2,
|
||||
smooth: bool = False,
|
||||
color_lookup: ColorLookup = ColorLookup.CLASS,
|
||||
):
|
||||
"""
|
||||
@@ -1692,12 +1833,14 @@ class TraceAnnotator(BaseAnnotator):
|
||||
trace_length (int): The maximum length of the trace in terms of historical
|
||||
points. Defaults to `30`.
|
||||
thickness (int): The thickness of the trace lines. Defaults to `2`.
|
||||
smooth (bool): Smooth the trace lines.
|
||||
color_lookup (ColorLookup): Strategy for mapping colors to annotations.
|
||||
Options are `INDEX`, `CLASS`, `TRACK`.
|
||||
"""
|
||||
self.color: Color | ColorPalette = color
|
||||
self.trace = Trace(max_size=trace_length, anchor=position)
|
||||
self.thickness = thickness
|
||||
self.smooth = smooth
|
||||
self.color_lookup: ColorLookup = color_lookup
|
||||
|
||||
@ensure_cv2_image_for_annotation
|
||||
@@ -1769,10 +1912,18 @@ class TraceAnnotator(BaseAnnotator):
|
||||
else custom_color_lookup,
|
||||
)
|
||||
xy = self.trace.get(tracker_id=tracker_id)
|
||||
spline_points = xy.astype(np.int32)
|
||||
|
||||
if len(xy) > 3 and self.smooth:
|
||||
x, y = xy[:, 0], xy[:, 1]
|
||||
tck, u = splprep([x, y], s=20)
|
||||
x_new, y_new = splev(np.linspace(0, 1, 100), tck)
|
||||
spline_points = np.stack([x_new, y_new], axis=1).astype(np.int32)
|
||||
|
||||
if len(xy) > 1:
|
||||
scene = cv2.polylines(
|
||||
scene,
|
||||
[xy.astype(np.int32)],
|
||||
[spline_points],
|
||||
False,
|
||||
color=color.as_bgr(),
|
||||
thickness=self.thickness,
|
||||
|
||||
@@ -142,7 +142,11 @@ def resolve_color(
|
||||
detection_idx=detection_idx,
|
||||
color_lookup=color_lookup,
|
||||
)
|
||||
if color_lookup == ColorLookup.TRACK and idx == PENDING_TRACK_ID:
|
||||
if (
|
||||
isinstance(color_lookup, ColorLookup)
|
||||
and color_lookup == ColorLookup.TRACK
|
||||
and idx == PENDING_TRACK_ID
|
||||
):
|
||||
return PENDING_TRACK_COLOR
|
||||
return get_color_by_index(color=color, idx=idx)
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ from supervision.detection.utils.masks import calculate_masks_centroids
|
||||
from supervision.detection.vlm import (
|
||||
LMM,
|
||||
VLM,
|
||||
from_deepseek_vl_2,
|
||||
from_florence_2,
|
||||
from_google_gemini_2_0,
|
||||
from_google_gemini_2_5,
|
||||
@@ -830,6 +831,7 @@ class Detections:
|
||||
| Google Gemini 2.0 | `GOOGLE_GEMINI_2_0` | detection | `resolution_wh` | `classes` |
|
||||
| Google Gemini 2.5 | `GOOGLE_GEMINI_2_5` | detection, segmentation | `resolution_wh` | `classes` |
|
||||
| Moondream | `MOONDREAM` | detection | `resolution_wh` | |
|
||||
| DeepSeek-VL2 | `DEEPSEEK_VL_2` | detection | `resolution_wh` | `classes` |
|
||||
|
||||
Args:
|
||||
lmm (Union[LMM, str]): The type of LMM (Large Multimodal Model) to use.
|
||||
@@ -1117,6 +1119,47 @@ class Detections:
|
||||
# array([[1752.28, 818.82, 2165.72, 1229.14],
|
||||
# [1908.01, 1346.67, 2585.99, 2024.11]])
|
||||
```
|
||||
|
||||
!!! example "DeepSeek-VL2"
|
||||
|
||||
|
||||
??? tip "Prompt engineering"
|
||||
|
||||
To get the best results from DeepSeek-VL2, use optimized prompts that leverage
|
||||
its object detection and visual grounding capabilities effectively.
|
||||
|
||||
**For general object detection, use the following user prompt:**
|
||||
|
||||
```
|
||||
<image>\\n<|ref|>The giraffe at the front<|/ref|>
|
||||
```
|
||||
|
||||
**For visual grounding, use the following user prompt:**
|
||||
|
||||
```
|
||||
<image>\\n<|grounding|>Detect the giraffes
|
||||
```
|
||||
|
||||
```python
|
||||
from PIL import Image
|
||||
import supervision as sv
|
||||
|
||||
deepseek_vl2_result = "<|ref|>The giraffe at the back<|/ref|><|det|>[[580, 270, 999, 904]]<|/det|><|ref|>The giraffe at the front<|/ref|><|det|>[[26, 31, 632, 998]]<|/det|><|end▁of▁sentence|>"
|
||||
|
||||
detections = sv.Detections.from_vlm(
|
||||
vlm=sv.VLM.DEEPSEEK_VL_2, result=deepseek_vl2_result, resolution_wh=image.size
|
||||
)
|
||||
|
||||
detections.xyxy
|
||||
# array([[ 420, 293, 724, 982],
|
||||
# [ 18, 33, 458, 1084]])
|
||||
|
||||
detections.class_id
|
||||
# array([0, 1])
|
||||
|
||||
detections.data
|
||||
# {'class_name': array(['The giraffe at the back', 'The giraffe at the front'], dtype='<U24')}
|
||||
```
|
||||
""" # noqa: E501
|
||||
|
||||
# filler logic mapping old from_lmm to new from_vlm
|
||||
@@ -1124,6 +1167,7 @@ class Detections:
|
||||
LMM.PALIGEMMA: VLM.PALIGEMMA,
|
||||
LMM.FLORENCE_2: VLM.FLORENCE_2,
|
||||
LMM.QWEN_2_5_VL: VLM.QWEN_2_5_VL,
|
||||
LMM.DEEPSEEK_VL_2: VLM.DEEPSEEK_VL_2,
|
||||
LMM.GOOGLE_GEMINI_2_0: VLM.GOOGLE_GEMINI_2_0,
|
||||
LMM.GOOGLE_GEMINI_2_5: VLM.GOOGLE_GEMINI_2_5,
|
||||
}
|
||||
@@ -1164,6 +1208,7 @@ class Detections:
|
||||
| Google Gemini 2.0 | `GOOGLE_GEMINI_2_0` | detection | `resolution_wh` | `classes` |
|
||||
| Google Gemini 2.5 | `GOOGLE_GEMINI_2_5` | detection, segmentation | `resolution_wh` | `classes` |
|
||||
| Moondream | `MOONDREAM` | detection | `resolution_wh` | |
|
||||
| DeepSeek-VL2 | `DEEPSEEK_VL_2` | detection | `resolution_wh` | `classes` |
|
||||
|
||||
Args:
|
||||
vlm (Union[VLM, str]): The type of VLM (Vision Language Model) to use.
|
||||
@@ -1452,6 +1497,47 @@ class Detections:
|
||||
# [1908.01, 1346.67, 2585.99, 2024.11]])
|
||||
```
|
||||
|
||||
!!! example "DeepSeek-VL2"
|
||||
|
||||
|
||||
??? tip "Prompt engineering"
|
||||
|
||||
To get the best results from DeepSeek-VL2, use optimized prompts that leverage
|
||||
its object detection and visual grounding capabilities effectively.
|
||||
|
||||
**For general object detection, use the following user prompt:**
|
||||
|
||||
```
|
||||
<image>\\n<|ref|>The giraffe at the front<|/ref|>
|
||||
```
|
||||
|
||||
**For visual grounding, use the following user prompt:**
|
||||
|
||||
```
|
||||
<image>\\n<|grounding|>Detect the giraffes
|
||||
```
|
||||
|
||||
```python
|
||||
from PIL import Image
|
||||
import supervision as sv
|
||||
|
||||
deepseek_vl2_result = "<|ref|>The giraffe at the back<|/ref|><|det|>[[580, 270, 999, 904]]<|/det|><|ref|>The giraffe at the front<|/ref|><|det|>[[26, 31, 632, 998]]<|/det|><|end▁of▁sentence|>"
|
||||
|
||||
detections = sv.Detections.from_vlm(
|
||||
vlm=sv.VLM.DEEPSEEK_VL_2, result=deepseek_vl2_result, resolution_wh=image.size
|
||||
)
|
||||
|
||||
detections.xyxy
|
||||
# array([[ 420, 293, 724, 982],
|
||||
# [ 18, 33, 458, 1084]])
|
||||
|
||||
detections.class_id
|
||||
# array([0, 1])
|
||||
|
||||
detections.data
|
||||
# {'class_name': array(['The giraffe at the back', 'The giraffe at the front'], dtype='<U24')}
|
||||
```
|
||||
|
||||
""" # noqa: E501
|
||||
|
||||
vlm = validate_vlm_parameters(vlm, result, kwargs)
|
||||
@@ -1466,6 +1552,11 @@ class Detections:
|
||||
data = {CLASS_NAME_DATA_FIELD: class_name}
|
||||
return cls(xyxy=xyxy, class_id=class_id, data=data)
|
||||
|
||||
if vlm == VLM.DEEPSEEK_VL_2:
|
||||
xyxy, class_id, class_name = from_deepseek_vl_2(result, **kwargs)
|
||||
data = {CLASS_NAME_DATA_FIELD: class_name}
|
||||
return cls(xyxy=xyxy, class_id=class_id, data=data)
|
||||
|
||||
if vlm == VLM.FLORENCE_2:
|
||||
xyxy, labels, mask, xyxyxyxy = from_florence_2(result, **kwargs)
|
||||
if len(xyxy) == 0:
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from supervision.detection.utils.iou_and_nms import box_iou_batch
|
||||
|
||||
__all__ = ["box_iou_batch"]
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def edit_distance(string_1: str, string_2: str, case_sensitive: bool = True) -> int:
|
||||
"""
|
||||
Calculates the minimum number of single-character edits required
|
||||
to transform one string into another. Allowed operations are insertion,
|
||||
deletion, and substitution.
|
||||
|
||||
Args:
|
||||
string_1 (str): The source string to be transformed.
|
||||
string_2 (str): The target string to transform into.
|
||||
case_sensitive (bool, optional): Whether comparison should be case-sensitive.
|
||||
Defaults to True.
|
||||
|
||||
Returns:
|
||||
int: The minimum number of edits required to convert `string_1`
|
||||
into `string_2`.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
sv.edit_distance("hello", "hello")
|
||||
# 0
|
||||
|
||||
sv.edit_distance("Test", "test", case_sensitive=True)
|
||||
# 1
|
||||
|
||||
sv.edit_distance("abc", "xyz")
|
||||
# 3
|
||||
|
||||
sv.edit_distance("hello", "")
|
||||
# 5
|
||||
|
||||
sv.edit_distance("", "")
|
||||
# 0
|
||||
|
||||
sv.edit_distance("hello world", "helloworld")
|
||||
# 1
|
||||
```
|
||||
"""
|
||||
if not case_sensitive:
|
||||
string_1 = string_1.lower()
|
||||
string_2 = string_2.lower()
|
||||
|
||||
if len(string_1) < len(string_2):
|
||||
string_1, string_2 = string_2, string_1
|
||||
|
||||
prev_row = list(range(len(string_2) + 1))
|
||||
curr_row = [0] * (len(string_2) + 1)
|
||||
|
||||
for i in range(1, len(string_1) + 1):
|
||||
curr_row[0] = i
|
||||
for j in range(1, len(string_2) + 1):
|
||||
if string_1[i - 1] == string_2[j - 1]:
|
||||
substitution_cost = 0
|
||||
else:
|
||||
substitution_cost = 1
|
||||
curr_row[j] = min(
|
||||
prev_row[j] + 1,
|
||||
curr_row[j - 1] + 1,
|
||||
prev_row[j - 1] + substitution_cost,
|
||||
)
|
||||
prev_row, curr_row = curr_row, prev_row
|
||||
|
||||
return prev_row[len(string_2)]
|
||||
|
||||
|
||||
def fuzzy_match_index(
|
||||
candidates: list[str],
|
||||
query: str,
|
||||
threshold: int,
|
||||
case_sensitive: bool = True,
|
||||
) -> int | None:
|
||||
"""
|
||||
Searches for the first string in `candidates` whose edit distance
|
||||
to `query` is less than or equal to `threshold`.
|
||||
|
||||
Args:
|
||||
candidates (list[str]): List of strings to search.
|
||||
query (str): String to compare against the candidates.
|
||||
threshold (int): Maximum allowed edit distance for a match.
|
||||
case_sensitive (bool, optional): Whether matching should be case-sensitive.
|
||||
|
||||
Returns:
|
||||
Optional[int]: Index of the first matching string in candidates,
|
||||
or None if no match is found.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
fuzzy_match_index(["cat", "dog", "rat"], "dat", threshold=1)
|
||||
# 0
|
||||
|
||||
fuzzy_match_index(["alpha", "beta", "gamma"], "bata", threshold=1)
|
||||
# 1
|
||||
|
||||
fuzzy_match_index(["one", "two", "three"], "ten", threshold=2)
|
||||
# None
|
||||
```
|
||||
"""
|
||||
for idx, candidate in enumerate(candidates):
|
||||
if edit_distance(candidate, query, case_sensitive=case_sensitive) <= threshold:
|
||||
return idx
|
||||
return None
|
||||
@@ -36,6 +36,7 @@ class LMM(Enum):
|
||||
PALIGEMMA = "paligemma"
|
||||
FLORENCE_2 = "florence_2"
|
||||
QWEN_2_5_VL = "qwen_2_5_vl"
|
||||
DEEPSEEK_VL_2 = "deepseek_vl_2"
|
||||
GOOGLE_GEMINI_2_0 = "gemini_2_0"
|
||||
GOOGLE_GEMINI_2_5 = "gemini_2_5"
|
||||
MOONDREAM = "moondream"
|
||||
@@ -76,6 +77,7 @@ class VLM(Enum):
|
||||
PALIGEMMA = "paligemma"
|
||||
FLORENCE_2 = "florence_2"
|
||||
QWEN_2_5_VL = "qwen_2_5_vl"
|
||||
DEEPSEEK_VL_2 = "deepseek_vl_2"
|
||||
GOOGLE_GEMINI_2_0 = "gemini_2_0"
|
||||
GOOGLE_GEMINI_2_5 = "gemini_2_5"
|
||||
MOONDREAM = "moondream"
|
||||
@@ -104,6 +106,7 @@ RESULT_TYPES: dict[VLM, type] = {
|
||||
VLM.PALIGEMMA: str,
|
||||
VLM.FLORENCE_2: dict,
|
||||
VLM.QWEN_2_5_VL: str,
|
||||
VLM.DEEPSEEK_VL_2: str,
|
||||
VLM.GOOGLE_GEMINI_2_0: str,
|
||||
VLM.GOOGLE_GEMINI_2_5: str,
|
||||
VLM.MOONDREAM: dict,
|
||||
@@ -113,6 +116,7 @@ REQUIRED_ARGUMENTS: dict[VLM, list[str]] = {
|
||||
VLM.PALIGEMMA: ["resolution_wh"],
|
||||
VLM.FLORENCE_2: ["resolution_wh"],
|
||||
VLM.QWEN_2_5_VL: ["input_wh", "resolution_wh"],
|
||||
VLM.DEEPSEEK_VL_2: ["resolution_wh"],
|
||||
VLM.GOOGLE_GEMINI_2_0: ["resolution_wh"],
|
||||
VLM.GOOGLE_GEMINI_2_5: ["resolution_wh"],
|
||||
VLM.MOONDREAM: ["resolution_wh"],
|
||||
@@ -122,6 +126,7 @@ ALLOWED_ARGUMENTS: dict[VLM, list[str]] = {
|
||||
VLM.PALIGEMMA: ["resolution_wh", "classes"],
|
||||
VLM.FLORENCE_2: ["resolution_wh"],
|
||||
VLM.QWEN_2_5_VL: ["input_wh", "resolution_wh", "classes"],
|
||||
VLM.DEEPSEEK_VL_2: ["resolution_wh", "classes"],
|
||||
VLM.GOOGLE_GEMINI_2_0: ["resolution_wh", "classes"],
|
||||
VLM.GOOGLE_GEMINI_2_5: ["resolution_wh", "classes"],
|
||||
VLM.MOONDREAM: ["resolution_wh"],
|
||||
@@ -310,6 +315,79 @@ def from_qwen_2_5_vl(
|
||||
return xyxy, class_id, class_name
|
||||
|
||||
|
||||
def from_deepseek_vl_2(
|
||||
result: str, resolution_wh: tuple[int, int], classes: list[str] | None = None
|
||||
) -> tuple[np.ndarray, np.ndarray | None, np.ndarray]:
|
||||
"""
|
||||
Parse bounding boxes from deepseek-vl2-formatted text, scale them to the specified
|
||||
resolution, and optionally filter by classes.
|
||||
|
||||
The DeepSeek-VL2 output typically contains pairs of <|ref|> ... <|/ref|> labels
|
||||
and <|det|> ... <|/det|> bounding box definitions. Each <|det|> section may
|
||||
contain one or more bounding boxes in the form [[x1, y1, x2, y2], [x1, y1, x2, y2], ...]
|
||||
(scaled to 0..999). For example:
|
||||
|
||||
```
|
||||
<|ref|>The giraffe at the back<|/ref|><|det|>[[580, 270, 999, 904]]<|/det|><|ref|>The giraffe at the front<|/ref|><|det|>[[26, 31, 632, 998]]<|/det|><|end▁of▁sentence|>
|
||||
```
|
||||
|
||||
Args:
|
||||
result: String containing deepseek-vl2-formatted locations and labels.
|
||||
resolution_wh: Tuple (width, height) to which we scale the box coordinates.
|
||||
classes: Optional list of valid class names. If provided, boxes and labels not
|
||||
in this list are filtered out.
|
||||
|
||||
Returns:
|
||||
xyxy (np.ndarray): An array of shape `(n, 4)` containing
|
||||
the bounding boxes coordinates in format `[x1, y1, x2, y2]`.
|
||||
class_id (Optional[np.ndarray]): An array of shape `(n,)` containing
|
||||
the class indices for each bounding box (or `None` if classes is not
|
||||
provided).
|
||||
class_name (np.ndarray): An array of shape `(n,)` containing
|
||||
the class labels for each bounding box.
|
||||
""" # noqa: E501
|
||||
|
||||
width, height = resolution_wh
|
||||
label_segments = re.findall(r"<\|ref\|>(.*?)<\|/ref\|>", result, flags=re.S)
|
||||
detection_segments = re.findall(r"<\|det\|>(.*?)<\|/det\|>", result, flags=re.S)
|
||||
|
||||
if len(label_segments) != len(detection_segments):
|
||||
raise ValueError(
|
||||
f"Number of ref tags ({len(label_segments)}) "
|
||||
f"and det tags ({len(detection_segments)}) in the result must be equal."
|
||||
)
|
||||
|
||||
xyxy, class_name_list = [], []
|
||||
for label, detection_blob in zip(label_segments, detection_segments):
|
||||
current_class_name = label.strip()
|
||||
for box in re.findall(r"\[(.*?)\]", detection_blob):
|
||||
x1, y1, x2, y2 = map(float, box.strip("[]").split(","))
|
||||
xyxy.append(
|
||||
[
|
||||
(x1 / 999 * width),
|
||||
(y1 / 999 * height),
|
||||
(x2 / 999 * width),
|
||||
(y2 / 999 * height),
|
||||
]
|
||||
)
|
||||
class_name_list.append(current_class_name)
|
||||
|
||||
xyxy = np.array(xyxy, dtype=np.float32)
|
||||
class_name = np.array(class_name_list)
|
||||
|
||||
if classes is not None:
|
||||
mask = np.array([name in classes for name in class_name], dtype=bool)
|
||||
xyxy = xyxy[mask]
|
||||
class_name = class_name[mask]
|
||||
class_id = np.array([classes.index(name) for name in class_name])
|
||||
else:
|
||||
unique_classes = sorted(list(set(class_name)))
|
||||
class_to_id = {name: i for i, name in enumerate(unique_classes)}
|
||||
class_id = np.array([class_to_id[name] for name in class_name])
|
||||
|
||||
return xyxy, class_id, class_name
|
||||
|
||||
|
||||
def from_florence_2(
|
||||
result: dict, resolution_wh: tuple[int, int]
|
||||
) -> tuple[np.ndarray, np.ndarray | None, np.ndarray | None, np.ndarray | None]:
|
||||
|
||||
@@ -11,8 +11,8 @@ from supervision.detection.utils.boxes import pad_boxes, spread_out_boxes
|
||||
from supervision.draw.color import Color
|
||||
from supervision.draw.utils import draw_rounded_rectangle
|
||||
from supervision.geometry.core import Rect
|
||||
from supervision.keypoint.core import KeyPoints
|
||||
from supervision.keypoint.skeletons import SKELETONS_BY_VERTEX_COUNT
|
||||
from supervision.key_points.core import KeyPoints
|
||||
from supervision.key_points.skeletons import SKELETONS_BY_VERTEX_COUNT
|
||||
from supervision.utils.conversion import ensure_cv2_image_for_annotation
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import numpy.typing as npt
|
||||
from supervision.config import CLASS_NAME_DATA_FIELD
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.detection.utils.internal import get_data_item, is_data_equal
|
||||
from supervision.validators import validate_keypoints_fields
|
||||
from supervision.validators import validate_key_points_fields
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -23,7 +23,7 @@ class KeyPoints:
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
Use [`sv.KeyPoints.from_ultralytics`](/latest/keypoint/core/#supervision.keypoint.core.KeyPoints.from_ultralytics)
|
||||
Use [`sv.KeyPoints.from_ultralytics`](/latest/keypoint/core/#supervision.key_points.core.KeyPoints.from_ultralytics)
|
||||
method, which accepts [YOLOv8-pose](https://docs.ultralytics.com/models/yolov8/), [YOLO11-pose](https://docs.ultralytics.com/models/yolo11/)
|
||||
[pose](https://docs.ultralytics.com/tasks/pose/) result.
|
||||
|
||||
@@ -41,7 +41,7 @@ class KeyPoints:
|
||||
|
||||
=== "Inference"
|
||||
|
||||
Use [`sv.KeyPoints.from_inference`](/latest/keypoint/core/#supervision.keypoint.core.KeyPoints.from_inference)
|
||||
Use [`sv.KeyPoints.from_inference`](/latest/keypoint/core/#supervision.key_points.core.KeyPoints.from_inference)
|
||||
method, which accepts [Inference](https://inference.roboflow.com/) pose result.
|
||||
|
||||
```python
|
||||
@@ -58,7 +58,7 @@ class KeyPoints:
|
||||
|
||||
=== "MediaPipe"
|
||||
|
||||
Use [`sv.KeyPoints.from_mediapipe`](/latest/keypoint/core/#supervision.keypoint.core.KeyPoints.from_mediapipe)
|
||||
Use [`sv.KeyPoints.from_mediapipe`](/latest/keypoint/core/#supervision.key_points.core.KeyPoints.from_mediapipe)
|
||||
method, which accepts [MediaPipe](https://github.com/google-ai-edge/mediapipe)
|
||||
pose result.
|
||||
|
||||
@@ -89,10 +89,61 @@ class KeyPoints:
|
||||
pose_landmarker_result, (image_width, image_height))
|
||||
```
|
||||
|
||||
=== "Transformers"
|
||||
|
||||
Use [`sv.KeyPoints.from_transformers`](/latest/keypoint/core/#supervision.key_points.core.KeyPoints.from_transformers)
|
||||
method, which accepts [ViTPose](https://huggingface.co/docs/transformers/en/model_doc/vitpose) result.
|
||||
|
||||
```python
|
||||
from PIL import Image
|
||||
import requests
|
||||
import supervision as sv
|
||||
import torch
|
||||
from transformers import (
|
||||
AutoProcessor,
|
||||
RTDetrForObjectDetection,
|
||||
VitPoseForPoseEstimation,
|
||||
)
|
||||
|
||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
image = Image.open(<SOURCE_IMAGE_PATH>)
|
||||
|
||||
DETECTION_MODEL_ID = "PekingU/rtdetr_r50vd_coco_o365"
|
||||
|
||||
detection_processor = AutoProcessor.from_pretrained(DETECTION_MODEL_ID, use_fast=True)
|
||||
detection_model = RTDetrForObjectDetection.from_pretrained(DETECTION_MODEL_ID, device_map=DEVICE)
|
||||
|
||||
inputs = detection_processor(images=frame, return_tensors="pt").to(DEVICE)
|
||||
|
||||
with torch.no_grad():
|
||||
outputs = detection_model(**inputs)
|
||||
|
||||
target_size = torch.tensor([(frame.height, frame.width)])
|
||||
results = detection_processor.post_process_object_detection(
|
||||
outputs, target_sizes=target_size, threshold=0.3)
|
||||
|
||||
detections = sv.Detections.from_transformers(results[0])
|
||||
boxes = sv.xyxy_to_xywh(detections[detections.class_id == 0].xyxy)
|
||||
|
||||
POSE_ESTIMATION_MODEL_ID = "usyd-community/vitpose-base-simple"
|
||||
|
||||
pose_estimation_processor = AutoProcessor.from_pretrained(POSE_ESTIMATION_MODEL_ID)
|
||||
pose_estimation_model = VitPoseForPoseEstimation.from_pretrained(
|
||||
POSE_ESTIMATION_MODEL_ID, device_map=DEVICE)
|
||||
|
||||
inputs = pose_estimation_processor(frame, boxes=[boxes], return_tensors="pt").to(DEVICE)
|
||||
|
||||
with torch.no_grad():
|
||||
outputs = pose_estimation_model(**inputs)
|
||||
|
||||
results = pose_estimation_processor.post_process_pose_estimation(outputs, boxes=[boxes])
|
||||
key_point = sv.KeyPoints.from_transformers(results[0])
|
||||
```
|
||||
|
||||
Attributes:
|
||||
xy (np.ndarray): An array of shape `(n, m, 2)` containing
|
||||
`n` detected objects, each composed of `m` equally-sized
|
||||
sets of keypoints, where each point is `[x, y]`.
|
||||
sets of key points, where each point is `[x, y]`.
|
||||
class_id (Optional[np.ndarray]): An array of shape
|
||||
`(n,)` containing the class ids of the detected objects.
|
||||
confidence (Optional[np.ndarray]): An array of shape
|
||||
@@ -109,7 +160,7 @@ class KeyPoints:
|
||||
data: dict[str, npt.NDArray[Any] | list] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self):
|
||||
validate_keypoints_fields(
|
||||
validate_key_points_fields(
|
||||
xy=self.xy,
|
||||
confidence=self.confidence,
|
||||
class_id=self.class_id,
|
||||
@@ -514,13 +565,13 @@ class KeyPoints:
|
||||
return cls.empty()
|
||||
|
||||
@classmethod
|
||||
def from_transformers(cls, transfomers_results: Any) -> KeyPoints:
|
||||
def from_transformers(cls, transformers_results: Any) -> KeyPoints:
|
||||
"""
|
||||
Create a `sv.KeyPoints` object from the
|
||||
[Transformers](https://github.com/huggingface/transformers) inference result.
|
||||
|
||||
Args:
|
||||
transfomers_results (Any): The output of a
|
||||
transformers_results (Any): The output of a
|
||||
Transformers model containing instances with prediction data.
|
||||
|
||||
Returns:
|
||||
@@ -545,9 +596,9 @@ class KeyPoints:
|
||||
DETECTION_MODEL_ID = "PekingU/rtdetr_r50vd_coco_o365"
|
||||
|
||||
detection_processor = AutoProcessor.from_pretrained(DETECTION_MODEL_ID, use_fast=True)
|
||||
detection_model = RTDetrForObjectDetection.from_pretrained(DETECTION_MODEL_ID, device_map=DEVICE)
|
||||
detection_model = RTDetrForObjectDetection.from_pretrained(DETECTION_MODEL_ID, device_map=device)
|
||||
|
||||
inputs = detection_processor(images=frame, return_tensors="pt").to(DEVICE)
|
||||
inputs = detection_processor(images=frame, return_tensors="pt").to(device)
|
||||
|
||||
with torch.no_grad():
|
||||
outputs = detection_model(**inputs)
|
||||
@@ -563,9 +614,9 @@ class KeyPoints:
|
||||
|
||||
pose_estimation_processor = AutoProcessor.from_pretrained(POSE_ESTIMATION_MODEL_ID)
|
||||
pose_estimation_model = VitPoseForPoseEstimation.from_pretrained(
|
||||
POSE_ESTIMATION_MODEL_ID, device_map=DEVICE)
|
||||
POSE_ESTIMATION_MODEL_ID, device_map=device)
|
||||
|
||||
inputs = pose_estimation_processor(frame, boxes=[boxes], return_tensors="pt").to(DEVICE)
|
||||
inputs = pose_estimation_processor(frame, boxes=[boxes], return_tensors="pt").to(device)
|
||||
|
||||
with torch.no_grad():
|
||||
outputs = pose_estimation_model(**inputs)
|
||||
@@ -576,8 +627,8 @@ class KeyPoints:
|
||||
|
||||
""" # noqa: E501 // docs
|
||||
|
||||
if "keypoints" in transfomers_results[0]:
|
||||
if transfomers_results[0]["keypoints"].cpu().numpy().size == 0:
|
||||
if "keypoints" in transformers_results[0]:
|
||||
if transformers_results[0]["keypoints"].cpu().numpy().size == 0:
|
||||
return cls.empty()
|
||||
|
||||
result_data = [
|
||||
@@ -585,7 +636,7 @@ class KeyPoints:
|
||||
result["keypoints"].cpu().numpy(),
|
||||
result["scores"].cpu().numpy(),
|
||||
)
|
||||
for result in transfomers_results
|
||||
for result in transformers_results
|
||||
]
|
||||
|
||||
xy, scores = zip(*result_data)
|
||||
@@ -599,55 +650,68 @@ class KeyPoints:
|
||||
return cls.empty()
|
||||
|
||||
def __getitem__(
|
||||
self, index: int | slice | list[int] | np.ndarray | str
|
||||
) -> KeyPoints | list | np.ndarray | None:
|
||||
"""
|
||||
Get a subset of the `sv.KeyPoints` object or access an item from its data field.
|
||||
|
||||
When provided with an integer, slice, list of integers, or a numpy array, this
|
||||
method returns a new `sv.KeyPoints` object that represents a subset of the
|
||||
original `sv.KeyPoints`. When provided with a string, it accesses the
|
||||
corresponding item in the data dictionary.
|
||||
|
||||
Args:
|
||||
index (Union[int, slice, List[int], np.ndarray, str]): The index, indices,
|
||||
or key to access a subset of the `sv.KeyPoints` or an item from the
|
||||
data.
|
||||
|
||||
Returns:
|
||||
A subset of the `sv.KeyPoints` object or an item from the data field.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
key_points = sv.KeyPoints()
|
||||
|
||||
# access the first keypoint using an integer index
|
||||
key_points[0]
|
||||
|
||||
# access the first 10 keypoints using index slice
|
||||
key_points[0:10]
|
||||
|
||||
# access selected keypoints using a list of indices
|
||||
key_points[[0, 2, 4]]
|
||||
|
||||
# access keypoints with selected class_id
|
||||
key_points[key_points.class_id == 0]
|
||||
|
||||
# access keypoints with confidence greater than 0.5
|
||||
key_points[key_points.confidence > 0.5]
|
||||
```
|
||||
"""
|
||||
self, index: int | slice | list[int] | np.ndarray | tuple | str
|
||||
) -> KeyPoints | np.ndarray | list | None:
|
||||
if isinstance(index, str):
|
||||
return self.data.get(index)
|
||||
if isinstance(index, int):
|
||||
index = [index]
|
||||
|
||||
if not isinstance(index, tuple):
|
||||
index = (index, slice(None))
|
||||
|
||||
i, j = index
|
||||
|
||||
if isinstance(i, int):
|
||||
i = [i]
|
||||
|
||||
if isinstance(i, list) and all(isinstance(x, bool) for x in i):
|
||||
i = np.array(i)
|
||||
if isinstance(j, list) and all(isinstance(x, bool) for x in j):
|
||||
j = np.array(j)
|
||||
|
||||
if isinstance(i, np.ndarray) and i.dtype == bool:
|
||||
i = np.flatnonzero(i)
|
||||
if isinstance(j, np.ndarray) and j.dtype == bool:
|
||||
j = np.flatnonzero(j)
|
||||
|
||||
if (
|
||||
isinstance(i, (list, np.ndarray))
|
||||
and isinstance(j, (list, np.ndarray))
|
||||
and not np.isscalar(i)
|
||||
and not np.isscalar(j)
|
||||
):
|
||||
i, j = np.ix_(i, j)
|
||||
|
||||
xy_selected = self.xy[i, j]
|
||||
|
||||
conf_selected = self.confidence[i, j] if self.confidence is not None else None
|
||||
|
||||
class_id_selected = self.class_id[i] if self.class_id is not None else None
|
||||
|
||||
data_selected = get_data_item(self.data, i)
|
||||
|
||||
if xy_selected.ndim == 1:
|
||||
xy_selected = xy_selected.reshape(1, 1, 2)
|
||||
if conf_selected is not None:
|
||||
conf_selected = conf_selected.reshape(1, 1)
|
||||
elif xy_selected.ndim == 2:
|
||||
if np.isscalar(index[0]) or (
|
||||
isinstance(index[0], np.ndarray) and index[0].ndim == 0
|
||||
):
|
||||
xy_selected = xy_selected[np.newaxis, ...]
|
||||
if conf_selected is not None:
|
||||
conf_selected = conf_selected[np.newaxis, ...]
|
||||
elif np.isscalar(index[1]) or (
|
||||
isinstance(index[1], np.ndarray) and index[1].ndim == 0
|
||||
):
|
||||
xy_selected = xy_selected[:, np.newaxis, :]
|
||||
if conf_selected is not None:
|
||||
conf_selected = conf_selected[:, np.newaxis]
|
||||
|
||||
return KeyPoints(
|
||||
xy=self.xy[index],
|
||||
confidence=self.confidence[index] if self.confidence is not None else None,
|
||||
class_id=self.class_id[index] if self.class_id is not None else None,
|
||||
data=get_data_item(self.data, index),
|
||||
xy=xy_selected,
|
||||
confidence=conf_selected,
|
||||
class_id=class_id_selected,
|
||||
data=data_selected,
|
||||
)
|
||||
|
||||
def __setitem__(self, key: str, value: np.ndarray | list):
|
||||
@@ -668,12 +732,12 @@ class KeyPoints:
|
||||
model = YOLO('yolov8s.pt')
|
||||
|
||||
result = model(image)[0]
|
||||
keypoints = sv.KeyPoints.from_ultralytics(result)
|
||||
key_points = sv.KeyPoints.from_ultralytics(result)
|
||||
|
||||
keypoints['class_name'] = [
|
||||
key_points['class_name'] = [
|
||||
model.model.names[class_id]
|
||||
for class_id
|
||||
in keypoints.class_id
|
||||
in key_points.class_id
|
||||
]
|
||||
```
|
||||
"""
|
||||
@@ -688,7 +752,7 @@ class KeyPoints:
|
||||
@classmethod
|
||||
def empty(cls) -> KeyPoints:
|
||||
"""
|
||||
Create an empty Keypoints object with no keypoints.
|
||||
Create an empty KeyPoints object with no key points.
|
||||
|
||||
Returns:
|
||||
An empty `sv.KeyPoints` object.
|
||||
@@ -706,9 +770,9 @@ class KeyPoints:
|
||||
"""
|
||||
Returns `True` if the `KeyPoints` object is considered empty.
|
||||
"""
|
||||
empty_keypoints = KeyPoints.empty()
|
||||
empty_keypoints.data = self.data
|
||||
return self == empty_keypoints
|
||||
empty_key_points = KeyPoints.empty()
|
||||
empty_key_points.data = self.data
|
||||
return self == empty_key_points
|
||||
|
||||
def as_detections(
|
||||
self, selected_keypoint_indices: Iterable[int] | None = None
|
||||
@@ -716,21 +780,21 @@ class KeyPoints:
|
||||
"""
|
||||
Convert a KeyPoints object to a Detections object. This
|
||||
approximates the bounding box of the detected object by
|
||||
taking the bounding box that fits all keypoints.
|
||||
taking the bounding box that fits all key points.
|
||||
|
||||
Arguments:
|
||||
selected_keypoint_indices (Optional[Iterable[int]]): The
|
||||
indices of the keypoints to include in the bounding box
|
||||
calculation. This helps focus on a subset of keypoints,
|
||||
e.g. when some are occluded. Captures all keypoints by default.
|
||||
indices of the key points to include in the bounding box
|
||||
calculation. This helps focus on a subset of key points,
|
||||
e.g. when some are occluded. Captures all key points by default.
|
||||
|
||||
Returns:
|
||||
detections (Detections): The converted detections object.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
keypoints = sv.KeyPoints.from_inference(...)
|
||||
detections = keypoints.as_detections()
|
||||
key_points = sv.KeyPoints.from_inference(...)
|
||||
detections = key_points.as_detections()
|
||||
```
|
||||
"""
|
||||
if self.is_empty():
|
||||
@@ -102,12 +102,12 @@ class MeanAveragePrecisionResult:
|
||||
f"maxDets=100 ] = {self.map50:.3f}\n"
|
||||
f"Average Precision (AP) @[ IoU=0.75 | area= all | "
|
||||
f"maxDets=100 ] = {self.map75:.3f}\n"
|
||||
f"Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] "
|
||||
f"= {self.small_objects.map50_95:.3f}\n"
|
||||
f"Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] \
|
||||
= {self.medium_objects.map50_95:.3f}\n"
|
||||
f"Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] \
|
||||
= {self.large_objects.map50_95:.3f}"
|
||||
f"Average Precision (AP) @[ IoU=0.50:0.95 | area= small | "
|
||||
f"maxDets=100 ] = {self.small_objects.map50_95:.3f}\n"
|
||||
f"Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | "
|
||||
f"maxDets=100 ] = {self.medium_objects.map50_95:.3f}\n"
|
||||
f"Average Precision (AP) @[ IoU=0.50:0.95 | area= large | "
|
||||
f"maxDets=100 ] = {self.large_objects.map50_95:.3f}"
|
||||
)
|
||||
|
||||
def to_pandas(self) -> pd.DataFrame:
|
||||
|
||||
@@ -53,7 +53,7 @@ def validate_confidence(confidence: Any, n: int) -> None:
|
||||
)
|
||||
|
||||
|
||||
def validate_keypoint_confidence(confidence: Any, n: int, m: int) -> None:
|
||||
def validate_key_point_confidence(confidence: Any, n: int, m: int) -> None:
|
||||
expected_shape = f"({n, m})"
|
||||
actual_shape = str(getattr(confidence, "shape", None))
|
||||
|
||||
@@ -126,7 +126,7 @@ def validate_detections_fields(
|
||||
validate_data(data, n)
|
||||
|
||||
|
||||
def validate_keypoints_fields(
|
||||
def validate_key_points_fields(
|
||||
xy: Any,
|
||||
class_id: Any,
|
||||
confidence: Any,
|
||||
@@ -136,7 +136,7 @@ def validate_keypoints_fields(
|
||||
m = len(xy[0]) if len(xy) > 0 else 0
|
||||
validate_xy(xy, n, m)
|
||||
validate_class_id(class_id, n)
|
||||
validate_keypoint_confidence(confidence, n, m)
|
||||
validate_key_point_confidence(confidence, n, m)
|
||||
validate_data(data, n)
|
||||
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ from test.test_utils import mock_detections
|
||||
def test_resolve_color_idx(
|
||||
detections: Detections,
|
||||
detection_idx: int,
|
||||
color_lookup: ColorLookup,
|
||||
color_lookup: ColorLookup | np.ndarray,
|
||||
expected_result: int | None,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
|
||||
@@ -6,7 +6,10 @@ from contextlib import nullcontext as does_not_raise
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from supervision.config import CLASS_NAME_DATA_FIELD
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.detection.vlm import (
|
||||
VLM,
|
||||
from_florence_2,
|
||||
from_google_gemini_2_0,
|
||||
from_google_gemini_2_5,
|
||||
@@ -1122,3 +1125,110 @@ def test_from_google_gemini_2_5(
|
||||
assert masks is not None
|
||||
assert masks.shape == expected_results[4].shape
|
||||
assert np.array_equal(masks, expected_results[4])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception, result, resolution_wh, classes, expected_detections",
|
||||
[
|
||||
(
|
||||
pytest.raises(ValueError),
|
||||
"",
|
||||
(100, 100),
|
||||
None,
|
||||
None,
|
||||
), # empty text
|
||||
(
|
||||
pytest.raises(ValueError),
|
||||
"random text",
|
||||
(100, 100),
|
||||
None,
|
||||
None,
|
||||
), # random text
|
||||
(
|
||||
does_not_raise(),
|
||||
"<|ref|>cat<|/ref|><|det|>[[100, 200, 300, 400]]<|/det|>",
|
||||
(1000, 1000),
|
||||
None,
|
||||
Detections(
|
||||
xyxy=np.array([[100.1, 200.2, 300.3, 400.4]]),
|
||||
class_id=np.array([0]),
|
||||
data={CLASS_NAME_DATA_FIELD: np.array(["cat"])},
|
||||
),
|
||||
), # single box, no classes
|
||||
(
|
||||
does_not_raise(),
|
||||
"<|ref|>cat<|/ref|><|det|>[[100, 200, 300, 400]]<|/det|>",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
Detections(
|
||||
xyxy=np.array([[100.1, 200.2, 300.3, 400.4]]),
|
||||
class_id=np.array([0]),
|
||||
data={CLASS_NAME_DATA_FIELD: np.array(["cat"])},
|
||||
),
|
||||
), # single box, with classes
|
||||
(
|
||||
does_not_raise(),
|
||||
"<|ref|>person<|/ref|><|det|>[[100, 200, 300, 400]]<|/det|>",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
Detections.empty(),
|
||||
), # single box, wrong class
|
||||
(
|
||||
does_not_raise(),
|
||||
(
|
||||
"<|ref|>cat<|/ref|><|det|>[[100, 200, 300, 400]]<|/det|>"
|
||||
"<|ref|>dog<|/ref|><|det|>[[500, 600, 700, 800]]<|/det|>"
|
||||
),
|
||||
(1000, 1000),
|
||||
["cat"],
|
||||
Detections(
|
||||
xyxy=np.array([[100.1, 200.2, 300.3, 400.4]]),
|
||||
class_id=np.array([0]),
|
||||
data={CLASS_NAME_DATA_FIELD: np.array(["cat"])},
|
||||
),
|
||||
), # multiple boxes, one class correct
|
||||
(
|
||||
pytest.raises(ValueError),
|
||||
"<|ref|>cat<|/ref|>",
|
||||
(100, 100),
|
||||
None,
|
||||
None,
|
||||
), # only ref
|
||||
(
|
||||
pytest.raises(ValueError),
|
||||
"<|det|>[[100, 200, 300, 400]]<|/det|>",
|
||||
(100, 100),
|
||||
None,
|
||||
None,
|
||||
), # only det
|
||||
],
|
||||
)
|
||||
def test_from_deepseek_vl_2(
|
||||
exception,
|
||||
result: str,
|
||||
resolution_wh: tuple[int, int],
|
||||
classes: list[str] | None,
|
||||
expected_detections: Detections,
|
||||
):
|
||||
with exception:
|
||||
detections = Detections.from_vlm(
|
||||
vlm=VLM.DEEPSEEK_VL_2,
|
||||
result=result,
|
||||
resolution_wh=resolution_wh,
|
||||
classes=classes,
|
||||
)
|
||||
|
||||
if expected_detections is None:
|
||||
return
|
||||
|
||||
assert len(detections) == len(expected_detections)
|
||||
|
||||
if len(detections) == 0:
|
||||
return
|
||||
|
||||
assert np.allclose(detections.xyxy, expected_detections.xyxy, atol=1e-1)
|
||||
assert np.array_equal(detections.class_id, expected_detections.class_id)
|
||||
assert np.array_equal(
|
||||
detections.data[CLASS_NAME_DATA_FIELD],
|
||||
expected_detections.data[CLASS_NAME_DATA_FIELD],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import pytest
|
||||
|
||||
from supervision.detection.utils.vlms import edit_distance, fuzzy_match_index
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"string_1, string_2, case_sensitive, expected_result",
|
||||
[
|
||||
# identical strings, various cases
|
||||
("hello", "hello", True, 0),
|
||||
("hello", "hello", False, 0),
|
||||
# case sensitive vs insensitive
|
||||
("Test", "test", True, 1),
|
||||
("Test", "test", False, 0),
|
||||
("CASE", "case", True, 4),
|
||||
("CASE", "case", False, 0),
|
||||
# completely different
|
||||
("abc", "xyz", True, 3),
|
||||
("abc", "xyz", False, 3),
|
||||
# one string empty
|
||||
("hello", "", True, 5),
|
||||
("", "world", True, 5),
|
||||
# single character cases
|
||||
("a", "b", True, 1),
|
||||
("A", "a", True, 1),
|
||||
("A", "a", False, 0),
|
||||
# whitespaces
|
||||
("hello world", "helloworld", True, 1),
|
||||
("test", " test", True, 1),
|
||||
# unicode and emoji
|
||||
("😊", "😊", True, 0),
|
||||
("😊", "😢", True, 1),
|
||||
# long string vs empty
|
||||
("a" * 100, "", True, 100),
|
||||
("", "b" * 100, True, 100),
|
||||
# prefix/suffix
|
||||
("prefix", "prefixes", True, 2),
|
||||
("suffix", "asuffix", True, 1),
|
||||
# leading/trailing whitespace
|
||||
(" hello", "hello", True, 1),
|
||||
("hello", "hello ", True, 1),
|
||||
# long almost-equal string
|
||||
(
|
||||
"The quick brown fox jumps over the lazy dog",
|
||||
"The quick brown fox jumps over the lazy cog",
|
||||
True,
|
||||
1,
|
||||
),
|
||||
(
|
||||
"The quick brown fox jumps over the lazy dog",
|
||||
"The quick brown fox jumps over the lazy cog",
|
||||
False,
|
||||
1,
|
||||
),
|
||||
# both empty
|
||||
("", "", True, 0),
|
||||
("", "", False, 0),
|
||||
# mixed case with symbols
|
||||
("123ABC!", "123abc!", True, 3),
|
||||
("123ABC!", "123abc!", False, 0),
|
||||
],
|
||||
)
|
||||
def test_edit_distance(string_1, string_2, case_sensitive, expected_result):
|
||||
assert (
|
||||
edit_distance(string_1, string_2, case_sensitive=case_sensitive)
|
||||
== expected_result
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"candidates, query, threshold, case_sensitive, expected_result",
|
||||
[
|
||||
# exact match at index 0
|
||||
(["cat", "dog", "rat"], "cat", 0, True, 0),
|
||||
# match at index 2 within threshold
|
||||
(["cat", "dog", "rat"], "dat", 1, True, 0),
|
||||
# no match due to high threshold
|
||||
(["cat", "dog", "rat"], "bat", 0, True, None),
|
||||
# multiple possible matches, returns first
|
||||
(["apple", "apply", "appla"], "apple", 1, True, 0),
|
||||
# case-insensitive match
|
||||
(["Alpha", "beta", "Gamma"], "alpha", 0, False, 0),
|
||||
# case-sensitive: no match
|
||||
(["Alpha", "beta", "Gamma"], "alpha", 0, True, None),
|
||||
# threshold boundary
|
||||
(["alpha", "beta", "gamma"], "bata", 1, True, 1),
|
||||
# no match (all distances too high)
|
||||
(["one", "two", "three"], "ten", 1, True, None),
|
||||
# unicode/emoji match
|
||||
(["😊", "😢", "😁"], "😄", 1, True, 0),
|
||||
(["😊", "😢", "😁"], "😊", 0, True, 0),
|
||||
# empty candidates
|
||||
([], "any", 2, True, None),
|
||||
# empty query, non-empty candidates
|
||||
(["", "abc"], "", 0, True, 0),
|
||||
(["", "abc"], "", 1, True, 0),
|
||||
(["a", "b", "c"], "", 1, True, 0),
|
||||
# non-empty query, empty candidate
|
||||
(["", ""], "a", 1, True, 0),
|
||||
# all candidates require higher edit than threshold
|
||||
(["short", "words", "only"], "longerword", 2, True, None),
|
||||
# repeated candidates
|
||||
(["a", "a", "a"], "b", 1, True, 0),
|
||||
],
|
||||
)
|
||||
def test_fuzzy_match_index(
|
||||
candidates, query, threshold, case_sensitive, expected_result
|
||||
):
|
||||
assert (
|
||||
fuzzy_match_index(
|
||||
candidates=candidates,
|
||||
query=query,
|
||||
threshold=threshold,
|
||||
case_sensitive=case_sensitive,
|
||||
)
|
||||
== expected_result
|
||||
)
|
||||
@@ -0,0 +1,268 @@
|
||||
from contextlib import nullcontext as DoesNotRaise
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from supervision.key_points.core import KeyPoints
|
||||
from test.test_utils import mock_key_points
|
||||
|
||||
KEY_POINTS = mock_key_points(
|
||||
xy=[
|
||||
[[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]],
|
||||
[[10, 11], [12, 13], [14, 15], [16, 17], [18, 19]],
|
||||
[[20, 21], [22, 23], [24, 25], [26, 27], [28, 29]],
|
||||
],
|
||||
confidence=[
|
||||
[0.8, 0.2, 0.6, 0.1, 0.5],
|
||||
[0.7, 0.9, 0.3, 0.4, 0.0],
|
||||
[0.1, 0.6, 0.8, 0.2, 0.7],
|
||||
],
|
||||
class_id=[0, 1, 2],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"key_points, index, expected_result, exception",
|
||||
[
|
||||
(
|
||||
KeyPoints.empty(),
|
||||
slice(None),
|
||||
KeyPoints.empty(),
|
||||
DoesNotRaise(),
|
||||
), # slice all key points when key points object empty
|
||||
(
|
||||
KEY_POINTS,
|
||||
slice(None),
|
||||
KEY_POINTS,
|
||||
DoesNotRaise(),
|
||||
), # slice all key points when key points object nonempty
|
||||
(
|
||||
KEY_POINTS,
|
||||
slice(0, 1),
|
||||
mock_key_points(
|
||||
xy=[[[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]]],
|
||||
confidence=[[0.8, 0.2, 0.6, 0.1, 0.5]],
|
||||
class_id=[0],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # select the first skeleton by slice
|
||||
(
|
||||
KEY_POINTS,
|
||||
slice(0, 2),
|
||||
mock_key_points(
|
||||
xy=[
|
||||
[[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]],
|
||||
[[10, 11], [12, 13], [14, 15], [16, 17], [18, 19]],
|
||||
],
|
||||
confidence=[
|
||||
[0.8, 0.2, 0.6, 0.1, 0.5],
|
||||
[0.7, 0.9, 0.3, 0.4, 0.0],
|
||||
],
|
||||
class_id=[0, 1],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # select the first skeleton by slice
|
||||
(
|
||||
KEY_POINTS,
|
||||
0,
|
||||
mock_key_points(
|
||||
xy=[[[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]]],
|
||||
confidence=[[0.8, 0.2, 0.6, 0.1, 0.5]],
|
||||
class_id=[0],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # select the first skeleton by index
|
||||
(
|
||||
KEY_POINTS,
|
||||
-1,
|
||||
mock_key_points(
|
||||
xy=[[[20, 21], [22, 23], [24, 25], [26, 27], [28, 29]]],
|
||||
confidence=[[0.1, 0.6, 0.8, 0.2, 0.7]],
|
||||
class_id=[2],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # select the last skeleton by index
|
||||
(
|
||||
KEY_POINTS,
|
||||
[0, 1],
|
||||
mock_key_points(
|
||||
xy=[
|
||||
[[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]],
|
||||
[[10, 11], [12, 13], [14, 15], [16, 17], [18, 19]],
|
||||
],
|
||||
confidence=[
|
||||
[0.8, 0.2, 0.6, 0.1, 0.5],
|
||||
[0.7, 0.9, 0.3, 0.4, 0.0],
|
||||
],
|
||||
class_id=[0, 1],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # select the first two skeletons by index; list
|
||||
(
|
||||
KEY_POINTS,
|
||||
np.array([0, 1]),
|
||||
mock_key_points(
|
||||
xy=[
|
||||
[[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]],
|
||||
[[10, 11], [12, 13], [14, 15], [16, 17], [18, 19]],
|
||||
],
|
||||
confidence=[
|
||||
[0.8, 0.2, 0.6, 0.1, 0.5],
|
||||
[0.7, 0.9, 0.3, 0.4, 0.0],
|
||||
],
|
||||
class_id=[0, 1],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # select the first two skeletons by index; np.array
|
||||
(
|
||||
KEY_POINTS,
|
||||
[True, True, False],
|
||||
mock_key_points(
|
||||
xy=[
|
||||
[[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]],
|
||||
[[10, 11], [12, 13], [14, 15], [16, 17], [18, 19]],
|
||||
],
|
||||
confidence=[
|
||||
[0.8, 0.2, 0.6, 0.1, 0.5],
|
||||
[0.7, 0.9, 0.3, 0.4, 0.0],
|
||||
],
|
||||
class_id=[0, 1],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # select only skeletons associated with positive filter; list
|
||||
(
|
||||
KEY_POINTS,
|
||||
np.array([True, True, False]),
|
||||
mock_key_points(
|
||||
xy=[
|
||||
[[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]],
|
||||
[[10, 11], [12, 13], [14, 15], [16, 17], [18, 19]],
|
||||
],
|
||||
confidence=[
|
||||
[0.8, 0.2, 0.6, 0.1, 0.5],
|
||||
[0.7, 0.9, 0.3, 0.4, 0.0],
|
||||
],
|
||||
class_id=[0, 1],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # select only skeletons associated with positive filter; list
|
||||
(
|
||||
KEY_POINTS,
|
||||
(slice(None), slice(None)),
|
||||
KEY_POINTS,
|
||||
DoesNotRaise(),
|
||||
), # slice all anchors from all skeletons
|
||||
(
|
||||
KEY_POINTS,
|
||||
(slice(None), slice(0, 1)),
|
||||
mock_key_points(
|
||||
xy=[[[0, 1]], [[10, 11]], [[20, 21]]],
|
||||
confidence=[[0.8], [0.7], [0.1]],
|
||||
class_id=[0, 1, 2],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # slice the first anchor from every skeleton
|
||||
(
|
||||
KEY_POINTS,
|
||||
(slice(None), slice(0, 2)),
|
||||
mock_key_points(
|
||||
xy=[[[0, 1], [2, 3]], [[10, 11], [12, 13]], [[20, 21], [22, 23]]],
|
||||
confidence=[[0.8, 0.2], [0.7, 0.9], [0.1, 0.6]],
|
||||
class_id=[0, 1, 2],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # slice the first anchor two anchors from every skeleton
|
||||
(
|
||||
KEY_POINTS,
|
||||
(slice(None), 0),
|
||||
mock_key_points(
|
||||
xy=[[[0, 1]], [[10, 11]], [[20, 21]]],
|
||||
confidence=[[0.8], [0.7], [0.1]],
|
||||
class_id=[0, 1, 2],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # select the first anchor from every skeleton by index
|
||||
(
|
||||
KEY_POINTS,
|
||||
(slice(None), -1),
|
||||
mock_key_points(
|
||||
xy=[[[8, 9]], [[18, 19]], [[28, 29]]],
|
||||
confidence=[[0.5], [0.0], [0.7]],
|
||||
class_id=[0, 1, 2],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # select the last anchor from every skeleton by index
|
||||
(
|
||||
KEY_POINTS,
|
||||
(slice(None), [0, 1]),
|
||||
mock_key_points(
|
||||
xy=[[[0, 1], [2, 3]], [[10, 11], [12, 13]], [[20, 21], [22, 23]]],
|
||||
confidence=[[0.8, 0.2], [0.7, 0.9], [0.1, 0.6]],
|
||||
class_id=[0, 1, 2],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # select the first two anchors from every skeleton by index; list
|
||||
(
|
||||
KEY_POINTS,
|
||||
(slice(None), np.array([0, 1])),
|
||||
mock_key_points(
|
||||
xy=[[[0, 1], [2, 3]], [[10, 11], [12, 13]], [[20, 21], [22, 23]]],
|
||||
confidence=[[0.8, 0.2], [0.7, 0.9], [0.1, 0.6]],
|
||||
class_id=[0, 1, 2],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # select the first two anchors from every skeleton by index; np.array
|
||||
(
|
||||
KEY_POINTS,
|
||||
(slice(None), [True, True, False, False, False]),
|
||||
mock_key_points(
|
||||
xy=[[[0, 1], [2, 3]], [[10, 11], [12, 13]], [[20, 21], [22, 23]]],
|
||||
confidence=[[0.8, 0.2], [0.7, 0.9], [0.1, 0.6]],
|
||||
class_id=[0, 1, 2],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # select only anchors associated with positive filter; list
|
||||
(
|
||||
KEY_POINTS,
|
||||
(slice(None), np.array([True, True, False, False, False])),
|
||||
mock_key_points(
|
||||
xy=[[[0, 1], [2, 3]], [[10, 11], [12, 13]], [[20, 21], [22, 23]]],
|
||||
confidence=[[0.8, 0.2], [0.7, 0.9], [0.1, 0.6]],
|
||||
class_id=[0, 1, 2],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # select only anchors associated with positive filter; np.array
|
||||
(
|
||||
KEY_POINTS,
|
||||
(0, 0),
|
||||
mock_key_points(
|
||||
xy=[
|
||||
[[0, 1]],
|
||||
],
|
||||
confidence=[
|
||||
[0.8],
|
||||
],
|
||||
class_id=[0],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # select the first anchor from the first skeleton by index
|
||||
(
|
||||
KEY_POINTS,
|
||||
(0, -1),
|
||||
mock_key_points(
|
||||
xy=[
|
||||
[[8, 9]],
|
||||
],
|
||||
confidence=[
|
||||
[0.5],
|
||||
],
|
||||
class_id=[0],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # select the last anchor from the first skeleton by index
|
||||
],
|
||||
)
|
||||
def test_key_points_getitem(key_points, index, expected_result, exception):
|
||||
with exception:
|
||||
result = key_points[index]
|
||||
assert result == expected_result
|
||||
+5
-6
@@ -3,14 +3,13 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.keypoint.core import KeyPoints
|
||||
from supervision.key_points.core import KeyPoints
|
||||
|
||||
|
||||
def mock_detections(
|
||||
xyxy: npt.NDArray[np.float32],
|
||||
xyxy: list[list[float]],
|
||||
mask: list[np.ndarray] | None = None,
|
||||
confidence: list[float] | None = None,
|
||||
class_id: list[int] | None = None,
|
||||
@@ -34,9 +33,9 @@ def mock_detections(
|
||||
)
|
||||
|
||||
|
||||
def mock_keypoints(
|
||||
xy: npt.NDArray[np.float32],
|
||||
confidence: list[float] | None = None,
|
||||
def mock_key_points(
|
||||
xy: list[list[list[float]]],
|
||||
confidence: list[list[float]] | None = None,
|
||||
class_id: list[int] | None = None,
|
||||
data: dict[str, list[Any]] | None = None,
|
||||
) -> KeyPoints:
|
||||
|
||||
Reference in New Issue
Block a user