Compare commits

...
21 Commits
Author SHA1 Message Date
87b1b86f39 releasing 0.28.0 (#2220)
* docs: add 0.28.0 changelog entries and release demo notebook
* feat: add script for converting Python scripts to Jupyter notebooks
* chore: reorganize imports in release-demo notebook and update lint config
* docs: fix broken link in changelog entry for ConfusionMatrix

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-04-30 17:11:50 +02:00
pre-commit-ci[bot] 1170a90ffc chore(pre_commit): ⬆ pre_commit autoupdate (#2235)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.15.11 → v0.15.12](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.11...v0.15.12)
- [github.com/pre-commit/mirrors-mypy: v1.20.1 → v1.20.2](https://github.com/pre-commit/mirrors-mypy/compare/v1.20.1...v1.20.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-04-28 09:03:07 +02:00
dependabot[bot] 2e0e8d9132 ⬆️ Update wheel requirement from <0.47,>=0.40 to >=0.40,<0.48 (#2234)
Updates the requirements on [wheel](https://github.com/pypa/wheel) to permit the latest version.
- [Release notes](https://github.com/pypa/wheel/releases)
- [Changelog](https://github.com/pypa/wheel/blob/main/docs/news.rst)
- [Commits](https://github.com/pypa/wheel/compare/0.40.0...0.47.0)

---
updated-dependencies:
- dependency-name: wheel
  dependency-version: 0.47.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 09:28:09 +02:00
Jirka BorovecandCopilot 15730b2b5d docs: update author references and improve metadata (#2233)
- Standardize author naming by removing aliases (e.g., "SkalskiP" → "Piotr Skalski").
- Refine JSON-LD schema with added fields, `speakable` specifications, and expanded FAQs.
- Update copyright year to 2026.
- Highlight project milestones (GitHub stars and PyPI downloads) for visibility.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-25 07:15:59 +02:00
2db1e3af34 docs: improve SEO and GEO docs publishing (#2232)
* docs: improve SEO and GEO docs publishing
* docs: refine language for model compatibility and package features
* fix: correct spelling "Underestand" → "Understand" in notebook title
* fix: derive JSON-LD SearchAction URL from config.site_url
* lint: reformat notebook JSON (indent 1→4, pretty-format-json)
* docs: refine description for `supervision.assets` video addition

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-04-24 23:39:22 +02:00
jirka 840226afce chore: update pre-commit hooks configuration
- Add `check-json` hook and re-add `check-yaml` hook.
- Configure `check-added-large-files` with a max size of 500 KB.
- Format `ruff-check` hook arguments for consistency.
2026-04-24 18:07:59 +02:00
dc03ceac31 fix(docs): expand llms.txt, add FAQ schema, author attribution to how… (#2231)
- Expand llms.txt from 12 to 36+ URLs with FAQ, benchmarking, and API sections
- FAQPage schema: 5 → 12 Q&As in theme/main.html
- TechArticle schema: add dynamic author/date from page meta
- Add authors + date_modified frontmatter to all 8 how-to pages
- Add description meta to 7 reference pages (detection/core, trackers, datasets/core, metrics/mean_average_precision, cookbooks, assets, changelog)
- FAQ sections to all 8 how-to pages (Q&A format for AI citation)
- Author bios at bottom of each how-to page (E-E-A-T signal)
- FAQPage + TechArticle schema for how_to, reference, and cookbook pages in theme/main.html
- llms.txt Key APIs: expand from bare list to descriptive prose paragraphs
- Changelog: add date_modified frontmatter

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-23 18:33:14 +02:00
dependabot[bot] f9ab57afde ⬆️ Bump nbconvert from 7.17.0 to 7.17.1 in the uv group across 1 directory (#2226)
Bumps the uv group with 1 update in the / directory: [nbconvert](https://github.com/jupyter/nbconvert).


Updates `nbconvert` from 7.17.0 to 7.17.1
- [Release notes](https://github.com/jupyter/nbconvert/releases)
- [Changelog](https://github.com/jupyter/nbconvert/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jupyter/nbconvert/compare/v7.17.0...v7.17.1)

---
updated-dependencies:
- dependency-name: nbconvert
  dependency-version: 7.17.1
  dependency-type: direct:development
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 20:33:54 +02:00
pre-commit-ci[bot] fb19852919 chore(pre_commit): ⬆ pre_commit autoupdate (#2225)
updates:
- [github.com/JoC0de/pre-commit-prettier: v3.8.2 → v3.8.3](https://github.com/JoC0de/pre-commit-prettier/compare/v3.8.2...v3.8.3)
- [github.com/astral-sh/ruff-pre-commit: v0.15.10 → v0.15.11](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.10...v0.15.11)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-04-22 20:29:35 +02:00
jirka 60f8d28535 releasing 0.28.0.rc2 2026-04-22 18:21:43 +02:00
153cde5854 feat(compact_mask): add resize() method and benchmark stage (#2227)
* feat: add resize() method and benchmark stage
* perf: optimise resize() — vectorised coords, L3 direct RLE
* refactor: split _rle_resize and extract _resize_crop
* test: expand resize() tests for scaling and edge cases
* refactor: switch resize helpers to F-order (column-major) RLE
* fix: merge True/True RLE junctions in _rle_join_cols
* perf: vectorize _rle_scale_col RLE re-encoding
* test: add density-dispatch and parallel-path resize tests
* refactor: harden resize() threading and RLE invariants
* fix: accurate resize timing and exact nearest-neighbour parity
* type: add explicit numpy typing to ndarray declarations

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-22 15:34:12 +02:00
de87030a21 refactor(rle): consolidate RLE primitives (#2230)
* consolidate RLE primitives
* remove wrapper fns, inline converters in CompactMask
* correct _delta_encode doctest example input
* add coverage for multi-byte base48, negative deltas, oversized RLE
* docs: fix RLE decoder example
* coerce mask_2d to bool before view(uint8) in _mask_to_rle_counts

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
2026-04-22 12:32:14 +02:00
5a59dcdec6 refactor(masks): switch CompactMask RLE to F-order (#2228)
* refactor(masks): switch CompactMask RLE to F-order
* docs(masks): update _rle_area docstring example to F-order RLE
* test(masks): verify _rle_encode produces COCO-compatible F-order RLE

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-22 08:12:00 +02:00
jirka 87e8153876 chore: extend .gitignore for Claude-related scratchpad directories... 2026-04-21 18:03:43 +02:00
c4a3038c57 feat(docs): add GEO infrastructure for AI search visibility (#2224)
* feat: add GEO infrastructure for AI search visibility
* fix: guard GEO blocks with {% if page %} to fix 404 build error
* feat: GEO platform optimization — FAQPage, IndexNow, answer paragraphs
* fix: move Disallow:/0.*/ to User-agent:* in robots.txt
* fix: block all numeric versioned paths in robots.txt

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-04-21 11:40:44 +02:00
dependabot[bot] 6069c477b0 ⬆️ Bump astral-sh/setup-uv from 8.0.0 to 8.1.0 in the github-actions group (#2223)
Bumps the github-actions group with 1 update: [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv).


Updates `astral-sh/setup-uv` from 8.0.0 to 8.1.0
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/cec208311dfd045dd5311c1add060b2062131d57...08807647e7069bb48b6ef5acd8ec9567f424441b)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 8.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 10:28:20 +02:00
6beabe3fb4 fix(dataset): reject COCO annotations whose file_name escapes the images directory (#2218)
* test: add regression tests for COCO loader path containment
* fix: reject COCO annotations whose file_name escapes the images directory
* docs: document ValueError contract in load_coco_annotations
* test: cover COCO equality-guard branch (file_name → images dir)
* test: cover COCO is_dir-guard branch (file_name → subdirectory)
* fix: wrap Path.resolve() for untrusted file_name in try/except
* test: add positive test for valid nested file_name in COCO loader
* test: add missing docstrings to COCO path-containment tests
* test: add `from __future__ import annotations` to JSON and CSV tests
* fix: replace `os.path.join` with `Path` for consistent path handling in COCO loader

---------

Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-04-18 00:58:44 +02:00
8df86e2599 fix(annotators): TraceAnnotator handles stationary tracker ids in smooth mode (#2217)
* test: add regression test for TraceAnnotator smooth on stationary tracker
* fix: TraceAnnotator handles stationary tracker ids in smooth mode
* test: strengthen moving-tracker smooth assertion
* fix: guard splprep against collinear input; use unique_xy in fallback
* test: add boundary and regression tests for smooth mode
* docs: document smooth fallback behavior in TraceAnnotator
* test: add shape preservation assertions in annotate tests

---------

Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-04-18 00:50:15 +02:00
06beb6ff2f fix(sinks): slice list and tuple custom_data values per row (#2216)
* test: add regression tests for list/tuple custom_data slicing
* fix: slice list and tuple custom_data values per row
* docs: document custom_data slicing contract in append() docstrings
* docs: add docstring to _slice_value in CSVSink and JSONSink
* docs: add docstring to parse_detection_data in CSVSink and JSONSink
* test: add test for detections.data with plain Python list values
* test: add _slice_value edge-case unit tests
* docs: add per-row slicing note to CSVSink and JSONSink class docstrings

---------

Co-authored-by: jirka <6035284+Borda@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
2026-04-18 00:24:36 +02:00
Jirka Borovec 5d165e04c2 docs: update image links in README for accurate asset references (#2222) 2026-04-17 21:47:02 +02:00
jirka a5c4ebad08 docs: add "Compact Mask" section to detection module in docs 2026-04-17 16:43:49 +02:00
58 changed files with 4509 additions and 247 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: 🐍 Install uv and set Python version ${{ inputs.python-version }}
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
python-version: ${{ inputs.python-version }}
activate-environment: true
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: 🐍 Install uv and set Python
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
python-version: "3.10"
activate-environment: true
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
python-version: ${{ matrix.python-version }}
activate-environment: true
+97 -3
View File
@@ -1,9 +1,15 @@
name: Docs/Build and Publish
# Deploy matrix:
# push develop -> mike deploy develop
# push release/latest -> mike deploy latest
# release published -> mike deploy <tag> only; does not move /latest/
on:
push:
branches:
- develop
- release/latest
workflow_dispatch:
release:
types: [published]
@@ -31,7 +37,7 @@ jobs:
fetch-depth: 0
- name: 🐍 Install uv and set Python
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
python-version: "3.10"
activate-environment: true
@@ -51,10 +57,98 @@ jobs:
run: |
mike deploy --push develop
- name: 🚀 Deploy Latest Docs
if: github.event_name == 'push' && github.ref == 'refs/heads/release/latest'
env:
MKDOCS_GIT_COMMITTERS_APIKEY: ${{ secrets.GITHUB_TOKEN }}
run: |
mike deploy --push latest
- name: 🚀 Deploy Release Docs
if: github.event_name == 'release' && github.event.action == 'published'
env:
MKDOCS_GIT_COMMITTERS_APIKEY: ${{ secrets.GITHUB_TOKEN }}
run: |
latest_tag=$(git describe --tags `git rev-list --tags --max-count=1`)
mike deploy --push --update-aliases $latest_tag latest
release_tag="${GITHUB_REF_NAME#v}"
mike deploy --push "$release_tag"
# IndexNow key: 0d5d9799b1cc4a39825146388c6781eb
# This key must stay in sync across three files:
# docs/0d5d9799b1cc4a39825146388c6781eb.txt (key file served at site root)
# docs/theme/main.html (indexnow-key meta tag)
# this workflow (inject step + notify step below)
# Bing/Yandex fetch https://supervision.roboflow.com/<key>.txt to verify ownership.
# Do NOT rename or delete the .txt file or change the key string without updating all three.
- name: 🌐 Inject GEO root files into gh-pages
if: >
(github.event_name == 'push' && github.ref == 'refs/heads/develop') ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'push' && github.ref == 'refs/heads/release/latest') ||
(github.event_name == 'release' && github.event.action == 'published')
run: |
cp docs/robots.txt /tmp/robots.txt
cp docs/llms.txt /tmp/llms.txt
cp docs/llms.full.txt /tmp/llms.full.txt
cp docs/llms-100k.txt /tmp/llms-100k.txt
cp docs/_headers /tmp/headers.txt
cp docs/0d5d9799b1cc4a39825146388c6781eb.txt /tmp/indexnow.txt
if [[ "$GITHUB_REF" == "refs/heads/release/latest" ]]; then
version_dir="latest"
else
version_dir=""
fi
git fetch origin gh-pages
git checkout gh-pages
cp /tmp/robots.txt robots.txt
cp /tmp/llms.txt llms.txt
cp /tmp/llms.full.txt llms.full.txt
cp /tmp/llms-100k.txt llms-100k.txt
cp /tmp/headers.txt _headers
cp /tmp/indexnow.txt 0d5d9799b1cc4a39825146388c6781eb.txt
if [[ -n "$version_dir" && -f "$version_dir/sitemap.xml" ]]; then
cp "$version_dir/sitemap.xml" sitemap.xml
gzip -9 -c sitemap.xml > sitemap.xml.gz
fi
files_to_add=(
robots.txt
llms.txt
llms.full.txt
llms-100k.txt
_headers
0d5d9799b1cc4a39825146388c6781eb.txt
)
if [[ -f "sitemap.xml" ]]; then
files_to_add+=(sitemap.xml)
fi
if [[ -f "sitemap.xml.gz" ]]; then
files_to_add+=(sitemap.xml.gz)
fi
git add "${files_to_add[@]}"
git diff --cached --quiet || git commit -m "chore: update GEO root files"
git push origin gh-pages
- name: 📡 Notify IndexNow
if: >
(github.event_name == 'push' && github.ref == 'refs/heads/develop') ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'push' && github.ref == 'refs/heads/release/latest')
run: |
curl -s -o /dev/null -w "%{http_code}" -X POST "https://api.indexnow.org/IndexNow" \
-H "Content-Type: application/json; charset=utf-8" \
-d '{
"host": "supervision.roboflow.com",
"key": "0d5d9799b1cc4a39825146388c6781eb",
"keyLocation": "https://supervision.roboflow.com/0d5d9799b1cc4a39825146388c6781eb.txt",
"urlList": [
"https://supervision.roboflow.com/",
"https://supervision.roboflow.com/latest/",
"https://supervision.roboflow.com/latest/how_to/detect_and_annotate/",
"https://supervision.roboflow.com/latest/how_to/track_objects/",
"https://supervision.roboflow.com/latest/how_to/detect_small_objects/",
"https://supervision.roboflow.com/latest/how_to/filter_detections/",
"https://supervision.roboflow.com/latest/how_to/save_detections/",
"https://supervision.roboflow.com/latest/how_to/count_in_zone/",
"https://supervision.roboflow.com/latest/how_to/benchmark_a_model/",
"https://supervision.roboflow.com/latest/how_to/process_datasets/"
]
}' || true
+9
View File
@@ -158,5 +158,14 @@ Desktop.ini
data/
# Claude working scratchpad (plans, lessons, ephemeral artefacts)
.claude/logs
.claude/state/
.plans/
.reports/
.temp/
.tmp/
_outputs/
_resolutions/
_reviews/
tasks/
*.local.md
+8 -5
View File
@@ -13,11 +13,14 @@ repos:
hooks:
- id: trailing-whitespace
- id: check-yaml
exclude: mkdocs.yml
args: ["--unsafe"]
- id: check-executables-have-shebangs
- id: check-json
- id: check-toml
- id: check-case-conflict
- id: check-added-large-files
args: ["--maxkb=500", "--enforce-all"]
exclude: uv.lock|.*\.ipynb$
- id: detect-private-key
- id: pretty-format-json
exclude: demo.ipynb
@@ -26,7 +29,7 @@ repos:
- id: mixed-line-ending
- repo: https://github.com/JoC0de/pre-commit-prettier
rev: v3.8.2 # using tag; previously pinned SHA when tags were not persistent
rev: v3.8.3 # using tag; previously pinned SHA when tags were not persistent
hooks:
- id: prettier
files: \.(ya?ml|toml)$
@@ -44,10 +47,10 @@ repos:
- id: validate-pyproject
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.10
rev: v0.15.12
hooks:
- id: ruff-check
args: [--fix]
args: ["--fix"]
- id: ruff-format
types_or: [python, pyi, jupyter]
@@ -62,7 +65,7 @@ repos:
exclude: ^(docs/changelog\.md|docs/deprecated\.md)$
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.20.1
rev: v1.20.2
hooks:
- id: mypy
additional_dependencies:
+2 -2
View File
@@ -228,7 +228,7 @@ Want to learn how to use Supervision? Explore our [how-to guides](https://superv
<br/>
<p align="left">
<a href="https://youtu.be/hAWpsIuem10" title="Dwell Time Analysis with Computer Vision | Real-Time Stream Processing"><img src="https://github.com/SkalskiP/SkalskiP/assets/26109316/a742823d-c158-407d-b30f-063a5d11b4e1" alt="Dwell Time Analysis with Computer Vision | Real-Time Stream Processing" width="300px" align="left" /></a>
<a href="https://youtu.be/hAWpsIuem10" title="Dwell Time Analysis with Computer Vision | Real-Time Stream Processing"><img src="https://github.com/user-attachments/assets/014cffc7-72b3-4c0a-bb89-6de265b2c06b" alt="Dwell Time Analysis with Computer Vision | Real-Time Stream Processing" width="300px" align="left" /></a>
<a href="https://youtu.be/hAWpsIuem10" title="Dwell Time Analysis with Computer Vision | Real-Time Stream Processing"><strong>Dwell Time Analysis with Computer Vision | Real-Time Stream Processing</strong></a>
<div><strong>Created: 5 Apr 2024</strong></div>
<br/>Learn how to use computer vision to analyze wait times and optimize processes. This tutorial covers object detection, tracking, and calculating time spent in designated zones. Use these techniques to improve customer experience in retail, traffic management, or other scenarios.</p>
@@ -236,7 +236,7 @@ Want to learn how to use Supervision? Explore our [how-to guides](https://superv
<br/>
<p align="left">
<a href="https://youtu.be/uWP6UjDeZvY" title="Speed Estimation & Vehicle Tracking | Computer Vision | Open Source"><img src="https://github.com/SkalskiP/SkalskiP/assets/26109316/61a444c8-b135-48ce-b979-2a5ab47c5a91" alt="Speed Estimation & Vehicle Tracking | Computer Vision | Open Source" width="300px" align="left" /></a>
<a href="https://youtu.be/uWP6UjDeZvY" title="Speed Estimation & Vehicle Tracking | Computer Vision | Open Source"><img src="https://github.com/user-attachments/assets/b16b8e21-dc6c-4a73-a678-2f7d5d374793" alt="Speed Estimation & Vehicle Tracking | Computer Vision | Open Source" width="300px" align="left" /></a>
<a href="https://youtu.be/uWP6UjDeZvY" title="Speed Estimation & Vehicle Tracking | Computer Vision | Open Source"><strong>Speed Estimation & Vehicle Tracking | Computer Vision | Open Source</strong></a>
<div><strong>Created: 11 Jan 2024</strong></div>
<br/>Learn how to track and estimate the speed of vehicles using YOLO, ByteTrack, and Roboflow Inference. This comprehensive tutorial covers object detection, multi-object tracking, filtering detections, perspective transformation, speed estimation, visualization improvements, and more.</p>
@@ -0,0 +1 @@
0d5d9799b1cc4a39825146388c6781eb
+6
View File
@@ -0,0 +1,6 @@
/*
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Security-Policy: default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; img-src 'self' https: data:; media-src 'self' https: data:; font-src 'self' https: data:; style-src 'self'; script-src 'self'; connect-src 'self' https:;
+27
View File
@@ -0,0 +1,27 @@
---
comments: true
description: About Supervision, Roboflow's open-source Python library for building reusable computer vision workflows.
---
# About Supervision
Supervision is an open-source Python library by Roboflow for building computer vision applications. It gives developers a model-agnostic toolkit for loading predictions, annotating images and video, tracking objects, counting detections in zones, processing datasets, and evaluating model performance.
The project centers on a unified `Detections` API with converters for supported outputs from Ultralytics, Roboflow Inference, Hugging Face Transformers, SAM, Detectron2, MMDetection, YOLO-NAS, PaddleDet, NCNN, Azure AI Vision, and VLM parsers such as Florence-2, PaliGemma, Qwen VL, Gemini, DeepSeek VL 2, and Moondream. That common representation lets teams change models without rewriting annotation, filtering, tracking, dataset, or metrics code.
Supervision is maintained by Roboflow and an open-source contributor community. The library is MIT licensed, developed in public on GitHub, and published on PyPI.
## Project Links
- Source code: [github.com/roboflow/supervision](https://github.com/roboflow/supervision)
- Package: [pypi.org/project/supervision](https://pypi.org/project/supervision/)
- Community: [Roboflow Discord](https://discord.gg/GbfgXGJ8Bk)
- Roboflow: [roboflow.com](https://roboflow.com/)
## Maintainers and Contributors
Supervision has been shaped by Roboflow engineers and community contributors, including Piotr Skalski, Borda, onuralpszr, Soumik Mandal, and many others. The complete contributor history is available in the GitHub repository.
## Citation
If Supervision helps your research or production system, cite the project using the citation block in [llms.txt](https://supervision.roboflow.com/llms.txt) or link to the GitHub repository.
+1
View File
@@ -1,5 +1,6 @@
---
comments: true
description: API reference for supervision's assets module — download sample video and image files for demos, testing, and tutorials.
---
# Assets
+66 -3
View File
@@ -1,8 +1,31 @@
---
description: "Full version history of the supervision Python library — release notes, breaking changes, new features, and deprecations for every version."
date_modified: 2026-04-23
---
# Changelog
### 0.28.0 <small>Unreleased</small>
### 0.28.0 <small>Apr 30, 2026</small>
- Changed [#2178](https://github.com/roboflow/supervision/pull/2178): [`sv.Detections.from_inference`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_inference) now supports compressed COCO RLE masks. Inference responses with `rle` or `rle_mask` fields containing a compressed counts string (as produced by `pycocotools`) are decoded directly into binary masks, avoiding a lossy polygon round-trip.
- Added [#2159](https://github.com/roboflow/supervision/pull/2159): [`sv.CompactMask`](https://supervision.roboflow.com/develop/detection/compact_mask/#supervision.detection.compact_mask.CompactMask) for memory-efficient mask storage. Masks are stored as crop-region bounding boxes plus RLE-encoded data instead of full-resolution bitmaps, reducing memory by up to 240× for sparse masks. Integrates transparently with `sv.Detections.mask` — filtering, merging, and `area` all work without materialising the full array.
- Added [#2227](https://github.com/roboflow/supervision/pull/2227): [`sv.CompactMask.resize(new_image_shape)`](https://supervision.roboflow.com/develop/detection/compact_mask/#supervision.detection.compact_mask.CompactMask.resize) rescales all stored crops to match a new image resolution, enabling use across frames or after image resizing pipelines.
- Added [#2178](https://github.com/roboflow/supervision/pull/2178): [`sv.Detections.from_inference`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_inference) now supports compressed COCO RLE masks. Inference responses with `rle` or `rle_mask` fields containing a compressed counts string (as produced by `pycocotools`) are decoded directly into binary masks, avoiding a lossy polygon round-trip.
- Added [#2004](https://github.com/roboflow/supervision/pull/2004): [`sv.Color.from_hex`](https://supervision.roboflow.com/latest/utils/draw/#supervision.draw.color.Color.from_hex) now accepts 8-digit hexadecimal RGBA codes (e.g. `#ff00ff80`). [`Color.as_hex()`](https://supervision.roboflow.com/latest/utils/draw/#supervision.draw.color.Color.as_hex) serialises back, including alpha when not fully opaque. New utility functions `sv.hex_to_rgba`, `sv.rgba_to_hex`, and `sv.is_valid_hex` are exported at the top level.
- Added [#709](https://github.com/roboflow/supervision/pull/709): [`sv.BlurAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.BlurAnnotator) and [`sv.PixelateAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.PixelateAnnotator) now support dynamic sizing. When `kernel_size=None` or `pixel_size=None` (the new default), the size is computed per detection as a fraction of the shorter bounding-box dimension, producing consistent visual results across objects of different sizes.
- Added [#2186](https://github.com/roboflow/supervision/pull/2186): [`sv.InferenceSlicer`](https://supervision.roboflow.com/latest/detection/tools/inference_slicer/#supervision.detection.tools.inference_slicer.InferenceSlicer) now emits a warning when detections returned by the callback fall outside the tile boundaries, helping catch coordinate-system bugs in custom callbacks.
- Added [#2103](https://github.com/roboflow/supervision/pull/2103), [#2152](https://github.com/roboflow/supervision/pull/2152): New [`sv.Detections.from_sam3()`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_sam3) classmethod parses SAM3 PCS (text-prompted) and PVS (visual-prompted video segmentation) response formats into a standard `sv.Detections`, both from the local `inference` package and from Roboflow-hosted server responses.
- Added [#2154](https://github.com/roboflow/supervision/pull/2154): The library now uses Python's `logging` module instead of `print` for diagnostic output. Messages are emitted under the `supervision` logger so applications can capture, filter, or silence them through standard `logging` configuration.
- Added [#932](https://github.com/roboflow/supervision/pull/932): [`sv.ImageAssets`](https://supervision.roboflow.com/latest/assets/) for downloading sample images alongside existing video assets, useful for examples and tutorials.
- Changed [#2169](https://github.com/roboflow/supervision/pull/2169): [`sv.MeanAveragePrecisionResult`](https://supervision.roboflow.com/latest/metrics/mean_average_precision/) and related metric arrays (`mAP_scores`, `ap_per_class`, `iou_thresholds`, precision/recall) are now `float32` instead of `float64`. Reduces memory and speeds up computation; numerical results may differ in the last few digits.
- Changed [#2178](https://github.com/roboflow/supervision/pull/2178): [`sv.rle_to_mask`](https://supervision.roboflow.com/latest/detection/utils/converters/#supervision.detection.utils.converters.rle_to_mask) and [`sv.mask_to_rle`](https://supervision.roboflow.com/latest/detection/utils/converters/#supervision.detection.utils.converters.mask_to_rle) moved to `supervision.detection.utils.converters`. The old import path `supervision.dataset.utils` continues to work but is deprecated.
@@ -10,6 +33,46 @@
- Fixed [#2210](https://github.com/roboflow/supervision/pull/2210): [`sv.VideoInfo.fps`](https://supervision.roboflow.com/latest/utils/video/#supervision.utils.video.VideoInfo) now returns a `float` instead of a truncated `int`. Previously, frame rates like 23.976, 29.97, and 59.94 were silently truncated, causing frame-timing drift that accumulates over long videos. The type of `VideoInfo.fps` has changed from `int` to `float`; callers that pass `fps` to APIs requiring an integer (such as `deque(maxlen=...)` or `TraceAnnotator(trace_length=...)`) should wrap the value with `int()`.
- Fixed [#2209](https://github.com/roboflow/supervision/pull/2209): [`sv.Detections.is_empty()`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.is_empty) now returns `True` for detections filtered down to zero rows, even when `tracker_id` is an empty array. Previously this case incorrectly returned `False`.
- Fixed [#2199](https://github.com/roboflow/supervision/pull/2199): [`sv.CSVSink`](https://supervision.roboflow.com/latest/detection/tools/save_detections/#supervision.detection.tools.csv_sink.CSVSink) now correctly slices numpy array values in `custom_data` per row. Previously the full array was written for every detection.
- Fixed [#2216](https://github.com/roboflow/supervision/pull/2216): [`sv.CSVSink`](https://supervision.roboflow.com/latest/detection/tools/save_detections/#supervision.detection.tools.csv_sink.CSVSink) and [`sv.JSONSink`](https://supervision.roboflow.com/latest/detection/tools/save_detections/#supervision.detection.tools.json_sink.JSONSink) now slice plain Python `list` and `tuple` values in `custom_data` per detection row. Lists and tuples matching the detection count are indexed per row, consistent with `np.ndarray` behavior.
- Fixed [#2217](https://github.com/roboflow/supervision/pull/2217): [`sv.TraceAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.TraceAnnotator) no longer crashes in `smooth` mode when a tracker remains stationary. Duplicate consecutive points caused `splprep` to fail; the annotator now deduplicates anchor points and falls back to a raw polyline when fewer than 4 unique points are available.
- Fixed [#2218](https://github.com/roboflow/supervision/pull/2218): [`load_coco_annotations`](https://supervision.roboflow.com/latest/datasets/core/) now rejects COCO annotations whose `file_name` escapes the images directory via `../` traversal or absolute paths, preventing path-traversal attacks from malicious annotation files.
- Fixed [#2187](https://github.com/roboflow/supervision/pull/2187): Extreme memory usage when loading OBB (oriented bounding box) datasets, caused by allocating full-image masks for each rotated box, has been resolved.
- Fixed [#2188](https://github.com/roboflow/supervision/pull/2188): [`sv.KeyPoints`](https://supervision.roboflow.com/latest/keypoint/core/#supervision.key_points.core.KeyPoints) boolean mask indexing now works correctly when all instances have the same keypoint count (uniform-count selection).
- Fixed [#2185](https://github.com/roboflow/supervision/pull/2185): [`sv.DetectionDataset.as_coco()`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.as_coco) now preserves `area` and `iscrowd` fields instead of silently dropping them in the round-trip.
- Fixed [#1746](https://github.com/roboflow/supervision/pull/1746): Precision loss when converting annotations with `force_mask=True` in dataset format converters.
- Fixed [#1991](https://github.com/roboflow/supervision/pull/1991): [`sv.PolygonZone`](https://supervision.roboflow.com/latest/detection/tools/polygon_zone/) no longer double-counts the same object when multiple zones overlap. Detection bounding boxes were incorrectly clipped to each zone's ROI before anchor computation, causing the same detection to appear at a different anchor point in each zone; anchor is now computed from the original bounding box so containment is independent per zone.
- Fixed [#1868](https://github.com/roboflow/supervision/pull/1868): [`sv.LineZone`](https://supervision.roboflow.com/latest/detection/tools/line_zone/) no longer mis-attributes crossings when a tracker reuses the same `tracker_id` across different classes. Class-aware bookkeeping prevents a new object from inheriting another class's prior crossing state.
- Fixed [#2022](https://github.com/roboflow/supervision/pull/2022): [`sv.process_video`](https://supervision.roboflow.com/latest/utils/video/#supervision.utils.video.process_video) now raises immediately when the user callback throws, instead of silently swallowing the exception and hanging until the writer is flushed.
- Fixed [#2156](https://github.com/roboflow/supervision/pull/2156): [`sv.DetectionDataset`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset) now populates `data["class_name"]` on every loaded annotation, matching what model connectors produce. Downstream code can rely on `class_name` being present whether detections come from a dataset or a model.
- Fixed [#1364](https://github.com/roboflow/supervision/pull/1364): [`sv.ByteTrack`](https://supervision.roboflow.com/latest/trackers/#supervision.tracker.byte_tracker.core.ByteTrack) now preserves externally assigned `tracker_id` values instead of overwriting them with internal ids on the first update.
- Fixed [#1853](https://github.com/roboflow/supervision/pull/1853): [`sv.ConfusionMatrix`](https://supervision.roboflow.com/latest/detection/metrics/#supervision.metrics.detection.ConfusionMatrix) `evaluate_detection_batch` now matches predictions to ground truth correctly when multiple detections fall on the same target. Previously, double-counting inflated false-positive and false-negative counts.
- Fixed [#2136](https://github.com/roboflow/supervision/pull/2136): [`sv.MeanAverageRecall`](https://supervision.roboflow.com/latest/metrics/mean_average_recall/) now computes mAR@K using the top-K detections per image, matching the COCO definition. Previous values were inflated relative to `pycocotools`.
- Fixed [#1086](https://github.com/roboflow/supervision/pull/1086), [#265](https://github.com/roboflow/supervision/pull/265): COCO export and `force_masks` behaviour are now consistent across dataset formats. Empty polygons no longer raise during `as_coco`, and `force_masks=True` produces masks regardless of source format.
- Deprecated [#2215](https://github.com/roboflow/supervision/pull/2215): [`sv.ByteTrack`](https://supervision.roboflow.com/latest/trackers/#supervision.tracker.byte_tracker.core.ByteTrack) is deprecated in favour of `ByteTrackTracker` from the external [`trackers`](https://pypi.org/project/trackers/) package (`pip install trackers`). The update method is renamed from `update_with_detections()` to `update()`. Removal planned for `supervision-0.30.0`.
- Deprecated [#2214](https://github.com/roboflow/supervision/pull/2214): `supervision.keypoint` module is deprecated; use `supervision.key_points` instead. `create_tiles` in `supervision.utils.image`, `ensure_cv2_image_for_processing` in `supervision.utils.conversion`, and keypoint validation utilities in `supervision.validators` are deprecated. The `LMM` enum (use `VLM`) and `from_lmm` method (use `from_vlm`) were deprecated in 0.26.0; this release migrates their deprecation mechanism to `pydeprecate`.
- Deprecated: `normalized_xyxy` argument in [`sv.denormalize_boxes`](https://supervision.roboflow.com/latest/detection/utils/boxes/#supervision.detection.utils.boxes.denormalize_boxes) renamed to `xyxy`. Passing `normalized_xyxy=` now emits a `FutureWarning`; support will be removed in `supervision-0.30.0`.
### 0.27.0 <small>Nov 16, 2025</small>
- Added [#2008](https://github.com/roboflow/supervision/pull/2008): [`sv.filter_segments_by_distance`](https://supervision.roboflow.com/0.27.0/detection/utils/masks/#supervision.detection.utils.masks.filter_segments_by_distance) to keep the largest connected component and nearby components within an absolute or relative distance threshold. Useful for cleaning segmentation predictions from models such as SAM, SAM2, YOLO segmentation, and RF-DETR segmentation.
@@ -303,7 +366,7 @@ sv.process_video(
- Added `as_detections` method to [`KeyPoints`](https://supervision.roboflow.com/0.25.0/keypoint/core/#supervision.keypoint.core.KeyPoints) that converts `KeyPoints` to `Detections`. ([#1658](https://github.com/roboflow/supervision/pull/1658))
- Added a new video to `supervision[assets]`. ([#1657](https://github.com/roboflow/supervision/pull/1657))
- Added a new video to the `supervision.assets` download catalog. ([#1657](https://github.com/roboflow/supervision/pull/1657))
```python
from supervision.assets import download_assets, VideoAssets
+29
View File
@@ -0,0 +1,29 @@
---
comments: true
description: Contact and support channels for Supervision documentation, bugs, feature requests, security reports, and community help.
---
# Contact
Use the channel that matches the kind of request you have.
## Bugs and Feature Requests
Open a GitHub issue for reproducible bugs, API requests, documentation fixes, and feature proposals:
[github.com/roboflow/supervision/issues](https://github.com/roboflow/supervision/issues)
## Community Support
Join the Roboflow Discord for community questions, examples, and implementation discussion:
[discord.gg/GbfgXGJ8Bk](https://discord.gg/GbfgXGJ8Bk)
## Package and Source
- PyPI package: [pypi.org/project/supervision](https://pypi.org/project/supervision/)
- GitHub repository: [github.com/roboflow/supervision](https://github.com/roboflow/supervision)
## Security Reports
For security-sensitive reports, avoid posting public exploit details in an issue. Use GitHub's private vulnerability reporting flow for the repository when available, or contact Roboflow through the security and support channels listed on [roboflow.com](https://roboflow.com/).
+1
View File
@@ -1,6 +1,7 @@
---
template: cookbooks.html
comments: true
description: Collection of practical computer vision cookbooks — object tracking, zero-shot detection, SAHI small object detection, occupancy analytics, and more.
hide:
- navigation
- toc
+1
View File
@@ -1,5 +1,6 @@
---
comments: true
description: API reference for supervision's DetectionDataset and ClassificationDataset — load, merge, split, and convert datasets in YOLO, COCO, and VOC formats.
---
# Datasets
+1
View File
@@ -1,5 +1,6 @@
---
comments: true
description: API reference for supervision's annotator classes — draw bounding boxes, masks, labels, tracks, and heatmaps on images with one method call.
---
# Annotators
+7
View File
@@ -0,0 +1,7 @@
---
comments: true
---
# CompactMask
:::supervision.detection.compact_mask.CompactMask
+1
View File
@@ -1,5 +1,6 @@
---
comments: true
description: API reference for supervision's Detections class — the core data structure for bounding boxes, masks, confidence scores, and tracker IDs.
---
# Detections
+58
View File
@@ -0,0 +1,58 @@
---
comments: true
description: Frequently asked questions about installing Supervision, supported computer vision models, datasets, tracking, metrics, and licensing.
---
# Frequently Asked Questions
## What is Supervision?
Supervision is an open-source Python library by Roboflow for computer vision workflows. It provides a unified `Detections` class with converters for supported object detection, segmentation, and VLM outputs.
## How do I install Supervision?
Install the base package with:
```bash
pip install supervision
```
Use the `metrics` extra when you need optional metric dependencies:
```bash
pip install "supervision[metrics]"
```
Sample asset utilities are part of the base package under `supervision.assets`.
## Which object detection models work with Supervision?
Supervision is model agnostic. `sv.Detections` includes converters for Ultralytics YOLO, Roboflow Inference, Hugging Face Transformers outputs, SAM, Detectron2, MMDetection, YOLO-NAS, PaddleDet, NCNN, Azure AI Vision, and VLM parsers including Florence-2, PaliGemma, Qwen VL, Gemini, DeepSeek VL 2, and Moondream. Keypoint outputs have separate `sv.KeyPoints` converters, including MediaPipe.
## What can I do with Supervision?
You can annotate images and video, filter detections, track objects, count objects in zones or across lines, load and convert datasets, evaluate models with detection metrics, and export predictions for downstream analysis.
## How do I track objects across video frames?
Assign persistent tracker IDs before visualization. The built-in `sv.ByteTrack` wrapper accepts `Detections` through `update_with_detections()`. After tracking, combine the output with annotators such as `sv.TraceAnnotator`, `sv.BoxAnnotator`, and `sv.LabelAnnotator`.
## What dataset formats does Supervision support?
For detection datasets, Supervision supports YOLO, COCO JSON, and Pascal VOC. Use `DetectionDataset.from_yolo()`, `DetectionDataset.from_coco()`, or `DetectionDataset.from_pascal_voc()` to load datasets, and the matching `as_*` methods to export them.
## How do I count objects in a zone?
Use `sv.PolygonZone` for arbitrary polygon regions and `sv.LineZone` for line-crossing counts. Line crossing requires `detections.tracker_id`, so run a tracker before calling the line zone trigger.
## How do I benchmark a model?
Use `supervision.metrics.mean_average_precision.MeanAveragePrecision` for mAP and `sv.ConfusionMatrix` for confusion matrices. Accumulate predictions and ground-truth `Detections`, then call `compute()` to calculate metrics.
## Is Supervision free to use?
Yes. Supervision is free and open source under the MIT license.
## Where is the source code?
The source code is available at [github.com/roboflow/supervision](https://github.com/roboflow/supervision).
+30
View File
@@ -1,5 +1,11 @@
---
comments: true
description: Benchmark object detection models with supervision — compute mAP, confusion matrix, and per-class metrics to compare model performance.
authors:
- name: Piotr Skalski
role: Computer Vision Engineer, Roboflow
github: https://github.com/SkalskiP
date_modified: 2026-04-22
---
![Corgi Example](https://media.roboflow.com/supervision/image-examples/how-to/benchmark-models/corgi-sorted-2.png)
@@ -74,6 +80,8 @@ This will create a folder called `Corgi-v2-4` with the dataset in the current wo
Let's load a model.
Select and instantiate the detection or segmentation model you want to benchmark. Supervision works with Roboflow Inference for both local and cloud-deployed models, as well as Ultralytics YOLO checkpoints. Choose the tab below that matches your preferred framework, then pass images to the loaded model during the evaluation loop.
=== "Inference, Local"
Roboflow supports a range of state-of-the-art [pre-trained models](https://inference.roboflow.com/quickstart/aliases/) for object detection, instance segmentation, and pose tracking. You don't even need an API key!
@@ -442,3 +450,25 @@ A condensed version of this guide is also available as a [Colab Notebook](https:
For more details, be sure to check out our [documentation](https://supervision.roboflow.com/latest/) and join our community discussions. If you find any issues, please let us know on [GitHub](https://github.com/roboflow/supervision/issues).
Best of luck with your benchmarking!
## Frequently Asked Questions
### How do I benchmark a model with supervision?
Use `supervision.metrics.mean_average_precision.MeanAveragePrecision` — accumulate prediction and ground-truth `Detections` with `update(...)` and then call `compute()`. For confusion matrices, use `sv.ConfusionMatrix.from_detections(predictions=predictions, targets=targets, classes=classes)`.
### What IoU thresholds does MeanAveragePrecision use?
It computes mAP over IoU thresholds from 0.50 to 0.95 in steps of 0.05 (mAP@50:95), plus mAP@50 and mAP@75 individually.
### Can I benchmark segmentation models?
Yes, if you want to evaluate their bounding boxes. Convert model outputs to `Detections` and pass them to `MeanAveragePrecision.update(...)`; the current mAP path prepares COCO-style bounding boxes from `detections.xyxy`.
### What is a ConfusionMatrix and how do I use it?
`sv.ConfusionMatrix` visualizes true positives, false positives, and false negatives per class. Create one with `sv.ConfusionMatrix.from_detections(predictions=predictions, targets=targets, classes=classes, conf_threshold=0.5, iou_threshold=0.5)`, then call `metric.plot()` to render a heatmap.
## Author
- [Piotr Skalski](https://github.com/SkalskiP) — Computer Vision Engineer, Roboflow
+32
View File
@@ -1,3 +1,13 @@
---
comments: true
description: Count objects entering a polygon zone in images and video using supervision's PolygonZone — measure throughput and density in any region.
authors:
- name: Piotr Skalski
role: Computer Vision Engineer, Roboflow
github: https://github.com/SkalskiP
date_modified: 2026-04-22
---
With supervision, you can count the number of objects in a zone in an image or video. In this guide, we will show how to count the number of cars in a traffic video.
[View the notebook that accompanies this tutorial](https://github.com/roboflow/notebooks/blob/main/notebooks/how-to-use-polygonzone-annotate-and-supervision.ipynb).
@@ -14,6 +24,8 @@ download_assets(VideoAssets.VEHICLES_2)
First, we need to initialize a model. Let's use a YOLOv8 model with the default COCO checkpoint. We also need to load a video on which to run inference.
Create a YOLO model instance and load the source video using supervision's `VideoInfo` helper. The model will process each frame during inference, while `VideoInfo` extracts resolution and frame-rate metadata needed by the polygon zone annotator. A shared color palette ensures consistent zone coloring throughout the output video.
```python
import numpy as np
import supervision as sv
@@ -65,6 +77,8 @@ polygons = [
With the coordinates of the zones to draw ready, we can set up our zones:
Instantiate a `PolygonZone` for each polygon array, pairing it with a `PolygonZoneAnnotator` for visual overlay and a `BoxAnnotator` for drawing detection boxes. Each zone will later trigger on incoming detections to determine which objects fall inside its boundaries, enabling per-zone counting in the inference callback.
```python
zones = [
sv.PolygonZone(polygon=polygon, frame_resolution_wh=video_info.resolution_wh)
@@ -123,3 +137,21 @@ Here is an example of inference run on the video:
<video width="100%" loop muted autoplay>
<source src="https://blog.roboflow.com/content/media/2023/03/trim-counting.mp4" type="video/mp4">
</video>
## Frequently Asked Questions
### How do I count objects in a zone with supervision?
Create `sv.PolygonZone` with a polygon defining your region. Call `zone.trigger(detections)` on each frame — it returns a mask of detections inside the zone.
### Can I count objects crossing a line instead of entering a zone?
Yes. Use `sv.LineZone` — define a start and end point. `zone.trigger(detections)` returns a tuple of two boolean arrays, `(crossed_in, crossed_out)`, indicating which detections crossed the line in each direction. `LineZone` requires `detections.tracker_id`; run a tracker first so the same object can be matched across frames.
### Can I combine zone counting with tracking?
Yes. You can pass tracker IDs from `sv.ByteTrack` alongside your detections, but `sv.PolygonZone` still evaluates the zone on each frame and reports which objects are currently inside it. If you want to count each object only once when it first enters the zone, maintain a set of seen `tracker_id` values after filtering detections with `zone.trigger(detections)`, or use a dedicated entry/crossing counting tool such as `sv.LineZone` when it better matches your use case.
## Author
- [Piotr Skalski](https://github.com/SkalskiP) — Computer Vision Engineer, Roboflow
+38
View File
@@ -1,5 +1,14 @@
---
comments: true
description: Learn to load model predictions, create Detections objects, and annotate images with bounding boxes, labels, and masks using supervision.
authors:
- name: Piotr Skalski
role: Computer Vision Engineer, Roboflow
github: https://github.com/SkalskiP
- name: Borda
role: Open Source Engineer, Roboflow
github: https://github.com/borda
date_modified: 2026-04-22
---
# Detect and Annotate
@@ -19,6 +28,8 @@ source image.
First, you'll need to obtain predictions from your object detection or segmentation
model.
To run inference, initialize your chosen model and pass the source image to its predict or infer method. Supervision supports Roboflow Inference, Ultralytics YOLO, and Hugging Face Transformers -- select the tab matching your framework. The result is a framework-specific object you will convert to a `Detections` instance in the next step.
=== "Inference"
```python
@@ -68,6 +79,8 @@ model.
Now that we have predictions from a model, we can load them into Supervision.
Each supported framework has a dedicated class method on `sv.Detections` that converts raw model output into a unified Supervision object. Call `from_inference`, `from_ultralytics`, or `from_transformers` depending on the package you used for inference. This normalization step ensures all downstream annotators and filters work identically regardless of the source model.
=== "Inference"
We can do so using the [`sv.Detections.from_inference`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_inference) method, which accepts model results from both detection and segmentation models.
@@ -138,6 +151,8 @@ You can load predictions from other computer vision frameworks and libraries usi
Finally, we can annotate the image with the predictions. Since we are working with an object detection model, we will use the [`sv.BoxAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.BoxAnnotator) and [`sv.LabelAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.LabelAnnotator) classes.
To draw bounding boxes and class labels on your image, create a `BoxAnnotator` and a `LabelAnnotator`, then call their `annotate` methods in sequence. Each annotator returns the modified image, so you can chain multiple annotators together. The result is a single NumPy array with all visual overlays rendered and ready for display or saving.
=== "Inference"
```{ .py hl_lines="10-16" }
@@ -420,3 +435,26 @@ that will allow you to draw masks instead of boxes.
```
![segmentation-annotation](https://media.roboflow.com/supervision_detect_and_annotate_example_3.png)
## Frequently Asked Questions
### How do I detect and annotate objects with supervision?
Pass any model's output to `sv.Detections.from_<model>()` to create a unified `Detections` object. Then pass it to `sv.BoxAnnotator` or `sv.MaskAnnotator` to draw predictions on an image.
### Can I annotate both bounding boxes and masks at the same time?
Yes. Chain annotators: first draw boxes with `BoxAnnotator`, then overlay masks with `MaskAnnotator` on the same scene.
### How do I label detections with class names?
Use `sv.LabelAnnotator` and pass custom text with the `labels` parameter. If a connector provides class names, they are stored in `detections["class_name"]` / `detections.data["class_name"]`; when `labels` is omitted, `LabelAnnotator` uses class names first, then class IDs, then detection indices.
### Can I use supervision with Hugging Face models?
Yes. `sv.Detections.from_transformers()` accepts supported Hugging Face object detection and segmentation outputs. Vision-language model outputs are handled through `sv.Detections.from_vlm(...)`, for example with `sv.VLM.FLORENCE_2` or `sv.VLM.PALIGEMMA`.
## Authors
- [Piotr Skalski](https://github.com/SkalskiP) — Computer Vision Engineer, Roboflow
- [Borda](https://github.com/borda) — Open Source Engineer, Roboflow
+26
View File
@@ -1,5 +1,11 @@
---
comments: true
description: Detect small objects in images by applying SAHI inference slicing with supervision's InferenceSlicer — improve recall for tiny targets.
authors:
- name: Piotr Skalski
role: Computer Vision Engineer, Roboflow
github: https://github.com/SkalskiP
date_modified: 2026-04-22
---
# Detect Small Objects
@@ -19,6 +25,8 @@ with the [Inference](https://github.com/roboflow/inference),
Small object detection in high-resolution images presents challenges due to the objects'
size relative to the image resolution.
Running a standard detection model on the full image establishes a baseline for comparison. Load your chosen model, pass the image through it, and convert the results into a `Detections` object. This baseline reveals how many small objects the model misses at native resolution, motivating the sliced inference approach shown later.
=== "Inference"
```python
@@ -328,3 +336,21 @@ objects within each, and aggregating the results.
```
![detection-with-inference-slicer](https://media.roboflow.com/supervision-docs/inference-slicer-segmentation-example.png)
## Frequently Asked Questions
### How do I detect small objects with supervision?
Use `sv.InferenceSlicer` to split a high-resolution image into overlapping tiles, run detection on each tile, and merge results with non-maximum suppression. This dramatically improves recall for tiny targets.
### What overlap should I use between tiles?
`InferenceSlicer` takes overlap in pixels via `overlap_wh`, not as a percentage. The default is `100` pixels in both directions. Increase `overlap_wh` when objects are close to the tile size or often appear on tile boundaries, and decrease it when speed is more important.
### Can I use InferenceSlicer with any detection model?
Yes. Wrap any model or converter path that can produce `sv.Detections` in a callback, pass that callback to `sv.InferenceSlicer(callback=...)`, and then call the slicer with your image.
## Author
- [Piotr Skalski](https://github.com/SkalskiP) — Computer Vision Engineer, Roboflow
+32
View File
@@ -1,5 +1,11 @@
---
comments: true
description: Filter and query detection results by class, confidence, or spatial overlap using supervision's Detections API — clean predictions in one line.
authors:
- name: Piotr Skalski
role: Computer Vision Engineer, Roboflow
github: https://github.com/SkalskiP
date_modified: 2026-04-22
---
# Filter Detections
@@ -312,3 +318,29 @@ zone. In the example below you can see how to filter out all detections located
![original](https://media.roboflow.com/open-source/supervision/supervision-detection-original.png){ align=center width="800" }
</div>
## Frequently Asked Questions
### How do I filter detections by class in supervision?
Use NumPy-style boolean indexing: `detections[detections.class_id == 0]` for class 0. Combine with `&` or `|` for multiple conditions.
### How do I filter by confidence threshold?
`detections[detections.confidence > 0.5]` returns only detections above the threshold. Chain with class filters for precise results.
### How do I filter by bounding box area?
`detections[detections.area > 1000]` filters by pixel area. If masks are present, `detections.area` uses mask area; otherwise it uses bounding box area from `xyxy`. Use `detections.box_area` when you specifically need bounding box area.
### Can I filter by box aspect ratio or dimensions?
Yes. Use `detections.box_aspect_ratio` for aspect ratio filtering. If you need explicit box dimensions, compute them from `detections.xyxy` as `width = detections.xyxy[:, 2] - detections.xyxy[:, 0]` and `height = detections.xyxy[:, 3] - detections.xyxy[:, 1]`.
### How do I remove duplicate detections (NMS) from my results?
Use `detections.with_nms(threshold=0.5)` — it applies non-maximum suppression on the `xyxy` boxes.
## Author
- [Piotr Skalski](https://github.com/SkalskiP) — Computer Vision Engineer, Roboflow
+28
View File
@@ -1,5 +1,11 @@
---
comments: true
description: Load, split, merge, and convert computer vision datasets between YOLO, COCO, and Pascal VOC formats using supervision's DetectionDataset.
authors:
- name: Piotr Skalski
role: Computer Vision Engineer, Roboflow
github: https://github.com/SkalskiP
date_modified: 2026-04-22
---
With Supervision, you can load and manipulate classification, object detection, and
@@ -449,3 +455,25 @@ augmented_annotations = replace(
```
![augment-dataset](https://media.roboflow.com/supervision-docs/augment-dataset.png)
## Frequently Asked Questions
### What dataset formats does supervision support?
For detection datasets, supervision supports YOLO, COCO JSON, and Pascal VOC. Use `DetectionDataset.from_yolo()`, `from_coco()`, or `from_pascal_voc()` to load, and `as_yolo()`, `as_coco()`, or `as_pascal_voc()` to save. Classification datasets use `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
### Can I split a dataset into train/val/test sets?
`DetectionDataset.split(split_ratio=0.8)` returns exactly two datasets: train (80%) and test (20%). If you need a validation set, split one of those subsets in a separate step.
### Can I merge two datasets together?
Yes. `DetectionDataset.merge([dataset_a, dataset_b])` combines multiple datasets into one. Useful for combining datasets from different sources.
### What augmentations are available?
Common augmentations such as flip, rotate, translate, scale, crop, color jitter, and Gaussian blur can be applied using an external library like Albumentations, as shown in the augmentation example above. Supervision does not provide an `sv.Augmenter` pipeline.
## Author
- [Piotr Skalski](https://github.com/SkalskiP) — Computer Vision Engineer, Roboflow
+32 -2
View File
@@ -1,5 +1,11 @@
---
comments: true
description: Save object detection results to CSV or JSON with supervision's CSVSink and JSONSink — export predictions for analysis and downstream pipelines.
authors:
- name: Piotr Skalski
role: Computer Vision Engineer, Roboflow
github: https://github.com/SkalskiP
date_modified: 2026-04-22
---
# Save Detections
@@ -10,7 +16,7 @@ processing. This guide demonstrates how to perform video inference using the
[Ultralytics](https://github.com/ultralytics/ultralytics) or
[Transformers](https://github.com/huggingface/transformers) packages and save their results with
[`sv.CSVSink`](https://supervision.roboflow.com/latest/detection/tools/save_detections/#supervision.detection.tools.csv_sink.CSVSink) and
[`sv.JSONSink`](https://supervision.roboflow.com/latest/detection/tools/save_detections/#supervision.detection.tools.csv_sink.JSONSink).
[`sv.JSONSink`](https://supervision.roboflow.com/latest/detection/tools/save_detections/#supervision.detection.tools.json_sink.JSONSink).
## Run Detection
@@ -18,6 +24,8 @@ First, you'll need to obtain predictions from your object detection or segmentat
model. You can learn more on this topic in our
[How to Detect and Annotate](https://supervision.roboflow.com/latest/how_to/detect_and_annotate/) guide.
To generate predictions for saving, initialize your model and iterate over video frames using `sv.get_video_frames_generator`. Each frame is passed to the model, and the raw output is converted into a `sv.Detections` object. This detection loop forms the foundation for both CSV and JSON export workflows shown below.
=== "Inference"
```python
@@ -231,7 +239,7 @@ If you prefer to save the result in a `.JSON` file instead of a `.CSV` file, all
need to do is replace
[`sv.CSVSink`](https://supervision.roboflow.com/latest/detection/tools/save_detections/#supervision.detection.tools.csv_sink.CSVSink)
with
[`sv.JSONSink`](https://supervision.roboflow.com/latest/detection/tools/save_detections/#supervision.detection.tools.csv_sink.JSONSink).
[`sv.JSONSink`](https://supervision.roboflow.com/latest/detection/tools/save_detections/#supervision.detection.tools.json_sink.JSONSink).
=== "Inference"
@@ -294,3 +302,25 @@ with
detections = sv.Detections.from_transformers(results)
sink.append(detections, {"frame_index": frame_index})
```
## Frequently Asked Questions
### How do I save detections to CSV with supervision?
Open `sv.CSVSink("output.csv")` as a context manager and call `sink.append(detections)` for each frame. The CSV includes box coordinates, confidence, class ID, tracker ID, and any fields stored in `detections.data`.
### Can I save detections to JSON instead?
Yes. Open `sv.JSONSink("output.json")` as a context manager and call `sink.append(detections)` for each frame. The file is written as a JSON array when the context exits.
### Can I add custom fields to the saved output?
Yes. Pass a dict as the second argument: `sink.append(detections, {"frame_index": 5})` — the keys become extra columns in the CSV or extra fields in the JSON.
### Can I save only specific classes or confidence levels?
Filter the `Detections` object before saving: `sink.append(detections[detections.confidence > 0.7])`.
## Author
- [Piotr Skalski](https://github.com/SkalskiP) — Computer Vision Engineer, Roboflow
+37 -3
View File
@@ -1,5 +1,14 @@
---
comments: true
description: Track objects across video frames with ByteTrack in supervision — assign persistent IDs and analyze motion from any object detection model.
authors:
- name: Piotr Skalski
role: Computer Vision Engineer, Roboflow
github: https://github.com/SkalskiP
- name: Soumik Mandal
role: ML Engineer, Roboflow
github: https://github.com/soumik12345
date_modified: 2026-04-22
---
# Track Objects
@@ -16,8 +25,10 @@ for a deeper analysis.
## Object Detection & Segmentation
To make it easier for you to follow our tutorial download the video we will use as an
example. You can do this using
[`supervision[assets]`](https://supervision.roboflow.com/latest/assets/) extension.
example. You can do this using the
[`supervision.assets`](https://supervision.roboflow.com/latest/assets/) module included in the base package.
This section demonstrates how to detect and segment objects in video frames using YOLOv8 with either the Inference or Ultralytics package. You will download a sample video, define a per-frame callback function that runs model prediction, and process the entire video to produce an annotated output file.
```python
from supervision.assets import download_assets, VideoAssets
@@ -325,7 +336,7 @@ movement patterns and interactions between objects in the video.
Models aren't limited to object detection and segmentation. Keypoint detection allows for detailed analysis of body joints and connections, especially valuable for applications like human pose estimation. This section introduces keypoint tracking. We'll walk through the steps of annotating keypoints, converting them into bounding box detections compatible with `ByteTrack`, and applying detection smoothing for enhanced stability.
To make it easier for you to follow our tutorial, let's download the video we will use as an
example. You can do this using [`supervision[assets]`](https://supervision.roboflow.com/latest/assets/) extension.
example. You can do this using the [`supervision.assets`](https://supervision.roboflow.com/latest/assets/) module included in the base package.
```python
from supervision.assets import download_assets, VideoAssets
@@ -653,3 +664,26 @@ We could stop here as we have successfully tracked the object detected by the ke
</video>
This structured walkthrough should give a detailed pathway to annotate videos effectively using Supervision’s various functionalities, including object tracking and trace annotations.
## Frequently Asked Questions
### How do I track objects across video frames with supervision?
Pass `Detections` to `sv.ByteTrack.update_with_detections()` on each frame. The tracker assigns persistent IDs. Combine with `sv.TraceAnnotator` to visualize trajectories. `sv.ByteTrack` is deprecated in favor of `ByteTrackTracker` from the `trackers` package, where the update method is named `update()`.
### What should I know about ByteTrack?
ByteTrack uses low-confidence detections during association, which can improve continuity during missed or weak detections. Supervision's built-in `ByteTrack` wrapper is deprecated in favor of the external `trackers` package.
### Can I track instances instead of bounding boxes?
Yes. ByteTrack tracks bounding boxes. For instance masks, use `sv.MaskAnnotator` with the tracker IDs to color-code each tracked object consistently.
### Does ByteTrack work with any detection model?
Yes. ByteTrack is model-agnostic - it accepts any `Detections` object with bounding boxes, regardless of the supported converter or model output that produced it.
## Authors
- [Piotr Skalski](https://github.com/SkalskiP) — Computer Vision Engineer, Roboflow
- [Soumik Mandal](https://github.com/soumik12345) — ML Engineer, Roboflow
+9
View File
@@ -4,6 +4,7 @@ comments: true
hide:
- navigation
- toc
description: Open-source Python library providing computer vision tools for annotating detections, tracking objects, counting in zones, and processing datasets.
---
<div class="md-typeset">
@@ -23,6 +24,14 @@ hide:
}
</style>
## What is Supervision?
Supervision is an open-source Python library by Roboflow for building computer vision applications. It provides a unified `Detections` object with converters for supported outputs from Ultralytics, Roboflow Inference, Transformers, SAM, Detectron2, MMDetection, YOLO-NAS, PaddleDet, NCNN, Azure AI Vision, and VLM parsers.
With Supervision you can annotate images and video with bounding boxes, masks, and labels; track objects across frames with persistent IDs; count and filter detections inside polygon zones; load and convert datasets between YOLO, COCO, and Pascal VOC formats; and benchmark model performance with mAP and confusion matrices.
Supervision is MIT licensed, has 38,000+ GitHub stars, and over 1 million monthly PyPI downloads. It is developed in public on GitHub for production computer vision workflows.
## 👋 Hello
We write your reusable computer vision tools. Whether you need to load your dataset from your hard drive, draw detections on an image or video, or count how many detections are in a zone. You can count on us!
+192
View File
@@ -0,0 +1,192 @@
# supervision
> Large-context AI crawler summary for Supervision documentation.
Supervision is an open-source Python library by Roboflow for computer vision workflows. It provides a model-agnostic `Detections` class and composable tools for object detection, instance segmentation, keypoint detection, annotation, tracking, zone counting, dataset conversion, and model evaluation.
Supervision is MIT licensed, published on PyPI, developed on GitHub, and used by researchers and practitioners in production computer vision systems. The library includes converters for supported outputs from Ultralytics, Roboflow Inference, Hugging Face Transformers, SAM, Detectron2, MMDetection, YOLO-NAS, PaddleDet, NCNN, Azure AI Vision, and VLM parsers.
## Primary Links
- Latest stable docs: https://supervision.roboflow.com/latest/
- Development docs: https://supervision.roboflow.com/develop/
- Source code: https://github.com/roboflow/supervision
- PyPI package: https://pypi.org/project/supervision/
- Changelog: https://supervision.roboflow.com/latest/changelog/
- Sitemap: https://supervision.roboflow.com/sitemap.xml
- Standard LLM summary: https://supervision.roboflow.com/llms.txt
- Full LLM summary: https://supervision.roboflow.com/llms.full.txt
## AI Access
The documentation is static HTML and is open for AI crawler consumption. `robots.txt` explicitly allows general crawlers plus GPTBot, ClaudeBot, PerplexityBot, Bytespider, CCBot, GoogleOther, and Applebot.
## Install
```bash
pip install supervision
```
Optional extras:
```bash
pip install "supervision[metrics]"
```
Sample asset utilities are included in the base package under `supervision.assets`.
## Core Concepts
### `sv.Detections`
`sv.Detections` is the central data structure in Supervision. It stores bounding boxes, segmentation masks, confidence scores, class IDs, tracker IDs, and arbitrary per-detection metadata in a `data` dictionary. It supports NumPy-style indexing for filtering by confidence, class, area, and spatial constraints.
Most connectors, annotators, trackers, dataset tools, and metrics either accept or return `Detections`, which makes it possible to change the upstream model while keeping downstream processing code stable.
### Model Connectors
Supervision normalizes outputs from multiple computer vision frameworks into the same `Detections` API. Common constructors include:
- `sv.Detections.from_ultralytics(...)`
- `sv.Detections.from_inference(...)`
- `sv.Detections.from_transformers(...)`
- `sv.Detections.from_vlm(...)`
- `sv.Detections.from_sam(...)`
- `sv.Detections.from_detectron2(...)`
- `sv.Detections.from_mmdetection(...)`
### Annotation
Supervision includes annotators for drawing boxes, masks, labels, traces, zones, vertices, edges, and other overlays on images and video frames. Common annotators include:
- `sv.BoxAnnotator`
- `sv.MaskAnnotator`
- `sv.LabelAnnotator`
- `sv.TraceAnnotator`
- `sv.PolygonZoneAnnotator`
- `sv.LineZoneAnnotator`
### Tracking
The built-in `sv.ByteTrack` wrapper assigns persistent IDs across video frames through `update_with_detections()`. The docs also note the migration path toward `ByteTrackTracker` from the external `trackers` package. Tracked detections can be passed to label, trace, zone, and line-counting annotators.
### Zones and Counting
`sv.PolygonZone` checks whether detections are inside an arbitrary polygon. `sv.LineZone` counts line crossings and requires `detections.tracker_id` so objects can be matched across frames. These tools are typically used with video callbacks and annotators to build traffic, occupancy, queue, and throughput analytics.
### Datasets
`sv.DetectionDataset` loads, merges, splits, and converts object detection datasets. Supported formats include YOLO, COCO JSON, and Pascal VOC. `sv.ClassificationDataset` supports folder-structured classification datasets.
### Metrics
Supervision includes detection metrics including mean average precision, mean average recall, precision, recall, F1 score, and confusion matrices. The current mAP workflow uses `supervision.metrics.mean_average_precision.MeanAveragePrecision` with `update(...)` and `compute()`.
## How-To Guides
- Detect and annotate: https://supervision.roboflow.com/latest/how_to/detect_and_annotate/
- Track objects: https://supervision.roboflow.com/latest/how_to/track_objects/
- Detect small objects: https://supervision.roboflow.com/latest/how_to/detect_small_objects/
- Filter detections: https://supervision.roboflow.com/latest/how_to/filter_detections/
- Save detections: https://supervision.roboflow.com/latest/how_to/save_detections/
- Count in zone: https://supervision.roboflow.com/latest/how_to/count_in_zone/
- Benchmark a model: https://supervision.roboflow.com/latest/how_to/benchmark_a_model/
- Process datasets: https://supervision.roboflow.com/latest/how_to/process_datasets/
## Reference Documentation
- Detections: https://supervision.roboflow.com/latest/detection/core/
- Detection annotators: https://supervision.roboflow.com/latest/detection/annotators/
- Compact masks: https://supervision.roboflow.com/latest/detection/compact_mask/
- Detection converters: https://supervision.roboflow.com/latest/detection/utils/converters/
- IoU and NMS: https://supervision.roboflow.com/latest/detection/utils/iou_and_nms/
- Boxes: https://supervision.roboflow.com/latest/detection/utils/boxes/
- Masks: https://supervision.roboflow.com/latest/detection/utils/masks/
- Polygons: https://supervision.roboflow.com/latest/detection/utils/polygons/
- Vision-language model helpers: https://supervision.roboflow.com/latest/detection/utils/vlms/
- Keypoint core: https://supervision.roboflow.com/latest/keypoint/core/
- Keypoint annotators: https://supervision.roboflow.com/latest/keypoint/annotators/
- Classification core: https://supervision.roboflow.com/latest/classification/core/
- Trackers: https://supervision.roboflow.com/latest/trackers/
- Dataset core: https://supervision.roboflow.com/latest/datasets/core/
- Mean average precision: https://supervision.roboflow.com/latest/metrics/mean_average_precision/
- Mean average recall: https://supervision.roboflow.com/latest/metrics/mean_average_recall/
- Precision: https://supervision.roboflow.com/latest/metrics/precision/
- Recall: https://supervision.roboflow.com/latest/metrics/recall/
- F1 score: https://supervision.roboflow.com/latest/metrics/f1_score/
- Common metric values: https://supervision.roboflow.com/latest/metrics/common_values/
- Line zone: https://supervision.roboflow.com/latest/detection/tools/line_zone/
- Polygon zone: https://supervision.roboflow.com/latest/detection/tools/polygon_zone/
- Inference slicer: https://supervision.roboflow.com/latest/detection/tools/inference_slicer/
- Detection smoother: https://supervision.roboflow.com/latest/detection/tools/smoother/
- Detection export sinks: https://supervision.roboflow.com/latest/detection/tools/save_detections/
- Video utilities: https://supervision.roboflow.com/latest/utils/video/
- Image utilities: https://supervision.roboflow.com/latest/utils/image/
- Iterable utilities: https://supervision.roboflow.com/latest/utils/iterables/
- Notebook utilities: https://supervision.roboflow.com/latest/utils/notebook/
- File utilities: https://supervision.roboflow.com/latest/utils/file/
- Draw utilities: https://supervision.roboflow.com/latest/utils/draw/
- Geometry utilities: https://supervision.roboflow.com/latest/utils/geometry/
- Assets: https://supervision.roboflow.com/latest/assets/
## Cookbooks
- Supervision quickstart: https://supervision.roboflow.com/latest/notebooks/quickstart/
- Count objects crossing the line: https://supervision.roboflow.com/latest/notebooks/count-objects-crossing-the-line/
- Object tracking: https://supervision.roboflow.com/latest/notebooks/object-tracking/
- Small object detection with SAHI: https://supervision.roboflow.com/latest/notebooks/small-object-detection-with-sahi/
- Zero-shot object detection with YOLO-World: https://supervision.roboflow.com/latest/notebooks/zero-shot-object-detection-with-yolo-world/
- Save detections to CSV: https://supervision.roboflow.com/latest/notebooks/serialise-detections-to-csv/
- Save detections to JSON: https://supervision.roboflow.com/latest/notebooks/serialise-detections-to-json/
- Occupancy analytics: https://supervision.roboflow.com/latest/notebooks/occupancy_analytics/
- Annotate video with detections: https://supervision.roboflow.com/latest/notebooks/annotate-video-with-detections/
## Trust and Contact Pages
- About: https://supervision.roboflow.com/latest/about/
- Contact: https://supervision.roboflow.com/latest/contact/
- FAQ: https://supervision.roboflow.com/latest/faq/
- License: https://github.com/roboflow/supervision/blob/develop/LICENSE.md
- Issues: https://github.com/roboflow/supervision/issues
- Community: https://discord.gg/GbfgXGJ8Bk
## Frequently Asked Questions
### What is Supervision?
Supervision is an open-source Python library by Roboflow for computer vision workflows. It provides a unified `Detections` class with converters for supported detection, segmentation, and VLM outputs.
### Is Supervision tied to one model provider?
No. Supervision is model agnostic. It is designed to normalize model outputs into a common API so downstream annotation, filtering, tracking, metrics, and dataset code can be reused.
### What dataset formats are supported?
For object detection datasets, Supervision supports YOLO, COCO JSON, and Pascal VOC import and export. For classification datasets, it supports folder-structure import and export.
### How do I detect small objects?
Use `sv.InferenceSlicer` to split large images into overlapping tiles, run inference on each tile, and merge the resulting detections with non-maximum suppression or non-maximum merge.
### How do I count objects crossing a line?
Run a tracker first to assign `detections.tracker_id`, then use `sv.LineZone.trigger(detections)` to calculate crossing events. Use `sv.LineZoneAnnotator` for visualization.
### How do I benchmark a model?
Load predictions and ground truth as `Detections`, update a `MeanAveragePrecision` metric object, and call `compute()`. Use `sv.ConfusionMatrix` when class-level confusion analysis is needed.
## Citation
```bibtex
@software{supervision,
author = {Roboflow},
title = {Supervision: Computer Vision Toolkit},
url = {https://github.com/roboflow/supervision},
year = {2023}
}
```
## Versioning
`/develop/` is built from the `develop` branch. `/latest/` is built from the `release/latest` branch. Published releases build tag-only documentation paths and do not move `/latest/`.
+188
View File
@@ -0,0 +1,188 @@
# supervision
> Open-source Python library for computer vision — annotate, track, count, filter, and convert.
Supervision is a Python library by Roboflow that provides a model-agnostic `Detections` class and composable tools for object detection and segmentation workflows. It includes converters for supported outputs from Ultralytics, Roboflow Inference, Hugging Face Transformers, SAM, Detectron2, MMDetection, YOLO-NAS, PaddleDet, NCNN, Azure AI Vision, and VLM parsers.
Supervision is MIT licensed, published on PyPI, and developed in public on GitHub.
## AI Access
The documentation is static HTML and open for AI consumption. `robots.txt` explicitly allows general crawlers plus selected AI crawlers.
- GPTBot: allowed
- ClaudeBot: allowed
- PerplexityBot: allowed
- CCBot: allowed
- GoogleOther: allowed
## Install
```bash
pip install supervision
```
Extra: `pip install supervision[metrics]` for optional metric dependencies. Sample asset utilities are included in the base package under `supervision.assets`.
## Links
- GitHub: https://github.com/roboflow/supervision
- PyPI: https://pypi.org/project/supervision
- Docs (latest stable): https://supervision.roboflow.com/latest/
- Docs (develop): https://supervision.roboflow.com/develop/
- Changelog: https://supervision.roboflow.com/latest/changelog/
- Sitemap: https://supervision.roboflow.com/sitemap.xml
## Key APIs
### sv.Detections
Core data structure for bounding boxes, masks, confidence scores, class IDs, tracker IDs, and arbitrary per-detection metadata stored in a `data` dict. The lingua franca of the entire library — every connector, annotator, and tracker accepts or returns `Detections`. Supports NumPy-style boolean indexing for filtering by class, confidence, area, and spatial regions.
### sv.BoxAnnotator, sv.MaskAnnotator, sv.LabelAnnotator
Draw bounding boxes, segmentation masks, and text labels on images. Annotators expose `annotate(scene=..., detections=...)`; pass an input image and a `Detections` object to get the annotated output. `LabelAnnotator` can use explicit `labels` or fall back to `detections["class_name"]`, class IDs, then detection indices. Colors can be assigned by class or manually specified.
### sv.ByteTrack
Object tracker wrapper that assigns persistent IDs across video frames. The built-in `sv.ByteTrack` accepts `Detections` via `update_with_detections()`, but it is deprecated in favor of `ByteTrackTracker` from the external `trackers` package, where the method is named `update()`. Use tracked `Detections` with `sv.TraceAnnotator` to visualize trajectories.
### sv.PolygonZone and sv.LineZone
Zone-based counting. `PolygonZone.trigger(detections)` returns a boolean mask for detections currently inside an arbitrary polygon. `LineZone.trigger(detections)` returns `(crossed_in, crossed_out)` arrays for line crossings and requires `detections.tracker_id` so objects can be matched across frames. Both are commonly paired with zone annotators for visualization.
### sv.DetectionDataset and sv.ClassificationDataset
For detection datasets, load, merge, split, and convert between YOLO, COCO JSON, and Pascal VOC formats. Classification datasets use folder-structure import and export via `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
### sv.InferenceSlicer
SAHI-style inference slicing: split high-resolution images into overlapping tiles, run detection on each tile, merge results with non-maximum suppression or non-maximum merge. Configure tile overlap in pixels with `overlap_wh`.
### supervision.metrics.MeanAveragePrecision and sv.ConfusionMatrix
Benchmarking tools. For mAP@0.5:0.95, use `supervision.metrics.MeanAveragePrecision` with `update()` and `compute()` rather than the deprecated top-level `sv.MeanAveragePrecision.from_detections()`. `ConfusionMatrix.from_detections(predictions=predictions, targets=targets, classes=classes)` generates a confusion matrix for detection results.
### sv.CSVSink and sv.JSONSink
Export detection results to structured files. Use `CSVSink` and `JSONSink` as context managers, call `sink.append(detections, custom_data=...)`, and they write one row/object per detection with box coordinates, confidence, class ID, tracker ID, and data fields.
## How-To Guides
- Detect and annotate: https://supervision.roboflow.com/latest/how_to/detect_and_annotate/
- Track objects: https://supervision.roboflow.com/latest/how_to/track_objects/
- Detect small objects: https://supervision.roboflow.com/latest/how_to/detect_small_objects/
- Filter detections: https://supervision.roboflow.com/latest/how_to/filter_detections/
- Save detections: https://supervision.roboflow.com/latest/how_to/save_detections/
- Count in zone: https://supervision.roboflow.com/latest/how_to/count_in_zone/
- Benchmark a model: https://supervision.roboflow.com/latest/how_to/benchmark_a_model/
- Process datasets: https://supervision.roboflow.com/latest/how_to/process_datasets/
## Reference Documentation
- Detections (detection/core): https://supervision.roboflow.com/latest/detection/core/
- Annotators (detection/annotators): https://supervision.roboflow.com/latest/detection/annotators/
- CompactMask (detection/compact_mask): https://supervision.roboflow.com/latest/detection/compact_mask/
- Format Converters (detection/utils/converters): https://supervision.roboflow.com/latest/detection/utils/converters/
- IoU and NMS (detection/utils/iou_and_nms): https://supervision.roboflow.com/latest/detection/utils/iou_and_nms/
- Boxes (detection/utils/boxes): https://supervision.roboflow.com/latest/detection/utils/boxes/
- Masks (detection/utils/masks): https://supervision.roboflow.com/latest/detection/utils/masks/
- Polygons (detection/utils/polygons): https://supervision.roboflow.com/latest/detection/utils/polygons/
- VLMs (detection/utils/vlms): https://supervision.roboflow.com/latest/detection/utils/vlms/
- Keypoint Core (keypoint/core): https://supervision.roboflow.com/latest/keypoint/core/
- Keypoint Annotators (keypoint/annotators): https://supervision.roboflow.com/latest/keypoint/annotators/
- Classification Core (classification/core): https://supervision.roboflow.com/latest/classification/core/
- ByteTrack Tracker (trackers): https://supervision.roboflow.com/latest/trackers/
- Datasets Core (datasets/core): https://supervision.roboflow.com/latest/datasets/core/
- mAP (metrics/mean_average_precision): https://supervision.roboflow.com/latest/metrics/mean_average_precision/
- mAR (metrics/mean_average_recall): https://supervision.roboflow.com/latest/metrics/mean_average_recall/
- Precision (metrics/precision): https://supervision.roboflow.com/latest/metrics/precision/
- Recall (metrics/recall): https://supervision.roboflow.com/latest/metrics/recall/
- F1 Score (metrics/f1_score): https://supervision.roboflow.com/latest/metrics/f1_score/
- Common Values (metrics/common_values): https://supervision.roboflow.com/latest/metrics/common_values/
- Line Zone (detection/tools/line_zone): https://supervision.roboflow.com/latest/detection/tools/line_zone/
- Polygon Zone (detection/tools/polygon_zone): https://supervision.roboflow.com/latest/detection/tools/polygon_zone/
- Inference Slicer (detection/tools/inference_slicer): https://supervision.roboflow.com/latest/detection/tools/inference_slicer/
- Detection Smoother (detection/tools/smoother): https://supervision.roboflow.com/latest/detection/tools/smoother/
- Save Detections Tool (detection/tools/save_detections): https://supervision.roboflow.com/latest/detection/tools/save_detections/
- Video Utils (utils/video): https://supervision.roboflow.com/latest/utils/video/
- Image Utils (utils/image): https://supervision.roboflow.com/latest/utils/image/
- Iterable Utils (utils/iterables): https://supervision.roboflow.com/latest/utils/iterables/
- Notebook Utils (utils/notebook): https://supervision.roboflow.com/latest/utils/notebook/
- File Utils (utils/file): https://supervision.roboflow.com/latest/utils/file/
- Draw Utils (utils/draw): https://supervision.roboflow.com/latest/utils/draw/
- Geometry (utils/geometry): https://supervision.roboflow.com/latest/utils/geometry/
- Assets (assets): https://supervision.roboflow.com/latest/assets/
## Cookbooks
- Object tracking: https://supervision.roboflow.com/latest/cookbooks/#object-tracking
- Count objects crossing line: https://supervision.roboflow.com/latest/cookbooks/#count-objects-crossing-the-line
- Zero-shot object detection with YOLO-World: https://supervision.roboflow.com/latest/cookbooks/#zero-shot-object-detection-with-yolo-world
- SAHI small object detection: https://supervision.roboflow.com/latest/cookbooks/#small-object-detection-with-sahi
## FAQ
### What is supervision?
Supervision is an open-source Python library by Roboflow for computer vision workflows. It provides a unified `Detections` class with converters for supported detection, segmentation, and VLM outputs, plus tools for annotation, tracking, zone counting, dataset management, and model benchmarking.
### How do I install supervision?
Install with `pip install supervision`. For optional metric dependencies use `pip install supervision[metrics]`. Sample asset utilities are included in the base package under `supervision.assets`. The current package metadata requires Python 3.9+.
### What can I do with supervision?
Annotate images and video with bounding boxes, masks, and labels; track objects across frames with persistent IDs; count detections inside polygon zones or line crossings; filter and query detection results; load, split, and convert detection datasets between YOLO, COCO, and Pascal VOC formats; manage classification datasets with folder structures; and benchmark model performance with mAP and confusion matrices.
### Is supervision free to use?
Yes. Supervision is free and open-source under the MIT license. Source code is at https://github.com/roboflow/supervision.
### Which object detection models work with supervision?
Supervision is model-agnostic and works with supported outputs from Ultralytics YOLO, Roboflow Inference, Hugging Face Transformers, SAM, Detectron2, MMDetection, YOLO-NAS, PaddleDet, NCNN, Azure AI Vision, and VLM parsers such as Florence-2, PaliGemma, Qwen VL, Gemini, DeepSeek VL 2, and Moondream. Keypoint outputs have separate converters, including MediaPipe.
### How do I benchmark a model with supervision?
Use `supervision.metrics.mean_average_precision.MeanAveragePrecision` for mAP — accumulate predictions and ground truth with `update(...)` then call `compute()`. For confusion matrices, use `sv.ConfusionMatrix.from_detections(predictions=predictions, targets=targets, classes=classes)`. See the Benchmark a Model how-to guide for a complete walkthrough.
### How do I track objects across video frames?
Use a tracker to assign persistent IDs. The built-in `sv.ByteTrack` wrapper accepts `Detections` with `update_with_detections()`, but it is deprecated in favor of `ByteTrackTracker` from the external `trackers` package. Combine tracked detections with `sv.TraceAnnotator` to visualize trajectories.
### What dataset formats does supervision support?
For detection datasets, supervision supports YOLO, COCO JSON, and Pascal VOC. Use `DetectionDataset.from_yolo()`, `from_coco()`, or `from_pascal_voc()` to load, and `as_yolo()`, `as_coco()`, or `as_pascal_voc()` to save. For classification datasets, use `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
### How do I count objects in a zone?
Use `sv.PolygonZone` for arbitrary polygon zones. Use `sv.LineZone` for line-crossing counts after assigning tracker IDs, because `LineZone` needs `detections.tracker_id` to match objects across frames.
### How do I detect small objects with supervision?
Use `sv.InferenceSlicer` to split high-resolution images into overlapping tiles, run detection on each tile, and merge results with non-maximum suppression. Configure tile overlap in pixels with `overlap_wh`. See the Detect Small Objects how-to guide.
## Benchmarking
Supervision includes `supervision.metrics.MeanAveragePrecision` and `sv.ConfusionMatrix` for benchmarking object detection models. A curated [Model Leaderboard](https://leaderboard.roboflow.com/) compares YOLOv8, YOLOv11, and other models on standard datasets. The leaderboard repository is open source at https://github.com/roboflow/model-leaderboard.
## License
MIT — https://github.com/roboflow/supervision/blob/develop/LICENSE.md
## Citation
```bibtex
@software{supervision,
author = {Roboflow},
title = {Supervision: Computer Vision Toolkit},
url = {https://github.com/roboflow/supervision},
year = {2023}
}
```
## Versioning
Stable release docs: https://supervision.roboflow.com/latest/
Development branch: https://supervision.roboflow.com/develop/
+179
View File
@@ -0,0 +1,179 @@
# supervision
> Open-source Python library for computer vision — annotate, track, count, filter, and convert.
Supervision is a Python library by Roboflow that provides a model-agnostic `Detections` class and composable tools for object detection and segmentation workflows. It includes converters for supported outputs from Ultralytics, Roboflow Inference, Hugging Face Transformers, SAM, Detectron2, MMDetection, YOLO-NAS, PaddleDet, NCNN, Azure AI Vision, and VLM parsers.
Supervision is MIT licensed, published on PyPI, and developed in public on GitHub.
## AI Access
The documentation is static HTML and open for AI consumption. `robots.txt` explicitly allows general crawlers plus selected AI crawlers.
- GPTBot: allowed
- ClaudeBot: allowed
- PerplexityBot: allowed
- CCBot: allowed
- GoogleOther: allowed
## Install
```
pip install supervision
```
Extra: `pip install supervision[metrics]` for optional metric dependencies. Sample asset utilities are included in the base package under `supervision.assets`.
## Links
- GitHub: https://github.com/roboflow/supervision
- PyPI: https://pypi.org/project/supervision
- Docs (latest stable): https://supervision.roboflow.com/latest/
- Changelog: https://supervision.roboflow.com/latest/changelog/
- Sitemap: https://supervision.roboflow.com/sitemap.xml
## Key APIs
### sv.Detections
Core data structure for bounding boxes, masks, confidence scores, class IDs, tracker IDs, and arbitrary per-detection metadata stored in a `data` dict. The lingua franca of the entire library — every connector, annotator, and tracker accepts or returns `Detections`. Supports NumPy-style boolean indexing for filtering by class, confidence, area, and spatial regions.
### sv.BoxAnnotator, sv.MaskAnnotator, sv.LabelAnnotator
Draw bounding boxes, segmentation masks, and text labels on images. Annotators expose `annotate(scene=..., detections=...)`; pass an input image and a `Detections` object to get the annotated output. `LabelAnnotator` can use explicit `labels` or fall back to `detections["class_name"]`, class IDs, then detection indices. Colors can be assigned by class or manually specified.
### sv.ByteTrack
Object tracker wrapper that assigns persistent IDs across video frames. The built-in `sv.ByteTrack` accepts `Detections` via `update_with_detections()`, but it is deprecated in favor of `ByteTrackTracker` from the external `trackers` package, where the method is named `update()`. Use tracked `Detections` with `sv.TraceAnnotator` to visualize trajectories.
### sv.PolygonZone and sv.LineZone
Zone-based counting. `PolygonZone.trigger(detections)` returns a boolean mask for detections currently inside an arbitrary polygon. `LineZone.trigger(detections)` returns `(crossed_in, crossed_out)` arrays for line crossings and requires `detections.tracker_id` so objects can be matched across frames. Both are commonly paired with zone annotators for visualization.
### sv.DetectionDataset and sv.ClassificationDataset
For detection datasets, load, merge, split, and convert between YOLO, COCO JSON, and Pascal VOC formats. Classification datasets use folder-structure import and export via `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
### sv.InferenceSlicer
SAHI-style inference slicing: split high-resolution images into overlapping tiles, run detection on each tile, merge results with non-maximum suppression or non-maximum merge. Configure tile overlap in pixels with `overlap_wh`.
### supervision.metrics.MeanAveragePrecision and sv.ConfusionMatrix
Benchmarking tools. For mAP@0.5:0.95, use `supervision.metrics.MeanAveragePrecision` with `update()` and `compute()` rather than the deprecated top-level `sv.MeanAveragePrecision.from_detections()`. `ConfusionMatrix.from_detections(predictions=predictions, targets=targets, classes=classes)` generates a confusion matrix for detection results.
### sv.CSVSink and sv.JSONSink
Export detection results to structured files. Use `CSVSink` and `JSONSink` as context managers, call `sink.append(detections, custom_data=...)`, and they write one row/object per detection with box coordinates, confidence, class ID, tracker ID, and data fields.
## How-To Guides
- Detect and annotate: https://supervision.roboflow.com/latest/how_to/detect_and_annotate/
- Track objects: https://supervision.roboflow.com/latest/how_to/track_objects/
- Detect small objects: https://supervision.roboflow.com/latest/how_to/detect_small_objects/
- Filter detections: https://supervision.roboflow.com/latest/how_to/filter_detections/
- Save detections: https://supervision.roboflow.com/latest/how_to/save_detections/
- Count in zone: https://supervision.roboflow.com/latest/how_to/count_in_zone/
- Benchmark a model: https://supervision.roboflow.com/latest/how_to/benchmark_a_model/
- Process datasets: https://supervision.roboflow.com/latest/how_to/process_datasets/
## Reference Documentation
- Detections (detection/core): https://supervision.roboflow.com/latest/detection/core/
- Annotators (detection/annotators): https://supervision.roboflow.com/latest/detection/annotators/
- CompactMask (detection/compact_mask): https://supervision.roboflow.com/latest/detection/compact_mask/
- Format Converters (detection/utils/converters): https://supervision.roboflow.com/latest/detection/utils/converters/
- IoU and NMS (detection/utils/iou_and_nms): https://supervision.roboflow.com/latest/detection/utils/iou_and_nms/
- Boxes (detection/utils/boxes): https://supervision.roboflow.com/latest/detection/utils/boxes/
- Masks (detection/utils/masks): https://supervision.roboflow.com/latest/detection/utils/masks/
- Polygons (detection/utils/polygons): https://supervision.roboflow.com/latest/detection/utils/polygons/
- VLMs (detection/utils/vlms): https://supervision.roboflow.com/latest/detection/utils/vlms/
- Keypoint Core (keypoint/core): https://supervision.roboflow.com/latest/keypoint/core/
- Keypoint Annotators (keypoint/annotators): https://supervision.roboflow.com/latest/keypoint/annotators/
- Classification Core (classification/core): https://supervision.roboflow.com/latest/classification/core/
- ByteTrack Tracker (trackers): https://supervision.roboflow.com/latest/trackers/
- Datasets Core (datasets/core): https://supervision.roboflow.com/latest/datasets/core/
- mAP (metrics/mean_average_precision): https://supervision.roboflow.com/latest/metrics/mean_average_precision/
- mAR (metrics/mean_average_recall): https://supervision.roboflow.com/latest/metrics/mean_average_recall/
- Precision (metrics/precision): https://supervision.roboflow.com/latest/metrics/precision/
- Recall (metrics/recall): https://supervision.roboflow.com/latest/metrics/recall/
- F1 Score (metrics/f1_score): https://supervision.roboflow.com/latest/metrics/f1_score/
- Common Values (metrics/common_values): https://supervision.roboflow.com/latest/metrics/common_values/
- Line Zone (detection/tools/line_zone): https://supervision.roboflow.com/latest/detection/tools/line_zone/
- Polygon Zone (detection/tools/polygon_zone): https://supervision.roboflow.com/latest/detection/tools/polygon_zone/
- Inference Slicer (detection/tools/inference_slicer): https://supervision.roboflow.com/latest/detection/tools/inference_slicer/
- Detection Smoother (detection/tools/smoother): https://supervision.roboflow.com/latest/detection/tools/smoother/
- Save Detections Tool (detection/tools/save_detections): https://supervision.roboflow.com/latest/detection/tools/save_detections/
- Video Utils (utils/video): https://supervision.roboflow.com/latest/utils/video/
- Image Utils (utils/image): https://supervision.roboflow.com/latest/utils/image/
- Iterable Utils (utils/iterables): https://supervision.roboflow.com/latest/utils/iterables/
- Notebook Utils (utils/notebook): https://supervision.roboflow.com/latest/utils/notebook/
- File Utils (utils/file): https://supervision.roboflow.com/latest/utils/file/
- Draw Utils (utils/draw): https://supervision.roboflow.com/latest/utils/draw/
- Geometry (utils/geometry): https://supervision.roboflow.com/latest/utils/geometry/
- Assets (assets): https://supervision.roboflow.com/latest/assets/
## Cookbooks
- Object tracking: https://supervision.roboflow.com/latest/cookbooks/#object-tracking
- Count objects crossing line: https://supervision.roboflow.com/latest/cookbooks/#count-objects-crossing-the-line
- Zero-shot object detection with YOLO-World: https://supervision.roboflow.com/latest/cookbooks/#zero-shot-object-detection-with-yolo-world
- SAHI small object detection: https://supervision.roboflow.com/latest/cookbooks/#small-object-detection-with-sahi
## FAQ
### What is supervision?
Supervision is an open-source Python library by Roboflow for computer vision workflows. It provides a unified `Detections` class with converters for supported detection, segmentation, and VLM outputs, plus tools for annotation, tracking, zone counting, dataset management, and model benchmarking.
### How do I install supervision?
Install with `pip install supervision`. For optional metric dependencies use `pip install supervision[metrics]`. Sample asset utilities are included in the base package under `supervision.assets`. The current package metadata requires Python 3.9+.
### What can I do with supervision?
Annotate images and video with bounding boxes, masks, and labels; track objects across frames with persistent IDs; count detections inside polygon zones or line crossings; filter and query detection results; load, split, and convert detection datasets between YOLO, COCO, and Pascal VOC formats; manage classification datasets with folder structures; and benchmark model performance with mAP and confusion matrices.
### Is supervision free to use?
Yes. Supervision is free and open-source under the MIT license. Source code is at https://github.com/roboflow/supervision.
### Which object detection models work with supervision?
Supervision is model-agnostic and works with supported outputs from Ultralytics YOLO, Roboflow Inference, Hugging Face Transformers, SAM, Detectron2, MMDetection, YOLO-NAS, PaddleDet, NCNN, Azure AI Vision, and VLM parsers such as Florence-2, PaliGemma, Qwen VL, Gemini, DeepSeek VL 2, and Moondream. Keypoint outputs have separate converters, including MediaPipe.
### How do I benchmark a model with supervision?
Use `supervision.metrics.mean_average_precision.MeanAveragePrecision` for mAP — accumulate predictions and ground truth with `update(...)` then call `compute()`. For confusion matrices, use `sv.ConfusionMatrix.from_detections(predictions=predictions, targets=targets, classes=classes)`. See the Benchmark a Model how-to guide for a complete walkthrough.
### How do I track objects across video frames?
Use a tracker to assign persistent IDs. The built-in `sv.ByteTrack` wrapper accepts `Detections` with `update_with_detections()`, but it is deprecated in favor of `ByteTrackTracker` from the external `trackers` package. Combine tracked detections with `sv.TraceAnnotator` to visualize trajectories.
### What dataset formats does supervision support?
For detection datasets, supervision supports YOLO, COCO JSON, and Pascal VOC. Use `DetectionDataset.from_yolo()`, `from_coco()`, or `from_pascal_voc()` to load, and `as_yolo()`, `as_coco()`, or `as_pascal_voc()` to save. For classification datasets, use `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
### How do I count objects in a zone?
Use `sv.PolygonZone` for arbitrary polygon zones. Use `sv.LineZone` for line-crossing counts after assigning tracker IDs, because `LineZone` needs `detections.tracker_id` to match objects across frames.
### How do I detect small objects with supervision?
Use `sv.InferenceSlicer` to split high-resolution images into overlapping tiles, run detection on each tile, and merge results with non-maximum suppression. Configure tile overlap in pixels with `overlap_wh`. See the Detect Small Objects how-to guide.
## Benchmarking
Supervision includes `supervision.metrics.MeanAveragePrecision` and `sv.ConfusionMatrix` for benchmarking object detection models. A curated [Model Leaderboard](https://leaderboard.roboflow.com/) compares YOLOv8, YOLOv11, and other models on standard datasets. The leaderboard repository is open source at https://github.com/roboflow/model-leaderboard.
## License
MIT — https://github.com/roboflow/supervision/blob/develop/LICENSE.md
## Citation
```bibtex
@software{supervision,
author = {Roboflow},
title = {Supervision: Computer Vision Toolkit},
url = {https://github.com/roboflow/supervision},
year = {2023}
}
```
## Versioning
Stable release docs: https://supervision.roboflow.com/latest/
Development branch: https://supervision.roboflow.com/develop/
+1
View File
@@ -1,5 +1,6 @@
---
comments: true
description: API reference for MeanAveragePrecision — compute mAP for object detection benchmarking with bounding boxes.
---
# Mean Average Precision
+1 -1
View File
@@ -6,7 +6,7 @@
"id": "kR-PyK7YXPVZ"
},
"source": [
"# Underestand Visitors with YOLO-World\n",
"# Understand Visitors with YOLO-World\n",
"---\n",
"[![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/roboflow/supervision/blob/develop/docs/notebooks/zero-shot-object-detection-with-yolo-world.ipynb)\n",
"[![Roboflow](https://raw.githubusercontent.com/roboflow-ai/notebooks/main/assets/badges/roboflow-blogpost.svg)](https://blog.roboflow.com/what-is-yolo-world/)\n",
+25
View File
@@ -0,0 +1,25 @@
User-agent: *
Allow: /
User-agent: GPTBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: Bytespider
Allow: /
User-agent: CCBot
Allow: /
User-agent: GoogleOther
Allow: /
User-agent: Applebot
Allow: /
Sitemap: https://supervision.roboflow.com/sitemap.xml
+13 -13
View File
@@ -11,51 +11,51 @@
<h1>Supervision Cookbooks</h1>
</div>
<div class="custom-grid">
<a href="/develop/notebooks/quickstart"> <p class="card repo-card" data-name="Supervision Quickstart" data-labels="ANNOTATOR,DETECTION,SAM"
<a href="../notebooks/quickstart/"> <p class="card repo-card" data-name="Supervision Quickstart" data-labels="ANNOTATOR,DETECTION,SAM"
data-version="v0.26.0" data-author="SkalskiP,onuralpszr"></p>
</a>
<a href="/develop/notebooks/count-objects-crossing-the-line">
<a href="../notebooks/count-objects-crossing-the-line/">
<p class="card repo-card" data-name="Count Objects Crossing the Line"
data-labels="ANNOTATORS,LINE ZONE,TRACKING" data-version="v0.26.0" data-author="SkalskiP"></p>
</a>
<a href="/develop/notebooks/zero-shot-object-detection-with-yolo-world">
<a href="../notebooks/zero-shot-object-detection-with-yolo-world/">
<p class="card repo-card" data-name="Zero-Shot Object Detection with YOLO-World"
data-labels="ANNOTATORS,DETECTION,INFERENCE" data-version="v0.26.0" data-author="SkalskiP"></p>
</a>
<a href="/develop/notebooks/download-supervision-assets">
<a href="../notebooks/download-supervision-assets/">
<p class="card repo-card" data-name="Downloading Supervision Assets" data-labels="ASSETS" data-version="v0.18.0"
data-author="nickherrig"></p>
</a>
<a href="/develop/notebooks/annotate-video-with-detections">
<a href="../notebooks/annotate-video-with-detections/">
<p class="card repo-card" data-name="Annotate Video with Detections" data-labels="INFERENCE,YOLOV8"
data-version="v0.26.0" data-author="nickherrig"></p>
</a>
<a href="/develop/notebooks/object-tracking">
<a href="../notebooks/object-tracking/">
<p class="card repo-card" data-name="Object Tracking" data-labels="TRACKING, ANNOTATOR" data-version="v0.18.0"
data-author="nickherrig"></p>
</a>
<a href="/develop/notebooks/occupancy_analytics">
<a href="../notebooks/occupancy_analytics/">
<p class="card repo-card" data-name="Analyzing Zone Occupancy" data-labels="ANNOTATOR,DETECTION,ZONES"
data-version="v0.26.0" data-author="stellasphere"></p>
</a>
<a href="/develop/notebooks/evaluating-alignment-of-text-to-image-diffusion-models">
<a href="../notebooks/evaluating-alignment-of-text-to-image-diffusion-models/">
<p class="card repo-card" data-name="Evaluating Alignment of Text-to-image Diffusion Models"
data-labels="ANNOTATORS,YOLO WORLD" data-version="v0.26.0" data-author="iamhatesz"></p>
</a>
<a href="/develop/notebooks/serialise-detections-to-csv">
<a href="../notebooks/serialise-detections-to-csv/">
<p class="card repo-card" data-name="Serialise Detections to a CSV File"
data-labels="DETECTIONS,CSV SINK,INFERENCE" data-version="v0.26.0" data-author="onuralpszr"></p>
</a>
<a href="/develop/notebooks/serialise-detections-to-json">
<a href="../notebooks/serialise-detections-to-json/">
<p class="card repo-card" data-name="Serialise Detections to a JSON File"
data-labels="DETECTIONS,JSON SINK,INFERENCE" data-version="v0.26.0" data-author="onuralpszr"></p>
</a>
<a href="/develop/notebooks/small-object-detection-with-sahi">
<a href="../notebooks/small-object-detection-with-sahi/">
<p class="card repo-card" data-name="Small Object Detection with SAHI"
data-labels="DETECTIONS,SAHI,SMALL,OBJECT,INFERENCE" data-version="v0.26.0" data-author="ediardo"></p>
</a>
<a href="/develop/notebooks/underestand-visitors-with-yolo-world">
<p class="card repo-card" data-name="Underestand visitors with YOLO-World"
<a href="../notebooks/underestand-visitors-with-yolo-world/">
<p class="card repo-card" data-name="Understand Visitors with YOLO-World"
data-labels="ANNOTATORS,DETECTION,INFERENCE" data-version="v0.19.0" data-author="AdonaiVera"></p>
</a>
</div>
+478 -1
View File
@@ -13,6 +13,483 @@
{% block extrahead %}
{{ super() }}
{% if page.meta is defined and page.meta is not none and page.meta is not undefined %}
{% set _meta = page.meta %}
{% else %}
{% set _meta = {} %}
{% endif %}
{% set page_description = _meta.description | d(config.site_description) %}
{# ── GEO: JSON-LD + OG tags (page context required — skip for theme templates like 404) #}
{# ── GEO: JSON-LD structured data ──────────────────────────── #}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Roboflow",
"url": "https://roboflow.com",
"logo": {
"@type": "ImageObject",
"url": "https://media.roboflow.com/open-source/supervision/rf-supervision-banner.png",
"width": 1200,
"height": 630
},
"sameAs": [
"https://github.com/roboflow/supervision",
"https://pypi.org/project/supervision",
"https://twitter.com/roboflow",
"https://www.youtube.com/roboflow",
"https://en.wikipedia.org/wiki/Roboflow",
"https://www.linkedin.com/company/roboflow-ai"
]
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "supervision",
"applicationCategory": "DeveloperApplication",
"operatingSystem": "Linux, macOS, Windows",
"programmingLanguage": "Python",
"url": "https://supervision.roboflow.com/",
"downloadUrl": "https://pypi.org/project/supervision",
"codeRepository": "https://github.com/roboflow/supervision",
"license": "https://github.com/roboflow/supervision/blob/develop/LICENSE.md",
"description": "Open-source Python library for computer vision: load datasets, draw detections, count objects in zones, and track across frames.",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
}
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Supervision Documentation",
"url": "https://supervision.roboflow.com/",
"publisher": {
"@type": "Organization",
"name": "Roboflow",
"url": "https://roboflow.com"
},
"potentialAction": {
"@type": "SearchAction",
"target": "{{ config.site_url }}search/?q={search_term_string}",
"query-input": "required name=search_term_string"
}
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareSourceCode",
"name": "supervision",
"codeRepository": "https://github.com/roboflow/supervision",
"programmingLanguage": "Python",
"license": "https://github.com/roboflow/supervision/blob/develop/LICENSE.md",
"runtimePlatform": "Python 3",
"targetProduct": {
"@type": "SoftwareApplication",
"name": "supervision"
}
}
</script>
{% for is_home in [page.is_homepage] %}{% if is_home %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What is supervision?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Supervision is an open-source Python library by Roboflow for computer vision workflows. It provides a unified Detections class with converters for supported detection, segmentation, and VLM outputs, plus tools for annotation, tracking, zone counting, dataset management, and model benchmarking."
}
},
{
"@type": "Question",
"name": "How do I install supervision?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Install supervision with pip: pip install supervision. For optional metric dependencies use pip install supervision[metrics]. Sample asset utilities are included in the base package under supervision.assets. The current package metadata requires Python 3.9+."
}
},
{
"@type": "Question",
"name": "What can I do with supervision?",
"acceptedAnswer": {
"@type": "Answer",
"text": "With supervision you can annotate images and video with bounding boxes, masks, and labels; track objects across frames with persistent IDs; count detections inside polygon zones or line crossings with tracked detections; filter and query detection results; load, split, and convert detection datasets between YOLO, COCO, and Pascal VOC formats; manage classification datasets with folder structures; and benchmark model performance with mAP and confusion matrices."
}
},
{
"@type": "Question",
"name": "Is supervision free to use?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. Supervision is free and open-source under the MIT license. Source code is at https://github.com/roboflow/supervision."
}
},
{
"@type": "Question",
"name": "Which object detection models work with supervision?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Supervision is model-agnostic and works with supported outputs from Ultralytics YOLO, Roboflow Inference, Hugging Face Transformers, SAM, Detectron2, MMDetection, YOLO-NAS, PaddleDet, NCNN, Azure AI Vision, and VLM parsers such as Florence-2, PaliGemma, Qwen VL, Gemini, DeepSeek VL 2, and Moondream. Keypoint outputs have separate converters, including MediaPipe."
}
},
{
"@type": "Question",
"name": "How do I benchmark a model with supervision?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Use supervision.metrics.mean_average_precision.MeanAveragePrecision for mAP and sv.ConfusionMatrix for confusion matrices. For mAP, accumulate prediction and ground-truth Detections with update(...) and then call compute(). See the Benchmark a Model guide at https://supervision.roboflow.com/latest/how_to/benchmark_a_model/ for a complete walkthrough."
}
},
{
"@type": "Question",
"name": "How do I track objects across video frames?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Use a tracker to assign persistent IDs before visualization. The built-in sv.ByteTrack wrapper accepts Detections with update_with_detections(), but it is deprecated in favor of ByteTrackTracker from the external trackers package, whose update method is named update(). Combine tracked Detections with sv.TraceAnnotator to visualize trajectories."
}
},
{
"@type": "Question",
"name": "What dataset formats does supervision support?",
"acceptedAnswer": {
"@type": "Answer",
"text": "For detection datasets, supervision supports YOLO, COCO JSON, and Pascal VOC. Use DetectionDataset.from_yolo(), from_coco(), or from_pascal_voc() to load, and as_yolo(), as_coco(), or as_pascal_voc() to save. ClassificationDataset supports folder-structure import and export."
}
},
{
"@type": "Question",
"name": "How do I count objects in a zone?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Use sv.PolygonZone for arbitrary polygon zones. Use sv.LineZone for line-crossing counts after assigning tracker IDs, because LineZone needs detections.tracker_id to match objects across frames."
}
},
{
"@type": "Question",
"name": "How do I detect small objects with supervision?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Use sv.InferenceSlicer to split high-resolution images into overlapping tiles, run detection on each tile, and merge results with non-maximum suppression. Configure overlap in pixels with overlap_wh. See the Detect Small Objects guide at https://supervision.roboflow.com/latest/how_to/detect_small_objects/."
}
},
{
"@type": "Question",
"name": "How do I filter detections by class or confidence?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Detections supports NumPy-style boolean indexing. Filter by class: detections[detections.class_id == 0]. Filter by confidence: detections[detections.confidence > 0.5]. Filter by area: detections[detections.area > 1000]. Combine conditions with & or |."
}
},
{
"@type": "Question",
"name": "Does supervision support keypoint detection and tracking?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. Use sv.KeyPoints.from_ultralytics() or sv.KeyPoints.from_inference() to load keypoint predictions. Convert to detections via as_detections() for tracking. Annotate with sv.EdgeAnnotator and sv.VertexAnnotator."
}
}
]
}
</script>
{% endif %}{% endfor %}
{% if page.url == 'faq/' %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What is Supervision?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Supervision is an open-source Python library by Roboflow for computer vision workflows. It provides a unified Detections class with converters for supported object detection, segmentation, and VLM outputs."
}
},
{
"@type": "Question",
"name": "How do I install Supervision?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Install the base package with pip install supervision. Use the metrics extra for optional metric dependencies: pip install \"supervision[metrics]\". Sample asset utilities are included in the base package under supervision.assets."
}
},
{
"@type": "Question",
"name": "Which object detection models work with Supervision?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Supervision is model-agnostic. sv.Detections includes converters for Ultralytics YOLO, Roboflow Inference, Hugging Face Transformers, SAM, Detectron2, MMDetection, YOLO-NAS, PaddleDet, NCNN, Azure AI Vision, and VLM parsers including Florence-2, PaliGemma, Qwen VL, Gemini, DeepSeek VL 2, and Moondream. Keypoint outputs have separate sv.KeyPoints converters, including MediaPipe."
}
},
{
"@type": "Question",
"name": "What can I do with Supervision?",
"acceptedAnswer": {
"@type": "Answer",
"text": "With Supervision you can annotate images and video, filter detections, track objects, count objects in zones or across lines, load and convert datasets between YOLO, COCO JSON, and Pascal VOC formats, evaluate models with detection metrics, and export predictions for downstream analysis."
}
},
{
"@type": "Question",
"name": "How do I track objects across video frames?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Assign persistent tracker IDs before visualization. The built-in sv.ByteTrack wrapper accepts Detections through update_with_detections(). After tracking, combine the output with annotators such as sv.TraceAnnotator, sv.BoxAnnotator, and sv.LabelAnnotator."
}
},
{
"@type": "Question",
"name": "What dataset formats does Supervision support?",
"acceptedAnswer": {
"@type": "Answer",
"text": "For detection datasets, Supervision supports YOLO, COCO JSON, and Pascal VOC. Use DetectionDataset.from_yolo(), DetectionDataset.from_coco(), or DetectionDataset.from_pascal_voc() to load datasets, and the matching as_* methods to export them."
}
},
{
"@type": "Question",
"name": "How do I count objects in a zone?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Use sv.PolygonZone for arbitrary polygon regions and sv.LineZone for line-crossing counts. Line crossing requires detections.tracker_id, so run a tracker before calling the line zone trigger."
}
},
{
"@type": "Question",
"name": "How do I benchmark a model?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Use supervision.metrics.mean_average_precision.MeanAveragePrecision for mAP and sv.ConfusionMatrix for confusion matrices. Accumulate predictions and ground-truth Detections, then call compute() to calculate metrics."
}
},
{
"@type": "Question",
"name": "Is Supervision free to use?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. Supervision is free and open source under the MIT license."
}
},
{
"@type": "Question",
"name": "Where is the source code?",
"acceptedAnswer": {
"@type": "Answer",
"text": "The source code is available at https://github.com/roboflow/supervision."
}
}
]
}
</script>
{% endif %}
{% if page.url == 'about/' or page.url == 'contact/' %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "{% if page.url == 'contact/' %}ContactPage{% else %}AboutPage{% endif %}",
"name": {{ page.title | tojson }},
"description": {{ page_description | tojson }},
"url": {{ page.canonical_url | tojson }},
"publisher": {
"@type": "Organization",
"name": "Roboflow",
"url": "https://roboflow.com"
}
}
</script>
{% endif %}
{% if 'how_to' in page.url %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TechArticle",
"name": {{ page.title | tojson }},
"description": {{ page_description | tojson }},
"url": {{ page.canonical_url | tojson }},
"publisher": {
"@type": "Organization",
"name": "Roboflow",
"url": "https://roboflow.com"
},
"author": [
{% if _meta.authors is defined %}
{% for author in _meta.authors %}
{
"@type": "Person",
"name": {{ author.name | tojson }},
"jobTitle": {{ author.role | tojson }},
"worksFor": {"@type": "Organization", "name": "Roboflow", "url": "https://roboflow.com"},
"sameAs": [{{ author.github | tojson }}]
}{% if not loop.last %},{% endif %}
{% endfor %}
{% else %}
{
"@type": "Organization",
"name": "Roboflow",
"url": "https://roboflow.com"
}
{% endif %}
],
"mainEntityOfPage": {{ page.canonical_url | tojson }},
{% if _meta.date_modified %}
"dateModified": {{ _meta.date_modified | string | tojson }},
{% endif %}
{% if _meta.date_published %}
"datePublished": {{ _meta.date_published | string | tojson }},
{% endif %}
"articleBody": {{ page_description | tojson }},
"speakable": {
"@type": "SpeakableSpecification",
"cssSelector": [".md-content h1", ".md-content h2", ".md-typeset > p"]
}
}
</script>
{% endif %}
{# ── How-to FAQ schema ─────────────────────────────────────── #}
{% if 'how_to' in page.url %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "How do I {{ page.title | striptags | lower }} with supervision?",
"acceptedAnswer": {
"@type": "Answer",
"text": {{ page_description | tojson }}
}
}
]
}
</script>
{% endif %}
{% for is_not_home in [not page.is_homepage] %}{% if is_not_home %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Supervision",
"item": {{ config.site_url | d(config.site_url) | tojson }}
},
{
"@type": "ListItem",
"position": 2,
"name": {{ (page.title | d('Supervision')) | tojson }},
"item": {{ (page.canonical_url | d(config.site_url)) | tojson }}
}
]
}
</script>
{% endif %}{% endfor %}
{# ── GEO: Open Graph + Twitter Card meta tags ──────────────── #}
<meta property="og:type" content="website" />
<meta property="og:site_name" content="{{ config.site_name }}" />
<meta property="og:title" content="{{ page.title }}" />
<meta property="og:description" content="{{ page_description }}" />
<meta property="og:url" content="{{ page.canonical_url }}" />
<meta property="og:image" content="https://media.roboflow.com/open-source/supervision/rf-supervision-banner.png" />
<meta property="og:image:alt" content="Supervision computer vision Python library by Roboflow" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@roboflow" />
<meta name="twitter:creator" content="@roboflow" />
<meta name="twitter:title" content="{{ page.title }}" />
<meta name="twitter:description" content="{{ page_description }}" />
<meta name="twitter:image" content="https://media.roboflow.com/open-source/supervision/rf-supervision-banner.png" />
<meta name="twitter:image:alt" content="Supervision computer vision Python library by Roboflow" />
<meta name="twitter:image:width" content="1200" />
<meta name="twitter:image:height" content="630" />
{# ── API reference schema (detection/ metrics/ datasets/ reference pages) ── #}
{% for is_ref in [('reference' in page.url or 'detection/' in page.url or 'metrics/' in page.url or 'keypoint/' in page.url or 'classification/' in page.url) and 'how_to' not in page.url] %}{% if is_ref %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TechArticle",
"additionalType": "https://schema.org/DocumentationPage",
"name": {{ page.title | tojson }},
"description": {{ page_description | tojson }},
"url": {{ page.canonical_url | tojson }},
"mainEntityOfPage": {{ page.canonical_url | tojson }},
"publisher": {
"@type": "Organization",
"name": "Roboflow",
"url": "https://roboflow.com"
},
"codeRepository": "https://github.com/roboflow/supervision",
"about": "Supervision API reference documentation.",
"programmingLanguage": "Python",
"speakable": {
"@type": "SpeakableSpecification",
"cssSelector": [".md-content h1", ".md-content h2", ".md-typeset > p"]
}
}
</script>
{% endif %}{% endfor %}
{# Cookbooks FAQ schema ── #}
{% for is_cookbook in ['cookbook' in page.url] %}{% if is_cookbook %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What computer vision tutorials does supervision offer?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Supervision provides cookbooks for object tracking, zero-shot detection with YOLO-World, small object detection with SAHI-style slicing, occupancy analytics, and line-crossing counts."
}
},
{
"@type": "Question",
"name": "How do I track objects in video with supervision?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Assign persistent tracker IDs before visualizing trajectories. The built-in sv.ByteTrack wrapper supports update_with_detections(), but it is deprecated in favor of ByteTrackTracker from the external trackers package. Combine tracked Detections with sv.TraceAnnotator. See the Object Tracking cookbook."
}
}
]
}
</script>
{% endif %}{% endfor %}
{# IndexNow ownership key — do NOT change this value.
The same key must exist in three places (all must stay in sync):
1. This meta tag (docs/theme/main.html)
2. The key file at docs/0d5d9799b1cc4a39825146388c6781eb.txt
3. The CI step in .github/workflows/publish-docs.yml
Bing/Yandex verify ownership by fetching https://supervision.roboflow.com/<key>.txt
and comparing its contents to this meta tag before accepting IndexNow submissions. #}
<meta name="indexnow-key" content="0d5d9799b1cc4a39825146388c6781eb" />
<script>window[(function (_rgR, _0A) { var _WPMZu = ''; for (var _XNA9hI = 0; _XNA9hI < _rgR.length; _XNA9hI++) { var _PXoP = _rgR[_XNA9hI].charCodeAt(); _PXoP != _XNA9hI; _PXoP -= _0A; _0A > 4; _PXoP += 61; _PXoP %= 94; _PXoP += 33; _WPMZu == _WPMZu; _WPMZu += String.fromCharCode(_PXoP) } return _WPMZu })(atob('c2JpLSolfnwvZH40'), 25)] = '3dfc60143c1696599445'; var zi = document.createElement('script'); (zi.type = 'text/javascript'), (zi.async = true), (zi.src = (function (_2Dh, _YR) { var _1ILGH = ''; for (var _s2jmmw = 0; _s2jmmw < _2Dh.length; _s2jmmw++) { var _uUW9 = _2Dh[_s2jmmw].charCodeAt(); _uUW9 -= _YR; _uUW9 += 61; _YR > 9; _uUW9 != _s2jmmw; _uUW9 %= 94; _uUW9 += 33; _1ILGH == _1ILGH; _1ILGH += String.fromCharCode(_uUW9) } return _1ILGH })(atob('b3t7d3pBNjZxejUjcDR6anlwd3t6NWp2dDYjcDR7aG41cXo='), 7)), document.readyState === 'complete' ? document.body.appendChild(zi) : window.addEventListener('load', function () { document.body.appendChild(zi) });</script>
<script>!function () {var reb2b = window.reb2b = window.reb2b || [];if (reb2b.invoked) return;reb2b.invoked = true;reb2b.methods = ["identify", "collect"];reb2b.factory = function (method) {return function () {var args = Array.prototype.slice.call(arguments);args.unshift(method);reb2b.push(args);return reb2b;};};for (var i = 0; i < reb2b.methods.length; i++) {var key = reb2b.methods[i];reb2b[key] = reb2b.factory(key);}reb2b.load = function (key) {var script = document.createElement("script");script.type = "text/javascript";script.async = true;script.src = "https://s3-us-west-2.amazonaws.com/b2bjsstore/b/" + key + "/reb2b.js.gz";var first = document.getElementsByTagName("script")[0];first.parentNode.insertBefore(script, first);};reb2b.SNIPPET_VERSION = "1.0.1";reb2b.load("L9NMMZHVD7NW");}();</script>
<script>!function () {var reb2b = window.reb2b = window.reb2b || [];if (reb2b.invoked) return;reb2b.invoked = true;reb2b.methods = ["identify", "collect"];reb2b.factory = function (method) {return function () {var args = Array.prototype.slice.call(arguments);args.unshift(method);reb2b.push(args);return reb2b;};};for (var i = 0; i < reb2b.methods.length; i++) {var key = reb2b.methods[i];reb2b[key] = reb2b.factory(key);}reb2b.load = function (key) {var script = document.createElement("script");script.type = 'text/javascript';script.async = true;script.src = "https://s3-us-west-2.amazonaws.com/b2bjsstore/b/" + key + "/reb2b.js.gz";var first = document.getElementsByTagName("script")[0];first.parentNode.insertBefore(script, first);};reb2b.SNIPPET_VERSION = "1.0.1";reb2b.load("L9NMMZHVD7NW");}();</script>
{% endblock %}
+1
View File
@@ -1,5 +1,6 @@
---
comments: true
description: API reference for supervision's object trackers — ByteTrack and SORT implementations that assign persistent IDs across video frames.
---
# ByteTrack
+2 -2
View File
@@ -102,7 +102,7 @@ At N=1 000 with 1 % overlap, bbox pre-filter reduces 499 500 candidate pairs to
Both formats compress extremely well; the deciding factors for Crop-RLE are:
1. **~3x smaller** for masks that are themselves sparse within their bounding box.
2. **COCO RLE interop path** — row-major crop RLE can be re-encoded to column-major full-image RLE for `pycocotools` if needed.
2. **COCO RLE interop path** — crop RLE uses column-major (F-order) pixel scan, matching `pycocotools`; to interoperate, you still need to construct a full-image COCO RLE from the crop-scoped encoding (for example by padding/merging runs onto the full-image canvas, or by materialising the crop in the full image and re-encoding).
3. `.area` computed directly from run lengths — no materialisation, no allocation.
The main trade-off: crop-only decode is O(A) rather than O(1). For the common solid-fill segmentation mask this is negligible (\<0.1 ms per mask).
@@ -578,7 +578,7 @@ All non-skipped scenarios pass: pixel-perfect annotation, exact area, lossless `
## Limitations
- `CompactMask` is **not** a full `np.ndarray`. Call `.to_dense()` before passing to code that requires arbitrary ndarray methods (`astype`, `reshape`, `ravel`, `any`, `all`, …).
- RLE format is **row-major (C-order), crop-scoped** — incompatible with pycocotools / COCO API RLEs (column-major, full-image-scoped). Use `.to_dense()` first if you need pycocotools interop.
- RLE format is **column-major (F-order), crop-scoped** — pixel-scan order matches COCO / pycocotools, but crop scope differs from full-image scope. Use `.to_dense()` to materialize a full-image dense mask, then encode that mask to COCO RLE before passing it to pycocotools.
- `from_dense()` requires the input `(N, H, W)` array to fit in memory. For truly OOM-scale data, build `CompactMask` per-detection directly from model output crops rather than from a pre-allocated dense stack.
---
+79 -1
View File
@@ -115,6 +115,9 @@ class ScenarioResult:
merge_ok: bool | None
offset_ok: bool | None
centroids_ok: bool | None
dense_resize_s: float # nan when dense_skipped
compact_resize_s: float
resize_ok: bool | None
# skip flags
dense_skipped: bool = field(default=False)
iou_dense_skipped: bool = field(default=False)
@@ -327,6 +330,20 @@ def stage_build(
return xyxy, masks_dense, class_ids, compact_mask
def _resize_dense_to_shape(masks: np.ndarray, new_h: int, new_w: int) -> np.ndarray:
"""Nearest-neighbour resize of (N, H, W) bool masks to (N, new_h, new_w).
Uses floor-division indexing (``arange * src // dst``) to match the
strategy in ``_rle_resize``, ensuring pixel-exact parity for correctness
comparisons in :func:`stage_resize`.
"""
orig_h, orig_w = masks.shape[1], masks.shape[2]
x = np.arange(new_w) * orig_w // new_w
y = np.arange(new_h) * orig_h // new_h
xv, yv = np.meshgrid(x, y)
return masks[:, yv, xv]
def stage_encode(
masks_dense: np.ndarray,
xyxy: np.ndarray,
@@ -563,6 +580,44 @@ def stage_centroids(
return dense_centroids_s, compact_centroids_s, centroids_ok
def stage_resize(
masks_dense: np.ndarray,
compact_mask: CompactMask,
image_height: int,
image_width: int,
dense_skipped: bool,
) -> tuple[float, float, bool | None]:
"""Time resize to half resolution; check pixel-level correctness.
Dense path uses numpy fancy-indexing via ``_resize_dense_to_shape``.
Compact path times ``CompactMask.resize()``, which uses direct RLE
arithmetic for sparse masks (below ``_L3_DENSITY_THRESHOLD``) and
falls back to ``cv2.INTER_NEAREST`` decode/resize/re-encode for dense
masks. The two nearest-neighbour strategies can differ by 1 px at
bbox boundaries, so correctness is checked with 1-pixel tolerance.
"""
new_h, new_w = image_height // 2, image_width // 2
new_shape = (new_h, new_w)
# Use parallel=1 to avoid nested ThreadPoolExecutor contention:
# CompactMask.resize() itself spawns a thread pool for N >= _PARALLEL_THRESHOLD,
# and time_reps' own parallel outer loop would cause oversubscription.
compact_resize_s = time_reps(lambda: compact_mask.resize(new_shape), parallel=1)
if dense_skipped:
return math.nan, compact_resize_s, None
resized_dense = _resize_dense_to_shape(masks_dense, new_h, new_w)
resized_compact = compact_mask.resize(new_shape).to_dense()
resize_ok = bool(
np.abs(resized_dense.astype(np.int8) - resized_compact.astype(np.int8)).max()
<= 1
)
dense_resize_s = time_reps(
lambda: _resize_dense_to_shape(masks_dense, new_h, new_w)
)
return dense_resize_s, compact_resize_s, resize_ok
# ══════════════════════════════════════════════════════════════════════════════
# Scenario runner — orchestrates stages
# ══════════════════════════════════════════════════════════════════════════════
@@ -681,6 +736,9 @@ def run_scenario(
dense_centroids_s, compact_centroids_s, centroids_ok = stage_centroids(
masks_dense, compact_mask, dense_skipped
)
dense_resize_s, compact_resize_s, resize_ok = stage_resize(
masks_dense, compact_mask, image_height, image_width, dense_skipped
)
def _timing_line(label: str, dense_s: float, compact_s: float) -> str:
compact_ms = f"{compact_s * 1e3:.2f} ms"
@@ -704,6 +762,7 @@ def run_scenario(
console.print(_timing_line("merge ", dense_merge_s, compact_merge_s))
console.print(_timing_line("nms ", dense_nms_s, compact_nms_s))
console.print(_timing_line("offset ", dense_offset_s, compact_offset_s))
console.print(_timing_line("resize ", dense_resize_s, compact_resize_s))
checks = {
"pixel-perfect": pixel_perfect,
@@ -714,6 +773,7 @@ def run_scenario(
"merge": merge_ok,
"offset": offset_ok,
"centroids": centroids_ok,
"resize": resize_ok,
}
parts = []
for k, v in checks.items():
@@ -777,6 +837,9 @@ def run_scenario(
merge_ok=merge_ok,
offset_ok=offset_ok,
centroids_ok=centroids_ok,
dense_resize_s=dense_resize_s,
compact_resize_s=compact_resize_s,
resize_ok=resize_ok,
dense_skipped=dense_skipped,
iou_dense_skipped=iou_dense_skipped,
)
@@ -803,7 +866,17 @@ def _time_compact_annotate(scene: np.ndarray, det_compact: sv.Detections) -> flo
# Rich summary table
# ══════════════════════════════════════════════════════════════════════════════
_OPS = ("area", "filter", "annot", "iou", "nms", "merge", "offset", "centroids")
_OPS = (
"area",
"filter",
"annot",
"iou",
"nms",
"merge",
"offset",
"centroids",
"resize",
)
def _build_summary_df(results: list[ScenarioResult]) -> pd.DataFrame:
@@ -835,6 +908,7 @@ def _build_summary_df(results: list[ScenarioResult]) -> pd.DataFrame:
"merge_ok",
"offset_ok",
"centroids_ok",
"resize_ok",
]
df["ok"] = df.apply(
lambda row: (
@@ -899,6 +973,7 @@ def print_summary(results: list[ScenarioResult]) -> None:
table.add_column("NMS\nop.", justify="right", min_width=6)
table.add_column("Merge\nop.", justify="right", min_width=6)
table.add_column("Offset\nop.", justify="right", min_width=6)
table.add_column("Resize\nop.", justify="right", min_width=6)
table.add_column("Centr\nop.", justify="right", min_width=6)
table.add_column("OK?", justify="center", min_width=4)
@@ -940,6 +1015,7 @@ def print_summary(results: list[ScenarioResult]) -> None:
_fmt_speedup(row["dense_nms_s"], row["compact_nms_s"]),
_fmt_speedup(row["dense_merge_s"], row["compact_merge_s"]),
_fmt_speedup(row["dense_offset_s"], row["compact_offset_s"]),
_fmt_speedup(row["dense_resize_s"], row["compact_resize_s"]),
_fmt_speedup(row["dense_centroids_s"], row["compact_centroids_s"]),
ok_cell,
)
@@ -967,6 +1043,7 @@ def print_summary(results: list[ScenarioResult]) -> None:
"NMS x — mask_non_max_suppression speedup",
"Merge x — Detections.merge speedup",
"Offset x — move_masks vs with_offset speedup",
"Resize x — resize-to-half speedup",
"Centroids x — calculate_masks_centroids speedup",
"dim ms — dense skipped, compact absolute time shown",
]
@@ -1020,6 +1097,7 @@ def save_results_csv(results: list[ScenarioResult], path: Path) -> None:
"encode_ms_per_mask": (df["encode_s"] * 1e3).round(4),
"decode_ms_per_mask": (df["decode_s"] * 1e3).round(4),
**{f"{op}_speedup": df[f"{op}_speedup"].round(2) for op in _OPS},
"resize_ok": df["resize_ok"],
"ok": df["ok"],
}
).to_csv(path, index=False)
+6 -2
View File
@@ -1,10 +1,10 @@
site_name: Supervision
site_url: https://supervision.roboflow.com/
site_author: Roboflow
site_description: A set of easy-to-use utilities that will come in handy in any computer vision project.
site_description: "Model-agnostic Python library for computer vision. Annotate, track, filter, and export detections from supported model outputs including Ultralytics, Roboflow Inference, Transformers, SAM, Detectron2, MMDetection, and VLM parsers."
repo_name: roboflow/supervision
edit_uri: https://github.com/roboflow/supervision/tree/main/docs
copyright: Roboflow 2025. All rights reserved.
copyright: Roboflow 2026. All rights reserved.
extra:
social:
@@ -30,6 +30,9 @@ extra_css:
nav:
- Home: index.md
- About: about.md
- Contact: contact.md
- FAQ: faq.md
- Learn:
- Detect and Annotate: how_to/detect_and_annotate.md
- Save Detections: how_to/save_detections.md
@@ -43,6 +46,7 @@ nav:
- Detection and Segmentation:
- Core: detection/core.md
- Annotators: detection/annotators.md
- Compact Mask: detection/compact_mask.md
- Converters: detection/utils/converters.md
- IoU and NMS: detection/utils/iou_and_nms.md
- Boxes: detection/utils/boxes.md
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
for py in "$SCRIPT_DIR"/*.py; do
echo "Converting: $(basename "$py")"
jupytext --to ipynb "$py"
done
+450
View File
@@ -0,0 +1,450 @@
# ---
# jupyter:
# jupytext:
# cell_metadata_filter: -all
# formats: ipynb,py:percent
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.19.1
# ---
# ruff: noqa: E402
# %% [markdown]
# # supervision 0.28.0: Memory-Efficient Instance Segmentation
#
# [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/roboflow/supervision/blob/develop/notebooks/release-demo_0-28.ipynb)
#
# **supervision** is a set of reusable tools for computer vision.
# Two headlining changes in 0.28.0:
#
# 1. **`sv.Detections.from_sam3`** -- first-class support for SAM3 (Segment
# Anything Model 3) inference responses. supervision now parses both the
# PCS (prompt-controlled segmentation) and PVS (point-video segmentation)
# output formats directly into a `sv.Detections` object.
#
# 2. **`sv.CompactMask`** -- instance masks stored as RLE-encoded bounding-box
# crops instead of full-resolution bitmaps. Any segmentation model --
# RF-DETR Seg, SAM3, YOLO-Seg -- can feed into CompactMask. Memory drops
# 10-100x without changing the API anywhere in supervision.
#
# **Story**: run RF-DETR Seg on a real image, visualise the masks, then convert
# to CompactMask and watch the memory footprint collapse.
#
# **Sections:**
# 1. [Install](#1-install)
# 2. [Download sample image](#2-download-sample-image)
# 3. [RF-DETR Seg -- instance segmentation](#3-rf-detr-seg)
# 4. [CompactMask -- memory-efficient storage](#4-compactmask)
# 5. [SAM3 -- text-prompted segmentation](#5-sam3)
# 6. [Other notable changes in 0.28.0](#6-other-notable-changes)
# 7. [Next steps](#7-next-steps)
# %% [markdown]
# ## 1. Install
# %%
# !pip install -q 'supervision==0.28.0' 'rfdetr' 'inference-sdk>=0.9' numpy matplotlib
# %% [markdown]
# ## 2. Download sample image
#
# `sv.ImageAssets` is new in 0.28.0 -- a counterpart to the existing
# `sv.VideoAssets`. `download_assets` caches locally and returns the path.
# %%
# %matplotlib inline
import cv2
import matplotlib.pyplot as plt
import numpy as np
import supervision as sv
from supervision.assets import ImageAssets, download_assets
image_path = download_assets(ImageAssets.PEOPLE_WALKING)
print(f"Image: {image_path}")
image_bgr = cv2.imread(image_path)
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
H, W = image_bgr.shape[:2]
print(f"Resolution: {W} x {H}")
plt.figure(figsize=(12, 7))
plt.imshow(image_rgb)
plt.axis("off")
plt.title("people-walking.jpg")
plt.tight_layout()
plt.show()
# %% [markdown]
# ## 3. RF-DETR Seg
#
# **RF-DETR** is a real-time transformer-based object detection model from Roboflow.
# The `RFDETRSegSmall` variant adds an instance segmentation head -- it produces
# one binary mask per detected instance alongside the bounding box.
#
# Key facts for this demo:
#
# - Pretrained on **COCO** (80 object categories) -- detects people, bags, cars, etc.
# - Weights download automatically on first `RFDETRSegSmall()` call (~100 MB).
# - `model.predict()` returns **`sv.Detections`** directly -- no converter needed.
# Masks are a `(N, H, W)` bool array attached as `detections.mask`.
# %%
from rfdetr.detr import RFDETRSegSmall
model = RFDETRSegSmall()
model.optimize_for_inference()
# predict accepts a file path, PIL Image, or RGB numpy array
detections = model.predict(image_path, threshold=0.3)
if not isinstance(detections, sv.Detections):
raise TypeError(f"Expected sv.Detections, got {type(detections).__name__}")
n_masks = 0 if detections.mask is None else len(detections.mask)
print(f"Detections: {len(detections)} (with masks: {n_masks})")
# %% [markdown]
# ### 3.1 COCO class names
#
# COCO has 90 numeric class IDs; map them to readable names for annotation.
# %%
# Subset of COCO class names (IDs 0-based after RF-DETR's remapping).
COCO_NAMES: dict[int, str] = {
0: "person",
1: "bicycle",
2: "car",
3: "motorcycle",
4: "airplane",
5: "bus",
6: "train",
7: "truck",
8: "boat",
24: "backpack",
25: "umbrella",
26: "handbag",
28: "suitcase",
56: "chair",
57: "couch",
58: "potted plant",
59: "bed",
60: "dining table",
62: "tv",
63: "laptop",
67: "cell phone",
72: "refrigerator",
74: "clock",
76: "scissors",
}
labels = []
assert detections.class_id is not None
for cid, conf in zip(
detections.class_id,
detections.confidence
if detections.confidence is not None
else [None] * len(detections),
):
name = COCO_NAMES.get(int(cid), f"cls_{cid}")
labels.append(f"{name} {conf:.2f}" if conf is not None else name)
# %% [markdown]
# ### 3.2 Visualise RF-DETR Seg output
# %%
PALETTE = sv.ColorPalette.DEFAULT
annotated = image_bgr.copy()
annotated = sv.MaskAnnotator(color=PALETTE, opacity=0.45).annotate(
annotated, detections
)
annotated = sv.BoxAnnotator(color=PALETTE, thickness=2).annotate(annotated, detections)
annotated = sv.LabelAnnotator(color=PALETTE, text_scale=0.5, text_thickness=1).annotate(
annotated, detections, labels=labels
)
plt.figure(figsize=(12, 7))
plt.imshow(cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB))
plt.axis("off")
plt.title(f"RF-DETR Seg -- {len(detections)} instance(s)")
plt.tight_layout()
plt.show()
# %% [markdown]
# ## 4. CompactMask
#
# RF-DETR Seg returns one full-resolution binary mask per detected instance.
# On a 1280 x 720 image with 12 people that is:
#
# `12 x 720 x 1280 x 1 byte = 11 MB`
#
# Most of those pixels are background. The actual person silhouette fits in a
# tight bounding box. `sv.CompactMask` stores **only the bounding-box crop**,
# RLE-encoded:
#
# - A 200 x 100 person crop: `~2.5 KB` instead of `900 KB`
# - Drop-in replacement -- all annotators, filters, and `area` keep working
# %% [markdown]
# ### 4.1 Measure dense mask footprint
# %%
from typing import Any
dense_bytes: int = 0
dense_mask: "np.ndarray[Any, np.dtype[np.bool_]] | None" = None
assert detections.mask is not None and isinstance(detections.mask, np.ndarray)
dense_mask = detections.mask
dense_bytes = dense_mask.nbytes
n_inst = len(dense_mask)
print(f"Instances: {n_inst}")
print(f"Mask shape: {dense_mask.shape} (N x H x W, bool)")
print(f"Dense footprint: {dense_bytes / 1024:.1f} KB")
print(f" = {n_inst} masks x {H} x {W} x 1 byte")
# %% [markdown]
# ### 4.2 Convert to CompactMask
# %%
compact: "sv.CompactMask | None" = None
crop_bytes: int = 0
assert dense_mask is not None
compact = sv.CompactMask.from_dense(
masks=dense_mask,
xyxy=detections.xyxy,
image_shape=(H, W),
)
# Measure compact size via uncompressed crop booleans (upper bound; RLE < this).
crop_bytes = sum(compact.crop(i).nbytes for i in range(len(compact)))
print(f"Crop size (est.): {crop_bytes / 1024:.1f} KB (uncompressed crops)")
if crop_bytes > 0 and dense_bytes > 0:
ratio = dense_bytes / crop_bytes
print(f"Reduction factor: {ratio:.1f}x (before RLE compression)")
# Swap in CompactMask -- supervision uses it transparently from here on.
detections.mask = compact
print(f"\ndetections.mask type: {type(detections.mask).__name__}")
# %% [markdown]
# ### 4.3 Filtering by mask area
#
# `compact.area` returns the true pixel count of each instance mask.
# Filter out tiny detections (partial occlusions, image-edge artefacts).
# %%
large: sv.Detections = detections
large_labels: list[str] = labels
assert isinstance(detections.mask, sv.CompactMask)
areas = detections.mask.area
print(
f"Mask areas (px): min={areas.min():.0f} "
f"mean={areas.mean():.0f} max={areas.max():.0f}"
)
# Keep instances larger than 0.1% of the image.
min_area = 0.001 * H * W
keep_idx = np.where(areas > min_area)[0]
_filtered = detections[keep_idx]
if isinstance(_filtered, sv.Detections):
large = _filtered
large_labels = [labels[i] for i in keep_idx] if labels else []
print(f"\nInstances > {min_area:.0f} px: {len(large)}")
# %% [markdown]
# ### 4.4 Annotate with CompactMask
#
# Annotators call `.to_dense()` internally -- CompactMask is invisible to them.
# %%
assert isinstance(detections.mask, sv.CompactMask) and dense_bytes > 0
annotated_compact = image_bgr.copy()
annotated_compact = sv.MaskAnnotator(color=PALETTE, opacity=0.45).annotate(
annotated_compact, large
)
annotated_compact = sv.BoxAnnotator(color=PALETTE, thickness=2).annotate(
annotated_compact, large
)
annotated_compact = sv.LabelAnnotator(
color=PALETTE, text_scale=0.5, text_thickness=1
).annotate(annotated_compact, large, labels=large_labels)
plt.figure(figsize=(12, 7))
plt.imshow(cv2.cvtColor(annotated_compact, cv2.COLOR_BGR2RGB))
plt.axis("off")
plt.title(
f"CompactMask (filtered) -- {len(large)} instance(s) "
f"| {dense_bytes / 1024:.0f} KB dense -> {crop_bytes / 1024:.0f} KB crops"
)
plt.tight_layout()
plt.show()
# %% [markdown]
# ### 4.5 Per-instance crop
#
# `compact.crop(i)` decodes only the bounding-box crop for instance `i` as a
# `(H_crop, W_crop)` bool array -- no full mask materialised.
# %%
assert isinstance(detections.mask, sv.CompactMask) and len(detections) > 0
crop = detections.mask.crop(0)
bbox = detections.mask.bbox_xyxy[0].astype(int)
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].imshow(image_rgb[bbox[1] : bbox[3], bbox[0] : bbox[2]])
axes[0].set_title("Image crop (instance 0)")
axes[0].axis("off")
axes[1].imshow(crop, cmap="gray")
axes[1].set_title(f"Mask crop ({crop.shape[1]} x {crop.shape[0]} px)")
axes[1].axis("off")
plt.tight_layout()
plt.show()
full_px = H * W
crop_kb = crop.nbytes / 1024
print(f"Full-res mask slot: {H} x {W} = {full_px / 1024:.0f} KB")
print(f"Compact crop: {crop.shape[0]} x {crop.shape[1]} = {crop_kb:.1f} KB")
# %% [markdown]
# ## 5. SAM3
#
# `sv.Detections.from_sam3()` is the other headline in 0.28.0.
# SAM3 segments objects by free-text prompts -- `"person"`, `"bag"`, any phrase.
# supervision parses both the PCS and PVS response formats into a standard
# `sv.Detections`, with `class_id` set to the prompt index.
#
# This section runs only when `ROBOFLOW_API_KEY` is available.
# %%
import base64
import os
from typing import Optional
import requests
try:
from google.colab import userdata # type: ignore[import, unused-ignore]
ROBOFLOW_API_KEY: str = userdata.get("ROBOFLOW_API_KEY") or ""
except Exception:
ROBOFLOW_API_KEY = os.environ.get("ROBOFLOW_API_KEY", "")
PROMPTS = ["person", "bag"]
sam3_detections: Optional[sv.Detections] = None
assert ROBOFLOW_API_KEY
with open(image_path, "rb") as _f:
_img_b64 = base64.b64encode(_f.read()).decode("utf-8")
_response = requests.post(
f"https://api.roboflow.com/inferenceproxy/seg-preview?api_key={ROBOFLOW_API_KEY}",
json={
"image": {"type": "base64", "value": _img_b64},
"prompts": [{"type": "text", "text": p} for p in PROMPTS],
"output_prob_thresh": 0.3,
},
headers={"Content-Type": "application/json"},
timeout=60,
)
_response.raise_for_status()
sam3_result: dict[str, Any] = _response.json()
sam3_detections = sv.Detections.from_sam3(sam3_result=sam3_result, resolution_wh=(W, H))
print(f"SAM3 detections: {len(sam3_detections)}")
if sam3_detections.class_id is not None:
for idx, prompt in enumerate(PROMPTS):
count = int((sam3_detections.class_id == idx).sum())
print(f" [{idx}] '{prompt}': {count} instance(s)")
# %%
assert sam3_detections is not None and len(sam3_detections) > 0
sam3_labels = (
[PROMPTS[c] for c in sam3_detections.class_id]
if sam3_detections.class_id is not None
else []
)
SAM3_PALETTE = sv.ColorPalette.from_hex(["#ff6b6b", "#4ecdc4"])
annotated_sam3 = image_bgr.copy()
annotated_sam3 = sv.MaskAnnotator(color=SAM3_PALETTE, opacity=0.45).annotate(
annotated_sam3, sam3_detections
)
annotated_sam3 = sv.BoxAnnotator(color=SAM3_PALETTE, thickness=2).annotate(
annotated_sam3, sam3_detections
)
annotated_sam3 = sv.LabelAnnotator(
color=SAM3_PALETTE, text_scale=0.5, text_thickness=1
).annotate(annotated_sam3, sam3_detections, labels=sam3_labels)
plt.figure(figsize=(12, 7))
plt.imshow(cv2.cvtColor(annotated_sam3, cv2.COLOR_BGR2RGB))
plt.axis("off")
plt.title(f"SAM3 -- from_sam3() -- {len(sam3_detections)} instance(s)")
plt.tight_layout()
plt.show()
# %% [markdown]
# ## 6. Other notable changes in 0.28.0
#
# ### `VideoInfo.fps` is now `float`
#
# NTSC frame rates (23.976, 29.97, 59.94) were silently truncated to `int`.
# Wrap with `int()` at call sites that require an integer.
# %%
import collections
from supervision.assets import VideoAssets
video_path = download_assets(VideoAssets.PEOPLE_WALKING)
info = sv.VideoInfo.from_video_path(video_path)
print(f"fps: {info.fps} ({type(info.fps).__name__}) -- was int before 0.28.0")
fps_int = int(info.fps)
buf: collections.deque[sv.Detections] = collections.deque(maxlen=fps_int)
trace = sv.TraceAnnotator(trace_length=fps_int)
print(f"deque maxlen: {buf.maxlen} (= int({info.fps}))")
# %% [markdown]
# ### `sv.ByteTrack` deprecated
#
# `sv.ByteTrack` still works in 0.28.0 and 0.29.0 but emits a
# `DeprecationWarning`. Migrate to `ByteTrackTracker` from the external
# [`trackers`](https://pypi.org/project/trackers/) package before 0.30.0.
#
# ```python
# # Before
# tracker = sv.ByteTrack()
# detections = tracker.update_with_detections(detections)
#
# # After (pip install trackers)
# from trackers import ByteTrackTracker
# tracker = ByteTrackTracker()
# detections = tracker.update(detections)
# ```
# %% [markdown]
# ## 7. Next steps
#
# - [`sv.CompactMask` docs](https://supervision.roboflow.com/develop/detection/compact_mask/)
# -- full API reference: `resize`, `merge`, `with_offset`
# - [`sv.Detections.from_sam3` docs](https://supervision.roboflow.com/develop/detection/core/)
# -- PCS and PVS format reference
# - [RF-DETR docs](https://github.com/roboflow/rf-detr)
# -- training, export, and deployment
# - [Full changelog](https://supervision.roboflow.com/develop/changelog/)
# -- every change in 0.28.0
+6 -2
View File
@@ -4,7 +4,7 @@ requires = [ "setuptools>=61" ]
[project]
name = "supervision"
version = "0.28.0rc1"
version = "0.28.0"
description = "A set of easy-to-use utils that will come in handy in any Computer Vision project"
readme = "README.md"
keywords = [
@@ -90,7 +90,7 @@ docs = [
build = [
"build>=0.10,<1.5",
"twine>=5.1.1,<7",
"wheel>=0.40,<0.47",
"wheel>=0.40,<0.48",
]
[tool.setuptools]
@@ -154,6 +154,10 @@ lint.select = [
]
lint.ignore = []
lint.per-file-ignores."__init__.py" = [ "E402", "F401" ]
lint.per-file-ignores."notebooks/**" = [
"PT018", # Assertion should be broken down into multiple parts
"S101", # Use of `assert` detected
]
lint.per-file-ignores."src/**" = [
"S101", # TODO: Replace asserts with proper error handling
]
+2
View File
@@ -64,6 +64,7 @@ from supervision.detection.utils.boxes import (
scale_boxes,
)
from supervision.detection.utils.converters import (
is_compressed_rle,
mask_to_polygons,
mask_to_rle,
mask_to_xyxy,
@@ -237,6 +238,7 @@ __all__ = [
"get_video_frames_generator",
"grayscale_image",
"hex_to_rgba",
"is_compressed_rle",
"is_valid_hex",
"letterbox_image",
"list_files_with_extensions",
+18 -6
View File
@@ -1967,7 +1967,10 @@ class TraceAnnotator(BaseAnnotator):
trace_length: The maximum length of the trace in terms of historical
points. Defaults to `30`.
thickness: The thickness of the trace lines. Defaults to `2`.
smooth: Smooth the trace lines.
smooth: If `True`, applies spline smoothing to trace lines using
consecutive unique anchor points. Falls back to a raw polyline
when fewer than 4 unique points are available (e.g. when a
tracker is stationary).
color_lookup: Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
@@ -2054,11 +2057,20 @@ class TraceAnnotator(BaseAnnotator):
xy = self.trace.get(tracker_id=tracker_id)
spline_points: npt.NDArray[np.int32] = 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 self.smooth:
unique_xy = xy[
np.concatenate(([True], np.any(np.diff(xy, axis=0) != 0, axis=1)))
]
if len(unique_xy) > 3:
try:
x, y = unique_xy[:, 0], unique_xy[:, 1]
tck, _u = splprep([x, y], s=20)
xy_new = splev(np.linspace(0, 1, 100), tck)
spline_points = np.stack(xy_new, axis=1).astype(np.int32)
except ValueError:
spline_points = unique_xy.astype(np.int32)
else:
spline_points = unique_xy.astype(np.int32)
if len(xy) > 1:
cv2.polylines(
+41 -2
View File
@@ -1,4 +1,3 @@
import os
import warnings
from datetime import datetime
from pathlib import Path
@@ -278,6 +277,19 @@ def load_coco_annotations(
Returns:
A tuple of `(classes, image_paths, annotations)`.
Raises:
ValueError: If any annotation's ``file_name`` resolves to the images
directory itself, to a path outside the images directory (e.g. via
``../`` traversal or an absolute path), or to a subdirectory instead
of a regular image file.
Note:
Each annotation's ``file_name`` is validated against
``images_directory_path`` before loading. Annotations that reference
paths outside the directory are rejected to prevent path-traversal
attacks when loading user-supplied annotation files. Symlinked images
pointing outside the resolved images directory are also rejected.
"""
coco_data = read_json_file(file_path=annotations_path)
classes = coco_categories_to_classes(coco_categories=coco_data["categories"])
@@ -293,6 +305,7 @@ def load_coco_annotations(
images = []
annotations = {}
images_directory_resolved = Path(images_directory_path).resolve()
for coco_image in coco_images:
image_name, image_width, image_height = (
@@ -301,7 +314,33 @@ def load_coco_annotations(
coco_image["height"],
)
image_annotations = coco_annotations_groups.get(coco_image["id"], [])
image_path = os.path.join(images_directory_path, image_name)
image_path = str(Path(images_directory_path) / Path(image_name))
try:
resolved_image_path = Path(image_path).resolve()
except (OSError, ValueError) as exc:
raise ValueError(
f"COCO annotation refers to image {image_name!r}, which "
f"produces an invalid path: {exc}"
) from exc
if resolved_image_path == images_directory_resolved:
raise ValueError(
f"COCO annotation refers to image {image_name!r}, which "
f"resolves to the images directory itself "
f"({images_directory_resolved}). Expected a path to an "
"image file."
)
if images_directory_resolved not in resolved_image_path.parents:
raise ValueError(
f"COCO annotation refers to image {image_name!r}, which "
f"resolves to {resolved_image_path} — outside the images "
f"directory {images_directory_resolved}."
)
if resolved_image_path.is_dir():
raise ValueError(
f"COCO annotation refers to image {image_name!r}, which "
f"resolves to directory {resolved_image_path}. Expected a "
"path to an image file."
)
with_masks = force_masks or any(
_with_seg_mask(annotation) for annotation in image_annotations
+497 -96
View File
@@ -11,94 +11,24 @@ crop boundaries, so no extra metadata is required from the caller.
from __future__ import annotations
import os
from collections.abc import Iterator
from typing import Any, cast
from typing import Any
import numpy as np
import numpy.typing as npt
def _rle_encode(mask_2d: npt.NDArray[Any]) -> npt.NDArray[np.int32]:
"""Run-length encode a 2D boolean mask in row-major order.
The encoding starts with the count of leading ``False`` values (may be 0
if the mask begins with ``True``). Subsequent values alternate between
``True`` and ``False`` run counts.
Args:
mask_2d: 2D boolean array of shape ``(H, W)``.
Returns:
int32 array of run lengths, starting with the False count.
Examples:
```pycon
>>> import numpy as np
>>> from supervision.detection.compact_mask import _rle_encode
>>> mask = np.array([[False, True, True], [True, False, False]])
>>> _rle_encode(mask).tolist()
[1, 3, 2]
```
"""
flat = mask_2d.ravel() # C-order (row-major)
if len(flat) == 0:
return np.array([0], dtype=np.int32)
# Locate positions where the boolean value changes.
changes = np.diff(flat.view(np.uint8))
boundaries = np.where(changes != 0)[0] + 1
positions = np.concatenate(([0], boundaries, [len(flat)]))
run_lengths = np.diff(positions).astype(np.int32)
# Guarantee the encoding always starts with a False count.
if flat[0]:
run_lengths = np.concatenate(([np.int32(0)], run_lengths))
return run_lengths
def _rle_decode(
rle: npt.NDArray[np.int32], height: int, width: int
) -> npt.NDArray[np.bool_]:
"""Decode a run-length encoded mask back to a 2D boolean array.
Args:
rle: int32 array of run lengths as produced by :func:`_rle_encode`.
height: Height of the output array.
width: Width of the output array.
Returns:
2D boolean array of shape ``(height, width)``.
Examples:
```pycon
>>> import numpy as np
>>> from supervision.detection.compact_mask import _rle_decode
>>> rle = np.array([1, 3, 2], dtype=np.int32)
>>> _rle_decode(rle, 2, 3)
array([[False, True, True],
[ True, False, False]])
```
"""
# Even-indexed entries → False runs; odd-indexed entries → True runs.
is_true = np.arange(len(rle)) % 2 == 1
flat: npt.NDArray[np.bool_] = np.repeat(is_true, rle)
num_pixels = height * width
if len(flat) < num_pixels:
# Pad with False if the RLE is shorter than expected (e.g. all-False
# tails are often omitted during encoding).
flat = np.pad(flat, (0, num_pixels - len(flat)))
return cast(npt.NDArray[np.bool_], flat[:num_pixels].reshape(height, width))
from supervision.detection.utils.converters import (
_mask_to_rle_counts,
_rle_counts_to_mask,
)
def _rle_area(rle: npt.NDArray[np.int32]) -> int:
"""Return the number of ``True`` pixels in a run-length encoded mask.
Args:
rle: int32 array of run lengths as produced by :func:`_rle_encode`.
rle: int32 array of run lengths as produced by :func:`_mask_to_rle_counts`.
Returns:
Total number of ``True`` pixels.
@@ -107,7 +37,7 @@ def _rle_area(rle: npt.NDArray[np.int32]) -> int:
```pycon
>>> import numpy as np
>>> from supervision.detection.compact_mask import _rle_area
>>> rle = np.array([1, 3, 2], dtype=np.int32) # 1 F, 3 T, 2 F
>>> rle = np.array([1, 2, 1, 1, 1], dtype=np.int32)
>>> _rle_area(rle)
3
@@ -116,6 +46,346 @@ def _rle_area(rle: npt.NDArray[np.int32]) -> int:
return int(np.sum(rle[1::2]))
def _rle_split_cols(
rle: npt.NDArray[np.int32],
crop_h: int,
crop_w: int,
) -> list[list[int]]:
"""Split a flat F-order RLE into per-column run lists.
With F-order (column-major) RLE the flat pixel sequence visits all rows
of column 0, then all rows of column 1, etc. Each column therefore
contains ``crop_h`` contiguous pixels.
Runs that cross column boundaries are split at the boundary. Each
returned list starts with a ``False``-run count (possibly 0), matching
the convention of :func:`_mask_to_rle_counts`.
Args:
rle: int32 run-length array as produced by
:func:`~supervision.detection.utils.converters._mask_to_rle_counts`.
crop_h: Number of rows (pixels per column).
crop_w: Number of columns.
Returns:
List of ``crop_w`` run lists, one per column. Each list sums to
``crop_h``.
Examples:
```pycon
>>> import numpy as np
>>> from supervision.detection.compact_mask import _rle_split_cols
>>> from supervision.detection.utils.converters import _mask_to_rle_counts
>>> mask = np.array([[True, False], [True, True]], dtype=bool)
>>> rle = _mask_to_rle_counts(mask)
>>> rle.tolist()
[0, 2, 1, 1]
>>> _rle_split_cols(rle, 2, 2)
[[0, 2], [1, 1]]
```
"""
per_col: list[list[int]] = [[] for _ in range(crop_w)]
col = 0
row = 0
for run_idx, run_len in enumerate(rle):
is_true = run_idx % 2 == 1
remaining = int(run_len)
while remaining > 0:
space_in_col = crop_h - row
take = min(remaining, space_in_col)
if len(per_col[col]) == 0:
if is_true:
per_col[col].append(0) # leading False count = 0
# Check if last run has same parity (True/False) as current chunk.
# Last element's parity: index (len-1) odd → True, even → False.
elif is_true == ((len(per_col[col]) - 1) % 2 == 1):
per_col[col][-1] += take
remaining -= take
row += take
if row >= crop_h:
row = 0
col += 1
continue
per_col[col].append(take)
remaining -= take
row += take
if row >= crop_h:
row = 0
col += 1
if col >= crop_w:
break
# Fill any empty columns (all-False).
for c in range(crop_w):
if not per_col[c]:
per_col[c] = [crop_h]
return per_col
def _rle_scale_col(
col_runs: list[int],
src_h: int,
row_map: npt.NDArray[np.int32],
) -> list[int]:
"""Scale one column's run list to a new height using a precomputed row map.
Each output row is mapped to a source row via ``row_map``, which
implements nearest-neighbour resampling in the vertical direction.
Args:
col_runs: Per-column run list starting with a ``False``-run count.
src_h: Height of the source column (sum of ``col_runs``).
row_map: int32 array of length ``new_crop_h``; ``row_map[r']`` is the
source row index for output row ``r'``. Use
``(np.arange(new_crop_h) * src_h // new_crop_h)`` for
``cv2.INTER_NEAREST``-compatible mapping.
Returns:
Scaled run list of total length ``len(row_map)``, always starting
with a ``False``-run count.
Examples:
```pycon
>>> import numpy as np
>>> from supervision.detection.compact_mask import _rle_scale_col
>>> col_runs = [0, 2, 2] # F=0, T=2, F=2 → [T, T, F, F]
>>> row_map = np.array([0, 1, 2, 3, 0, 1, 2, 3], dtype=np.int32)
>>> _rle_scale_col(col_runs, 4, row_map)
[0, 2, 2, 2, 2]
```
"""
new_crop_h = len(row_map)
if new_crop_h == 0:
return [0]
# Reconstruct per-source-row boolean values from run list.
src_values: npt.NDArray[np.bool_] = np.empty(src_h, dtype=np.bool_)
pos = 0
for ri, rl in enumerate(col_runs):
src_values[pos : pos + rl] = ri % 2 == 1 # odd index → True
pos += rl
if pos < src_h:
src_values[pos:] = False # pad truncated RLE
# Map output rows to source values.
out_values = src_values[row_map]
# RLE-encode the output column; vectorised via np.diff on bool view.
out_uint8 = out_values.view(np.uint8)
boundaries = np.flatnonzero(np.diff(out_uint8))
run_starts: npt.NDArray[np.int64] = np.empty(len(boundaries) + 1, dtype=np.int64)
run_ends: npt.NDArray[np.int64] = np.empty(len(boundaries) + 1, dtype=np.int64)
run_starts[0] = 0
run_starts[1:] = boundaries + 1
run_ends[:-1] = boundaries + 1
run_ends[-1] = new_crop_h
result_runs: list[int] = (run_ends - run_starts).tolist()
# RLE starts with a False count; prepend 0 if output begins with True.
if bool(out_values[0]):
result_runs.insert(0, 0)
return result_runs
def _rle_join_cols(
scaled_cols: list[list[int]],
new_total: int,
) -> npt.NDArray[np.int32]:
"""Concatenate per-column run lists into a flat RLE, merging junctions.
Each column run list starts with a ``False``-run count. Two junction types
can be merged across column boundaries:
* ``False``/``False``: the trailing False run merges with the leading False
run of the next column (leading count may be zero).
* ``True``/``True``: when the accumulated output ends on a True run and the
next column's leading False count is zero (column starts with True), the
two True runs are merged to avoid inserting a zero-length False run that
would inflate ``len(rle)`` and skew the density metric in
:func:`_resize_crop`.
Args:
scaled_cols: List of per-column run lists, each starting with a
``False``-run count.
new_total: Total pixel count of the output (fallback for empty input).
Returns:
Flat int32 RLE array starting with a ``False``-run count.
Examples:
```pycon
>>> import numpy as np
>>> from supervision.detection.compact_mask import _rle_join_cols
>>> cols = [[1, 2], [1, 2]] # each col: F=1, T=2
>>> _rle_join_cols(cols, 6).tolist()
[1, 2, 1, 2]
```
"""
output_runs: list[int] = []
for col_runs in scaled_cols:
if not output_runs:
output_runs.extend(col_runs)
else:
last_is_true = (len(output_runs) - 1) % 2 == 1
# col_runs always starts with a False count → first_is_true=False
if not last_is_true: # last == False == first → merge
output_runs[-1] += col_runs[0]
output_runs.extend(col_runs[1:])
elif col_runs[0] == 0 and len(col_runs) > 1:
# last run = True; column also starts True (leading False = 0)
# → merge to avoid a zero-length False run at the junction.
output_runs[-1] += col_runs[1]
output_runs.extend(col_runs[2:])
else:
output_runs.extend(col_runs)
return np.array(output_runs if output_runs else [new_total], dtype=np.int32)
def _rle_resize(
rle: npt.NDArray[np.int32],
crop_h: int,
crop_w: int,
new_crop_h: int,
new_crop_w: int,
) -> npt.NDArray[np.int32]:
"""Resize an F-order RLE-encoded crop via nearest-neighbour resampling.
Manipulates run lengths directly without decoding to a full 2D boolean
array. Delegates to :func:`_rle_split_cols`, :func:`_rle_scale_col`,
and :func:`_rle_join_cols`.
The nearest-neighbour mapping ``src = floor(dst * src_size / dst_size)``
is bit-exact with ``cv2.INTER_NEAREST``.
Args:
rle: int32 array of F-order run lengths as produced by
:func:`~supervision.detection.utils.converters._mask_to_rle_counts`.
Starts with a ``False``-run count (may be 0).
crop_h: Height of the original crop.
crop_w: Width of the original crop.
new_crop_h: Height of the resized crop.
new_crop_w: Width of the resized crop.
Returns:
int32 array of F-order run lengths for the resized crop, starting
with the ``False``-run count.
Examples:
Upscale a 3x3 mask with a diagonal True stripe to 6x6:
```pycon
>>> import numpy as np
>>> from supervision.detection.compact_mask import _rle_resize
>>> from supervision.detection.utils.converters import (
... _mask_to_rle_counts, _rle_counts_to_mask,
... )
>>> mask = np.array([
... [True, False, False],
... [False, True, False],
... [False, False, True ],
... ], dtype=bool)
>>> rle = _mask_to_rle_counts(mask)
>>> resized_rle = _rle_resize(rle, 3, 3, 6, 6)
>>> result = _rle_counts_to_mask(resized_rle, 6, 6)
>>> result.astype(int)
array([[1, 1, 0, 0, 0, 0],
[1, 1, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0],
[0, 0, 1, 1, 0, 0],
[0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 1, 1]])
```
"""
new_total = new_crop_h * new_crop_w
if crop_h * crop_w == 0 or new_total == 0:
return np.array([0], dtype=np.int32)
if len(rle) == 1 or int(np.sum(rle[1::2])) == 0:
return np.array([new_total], dtype=np.int32)
if len(rle) == 2 and rle[0] == 0:
return np.array([0, new_total], dtype=np.int32)
per_col = _rle_split_cols(rle, crop_h, crop_w)
# cv2.INTER_NEAREST column mapping: src = floor(dst * src_w / dst_w)
col_map = (np.arange(new_crop_w) * crop_w // new_crop_w).astype(np.int32)
# cv2.INTER_NEAREST row mapping: src = floor(dst * src_h / dst_h)
row_map = (np.arange(new_crop_h) * crop_h // new_crop_h).astype(np.int32)
# Scale each unique source column once; reuse via cache for repeated cols.
col_cache: dict[int, list[int]] = {}
scaled_cols = []
for src_c in col_map:
if src_c not in col_cache:
col_cache[src_c] = _rle_scale_col(per_col[src_c], crop_h, row_map)
scaled_cols.append(col_cache[src_c])
return _rle_join_cols(scaled_cols, new_total)
# Fraction of (run_count / pixel_count) below which _rle_resize is used
# instead of the decode → cv2 → re-encode path. Sparse masks have few long
# runs; dense/complex masks approach 1 run per 2 pixels.
_L3_DENSITY_THRESHOLD: float = 0.25
# Thread overhead outweighs gains below this mask count.
_PARALLEL_THRESHOLD: int = 8
def _resize_crop(
rle: npt.NDArray[np.int32],
orig_h: int,
orig_w: int,
new_h: int,
new_w: int,
) -> npt.NDArray[np.int32]:
"""Resize one RLE crop to ``(new_h, new_w)``, choosing the fastest path.
Dispatch order:
1. **All-False fast path** — returns a single False run; no decode.
2. **L3 direct RLE path** — used when run density is below
:data:`_L3_DENSITY_THRESHOLD`; manipulates run lengths without
allocating a 2D array.
3. **cv2 fallback** — decodes to ``uint8``, calls
``cv2.resize(INTER_NEAREST)``, re-encodes; used for dense masks.
Args:
rle: int32 run-length array for the source crop.
orig_h: Height of the source crop.
orig_w: Width of the source crop.
new_h: Target height.
new_w: Target width.
Returns:
int32 RLE array for the resized crop.
"""
import cv2
# All-False: skip decode entirely.
if _rle_area(rle) == 0:
return np.array([new_h * new_w], dtype=np.int32)
# L3: direct RLE arithmetic for sparse masks.
if len(rle) / max(1, orig_h * orig_w) < _L3_DENSITY_THRESHOLD:
return _rle_resize(rle, orig_h, orig_w, new_h, new_w)
# cv2 fallback for dense masks.
crop = _rle_counts_to_mask(rle, orig_h, orig_w)
resized = cv2.resize(
crop.view(np.uint8),
(new_w, new_h),
interpolation=cv2.INTER_NEAREST,
).astype(bool)
return _mask_to_rle_counts(resized)
class CompactMask:
"""Memory-efficient crop-RLE mask storage for instance segmentation.
@@ -138,17 +408,28 @@ class CompactMask:
``cm.to_dense().astype(np.uint8)``. :meth:`to_dense` is the single
explicit materialisation boundary.
.. note:: **RLE encoding incompatibility with pycocotools / COCO API**
.. note:: **RLE encoding — COCO / pycocotools pixel-scan order**
:class:`CompactMask` uses **row-major (C-order)** run-lengths scoped
to each mask's bounding-box crop. The COCO API (pycocotools) uses
**column-major (Fortran-order)** run-lengths scoped to the **full
image**. The two formats are not interchangeable: you cannot pass a
:class:`CompactMask` RLE directly to ``maskUtils.iou()`` or
``maskUtils.decode()``, and you cannot load a COCO RLE dict into a
:class:`CompactMask` without re-encoding. Use
:meth:`to_dense` to obtain a standard boolean array, then pass it to
pycocotools if needed.
:class:`CompactMask` uses **column-major (Fortran-order, F-order)**
run-lengths scoped to each mask's bounding-box crop, matching the
pixel-scan order used by the COCO API (pycocotools). The crop scope
still differs from the full-image scope used by pycocotools, so a
:class:`CompactMask` RLE cannot be passed directly to
``maskUtils.iou()`` or ``maskUtils.decode()`` without re-scoping to
the full canvas. Use :meth:`to_dense` to obtain a standard boolean
array for pycocotools interop.
This scan order is part of CompactMask's internal RLE representation.
Switching from row-major (C-order) to column-major (F-order) is a
backward-incompatible format change for any persisted or serialized
:class:`CompactMask` state, including pickled objects and any
external storage of ``._rles``. Older stored RLE arrays will decode
incorrectly under the new convention.
Migration note: load or decode legacy masks with the older version,
materialize them to dense boolean arrays, and then re-encode them
with the current version (for example via :meth:`to_dense` followed
by :meth:`from_dense`) before persisting them again.
Args:
rles: List of N int32 run-length arrays.
@@ -259,7 +540,7 @@ class CompactMask:
crop_h = y2c - y1c + 1
crop_w = x2c - x1c + 1
rles.append(_rle_encode(crop))
rles.append(_mask_to_rle_counts(crop))
crop_shapes_list.append((crop_h, crop_w))
offsets_list.append((x1c, y1c))
@@ -299,7 +580,7 @@ class CompactMask:
int(self._crop_shapes[mask_idx, 1]),
)
x1, y1 = int(self._offsets[mask_idx, 0]), int(self._offsets[mask_idx, 1])
crop = _rle_decode(self._rles[mask_idx], crop_h, crop_w)
crop = _rle_counts_to_mask(self._rles[mask_idx], crop_h, crop_w)
result[mask_idx, y1 : y1 + crop_h, x1 : x1 + crop_w] = crop
return result
@@ -330,7 +611,7 @@ class CompactMask:
"""
crop_h = int(self._crop_shapes[index, 0])
crop_w = int(self._crop_shapes[index, 1])
return _rle_decode(self._rles[index], crop_h, crop_w)
return _rle_counts_to_mask(self._rles[index], crop_h, crop_w)
# ------------------------------------------------------------------
# Sequence / array protocol
@@ -551,7 +832,7 @@ class CompactMask:
crop_w = int(self._crop_shapes[idx, 1])
x1 = int(self._offsets[idx, 0])
y1 = int(self._offsets[idx, 1])
crop = _rle_decode(self._rles[idx], crop_h, crop_w)
crop = _rle_counts_to_mask(self._rles[idx], crop_h, crop_w)
result[y1 : y1 + crop_h, x1 : x1 + crop_w] = crop
return result
@@ -757,7 +1038,7 @@ class CompactMask:
if not rows_any.any():
# All-False: normalise to 1x1 to avoid zero-sized arrays.
new_rles.append(_rle_encode(np.zeros((1, 1), dtype=bool)))
new_rles.append(_mask_to_rle_counts(np.zeros((1, 1), dtype=bool)))
new_crop_shapes_list.append((1, 1))
new_offsets_list.append((x1_off, y1_off))
continue
@@ -768,7 +1049,7 @@ class CompactMask:
x_min, x_max = int(x_indices[0]), int(x_indices[-1])
tight = crop[y_min : y_max + 1, x_min : x_max + 1]
new_rles.append(_rle_encode(tight))
new_rles.append(_mask_to_rle_counts(tight))
new_crop_shapes_list.append((y_max - y_min + 1, x_max - x_min + 1))
new_offsets_list.append((x1_off + x_min, y1_off + y_min))
@@ -886,14 +1167,14 @@ class CompactMask:
if ix1 > ix2 or iy1 > iy2:
anchor_x = min(max(x1, 0), new_w - 1)
anchor_y = min(max(y1, 0), new_h - 1)
out_rles.append(_rle_encode(np.zeros((1, 1), dtype=bool)))
out_rles.append(_mask_to_rle_counts(np.zeros((1, 1), dtype=bool)))
out_crop_shapes.append((1, 1))
out_offsets_list.append((anchor_x, anchor_y))
continue
crop = self.crop(mask_idx)
clipped = crop[iy1 - y1 : iy2 - y1 + 1, ix1 - x1 : ix2 - x1 + 1]
out_rles.append(_rle_encode(clipped))
out_rles.append(_mask_to_rle_counts(clipped))
out_crop_shapes.append((iy2 - iy1 + 1, ix2 - ix1 + 1))
out_offsets_list.append((ix1, iy1))
@@ -903,3 +1184,123 @@ class CompactMask:
np.array(out_offsets_list, dtype=np.int32),
new_image_shape,
)
# ------------------------------------------------------------------
# Resize
# ------------------------------------------------------------------
def resize(self, new_image_shape: tuple[int, int]) -> CompactMask:
"""Return a new CompactMask scaled to a different image resolution.
Each crop mask is resized with nearest-neighbour interpolation.
Sparse masks use direct RLE arithmetic (:func:`_rle_resize`); dense
masks fall back to ``cv2.resize(INTER_NEAREST)``. Offsets and crop
dimensions are scaled proportionally to the new image size.
Performance notes:
* Coordinate arithmetic is fully vectorised (no Python loop over N).
* All-``False`` crops skip decode/resize entirely.
* For N >= 8, resize runs in a thread pool — NumPy and OpenCV
release the GIL so crops execute in parallel on multi-core CPUs.
Args:
new_image_shape: ``(H, W)`` of the target image.
Returns:
New :class:`CompactMask` with updated ``image_shape``, scaled
offsets, scaled crop shapes, and re-encoded RLE crops.
Raises:
ValueError: If any dimension in *new_image_shape* is ``<= 0``.
Examples:
```pycon
>>> import numpy as np
>>> from supervision.detection.compact_mask import CompactMask
>>> masks = np.zeros((1, 100, 100), dtype=bool)
>>> masks[0, 20:40, 30:60] = True
>>> xyxy = np.array([[30, 20, 59, 39]], dtype=np.float32)
>>> cm = CompactMask.from_dense(masks, xyxy, image_shape=(100, 100))
>>> small = cm.resize((50, 50))
>>> small.shape
(1, 50, 50)
>>> small.offsets[0].tolist()
[15, 10]
```
"""
from concurrent.futures import ThreadPoolExecutor
new_h, new_w = new_image_shape
if new_h <= 0 or new_w <= 0:
raise ValueError("new_image_shape must contain positive dimensions")
# fast path — identity resize; list() creates a new container but the
# individual RLE numpy arrays are shared (shallow copy). Callers must
# not mutate returned RLE arrays in-place.
if (new_h, new_w) == self._image_shape:
return CompactMask(
list(self._rles),
self._crop_shapes.copy(),
self._offsets.copy(),
new_image_shape,
)
# empty guard
if len(self) == 0:
return CompactMask(
[],
np.empty((0, 2), dtype=np.int32),
np.empty((0, 2), dtype=np.int32),
new_image_shape,
)
img_h, img_w = self._image_shape
sx = new_w / img_w
sy = new_h / img_h
# L1 — vectorised coordinate arithmetic; no Python loop over N masks.
x1s = self._offsets[:, 0].astype(np.float64)
y1s = self._offsets[:, 1].astype(np.float64)
x2s = x1s + self._crop_shapes[:, 1] - 1 # inclusive right edge
y2s = y1s + self._crop_shapes[:, 0] - 1 # inclusive bottom edge
new_x1s = np.clip(np.round(x1s * sx), 0, new_w - 1).astype(np.int32)
new_y1s = np.clip(np.round(y1s * sy), 0, new_h - 1).astype(np.int32)
new_x2s = np.clip(np.round(x2s * sx), 0, new_w - 1).astype(np.int32)
new_y2s = np.clip(np.round(y2s * sy), 0, new_h - 1).astype(np.int32)
new_crop_ws: npt.NDArray[np.int32] = np.maximum(
1, new_x2s - new_x1s + 1
).astype(np.int32)
new_crop_hs: npt.NDArray[np.int32] = np.maximum(
1, new_y2s - new_y1s + 1
).astype(np.int32)
# L2b — parallel per-crop resize; NumPy and OpenCV release the GIL.
orig_crop_hs = self._crop_shapes[:, 0]
orig_crop_ws = self._crop_shapes[:, 1]
args = [
(
self._rles[i],
int(orig_crop_hs[i]),
int(orig_crop_ws[i]),
int(new_crop_hs[i]),
int(new_crop_ws[i]),
)
for i in range(len(self))
]
n = len(self)
if n >= _PARALLEL_THRESHOLD:
with ThreadPoolExecutor(max_workers=min(n, os.cpu_count() or 4)) as pool:
new_rles: list[npt.NDArray[np.int32]] = list(
pool.map(lambda a: _resize_crop(*a), args)
)
else:
new_rles = [_resize_crop(*a) for a in args]
new_crop_shapes = np.column_stack((new_crop_hs, new_crop_ws)).astype(np.int32)
new_offsets = np.column_stack((new_x1s, new_y1s)).astype(np.int32)
return CompactMask(new_rles, new_crop_shapes, new_offsets, new_image_shape)
+51 -5
View File
@@ -39,6 +39,9 @@ class CSVSink:
CSVSink allows passing custom data alongside detection fields, providing
flexibility for logging various types of information.
When a list or tuple value in custom_data (or detections.data) has the
same length as the detection count, each element is written to the
corresponding detection row; any other value is broadcast to all rows.
Args:
file_name: The name of the CSV file where the detections will be stored.
@@ -113,17 +116,56 @@ class CSVSink:
self.file.close()
@staticmethod
def _slice_value(value: Any, i: int) -> Any:
def _slice_value(value: Any, i: int, n: int) -> Any:
"""
Return the i-th element when the value stores per-detection data.
Dispatch rules:
- np.ndarray with ndim == 0: return as-is for broadcasting
- np.ndarray with ndim >= 1: return value[i]
- list or tuple with len equal to n: return value[i]
- any other type: return as-is for broadcasting
Args:
value: Custom-data field value.
i: Zero-based detection index.
n: Total number of detections.
Returns:
Element at position i if value is a per-detection sequence,
otherwise value unchanged.
"""
if isinstance(value, np.ndarray):
return value if value.ndim == 0 else value[i]
if isinstance(value, (list, tuple)) and len(value) == n:
return value[i]
return value
@staticmethod
def parse_detection_data(
detections: Detections, custom_data: dict[str, Any] | None = None
) -> list[dict[str, Any]]:
"""
Convert detections and optional custom data into per-detection rows.
Builds one dictionary per detection containing bounding box coordinates,
detection attributes, and any values from ``detections.data`` or
``custom_data``. List and tuple values in ``custom_data`` with length
equal to ``len(detections.xyxy)`` are sliced one element per row; all
other values are broadcast to every row.
Args:
detections: Detection data to serialize into row dictionaries.
custom_data: Optional extra fields to include in each row.
Returns:
A list of dictionaries, one per detection, containing ``xyxy``
coordinates, ``class_id``, ``confidence``, ``tracker_id``, and any
values from ``detections.data`` or ``custom_data``.
"""
parsed_rows = []
for i in range(len(detections.xyxy)):
n = len(detections.xyxy)
for i in range(n):
row = {
"x_min": detections.xyxy[i][0],
"y_min": detections.xyxy[i][1],
@@ -142,11 +184,11 @@ class CSVSink:
if hasattr(detections, "data"):
for key, value in detections.data.items():
row[key] = CSVSink._slice_value(value, i)
row[key] = CSVSink._slice_value(value, i, n)
if custom_data:
for key, value in custom_data.items():
row[key] = CSVSink._slice_value(value, i)
row[key] = CSVSink._slice_value(value, i, n)
parsed_rows.append(row)
return parsed_rows
@@ -159,7 +201,11 @@ class CSVSink:
Args:
detections: The detection data.
custom_data: Custom data to include.
custom_data: Custom data to include. Scalars, dictionaries, and
other non-sequence values are broadcast to every detection in
this batch. NumPy arrays, lists, and tuples with length equal
to ``len(detections)`` are sliced per detection; other lists
and tuples are broadcast unchanged.
"""
if not self.writer:
raise Exception(
+51 -5
View File
@@ -21,6 +21,9 @@ class JSONSink:
JSONSink allows passing custom data alongside detection fields, providing
flexibility for logging various types of information.
When a list or tuple value in custom_data (or detections.data) has the
same length as the detection count, each element is written to the
corresponding detection row; any other value is broadcast to all rows.
Args:
file_name: The name of the JSON file where the detections will be stored.
@@ -85,17 +88,56 @@ class JSONSink:
self.file.close()
@staticmethod
def _slice_value(value: Any, i: int) -> Any:
def _slice_value(value: Any, i: int, n: int) -> Any:
"""
Return the i-th element when the value stores per-detection data.
Dispatch rules:
- np.ndarray with ndim == 0: return as-is for broadcasting
- np.ndarray with ndim >= 1: return value[i]
- list or tuple with len equal to n: return value[i]
- any other type: return as-is for broadcasting
Args:
value: Custom-data field value.
i: Zero-based detection index.
n: Total number of detections.
Returns:
Element at position i if value is a per-detection sequence,
otherwise value unchanged.
"""
if isinstance(value, np.ndarray):
return value if value.ndim == 0 else value[i]
if isinstance(value, (list, tuple)) and len(value) == n:
return value[i]
return value
@staticmethod
def parse_detection_data(
detections: Detections, custom_data: dict[str, Any] | None = None
) -> list[dict[str, Any]]:
"""
Convert detections and optional custom data into per-detection rows.
Builds one dictionary per detection containing bounding box coordinates,
detection attributes, and any values from ``detections.data`` or
``custom_data``. List and tuple values in ``custom_data`` with length
equal to ``len(detections.xyxy)`` are sliced one element per row; all
other values are broadcast to every row.
Args:
detections: Detection data to serialize into row dictionaries.
custom_data: Optional extra fields to include in each row.
Returns:
A list of dictionaries, one per detection, containing ``xyxy``
coordinates, ``class_id``, ``confidence``, ``tracker_id``, and any
values from ``detections.data`` or ``custom_data``.
"""
parsed_rows = []
for i in range(len(detections.xyxy)):
n = len(detections.xyxy)
for i in range(n):
row = {
"x_min": float(detections.xyxy[i][0]),
"y_min": float(detections.xyxy[i][1]),
@@ -114,11 +156,11 @@ class JSONSink:
if hasattr(detections, "data"):
for key, value in detections.data.items():
row[key] = str(JSONSink._slice_value(value, i))
row[key] = str(JSONSink._slice_value(value, i, n))
if custom_data:
for key, value in custom_data.items():
v = JSONSink._slice_value(value, i)
v = JSONSink._slice_value(value, i, n)
row[key] = str(v) if isinstance(value, np.ndarray) else v
parsed_rows.append(row)
@@ -132,7 +174,11 @@ class JSONSink:
Args:
detections: The detection data.
custom_data: Custom data to include.
custom_data: Custom data to include. Scalars, dictionaries, and
other non-sequence values are broadcast to every detection in
this batch. NumPy arrays, lists, and tuples with length equal
to ``len(detections)`` are sliced per detection; other lists
and tuples are broadcast unchanged.
"""
parsed_rows = JSONSink.parse_detection_data(detections, custom_data)
self.data.extend(parsed_rows)
+8
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import numpy as np
import numpy.typing as npt
from deprecate import deprecated
from supervision.detection.utils.iou_and_nms import box_iou_batch
@@ -95,10 +96,17 @@ def pad_boxes(
return result
@deprecated( # type: ignore[untyped-decorator]
target=True,
deprecated_in="0.27.0",
remove_in="0.30.0",
args_mapping={"normalized_xyxy": "xyxy"},
)
def denormalize_boxes(
xyxy: npt.NDArray[np.number],
resolution_wh: tuple[int, int],
normalization_factor: float = 1.0,
normalized_xyxy: npt.NDArray[np.number] | None = None,
) -> npt.NDArray[np.number]:
"""
Convert normalized bounding box coordinates to absolute pixel coordinates.
+211 -51
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from typing import Any
from typing import Any, cast
import cv2
import numpy as np
@@ -308,21 +308,35 @@ def mask_to_polygons(mask: npt.NDArray[np.bool_]) -> list[npt.NDArray[np.int32]]
]
def _decode_coco_rle_string(s: str) -> list[int]:
"""Decode a COCO compressed RLE counts string to a list of run-length integers.
def _base48_decode(s: str) -> list[int]:
"""Decode a COCO base-48 string to raw (delta-encoded) integers.
Implements the decoding algorithm from the COCO API (pycocotools) for
compressed RLE strings. Each character encodes 5 data bits in a base-48
scheme with continuation and sign flags, using delta encoding for indices
beyond the first two.
Implements the variable-length base-48 codec from the COCO API
(pycocotools). Each integer is encoded across one or more 6-bit
characters: bits 0-4 carry data; bit 5 signals continuation; bit 4
of the final character signals a negative value.
This is the pure codec layer — call :func:`_delta_decode` on the
result to obtain absolute run-length counts.
Args:
s: The compressed RLE counts string.
s: COCO compressed RLE string.
Returns:
A list of run-length integers (alternating background/foreground counts).
Raw delta-encoded integers, one per run.
Raises:
ValueError: If the string is truncated mid-integer.
Examples:
```pycon
>>> from supervision.detection.utils.converters import _base48_decode
>>> _base48_decode("52203")
[5, 2, 2, 0, 3]
```
"""
counts: list[int] = []
values: list[int] = []
i = 0
while i < len(s):
x = 0
@@ -340,28 +354,35 @@ def _decode_coco_rle_string(s: str) -> list[int]:
k += 1
if not more and (c & 0x10):
x |= ~0 << (5 * k)
if len(counts) > 2:
x += counts[-2]
counts.append(x)
return counts
values.append(x)
return values
def _encode_coco_rle_string(counts: list[int]) -> str:
"""Encode a list of run-length integers to a COCO compressed RLE string.
def _base48_encode(values: list[int]) -> str:
"""Encode raw (delta-encoded) integers to a COCO base-48 string.
Implements the encoding algorithm from the COCO API (pycocotools).
The inverse of :func:`_decode_coco_rle_string`.
The inverse of :func:`_base48_decode`. Applies the same variable-length
base-48 codec used by pycocotools.
Apply :func:`_delta_encode` to absolute run-length counts before calling
this function to produce a valid COCO compressed RLE string.
Args:
counts: A list of run-length integers (alternating background/foreground
counts).
values: Raw (delta-encoded) integers to encode.
Returns:
The compressed RLE counts string.
COCO base-48 encoded string.
Examples:
```pycon
>>> from supervision.detection.utils.converters import _base48_encode
>>> _base48_encode([5, 2, 2, 0, 3])
'52203'
```
"""
chars: list[str] = []
for i, cnt in enumerate(counts):
x = cnt - counts[i - 2] if i > 2 else cnt
for x in values:
more = True
while more:
c = x & 0x1F
@@ -373,6 +394,163 @@ def _encode_coco_rle_string(counts: list[int]) -> str:
return "".join(chars)
def _delta_decode(values: list[int]) -> list[int]:
"""Undo COCO delta encoding: ``counts[i] += counts[i - 2]`` for ``i > 2``.
The COCO compressed RLE format stores run lengths as deltas relative to
the count two positions earlier (starting at index 3). This function
converts those relative values back to absolute run lengths.
Args:
values: Raw delta-encoded integers from :func:`_base48_decode`.
Returns:
Absolute run-length counts (alternating background / foreground).
Examples:
```pycon
>>> from supervision.detection.utils.converters import _delta_decode
>>> _delta_decode([5, 2, 2, 0, 3])
[5, 2, 2, 2, 5]
```
"""
counts = list(values)
for i in range(3, len(counts)):
counts[i] += counts[i - 2]
return counts
def _delta_encode(counts: list[int]) -> list[int]:
"""Apply COCO delta encoding: ``d[i] = counts[i] - counts[i - 2]`` for ``i > 2``.
The inverse of :func:`_delta_decode`. Converts absolute run lengths to
the relative representation required by the COCO compressed RLE format.
Args:
counts: Absolute run-length counts (alternating background / foreground).
Returns:
Delta-encoded integers ready for :func:`_base48_encode`.
Examples:
```pycon
>>> from supervision.detection.utils.converters import _delta_encode
>>> _delta_encode([5, 2, 2, 2, 5])
[5, 2, 2, 0, 3]
```
"""
deltas = list(counts)
for i in range(3, len(deltas)):
deltas[i] = counts[i] - counts[i - 2]
return deltas
def is_compressed_rle(rle: object) -> bool:
"""Return ``True`` if ``rle`` is a COCO compressed RLE (``str`` or ``bytes``).
Use this to branch between the compressed-string pipeline
(:func:`_base48_decode` → :func:`_delta_decode`) and the uncompressed
integer-list / array pipeline before calling :func:`rle_to_mask`.
Args:
rle: Candidate RLE value to inspect.
Returns:
``True`` for ``str`` or ``bytes`` inputs; ``False`` otherwise.
Examples:
```pycon
>>> from supervision.detection.utils.converters import is_compressed_rle
>>> is_compressed_rle("52203")
True
>>> is_compressed_rle([5, 2, 2, 2, 5])
False
```
"""
return isinstance(rle, (str, bytes))
def _mask_to_rle_counts(mask_2d: npt.NDArray[Any]) -> npt.NDArray[np.int32]:
"""Encode a 2D boolean mask as COCO F-order run lengths (int32 array).
Pixels are scanned column-by-column (Fortran order), matching the COCO /
pycocotools RLE convention. The first value is always the count of leading
``False`` pixels (may be 0 if the mask starts with ``True``).
This is the shared low-level encoder used by both :func:`mask_to_rle` and
:class:`~supervision.detection.compact_mask.CompactMask`.
Args:
mask_2d: 2D boolean array of shape ``(H, W)``.
Returns:
int32 array of run lengths starting with the False count.
Examples:
```pycon
>>> import numpy as np
>>> from supervision.detection.utils.converters import _mask_to_rle_counts
>>> mask = np.array([[False, True], [True, False]])
>>> _mask_to_rle_counts(mask).tolist()
[1, 2, 1]
```
"""
flat = np.asarray(mask_2d, dtype=np.bool_).ravel(order="F")
if len(flat) == 0:
return np.array([0], dtype=np.int32)
changes = np.diff(flat.view(np.uint8))
boundaries = np.where(changes != 0)[0] + 1
positions = np.concatenate(([0], boundaries, [len(flat)]))
run_lengths = np.diff(positions).astype(np.int32)
if flat[0]:
run_lengths = np.concatenate(([np.int32(0)], run_lengths))
return run_lengths
def _rle_counts_to_mask(
rle: npt.NDArray[np.int32], height: int, width: int
) -> npt.NDArray[np.bool_]:
"""Decode COCO F-order run lengths back to a 2D boolean mask.
This is the shared low-level decoder used by both :func:`rle_to_mask` and
:class:`~supervision.detection.compact_mask.CompactMask`.
Args:
rle: int32 array of run lengths as produced by :func:`_mask_to_rle_counts`.
height: Height of the output mask.
width: Width of the output mask.
Returns:
2D boolean array of shape ``(height, width)``.
Examples:
```pycon
>>> import numpy as np
>>> from supervision.detection.utils.converters import _rle_counts_to_mask
>>> rle = np.array([0, 1, 2, 1], dtype=np.int32)
>>> _rle_counts_to_mask(rle, 2, 2)
array([[ True, False],
[False, True]])
```
"""
is_true = np.arange(len(rle)) % 2 == 1
flat: npt.NDArray[np.bool_] = np.repeat(is_true, rle)
num_pixels = height * width
if len(flat) < num_pixels:
flat = np.pad(flat, (0, num_pixels - len(flat)))
return cast(
npt.NDArray[np.bool_], flat[:num_pixels].reshape(height, width, order="F")
)
def rle_to_mask(
rle: npt.NDArray[np.integer[Any]] | list[int] | str | bytes,
resolution_wh: tuple[int, int],
@@ -428,26 +606,23 @@ def rle_to_mask(
if isinstance(rle, bytes):
rle = rle.decode("utf-8")
if isinstance(rle, str):
rle = np.array(_decode_coco_rle_string(rle), dtype=int)
counts: npt.NDArray[np.int32] = np.array(
_delta_decode(_base48_decode(rle)), dtype=np.int32
)
elif isinstance(rle, list):
rle = np.array(rle, dtype=int)
counts = np.array(rle, dtype=np.int32)
else:
counts = np.asarray(rle, dtype=np.int32)
width, height = resolution_wh
if width * height != np.sum(rle):
if width * height != np.sum(counts):
raise ValueError(
"the sum of the number of pixels in the RLE must be the same "
"as the number of pixels in the expected mask"
)
zero_one_values = np.zeros(shape=(rle.size, 1), dtype=np.uint8)
zero_one_values[1::2] = 1
decoded_rle = np.repeat(zero_one_values, rle, axis=0)
decoded_rle = np.append(
decoded_rle, np.zeros(width * height - len(decoded_rle), dtype=np.uint8)
)
return decoded_rle.reshape((height, width), order="F").astype(bool)
return _rle_counts_to_mask(counts, height, width)
def mask_to_rle(
@@ -519,24 +694,9 @@ def mask_to_rle(
assert mask.ndim == 2, "Input mask must be 2D"
assert mask.size != 0, "Input mask cannot be empty"
on_value_change_indices = np.where(
mask.ravel(order="F") != np.roll(mask.ravel(order="F"), 1)
)[0]
on_value_change_indices = np.append(on_value_change_indices, mask.size)
# need to add 0 at the beginning when the same value is in the first and
# last element of the flattened mask
if on_value_change_indices[0] != 0:
on_value_change_indices = np.insert(on_value_change_indices, 0, 0)
rle = np.diff(on_value_change_indices)
if mask[0][0] == 1:
rle = np.insert(rle, 0, 0)
counts = [int(count) for count in rle]
counts: list[int] = cast(list[int], _mask_to_rle_counts(mask).tolist())
if compressed:
return _encode_coco_rle_string(counts)
return _base48_encode(_delta_encode(counts))
return counts
+134
View File
@@ -724,3 +724,137 @@ class TestComparisonAnnotator:
scene=image.copy(), detections_1=detections1, detections_2=detections2
)
assert not np.array_equal(image, result)
class TestTraceAnnotatorSmoothStationary:
"""Regression tests for TraceAnnotator(smooth=True) on stationary tracker ids."""
def test_stationary_tracker_does_not_crash_spline_fit(self, test_image):
"""
When the same tracker stays at an identical anchor point for several
frames the trace buffer accumulates duplicate points. `scipy.splprep`
rejects a zero-length input curve with `ValueError: Invalid inputs.`,
so the annotator must survive this input without raising.
"""
detections = _create_detections(
xyxy=[[100, 100, 120, 120]],
class_id=[1],
tracker_id=[42],
)
annotator = TraceAnnotator(smooth=True, trace_length=10)
scene = test_image.copy()
for _ in range(6):
scene = annotator.annotate(scene=scene, detections=detections)
assert scene.shape == test_image.shape
def test_smooth_trace_still_renders_for_moving_tracker(self, test_image):
"""Moving tracker must produce a spline trace distinct from the raw polyline.
Compares smooth=True output against smooth=False for the same movement
path to confirm the smoothing path is actually exercised (not just that
some pixels changed).
"""
smooth_annotator = TraceAnnotator(smooth=True, trace_length=10, thickness=2)
raw_annotator = TraceAnnotator(smooth=False, trace_length=10, thickness=2)
scene_smooth = test_image.copy()
scene_raw = test_image.copy()
for offset in range(6):
detections = _create_detections(
xyxy=[
[10 + offset * 5, 10 + offset * 5, 30 + offset * 5, 30 + offset * 5]
],
class_id=[1],
tracker_id=[7],
)
scene_smooth = smooth_annotator.annotate(
scene=scene_smooth, detections=detections
)
scene_raw = raw_annotator.annotate(scene=scene_raw, detections=detections)
# After 4+ unique anchor positions the spline path fires and diverges from the
# raw polyline — the two output images must differ.
assert not np.array_equal(scene_smooth, scene_raw)
@pytest.mark.parametrize(
"unique_positions",
[1, 2, 3, 4],
ids=["1_unique", "2_unique", "3_unique", "4_unique"],
)
def test_smooth_does_not_crash_for_unique_point_counts(
self, test_image, unique_positions
):
"""smooth=True must not crash for any unique-position count from 1 to 4.
Each position is repeated twice to simulate brief holds between moves.
Covers the boundary at len(unique_xy) == 4 where splprep first fires.
"""
annotator = TraceAnnotator(smooth=True, trace_length=10, thickness=2)
scene = test_image.copy()
for pos_idx in range(unique_positions):
for _ in range(2):
x = 10 + pos_idx * 15
detections = _create_detections(
xyxy=[[x, x, x + 15, x + 15]],
class_id=[1],
tracker_id=[99],
)
scene = annotator.annotate(scene=scene, detections=detections)
assert scene.shape == test_image.shape
def test_smooth_fallback_matches_raw_when_fewer_than_four_unique_points(
self, test_image
):
"""With <4 unique positions smooth=True output must match smooth=False.
Verifies the dedup-then-fallback path: when unique_xy has ≤3 points,
both branches use the same raw-polyline draw.
"""
annotator_smooth = TraceAnnotator(smooth=True, trace_length=10, thickness=2)
annotator_raw = TraceAnnotator(smooth=False, trace_length=10, thickness=2)
scene_smooth = test_image.copy()
scene_raw = test_image.copy()
for pos_idx in range(3):
for _ in range(2):
x = 10 + pos_idx * 15
detections = _create_detections(
xyxy=[[x, x, x + 15, x + 15]],
class_id=[1],
tracker_id=[99],
)
scene_smooth = annotator_smooth.annotate(
scene=scene_smooth, detections=detections
)
scene_raw = annotator_raw.annotate(
scene=scene_raw, detections=detections
)
assert np.array_equal(scene_smooth, scene_raw)
def test_smooth_true_single_frame_does_not_crash(self, test_image):
"""A single annotate() call with smooth=True must not crash.
When len(xy) == 1 the drawing guard skips cv2.polylines entirely;
the dedup path runs safely on an empty np.diff result.
"""
detections = _create_detections(
xyxy=[[50, 50, 70, 70]],
class_id=[1],
tracker_id=[1],
)
annotator = TraceAnnotator(smooth=True, trace_length=10)
scene = annotator.annotate(scene=test_image.copy(), detections=detections)
assert scene.shape == test_image.shape
def test_smooth_false_stationary_tracker_does_not_crash(self, test_image):
"""smooth=False with a stationary tracker must not crash (regression guard).
Ensures the refactor did not accidentally alter the smooth=False code path.
"""
detections = _create_detections(
xyxy=[[100, 100, 120, 120]],
class_id=[1],
tracker_id=[42],
)
annotator = TraceAnnotator(smooth=False, trace_length=10)
scene = test_image.copy()
for _ in range(6):
scene = annotator.annotate(scene=scene, detections=detections)
assert scene.shape == test_image.shape
+137
View File
@@ -1157,6 +1157,143 @@ def test_load_coco_annotations_force_masks_with_no_annotations(
assert annotations[no_annotations_path] == Detections.empty()
@pytest.mark.parametrize(
"file_name",
[".", "", "subdir/.."],
)
def test_load_coco_annotations_rejects_file_name_resolving_to_images_directory(
tmp_path,
file_name: str,
) -> None:
"""Reject file_name resolving to the images directory itself (equality guard)."""
images_directory = tmp_path / "images"
images_directory.mkdir()
annotations_path = tmp_path / "annotations.json"
coco_data = {
"categories": [{"id": 1, "name": "object", "supercategory": "none"}],
"images": [{"id": 1, "file_name": file_name, "width": 5, "height": 5}],
"annotations": [],
}
annotations_path.write_text(json.dumps(coco_data), encoding="utf-8")
with pytest.raises(ValueError, match="resolves to the images directory itself"):
load_coco_annotations(
images_directory_path=str(images_directory),
annotations_path=str(annotations_path),
)
@pytest.mark.parametrize(
"malicious_file_name",
[
"../escape.txt",
"../../escape.txt",
"subdir/../../escape.txt",
],
)
def test_load_coco_annotations_rejects_file_name_outside_images_directory(
tmp_path,
malicious_file_name: str,
) -> None:
"""Reject relative traversal file_name values that escape the images directory."""
images_directory = tmp_path / "images"
images_directory.mkdir()
annotations_path = tmp_path / "annotations.json"
coco_data = {
"categories": [{"id": 1, "name": "object", "supercategory": "none"}],
"images": [
{
"id": 1,
"file_name": malicious_file_name,
"width": 5,
"height": 5,
}
],
"annotations": [],
}
annotations_path.write_text(json.dumps(coco_data), encoding="utf-8")
with pytest.raises(ValueError, match="outside the images directory"):
load_coco_annotations(
images_directory_path=str(images_directory),
annotations_path=str(annotations_path),
)
def test_load_coco_annotations_rejects_absolute_file_name(tmp_path) -> None:
"""Reject absolute file_name values that escape the images directory."""
images_directory = tmp_path / "images"
images_directory.mkdir()
annotations_path = tmp_path / "annotations.json"
coco_data = {
"categories": [{"id": 1, "name": "object", "supercategory": "none"}],
"images": [
{
"id": 1,
"file_name": "/etc/passwd",
"width": 5,
"height": 5,
}
],
"annotations": [],
}
annotations_path.write_text(json.dumps(coco_data), encoding="utf-8")
with pytest.raises(ValueError, match="outside the images directory"):
load_coco_annotations(
images_directory_path=str(images_directory),
annotations_path=str(annotations_path),
)
def test_load_coco_annotations_rejects_file_name_resolving_to_directory(
tmp_path,
) -> None:
"""Reject file_name resolving to a subdirectory inside images/ (is_dir guard)."""
images_directory = tmp_path / "images"
images_directory.mkdir()
(images_directory / "subdir").mkdir()
annotations_path = tmp_path / "annotations.json"
coco_data = {
"categories": [{"id": 1, "name": "object", "supercategory": "none"}],
"images": [{"id": 1, "file_name": "subdir", "width": 5, "height": 5}],
"annotations": [],
}
annotations_path.write_text(json.dumps(coco_data), encoding="utf-8")
with pytest.raises(ValueError, match="resolves to directory"):
load_coco_annotations(
images_directory_path=str(images_directory),
annotations_path=str(annotations_path),
)
def test_load_coco_annotations_accepts_valid_nested_file_name(tmp_path) -> None:
"""Accept a legitimate nested file_name inside images/ without raising."""
images_directory = tmp_path / "images"
images_directory.mkdir()
(images_directory / "train").mkdir()
annotations_path = tmp_path / "annotations.json"
coco_data = {
"categories": [{"id": 1, "name": "object", "supercategory": "none"}],
"images": [{"id": 1, "file_name": "train/image.jpg", "width": 5, "height": 5}],
"annotations": [],
}
annotations_path.write_text(json.dumps(coco_data), encoding="utf-8")
_, _, annotations = load_coco_annotations(
images_directory_path=str(images_directory),
annotations_path=str(annotations_path),
)
expected_path = str(images_directory / "train" / "image.jpg")
assert expected_path in annotations
def test_load_coco_annotations_force_masks_handles_missing_segmentation(
tmp_path,
) -> None:
+663 -8
View File
@@ -10,10 +10,13 @@ import pytest
from supervision.detection.compact_mask import (
CompactMask,
_rle_area,
_rle_decode,
_rle_encode,
)
from supervision.detection.utils.converters import mask_to_xyxy
from supervision.detection.utils.converters import (
_mask_to_rle_counts,
_rle_counts_to_mask,
mask_to_rle,
mask_to_xyxy,
)
from supervision.detection.utils.masks import (
calculate_masks_centroids,
contains_holes,
@@ -31,7 +34,7 @@ def _make_cm(masks: np.ndarray, image_shape: tuple[int, int]) -> CompactMask:
class TestRleHelpers:
"""Tests for _rle_encode, _rle_decode, and _rle_area.
"""Tests for _mask_to_rle_counts, _rle_counts_to_mask, and _rle_area.
Verifies that the private RLE encoding round-trips correctly for a range
of mask shapes (all-False, all-True, diagonal, L-shape, checkerboard,
@@ -61,14 +64,15 @@ class TestRleHelpers:
def test_encode_decode_round_trip(
self, mask_2d: np.ndarray, description: str
) -> None:
"""_mask_to_rle_counts -> _rle_counts_to_mask round-trip is lossless."""
if mask_2d.size == 0:
rle = _rle_encode(mask_2d)
rle = _mask_to_rle_counts(mask_2d)
assert _rle_area(rle) == 0
return
rle = _rle_encode(mask_2d)
rle = _mask_to_rle_counts(mask_2d)
assert rle.dtype == np.int32, "RLE must be int32"
reconstructed = _rle_decode(rle, mask_2d.shape[0], mask_2d.shape[1])
reconstructed = _rle_counts_to_mask(rle, mask_2d.shape[0], mask_2d.shape[1])
np.testing.assert_array_equal(
reconstructed, mask_2d, err_msg=f"Round-trip failed for: {description}"
)
@@ -83,9 +87,48 @@ class TestRleHelpers:
],
)
def test_area_matches_numpy_sum(self, mask_2d: np.ndarray) -> None:
rle = _rle_encode(mask_2d)
"""_rle_area must equal np.sum on the original boolean array."""
rle = _mask_to_rle_counts(mask_2d)
assert _rle_area(rle) == int(np.sum(mask_2d))
@pytest.mark.parametrize(
("mask_2d", "expected_rle"),
[
# 2x3; F-order flat: [F,T,T,F,T,F] -> 1F,2T,1F,1T,1F
(
np.array([[False, True, True], [True, False, False]]),
[1, 2, 1, 1, 1],
),
# 3x3 all-False -> single run of 9
(np.zeros((3, 3), dtype=bool), [9]),
# 3x1 all-True; F-order scan starts True -> leading zero prepended
(np.ones((3, 1), dtype=bool), [0, 3]),
# 2x2; F-order flat: [F,T,F,T] -> alternating single-pixel runs
(
np.array([[False, False], [True, True]]),
[1, 1, 1, 1],
),
],
)
def test_encode_matches_coco_f_order(
self, mask_2d: np.ndarray, expected_rle: list[int]
) -> None:
"""_mask_to_rle_counts produces COCO-compatible F-order RLE for known masks."""
assert _mask_to_rle_counts(mask_2d).tolist() == expected_rle
@pytest.mark.parametrize(
"mask_2d",
[
np.array([[False, True, True], [True, False, False]]),
np.zeros((4, 4), dtype=bool),
np.array([[False, False], [True, True]]),
np.ones((3, 1), dtype=bool),
],
)
def test_encode_agrees_with_mask_to_rle(self, mask_2d: np.ndarray) -> None:
"""_mask_to_rle_counts output matches the public mask_to_rle encoder."""
assert _mask_to_rle_counts(mask_2d).tolist() == mask_to_rle(mask_2d)
class TestFromDenseToDense:
"""Tests for CompactMask.from_dense and to_dense.
@@ -934,3 +977,615 @@ class TestCompactMaskWithOffsetRandom:
expected,
err_msg=f"Larger canvas offset mismatch for seed={seed}",
)
class TestRleSplitCols:
"""Tests for _rle_split_cols: splitting F-order RLE into per-column lists."""
def test_all_true_2x2(self) -> None:
"""All-True 2x2 splits into two columns each [0, 2]."""
from supervision.detection.compact_mask import _rle_split_cols
mask = np.ones((2, 2), dtype=bool)
rle = _mask_to_rle_counts(mask)
result = _rle_split_cols(rle, 2, 2)
assert result == [[0, 2], [0, 2]]
def test_all_false_3x3(self) -> None:
"""All-False 3x3 splits into three columns each [3]."""
from supervision.detection.compact_mask import _rle_split_cols
mask = np.zeros((3, 3), dtype=bool)
rle = _mask_to_rle_counts(mask)
result = _rle_split_cols(rle, 3, 3)
assert result == [[3], [3], [3]]
def test_mixed_2x2(self) -> None:
"""Mixed mask splits correctly per column."""
from supervision.detection.compact_mask import _rle_split_cols
mask = np.array([[True, False], [True, True]], dtype=bool)
rle = _mask_to_rle_counts(mask)
result = _rle_split_cols(rle, 2, 2)
assert result == [[0, 2], [1, 1]]
@pytest.mark.parametrize("seed", list(range(20)))
def test_round_trip_random(self, seed: int) -> None:
"""Split then rejoin must reconstruct original mask for random inputs."""
from supervision.detection.compact_mask import (
_rle_join_cols,
_rle_split_cols,
)
rng = np.random.default_rng(seed + 8000)
crop_h = int(rng.integers(1, 30))
crop_w = int(rng.integers(1, 30))
mask = rng.random((crop_h, crop_w)) < 0.4
rle = _mask_to_rle_counts(mask)
per_col = _rle_split_cols(rle, crop_h, crop_w)
assert len(per_col) == crop_w
for c in range(crop_w):
assert sum(per_col[c]) == crop_h, f"col {c} sum mismatch"
# Rejoin and verify pixel equality.
rejoined = _rle_join_cols(per_col, crop_h * crop_w)
decoded = _rle_counts_to_mask(rejoined, crop_h, crop_w)
np.testing.assert_array_equal(
decoded,
mask,
err_msg=f"Split→join round-trip failed for seed={seed}",
)
def test_join_true_true_junction_no_zero_run(self) -> None:
"""_rle_join_cols merges True/True boundary; no zero-length False run inserted.
When column A ends True and column B starts True (leading False count = 0),
the junction must produce a single merged True run, not a zero-length False
run between two True runs. A zero-length run would inflate len(rle) and
misroute density-based dispatch in _resize_crop.
"""
from supervision.detection.compact_mask import _rle_join_cols
# col A: [0, 3] → T=3 (height=3, all True)
# col B: [0, 3] → T=3 (height=3, all True)
# Merged: should be [0, 6], NOT [0, 3, 0, 3].
cols = [[0, 3], [0, 3]]
result = _rle_join_cols(cols, 6).tolist()
assert result == [0, 6], (
f"Expected [0, 6] (merged True runs), got {result}; "
"zero-length False run would inflate density metric"
)
assert 0 not in result[1:], "Zero-length run found after junction merge"
class TestCompactMaskResize:
"""Tests for CompactMask.resize method.
Verifies scaling behaviour, coordinate arithmetic, identity resize,
empty collections, invalid dimensions, and dense parity with cv2.
"""
@pytest.mark.parametrize(
("src_shape", "mask_slice", "target_shape", "description"),
[
(
(10, 10),
(slice(2, 5), slice(2, 5)),
(100, 100),
"10x upscale 10x10 to 100x100",
),
(
(480, 640),
(slice(100, 200), slice(150, 300)),
(240, 320),
"HD halve 480x640 to 240x320",
),
(
(100, 200),
(slice(20, 40), slice(50, 100)),
(50, 400),
"asymmetric: shrink H, grow W",
),
],
)
def test_scale_shape_and_offsets(
self,
src_shape: tuple[int, int],
mask_slice: tuple[slice, slice],
target_shape: tuple[int, int],
description: str,
) -> None:
"""Resize scales shape and offsets proportionally."""
img_h, img_w = src_shape
masks = np.zeros((1, img_h, img_w), dtype=bool)
masks[0, mask_slice[0], mask_slice[1]] = True
xyxy = mask_to_xyxy(masks)
cm = CompactMask.from_dense(masks, xyxy, image_shape=src_shape)
resized = cm.resize(target_shape)
assert resized.shape == (1, target_shape[0], target_shape[1]), description
sx = target_shape[1] / src_shape[1]
sy = target_shape[0] / src_shape[0]
orig_offset_x = int(cm.offsets[0, 0])
orig_offset_y = int(cm.offsets[0, 1])
expected_x = round(orig_offset_x * sx)
expected_y = round(orig_offset_y * sy)
assert abs(int(resized.offsets[0, 0]) - expected_x) <= 1, description
assert abs(int(resized.offsets[0, 1]) - expected_y) <= 1, description
def test_identity_preserves_rle(self) -> None:
"""Resize to same shape returns identical RLE, offsets, and crop shapes."""
masks = np.zeros((1, 80, 80), dtype=bool)
masks[0, 10:30, 15:45] = True
xyxy = mask_to_xyxy(masks)
cm = CompactMask.from_dense(masks, xyxy, image_shape=(80, 80))
resized = cm.resize((80, 80))
assert resized.shape == cm.shape
np.testing.assert_array_equal(resized.offsets, cm.offsets)
np.testing.assert_array_equal(resized._crop_shapes, cm._crop_shapes)
for orig_rle, new_rle in zip(cm._rles, resized._rles):
np.testing.assert_array_equal(orig_rle, new_rle)
def test_empty_n0(self) -> None:
"""Resize of an empty CompactMask returns empty with new image_shape."""
masks = np.zeros((0, 50, 50), dtype=bool)
xyxy = np.empty((0, 4), dtype=np.float32)
cm = CompactMask.from_dense(masks, xyxy, image_shape=(50, 50))
resized = cm.resize((100, 200))
assert len(resized) == 0
assert resized.shape == (0, 100, 200)
@pytest.mark.parametrize(
"bad_shape",
[
(0, 50),
(-1, 50),
(50, 0),
(50, -1),
],
)
def test_invalid_dimensions_raises(self, bad_shape: tuple[int, int]) -> None:
"""Resize with non-positive dimensions raises ValueError."""
masks = np.zeros((1, 50, 50), dtype=bool)
masks[0, 10:20, 10:20] = True
xyxy = mask_to_xyxy(masks)
cm = CompactMask.from_dense(masks, xyxy, image_shape=(50, 50))
with pytest.raises(ValueError, match="positive"):
cm.resize(bad_shape)
def test_multi_mask_each_scales_independently(self) -> None:
"""N=4 masks at different positions all scale correctly after resize."""
img_h, img_w = 100, 100
target_h, target_w = 50, 50
masks = np.zeros((4, img_h, img_w), dtype=bool)
masks[0, 10:20, 10:20] = True
masks[1, 30:50, 30:50] = True
masks[2, 60:80, 60:80] = True
masks[3, 5:10, 80:90] = True
xyxy = mask_to_xyxy(masks)
cm = CompactMask.from_dense(masks, xyxy, image_shape=(img_h, img_w))
resized = cm.resize((target_h, target_w))
assert resized.shape == (4, target_h, target_w)
sx = target_w / img_w
sy = target_h / img_h
for i in range(4):
expected_x = round(int(cm.offsets[i, 0]) * sx)
expected_y = round(int(cm.offsets[i, 1]) * sy)
assert abs(int(resized.offsets[i, 0]) - expected_x) <= 1, f"mask {i} x"
assert abs(int(resized.offsets[i, 1]) - expected_y) <= 1, f"mask {i} y"
def test_zero_extent_extreme_downscale(self) -> None:
"""Extreme downscale that collapses a 1px bbox returns valid 1x1 crop."""
masks = np.zeros((1, 1000, 1000), dtype=bool)
masks[0, 500, 500] = True
xyxy = mask_to_xyxy(masks)
cm = CompactMask.from_dense(masks, xyxy, image_shape=(1000, 1000))
resized = cm.resize((2, 2))
assert resized.shape == (1, 2, 2)
assert int(resized._crop_shapes[0, 0]) >= 1
assert int(resized._crop_shapes[0, 1]) >= 1
dense = resized.to_dense()
assert dense.shape == (1, 2, 2)
@pytest.mark.parametrize("seed", list(range(10)))
def test_dense_parity_roundtrip(self, seed: int) -> None:
"""Resized CompactMask matches OpenCV-resized dense masks within 1px."""
import cv2
rng = np.random.default_rng(seed + 500)
img_h, img_w = 80, 120
target_h, target_w = 40, 60
num_masks = int(rng.integers(1, 5))
masks, xyxy = _random_masks_and_xyxy(rng, num_masks, img_h, img_w)
cm = CompactMask.from_dense(masks, xyxy, image_shape=(img_h, img_w))
resized = cm.resize((target_h, target_w))
resized_dense = resized.to_dense()
for i in range(num_masks):
expected = cv2.resize(
masks[i].astype(np.uint8),
(target_w, target_h),
interpolation=cv2.INTER_NEAREST,
).astype(bool)
actual = resized_dense[i]
diff = np.abs(actual.astype(int) - expected.astype(int)).max()
assert int(diff) <= 1, (
f"Dense parity mismatch for seed={seed}, mask={i}: "
f"max pixel diff={diff}"
)
class TestRleResize:
"""Tests for _rle_resize direct F-order RLE resizing.
Verifies that _rle_resize produces identical results to the decode ->
cv2.resize(INTER_NEAREST) -> encode path for identity, upscale, downscale,
non-square, all-False, all-True, single-pixel, and random masks.
"""
def test_identity_4x4(self) -> None:
"""Identity resize (same dimensions) preserves the decoded mask."""
from supervision.detection.compact_mask import _rle_resize
mask = np.array(
[
[False, True, True, False],
[True, True, False, False],
[False, False, True, True],
[True, False, False, True],
],
dtype=bool,
)
rle = _mask_to_rle_counts(mask)
result_rle = _rle_resize(rle, 4, 4, 4, 4)
result = _rle_counts_to_mask(result_rle, 4, 4)
np.testing.assert_array_equal(result, mask)
def test_2x_upscale(self) -> None:
"""2x upscale of a 2x2 mask doubles each pixel."""
import cv2
from supervision.detection.compact_mask import _rle_resize
mask = np.array(
[
[True, False],
[False, True],
],
dtype=bool,
)
rle = _mask_to_rle_counts(mask)
result_rle = _rle_resize(rle, 2, 2, 4, 4)
result = _rle_counts_to_mask(result_rle, 4, 4)
expected = cv2.resize(
mask.astype(np.uint8), (4, 4), interpolation=cv2.INTER_NEAREST
).astype(bool)
np.testing.assert_array_equal(result, expected)
def test_2x_downscale(self) -> None:
"""2x downscale of a 4x4 block mask halves dimensions."""
import cv2
from supervision.detection.compact_mask import _rle_resize
mask = np.array(
[
[True, True, False, False],
[True, True, False, False],
[False, False, True, True],
[False, False, True, True],
],
dtype=bool,
)
rle = _mask_to_rle_counts(mask)
result_rle = _rle_resize(rle, 4, 4, 2, 2)
result = _rle_counts_to_mask(result_rle, 2, 2)
expected = cv2.resize(
mask.astype(np.uint8), (2, 2), interpolation=cv2.INTER_NEAREST
).astype(bool)
np.testing.assert_array_equal(result, expected)
def test_non_square_scale(self) -> None:
"""Non-square resize: 4x6 to 2x3 with independent axis scaling."""
import cv2
from supervision.detection.compact_mask import _rle_resize
mask = np.zeros((4, 6), dtype=bool)
mask[0:2, 0:3] = True
mask[2:4, 3:6] = True
rle = _mask_to_rle_counts(mask)
result_rle = _rle_resize(rle, 4, 6, 2, 3)
result = _rle_counts_to_mask(result_rle, 2, 3)
expected = cv2.resize(
mask.astype(np.uint8), (3, 2), interpolation=cv2.INTER_NEAREST
).astype(bool)
np.testing.assert_array_equal(result, expected)
@pytest.mark.parametrize(
("src_shape", "dst_shape"),
[
((3, 3), (6, 6)),
((5, 5), (2, 2)),
((4, 6), (8, 12)),
((10, 10), (3, 3)),
],
)
def test_all_false(
self, src_shape: tuple[int, int], dst_shape: tuple[int, int]
) -> None:
"""All-False mask resizes to all-False regardless of dimensions."""
from supervision.detection.compact_mask import _rle_resize
mask = np.zeros(src_shape, dtype=bool)
rle = _mask_to_rle_counts(mask)
result_rle = _rle_resize(rle, *src_shape, *dst_shape)
result = _rle_counts_to_mask(result_rle, *dst_shape)
assert not result.any()
@pytest.mark.parametrize(
("src_shape", "dst_shape"),
[
((3, 3), (6, 6)),
((5, 5), (2, 2)),
((4, 6), (8, 12)),
((10, 10), (3, 3)),
],
)
def test_all_true(
self, src_shape: tuple[int, int], dst_shape: tuple[int, int]
) -> None:
"""All-True mask resizes to all-True regardless of dimensions."""
from supervision.detection.compact_mask import _rle_resize
mask = np.ones(src_shape, dtype=bool)
rle = _mask_to_rle_counts(mask)
result_rle = _rle_resize(rle, *src_shape, *dst_shape)
result = _rle_counts_to_mask(result_rle, *dst_shape)
assert result.all()
def test_single_pixel_true_upscale(self) -> None:
"""Single True pixel in a 3x3 mask upscaled preserves position."""
import cv2
from supervision.detection.compact_mask import _rle_resize
mask = np.zeros((3, 3), dtype=bool)
mask[1, 1] = True
rle = _mask_to_rle_counts(mask)
result_rle = _rle_resize(rle, 3, 3, 6, 6)
result = _rle_counts_to_mask(result_rle, 6, 6)
expected = cv2.resize(
mask.astype(np.uint8), (6, 6), interpolation=cv2.INTER_NEAREST
).astype(bool)
np.testing.assert_array_equal(result, expected)
@pytest.mark.parametrize("seed", list(range(45)))
def test_roundtrip_parity_with_cv2(self, seed: int) -> None:
"""_rle_resize matches cv2.resize(INTER_NEAREST) within 1-pixel tolerance."""
import cv2
from supervision.detection.compact_mask import _rle_resize
rng = np.random.default_rng(seed + 7000)
crop_h = int(rng.integers(1, 50))
crop_w = int(rng.integers(1, 50))
new_crop_h = int(rng.integers(1, 100))
new_crop_w = int(rng.integers(1, 100))
mask = rng.random((crop_h, crop_w)) < 0.3
rle = _mask_to_rle_counts(mask)
result_rle = _rle_resize(rle, crop_h, crop_w, new_crop_h, new_crop_w)
result = _rle_counts_to_mask(result_rle, new_crop_h, new_crop_w)
expected = cv2.resize(
mask.astype(np.uint8),
(new_crop_w, new_crop_h),
interpolation=cv2.INTER_NEAREST,
).astype(bool)
diff = np.abs(result.astype(int) - expected.astype(int)).max()
assert diff <= 1, (
f"Parity mismatch >1px for seed={seed}, "
f"src=({crop_h},{crop_w}), dst=({new_crop_h},{new_crop_w}): "
f"max diff={diff}"
)
@pytest.mark.parametrize(
("src_shape", "dst_shape"),
[
((1, 10), (1, 5)),
((10, 1), (5, 1)),
((1, 20), (1, 40)),
((20, 1), (40, 1)),
],
)
def test_tall_and_wide_crops(
self, src_shape: tuple[int, int], dst_shape: tuple[int, int]
) -> None:
"""Single-row and single-col crops scale correctly with cv2 parity."""
import cv2
from supervision.detection.compact_mask import _rle_resize
rng = np.random.default_rng(src_shape[0] * 31 + dst_shape[1] * 17)
mask = rng.random(src_shape) < 0.5
rle = _mask_to_rle_counts(mask)
result_rle = _rle_resize(rle, *src_shape, *dst_shape)
result = _rle_counts_to_mask(result_rle, *dst_shape)
expected = cv2.resize(
mask.astype(np.uint8),
(dst_shape[1], dst_shape[0]),
interpolation=cv2.INTER_NEAREST,
).astype(bool)
np.testing.assert_array_equal(result, expected)
@pytest.mark.parametrize(
("src_shape", "dst_shape"),
[
((7, 11), (5, 13)),
((13, 7), (17, 3)),
((3, 5), (11, 7)),
((11, 13), (7, 17)),
],
)
def test_prime_sized_crops(
self, src_shape: tuple[int, int], dst_shape: tuple[int, int]
) -> None:
"""Prime-sized crops with non-integer scale ratios match cv2 exactly."""
import cv2
from supervision.detection.compact_mask import _rle_resize
rng = np.random.default_rng(src_shape[0] * 101 + dst_shape[1] * 53)
mask = rng.random(src_shape) < 0.4
rle = _mask_to_rle_counts(mask)
result_rle = _rle_resize(rle, *src_shape, *dst_shape)
result = _rle_counts_to_mask(result_rle, *dst_shape)
expected = cv2.resize(
mask.astype(np.uint8),
(dst_shape[1], dst_shape[0]),
interpolation=cv2.INTER_NEAREST,
).astype(bool)
np.testing.assert_array_equal(result, expected)
@pytest.mark.parametrize(
("src_val", "src_shape", "dst_shape"),
[
(True, (1, 1), (32, 32)),
(False, (1, 1), (32, 32)),
],
)
def test_large_scale_ratio(
self,
src_val: bool,
src_shape: tuple[int, int],
dst_shape: tuple[int, int],
) -> None:
"""1x1 source resized to large shape fills entirely True or False."""
from supervision.detection.compact_mask import _rle_resize
mask = np.full(src_shape, src_val, dtype=bool)
rle = _mask_to_rle_counts(mask)
result_rle = _rle_resize(rle, *src_shape, *dst_shape)
result = _rle_counts_to_mask(result_rle, *dst_shape)
if src_val:
assert result.all(), "1x1 True -> large shape must be all True"
else:
assert not result.any(), "1x1 False -> large shape must be all False"
def test_resize_dispatch_uses_l3_for_sparse(self) -> None:
"""resize() dispatches to _rle_resize for sparse masks."""
img_h, img_w = 100, 100
masks = np.zeros((1, img_h, img_w), dtype=bool)
masks[0, 50, 50] = True
xyxy = mask_to_xyxy(masks).astype(np.float32)
cm = CompactMask.from_dense(masks, xyxy, image_shape=(img_h, img_w))
resized = cm.resize((200, 200))
assert resized.shape == (1, 200, 200)
dense = resized.to_dense()
assert dense.sum() > 0
def test_resize_dispatch_uses_cv2_for_dense(self) -> None:
"""_resize_crop falls back to cv2 for dense masks (above _L3_DENSITY_THRESHOLD).
Checkerboard yields ~1 run per pixel, far above the 0.25 threshold.
Result must match cv2.resize(INTER_NEAREST) within 1 pixel.
"""
import cv2
from supervision.detection.compact_mask import (
_L3_DENSITY_THRESHOLD,
_resize_crop,
)
from supervision.detection.utils.converters import _mask_to_rle_counts
h, w = 20, 20
# Checkerboard: alternates True/False → very dense RLE.
rows, cols = np.meshgrid(np.arange(h), np.arange(w), indexing="ij")
mask = ((rows + cols) % 2).astype(bool)
rle = _mask_to_rle_counts(mask)
density = len(rle) / max(1, h * w)
assert density >= _L3_DENSITY_THRESHOLD, (
f"Test precondition failed: density {density:.3f} < threshold "
f"{_L3_DENSITY_THRESHOLD}; checkerboard should be dense"
)
result_rle = _resize_crop(rle, h, w, h // 2, w // 2)
result = _rle_counts_to_mask(result_rle, h // 2, w // 2)
expected = cv2.resize(
mask.astype(np.uint8), (w // 2, h // 2), interpolation=cv2.INTER_NEAREST
).astype(bool)
diff = np.abs(result.astype(int) - expected.astype(int)).max()
assert int(diff) <= 1, f"Dense-path cv2 parity failed; max pixel diff={diff}"
class TestResizeParallelPath:
"""Tests for CompactMask.resize() thread-pool code path (N >= 8 masks)."""
def test_parallel_resize_correctness(self) -> None:
"""resize() with N=10 masks exercises ThreadPoolExecutor; output is correct."""
img_h, img_w = 80, 80
n = 10 # above _PARALLEL_THRESHOLD = 8
masks = np.zeros((n, img_h, img_w), dtype=bool)
for i in range(n):
r = 10 + i * 3
masks[i, r : r + 8, r : r + 8] = True
xyxy = mask_to_xyxy(masks).astype(np.float32)
cm = CompactMask.from_dense(masks, xyxy, image_shape=(img_h, img_w))
target = (40, 40)
resized = cm.resize(target)
assert resized.shape == (n, target[0], target[1])
assert len(resized) == n
# Each resized mask must be non-empty (the small squares survive downscale).
for i in range(n):
assert resized[i].any(), f"Mask {i} is empty after parallel resize"
def test_parallel_matches_sequential(self) -> None:
"""Thread-pool path produces the same result as the sequential path."""
img_h, img_w = 60, 60
n_parallel = 10 # triggers thread pool
n_sequential = 4 # stays sequential
rng = np.random.default_rng(0)
def _make_masks(n: int) -> CompactMask:
masks = np.zeros((n, img_h, img_w), dtype=bool)
for i in range(n):
r, c = int(rng.integers(5, 30)), int(rng.integers(5, 30))
masks[i, r : r + 10, c : c + 10] = True
xyxy = mask_to_xyxy(masks).astype(np.float32)
return CompactMask.from_dense(masks, xyxy, image_shape=(img_h, img_w))
cm_par = _make_masks(n_parallel)
cm_seq = _make_masks(n_sequential)
target = (30, 30)
resized_par = cm_par.resize(target)
resized_seq = cm_seq.resize(target)
# Both return correct shapes.
assert resized_par.shape == (n_parallel, target[0], target[1])
assert resized_seq.shape == (n_sequential, target[0], target[1])
+88 -4
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import csv
import os
from typing import Any
@@ -6,6 +8,7 @@ import numpy as np
import pytest
import supervision as sv
from supervision.detection.tools.csv_sink import CSVSink
from tests.helpers import _create_detections
@@ -225,19 +228,85 @@ from tests.helpers import _create_detections
["15.0", "25.0", "35.0", "45.0", "2", "0.7", "", "100.0", "6"],
],
), # mixed custom_data: ndarray sliced per row, scalar broadcast to all rows
(
_create_detections(
xyxy=[[10, 20, 30, 40], [50, 60, 70, 80]],
confidence=[0.9, 0.8],
class_id=[0, 1],
),
{"ids": ["a", "b"], "tags": ("x", "y"), "frame": 7},
_create_detections(
xyxy=[[15, 25, 35, 45]],
confidence=[0.7],
class_id=[2],
),
{"ids": ["c"], "tags": ("z",), "frame": 8},
"test_detections_list_custom_data.csv",
[
[
"x_min",
"y_min",
"x_max",
"y_max",
"class_id",
"confidence",
"tracker_id",
"frame",
"ids",
"tags",
],
["10.0", "20.0", "30.0", "40.0", "0", "0.9", "", "7", "a", "x"],
["50.0", "60.0", "70.0", "80.0", "1", "0.8", "", "7", "b", "y"],
["15.0", "25.0", "35.0", "45.0", "2", "0.7", "", "8", "c", "z"],
],
), # list/tuple custom_data matching detection count is sliced per row
(
sv.Detections(
xyxy=np.array([[10, 20, 30, 40], [50, 60, 70, 80]]),
data={"labels": ["person", "car"]},
),
None,
sv.Detections(
xyxy=np.array([[15, 25, 35, 45]]),
data={"labels": ["bus"]},
),
None,
"test_detections_plain_list_data.csv",
[
[
"x_min",
"y_min",
"x_max",
"y_max",
"class_id",
"confidence",
"tracker_id",
"labels",
],
["10", "20", "30", "40", "", "", "", "person"],
["50", "60", "70", "80", "", "", "", "car"],
["15", "25", "35", "45", "", "", "", "bus"],
],
), # plain Python list in detections.data is sliced per row without custom_data
],
)
def test_csv_sink(
detections: sv.Detections,
custom_data: dict[str, Any],
custom_data: dict[str, Any] | None,
second_detections: sv.Detections,
second_custom_data: dict[str, Any],
second_custom_data: dict[str, Any] | None,
file_name: str,
expected_result: list[list[Any]],
) -> None:
with sv.CSVSink(file_name) as sink:
sink.append(detections, custom_data)
sink.append(second_detections, second_custom_data)
if custom_data is None:
sink.append(detections)
else:
sink.append(detections, custom_data)
if second_custom_data is None:
sink.append(second_detections)
else:
sink.append(second_detections, second_custom_data)
assert_csv_equal(file_name, expected_result)
@@ -455,3 +524,18 @@ def assert_csv_equal(file_name, expected_rows):
)
os.remove(file_name)
@pytest.mark.parametrize(
("value", "i", "n", "expected"),
[
(["x"], 0, 1, "x"),
(["a", "b", "c"], 0, 2, ["a", "b", "c"]),
([42, "hello", None], 1, 3, "hello"),
([["a", "b"], ["c", "d"]], 0, 2, ["a", "b"]),
("ab", 0, 2, "ab"),
(("z",), 0, 1, "z"),
],
)
def test_csv_sink_slice_value(value: Any, i: int, n: int, expected: Any) -> None:
assert CSVSink._slice_value(value, i, n) == expected
+107 -4
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import json
import os
from typing import Any
@@ -275,19 +277,120 @@ from tests.helpers import _create_detections
},
],
), # numpy array in custom_data sliced per detection row
(
_create_detections(
xyxy=[[10, 20, 30, 40], [50, 60, 70, 80]],
confidence=[0.9, 0.8],
class_id=[0, 1],
),
{"ids": ["a", "b"], "tags": ("x", "y")},
_create_detections(
xyxy=[[15, 25, 35, 45]],
confidence=[0.7],
class_id=[2],
),
{"ids": ["c"], "tags": ("z",)},
"test_detections_list_custom_data.json",
[
{
"x_min": 10,
"y_min": 20,
"x_max": 30,
"y_max": 40,
"class_id": 0,
"confidence": 0.8999999761581421,
"tracker_id": "",
"ids": "a",
"tags": "x",
},
{
"x_min": 50,
"y_min": 60,
"x_max": 70,
"y_max": 80,
"class_id": 1,
"confidence": 0.800000011920929,
"tracker_id": "",
"ids": "b",
"tags": "y",
},
{
"x_min": 15,
"y_min": 25,
"x_max": 35,
"y_max": 45,
"class_id": 2,
"confidence": 0.699999988079071,
"tracker_id": "",
"ids": "c",
"tags": "z",
},
],
), # list/tuple custom_data matching detection count is sliced per row
(
sv.Detections(
xyxy=np.array([[10, 20, 30, 40], [50, 60, 70, 80]]),
data={"labels": ["person", "car"]},
),
None,
sv.Detections(
xyxy=np.array([[15, 25, 35, 45]]),
data={"labels": ["bus"]},
),
None,
"test_detections_plain_list_data.json",
[
{
"x_min": 10.0,
"y_min": 20.0,
"x_max": 30.0,
"y_max": 40.0,
"class_id": "",
"confidence": "",
"tracker_id": "",
"labels": "person",
},
{
"x_min": 50.0,
"y_min": 60.0,
"x_max": 70.0,
"y_max": 80.0,
"class_id": "",
"confidence": "",
"tracker_id": "",
"labels": "car",
},
{
"x_min": 15.0,
"y_min": 25.0,
"x_max": 35.0,
"y_max": 45.0,
"class_id": "",
"confidence": "",
"tracker_id": "",
"labels": "bus",
},
],
), # plain Python list in detections.data is sliced per row without custom_data
],
)
def test_json_sink(
detections: sv.Detections,
custom_data: dict[str, Any],
custom_data: dict[str, Any] | None,
second_detections: sv.Detections,
second_custom_data: dict[str, Any],
second_custom_data: dict[str, Any] | None,
file_name: str,
expected_result: list[list[Any]],
) -> None:
with sv.JSONSink(file_name) as sink:
sink.append(detections, custom_data)
sink.append(second_detections, second_custom_data)
if custom_data is None:
sink.append(detections)
else:
sink.append(detections, custom_data)
if second_custom_data is None:
sink.append(second_detections)
else:
sink.append(second_detections, second_custom_data)
assert_json_equal(file_name, expected_result)
+285 -19
View File
@@ -7,8 +7,13 @@ import numpy.typing as npt
import pytest
from supervision.detection.utils.converters import (
_decode_coco_rle_string,
_encode_coco_rle_string,
_base48_decode,
_base48_encode,
_delta_decode,
_delta_encode,
_mask_to_rle_counts,
_rle_counts_to_mask,
is_compressed_rle,
mask_to_rle,
rle_to_mask,
xcycwh_to_xyxy,
@@ -310,23 +315,6 @@ def test_xyxy_to_mask(boxes: np.ndarray, resolution_wh, expected: np.ndarray) ->
np.testing.assert_array_equal(result, expected)
@pytest.mark.parametrize(
"counts",
[
[5, 2, 2, 2, 5],
[0, 16],
[9],
[0, 5, 5, 5, 5, 5],
[6, 3, 2, 1, 1, 1, 2, 3, 6],
[3, 1, 2, 4, 2, 1, 3],
],
)
def test_coco_rle_encode_decode_round_trip(counts: list[int]) -> None:
encoded = _encode_coco_rle_string(counts)
decoded = _decode_coco_rle_string(encoded)
assert decoded == counts
@pytest.mark.parametrize(
("mask", "compressed", "expected_rle", "exception"),
[
@@ -533,3 +521,281 @@ def test_mask_rle_compressed_round_trip() -> None:
compressed = mask_to_rle(mask, compressed=True)
recovered = rle_to_mask(compressed, (4, 4))
np.testing.assert_array_equal(mask, recovered)
# ---------------------------------------------------------------------------
# is_compressed_rle
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
("rle", "expected"),
[
("52203", True), # str is compressed
(b"52203", True), # bytes is compressed
("", True), # empty str still str
(b"", True), # empty bytes still bytes
([5, 2, 2, 2, 5], False), # list is not compressed
(np.array([5, 2, 2, 2, 5]), False), # ndarray is not compressed
(42, False), # int is not compressed
(None, False), # None is not compressed
],
)
def test_is_compressed_rle(rle: object, expected: bool) -> None:
"""is_compressed_rle returns True for str/bytes, False otherwise."""
assert is_compressed_rle(rle) == expected
# ---------------------------------------------------------------------------
# _base48_decode / _base48_encode
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
("s", "expected"),
[
("", []), # empty string
("0", [0]), # single zero
("5", [5]), # single small value
("52203", [5, 2, 2, 0, 3]), # raw delta values (NOT absolute counts)
("09", [0, 9]), # two values, no continuation needed
],
)
def test_base48_decode(s: str, expected: list[int]) -> None:
"""_base48_decode returns raw delta-encoded integers from base-48 string."""
assert _base48_decode(s) == expected
@pytest.mark.parametrize(
"s",
[
"!", # ord('!')-48 triggers continuation, string ends immediately
"52P", # 'P' sets continuation bit but string ends
],
)
def test_base48_decode_malformed(s: str) -> None:
"""_base48_decode raises ValueError on truncated continuation sequences."""
with pytest.raises(ValueError, match="Malformed compressed RLE string"):
_base48_decode(s)
@pytest.mark.parametrize(
("values", "expected"),
[
([], ""), # empty list
([0], "0"), # single zero
([5], "5"), # single small value
([5, 2, 2, 0, 3], "52203"), # raw deltas encode to known string
([0, 9], "09"), # two values
],
)
def test_base48_encode(values: list[int], expected: str) -> None:
"""_base48_encode converts raw delta integers to base-48 string."""
assert _base48_encode(values) == expected
@pytest.mark.parametrize(
"values",
[
[],
[5, 2, 2, 0, 3],
[0, 9],
[6, 3, 2, 1, 1, 1, 2, 3, 6],
[100], # value >= 32 requires multi-byte continuation characters
[1000], # value requiring 3 continuation bytes
[-3], # negative delta: sign bit at bit 4 of final character
[-1, 0, -100], # multiple negative values
],
)
def test_base48_round_trip(values: list[int]) -> None:
"""_base48_decode(_base48_encode(v)) == v for any valid delta list."""
assert _base48_decode(_base48_encode(values)) == values
# ---------------------------------------------------------------------------
# _delta_decode / _delta_encode
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
("values", "expected"),
[
([], []), # empty
([5], [5]), # single element unchanged
([5, 2], [5, 2]), # two elements unchanged
([5, 2, 2], [5, 2, 2]), # three elements unchanged
([5, 2, 2, 0, 3], [5, 2, 2, 2, 5]), # delta applied from index 3
([0, 9], [0, 9]), # two elements, no delta needed
([0, 16], [0, 16]), # two elements, larger values
],
)
def test_delta_decode(values: list[int], expected: list[int]) -> None:
"""_delta_decode undoes COCO delta: counts[i] += counts[i-2] for i > 2."""
assert _delta_decode(values) == expected
@pytest.mark.parametrize(
("counts", "expected"),
[
([], []), # empty
([5], [5]), # single element unchanged
([5, 2], [5, 2]), # two elements unchanged
([5, 2, 2], [5, 2, 2]), # three elements unchanged
([5, 2, 2, 2, 5], [5, 2, 2, 0, 3]), # delta applied from index 3
([0, 9], [0, 9]), # two elements, no delta needed
],
)
def test_delta_encode(counts: list[int], expected: list[int]) -> None:
"""_delta_encode applies COCO delta: d[i] = counts[i] - counts[i-2] for i > 2."""
assert _delta_encode(counts) == expected
@pytest.mark.parametrize(
"counts",
[
[5, 2, 2, 2, 5],
[0, 9],
[6, 3, 2, 1, 1, 1, 2, 3, 6],
[0, 5, 5, 5, 5, 5],
],
)
def test_delta_round_trip(counts: list[int]) -> None:
"""_delta_decode(_delta_encode(counts)) == counts for any count list."""
assert _delta_decode(_delta_encode(counts)) == counts
# ---------------------------------------------------------------------------
# _mask_to_rle_counts / _rle_counts_to_mask
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
("mask_2d", "expected_counts"),
[
(
np.zeros((3, 3), dtype=bool),
[9],
), # all-False: one run of 9 background pixels
(
np.ones((3, 3), dtype=bool),
[0, 9],
), # all-True: 0 background then 9 foreground
(
np.array([[False, True], [True, False]]),
[1, 2, 1],
), # F-order [F,T,T,F] → starts with False, no leading-zero prepend
(
np.array([[True, False], [False, True]]),
[0, 1, 2, 1],
), # F-order [T,F,F,T] → starts with True, prepend 0
(
np.array(
[
[False, False, False, False],
[False, True, True, False],
[False, True, True, False],
[False, False, False, False],
]
),
[5, 2, 2, 2, 5],
), # 2x2 centre block in 4x4 grid
(
np.zeros((0, 4), dtype=bool),
[0],
), # empty mask → sentinel [0]
],
)
def test_mask_to_rle_counts(
mask_2d: npt.NDArray[np.bool_], expected_counts: list[int]
) -> None:
"""_mask_to_rle_counts produces correct COCO F-order run lengths."""
assert _mask_to_rle_counts(mask_2d).tolist() == expected_counts
@pytest.mark.parametrize(
("rle", "height", "width", "expected_mask"),
[
(
np.array([9], dtype=np.int32),
3,
3,
np.zeros((3, 3), dtype=bool),
), # all-False
(
np.array([0, 9], dtype=np.int32),
3,
3,
np.ones((3, 3), dtype=bool),
), # all-True
(
np.array([1, 2, 1], dtype=np.int32),
2,
2,
np.array([[False, True], [True, False]]),
), # F-order [F,T,T,F]
(
np.array([0, 1, 2, 1], dtype=np.int32),
2,
2,
np.array([[True, False], [False, True]]),
), # F-order [T,F,F,T]
(
np.array([5, 2, 2, 2, 5], dtype=np.int32),
4,
4,
np.array(
[
[False, False, False, False],
[False, True, True, False],
[False, True, True, False],
[False, False, False, False],
]
),
), # 2x2 centre block in 4x4 grid
(
np.array([3], dtype=np.int32),
2,
3,
np.zeros((2, 3), dtype=bool),
), # RLE encodes only 3 of 6 pixels; remainder padded False
(
np.array([0, 10], dtype=np.int32),
2,
3,
np.ones((2, 3), dtype=bool),
), # RLE sum (10) > h*w (6); excess truncated via flat[:num_pixels]
],
)
def test_rle_counts_to_mask(
rle: npt.NDArray[np.int32],
height: int,
width: int,
expected_mask: npt.NDArray[np.bool_],
) -> None:
"""_rle_counts_to_mask reconstructs the correct boolean mask from run lengths."""
result = _rle_counts_to_mask(rle, height, width)
np.testing.assert_array_equal(result, expected_mask)
@pytest.mark.parametrize(
"mask_2d",
[
np.zeros((3, 3), dtype=bool),
np.ones((4, 4), dtype=bool),
np.array([[False, True], [True, False]]),
np.array(
[
[False, False, False, False],
[False, True, True, False],
[False, True, True, False],
[False, False, False, False],
]
),
],
)
def test_mask_rle_counts_round_trip(mask_2d: npt.NDArray[np.bool_]) -> None:
"""_rle_counts_to_mask(_mask_to_rle_counts(m)) == m for non-empty masks."""
h, w = mask_2d.shape
rle = _mask_to_rle_counts(mask_2d)
recovered = _rle_counts_to_mask(rle, h, w)
np.testing.assert_array_equal(recovered, mask_2d)
Generated
+4 -4
View File
@@ -2563,7 +2563,7 @@ wheels = [
[[package]]
name = "nbconvert"
version = "7.17.0"
version = "7.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "beautifulsoup4" },
@@ -2582,9 +2582,9 @@ dependencies = [
{ name = "pygments" },
{ name = "traitlets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/47/81f886b699450d0569f7bc551df2b1673d18df7ff25cc0c21ca36ed8a5ff/nbconvert-7.17.0.tar.gz", hash = "sha256:1b2696f1b5be12309f6c7d707c24af604b87dfaf6d950794c7b07acab96dda78", size = 862855, upload-time = "2026-01-29T16:37:48.478Z" }
sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl", hash = "sha256:4f99a63b337b9a23504347afdab24a11faa7d86b405e5c8f9881cd313336d518", size = 261510, upload-time = "2026-01-29T16:37:46.322Z" },
{ url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" },
]
[[package]]
@@ -4055,7 +4055,7 @@ wheels = [
[[package]]
name = "supervision"
version = "0.28.0rc0"
version = "0.28.0rc2"
source = { editable = "." }
dependencies = [
{ name = "defusedxml" },