Compare commits

...
53 Commits
Author SHA1 Message Date
SkalskiP e1393691a4 bump version from 0.26.1 to 0.27.0rc0 2025-07-31 17:49:06 +02:00
Piotr Skalski 90f8582e4f Merge pull request #1919 from Ashp116/feature/spline-tracer
FEATURE: Spline Annotations
2025-07-31 16:53:33 +02:00
Ashp116 0c9fea4d9f UPDATE: Change Smooth to bool 2025-07-30 18:24:14 -04:00
Ashp116 cdca2e6b51 UPDATE: Removed SplineAnnotator and added smooth flag for TraceAnnotator 2025-07-30 13:48:38 -04:00
pre-commit-ci[bot] 8407d5a692 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-30 07:15:02 +00:00
Ashp116 eaf6cdd477 Precommit 2025-07-30 03:14:00 -04:00
pre-commit-ci[bot] f447b6fa95 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-30 07:10:19 +00:00
Ashp116 8c6ac2dd29 UPDATE: Made smoothing and spline_order as hyperparameters 2025-07-30 02:53:58 -04:00
Ashp116 22b6d3d464 ADD: Added a spline annotator 2025-07-30 02:04:24 -04:00
Piotr Skalski dda3900943 Merge pull request #1917 from roboflow/feat/label-annotator-offset
add: support for an additional parameter `text_anchor_offset` in label annotators
2025-07-29 14:50:34 +02:00
Piotr Skalski ac0c367e81 Merge pull request #1914 from roboflow/dependabot/github_actions/develop/astral-sh/setup-uv-6.4.3
⬆️ Bump astral-sh/setup-uv from 6.4.1 to 6.4.3
2025-07-29 14:48:27 +02:00
SkalskiP 6f2490cc23 fix formatting 2025-07-29 14:43:38 +02:00
Piotr Skalski 4a9d06eb50 Merge pull request #1918 from roboflow/pre-commit-ci-update-config
chore(pre_commit): ⬆ pre_commit autoupdate
2025-07-29 14:40:38 +02:00
pre-commit-ci[bot] 21e311a319 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-29 12:40:11 +00:00
SkalskiP 71a14812fe LabelAnnotator and RichLabelAnnotator docs improvement 2025-07-29 14:39:41 +02:00
SkalskiP 35fdcb0a1a Merge branch 'develop' into feat/label-annotator-offset 2025-07-29 12:46:06 +02:00
Piotr Skalski 6d26a9766c Merge pull request #1915 from roboflow/feature/advanced_keypoints_indexing_and_slicing
advanced `sv.KeyPoints` filtering and slicing
2025-07-29 12:45:29 +02:00
SkalskiP 1cfaef9405 Merge branch 'develop' into feature/advanced_keypoints_indexing_and_slicing 2025-07-29 12:42:34 +02:00
pre-commit-ci[bot] 063e2092c8 chore(pre_commit): ⬆ pre_commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.12.4 → v0.12.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.4...v0.12.5)
2025-07-28 17:28:44 +00:00
soumik12345 cb215d26f3 add: support for an additional parameter text_anchor_offset in _BaseLabelAnnotator 2025-07-28 19:03:17 +05:30
Soumik Rakshit 8bbe80e683 Merge pull request #1916 from AlexBodner/fix/docs-vitpose
Corrected vitpose docstring
2025-07-28 18:38:22 +05:30
Alex Bodner 9a0483446a fixed docs 2025-07-28 09:53:44 -03:00
pre-commit-ci[bot] f4561f9ba7 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-28 06:29:08 +00:00
SkalskiP beaf89cb22 initial version of advanced key points filtering and slicing + some naming refactor 2025-07-28 08:24:43 +02:00
dependabot[bot] 96b58c2860 ⬆️ Bump astral-sh/setup-uv from 6.4.1 to 6.4.3
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 6.4.1 to 6.4.3.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/7edac99f961f18b581bbd960d59d049f04c0002f...e92bafb6253dcd438e0484186d7669ea7a8ca1cc)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 6.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 00:07:27 +00:00
Piotr Skalski 8873da4a30 Merge pull request #1902 from roboflow/dependabot/github_actions/develop/astral-sh/setup-uv-6.4.1
⬆️ Bump astral-sh/setup-uv from 6.3.1 to 6.4.1
2025-07-26 17:57:44 +02:00
Piotr Skalski 1362850986 Merge pull request #1904 from roboflow/pre-commit-ci-update-config
chore(pre_commit): ⬆ pre_commit autoupdate
2025-07-26 17:57:15 +02:00
Piotr Skalski 98a4391677 Merge pull request #1913 from roboflow/fix/pending_track_id
fix for #1911
2025-07-26 17:51:14 +02:00
SkalskiP 036a956473 fix for #1911 2025-07-26 17:46:04 +02:00
Piotr Skalski b381fa198f Merge pull request #1884 from roboflow/feat/from_deepseek_vl_2_support
feat: 🚀 from_vlm deepseek vl 2 support
2025-07-25 16:57:44 +02:00
pre-commit-ci[bot] 8ca2e9fe8d fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-25 14:44:15 +00:00
soumik12345 bc1d73a307 chore: address feedback 2025-07-25 20:13:28 +05:30
Piotr Skalski 9679cd5c4e Merge pull request #1912 from roboflow/feature/edit_distance_and_fuzzy_match
`edit_distance` and `fuzzy_match_index` that will allow to match faulty class name generated by VLMs with exact class names
2025-07-25 14:52:24 +02:00
pre-commit-ci[bot] 00817df9e2 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-25 11:41:30 +00:00
SkalskiP ec189bf388 new, more memory efficient edit_distance implementation 2025-07-25 13:41:03 +02:00
SkalskiP 58bdce5707 tests fixed 2025-07-24 15:13:46 +02:00
pre-commit-ci[bot] 8262b6e64b fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-24 13:06:48 +00:00
SkalskiP 72fe0bf47f initial version of edit_distance and fuzzy_match_index that will allow to match faulty class name generated by VLMs with exact class names 2025-07-24 15:05:19 +02:00
soumik12345 137078602b add: tests 2025-07-24 15:17:05 +05:30
soumik12345 8fa21dd165 update: from_deepseek_vl_2 2025-07-24 15:04:59 +05:30
soumik12345 da70389546 add: prompt engineering tip for deepseek-vl2 2025-07-24 14:23:24 +05:30
soumik12345 cc1525707a update: docstring for Detections.from_vlm 2025-07-24 13:11:06 +05:30
soumik12345 b3aee43c3b update: from_deepseek_vl_2 logic 2025-07-24 12:55:40 +05:30
Soumik Rakshit 932c1aa91e Merge pull request #1910 from rafaelpadilla/fix/import_box_iou_batch
exposing `box_iou_batch`  in `supervision/detection/utils__init__.py`
2025-07-23 22:00:55 +05:30
rcvsq 76ee05420d exposing box_iou_batch in supervision/detection/utils__init__.py 2025-07-23 14:58:09 +00:00
soumik12345 a470bc2976 chore: make pre-commit happy 2025-07-23 18:36:31 +05:30
Soumik Rakshit cf725dd4db Merge branch 'develop' into feat/from_deepseek_vl_2_support 2025-07-23 18:31:07 +05:30
pre-commit-ci[bot] b1780fbae5 chore(pre_commit): ⬆ pre_commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.12.3 → v0.12.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.3...v0.12.4)
2025-07-21 17:29:41 +00:00
dependabot[bot] 1e84351d60 ⬆️ Bump astral-sh/setup-uv from 6.3.1 to 6.4.1
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 6.3.1 to 6.4.1.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/bd01e18f51369d5a26f1651c3cb451d3417e3bba...7edac99f961f18b581bbd960d59d049f04c0002f)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 6.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 00:32:44 +00:00
Onuralp SEZER 535cb2a91b fix(vlm): 📝 correct formatting in docstring for from_deepseek_vl_2 function 2025-07-11 18:49:50 +03:00
pre-commit-ci[bot] 8e845caeeb fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-11 15:44:15 +00:00
Onuralp SEZER 9e2a535044 Merge branch 'develop' into feat/from_deepseek_vl_2_support 2025-07-11 18:43:02 +03:00
SkalskiP f737782ae8 initial commit 2025-04-07 14:56:58 +02:00
34 changed files with 1153 additions and 139 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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]
+18
View File
@@ -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
+2 -2
View File
@@ -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).
+6 -6
View File
@@ -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
+1 -1
View File
@@ -5,4 +5,4 @@ status: new
# Keypoint Detection
:::supervision.keypoint.core.KeyPoints
:::supervision.key_points.core.KeyPoints
+3 -1
View File
@@ -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:
+1
View File
@@ -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
View File
@@ -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 = [
+5 -2
View File
@@ -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
View File
@@ -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
)
```
![label-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/label-annotator-example-purple.png)
"""
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,
+5 -1
View File
@@ -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)
+91
View File
@@ -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:
+3
View File
@@ -0,0 +1,3 @@
from supervision.detection.utils.iou_and_nms import box_iou_batch
__all__ = ["box_iou_batch"]
+105
View File
@@ -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
+78
View File
@@ -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:
+3 -3
View File
@@ -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)
+1 -1
View File
@@ -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:
+110
View File
@@ -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],
)
+117
View File
@@ -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
)
View File
+268
View File
@@ -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
View File
@@ -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: