Compare commits

...
240 Commits
Author SHA1 Message Date
Jirka Borovec a90a1c967b Debugging workflow permissions and add PR dry-run (#2083)
* Debugging workflow permissions and add PR dry-run

* ls all

* path: dist/
2026-01-19 14:23:14 +01:00
Jirka Borovec e2c380acbc Debugging workflow permissions and add PR dry-run (#2082) 2026-01-19 12:35:46 +01:00
SkalskiP 819d74f57c bump version from 0.27.0 to 0.28.0rc0 2026-01-19 12:10:26 +01:00
jirka 47e6a4bc88 Rename workflow file for documentation CI process 2026-01-16 18:51:30 +01:00
09de0f4f06 feat(ruff): add pytest linting rule (#2053)
* feat(ruff): add pytest linting rule
* --unsafe-fixes
* fix(pre_commit): 🎨 auto format pre-commit hooks


---------

Co-authored-by: jiri BOROVEC <jirka@Jiris-MacBook-Pro-J4N257Y6Q2.local>
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>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-01-13 19:34:22 +01:00
f1e0c9cdd7 chore(pre_commit): add pyproject-fmt and validate-pyproject hooks (#2045)
* chore(pre_commit): add pyproject-fmt and validate-pyproject hooks
* fix(pre_commit): 🎨 auto format pre-commit hooks
* Apply suggestions from code review
* format pre-commit hooks for consistency

---------

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>
Co-authored-by: jiri BOROVEC <jirka@Jiris-MacBook-Pro-J4N257Y6Q2.local>
2026-01-13 18:48:06 +01:00
KshitijAucharmalandJirka Borovec 72d55a2938 docs: changed --extra to --group for dev (#1972)
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
2026-01-13 18:33:09 +01:00
3abb6245c1 chore(pre_commit): ⬆ pre_commit autoupdate (#2075)
* chore(pre_commit): ⬆ pre_commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.14.10 → v0.14.11](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.10...v0.14.11)
- [github.com/executablebooks/mdformat: 0.7.17 → 1.0.0](https://github.com/executablebooks/mdformat/compare/0.7.17...1.0.0)

* fix(pre_commit): 🎨 auto format pre-commit hooks
* Apply suggestions from code review

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: jirka <jirka.borovec@seznam.cz>
2026-01-12 20:15:27 +01:00
Jirka BorovecandCopilot a352e23d95 Switch PR Conflict Labeler workflow to eps1lon/actions-label-merge-conflict (#2076)
* Switch PR Conflict Labeler workflow to `eps1lon/actions-label-merge-conflict`
* Apply suggestions from code review

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-12 20:11:36 +01:00
Jirka BorovecandCopilot 75a2036ea3 Add GitHub Action for labeling PRs with conflicts (#2073)
* Add GitHub Action for labeling PRs with conflicts
* Apply suggestions from code review

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-12 16:06:55 +00:00
3d7642c09e chore(pre_commit): enable mdformat with additional dependencies (#2043)
* chore(pre_commit): enable mdformat with additional dependencies
* fix(pre_commit): 🎨 auto format pre-commit hooks
* Fix mdformat-introduced syntax errors in documentation (#2044)

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Borda <6035284+Borda@users.noreply.github.com>
2026-01-12 16:43:17 +01:00
fabbff2b60 Remove security noqa comments by fixing underlying issues (#2064)
* Fix security issues and remove noqa comments
- Replace MD5 hash_new with md5() and add usedforsecurity=False flag
- Add documentation explaining MD5 is for file integrity, not security
- Add timeout parameter to requests.get call
- Add explicit shell=False and check=False to subprocess.run
- Remove all security-related noqa comments
- Add per-file ignores for S311 in tests and S603 in examples

---------

Co-authored-by: jirka <jirka.borovec@seznam.cz>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Borda <6035284+Borda@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-12 15:25:41 +01:00
Jirka BorovecandCopilot 751eddc0ce Add Discord link to issues for community collaboration (#2074)
* Add Discord link to issue templates for community collaboration

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-12 15:11:23 +01:00
Jirka BorovecandCopilot 8df7aea949 Update CI workflows for documentation build and deployment (#2063)
Material for MkDocs Insiders – Now free for everyone
-> https://squidfunk.github.io/mkdocs-material/blog/2025/11/11/insiders-now-free-for-everyone/

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-12 15:06:08 +01:00
bbc96e14c9 docs: Update CONTRIBUTING.md for clarity and formatting improvements (#2060)
* docs: Update CONTRIBUTING.md for clarity and formatting improvements
* fix(pre_commit): 🎨 auto format pre-commit hooks
* Apply suggestions from code review
* Update command for installing project dependencies
* Update CONTRIBUTING.md

---------

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-01-12 14:05:28 +01:00
Jirka Borovecandpre-commit-ci[bot] 5695b7f58f chore: Update issue templates for bug reports and feature requests (#2052)
* chore: Update issue templates for bug reports and feature requests
* fix(pre_commit): 🎨 auto format pre-commit hooks

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-12 12:32:14 +00:00
Jirka Borovec a1c58a92e2 Update classifiers in pyproject.toml to reflect production status and add multimedia topics (#2047) 2026-01-12 11:04:00 +00:00
Jirka Borovec 886a44b102 Update authors and maintainers in pyproject.toml (#2046) 2026-01-12 12:00:27 +01:00
dependabot[bot] 9b91c779e5 ⬆️ Update pytest-cov requirement (#2071)
Updates the requirements on [pytest-cov](https://github.com/pytest-dev/pytest-cov) to permit the latest version.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v4.0.0...v7.0.0)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-version: 7.0.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-01-12 11:59:52 +01:00
Jirka Borovecandpre-commit-ci[bot] 3697b2298c chore: add pull request template for improved PR process (#2042)
* chore: add pull request template for improved submission process
* fix(pre_commit): 🎨 auto format pre-commit hooks
* Apply suggestions from code review

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-12 11:58:57 +01:00
Jirka BorovecandCopilot Autofix powered by AI e861878a22 chore: Add shared build-package workflow for (pre)publishing (#2050)
* chore: Add shared build-package workflow for (pre)publishing
* Potential fix for code scanning alert no. 4: Workflow does not contain permissions

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-01-12 10:10:04 +01:00
Jirka Borovec 834fd8db05 ⬆️ Update dependabot config to group GH Actions bumps (#2034) 2026-01-12 10:01:13 +01:00
Jirka Borovec fba1713944 ci: add UV cache pruning step (#2067) 2026-01-12 09:53:15 +01:00
b143121859 Add pytest-cov dependency and integrate Codecov in CI workflow (#2066)
* Add pytest-cov dependency and integrate Codecov in CI workflow
* fix(pre_commit): 🎨 auto format pre-commit hooks
* Apply suggestions from code review

---------

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-01-12 09:41:44 +01:00
Jirka Borovec 132cc056f6 chore(pre_commit): remove exclusion for trailing-whitespace hook (#2049) 2026-01-12 09:30:54 +01:00
dependabot[bot] f6e0896278 ⬆️ Bump astral-sh/setup-uv from 7.1.6 to 7.2.0 (#2070)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 7.1.6 to 7.2.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/681c641aba71e4a1c380be3ab5e12ad51f415867...61cb8a9741eeb8a550a1b8544337180c0fc8476b)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-12 09:30:04 +01:00
dependabot[bot] de4f060d3b ⬆️ Update build requirement from <1.4,>=0.10 to >=0.10,<1.5 (#2069)
Updates the requirements on [build](https://github.com/pypa/build) to permit the latest version.
- [Release notes](https://github.com/pypa/build/releases)
- [Changelog](https://github.com/pypa/build/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/build/compare/0.10.0...1.4.0)

---
updated-dependencies:
- dependency-name: build
  dependency-version: 1.4.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-01-12 09:29:41 +01:00
Jirka Borovec 14b7f91c5c chore(pre_commit): remove duplicated pyupgrade hook (#2041) 2026-01-12 09:29:12 +01:00
Jirka BorovecandCopilot 5016ed3c8a chore(pre_commit): update Ruff rules with its references (#2040)
The `fixanle` they do not have any impact if they are not listed in selected.

In Ruff, the select list determines which rules are active (checked). The fixable list merely controls which of those active rules are allowed to automatically modify your code when running with --fix.

If a rule is listed in fixable but not in select, it is effectively disabled; Ruff won't look for it, so there will be no errors to fix. Listing extra items in fixable is harmless but redundant code clutter.

The default value for fixable in Ruff is ["ALL"].

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-12 08:23:49 +01:00
Jirka Borovec d570366fb4 chore(pre_commit): convert bandit into Ruff's S rule & add TODOs for security improvements (#2039) 2026-01-12 08:08:19 +01:00
95ef33a102 Add testing-guardian job to aggregate test results (#2037)
* Add testing-guardian job to aggregate test results
* Potential fix for code scanning alert no. 2: Workflow does not contain permissions
* Apply suggestions from code review

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-06 17:28:33 +01:00
Jirka BorovecandCopilot fc11beac78 Add push triggers for main and develop branches in workflow files (#2033)
* Add push triggers for `main` and `develop` branches in workflow files
* Add conditional concurrency cancellation for PR vs push events (#2035)

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-01-06 15:39:28 +01:00
pre-commit-ci[bot] cb1fcecaf2 chore(pre_commit): ⬆ pre_commit autoupdate (#2032)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.13.3 → v0.14.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.3...v0.14.10)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-05 20:29:04 +01:00
Christian ClaussandJirka Borovec afdbdc55f7 pre-commit autoupdate 2025-10-04 (#1976)
* pre-commit autoupdate 2025-10-04
* Rename ruff hook to ruff-check in pre-commit config

---------

Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
2026-01-05 17:27:28 +01:00
28dfc39c02 chore(pre_commit): ⬆ pre_commit autoupdate (#1928)
* chore(pre_commit): ⬆ pre_commit autoupdate

updates:
- [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0)
- [github.com/PyCQA/bandit: 1.8.6 → 1.9.2](https://github.com/PyCQA/bandit/compare/1.8.6...1.9.2)
- [github.com/astral-sh/ruff-pre-commit: v0.12.5 → v0.14.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.5...v0.14.10)
- [github.com/asottile/pyupgrade: v3.20.0 → v3.21.2](https://github.com/asottile/pyupgrade/compare/v3.20.0...v3.21.2)

* linting

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: jirka <jirka.borovec@seznam.cz>
2026-01-05 17:21:58 +01:00
dependabot[bot] 793f246ebc ⬆️ Bump actions/create-github-app-token from 2.0.6 to 2.2.1 (#2024)
Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 2.0.6 to 2.2.1.
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Commits](https://github.com/actions/create-github-app-token/compare/df432ceedc7162793a195dd1713ff69aefc7379e...29824e69f54612133e76f7eaac726eef6c875baf)

---
updated-dependencies:
- dependency-name: actions/create-github-app-token
  dependency-version: 2.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-05 16:48:24 +01:00
dependabot[bot] 9aa9ef5bbe ⬆️ Bump actions/checkout from 4.2.2 to 6.0.1 (#2025)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.2 to 6.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/11bd71901bbe5b1630ceea73d27597364c9af683...8e8c483db84b4bee98b60c0593521ed34d9990e8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-05 16:48:15 +01:00
dependabot[bot] 6e18804c55 ⬆️ Bump astral-sh/setup-uv from 7.1.2 to 7.1.6 (#2029)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 7.1.2 to 7.1.6.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41...681c641aba71e4a1c380be3ab5e12ad51f415867)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-05 16:48:02 +01:00
Jirka Borovec bca3c60bb3 resolve docs build / pin mkdocstrings-python<2.0.0 (#2031) 2026-01-05 15:48:18 +01:00
SkalskiP 7f7b25d7e7 bump version from 0.27.0rc6 to 0.27.0 2025-11-16 19:02:42 +01:00
SkalskiP 72506888c0 bump version from 0.27.0rc5 to 0.27.0rc6 2025-11-16 16:31:22 +01:00
Piotr Skalski c5cc24f22d Merge pull request #2017 from roboflow/0.27.0-changelog
supervision 0.27.0 changelog
2025-11-16 16:18:50 +01:00
SkalskiP 47574ed29d supervision 0.27.0 changelog 2025-11-16 16:15:10 +01:00
Piotr Skalski 6119f2017e Merge pull request #2016 from roboflow/feature/make_box_aspect_ratio_part_of_detections_api
add `box_aspect_ratio` property to `sv.Detections`
2025-11-16 13:39:49 +01:00
SkalskiP bbc37b4b6a fix failing tests 2025-11-16 13:34:51 +01:00
SkalskiP e12bfe263d add box_aspect_ratio property to sv.Detections 2025-11-16 13:27:29 +01:00
Piotr Skalski 373fbae1fa Merge pull request #2015 from roboflow/feature/support_for_qwen_3_vl
Qwen3-VL support
2025-11-16 01:02:29 +01:00
SkalskiP 790ccace2c Merge remote-tracking branch 'origin/feature/support_for_qwen_3_vl' into feature/support_for_qwen_3_vl 2025-11-15 22:54:34 +01:00
SkalskiP a99015ac2d more Qwen2.5-VL tests 2025-11-15 22:51:22 +01:00
pre-commit-ci[bot] 52c772d778 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-11-15 21:48:47 +00:00
SkalskiP 5af13c0f8c add Qwen3-VL prompting example 2025-11-15 22:48:20 +01:00
SkalskiP e0edb28ce6 Merge remote-tracking branch 'origin/feature/support_for_qwen_3_vl' into feature/support_for_qwen_3_vl 2025-11-15 22:27:20 +01:00
SkalskiP c35a35cefd plug Qwen3-VL into sv.Detections.from_vlm 2025-11-15 22:27:11 +01:00
pre-commit-ci[bot] 9bd8f71f26 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-11-15 21:19:57 +00:00
SkalskiP e29e3832fb plug Qwen3-VL into sv.Detections.from_vlm 2025-11-15 22:19:28 +01:00
SkalskiP a287e68c92 Merge remote-tracking branch 'origin/feature/support_for_qwen_3_vl' into feature/support_for_qwen_3_vl 2025-11-15 22:06:56 +01:00
SkalskiP 0422a66eaa plug Qwen3-VL into sv.Detections.from_vlm 2025-11-15 22:05:46 +01:00
pre-commit-ci[bot] 31db2ded11 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-11-15 20:40:07 +00:00
SkalskiP 9ab9650763 plug Qwen3-VL into sv.Detections.from_vlm 2025-11-15 21:39:23 +01:00
SkalskiP 353bd5336b Merge remote-tracking branch 'origin/feature/support_for_qwen_3_vl' into feature/support_for_qwen_3_vl 2025-11-15 21:37:05 +01:00
SkalskiP 7301156e46 plug Qwen3-VL into sv.Detections.from_vlm 2025-11-15 21:36:54 +01:00
pre-commit-ci[bot] e68c908077 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-11-15 20:29:38 +00:00
SkalskiP 2edcbdc487 initial commit with from_qwen_3_vl added, from_qwen_2_5_vl improved 2025-11-15 21:27:55 +01:00
Piotr Skalski 25c2f5cf0d Merge pull request #1997 from AnonymDevOSS/feat/threads-queues-video-process
feat: add threaded I/O pipeline for video processing
2025-11-15 02:40:10 +01:00
SkalskiP 316cb6271d make ruff happy 2025-11-15 02:33:11 +01:00
SkalskiP 70e9af9200 make ruff happy 2025-11-15 02:30:55 +01:00
pre-commit-ci[bot] 38f89125b1 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-11-15 01:10:49 +00:00
SkalskiP 80d76e712a style and docstring improvements 2025-11-15 02:10:04 +01:00
SkalskiP a296712000 improved InferenceSlicer docstring 2025-11-15 00:00:52 +01:00
Piotr Skalski 12d7c369db Merge pull request #2014 from roboflow/0.27.0-deprecations
remove 0.27.0 deprecations + InferenceSlicer improvements
2025-11-14 23:51:36 +01:00
pre-commit-ci[bot] c8223cbab2 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-11-14 22:15:39 +00:00
SkalskiP 2c522a9931 improved InferenceSlicer docstring 2025-11-14 23:14:34 +01:00
AnonymDevOSS fc0e133ea9 added example as it was delited by mistake 2025-11-14 21:53:14 +01:00
AnonymDevOSS 15364ac9a3 Rename process_video_threads to process_video and remove legacy implementation and redundant tests 2025-11-14 21:45:06 +01:00
pre-commit-ci[bot] e82b5b9d74 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-11-14 16:58:25 +00:00
SkalskiP c8f5c741fd reimplement crop_image to skip type casting when Pillow Image given
`get_image_resolution_wh` added
2025-11-14 17:55:07 +01:00
SkalskiP 8d464aa1b5 improve InferenceSlicer implementation lowering the count of generated boxes 2025-11-14 16:42:10 +01:00
SkalskiP ce28ab7fc7 fix uti tests for InferenceSlicer._generate_offset 2025-11-14 15:49:22 +01:00
SkalskiP fae1bc2661 bump version from 0.27.0rc4 to 0.27.0rc5 2025-11-14 14:52:57 +01:00
SkalskiP 4663fab4e4 update new docs page marks 2025-11-14 14:52:39 +01:00
SkalskiP db0892f3e9 remove unnecessary package-level import 2025-11-14 14:52:03 +01:00
Piotr Skalski d330bafaa7 Merge pull request #1926 from roboflow/dependabot/pip/develop/mkdocstrings-gte-0.25.2-and-lt-0.31.0
⬆️ Update mkdocstrings requirement from <0.30.0,>=0.25.2 to >=0.25.2,<0.31.0
2025-11-14 14:12:16 +01:00
Piotr Skalski 8b91d733af Merge pull request #1955 from roboflow/dependabot/github_actions/develop/pypa/gh-action-pypi-publish-1.13.0
⬆️ Bump pypa/gh-action-pypi-publish from 1.12.4 to 1.13.0
2025-11-14 14:11:57 +01:00
Piotr Skalski 97bf652d90 Merge pull request #1927 from roboflow/dependabot/pip/develop/build-gte-0.10-and-lt-1.4
⬆️ Update build requirement from <1.3,>=0.10 to >=0.10,<1.4
2025-11-14 14:11:24 +01:00
Piotr Skalski b0bbb74558 Merge pull request #1965 from roboflow/dependabot/github_actions/develop/actions/github-script-8.0.0
⬆️ Bump actions/github-script from 7.0.1 to 8.0.0
2025-11-14 13:50:13 +01:00
Piotr Skalski 1d73468dc5 Merge pull request #2012 from jackiehimel/fix/bugs-1959-2000
fix: correct numpy indexing in denormalize_boxes and add ultralytics validation
2025-11-14 13:25:26 +01:00
SkalskiP dfd2a60855 update denormalize_boxes docstring and examples; update from_google_gemini_2_0, from_google_gemini_2_5 and from_moondream to use latest version of denormalize_boxes 2025-11-14 13:12:39 +01:00
SkalskiP f563c5a634 update denormalize_boxes docstring and examples 2025-11-14 12:53:58 +01:00
SkalskiP 35272b40d3 remove ultralytics tests 2025-11-14 12:38:55 +01:00
Piotr Skalski 9320be5d5b Merge pull request #2013 from dominikWin/docs/how-to-json-path
docs: Update JSONSink example path
2025-11-14 12:28:25 +01:00
Piotr Skalski d109fba9d8 Merge pull request #2001 from AnonymDevOSS/feat/speed-up-box-iou-batch
feat: speed up box iou batch using 2d in-place ops instead of 3d
2025-11-14 12:26:25 +01:00
SkalskiP 42d92ba913 making ruff happy 2025-11-14 12:00:45 +01:00
pre-commit-ci[bot] 1e2dc8f88b fix(pre_commit): 🎨 auto format pre-commit hooks 2025-11-14 10:58:01 +00:00
SkalskiP f716b7fa3b improve docstrings, examples, variable naming conventions and unit tests 2025-11-14 11:57:31 +01:00
Dominik Winecki f6d52b53db docs: Update JSONSink example path 2025-11-13 13:48:16 -05:00
pre-commit-ci[bot] 61cca76acc fix(pre_commit): 🎨 auto format pre-commit hooks 2025-11-13 02:10:25 +00:00
jackiehimel 9ad850af46 fix: correct numpy indexing in denormalize_boxes and add ultralytics validation
- Fix denormalize_boxes numpy indexing bug that caused IndexError with 3+ boxes
- Add validation for missing boxes attribute in from_ultralytics
- Add comprehensive test coverage (11 new tests)

Fixes #1959
Fixes #2000
2025-11-12 20:58:25 -05:00
AnonymDevOSS eee6a85eaa Merge branch 'feat/speed-up-box-iou-batch' of github.com:AnonymDevOSS/supervision into feat/speed-up-box-iou-batch 2025-11-06 23:01:49 +01:00
AnonymDevOSS bb87ab0a6e fixed autoformat errors 2025-11-06 23:00:33 +01:00
pre-commit-ci[bot] da7c5cd65a fix(pre_commit): 🎨 auto format pre-commit hooks 2025-11-06 21:57:29 +00:00
AnonymDevOSS ceefe0bab2 feat/ speed up box iou - replaced original function; added tests 2025-11-06 22:52:25 +01:00
Piotr Skalski 78439e03cc Merge pull request #1999 from roboflow/dependabot/github_actions/develop/astral-sh/setup-uv-7.1.2
⬆️ Bump astral-sh/setup-uv from 6.4.3 to 7.1.2
2025-11-06 00:09:30 +01:00
Piotr Skalski b66416e074 Merge pull request #1993 from ernestlwt/fix/overlapmetric-to-upper
fix: Overlapmetric should check string using uppercase conversion
2025-11-06 00:07:20 +01:00
Piotr Skalski 6c7c49e051 Merge pull request #1974 from kshitijaucharmal/fix/add-pre-commit-in-pyproject
fix: added precommit under dev group
2025-11-05 23:58:32 +01:00
SkalskiP b1b69ad972 bump version from 0.27.0rc3 to 0.27.0rc4 2025-11-05 23:53:36 +01:00
SkalskiP 52be9dc818 fix filter_segments_by_distance docstring 2025-11-05 21:19:33 +01:00
Piotr Skalski c7e95483bf Merge pull request #2008 from roboflow/feature/filter_segments_by_distance
initial version of `filter_segments_by_distance`
2025-11-05 20:41:43 +01:00
SkalskiP 65f6e418a1 making flake8 happy 2025-11-05 20:34:22 +01:00
pre-commit-ci[bot] 1fef946a19 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-11-05 19:29:22 +00:00
SkalskiP 4c3d51f186 making flake8 happy 2025-11-05 20:28:55 +01:00
pre-commit-ci[bot] 9340cb962b fix(pre_commit): 🎨 auto format pre-commit hooks 2025-11-05 19:23:02 +00:00
SkalskiP 6578b4ea52 initial version of filter_segments_by_distance 2025-11-05 20:18:10 +01:00
SkalskiP 0bd60874ca bump version from 0.27.0rc2 to 0.27.0rc3 2025-11-05 13:39:13 +01:00
Piotr Skalski b02ec93b30 Merge pull request #2006 from roboflow/feature/xyxy_to_mask
Feature/xyxy to mask
2025-11-05 13:23:13 +01:00
pre-commit-ci[bot] 89ee57f519 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-11-05 12:14:45 +00:00
SkalskiP 1c8398942a docs and examples 2025-11-05 13:12:49 +01:00
pre-commit-ci[bot] 3e7e4caf3f fix(pre_commit): 🎨 auto format pre-commit hooks 2025-11-05 12:03:55 +00:00
SkalskiP a3ea8cdb1f bump version from 0.27.0rc1 to 0.27.0rc2 2025-11-05 13:01:30 +01:00
SkalskiP 32dca290ef xyxy_to_mask implementation + tests 2025-11-05 13:00:11 +01:00
AnonymDevOSS 03f623920a feat: tests for add threaded I/O pipeline for video processing 2025-10-28 14:05:52 +01:00
AnonymDevOSS 98476dfacf feat: add threaded I/O pipeline for video processing
Implements pipeline with bounded queues to overlap decode,
compute and encode. Reduces I/O stalls.
2025-10-28 14:05:37 +01:00
AnonymDevOSS 35ff87b08d feat: speed up box iou batch using 2d in-place ops instead of 3d
without creating intermediary (N, M, 2) arrays.
2025-10-28 13:51:28 +01:00
dependabot[bot] 03eed3fd64 ⬆️ Bump astral-sh/setup-uv from 6.4.3 to 7.1.2
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 6.4.3 to 7.1.2.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/e92bafb6253dcd438e0484186d7669ea7a8ca1cc...85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 00:05:24 +00:00
Ernest Lim 272d9ed5bf fix: Overlapmetric should check string using uppercase conversion 2025-10-16 16:12:28 +08:00
Kshitij Aucharmal 371f8ecc5b fix: added precommit under dev group 2025-10-03 19:46:46 +05:30
Piotr Skalski 171687f3ab Merge pull request #1968 from kshitijaucharmal/develop
Fixed: random print("hm") in merge_metadata
2025-09-25 17:22:01 +02:00
Kshitij Aucharmal 421ad21c98 Fixed: random print("hm") in merge_metadata 2025-09-24 23:25:12 +05:30
dependabot[bot] 50358e96b6 ⬆️ Bump actions/github-script from 7.0.1 to 8.0.0
Bumps [actions/github-script](https://github.com/actions/github-script) from 7.0.1 to 8.0.0.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/60a0d83039c74a4aee543508d2ffcb1c3799cdea...ed597411d8f924073f98dfc5c65a23a2325f34cd)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: 8.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-22 00:11:46 +00:00
dependabot[bot] 86e10db19e ⬆️ Bump pypa/gh-action-pypi-publish from 1.12.4 to 1.13.0
Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.12.4 to 1.13.0.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/76f52bc884231f62b9a034ebfe128415bbaabdfc...ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 00:07:58 +00:00
Piotr Skalski deb1c9c4f4 Merge pull request #1945 from roboflow/docs/refactor_0.27.0
docs/refactor 0.27.0 missing commits
2025-08-07 16:49:03 +02:00
pre-commit-ci[bot] 6679b1f5a9 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-08-07 14:45:36 +00:00
Piotr Skalski c7de70ed20 Merge pull request #1943 from roboflow/docs/refactor_0.27.0
docs/refactor 0.27.0
2025-08-07 13:46:36 +02:00
SkalskiP 2cdb70eb91 Merge remote-tracking branch 'origin/docs/refactor_0.27.0' into docs/refactor_0.27.0
# Conflicts:
#	supervision/utils/image.py
2025-08-07 13:32:56 +02:00
SkalskiP 2ef029ef4b visualize examples 2025-08-07 13:32:44 +02:00
pre-commit-ci[bot] dce96d462e fix(pre_commit): 🎨 auto format pre-commit hooks 2025-08-07 10:36:03 +00:00
SkalskiP 3fcbef9482 Merge remote-tracking branch 'origin/docs/refactor_0.27.0' into docs/refactor_0.27.0
# Conflicts:
#	supervision/utils/image.py
2025-08-07 12:35:35 +02:00
SkalskiP c8b966be1a align new utils with naming conventions 2025-08-07 12:35:09 +02:00
pre-commit-ci[bot] 72621dce32 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-08-07 10:30:22 +00:00
SkalskiP 098d5e3eb1 Merge remote-tracking branch 'origin/docs/refactor_0.27.0' into docs/refactor_0.27.0
# Conflicts:
#	supervision/utils/image.py
2025-08-07 12:29:54 +02:00
SkalskiP 8c49465d49 align new utils with naming conventions 2025-08-07 12:29:05 +02:00
pre-commit-ci[bot] 4ad36db3bb fix(pre_commit): 🎨 auto format pre-commit hooks 2025-08-07 09:53:22 +00:00
SkalskiP a933b93234 update image utils docs; deprecate overlay_image image 2025-08-07 11:51:59 +02:00
SkalskiP cbb58a8c6f tint_image and grayscale_image functions added 2025-08-07 10:01:50 +02:00
SkalskiP 8fc22f0281 move ImageType to a more suitable location 2025-08-07 07:54:50 +02:00
SkalskiP 4b7b347aea drop create tiles from public API 2025-08-07 07:45:55 +02:00
SkalskiP 5c6745291b after few tests drop support for pycon in docs examples 2025-08-07 07:41:52 +02:00
SkalskiP 83d490dadc initial support for pycon docs examples 2025-08-06 23:19:21 +02:00
SkalskiP 005cdbd37a bump version from 0.27.0rc0 to 0.27.0rc1 2025-08-06 12:27:16 +02:00
Piotr Skalski ade2668ae3 Merge pull request #1932 from roboflow/fix/numpy.int64-object-has-no-attribute-splitlines
fix: AttributeError: 'numpy.int64' object has no attribute 'splitlines'
2025-08-06 12:25:46 +02:00
pre-commit-ci[bot] 46e5965680 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-08-06 10:17:35 +00:00
SkalskiP aab3160cd7 make ruff happy 2025-08-06 12:17:04 +02:00
pre-commit-ci[bot] 13ab215fdf fix(pre_commit): 🎨 auto format pre-commit hooks 2025-08-06 10:05:47 +00:00
SkalskiP a08cda30fb ready for review 2025-08-06 12:02:51 +02:00
dependabot[bot] 652b0a190f ⬆️ Update build requirement from <1.3,>=0.10 to >=0.10,<1.4
Updates the requirements on [build](https://github.com/pypa/build) to permit the latest version.
- [Release notes](https://github.com/pypa/build/releases)
- [Changelog](https://github.com/pypa/build/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/build/compare/0.10.0...1.3.0)

---
updated-dependencies:
- dependency-name: build
  dependency-version: 1.3.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-04 00:52:10 +00:00
dependabot[bot] 6f87e2639e ⬆️ Update mkdocstrings requirement
Updates the requirements on [mkdocstrings](https://github.com/mkdocstrings/mkdocstrings) to permit the latest version.
- [Release notes](https://github.com/mkdocstrings/mkdocstrings/releases)
- [Changelog](https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mkdocstrings/mkdocstrings/compare/0.25.2...0.30.0)

---
updated-dependencies:
- dependency-name: mkdocstrings
  dependency-version: 0.30.0
  dependency-type: direct:production
...

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 00:32:44 +00:00
Piotr Skalski baa0402d5a Merge pull request #1901 from roboflow/fix/inference-overlap-metric-fix
fix:  🐞 update inference_slicer.py for improved detection handling
2025-07-21 00:07:28 +02:00
pre-commit-ci[bot] c54d16c57e fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-20 22:02:42 +00:00
SkalskiP 1adb75520f bump version from 0.26.0 to 0.26.1; improve docstrings; unify naming; improve parsing 2025-07-21 00:02:14 +02:00
pre-commit-ci[bot] db51a9ff98 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-20 17:12:24 +00:00
Onuralp SEZER f044398c17 fix: 🐞 update inference_slicer.py for improved detection handling 2025-07-20 20:11:37 +03:00
pre-commit-ci[bot] 704652a0d2 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-18 15:19:38 +00:00
Balthasar 6e3001194d improve tests 2025-07-18 17:19:10 +02:00
Balthasar d7774381cc Merge branch 'develop' into fix/mAP-area 2025-07-18 12:11:05 +02:00
Soumik Rakshit 1c8ec0a287 Merge pull request #1898 from roboflow/fix/negative-mAP-values
fix: handle -1 sentinel values in mAP calculation to prevent negative results and mimic pycocotools behaviour
2025-07-18 15:32:21 +05:30
Balthasar 118e3a496c fix: line length 2025-07-18 10:09:53 +02:00
pre-commit-ci[bot] a139fb0b49 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-18 08:00:37 +00:00
Balthasar afdbb0a75c fix: keep it DRY 2025-07-18 09:41:06 +02:00
Balthasar 001a4254f8 fix: improve code, more fixes and add tests 2025-07-18 09:41:06 +02:00
Balthasar 2e72e70893 basic fix 2025-07-18 09:41:06 +02:00
pre-commit-ci[bot] 2907c40ecc fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-17 08:00:04 +00:00
Balthasar 2c67cee720 fix: tests and adjust comment 2025-07-17 09:57:55 +02:00
Balthasar 0ddbcb2010 fix: start id with 1 because 0 means no match 2025-07-17 09:49:21 +02:00
pre-commit-ci[bot] 40d4c6f554 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-16 18:07:52 +00:00
Balthasar bf88636487 Merge branch 'fix/mAP-area' of https://github.com/roboflow/supervision into fix/mAP-area 2025-07-16 20:07:30 +02:00
Balthasar a444b3bde8 fix: cleanup comment 2025-07-16 20:06:27 +02:00
pre-commit-ci[bot] 4009f9d886 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-16 18:00:47 +00:00
Balthasar 3f926c618a fix: cleanup comments 2025-07-16 19:51:59 +02:00
Balthasar 71d6670e7d fix(metrics): update area calculation in MeanAveragePrecision to use area property as fallback if it's not COCO 2025-07-16 19:50:37 +02:00
Piotr Skalski 08226e9199 Merge pull request #1893 from roboflow/main
Merge pull request #1742 from roboflow/develop
2025-07-16 02:50:11 +02:00
Onuralp SEZER 535cb2a91b fix(vlm): 📝 correct formatting in docstring for from_deepseek_vl_2 function 2025-07-11 18:49:50 +03:00
pre-commit-ci[bot] 8e845caeeb fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-11 15:44:15 +00:00
Onuralp SEZER 9e2a535044 Merge branch 'develop' into feat/from_deepseek_vl_2_support 2025-07-11 18:43:02 +03:00
SkalskiP f737782ae8 initial commit 2025-04-07 14:56:58 +02:00
116 changed files with 5426 additions and 2093 deletions
+45
View File
@@ -0,0 +1,45 @@
# see https://docs.codecov.io/docs/codecov-yaml
# Validation check:
# $ curl --data-binary @.codecov.yml https://codecov.io/validate
# https://docs.codecov.io/docs/codecovyml-reference
codecov:
bot: "codecov-io"
strict_yaml_branch: "yaml-config"
require_ci_to_pass: yes
notify:
# after_n_builds: 2
wait_for_ci: yes
coverage:
precision: 0 # 2 = xx.xx%, 0 = xx%
round: nearest # how coverage is rounded: down/up/nearest
range: 40...100 # custom range of coverage colors from red -> yellow -> green
status:
# https://codecov.readme.io/v1.0/docs/commit-status
project:
default:
informational: true
target: 95% # specify the target coverage for each commit status
threshold: 10% # allow this little decrease on project
# https://github.com/codecov/support/wiki/Filtering-Branches
# branches: master
if_ci_failed: error
# https://github.com/codecov/support/wiki/Patch-Status
patch:
default:
informational: true
target: 95% # enforce same coverage target for new/changed code as the project
threshold: 10% # allow only a small decrease on patch coverage
changes: false
# https://docs.codecov.com/docs/github-checks#disabling-github-checks-patch-annotations
github_checks:
annotations: false
comment:
layout: header, diff
require_changes: false
behavior: default # update if exists else create new
# branches: *
+1 -1
View File
@@ -1,4 +1,4 @@
# These owners will be the default owners for everything in
# the repo. They will be requested for review when someone
# opens a pull request.
* @SkalskiP @onuralpszr
* @SkalskiP @soumik12345
+33 -30
View File
@@ -1,65 +1,68 @@
name: 🐞 Bug Report
# title: " "
description: Problems with Supervision
title: "[Bug]: "
description: Report a bug or unexpected behavior in Supervision
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thank you for submitting a Supervision 🐞 Bug Report!
- type: checkboxes
attributes:
label: Search before asking
description: >
Please search the [issues](https://github.com/roboflow/supervision/issues) to see if a similar bug report already exists.
description: Please search [issues](https://github.com/roboflow/supervision/issues) and [discussions](https://github.com/roboflow/supervision/discussions) first.
options:
- label: >
I have searched the Supervision [issues](https://github.com/roboflow/supervision/issues) and found no similar bug report.
- label: I have searched the issues and discussions and found no similar bug report.
required: true
- type: textarea
attributes:
label: Bug
description: Provide console output with error messages and/or screenshots of the bug.
description: Describe the bug, what you expected, and what actually happened.
placeholder: |
💡 ProTip! Include as much information as possible (screenshots, logs, tracebacks etc.) to receive the most helpful response.
**What's the bug?**
Brief description...
**Expected behavior:**
What should happen...
**Actual behavior:**
What actually happens...
**Error/Traceback (if any):**
```python
# Paste error messages here
```
validations:
required: true
- type: textarea
attributes:
label: Environment
description: Please specify the software and hardware you used to produce the bug.
description: Your setup details
placeholder: |
- Supervision: 0.1.0
- OS: Ubuntu 20.04
- Python: 3.8.10
- Supervision: 0.23.0
- Python: 3.10.12
- OS: Ubuntu 22.04
value: |
- Supervision:
- Python:
- OS:
validations:
required: false
required: true
- type: textarea
attributes:
label: Minimal Reproducible Example
description: >
When asking a question, people will be better able to provide help if you provide code that they can easily understand and use to **reproduce** the problem.
This is referred to by community members as creating a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example).
description: Code to reproduce the bug ([guide](https://stackoverflow.com/help/minimal-reproducible-example))
placeholder: |
```
# Code to reproduce your issue here
```python
import supervision as sv
# Your code here
```
validations:
required: false
- type: textarea
attributes:
label: Additional
description: Anything else you would like to share?
- type: checkboxes
attributes:
label: Are you willing to submit a PR?
description: >
(Optional) We encourage you to submit a [Pull Request](https://github.com/roboflow/supervision/pulls) (PR) to help improve Supervision for everyone, especially if you have a good understanding of how to implement a fix or feature.
description: (Optional) We encourage community contributions!
options:
- label: Yes I'd like to help by submitting a PR!
+17
View File
@@ -0,0 +1,17 @@
blank_issues_enabled: false
contact_links:
- name: ❓ Ask a Question
url: https://github.com/roboflow/supervision/discussions/new?category=q-a
about: Ask questions about using Supervision in GitHub Discussions
- name: 📚 Documentation
url: https://supervision.roboflow.com/
about: Check the official Supervision documentation for guides and API references
- name: 💬 GitHub Discussions
url: https://github.com/roboflow/supervision/discussions
about: Join community discussions, share ideas, and get help
- name: 🌟 Show and Tell
url: https://github.com/roboflow/supervision/discussions/categories/show-and-tell
about: Share your projects and creations built with Supervision
- name: 🗣️ Roboflow Discord
url: https://discord.com/invite/GbfgXGJ8Bk
about: Join our Discord to collaborate with other community members
+20 -19
View File
@@ -1,6 +1,6 @@
name: 🤩 Feature Request
description: Suggest a Supervision idea
# title: " "
title: "[Feature]: "
description: Suggest a new feature or improvement
labels: [enhancement]
body:
- type: markdown
@@ -11,39 +11,40 @@ body:
- type: checkboxes
attributes:
label: Search before asking
description: >
Please search the [issues](https://github.com/roboflow/supervision/issues) to see if a similar feature request already exists.
description: Please search [issues](https://github.com/roboflow/supervision/issues) and [discussions](https://github.com/roboflow/supervision/discussions) first.
options:
- label: >
I have searched the Supervision [issues](https://github.com/roboflow/supervision/issues) and found no similar feature requests.
- label: I have searched the issues and discussions and found no similar feature request.
required: true
- type: textarea
attributes:
label: Description
description: A short description of your feature.
label: Feature Description
description: What feature would you like and why?
placeholder: |
What new feature would you like to see in Supervision?
**What:** Describe the feature...
**Why:** What problem does it solve? Who would benefit?
**How (optional):** Any ideas on implementation?
validations:
required: true
- type: textarea
attributes:
label: Use case
description: |
Describe the use case of your feature request. It will help us understand and prioritize the feature request.
label: Example Usage
description: (Optional) Show how you'd like to use this feature
placeholder: |
How would this feature be used, and who would use it?
```python
import supervision as sv
- type: textarea
attributes:
label: Additional
description: Anything else you would like to share?
# Your example usage here
```
validations:
required: false
- type: checkboxes
attributes:
label: Are you willing to submit a PR?
description: >
(Optional) We encourage you to submit a [Pull Request](https://github.com/roboflow/supervision/pulls) (PR) to help improve Supervision for everyone, especially if you have a good understanding of how to implement a fix or feature.
description: (Optional) We encourage community contributions!
options:
- label: Yes I'd like to help by submitting a PR!
-33
View File
@@ -1,33 +0,0 @@
name: ❓ Question
description: Ask a Supervision question
# title: " "
labels: [question]
body:
- type: markdown
attributes:
value: |
Thank you for asking a Supervision ❓ Question!
- type: checkboxes
attributes:
label: Search before asking
description: >
Please search the [issues](https://github.com/roboflow/supervision/issues) to see if a similar feature request already exists.
options:
- label: >
I have searched the Supervision [issues](https://github.com/roboflow/supervision/issues) and found no similar feature requests.
required: true
- type: textarea
attributes:
label: Question
description: What is your question?
placeholder: |
💡 ProTip! Include as much information as possible (screenshots, logs, tracebacks etc.) to receive the most helpful response.
validations:
required: true
- type: textarea
attributes:
label: Additional
description: Anything else you would like to share?
+67
View File
@@ -0,0 +1,67 @@
<details>
<summary>Before submitting</summary>
- [ ] Self-reviewed the code
- [ ] Updated documentation, follow [Google-style](https://google.github.io/styleguide/pyguide.html#383-functions-and-methods)
- [ ] Added docs entry for autogeneration (if new functions/classes)
- [ ] Added/updated tests
- [ ] All tests pass locally
</details>
## Description
<!-- Provide a clear and concise description of your changes -->
## Type of Change
<!-- Mark the relevant option with an "x" and delete the others -->
- 🐛 Bug fix (non-breaking change which fixes an issue)
- ✨ New feature (non-breaking change which adds functionality)
- 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
- 📝 Documentation update
- 🧪 Test update
- 🔨 Refactoring (no functional changes)
- ⚡ Performance improvement
- 🔧 Chore (dependencies, configs, etc.)
## Motivation and Context
<!-- Why is this change required? What problem does it solve? -->
<!-- If it fixes an open issue, please link to the issue here -->
Closes #(issue)
## Changes Made
<!-- List the main changes made in this PR -->
-
-
-
## Testing
<!-- Describe the tests you ran and how to reproduce them -->
- [ ] I have tested this code locally
- [ ] I have added unit tests that prove my fix is effective or that my feature works
- [ ] All new and existing tests pass
## Google Colab (optional)
<!-- If applicable, provide a link to a Google Colab notebook demonstrating the feature/fix -->
<!-- Ensure the notebook is publicly accessible -->
Colab link:
## Screenshots/Videos (optional)
<!-- If applicable, add screenshots or videos to demonstrate the changes -->
## Additional Notes
<!-- Any additional information that reviewers should know -->
+4
View File
@@ -8,6 +8,10 @@ updates:
commit-message:
prefix: ⬆️
target-branch: "develop"
groups:
github-actions:
patterns:
- "*"
# Python
- package-ecosystem: "pip"
directory: "/"
+37
View File
@@ -0,0 +1,37 @@
name: Build Package
permissions:
contents: read
on:
workflow_call:
inputs:
python-version:
required: false
type: string
default: "3.10"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: 🐍 Install uv and set Python version ${{ inputs.python-version }}
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
with:
python-version: ${{ inputs.python-version }}
activate-environment: true
- name: 🏗️ Build source and wheel distributions
run: |
uv pip install -r pyproject.toml --group build
uv build
uv run twine check --strict dist/*
- name: 📤 Upload distribution artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
@@ -1,37 +1,39 @@
name: 🧪 Docs Test WorkFlow 📚
name: Test Docs Build
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
concurrency:
group: docs-test-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
# Restrict permissions by default
permissions:
contents: read # Required for checkout
checks: write # Required for test reporting
jobs:
docs-build-test:
docs-build:
name: Test docs build
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
python-version: ["3.10"]
steps:
- name: 📥 Checkout the repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
- name: 🐍 Install uv and set Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: 🐍 Install uv and set Python
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
with:
python-version: ${{ matrix.python-version }}
python-version: "3.10"
activate-environment: true
- name: 🏗️ Install dependencies
run: |
uv pip install -r pyproject.toml --group docs
run: uv pip install -r pyproject.toml --group docs
- name: 🧪 Test Docs Build
run: uv run mkdocs build --verbose
+87
View File
@@ -0,0 +1,87 @@
name: 🔧 Pytest/Test Workflow
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
permissions:
contents: read
concurrency:
group: pytest-test-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build-pkg:
uses: ./.github/workflows/build-package.yml
run-tests:
name: Import Test and Pytest Run
# needs: build # todo: consider using this build package for testing
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest", "windows-latest", "macos-latest"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
runs-on: ${{ matrix.os }}
steps:
- name: 📥 Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
with:
python-version: ${{ matrix.python-version }}
activate-environment: true
- name: 🚀 Install Packages
run: uv pip install -r pyproject.toml --group dev --group docs --extra metrics
- name: 📦 Run the Import test
run: uv run python -c "import supervision; from supervision import assets; from supervision import metrics; print(supervision.__version__)"
- name: 🧪 Run the Test
run: uv run pytest --cov=supervision --cov-report=xml
- name: Generate Coverage Report
run: |
coverage xml
coverage report
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: "coverage.xml"
flags: cpu,${{ runner.os }},python${{ matrix.python-version }}
env_vars: OS,PYTHON
name: codecov-umbrella
fail_ci_if_error: false
- name: Minimize uv cache
run: uv cache prune --ci
testing-guardian:
runs-on: ubuntu-latest
needs: run-tests
if: always()
steps:
- name: 📋 Display test result
run: echo "${{ needs.run-tests.result }}"
- name: ❌ Fail guardian on test failure
if: needs.run-tests.result == 'failure'
run: exit 1
# Ensure that cancelled or skipped test runs still cause this guardian job to fail,
# using an explicit exit code instead of relying on timeout behavior.
- name: ⚠️ cancelled or skipped...
if: contains(fromJSON('["cancelled", "skipped"]'), needs.run-tests.result)
run: |
echo "run-tests job result is '${{ needs.run-tests.result }}'; failing explicitly."
exit 1
- name: ✅ tests succeeded
if: needs.run-tests.result == 'success'
run: echo "All tests completed successfully in job 'run-tests'."
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Clear cache
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
console.log("Starting cache cleanup...")
@@ -1,22 +0,0 @@
name: Combine Dependabot PRs
on:
schedule:
- cron: "0 1 * * 3" # Wednesday at 01:00
workflow_dispatch: # allows you to manually trigger the workflow
permissions:
contents: write
pull-requests: write
checks: read
jobs:
combine-prs:
name: Combine
runs-on: ubuntu-latest
steps:
- name: combine-prs
id: combine-prs
uses: github/combine-prs@2909f404763c3177a456e052bdb7f2e85d3a7cb3 # v5.2.0
with:
labels: combined-pr
+27
View File
@@ -0,0 +1,27 @@
name: PR Conflict Labeler
on:
# So that PRs touching the same files as the push are updated
push:
branches:
- main
- develop
# So that the `dirtyLabel` is removed if conflicts are resolved
# We recommend `pull_request_target` so that github secrets are available.
# In `pull_request` we wouldn't be able to change labels of fork PRs
pull_request_target:
types: [synchronize]
permissions:
pull-requests: write
issues: write
jobs:
main:
runs-on: ubuntu-latest
steps:
- name: check if prs are dirty
uses: eps1lon/actions-label-merge-conflict@v3
with:
dirtyLabel: "has conflicts"
removeOnDirtyLabel: "has conflicts"
repoToken: "${{ secrets.GITHUB_TOKEN }}"
+14 -26
View File
@@ -20,53 +20,41 @@ permissions:
pull-requests: write # Required for PR comments
jobs:
deploy:
docs-build-deploy:
name: Publish Docs
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
python-version: ["3.10"]
steps:
- name: 📥 Checkout the repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
- name: 🐍 Install uv and set Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: 🐍 Install uv and set Python
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
with:
python-version: ${{ matrix.python-version }}
python-version: "3.10"
activate-environment: true
- name: 🔑 Create GitHub App token (mkdocs)
id: mkdocs_token
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
with:
app-id: ${{ secrets.MKDOCS_APP_ID }}
private-key: ${{ secrets.MKDOCS_PEM }}
owner: roboflow
repositories: mkdocs-material-insiders
- name: 🏗️ Install dependencies
run: |
uv pip install -r pyproject.toml --group docs
# Install mkdocs-material-insiders using the GitHub App token
uv pip install "git+https://roboflow:${{ steps.mkdocs_token.outputs.token }}@github.com/roboflow/mkdocs-material-insiders.git@9.5.49-insiders-4.53.14#egg=mkdocs-material[imaging]"
run: uv pip install -r pyproject.toml --group docs
- name: ⚙️ Configure git for github-actions
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --global user.name "${{ github.actor }}"
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
- name: 🚀 Deploy Development Docs
if: (github.event_name == 'push' && github.ref == 'refs/heads/develop') || github.event_name == 'workflow_dispatch'
env:
MKDOCS_GIT_COMMITTERS_APIKEY: ${{ secrets.GITHUB_TOKEN }}
run: |
MKDOCS_GIT_COMMITTERS_APIKEY=${{ secrets.GITHUB_TOKEN }} uv run mike deploy --push develop
uv run mike deploy --push develop
- 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`)
MKDOCS_GIT_COMMITTERS_APIKEY=${{ secrets.GITHUB_TOKEN }} uv run mike deploy --push --update-aliases $latest_tag latest
uv run mike deploy --push --update-aliases $latest_tag latest
+19 -18
View File
@@ -7,12 +7,23 @@ on:
- "[0-9]+.[0-9]+[0-9]+.[0-9]+b[0-9]"
- "[0-9]+.[0-9]+[0-9]+.[0-9]+rc[0-9]"
workflow_dispatch:
pull_request:
branches: [main, develop]
paths:
- '.github/workflows/build-package.yml'
- '.github/workflows/publish-pre-release.yml'
permissions: {} # Explicitly remove all permissions by default
jobs:
build:
permissions:
contents: read
uses: ./.github/workflows/build-package.yml
publish-pre-release:
name: Publish Pre-release Package
needs: build
runs-on: ubuntu-latest
environment:
name: test
@@ -21,27 +32,17 @@ jobs:
permissions:
id-token: write # Required for PyPI publishing
contents: read # Required for checkout
strategy:
matrix:
python-version: ["3.10"]
steps:
- name: 📥 Checkout the repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: 📥 Download distribution artifacts
uses: actions/download-artifact@v4
with:
python-version: ${{ matrix.python-version }}
activate-environment: true
- name: 🏗️ Build source and wheel distributions
run: |
uv pip install -r pyproject.toml --group build
uv build
uv run twine check --strict dist/*
name: dist
path: dist/
- name: List distribution artifacts
run: ls -lh dist/
- name: 🚀 Publish to PyPi
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
if: github.event_name != 'pull_request'
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
attestations: true
+19 -18
View File
@@ -5,12 +5,23 @@ on:
tags:
- "[0-9]+.[0-9]+[0-9]+.[0-9]"
workflow_dispatch:
pull_request:
branches: [main, develop]
paths:
- '.github/workflows/build-package.yml'
- '.github/workflows/publish-release.yml'
permissions: {} # Explicitly remove all permissions by default
jobs:
build:
permissions:
contents: read
uses: ./.github/workflows/build-package.yml
publish-release:
name: Publish Release Package
needs: build
runs-on: ubuntu-latest
environment:
name: release
@@ -19,27 +30,17 @@ jobs:
permissions:
id-token: write # Required for PyPI publishing
contents: read # Required for checkout
strategy:
matrix:
python-version: ["3.10"]
steps:
- name: 📥 Checkout the repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: 📥 Download distribution artifacts
uses: actions/download-artifact@v4
with:
python-version: ${{ matrix.python-version }}
activate-environment: true
- name: 🏗️ Build source and wheel distributions
run: |
uv pip install -r pyproject.toml --group build
uv build
uv run twine check --strict dist/*
name: dist
path: dist/
- name: List distribution artifacts
run: ls -lh dist/
- name: 🚀 Publish to PyPi
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
if: github.event_name != 'pull_request'
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
attestations: true
+19 -18
View File
@@ -2,12 +2,23 @@ name: Publish Supervision Releases to TestPyPI
on:
workflow_dispatch:
pull_request:
branches: [main, develop]
paths:
- '.github/workflows/build-package.yml'
- '.github/workflows/publish-testpypi.yml'
permissions: {} # Explicitly remove all permissions by default
jobs:
build:
permissions:
contents: read
uses: ./.github/workflows/build-package.yml
publish-testpypi:
name: Publish Release Package
needs: build
runs-on: ubuntu-latest
environment:
name: release
@@ -16,28 +27,18 @@ jobs:
permissions:
id-token: write # Required for PyPI publishing
contents: read # Required for checkout
strategy:
matrix:
python-version: ["3.10"]
steps:
- name: 📥 Checkout the repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: 📥 Download distribution artifacts
uses: actions/download-artifact@v4
with:
python-version: ${{ matrix.python-version }}
activate-environment: true
- name: 🏗️ Build source and wheel distributions
run: |
uv pip install -r pyproject.toml --group build
uv build
uv run twine check --strict dist/*
name: dist
path: dist/
- name: List distribution artifacts
run: ls -lh dist/
- name: 🚀 Publish to Test-PyPi
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
if: github.event_name != 'pull_request'
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
repository-url: https://test.pypi.org/legacy/
attestations: true
-35
View File
@@ -1,35 +0,0 @@
name: 🔧 Pytest/Test Workflow
on:
pull_request:
branches: [main, develop]
jobs:
run-tests:
name: Import Test and Pytest Run
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
runs-on: ${{ matrix.os }}
steps:
- name: 📥 Checkout the repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
with:
python-version: ${{ matrix.python-version }}
activate-environment: true
- name: 🚀 Install Packages
run: uv pip install -r pyproject.toml --group dev --group docs --extra metrics
- name: 🧪 Run the Import test
run: uv run python -c "import supervision; from supervision import assets; from supervision import metrics; print(supervision.__version__)"
- name: 🧪 Run the Test
run: uv run pytest
+20 -26
View File
@@ -1,4 +1,3 @@
ci:
autofix_prs: true
autoupdate_schedule: weekly
@@ -7,10 +6,9 @@ ci:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: trailing-whitespace
exclude: test/.*\.py
- id: check-yaml
exclude: mkdocs.yml
- id: check-executables-have-shebangs
@@ -24,28 +22,32 @@ repos:
- id: end-of-file-fixer
- id: mixed-line-ending
- repo: https://github.com/PyCQA/bandit
rev: '1.8.6'
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.6.0
hooks:
- id: bandit
args: ["-c", "pyproject.toml"]
additional_dependencies: ["bandit[toml]"]
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.24.1
hooks:
- id: validate-pyproject
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.3
rev: v0.14.11
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-check
args: [--fix]
- id: ruff-format
types_or: [ python, pyi, jupyter ]
# - repo: https://github.com/executablebooks/mdformat
# rev: 0.7.17
# hooks:
# - id: mdformat
# additional_dependencies:
# - "mdformat-mkdocs[recommended]>=2.1.0"
# args: ["--number"]
- repo: https://github.com/executablebooks/mdformat
rev: 1.0.0
hooks:
- id: mdformat
additional_dependencies:
- "mdformat-mkdocs[recommended]>=2.1.0"
args: ["--number"]
exclude: ^docs/
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
@@ -53,11 +55,3 @@ repos:
- id: codespell
additional_dependencies:
- tomli
- repo: https://github.com/asottile/pyupgrade
rev: v3.20.0
hooks:
- id: pyupgrade
args: ["--py310-plus"]
additional_dependencies:
- tomli
+5 -5
View File
@@ -21,20 +21,20 @@ community include:
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall
community
community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of
any kind
any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address,
without their explicit permission
without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
professional setting
## Enforcement Responsibilities
+88 -27
View File
@@ -83,18 +83,20 @@ git push -u origin <your_branch_name>
Use conventional commit messages to clearly describe your changes. The format is:
<type>\[optional scope\]: <description>
```
<type>[optional scope]: <description>
```
Common types include:
- feat: A new feature
- fix: A bug fix
- docs: Documentation only changes
- style: Changes that do not affect the meaning of the code (white-space, formatting, etc)
- refactor: A code change that neither fixes a bug nor adds a feature
- perf: A code change that improves performance
- test: Adding missing tests or correcting existing tests
- chore: Changes to the build process or auxiliary tools and libraries
- `feat`: A new feature
- `fix`: A bug fix
- `docs`: Documentation only changes
- `style`: Changes that do not affect the meaning of the code (white-space, formatting, etc)
- `refactor`: A code change that neither fixes a bug nor adds a feature
- `perf`: A code change that improves performance
- `test`: Adding missing tests or correcting existing tests
- `chore`: Changes to the build process or auxiliary tools and libraries
Then, go back to your fork of the `supervision` repository, click "Pull Requests", and click "New Pull Request".
@@ -116,7 +118,7 @@ When creating new functions, please ensure you have the following:
2. Unit tests for the function.
3. Examples in the documentation for the function.
4. Created an entry in our docs to autogenerate the documentation for the function.
5. Please share a Google Colab with minimal code to test new feature or reproduce PR whenever it is possible. Please ensure that Google Colab can be accessed without any issue.
5. Please share a Google Colab with minimal code to test a new feature or reproduce the issue whenever possible. Please ensure that Google Colab can be accessed without any restrictions.
When you submit your Pull Request, you will be asked to sign a Contributor License Agreement (CLA) by the `cla-assistant` GitHub bot. We can only respond to PRs from contributors who have signed the project CLA.
@@ -128,7 +130,7 @@ PRs must pass all tests and linting requirements before they can be merged.
Before starting your work on the project, set up your development environment:
1. Clone your fork of the project (recommended to use shallow clone of develop branch):
1. **Clone your fork of the project:**
**Option A: Recommended for most contributors (shallow clone of develop branch):**
@@ -139,33 +141,50 @@ Before starting your work on the project, set up your development environment:
Replace `YOUR_USERNAME` with your GitHub username.
> Note: Using `--depth 1` creates a shallow clone with minimal history and `-b develop` ensures you start with the development branch. This significantly reduces download size while providing everything needed to contribute.
> **Note**: Using `--depth 1` creates a shallow clone with minimal history and `-b develop` ensures you start with the development branch. This significantly reduces download size while providing everything needed to contribute.
**Option B: Full repository clone (if you need complete history):**
```bash
git clone https://github.com/YOUR_USERNAME/supervision.git
cd supervision
git checkout develop
```
2. Create and activate a virtual environment:
2. **Set up the upstream remote:**
```bash
git remote add upstream https://github.com/roboflow/supervision.git
git fetch upstream
```
3. **Create and activate a virtual environment:**
**On Linux/macOS:**
```bash
python3 -m venv .venv
source .venv/bin/activate
```
3. Install `uv`:
**On Windows:**
```cmd
python -m venv .venv
.venv\Scripts\activate
```
4. **Install `uv`:**
Follow the instructions on the [uv installation page](https://docs.astral.sh/uv/getting-started/installation/).
4. Install project dependencies:
5. **Install project dependencies:**
```bash
uv pip install -r pyproject.toml --extra dev --extra docs --extra metrics
uv pip install -r pyproject.toml --group dev --group docs --extra metrics
```
5. Run pytest to verify the setup:
6. **Verify the setup:**
```bash
uv run pytest
@@ -181,13 +200,29 @@ Furthermore, we have integrated a pre-commit GitHub Action into our workflow. Th
To run the pre-commit tool, follow these steps:
1. Install pre-commit by running the following command: `uv pip install -r pyproject.toml --extra dev`. It will not only install pre-commit but also install all the deps and dev-deps of project
1. **Install pre-commit** (already included if you followed the installation steps above):
2. Once pre-commit is installed, navigate to the project's root directory.
```bash
uv sync --group dev
```
3. Run the command `pre-commit run --all-files`. This will execute the pre-commit hooks configured for this project against the modified files. If any issues are found, the pre-commit tool will provide feedback on how to resolve them. Make the necessary changes and re-run the pre-commit command until all issues are resolved.
2. **Navigate to the project's root directory** (if not already there).
4. You can also install pre-commit as a git hook by executing `pre-commit install`. Every time you do a `git commit` pre-commit run automatically for you.
3. **Run pre-commit checks**:
```bash
uv run pre-commit run --all-files
```
This will execute the pre-commit hooks configured for this project. If any issues are found, the pre-commit tool will provide feedback on how to resolve them. Make the necessary changes and re-run the command until all issues are resolved.
4. **Install pre-commit as a git hook** (optional but recommended):
```bash
uv run pre-commit install
```
This will automatically run pre-commit checks every time you make a `git commit`.
### Docstrings
@@ -197,13 +232,27 @@ All new functions and classes in `supervision` should include docstrings. This i
### Type checking
So far, **there is no type checking with mypy**. See [issue](https://github.com/roboflow-ai/template-python/issues/4).
Currently, there is no systematic type checking with mypy implemented in the project. This is a known limitation that may be addressed in future updates.
## 📝 Documentation
The `supervision` documentation is stored in a folder called `docs`. The project documentation is built using `mkdocs`.
To run the documentation, install the project requirements with `uv pip install -r pyproject.toml --extra dev --extra docs`. Then, run `mkdocs serve` to start the documentation server.
To run the documentation locally:
1. **Install documentation dependencies** (if not already installed):
```bash
uv sync --group docs
```
2. **Start the documentation server**:
```bash
uv run mkdocs serve
```
3. **Access the documentation** at `http://127.0.0.1:8000` in your browser.
You can learn more about mkdocs on the [mkdocs website](https://www.mkdocs.org/).
@@ -216,14 +265,26 @@ submit a PR with your example. Here are some guidelines for submitting a new exa
- Create a new notebook in the [`docs/notebooks`](https://github.com/roboflow/supervision/tree/develop/docs/notebooks) folder.
- Add a link to the new notebook in [`docs/theme/cookbooks.html`](https://github.com/roboflow/supervision/blob/develop/docs/theme/cookbooks.html). Make sure to add the path to the new notebook, as well as a title, labels, author and supervision version.
- Use the [Count Objects Crossing the Line](https://supervision.roboflow.com/develop/notebooks/count-objects-crossing-the-line/) example as a template for your new example.
- Freeze the version of `supervision` you are using.
- Place an appropriate Open in Colab button at the top of the notebook. You can find an example of such a button in the aforementioned `Count Objects Crossing the Line` cookbook.
- Notebook should be self-contained. If you rely on external data ( videos, images, etc.) or libraries, include download and installation commands in the notebook.
- Pin the version of `supervision` you are using in the notebook.
- Place an appropriate "Open in Colab" button at the top of the notebook. You can find an example of such a button in the aforementioned `Count Objects Crossing the Line` cookbook.
- **Notebook should be self-contained**. If you rely on external data (videos, images, etc.) or libraries, include download and installation commands in the notebook.
- Annotate the code with appropriate comments, including links to the documentation describing each of the tools you have used.
## 🧪 Tests
[`pytests`](https://docs.pytest.org/en/7.1.x/) is used to run our tests.
[`pytest`](https://docs.pytest.org/en/7.1.x/) is used to run our tests.
To run tests:
```bash
uv run pytest
```
To run tests with coverage:
```bash
uv run pytest --cov=supervision
```
## 📄 License
+81 -82
View File
@@ -24,7 +24,7 @@
[![discord](https://img.shields.io/discord/1159501506232451173?logo=discord&label=discord&labelColor=fff&color=5865f2&link=https%3A%2F%2Fdiscord.gg%2FGbfgXGJ8Bk)](https://discord.gg/GbfgXGJ8Bk)
[![built-with-material-for-mkdocs](https://img.shields.io/badge/Material_for_MkDocs-526CFE?logo=MaterialForMkDocs&logoColor=white)](https://squidfunk.github.io/mkdocs-material/)
<div align="center">
<div align="center">
<a href="https://trendshift.io/repositories/124" target="_blank"><img src="https://trendshift.io/api/badge/repositories/124" alt="roboflow%2Fsupervision | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
@@ -70,21 +70,21 @@ len(detections)
- inference
Running with [Inference](https://github.com/roboflow/inference) requires a [Roboflow API KEY](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key).
Running with [Inference](https://github.com/roboflow/inference) requires a [Roboflow API KEY](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key).
```python
import cv2
import supervision as sv
from inference import get_model
```python
import cv2
import supervision as sv
from inference import get_model
image = cv2.imread(...)
model = get_model(model_id="yolov8s-640", api_key=<ROBOFLOW API KEY>)
result = model.infer(image)[0]
detections = sv.Detections.from_inference(result)
image = cv2.imread(...)
model = get_model(model_id="yolov8s-640", api_key="ROBOFLOW_API_KEY")
result = model.infer(image)[0]
detections = sv.Detections.from_inference(result)
len(detections)
# 5
```
len(detections)
# 5
```
</details>
@@ -100,9 +100,7 @@ image = cv2.imread(...)
detections = sv.Detections(...)
box_annotator = sv.BoxAnnotator()
annotated_frame = box_annotator.annotate(
scene=image.copy(),
detections=detections)
annotated_frame = box_annotator.annotate(scene=image.copy(), detections=detections)
```
https://github.com/roboflow/supervision/assets/26109316/691e219c-0565-4403-9218-ab5644f39bce
@@ -115,8 +113,8 @@ Supervision provides a set of [utils](https://supervision.roboflow.com/latest/da
import supervision as sv
from roboflow import Roboflow
project = Roboflow().workspace(<WORKSPACE_ID>).project(<PROJECT_ID>)
dataset = project.version(<PROJECT_VERSION>).download("coco")
project = Roboflow().workspace("WORKSPACE_ID").project("PROJECT_ID")
dataset = project.version("PROJECT_VERSION").download("coco")
ds = sv.DetectionDataset.from_coco(
images_directory_path=f"{dataset.location}/train",
@@ -124,10 +122,11 @@ ds = sv.DetectionDataset.from_coco(
)
path, image, annotation = ds[0]
# loads image on demand
# loads image on demand
for path, image, annotation in ds:
# loads image on demand
pass
```
<details close>
@@ -135,88 +134,88 @@ for path, image, annotation in ds:
- load
```python
dataset = sv.DetectionDataset.from_yolo(
images_directory_path=...,
annotations_directory_path=...,
data_yaml_path=...
)
```python
dataset = sv.DetectionDataset.from_yolo(
images_directory_path=...,
annotations_directory_path=...,
data_yaml_path=...,
)
dataset = sv.DetectionDataset.from_pascal_voc(
images_directory_path=...,
annotations_directory_path=...
)
dataset = sv.DetectionDataset.from_pascal_voc(
images_directory_path=...,
annotations_directory_path=...,
)
dataset = sv.DetectionDataset.from_coco(
images_directory_path=...,
annotations_path=...
)
```
dataset = sv.DetectionDataset.from_coco(
images_directory_path=...,
annotations_path=...,
)
```
- split
```python
train_dataset, test_dataset = dataset.split(split_ratio=0.7)
test_dataset, valid_dataset = test_dataset.split(split_ratio=0.5)
```python
train_dataset, test_dataset = dataset.split(split_ratio=0.7)
test_dataset, valid_dataset = test_dataset.split(split_ratio=0.5)
len(train_dataset), len(test_dataset), len(valid_dataset)
# (700, 150, 150)
```
len(train_dataset), len(test_dataset), len(valid_dataset)
# (700, 150, 150)
```
- merge
```python
ds_1 = sv.DetectionDataset(...)
len(ds_1)
# 100
ds_1.classes
# ['dog', 'person']
```python
ds_1 = sv.DetectionDataset(...)
len(ds_1)
# 100
ds_1.classes
# ['dog', 'person']
ds_2 = sv.DetectionDataset(...)
len(ds_2)
# 200
ds_2.classes
# ['cat']
ds_2 = sv.DetectionDataset(...)
len(ds_2)
# 200
ds_2.classes
# ['cat']
ds_merged = sv.DetectionDataset.merge([ds_1, ds_2])
len(ds_merged)
# 300
ds_merged.classes
# ['cat', 'dog', 'person']
```
ds_merged = sv.DetectionDataset.merge([ds_1, ds_2])
len(ds_merged)
# 300
ds_merged.classes
# ['cat', 'dog', 'person']
```
- save
```python
dataset.as_yolo(
images_directory_path=...,
annotations_directory_path=...,
data_yaml_path=...
)
```python
dataset.as_yolo(
images_directory_path=...,
annotations_directory_path=...,
data_yaml_path=...,
)
dataset.as_pascal_voc(
images_directory_path=...,
annotations_directory_path=...
)
dataset.as_pascal_voc(
images_directory_path=...,
annotations_directory_path=...,
)
dataset.as_coco(
images_directory_path=...,
annotations_path=...
)
```
dataset.as_coco(
images_directory_path=...,
annotations_path=...,
)
```
- convert
```python
sv.DetectionDataset.from_yolo(
images_directory_path=...,
annotations_directory_path=...,
data_yaml_path=...
).as_pascal_voc(
images_directory_path=...,
annotations_directory_path=...
)
```
```python
sv.DetectionDataset.from_yolo(
images_directory_path=...,
annotations_directory_path=...,
data_yaml_path=...,
).as_pascal_voc(
images_directory_path=...,
annotations_directory_path=...,
)
```
</details>
+87 -7
View File
@@ -1,5 +1,85 @@
# Changelog
### 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.
- Added [#2006](https://github.com/roboflow/supervision/pull/2006): [`sv.xyxy_to_mask`](https://supervision.roboflow.com/0.27.0/detection/utils/converters/#supervision.detection.utils.converters.xyxy_to_mask) to convert bounding boxes into 2D boolean masks, where each mask corresponds to a single box.
- Added [#1943](https://github.com/roboflow/supervision/pull/1943): [`sv.tint_image`](https://supervision.roboflow.com/0.27.0/utils/image/#supervision.utils.image.tint_image) to apply a solid color overlay to an image at a given opacity. Works with both NumPy and PIL inputs.
- Added [#1943](https://github.com/roboflow/supervision/pull/1943): [`sv.grayscale_image`](https://supervision.roboflow.com/0.27.0/utils/image/#supervision.utils.image.tint_image) to convert an image to 3 channel grayscale for compatibility with color based drawing utilities.
- Added [#2014](https://github.com/roboflow/supervision/pull/2014): [`sv.get_image_resolution_wh`](https://supervision.roboflow.com/0.27.0/utils/image/#supervision.utils.image.get_image_resolution_wh) as a unified way to read image width and height from NumPy and PIL inputs.
- Added [#1912](https://github.com/roboflow/supervision/pull/1912): [`sv.edit_distance`](https://supervision.roboflow.com/0.27.0/detection/utils/vlms/#supervision.detection.utils.vlms.edit_distance) for Levenshtein distance between two strings. Supports insert, delete, and substitute operations.
- Added [#1912](https://github.com/roboflow/supervision/pull/1912): [`sv.fuzzy_match_index`](https://supervision.roboflow.com/0.27.0/detection/utils/vlms/#supervision.detection.utils.vlms.fuzzy_match_index) to find the first close match in a list using edit distance.
- Changed [#2015](https://github.com/roboflow/supervision/pull/2015): [`sv.Detections.from_vlm`](https://supervision.roboflow.com/0.27.0/detection/core/#supervision.detection.core.Detections.from_vlm) and legacy `from_lmm` now support Qwen3 VL via `vlm=sv.VLM.QWEN_3_VL`.
- Changed [#1884](https://github.com/roboflow/supervision/pull/1884): [`sv.Detections.from_vlm`](https://supervision.roboflow.com/0.27.0/detection/core/#supervision.detection.core.Detections.from_vlm) and legacy `from_lmm` now support DeepSeek VL 2 via `vlm=sv.VLM.DEEPSEEK_VL_2`.
- Changed [#2015](https://github.com/roboflow/supervision/pull/2015): [`sv.Detections.from_vlm`](https://supervision.roboflow.com/0.27.0/detection/core/#supervision.detection.core.Detections.from_vlm) now parses Qwen 2.5 VL outputs more robustly and handles incomplete or truncated JSON responses.
- Changed [#2014](https://github.com/roboflow/supervision/pull/2014): [`sv.InferenceSlicer`](https://supervision.roboflow.com/0.27.0/detection/tools/inference_slicer/#supervision.detection.tools.inference_slicer.InferenceSlicer) now uses a new offset generation logic that removes redundant tiles and aligns borders cleanly. This reduces the number of processed tiles and shortens inference time without hurting detection quality.
- Changed [#2016](https://github.com/roboflow/supervision/pull/2016): [`sv.Detections`](https://supervision.roboflow.com/0.27.0/detection/core/#supervision.detection.core.Detections) now includes a `box_aspect_ratio` property for vectorized aspect ratio computation, useful for filtering detections based on box shape.
- Changed [#2001](https://github.com/roboflow/supervision/pull/2001): Significantly improved the performance of [`sv.box_iou_batch`](https://supervision.roboflow.com/0.27.0/detection/utils/iou_and_nms/#supervision.detection.utils.iou_and_nms.box_iou_batch). On internal benchmarks, processing runs approximately 2x to 5x faster.
- Changed [#1997](https://github.com/roboflow/supervision/pull/1997): [`sv.process_video`](https://supervision.roboflow.com/0.27.0/utils/video/#supervision.utils.video.process_video) now uses a threaded reader, processor, and writer pipeline. This removes I/O stalls and improves throughput while keeping the callback single threaded and safe for stateful models.
- Changed: [`sv.denormalize_boxes`](https://supervision.roboflow.com/0.27.0/detection/utils/boxes/#supervision.detection.utils.boxes.denormalize_boxes) now supports batch conversion of bounding boxes. The function accepts arrays of shape `(N, 4)` and returns a batch of absolute pixel coordinates.
- Changed [#1917](https://github.com/roboflow/supervision/pull/1917): [`sv.LabelAnnotator`](https://supervision.roboflow.com/develop/0.27.0/annotators/#supervision.annotators.core.LabelAnnotator) and [`sv.RichLabelAnnotator`](https://supervision.roboflow.com/develop/0.27.0/annotators/#supervision.annotators.core.LabelAnnotator) now accept `text_offset=(x, y)` to shift the label relative to `text_position`. Works with smart label position and line wrapping.
!!! failure "Removed"
Removed the deprecated `overlap_ratio_wh` argument from `sv.InferenceSlicer`. Use the pixel based `overlap_wh` argument to control slice overlap.
!!! info "Tip"
Convert your old ratio based overlap to pixel based overlap by multiplying each ratio by the slice dimensions.
```python
# before
slice_wh = (640, 640)
overlap_ratio_wh = (0.25, 0.25)
slicer = sv.InferenceSlicer(
callback=callback,
slice_wh=slice_wh,
overlap_ratio_wh=overlap_ratio_wh,
overlap_filter=sv.OverlapFilter.NON_MAX_SUPPRESSION,
)
# after
overlap_wh = (
int(overlap_ratio_wh[0] * slice_wh[0]),
int(overlap_ratio_wh[1] * slice_wh[1]),
)
slicer = sv.InferenceSlicer(
callback=callback,
slice_wh=slice_wh,
overlap_wh=overlap_wh,
overlap_filter=sv.OverlapFilter.NON_MAX_SUPPRESSION,
)
```
### 0.26.1 <small>Jul 22, 2025</small>
- Fixed [1894](https://github.com/roboflow/supervision/pull/1894): Error in [`sv.MeanAveragePrecision`](https://supervision.roboflow.com/0.26.1/metrics/mean_average_precision/#supervision.metrics.mean_average_precision.MeanAveragePrecision) where the area used for size-specific evaluation (small / medium / large) was always zero unless explicitly provided in `sv.Detections.data`.
- Fixed [1895](https://github.com/roboflow/supervision/pull/1895): `ID=0` bug in [`sv.MeanAveragePrecision`](https://supervision.roboflow.com/0.26.1/metrics/mean_average_precision/#supervision.metrics.mean_average_precision.MeanAveragePrecision) where objects were getting `0.0` mAP despite perfect IoU matches due to a bug in annotation ID assignment.
- Fixed [1898](https://github.com/roboflow/supervision/pull/1898): Issue where [`sv.MeanAveragePrecision`](https://supervision.roboflow.com/0.26.1/metrics/mean_average_precision/#supervision.metrics.mean_average_precision.MeanAveragePrecision) could return negative values when certain object size categories have no data.
- Fixed [1901](https://github.com/roboflow/supervision/pull/1901): `match_metric` support for [`sv.Detections.with_nms`](https://supervision.roboflow.com/0.26.1/metrics/mean_average_precision/#supervision.detection.core.Detections.with_nms).
- Fixed [1906](https://github.com/roboflow/supervision/pull/1906): `border_thickness` parameter usage for [`sv.PercentageBarAnnotator`](https://supervision.roboflow.com/0.26.1/metrics/mean_average_precision/#supervision.annotators.core.PercentageBarAnnotator).
### 0.26.0 <small>Jul 16, 2025</small>
!!! failure "Removed"
@@ -153,7 +233,7 @@
- Changed [#1786](https://github.com/roboflow/supervision/pull/1786): Significantly improved the speed of HSV color mapping in [`sv.HeatMapAnnotator`](https://supervision.roboflow.com/0.26.0/detection/annotators/#supervision.annotators.core.HeatMapAnnotator), achieving approximately 28x faster performance on 1920x1080 frames.
- Fix [#1834](https://github.com/roboflow/supervision/pull/1834): Supervision’s [`sv.MeanAveragePrecision`](https://supervision.roboflow.com/0.26.0/metrics/mean_average_precision/#supervision.metrics.mean_average_precision.MeanAveragePrecision) is now fully aligned with [pycocotools](https://github.com/ppwwyyxx/cocoapi), the official COCO evaluation tool, ensuring accurate and standardized metrics. This update enabled us to launch a new version of the [Computer Vision Model Leaderboard](https://leaderboard.roboflow.com/).
- Fixed [#1834](https://github.com/roboflow/supervision/pull/1834): Supervision’s [`sv.MeanAveragePrecision`](https://supervision.roboflow.com/0.26.0/metrics/mean_average_precision/#supervision.metrics.mean_average_precision.MeanAveragePrecision) is now fully aligned with [pycocotools](https://github.com/ppwwyyxx/cocoapi), the official COCO evaluation tool, ensuring accurate and standardized metrics. This update enabled us to launch a new version of the [Computer Vision Model Leaderboard](https://leaderboard.roboflow.com/).
```python
import supervision as sv
@@ -173,7 +253,7 @@
# Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.629
```
- Fix [#1767](https://github.com/roboflow/supervision/pull/1767): Fixed losing `sv.Detections.data` when detections filtering.
- Fixed [#1767](https://github.com/roboflow/supervision/pull/1767): Fixed losing `sv.Detections.data` when detections filtering.
### 0.25.0 <small>Nov 12, 2024</small>
@@ -557,9 +637,9 @@ detections = sv.Detections.from_sam(sam_result=sam_result)
- Changed [#1434](https://github.com/roboflow/supervision/pull/1434): [`InferenceSlicer`](https://supervision.roboflow.com/0.23.0/detection/tools/inference_slicer/) now features an `overlap_wh` parameter, making it easier to compute slice sizes when handling overlapping slices.
- Fix [#1448](https://github.com/roboflow/supervision/pull/1448): Various annotator type issues have been resolved, supporting expanded error handling.
- Fixed [#1448](https://github.com/roboflow/supervision/pull/1448): Various annotator type issues have been resolved, supporting expanded error handling.
- Fix [#1348](https://github.com/roboflow/supervision/pull/1348): Introduced a new method for [seeking to a specific video frame](https://supervision.roboflow.com/0.23.0/utils/video/#supervision.utils.video.get_video_frames_generator), addressing cases where traditional seek methods were failing. It can be enabled with `iterative_seek=True`.
- Fixed [#1348](https://github.com/roboflow/supervision/pull/1348): Introduced a new method for [seeking to a specific video frame](https://supervision.roboflow.com/0.23.0/utils/video/#supervision.utils.video.get_video_frames_generator), addressing cases where traditional seek methods were failing. It can be enabled with `iterative_seek=True`.
```python
import supervision as sv
@@ -572,7 +652,7 @@ for frame in sv.get_video_frames_generator(
...
```
- Fix [#1424](https://github.com/roboflow/supervision/pull/1424): `plot_image` function now clearly indicates that the size is in inches.
- Fixed [#1424](https://github.com/roboflow/supervision/pull/1424): `plot_image` function now clearly indicates that the size is in inches.
!!! failure "Removed"
@@ -1285,7 +1365,7 @@ array([
### 0.11.1 <small>June 29, 2023</small>
- Fix [#165](https://github.com/roboflow/supervision/pull/165): [`as_folder_structure`](/0.11.1/dataset/core/#supervision.dataset.core.ClassificationDataset.as_folder_structure) fails to save [`sv.ClassificationDataset`](/0.11.1/dataset/core/#classificationdataset) when it is result of inference.
- Fixed [#165](https://github.com/roboflow/supervision/pull/165): [`as_folder_structure`](/0.11.1/dataset/core/#supervision.dataset.core.ClassificationDataset.as_folder_structure) fails to save [`sv.ClassificationDataset`](/0.11.1/dataset/core/#classificationdataset) when it is result of inference.
### 0.11.0 <small>June 28, 2023</small>
@@ -1331,7 +1411,7 @@ array([
- Added [#162](https://github.com/roboflow/supervision/pull/162): additional `start` and `end` arguments to [`sv.get_video_frames_generator`](/0.11.0/utils/video/#get_video_frames_generator) allowing to generate frames only for a selected part of the video.
- Fix [#157](https://github.com/roboflow/supervision/pull/157): incorrect loading of YOLO dataset class names from `data.yaml`.
- Fixed [#157](https://github.com/roboflow/supervision/pull/157): incorrect loading of YOLO dataset class names from `data.yaml`.
### 0.10.0 <small>June 14, 2023</small>
-1
View File
@@ -1,6 +1,5 @@
---
comments: true
status: new
---
# Annotators
-1
View File
@@ -1,6 +1,5 @@
---
comments: true
status: new
---
# Detections
-1
View File
@@ -1,6 +1,5 @@
---
comments: true
status: new
---
# Boxes Utils
+6
View File
@@ -58,3 +58,9 @@ status: new
</div>
:::supervision.detection.utils.converters.polygon_to_xyxy
<div class="md-typeset">
<h2><a href="#supervision.detection.utils.converters.xyxy_to_mask">xyxy_to_mask</a></h2>
</div>
:::supervision.detection.utils.converters.xyxy_to_mask
-1
View File
@@ -1,6 +1,5 @@
---
comments: true
status: new
---
# IoU and NMS Utils
+6
View File
@@ -22,3 +22,9 @@ status: new
</div>
:::supervision.detection.utils.masks.contains_multiple_segments
<div class="md-typeset">
<h2><a href="#supervision.detection.utils.masks.filter_segments_by_distance">filter_segments_by_distance</a></h2>
</div>
:::supervision.detection.utils.masks.filter_segments_by_distance
-1
View File
@@ -1,6 +1,5 @@
---
comments: true
status: new
---
# Polygons Utils
+18
View File
@@ -0,0 +1,18 @@
---
comments: true
status: new
---
# VLMs Utils
<div class="md-typeset">
<h2><a href="#supervision.detection.utils.vlms.edit_distance">edit_distance</a></h2>
</div>
:::supervision.detection.utils.vlms.edit_distance
<div class="md-typeset">
<h2><a href="#supervision.detection.utils.vlms.fuzzy_match_index">fuzzy_match_index</a></h2>
</div>
:::supervision.detection.utils.vlms.fuzzy_match_index
-1
View File
@@ -1,6 +1,5 @@
---
comments: true
status: new
---
![Corgi Example](https://media.roboflow.com/supervision/image-examples/how-to/benchmark-models/corgi-sorted-2.png)
+1 -4
View File
@@ -331,12 +331,9 @@ for i in range(16):
annotated_image = label_annotator.annotate(annotated_image, annotations, labels)
annotated_images.append(annotated_image)
grid = sv.create_tiles(
sv.plot_images_grid(
annotated_images,
grid_size=(4, 4),
single_tile_size=(400, 400),
tile_padding_color=sv.Color.WHITE,
tile_margin_color=sv.Color.WHITE
)
```
+3 -3
View File
@@ -234,7 +234,7 @@ with
model = get_model(model_id="yolov8n-640")
frames_generator = sv.get_video_frames_generator(<SOURCE_VIDEO_PATH>)
with sv.JSONSink(<TARGET_CSV_PATH>) as sink:
with sv.JSONSink(<TARGET_JSON_PATH>) as sink:
for frame_index, frame in enumerate(frames_generator):
results = model.infer(image)[0]
@@ -250,7 +250,7 @@ with
model = YOLO("yolov8n.pt")
frames_generator = sv.get_video_frames_generator(<SOURCE_VIDEO_PATH>)
with sv.JSONSink(<TARGET_CSV_PATH>) as sink:
with sv.JSONSink(<TARGET_JSON_PATH>) as sink:
for frame_index, frame in enumerate(frames_generator):
results = model(frame)[0]
@@ -268,7 +268,7 @@ with
model = DetrForObjectDetection.from_pretrained("facebook/detr-resnet-50")
frames_generator = sv.get_video_frames_generator(<SOURCE_VIDEO_PATH>)
with sv.JSONSink(<TARGET_CSV_PATH>) as sink:
with sv.JSONSink(<TARGET_JSON_PATH>) as sink:
for frame_index, frame in enumerate(frames_generator):
frame = sv.cv2_to_pillow(frame)
+2 -3
View File
@@ -1,6 +1,5 @@
---
comments: true
status: new
---
# Track Objects
@@ -345,7 +344,7 @@ Supervision is versatile and compatible with various models. Check this [link](/
We will define a `callback` function, which will process each frame of the video by obtaining model predictions and then annotating the frame based on these predictions.
Let's immediately visualize the results with our [`EdgeAnnotator`](/latest/keypoint/annotators/#supervision.keypoint.annotators.EdgeAnnotator) and [`VertexAnnotator`](https://supervision.roboflow.com/latest/keypoint/annotators/#supervision.keypoint.annotators.VertexAnnotator).
Let's immediately visualize the results with our [`EdgeAnnotator`](/latest/keypoint/annotators/#supervision.key_points.annotators.EdgeAnnotator) and [`VertexAnnotator`](https://supervision.roboflow.com/latest/keypoint/annotators/#supervision.key_points.annotators.VertexAnnotator).
=== "Ultralytics"
@@ -408,7 +407,7 @@ Let's immediately visualize the results with our [`EdgeAnnotator`](/latest/keypo
### Convert to Detections
Keypoint tracking is currently supported via the conversion of `KeyPoints` to `Detections`. This is achieved with the [`KeyPoints.as_detections()`](/latest/keypoint/core/#supervision.keypoint.core.KeyPoints.as_detections) function.
Keypoint tracking is currently supported via the conversion of `KeyPoints` to `Detections`. This is achieved with the [`KeyPoints.as_detections()`](/latest/keypoint/core/#supervision.key_points.core.KeyPoints.as_detections) function.
Let's convert to detections and visualize the results with our [`BoxAnnotator`](/latest/detection/annotators/#supervision.annotators.core.BoxAnnotator).
+6 -6
View File
@@ -78,19 +78,19 @@ comments: true
</div>
<div class="md-typeset">
<h2><a href="#supervision.keypoint.annotators.VertexAnnotator">VertexAnnotator</a></h2>
<h2><a href="#supervision.key_points.annotators.VertexAnnotator">VertexAnnotator</a></h2>
</div>
:::supervision.keypoint.annotators.VertexAnnotator
:::supervision.key_points.annotators.VertexAnnotator
<div class="md-typeset">
<h2><a href="#supervision.keypoint.annotators.EdgeAnnotator">EdgeAnnotator</a></h2>
<h2><a href="#supervision.key_points.annotators.EdgeAnnotator">EdgeAnnotator</a></h2>
</div>
:::supervision.keypoint.annotators.EdgeAnnotator
:::supervision.key_points.annotators.EdgeAnnotator
<div class="md-typeset">
<h2><a href="#supervision.keypoint.annotators.VertexLabelAnnotator">VertexLabelAnnotator</a></h2>
<h2><a href="#supervision.key_points.annotators.VertexLabelAnnotator">VertexLabelAnnotator</a></h2>
</div>
:::supervision.keypoint.annotators.VertexLabelAnnotator
:::supervision.key_points.annotators.VertexLabelAnnotator
+1 -2
View File
@@ -1,8 +1,7 @@
---
comments: true
status: new
---
# Keypoint Detection
:::supervision.keypoint.core.KeyPoints
:::supervision.key_points.core.KeyPoints
-1
View File
@@ -1,6 +1,5 @@
---
comments: true
status: new
---
# Mean Average Precision
+15 -2
View File
@@ -1,5 +1,6 @@
---
comments: true
status: new
---
# Image Utils
@@ -29,10 +30,22 @@ comments: true
:::supervision.utils.image.letterbox_image
<div class="md-typeset">
<h2><a href="#supervision.utils.image.overlay_image">overlay_image</a></h2>
<h2><a href="#supervision.utils.image.tint_image">tint_image</a></h2>
</div>
:::supervision.utils.image.overlay_image
:::supervision.utils.image.tint_image
<div class="md-typeset">
<h2><a href="#supervision.utils.image.grayscale_image">grayscale_image</a></h2>
</div>
:::supervision.utils.image.grayscale_image
<div class="md-typeset">
<h2><a href="#supervision.utils.image.get_image_resolution_wh">get_image_resolution_wh</a></h2>
</div>
:::supervision.utils.image.get_image_resolution_wh
<div class="md-typeset">
<h2><a href="#supervision.utils.image.ImageSink">ImageSink</a></h2>
+2 -2
View File
@@ -21,7 +21,7 @@ https://github.com/roboflow/supervision/assets/26109316/f84db7b5-79e2-4142-a1da-
cd supervision/examples/count_people_in_zone
```
- setup python environment and activate it \[optional\]
- setup python environment and activate it [optional]
```bash
python3 -m venv venv
@@ -110,7 +110,7 @@ https://github.com/roboflow/supervision/assets/26109316/f84db7b5-79e2-4142-a1da-
```bash
python inference_example.py \
--roboflow_api_key <ROBOFLOW API KEY> \
--roboflow_api_key "ROBOFLOW_API_KEY" \
--zone_configuration_path data/multi-zone-config.json \
--source_video_path data/market-square.mp4 \
--confidence_threshold 0.3 \
+1 -1
View File
@@ -15,7 +15,7 @@ supervision package for multiple tasks such as drawing heatmap annotations, trac
cd supervision/examples/heatmap_and_track
```
- setup python environment and activate it \[optional\]
- setup python environment and activate it [optional]
```bash
python3 -m venv venv
+3 -3
View File
@@ -11,7 +11,7 @@ supervision package for multiple tasks such as tracking, annotations, etc.
https://github.com/roboflow/supervision/assets/26109316/d50118c1-2ae4-458d-915a-5d860fd36f71
> \[!IMPORTANT\]
> [!IMPORTANT]
> Adjust the [`SOURCE`](https://github.com/roboflow/supervision/blob/e32b05a636dab2ea1f39299e529c4b22b8baa8da/examples/speed_estimation/ultralytics_example.py#L10)
> and [`TARGET`](https://github.com/roboflow/supervision/blob/e32b05a636dab2ea1f39299e529c4b22b8baa8da/examples/speed_estimation/ultralytics_example.py#L15)
> configuration if you plan to run a speed estimation script on your video file. Those must be adjusted separately for each camera view. You can learn more
@@ -26,7 +26,7 @@ https://github.com/roboflow/supervision/assets/26109316/d50118c1-2ae4-458d-915a-
cd supervision/examples/speed_estimation
```
- setup python environment and activate it \[optional\]
- setup python environment and activate it [optional]
```bash
python3.10 -m venv venv
@@ -90,7 +90,7 @@ https://github.com/roboflow/supervision/assets/26109316/d50118c1-2ae4-458d-915a-
```bash
python inference_example.py \
--roboflow_api_key <ROBOFLOW API KEY> \
--roboflow_api_key "ROBOFLOW_API_KEY" \
--source_video_path data/vehicles.mp4 \
--target_video_path data/vehicles-result.mp4 \
--confidence_threshold 0.3 \
+1 -1
View File
@@ -20,7 +20,7 @@ https://github.com/roboflow/supervision/assets/26109316/d051cc8a-dd15-41d4-aa36-
cd supervision/examples/time_in_zone
```
- setup python environment and activate it \[optional\]
- setup python environment and activate it [optional]
```bash
python3 -m venv venv
+3 -1
View File
@@ -1,4 +1,6 @@
supervision
ultralytics
inference
pytube
# https://github.com/pytube/pytube/issues/2044
# pytube
pytubefix
@@ -3,7 +3,7 @@ from __future__ import annotations
import argparse
import os
from pytube import YouTube
from pytubefix import YouTube
def main(url: str, output_path: str | None, file_name: str | None) -> None:
@@ -77,7 +77,7 @@ def run_command_in_thread(command: list) -> Thread:
def run_command(command: list) -> int:
process = subprocess.run(command)
process = subprocess.run(command) # noqa: S603 # TODO: Validate command input to prevent execution of untrusted input
return process.returncode
+2 -2
View File
@@ -14,7 +14,7 @@ detection and Supervision for tracking and annotation.
cd supervision/examples/tracking
```
- setup python environment and activate it \[optional\]
- setup python environment and activate it [optional]
```bash
python3 -m venv venv
@@ -79,7 +79,7 @@ detection and Supervision for tracking and annotation.
```bash
python inference_example.py \
--roboflow_api_key <ROBOFLOW API KEY> \
--roboflow_api_key "ROBOFLOW_API_KEY" \
--source_video_path input.mp4 \
--target_video_path tracking_result.mp4
```
+2 -2
View File
@@ -17,7 +17,7 @@ https://github.com/roboflow/supervision/assets/26109316/c9436828-9fbf-4c25-ae8c-
cd supervision/examples/traffic_analysis
```
- setup python environment and activate it \[optional\]
- setup python environment and activate it [optional]
```bash
python3 -m venv venv
@@ -101,7 +101,7 @@ https://github.com/roboflow/supervision/assets/26109316/c9436828-9fbf-4c25-ae8c-
```bash
python inference_example.py \
--roboflow_api_key <ROBOFLOW API KEY> \
--roboflow_api_key "ROBOFLOW_API_KEY" \
--source_video_path data/traffic_analysis.mov \
--confidence_threshold 0.3 \
--iou_threshold 0.5 \
+2 -1
View File
@@ -26,7 +26,7 @@ extra:
extra_css:
- stylesheets/extra.css
- stylesheets/cookbooks-card.css
- stylesheets/cookbooks_card.css
nav:
- Home: index.md
@@ -47,6 +47,7 @@ nav:
- Boxes: detection/utils/boxes.md
- Masks: detection/utils/masks.md
- Polygons: detection/utils/polygons.md
- VLMs: detection/utils/vlms.md
- Keypoint Detection:
- Core: keypoint/core.md
- Annotators: keypoint/annotators.md
+151 -195
View File
@@ -1,218 +1,184 @@
[build-system]
build-backend = "setuptools.build_meta"
requires = [ "setuptools>=61" ]
[project]
name = "supervision"
version = "0.28.0rc0"
description = "A set of easy-to-use utils that will come in handy in any Computer Vision project"
license = { text = "MIT" }
version = "0.26.0"
readme = "README.md"
requires-python = ">=3.9"
authors = [
{ name = "Piotr Skalski", email = "piotr.skalski92@gmail.com" }
]
maintainers = [
{ name = "Piotr Skalski", email = "piotr.skalski92@gmail.com" },
]
keywords = [
"machine-learning",
"deep-learning",
"vision",
"ML",
"DL",
"AI",
"Roboflow",
"AI",
"deep-learning",
"DL",
"machine-learning",
"ML",
"Roboflow",
"vision",
]
license = { text = "MIT" }
maintainers = [
{ name = "Piotr Skalski", email = "piotr@roboflow.com" },
]
authors = [
{ name = "Roboflow et al.", email = "develop@roboflow.com" },
]
requires-python = ">=3.9"
classifiers = [
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Intended Audience :: Education',
'Intended Audience :: Science/Research',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3 :: Only',
'Topic :: Software Development',
'Topic :: Scientific/Engineering',
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Topic :: Scientific/Engineering :: Image Recognition",
'Typing :: Typed',
'Operating System :: Microsoft :: Windows',
'Operating System :: POSIX :: Linux',
'Operating System :: MacOS',
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: Education",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Operating System :: MacOS",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Multimedia :: Graphics",
"Topic :: Multimedia :: Video",
"Topic :: Scientific/Engineering",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Topic :: Scientific/Engineering :: Image Recognition",
"Topic :: Software Development",
"Typing :: Typed",
]
dependencies = [
"numpy>=1.21.2",
"scipy>=1.10.0",
"matplotlib>=3.6.0",
"pyyaml>=5.3",
"defusedxml>=0.7.1",
"pillow>=9.4",
"requests>=2.26.0",
"tqdm>=4.62.3",
"opencv-python>=4.5.5.64"
"defusedxml>=0.7.1",
"matplotlib>=3.6",
"numpy>=1.21.2",
"opencv-python>=4.5.5.64",
"pillow>=9.4",
"pyyaml>=5.3",
"requests>=2.26",
"scipy>=1.10",
"tqdm>=4.62.3",
]
[project.urls]
Homepage = "https://github.com/roboflow/supervision"
Repository = "https://github.com/roboflow/supervision"
Documentation = "https://supervision.roboflow.com/latest/"
[project.optional-dependencies]
metrics = [
"pandas>=2.0.0",
optional-dependencies.metrics = [
"pandas>=2",
]
urls.Documentation = "https://supervision.roboflow.com/latest/"
urls.Homepage = "https://github.com/roboflow/supervision"
urls.Repository = "https://github.com/roboflow/supervision"
[dependency-groups]
dev = [
"pytest>=7.2.2,<9.0.0",
"tox>=4.11.4",
"notebook>=6.5.3,<8.0.0",
"ipywidgets>=8.1.1",
"jupytext>=1.16.1",
"nbconvert>=7.14.2",
"docutils!=0.21"
"docutils!=0.21",
"ipywidgets>=8.1.1",
"jupytext>=1.16.1",
"nbconvert>=7.14.2",
"notebook>=6.5.3,<8",
"pre-commit>=3.8",
"pytest>=7.2.2,<9",
"pytest-cov>=4,<8",
"tox>=4.11.4",
]
docs = [
"mkdocs-material[imaging]>=9.5.5",
"mkdocstrings>=0.25.2,<0.30.0",
"mkdocstrings-python>=1.10.9",
"mike>=2.0.0",
"mkdocs-jupyter>=0.24.3",
"mkdocs-git-committers-plugin-2>=2.4.1; python_version >= '3.9' and python_version < '4'",
"mkdocs-git-revision-date-localized-plugin>=1.2.4"
"mike>=2",
"mkdocs-git-committers-plugin-2>=2.4.1; python_version>='3.9' and python_version<'4'",
"mkdocs-git-revision-date-localized-plugin>=1.2.4",
"mkdocs-jupyter>=0.24.3",
"mkdocs-material[imaging]>=9.7",
"mkdocstrings>=0.25.2,<0.31",
"mkdocstrings-python>=1.10.9,<2", # todo: breaking changes in 2.x
]
build = [
"twine>=5.1.1,<7.0.0",
"wheel>=0.40,<0.46",
"build>=0.10,<1.3"
"build>=0.10,<1.5",
"twine>=5.1.1,<7",
"wheel>=0.40,<0.46",
]
[tool.bandit]
target = ["test", "supervision"]
tests = ["B201", "B301", "B318", "B314", "B303", "B413", "B412"]
[tool.setuptools]
include-package-data = false
[tool.autoflake]
check = true
imports = ["cv2", "supervision"]
[tool.setuptools.packages.find]
include = [ "supervision*" ]
exclude = [ "docs*", "test*", "examples*" ]
[tool.setuptools.package-data]
supervision = [ "py.typed" ]
[tool.ruff]
target-version = "py39"
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
"yarn-error.log",
"yarn.lock",
"docs",
]
line-length = 88
indent-width = 4
[tool.ruff.lint]
# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
select = ["E", "F", "I", "A", "Q", "W", "RUF", "UP"]
ignore = []
# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"I",
"N",
"Q",
"S",
"T",
"W",
"ANN",
"ARG",
"BLE",
"COM",
"DJ",
"DTZ",
"EM",
"ERA",
"EXE",
"FBT",
"ICN",
"INP",
"ISC",
"NPY",
"PD",
"PGH",
"PIE",
"PL",
"PT",
"PTH",
"PYI",
"RET",
"RSE",
"RUF",
"SIM",
"SLF",
"TCH",
"TID",
"TRY",
"UP",
"YTT",
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"docs",
"node_modules",
"venv",
"yarn-error.log",
"yarn.lock",
]
unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
pylint.max-args = 20
[tool.ruff.lint.flake8-quotes]
inline-quotes = "double"
multiline-quotes = "double"
docstring-quotes = "double"
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["E402", "F401"]
[tool.ruff.lint.mccabe]
# Flag errors (`C901`) whenever the complexity level exceeds 5.
max-complexity = 20
[tool.ruff.lint.isort]
order-by-type = true
no-sections = false
[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
format.indent-style = "space"
# Like Black, use double quotes for strings.
format.quote-style = "double"
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
format.line-ending = "auto"
# Like Black, respect magic trailing commas.
format.skip-magic-trailing-comma = false
# Enable linting rules for code style, imports, and best practices.
lint.select = [
"A", # flake8-builtins - https://docs.astral.sh/ruff/rules/#flake8-builtins-a
"E", # pycodestyle errors - https://docs.astral.sh/ruff/rules/#error-e
"F", # Pyflakes - https://docs.astral.sh/ruff/rules/#pyflakes-f
"I", # isort - https://docs.astral.sh/ruff/rules/#isort-i
"PT", # pytest - https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt
"Q", # flake8-quotes - https://docs.astral.sh/ruff/rules/#flake8-quotes-q
"RUF", # Ruff-specific rules - https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf
"S", # bandit - https://docs.astral.sh/ruff/rules/#flake8-bandit-s
"UP", # pyupgrade - https://docs.astral.sh/ruff/rules/#pyupgrade-up
"W", # pycodestyle warnings - https://docs.astral.sh/ruff/rules/#pycodestyle-w
]
lint.ignore = [ ]
lint.per-file-ignores."__init__.py" = [ "E402", "F401" ]
lint.per-file-ignores."supervision/**" = [
"S101", # TODO: Replace asserts with proper error handling
]
lint.per-file-ignores."test/**" = [
"PT011", # TODO: `pytest.raises(Exception)` is too broad, set the `match` parameter or use a more specific exception
"S101", # Use of `assert` detected
]
lint.unfixable = [ ]
# Allow unused variables when underscore-prefixed.
lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
lint.flake8-quotes.docstring-quotes = "double"
lint.flake8-quotes.inline-quotes = "double"
lint.flake8-quotes.multiline-quotes = "double"
lint.isort.no-sections = false
lint.isort.order-by-type = true
# Flag errors (`C901`) whenever the complexity level exceeds 5.
lint.mccabe.max-complexity = 20
lint.pydocstyle.convention = "google"
lint.pylint.max-args = 20
[tool.codespell]
skip = "*.ipynb"
@@ -220,16 +186,6 @@ count = true
quiet-level = 3
ignore-words-list = "STrack,sTrack,strack"
[tool.setuptools]
include-package-data = false
[tool.setuptools.packages.find]
include = ["supervision*"]
exclude = ["docs*", "test*", "examples*"]
[tool.setuptools.package-data]
supervision = ["py.typed"]
[build-system]
requires = ["setuptools >= 61.0"]
build-backend = "setuptools.build_meta"
[tool.autoflake]
check = true
imports = [ "cv2", "supervision" ]
+15 -4
View File
@@ -65,6 +65,7 @@ from supervision.detection.utils.converters import (
polygon_to_xyxy,
xcycwh_to_xyxy,
xywh_to_xyxy,
xyxy_to_mask,
xyxy_to_polygons,
xyxy_to_xcycarh,
xyxy_to_xywh,
@@ -86,12 +87,14 @@ from supervision.detection.utils.masks import (
calculate_masks_centroids,
contains_holes,
contains_multiple_segments,
filter_segments_by_distance,
move_masks,
)
from supervision.detection.utils.polygons import (
approximate_polygon,
filter_polygons_by_area,
)
from supervision.detection.utils.vlms import edit_distance, fuzzy_match_index
from supervision.detection.vlm import LMM, VLM
from supervision.draw.color import Color, ColorPalette
from supervision.draw.utils import (
@@ -107,24 +110,26 @@ from supervision.draw.utils import (
)
from supervision.geometry.core import Point, Position, Rect
from supervision.geometry.utils import get_polygon_center
from supervision.keypoint.annotators import (
from supervision.key_points.annotators import (
EdgeAnnotator,
VertexAnnotator,
VertexLabelAnnotator,
)
from supervision.keypoint.core import KeyPoints
from supervision.key_points.core import KeyPoints
from supervision.metrics.detection import ConfusionMatrix, MeanAveragePrecision
from supervision.tracker.byte_tracker.core import ByteTrack
from supervision.utils.conversion import cv2_to_pillow, pillow_to_cv2
from supervision.utils.file import list_files_with_extensions
from supervision.utils.image import (
ImageSink,
create_tiles,
crop_image,
get_image_resolution_wh,
grayscale_image,
letterbox_image,
overlay_image,
resize_image,
scale_image,
tint_image,
)
from supervision.utils.notebook import plot_image, plot_images_grid
from supervision.utils.video import (
@@ -205,7 +210,6 @@ __all__ = [
"clip_boxes",
"contains_holes",
"contains_multiple_segments",
"create_tiles",
"crop_image",
"cv2_to_pillow",
"draw_filled_polygon",
@@ -215,10 +219,15 @@ __all__ = [
"draw_polygon",
"draw_rectangle",
"draw_text",
"edit_distance",
"filter_polygons_by_area",
"filter_segments_by_distance",
"fuzzy_match_index",
"get_coco_class_index_mapping",
"get_image_resolution_wh",
"get_polygon_center",
"get_video_frames_generator",
"grayscale_image",
"letterbox_image",
"list_files_with_extensions",
"mask_iou_batch",
@@ -242,8 +251,10 @@ __all__ = [
"rle_to_mask",
"scale_boxes",
"scale_image",
"tint_image",
"xcycwh_to_xyxy",
"xywh_to_xyxy",
"xyxy_to_mask",
"xyxy_to_polygons",
"xyxy_to_xcycarh",
"xyxy_to_xywh",
+1 -13
View File
@@ -1,19 +1,7 @@
from abc import ABC, abstractmethod
from typing import TypeVar
import numpy as np
from PIL import Image
from supervision.detection.core import Detections
ImageType = TypeVar("ImageType", np.ndarray, Image.Image)
"""
An image of type `np.ndarray` or `PIL.Image.Image`.
Unlike a `Union`, ensures the type remains consistent. If a function
takes an `ImageType` argument and returns an `ImageType`, when you
pass an `np.ndarray`, you will get an `np.ndarray` back.
"""
from supervision.draw.base import ImageType
class BaseAnnotator(ABC):
+210 -55
View File
@@ -7,8 +7,9 @@ import cv2
import numpy as np
import numpy.typing as npt
from PIL import Image, ImageDraw, ImageFont
from scipy.interpolate import splev, splprep
from supervision.annotators.base import BaseAnnotator, ImageType
from supervision.annotators.base import BaseAnnotator
from supervision.annotators.utils import (
PENDING_TRACK_ID,
ColorLookup,
@@ -28,12 +29,13 @@ from supervision.detection.utils.converters import (
polygon_to_mask,
xyxy_to_polygons,
)
from supervision.draw.base import ImageType
from supervision.draw.color import Color, ColorPalette
from supervision.draw.utils import draw_polygon, draw_rounded_rectangle, draw_text
from supervision.geometry.core import Point, Position, Rect
from supervision.utils.conversion import (
ensure_cv2_image_for_annotation,
ensure_pil_image_for_annotation,
ensure_cv2_image_for_class_method,
ensure_pil_image_for_class_method,
)
from supervision.utils.image import (
crop_image,
@@ -51,25 +53,28 @@ class _BaseLabelAnnotator(BaseAnnotator):
Attributes:
color (Union[Color, ColorPalette]): The color to use for the label background.
color_lookup (ColorLookup): The method used to determine the color of the label.
text_color (Union[Color, ColorPalette]): The color to use for the label text.
text_padding (int): The padding around the label text, in pixels.
text_anchor (Position): The position of the text relative to the detection
bounding box.
color_lookup (ColorLookup): The method used to determine the color of the label.
bounding box.
text_offset (Tuple[int, int]): A tuple of 2D coordinates `(x, y)` to
offset the text position from the anchor point, in pixels.
border_radius (int): The radius of the label background corners, in pixels.
smart_position (bool): Whether to intelligently adjust the label position to
avoid overlapping with other elements.
avoid overlapping with other elements.
max_line_length (Optional[int]): Maximum number of characters per line before
wrapping the text. None means no wrapping.
wrapping the text. None means no wrapping.
"""
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color_lookup: ColorLookup = ColorLookup.CLASS,
text_color: Color | ColorPalette = Color.WHITE,
text_padding: int = 10,
text_position: Position = Position.TOP_LEFT,
color_lookup: ColorLookup = ColorLookup.CLASS,
text_offset: tuple[int, int] = (0, 0),
border_radius: int = 0,
smart_position: bool = False,
max_line_length: int | None = None,
@@ -79,27 +84,29 @@ class _BaseLabelAnnotator(BaseAnnotator):
Args:
color (Union[Color, ColorPalette], optional): The color to use for the label
background.
background.
color_lookup (ColorLookup, optional): The method used to determine the color
of the label
text_color (Union[Color, ColorPalette], optional): The color to use for the
label text.
label text.
text_padding (int, optional): The padding around the label text, in pixels.
text_position (Position, optional): The position of the text relative to the
detection bounding box.
color_lookup (ColorLookup, optional): The method used to determine the color
of the label
detection bounding box.
text_offset (Tuple[int, int], optional): A tuple of 2D coordinates
`(x, y)` to offset the text position from the anchor point, in pixels.
border_radius (int, optional): The radius of the label background corners,
in pixels.
in pixels.
smart_position (bool, optional): Whether to intelligently adjust the label
position to avoid overlapping with other elements.
position to avoid overlapping with other elements.
max_line_length (Optional[int], optional): Maximum number of characters per
line before wrapping the text. None means no wrapping.
line before wrapping the text. None means no wrapping.
"""
self.color: Color | ColorPalette = color
self.color_lookup: ColorLookup = color_lookup
self.text_color: Color | ColorPalette = text_color
self.text_padding: int = text_padding
self.text_anchor: Position = text_position
self.color_lookup: ColorLookup = color_lookup
self.text_offset: tuple[int, int] = text_offset
self.border_radius: int = border_radius
self.smart_position = smart_position
self.max_line_length: int | None = max_line_length
@@ -171,7 +178,7 @@ class BoxAnnotator(BaseAnnotator):
self.thickness: int = thickness
self.color_lookup: ColorLookup = color_lookup
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType,
@@ -254,7 +261,7 @@ class OrientedBoxAnnotator(BaseAnnotator):
self.thickness: int = thickness
self.color_lookup: ColorLookup = color_lookup
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType,
@@ -343,7 +350,7 @@ class MaskAnnotator(BaseAnnotator):
self.opacity = opacity
self.color_lookup: ColorLookup = color_lookup
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType,
@@ -433,7 +440,7 @@ class PolygonAnnotator(BaseAnnotator):
self.thickness: int = thickness
self.color_lookup: ColorLookup = color_lookup
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType,
@@ -520,7 +527,7 @@ class ColorAnnotator(BaseAnnotator):
self.color_lookup: ColorLookup = color_lookup
self.opacity = opacity
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType,
@@ -616,7 +623,7 @@ class HaloAnnotator(BaseAnnotator):
self.color_lookup: ColorLookup = color_lookup
self.kernel_size: int = kernel_size
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType,
@@ -716,7 +723,7 @@ class EllipseAnnotator(BaseAnnotator):
self.end_angle: int = end_angle
self.color_lookup: ColorLookup = color_lookup
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType,
@@ -757,7 +764,7 @@ class EllipseAnnotator(BaseAnnotator):
"""
assert isinstance(scene, np.ndarray)
for detection_idx in range(len(detections)):
x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
x1, _y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
color = resolve_color(
color=self.color,
detections=detections,
@@ -808,7 +815,7 @@ class BoxCornerAnnotator(BaseAnnotator):
self.corner_length: int = corner_length
self.color_lookup: ColorLookup = color_lookup
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType,
@@ -897,7 +904,7 @@ class CircleAnnotator(BaseAnnotator):
self.thickness: int = thickness
self.color_lookup: ColorLookup = color_lookup
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType,
@@ -996,7 +1003,7 @@ class DotAnnotator(BaseAnnotator):
self.outline_thickness = outline_thickness
self.outline_color: Color | ColorPalette = outline_color
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType,
@@ -1076,37 +1083,100 @@ class LabelAnnotator(_BaseLabelAnnotator):
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color_lookup: ColorLookup = ColorLookup.CLASS,
text_color: Color | ColorPalette = Color.WHITE,
text_scale: float = 0.5,
text_thickness: int = 1,
text_padding: int = 10,
text_position: Position = Position.TOP_LEFT,
color_lookup: ColorLookup = ColorLookup.CLASS,
text_offset: tuple[int, int] = (0, 0),
border_radius: int = 0,
smart_position: bool = False,
max_line_length: int | None = None,
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating the text background.
color_lookup (ColorLookup): Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
text_color (Union[Color, ColorPalette]): The color or color palette to use
for the text.
text_scale (float): Font scale for the text.
text_thickness (int): Thickness of the text characters.
text_padding (int): Padding around the text within its background box.
text_position (Position): Position of the text relative to the detection.
Possible values are defined in the `Position` enum.
text_offset (Tuple[int, int]): A tuple of 2D coordinates `(x, y)` to
offset the text position from the anchor point, in pixels.
border_radius (int): The radius to apply round edges. If the selected
value is higher than the lower dimension, width or height, is clipped.
smart_position (bool): Spread out the labels to avoid overlapping.
max_line_length (Optional[int]): Maximum number of characters per line
before wrapping the text. None means no wrapping.
"""
self.text_scale: float = text_scale
self.text_thickness: int = text_thickness
super().__init__(
color=color,
color_lookup=color_lookup,
text_color=text_color,
text_padding=text_padding,
text_position=text_position,
color_lookup=color_lookup,
text_offset=text_offset,
border_radius=border_radius,
smart_position=smart_position,
max_line_length=max_line_length,
)
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType, # Ensure scene is initially a NumPy array here
scene: ImageType,
detections: Detections,
labels: list[str] | None = None,
custom_color_lookup: np.ndarray | None = None,
) -> np.ndarray:
"""
Annotates the given scene with labels based on the provided detections.
Args:
scene (ImageType): The image where labels will be drawn.
`ImageType` is a flexible type, accepting either `numpy.ndarray`
or `PIL.Image.Image`.
detections (Detections): Object detections to annotate.
labels (Optional[List[str]]): Custom labels for each detection.
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
Allows to override the default color mapping strategy.
Returns:
The annotated image, matching the type of `scene` (`numpy.ndarray`
or `PIL.Image.Image`)
Example:
```python
import supervision as sv
image = ...
detections = sv.Detections(...)
labels = [
f"{class_name} {confidence:.2f}"
for class_name, confidence
in zip(detections['class_name'], detections.confidence)
]
label_annotator = sv.LabelAnnotator(text_position=sv.Position.CENTER)
annotated_frame = label_annotator.annotate(
scene=image.copy(),
detections=detections,
labels=labels
)
```
![label-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/label-annotator-example-purple.png)
"""
assert isinstance(scene, np.ndarray)
validate_labels(labels, detections)
@@ -1144,7 +1214,12 @@ class LabelAnnotator(_BaseLabelAnnotator):
anchor=self.text_anchor
).astype(int)
for label, center_coords in zip(labels, anchors_coordinates):
for label, center_coordinates in zip(labels, anchors_coordinates):
center_coordinates = (
center_coordinates[0] + self.text_offset[0],
center_coordinates[1] + self.text_offset[1],
)
wrapped_lines = wrap_text(label, self.max_line_length)
line_heights = []
line_widths = []
@@ -1170,7 +1245,7 @@ class LabelAnnotator(_BaseLabelAnnotator):
height_padded = total_height + 2 * self.text_padding
text_background_xyxy = resolve_text_background_xyxy(
center_coordinates=tuple(center_coords),
center_coordinates=center_coordinates,
text_wh=(width_padded, height_padded),
position=self.text_anchor,
)
@@ -1317,31 +1392,54 @@ class RichLabelAnnotator(_BaseLabelAnnotator):
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color_lookup: ColorLookup = ColorLookup.CLASS,
text_color: Color | ColorPalette = Color.WHITE,
font_path: str | None = None,
font_size: int = 10,
text_padding: int = 10,
text_position: Position = Position.TOP_LEFT,
color_lookup: ColorLookup = ColorLookup.CLASS,
text_offset: tuple[int, int] = (0, 0),
border_radius: int = 0,
smart_position: bool = False,
max_line_length: int | None = None,
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating the text background.
color_lookup (ColorLookup): Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
text_color (Union[Color, ColorPalette]): The color to use for the text.
font_path (Optional[str]): Path to the font file (e.g., ".ttf" or ".otf")
to use for rendering text. If `None`, the default PIL font will be used.
font_size (int): Font size for the text.
text_padding (int): Padding around the text within its background box.
text_position (Position): Position of the text relative to the detection.
Possible values are defined in the `Position` enum.
text_offset (Tuple[int, int]): A tuple of 2D coordinates `(x, y)` to
offset the text position from the anchor point, in pixels.
border_radius (int): The radius to apply round edges. If the selected
value is higher than the lower dimension, width or height, is clipped.
smart_position (bool): Spread out the labels to avoid overlapping.
max_line_length (Optional[int]): Maximum number of characters per line
before wrapping the text. None means no wrapping.
"""
self.font_path = font_path
self.font_size = font_size
self.font = self._load_font(font_size, font_path)
super().__init__(
color=color,
color_lookup=color_lookup,
text_color=text_color,
text_padding=text_padding,
text_position=text_position,
color_lookup=color_lookup,
text_offset=text_offset,
border_radius=border_radius,
smart_position=smart_position,
max_line_length=max_line_length,
)
@ensure_pil_image_for_annotation
@ensure_pil_image_for_class_method
def annotate(
self,
scene: ImageType,
@@ -1349,6 +1447,44 @@ class RichLabelAnnotator(_BaseLabelAnnotator):
labels: list[str] | None = None,
custom_color_lookup: np.ndarray | None = None,
) -> ImageType:
"""
Annotates the given scene with labels based on the provided
detections, with support for Unicode characters.
Args:
scene (ImageType): The image where labels will be drawn.
`ImageType` is a flexible type, accepting either `numpy.ndarray`
or `PIL.Image.Image`.
detections (Detections): Object detections to annotate.
labels (Optional[List[str]]): Custom labels for each detection.
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
Allows to override the default color mapping strategy.
Returns:
The annotated image, matching the type of `scene` (`numpy.ndarray`
or `PIL.Image.Image`)
Example:
```python
import supervision as sv
image = ...
detections = sv.Detections(...)
labels = [
f"{class_name} {confidence:.2f}"
for class_name, confidence
in zip(detections['class_name'], detections.confidence)
]
rich_label_annotator = sv.RichLabelAnnotator(font_path="path/to/font.ttf")
annotated_frame = label_annotator.annotate(
scene=image.copy(),
detections=detections,
labels=labels
)
```
"""
assert isinstance(scene, Image.Image)
validate_labels(labels, detections)
@@ -1386,7 +1522,12 @@ class RichLabelAnnotator(_BaseLabelAnnotator):
anchor=self.text_anchor
).astype(int)
for label, center_coords in zip(labels, anchor_coordinates):
for label, center_coordinates in zip(labels, anchor_coordinates):
center_coordinates = (
center_coordinates[0] + self.text_offset[0],
center_coordinates[1] + self.text_offset[1],
)
wrapped_lines = wrap_text(label, self.max_line_length)
# Calculate the total text height and maximum width
@@ -1409,7 +1550,7 @@ class RichLabelAnnotator(_BaseLabelAnnotator):
height_padded = int(total_height + 2 * self.text_padding)
text_background_xyxy = resolve_text_background_xyxy(
center_coordinates=tuple(center_coords),
center_coordinates=center_coordinates,
text_wh=(width_padded, height_padded),
position=self.text_anchor,
)
@@ -1480,7 +1621,7 @@ class RichLabelAnnotator(_BaseLabelAnnotator):
)
# Move to the next line position
left, top, right, bottom = draw.textbbox((0, 0), line, font=self.font)
_left, top, _right, bottom = draw.textbbox((0, 0), line, font=self.font)
line_height = bottom - top
y_position += line_height + self.text_padding
@@ -1525,7 +1666,7 @@ class IconAnnotator(BaseAnnotator):
self.position = icon_position
self.offset_xy = offset_xy
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self, scene: ImageType, detections: Detections, icon_path: str | list[str]
) -> ImageType:
@@ -1614,7 +1755,7 @@ class BlurAnnotator(BaseAnnotator):
"""
self.kernel_size: int = kernel_size
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType,
@@ -1681,6 +1822,7 @@ class TraceAnnotator(BaseAnnotator):
position: Position = Position.CENTER,
trace_length: int = 30,
thickness: int = 2,
smooth: bool = False,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
"""
@@ -1692,15 +1834,17 @@ class TraceAnnotator(BaseAnnotator):
trace_length (int): The maximum length of the trace in terms of historical
points. Defaults to `30`.
thickness (int): The thickness of the trace lines. Defaults to `2`.
smooth (bool): Smooth the trace lines.
color_lookup (ColorLookup): Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Color | ColorPalette = color
self.trace = Trace(max_size=trace_length, anchor=position)
self.thickness = thickness
self.smooth = smooth
self.color_lookup: ColorLookup = color_lookup
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType,
@@ -1769,10 +1913,18 @@ class TraceAnnotator(BaseAnnotator):
else custom_color_lookup,
)
xy = self.trace.get(tracker_id=tracker_id)
spline_points = xy.astype(np.int32)
if len(xy) > 3 and self.smooth:
x, y = xy[:, 0], xy[:, 1]
tck, _u = splprep([x, y], s=20)
x_new, y_new = splev(np.linspace(0, 1, 100), tck)
spline_points = np.stack([x_new, y_new], axis=1).astype(np.int32)
if len(xy) > 1:
scene = cv2.polylines(
scene,
[xy.astype(np.int32)],
[spline_points],
False,
color=color.as_bgr(),
thickness=self.thickness,
@@ -1814,7 +1966,7 @@ class HeatMapAnnotator(BaseAnnotator):
self.low_hue = low_hue
self.heat_mask: npt.NDArray[np.float32] | None = None
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(self, scene: ImageType, detections: Detections) -> ImageType:
"""
Annotates the scene with a heatmap based on the provided detections.
@@ -1896,7 +2048,7 @@ class PixelateAnnotator(BaseAnnotator):
"""
self.pixel_size: int = pixel_size
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType,
@@ -1993,7 +2145,7 @@ class TriangleAnnotator(BaseAnnotator):
self.outline_thickness: int = outline_thickness
self.outline_color: Color | ColorPalette = outline_color
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType,
@@ -2105,7 +2257,7 @@ class RoundBoxAnnotator(BaseAnnotator):
raise ValueError("roundness attribute must be float between (0, 1.0]")
self.roundness: float = roundness
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType,
@@ -2239,10 +2391,13 @@ class PercentageBarAnnotator(BaseAnnotator):
self.position: Position = position
self.color_lookup: ColorLookup = color_lookup
if border_thickness is None:
self.border_thickness = int(0.15 * self.height)
self.border_thickness = (
border_thickness
if border_thickness is not None
else int(0.15 * self.height)
)
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType,
@@ -2423,7 +2578,7 @@ class CropAnnotator(BaseAnnotator):
self.border_thickness: int = border_thickness
self.border_color_lookup: ColorLookup = border_color_lookup
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self,
scene: ImageType,
@@ -2572,7 +2727,7 @@ class BackgroundOverlayAnnotator(BaseAnnotator):
self.opacity = opacity
self.force_box = force_box
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(self, scene: ImageType, detections: Detections) -> ImageType:
"""
Applies a colored overlay to the scene outside of the detected regions.
@@ -2670,7 +2825,7 @@ class ComparisonAnnotator:
self.label_scale = label_scale
self.text_thickness = int(self.label_scale + 1.2)
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(
self, scene: ImageType, detections_1: Detections, detections_2: Detections
) -> ImageType:
+22 -14
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import textwrap
from enum import Enum
from typing import Any
import numpy as np
@@ -142,35 +143,45 @@ def resolve_color(
detection_idx=detection_idx,
color_lookup=color_lookup,
)
if color_lookup == ColorLookup.TRACK and idx == PENDING_TRACK_ID:
if (
isinstance(color_lookup, ColorLookup)
and color_lookup == ColorLookup.TRACK
and idx == PENDING_TRACK_ID
):
return PENDING_TRACK_COLOR
return get_color_by_index(color=color, idx=idx)
def wrap_text(text: str, max_line_length=None) -> list[str]:
def wrap_text(text: Any, max_line_length=None) -> list[str]:
"""
Wraps text to the specified maximum line length, respecting existing newlines.
Uses the textwrap library for robust text wrapping.
Wrap `text` to the specified maximum line length, respecting existing
newlines. Falls back to str() if `text` is not already a string.
Args:
text (str): The text to wrap.
text (Any): The text (or object) to wrap.
max_line_length (int | None): Maximum width for each wrapped line.
Returns:
List[str]: A list of text lines after wrapping.
list[str]: Wrapped lines.
"""
if not text:
return [""]
if not isinstance(text, str):
text = str(text)
if max_line_length is None:
return text.splitlines() or [""]
if max_line_length <= 0:
raise ValueError("max_line_length must be a positive integer")
paragraphs = text.split("\n")
all_lines = []
all_lines: list[str] = []
for paragraph in paragraphs:
if not paragraph:
# Keep empty lines
if paragraph == "":
all_lines.append("")
continue
@@ -182,12 +193,9 @@ def wrap_text(text: str, max_line_length=None) -> list[str]:
drop_whitespace=True,
)
if wrapped:
all_lines.extend(wrapped)
else:
all_lines.append("")
all_lines.extend(wrapped or [""])
return all_lines if all_lines else [""]
return all_lines or [""]
def validate_labels(labels: list[str] | None, detections: Detections):
+8 -4
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
import os
from hashlib import new as hash_new
from hashlib import md5
from pathlib import Path
from shutil import copyfileobj
@@ -15,6 +15,9 @@ def is_md5_hash_matching(filename: str, original_md5_hash: str) -> bool:
"""
Check if the MD5 hash of a file matches the original hash.
Note: MD5 is used here for file integrity checking (detecting corruption),
not for cryptographic security purposes.
Parameters:
filename (str): The path to the file to be checked as a string.
original_md5_hash (str): The original MD5 hash to compare against.
@@ -27,8 +30,7 @@ def is_md5_hash_matching(filename: str, original_md5_hash: str) -> bool:
with open(filename, "rb") as file:
file_contents = file.read()
computed_md5_hash = hash_new(name="MD5")
computed_md5_hash.update(file_contents)
computed_md5_hash = md5(file_contents, usedforsecurity=False)
return computed_md5_hash.hexdigest() == original_md5_hash
@@ -57,7 +59,9 @@ def download_assets(asset_name: VideoAssets | str) -> str:
if not Path(filename).exists() and filename in VIDEO_ASSETS:
print(f"Downloading {filename} assets \n")
response = get(VIDEO_ASSETS[filename][0], stream=True, allow_redirects=True)
response = get(
VIDEO_ASSETS[filename][0], stream=True, allow_redirects=True, timeout=30
)
response.raise_for_status()
file_size = int(response.headers.get("Content-Length", 0))
+219 -16
View File
@@ -40,12 +40,14 @@ from supervision.detection.utils.masks import calculate_masks_centroids
from supervision.detection.vlm import (
LMM,
VLM,
from_deepseek_vl_2,
from_florence_2,
from_google_gemini_2_0,
from_google_gemini_2_5,
from_moondream,
from_paligemma,
from_qwen_2_5_vl,
from_qwen_3_vl,
validate_vlm_parameters,
)
from supervision.geometry.core import Position
@@ -295,18 +297,24 @@ class Detections:
class_id=np.arange(len(ultralytics_results)),
)
class_id = ultralytics_results.boxes.cls.cpu().numpy().astype(int)
class_names = np.array([ultralytics_results.names[i] for i in class_id])
return cls(
xyxy=ultralytics_results.boxes.xyxy.cpu().numpy(),
confidence=ultralytics_results.boxes.conf.cpu().numpy(),
class_id=class_id,
mask=extract_ultralytics_masks(ultralytics_results),
tracker_id=ultralytics_results.boxes.id.int().cpu().numpy()
if ultralytics_results.boxes.id is not None
else None,
data={CLASS_NAME_DATA_FIELD: class_names},
)
if (
hasattr(ultralytics_results, "boxes")
and ultralytics_results.boxes is not None
):
class_id = ultralytics_results.boxes.cls.cpu().numpy().astype(int)
class_names = np.array([ultralytics_results.names[i] for i in class_id])
return cls(
xyxy=ultralytics_results.boxes.xyxy.cpu().numpy(),
confidence=ultralytics_results.boxes.conf.cpu().numpy(),
class_id=class_id,
mask=extract_ultralytics_masks(ultralytics_results),
tracker_id=ultralytics_results.boxes.id.int().cpu().numpy()
if ultralytics_results.boxes.id is not None
else None,
data={CLASS_NAME_DATA_FIELD: class_names},
)
return cls.empty()
@classmethod
def from_yolo_nas(cls, yolo_nas_results) -> Detections:
@@ -830,6 +838,7 @@ class Detections:
| Google Gemini 2.0 | `GOOGLE_GEMINI_2_0` | detection | `resolution_wh` | `classes` |
| Google Gemini 2.5 | `GOOGLE_GEMINI_2_5` | detection, segmentation | `resolution_wh` | `classes` |
| Moondream | `MOONDREAM` | detection | `resolution_wh` | |
| DeepSeek-VL2 | `DEEPSEEK_VL_2` | detection | `resolution_wh` | `classes` |
Args:
lmm (Union[LMM, str]): The type of LMM (Large Multimodal Model) to use.
@@ -943,6 +952,36 @@ class Detections:
# array([0, 1])
```
!!! example "Qwen3-VL"
```python
import supervision as sv
qwen_3_vl_result = \"\"\"```json
[
{"bbox_2d": [139, 768, 315, 954], "label": "cat"},
{"bbox_2d": [366, 679, 536, 849], "label": "dog"}
]
```\"\"\"
detections = sv.Detections.from_lmm(
sv.LMM.QWEN_3_VL,
qwen_3_vl_result,
resolution_wh=(1000, 1000),
classes=['cat', 'dog'],
)
detections.xyxy
# array([[139., 768., 315., 954.], [366., 679., 536., 849.]])
detections.class_id
# array([0, 1])
detections.data
# {'class_name': array(['cat', 'dog'], dtype='<U10')}
detections.class_id
# array([0, 1])
```
!!! example "Gemini 2.0"
```python
import supervision as sv
@@ -1117,6 +1156,47 @@ class Detections:
# array([[1752.28, 818.82, 2165.72, 1229.14],
# [1908.01, 1346.67, 2585.99, 2024.11]])
```
!!! example "DeepSeek-VL2"
??? tip "Prompt engineering"
To get the best results from DeepSeek-VL2, use optimized prompts that leverage
its object detection and visual grounding capabilities effectively.
**For general object detection, use the following user prompt:**
```
<image>\\n<|ref|>The giraffe at the front<|/ref|>
```
**For visual grounding, use the following user prompt:**
```
<image>\\n<|grounding|>Detect the giraffes
```
```python
from PIL import Image
import supervision as sv
deepseek_vl2_result = "<|ref|>The giraffe at the back<|/ref|><|det|>[[580, 270, 999, 904]]<|/det|><|ref|>The giraffe at the front<|/ref|><|det|>[[26, 31, 632, 998]]<|/det|><|end▁of▁sentence|>"
detections = sv.Detections.from_vlm(
vlm=sv.VLM.DEEPSEEK_VL_2, result=deepseek_vl2_result, resolution_wh=image.size
)
detections.xyxy
# array([[ 420, 293, 724, 982],
# [ 18, 33, 458, 1084]])
detections.class_id
# array([0, 1])
detections.data
# {'class_name': array(['The giraffe at the back', 'The giraffe at the front'], dtype='<U24')}
```
""" # noqa: E501
# filler logic mapping old from_lmm to new from_vlm
@@ -1124,6 +1204,7 @@ class Detections:
LMM.PALIGEMMA: VLM.PALIGEMMA,
LMM.FLORENCE_2: VLM.FLORENCE_2,
LMM.QWEN_2_5_VL: VLM.QWEN_2_5_VL,
LMM.DEEPSEEK_VL_2: VLM.DEEPSEEK_VL_2,
LMM.GOOGLE_GEMINI_2_0: VLM.GOOGLE_GEMINI_2_0,
LMM.GOOGLE_GEMINI_2_5: VLM.GOOGLE_GEMINI_2_5,
}
@@ -1161,9 +1242,11 @@ class Detections:
| PaliGemma | `PALIGEMMA` | detection | `resolution_wh` | `classes` |
| PaliGemma 2 | `PALIGEMMA` | detection | `resolution_wh` | `classes` |
| Qwen2.5-VL | `QWEN_2_5_VL` | detection | `resolution_wh`, `input_wh` | `classes` |
| Qwen3-VL | `QWEN_3_VL` | detection | `resolution_wh`, | `classes` |
| Google Gemini 2.0 | `GOOGLE_GEMINI_2_0` | detection | `resolution_wh` | `classes` |
| Google Gemini 2.5 | `GOOGLE_GEMINI_2_5` | detection, segmentation | `resolution_wh` | `classes` |
| Moondream | `MOONDREAM` | detection | `resolution_wh` | |
| DeepSeek-VL2 | `DEEPSEEK_VL_2` | detection | `resolution_wh` | `classes` |
Args:
vlm (Union[VLM, str]): The type of VLM (Vision Language Model) to use.
@@ -1277,6 +1360,36 @@ class Detections:
# array([0, 1])
```
!!! example "Qwen3-VL"
```python
import supervision as sv
qwen_3_vl_result = \"\"\"```json
[
{"bbox_2d": [139, 768, 315, 954], "label": "cat"},
{"bbox_2d": [366, 679, 536, 849], "label": "dog"}
]
```\"\"\"
detections = sv.Detections.from_vlm(
sv.VLM.QWEN_3_VL,
qwen_3_vl_result,
resolution_wh=(1000, 1000),
classes=['cat', 'dog'],
)
detections.xyxy
# array([[139., 768., 315., 954.], [366., 679., 536., 849.]])
detections.class_id
# array([0, 1])
detections.data
# {'class_name': array(['cat', 'dog'], dtype='<U10')}
detections.class_id
# array([0, 1])
```
!!! example "Gemini 2.0"
```python
import supervision as sv
@@ -1452,6 +1565,47 @@ class Detections:
# [1908.01, 1346.67, 2585.99, 2024.11]])
```
!!! example "DeepSeek-VL2"
??? tip "Prompt engineering"
To get the best results from DeepSeek-VL2, use optimized prompts that leverage
its object detection and visual grounding capabilities effectively.
**For general object detection, use the following user prompt:**
```
<image>\\n<|ref|>The giraffe at the front<|/ref|>
```
**For visual grounding, use the following user prompt:**
```
<image>\\n<|grounding|>Detect the giraffes
```
```python
from PIL import Image
import supervision as sv
deepseek_vl2_result = "<|ref|>The giraffe at the back<|/ref|><|det|>[[580, 270, 999, 904]]<|/det|><|ref|>The giraffe at the front<|/ref|><|det|>[[26, 31, 632, 998]]<|/det|><|end▁of▁sentence|>"
detections = sv.Detections.from_vlm(
vlm=sv.VLM.DEEPSEEK_VL_2, result=deepseek_vl2_result, resolution_wh=image.size
)
detections.xyxy
# array([[ 420, 293, 724, 982],
# [ 18, 33, 458, 1084]])
detections.class_id
# array([0, 1])
detections.data
# {'class_name': array(['The giraffe at the back', 'The giraffe at the front'], dtype='<U24')}
```
""" # noqa: E501
vlm = validate_vlm_parameters(vlm, result, kwargs)
@@ -1464,6 +1618,18 @@ class Detections:
if vlm == VLM.QWEN_2_5_VL:
xyxy, class_id, class_name = from_qwen_2_5_vl(result, **kwargs)
data = {CLASS_NAME_DATA_FIELD: class_name}
confidence = np.ones(len(xyxy), dtype=float)
return cls(xyxy=xyxy, class_id=class_id, confidence=confidence, data=data)
if vlm == VLM.QWEN_3_VL:
xyxy, class_id, class_name = from_qwen_3_vl(result, **kwargs)
data = {CLASS_NAME_DATA_FIELD: class_name}
confidence = np.ones(len(xyxy), dtype=float)
return cls(xyxy=xyxy, class_id=class_id, confidence=confidence, data=data)
if vlm == VLM.DEEPSEEK_VL_2:
xyxy, class_id, class_name = from_deepseek_vl_2(result, **kwargs)
data = {CLASS_NAME_DATA_FIELD: class_name}
return cls(xyxy=xyxy, class_id=class_id, data=data)
if vlm == VLM.FLORENCE_2:
@@ -1922,6 +2088,43 @@ class Detections:
"""
return (self.xyxy[:, 3] - self.xyxy[:, 1]) * (self.xyxy[:, 2] - self.xyxy[:, 0])
@property
def box_aspect_ratio(self) -> np.ndarray:
"""
Compute the aspect ratio (width divided by height) for each bounding box.
Returns:
np.ndarray: Array of shape `(N,)` containing aspect ratios, where `N` is the
number of boxes (width / height for each box).
Examples:
```python
import numpy as np
import supervision as sv
xyxy = np.array([
[10, 10, 50, 50],
[60, 10, 180, 50],
[10, 60, 50, 180],
])
detections = sv.Detections(xyxy=xyxy)
detections.box_aspect_ratio
# array([1.0, 3.0, 0.33333333])
ar = detections.box_aspect_ratio
detections[(ar < 2.0) & (ar > 0.5)].xyxy
# array([[10., 10., 50., 50.]])
```
"""
widths = self.xyxy[:, 2] - self.xyxy[:, 0]
heights = self.xyxy[:, 3] - self.xyxy[:, 1]
aspect_ratios = np.full_like(widths, np.nan, dtype=np.float64)
np.divide(widths, heights, out=aspect_ratios, where=heights != 0)
return aspect_ratios
def with_nms(
self,
threshold: float = 0.5,
@@ -1939,8 +2142,8 @@ class Detections:
class_agnostic (bool): Whether to perform class-agnostic
non-maximum suppression. If True, the class_id of each detection
will be ignored. Defaults to False.
overlap_metric (OverlapMetric): Metric used for measuring overlap between
detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of
overlap between pairs of masks or boxes (e.g., IoU, IoS).
Returns:
Detections: A new Detections object containing the subset of detections
@@ -2003,8 +2206,8 @@ class Detections:
class_agnostic (bool): Whether to perform class-agnostic
non-maximum merging. If True, the class_id of each detection
will be ignored. Defaults to False.
overlap_metric (OverlapMetric): Metric used for measuring overlap between
detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of
overlap between pairs of masks or boxes (e.g., IoU, IoS).
Returns:
Detections: A new Detections object containing the subset of detections
+210 -168
View File
@@ -9,13 +9,11 @@ import numpy as np
from supervision.config import ORIENTED_BOX_COORDINATES
from supervision.detection.core import Detections
from supervision.detection.utils.boxes import move_boxes, move_oriented_boxes
from supervision.detection.utils.iou_and_nms import OverlapFilter
from supervision.detection.utils.iou_and_nms import OverlapFilter, OverlapMetric
from supervision.detection.utils.masks import move_masks
from supervision.utils.image import crop_image
from supervision.utils.internal import (
SupervisionWarnings,
warn_deprecated,
)
from supervision.draw.base import ImageType
from supervision.utils.image import crop_image, get_image_resolution_wh
from supervision.utils.internal import SupervisionWarnings
def move_detections(
@@ -53,111 +51,106 @@ def move_detections(
class InferenceSlicer:
"""
InferenceSlicer performs slicing-based inference for small target detection. This
method, often referred to as
[Slicing Adaptive Inference (SAHI)](https://ieeexplore.ieee.org/document/9897990),
involves dividing a larger image into smaller slices, performing inference on each
slice, and then merging the detections.
Perform tiled inference on large images by slicing them into overlapping patches.
This class divides an input image into overlapping slices of configurable size
and overlap, runs inference on each slice through a user-provided callback, and
merges the resulting detections. The slicing process allows efficient processing
of large images with limited resources while preserving detection accuracy via
configurable overlap and post-processing of overlaps. Uses multi-threading for
parallel slice inference.
Args:
slice_wh (Tuple[int, int]): Dimensions of each slice measured in pixels. The
tuple should be in the format `(width, height)`.
overlap_ratio_wh (Optional[Tuple[float, float]]): [⚠️ Deprecated: please set
to `None` and use `overlap_wh`] A tuple representing the
desired overlap ratio for width and height between consecutive slices.
Each value should be in the range [0, 1), where 0 means no overlap and
a value close to 1 means high overlap.
overlap_wh (Optional[Tuple[int, int]]): A tuple representing the desired
overlap for width and height between consecutive slices measured in pixels.
Each value should be greater than or equal to 0. Takes precedence over
`overlap_ratio_wh`.
overlap_filter (Union[OverlapFilter, str]): Strategy for
filtering or merging overlapping detections in slices.
iou_threshold (float): Intersection over Union (IoU) threshold
used when filtering by overlap.
match_metric (str): Metric used for matching detections in slices.
"IOU" or "IOS". Defaults "IOU".
callback (Callable): A function that performs inference on a given image
slice and returns detections.
thread_workers (int): Number of threads for parallel execution.
callback (Callable[[ImageType], Detections]): Inference function that takes
a sliced image and returns a `Detections` object.
slice_wh (int or tuple[int, int]): Size of each slice `(width, height)`.
If int, both width and height are set to this value.
overlap_wh (int or tuple[int, int]): Overlap size `(width, height)` between
slices. If int, both width and height are set to this value.
overlap_filter (OverlapFilter or str): Strategy to merge overlapping
detections (`NON_MAX_SUPPRESSION`, `NON_MAX_MERGE`, or `NONE`).
iou_threshold (float): IOU threshold used in merging overlap filtering.
overlap_metric (OverlapMetric or str): Metric to compute overlap
(`IOU` or `IOS`).
thread_workers (int): Number of threads for concurrent slice inference.
Note:
The class ensures that slices do not exceed the boundaries of the original
image. As a result, the final slices in the row and column dimensions might be
smaller than the specified slice dimensions if the image's width or height is
not a multiple of the slice's width or height minus the overlap.
Raises:
ValueError: If `slice_wh` or `overlap_wh` are invalid or inconsistent.
Example:
```python
import cv2
import supervision as sv
from rfdetr import RFDETRMedium
model = RFDETRMedium()
def callback(tile):
return model.predict(tile)
slicer = sv.InferenceSlicer(callback, slice_wh=640, overlap_wh=100)
image = cv2.imread("example.png")
detections = slicer(image)
```
```python
import supervision as sv
from PIL import Image
from ultralytics import YOLO
model = YOLO("yolo11m.pt")
def callback(tile):
results = model(tile)[0]
return sv.Detections.from_ultralytics(results)
slicer = sv.InferenceSlicer(callback, slice_wh=640, overlap_wh=100)
image = Image.open("example.png")
detections = slicer(image)
```
"""
def __init__(
self,
callback: Callable[[np.ndarray], Detections],
slice_wh: tuple[int, int] = (320, 320),
overlap_ratio_wh: tuple[float, float] | None = (0.2, 0.2),
overlap_wh: tuple[int, int] | None = None,
callback: Callable[[ImageType], Detections],
slice_wh: int | tuple[int, int] = 640,
overlap_wh: int | tuple[int, int] = 100,
overlap_filter: OverlapFilter | str = OverlapFilter.NON_MAX_SUPPRESSION,
iou_threshold: float = 0.5,
match_metric: str = "IOU",
overlap_metric: OverlapMetric | str = OverlapMetric.IOU,
thread_workers: int = 1,
):
if overlap_ratio_wh is not None:
warn_deprecated(
"`overlap_ratio_wh` in `InferenceSlicer.__init__` is deprecated and "
"will be removed in `supervision-0.27.0`. Please manually set it to "
"`None` and use `overlap_wh` instead."
)
slice_wh_norm = self._normalize_slice_wh(slice_wh)
overlap_wh_norm = self._normalize_overlap_wh(overlap_wh)
self._validate_overlap(overlap_ratio_wh, overlap_wh)
self.overlap_ratio_wh = overlap_ratio_wh
self.overlap_wh = overlap_wh
self._validate_overlap(slice_wh=slice_wh_norm, overlap_wh=overlap_wh_norm)
self.slice_wh = slice_wh
self.slice_wh = slice_wh_norm
self.overlap_wh = overlap_wh_norm
self.iou_threshold = iou_threshold
self.match_metric = match_metric
self.overlap_metric = OverlapMetric.from_value(overlap_metric)
self.overlap_filter = OverlapFilter.from_value(overlap_filter)
self.callback = callback
self.thread_workers = thread_workers
def __call__(self, image: np.ndarray) -> Detections:
def __call__(self, image: ImageType) -> Detections:
"""
Performs slicing-based inference on the provided image using the specified
callback.
Perform tiled inference on the full image and return merged detections.
Args:
image (np.ndarray): The input image on which inference needs to be
performed. The image should be in the format
`(height, width, channels)`.
image (ImageType): The full image to run inference on.
Returns:
Detections: A collection of detections for the entire image after merging
results from all slices and applying NMS.
Example:
```python
import cv2
import supervision as sv
from ultralytics import YOLO
image = cv2.imread(SOURCE_IMAGE_PATH)
model = YOLO(...)
def callback(image_slice: np.ndarray) -> sv.Detections:
result = model(image_slice)[0]
return sv.Detections.from_ultralytics(result)
slicer = sv.InferenceSlicer(
callback=callback,
overlap_filter_strategy=sv.OverlapFilter.NON_MAX_SUPPRESSION,
)
detections = slicer(image)
```
Detections: Merged detections across all slices.
"""
detections_list = []
resolution_wh = (image.shape[1], image.shape[0])
detections_list: list[Detections] = []
resolution_wh = get_image_resolution_wh(image)
offsets = self._generate_offset(
resolution_wh=resolution_wh,
slice_wh=self.slice_wh,
overlap_ratio_wh=self.overlap_ratio_wh,
overlap_wh=self.overlap_wh,
)
@@ -171,129 +164,178 @@ class InferenceSlicer:
merged = Detections.merge(detections_list=detections_list)
if self.overlap_filter == OverlapFilter.NONE:
return merged
elif self.overlap_filter == OverlapFilter.NON_MAX_SUPPRESSION:
if self.overlap_filter == OverlapFilter.NON_MAX_SUPPRESSION:
return merged.with_nms(
threshold=self.iou_threshold, match_metric=self.match_metric
threshold=self.iou_threshold,
overlap_metric=self.overlap_metric,
)
elif self.overlap_filter == OverlapFilter.NON_MAX_MERGE:
if self.overlap_filter == OverlapFilter.NON_MAX_MERGE:
return merged.with_nmm(
threshold=self.iou_threshold, match_metric=self.match_metric
threshold=self.iou_threshold,
overlap_metric=self.overlap_metric,
)
else:
warnings.warn(
f"Invalid overlap filter strategy: {self.overlap_filter}",
category=SupervisionWarnings,
)
return merged
def _run_callback(self, image, offset) -> Detections:
warnings.warn(
f"Invalid overlap filter strategy: {self.overlap_filter}",
category=SupervisionWarnings,
)
return merged
def _run_callback(self, image: ImageType, offset: np.ndarray) -> Detections:
"""
Run the provided callback on a slice of an image.
Run detection callback on a sliced portion of the image and adjust coordinates.
Args:
image (np.ndarray): The input image on which inference needs to run
offset (np.ndarray): An array of shape `(4,)` containing coordinates
for the slice.
image (ImageType): The full image.
offset (numpy.ndarray): Coordinates `(x_min, y_min, x_max, y_max)` defining
the slice region.
Returns:
Detections: A collection of detections for the slice.
Detections: Detections adjusted to the full image coordinate system.
"""
image_slice = crop_image(image=image, xyxy=offset)
image_slice: ImageType = crop_image(image=image, xyxy=offset)
detections = self.callback(image_slice)
resolution_wh = (image.shape[1], image.shape[0])
resolution_wh = get_image_resolution_wh(image)
detections = move_detections(
detections=detections, offset=offset[:2], resolution_wh=resolution_wh
detections=detections,
offset=offset[:2],
resolution_wh=resolution_wh,
)
return detections
@staticmethod
def _normalize_slice_wh(
slice_wh: int | tuple[int, int],
) -> tuple[int, int]:
if isinstance(slice_wh, int):
if slice_wh <= 0:
raise ValueError(
f"`slice_wh` must be a positive integer. Received: {slice_wh}"
)
return slice_wh, slice_wh
if isinstance(slice_wh, tuple) and len(slice_wh) == 2:
width, height = slice_wh
if width <= 0 or height <= 0:
raise ValueError(
f"`slice_wh` values must be positive. Received: {slice_wh}"
)
return width, height
raise ValueError(
"`slice_wh` must be an int or a tuple of two positive integers "
"(slice_w, slice_h). "
f"Received: {slice_wh}"
)
return detections
@staticmethod
def _normalize_overlap_wh(
overlap_wh: int | tuple[int, int],
) -> tuple[int, int]:
if isinstance(overlap_wh, int):
if overlap_wh < 0:
raise ValueError(
"`overlap_wh` must be a non negative integer. "
f"Received: {overlap_wh}"
)
return overlap_wh, overlap_wh
if isinstance(overlap_wh, tuple) and len(overlap_wh) == 2:
overlap_w, overlap_h = overlap_wh
if overlap_w < 0 or overlap_h < 0:
raise ValueError(
f"`overlap_wh` values must be non negative. Received: {overlap_wh}"
)
return overlap_w, overlap_h
raise ValueError(
"`overlap_wh` must be an int or a tuple of two non negative integers "
"(overlap_w, overlap_h). "
f"Received: {overlap_wh}"
)
@staticmethod
def _generate_offset(
resolution_wh: tuple[int, int],
slice_wh: tuple[int, int],
overlap_ratio_wh: tuple[float, float] | None,
overlap_wh: tuple[int, int] | None,
overlap_wh: tuple[int, int],
) -> np.ndarray:
"""
Generate offset coordinates for slicing an image based on the given resolution,
slice dimensions, and overlap ratios.
Generate bounding boxes defining the coordinates of image slices with overlap.
Args:
resolution_wh (Tuple[int, int]): A tuple representing the width and height
of the image to be sliced.
slice_wh (Tuple[int, int]): Dimensions of each slice measured in pixels. The
tuple should be in the format `(width, height)`.
overlap_ratio_wh (Optional[Tuple[float, float]]): A tuple representing the
desired overlap ratio for width and height between consecutive slices.
Each value should be in the range [0, 1), where 0 means no overlap and
a value close to 1 means high overlap.
overlap_wh (Optional[Tuple[int, int]]): A tuple representing the desired
overlap for width and height between consecutive slices measured in
pixels. Each value should be greater than or equal to 0.
resolution_wh (tuple[int, int]): Image resolution `(width, height)`.
slice_wh (tuple[int, int]): Size of each slice `(width, height)`.
overlap_wh (tuple[int, int]): Overlap size between slices `(width, height)`.
Returns:
np.ndarray: An array of shape `(n, 4)` containing coordinates for each
slice in the format `[xmin, ymin, xmax, ymax]`.
Note:
The function ensures that slices do not exceed the boundaries of the
original image. As a result, the final slices in the row and column
dimensions might be smaller than the specified slice dimensions if the
image's width or height is not a multiple of the slice's width or
height minus the overlap.
numpy.ndarray: Array of shape `(num_slices, 4)` with each row as
`(x_min, y_min, x_max, y_max)` coordinates for a slice.
"""
slice_width, slice_height = slice_wh
image_width, image_height = resolution_wh
overlap_width = (
overlap_wh[0]
if overlap_wh is not None
else int(overlap_ratio_wh[0] * slice_width)
overlap_width, overlap_height = overlap_wh
stride_x = slice_width - overlap_width
stride_y = slice_height - overlap_height
def _compute_axis_starts(
image_size: int,
slice_size: int,
stride: int,
) -> list[int]:
if image_size <= slice_size:
return [0]
if stride == slice_size:
return np.arange(0, image_size, stride).tolist()
last_start = image_size - slice_size
starts = np.arange(0, last_start, stride).tolist()
if not starts or starts[-1] != last_start:
starts.append(last_start)
return starts
x_starts = _compute_axis_starts(
image_size=image_width,
slice_size=slice_width,
stride=stride_x,
)
overlap_height = (
overlap_wh[1]
if overlap_wh is not None
else int(overlap_ratio_wh[1] * slice_height)
y_starts = _compute_axis_starts(
image_size=image_height,
slice_size=slice_height,
stride=stride_y,
)
width_stride = slice_width - overlap_width
height_stride = slice_height - overlap_height
x_min, y_min = np.meshgrid(x_starts, y_starts)
x_max = np.clip(x_min + slice_width, 0, image_width)
y_max = np.clip(y_min + slice_height, 0, image_height)
ws = np.arange(0, image_width, width_stride)
hs = np.arange(0, image_height, height_stride)
xmin, ymin = np.meshgrid(ws, hs)
xmax = np.clip(xmin + slice_width, 0, image_width)
ymax = np.clip(ymin + slice_height, 0, image_height)
offsets = np.stack([xmin, ymin, xmax, ymax], axis=-1).reshape(-1, 4)
offsets = np.stack(
[x_min, y_min, x_max, y_max],
axis=-1,
).reshape(-1, 4)
return offsets
@staticmethod
def _validate_overlap(
overlap_ratio_wh: tuple[float, float] | None,
overlap_wh: tuple[int, int] | None,
slice_wh: tuple[int, int],
overlap_wh: tuple[int, int],
) -> None:
if overlap_ratio_wh is not None and overlap_wh is not None:
overlap_w, overlap_h = overlap_wh
slice_w, slice_h = slice_wh
if overlap_w < 0 or overlap_h < 0:
raise ValueError(
"Both `overlap_ratio_wh` and `overlap_wh` cannot be provided. "
"Please provide only one of them."
)
if overlap_ratio_wh is None and overlap_wh is None:
raise ValueError(
"Either `overlap_ratio_wh` or `overlap_wh` must be provided. "
"Please provide one of them."
"Overlap values must be greater than or equal to 0. "
f"Received: {overlap_wh}"
)
if overlap_ratio_wh is not None:
if not (0 <= overlap_ratio_wh[0] < 1 and 0 <= overlap_ratio_wh[1] < 1):
raise ValueError(
"Overlap ratios must be in the range [0, 1). "
f"Received: {overlap_ratio_wh}"
)
if overlap_wh is not None:
if not (overlap_wh[0] >= 0 and overlap_wh[1] >= 0):
raise ValueError(
"Overlap values must be greater than or equal to 0. "
f"Received: {overlap_wh}"
)
if overlap_w >= slice_w or overlap_h >= slice_h:
raise ValueError(
"`overlap_wh` must be smaller than `slice_wh` in both dimensions "
f"to keep a positive stride. Received overlap_wh={overlap_wh}, "
f"slice_wh={slice_wh}."
)
+41 -34
View File
@@ -14,8 +14,8 @@ def clip_boxes(xyxy: np.ndarray, resolution_wh: tuple[int, int]) -> np.ndarray:
xyxy (np.ndarray): A numpy array of shape `(N, 4)` where each
row corresponds to a bounding box in
the format `(x_min, y_min, x_max, y_max)`.
resolution_wh (Tuple[int, int]): A tuple of the form `(width, height)`
representing the resolution of the frame.
resolution_wh (Tuple[int, int]): A tuple of the form
`(width, height)` representing the resolution of the frame.
Returns:
np.ndarray: A numpy array of shape `(N, 4)` where each row
@@ -95,24 +95,27 @@ def pad_boxes(xyxy: np.ndarray, px: int, py: int | None = None) -> np.ndarray:
def denormalize_boxes(
normalized_xyxy: np.ndarray,
xyxy: np.ndarray,
resolution_wh: tuple[int, int],
normalization_factor: float = 1.0,
) -> np.ndarray:
"""
Converts normalized bounding box coordinates to absolute pixel values.
Convert normalized bounding box coordinates to absolute pixel coordinates.
Multiplies each bounding box coordinate by image size and divides by
`normalization_factor`, mapping values from normalized `[0, normalization_factor]`
to absolute pixel values for a given resolution.
Args:
normalized_xyxy (np.ndarray): A numpy array of shape `(N, 4)` where each row
contains normalized coordinates in the format `(x_min, y_min, x_max, y_max)`,
with values between 0 and `normalization_factor`.
resolution_wh (Tuple[int, int]): A tuple `(width, height)` representing the
target image resolution.
normalization_factor (float, optional): The normalization range of the input
coordinates. Defaults to 1.0.
xyxy (`numpy.ndarray`): Normalized bounding boxes of shape `(N, 4)`,
where each row is `(x_min, y_min, x_max, y_max)`, values in
`[0, normalization_factor]`.
resolution_wh (`tuple[int, int]`): Target image resolution as `(width, height)`.
normalization_factor (`float`): Maximum value of input coordinate range.
Defaults to `1.0`.
Returns:
np.ndarray: An array of shape `(N, 4)` with absolute coordinates in
(`numpy.ndarray`): Array of shape `(N, 4)` with absolute coordinates in
`(x_min, y_min, x_max, y_max)` format.
Examples:
@@ -120,35 +123,39 @@ def denormalize_boxes(
import numpy as np
import supervision as sv
# Default normalization (0-1)
normalized_xyxy = np.array([
xyxy = np.array([
[0.1, 0.2, 0.5, 0.6],
[0.3, 0.4, 0.7, 0.8]
[0.3, 0.4, 0.7, 0.8],
[0.2, 0.1, 0.6, 0.5]
])
resolution_wh = (100, 200)
sv.denormalize_boxes(normalized_xyxy, resolution_wh)
# array([
# [ 10., 40., 50., 120.],
# [ 30., 80., 70., 160.]
# ])
# Custom normalization (0-100)
normalized_xyxy = np.array([
[10., 20., 50., 60.],
[30., 40., 70., 80.]
])
sv.denormalize_boxes(normalized_xyxy, resolution_wh, normalization_factor=100.0)
sv.denormalize_boxes(xyxy, (1280, 720))
# array([
# [ 10., 40., 50., 120.],
# [ 30., 80., 70., 160.]
# [128., 144., 640., 432.],
# [384., 288., 896., 576.],
# [256., 72., 768., 360.]
# ])
```
""" # noqa E501 // docs
width, height = resolution_wh
result = normalized_xyxy.copy()
result[[0, 2]] = (result[[0, 2]] * width) / normalization_factor
result[[1, 3]] = (result[[1, 3]] * height) / normalization_factor
```
import numpy as np
import supervision as sv
xyxy = np.array([
[256., 128., 768., 640.]
])
sv.denormalize_boxes(xyxy, (1280, 720), normalization_factor=1024.0)
# array([
# [320., 90., 960., 450.]
# ])
```
"""
width, height = resolution_wh
result = xyxy.copy()
result[:, [0, 2]] = (result[:, [0, 2]] * width) / normalization_factor
result[:, [1, 3]] = (result[:, [1, 3]] * height) / normalization_factor
return result
+64
View File
@@ -229,6 +229,70 @@ def mask_to_xyxy(masks: np.ndarray) -> np.ndarray:
return xyxy
def xyxy_to_mask(boxes: np.ndarray, resolution_wh: tuple[int, int]) -> np.ndarray:
"""
Converts a 2D `np.ndarray` of bounding boxes into a 3D `np.ndarray` of bool masks.
Parameters:
boxes (np.ndarray): A 2D `np.ndarray` of shape `(N, 4)`
containing bounding boxes `(x_min, y_min, x_max, y_max)`
resolution_wh (Tuple[int, int]): A tuple `(width, height)` specifying
the resolution of the output masks
Returns:
np.ndarray: A 3D `np.ndarray` of shape `(N, height, width)`
containing 2D bool masks for each bounding box
Examples:
```python
import numpy as np
import supervision as sv
boxes = np.array([[0, 0, 2, 2]])
sv.xyxy_to_mask(boxes, (5, 5))
# array([
# [[ True, True, True, False, False],
# [ True, True, True, False, False],
# [ True, True, True, False, False],
# [False, False, False, False, False],
# [False, False, False, False, False]]
# ])
boxes = np.array([[0, 0, 1, 1], [3, 3, 4, 4]])
sv.xyxy_to_mask(boxes, (5, 5))
# array([
# [[ True, True, False, False, False],
# [ True, True, False, False, False],
# [False, False, False, False, False],
# [False, False, False, False, False],
# [False, False, False, False, False]],
#
# [[False, False, False, False, False],
# [False, False, False, False, False],
# [False, False, False, False, False],
# [False, False, False, True, True],
# [False, False, False, True, True]]
# ])
```
"""
width, height = resolution_wh
n = boxes.shape[0]
masks = np.zeros((n, height, width), dtype=bool)
for i, (x_min, y_min, x_max, y_max) in enumerate(boxes):
x_min = max(0, int(x_min))
y_min = max(0, int(y_min))
x_max = min(width - 1, int(x_max))
y_max = min(height - 1, int(y_max))
if x_max >= x_min and y_max >= y_min:
masks[i, y_min : y_max + 1, x_min : x_max + 1] = True
return masks
def mask_to_polygons(mask: np.ndarray) -> list[np.ndarray]:
"""
Converts a binary mask to a list of polygons.
-1
View File
@@ -271,7 +271,6 @@ def merge_metadata(metadata_list: list[dict[str, Any]]) -> dict[str, Any]:
"{type(value)}, {type(other_value)}."
)
else:
print("hm")
if merged_metadata[key] != value:
raise ValueError(f"Conflicting metadata for key: '{key}'.")
+125 -92
View File
@@ -64,7 +64,7 @@ class OverlapMetric(Enum):
IOS = "IOS"
@classmethod
def list(cls):
def list(cls) -> list[str]:
return list(map(lambda c: c.value, cls))
@classmethod
@@ -72,7 +72,7 @@ class OverlapMetric(Enum):
if isinstance(value, cls):
return value
if isinstance(value, str):
value = value.lower()
value = value.upper()
try:
return cls(value)
except ValueError:
@@ -86,90 +86,107 @@ class OverlapMetric(Enum):
def box_iou(
box_true: list[float] | np.ndarray,
box_detection: list[float] | np.ndarray,
overlap_metric: OverlapMetric | str = OverlapMetric.IOU,
) -> float:
r"""
Compute the Intersection over Union (IoU) between two bounding boxes.
"""
Compute overlap metric between two bounding boxes.
\[
\text{IoU} = \frac{|\text{box}_{\text{true}} \cap \text{box}_{\text{detection}}|}{|\text{box}_{\text{true}} \cup \text{box}_{\text{detection}}|}
\]
Note:
Use `box_iou` when computing IoU between two individual boxes.
For comparing multiple boxes (arrays of boxes), use `box_iou_batch` for better
performance.
Supports standard IOU (intersection-over-union) and IOS
(intersection-over-smaller-area) metrics. Returns the overlap value in range
`[0, 1]`.
Args:
box_true (Union[List[float], np.ndarray]): A single bounding box represented as
[x_min, y_min, x_max, y_max].
box_detection (Union[List[float], np.ndarray]):
A single bounding box represented as [x_min, y_min, x_max, y_max].
box_true (`list[float]` or `numpy.array`): Ground truth box in format
`(x_min, y_min, x_max, y_max)`.
box_detection (`list[float]` or `numpy.array`): Detected box in format
`(x_min, y_min, x_max, y_max)`.
overlap_metric (`OverlapMetric` or `str`): Overlap type.
Use `OverlapMetric.IOU` for IOU or
`OverlapMetric.IOS` for IOS. Defaults to `OverlapMetric.IOU`.
Returns:
IoU (float): IoU score between the two boxes. Ranges from 0.0 (no overlap)
to 1.0 (perfect overlap).
(`float`): Overlap value between boxes in `[0, 1]`.
Raises:
ValueError: If `overlap_metric` is not IOU or IOS.
Examples:
```python
import numpy as np
```
import supervision as sv
box_true = np.array([100, 100, 200, 200])
box_detection = np.array([150, 150, 250, 250])
box_true = [100, 100, 200, 200]
box_detection = [150, 150, 250, 250]
sv.box_iou(box_true=box_true, box_detection=box_detection)
# 0.14285814285714285
sv.box_iou(box_true, box_detection, overlap_metric=sv.OverlapMetric.IOU)
# 0.14285714285714285
sv.box_iou(box_true, box_detection, overlap_metric=sv.OverlapMetric.IOS)
# 0.25
```
""" # noqa: E501
box_true = np.array(box_true)
box_detection = np.array(box_detection)
"""
overlap_metric = OverlapMetric.from_value(overlap_metric)
x_min_true, y_min_true, x_max_true, y_max_true = np.array(box_true)
x_min_det, y_min_det, x_max_det, y_max_det = np.array(box_detection)
inter_x1 = max(box_true[0], box_detection[0])
inter_y1 = max(box_true[1], box_detection[1])
inter_x2 = min(box_true[2], box_detection[2])
inter_y2 = min(box_true[3], box_detection[3])
x_min_inter = max(x_min_true, x_min_det)
y_min_inter = max(y_min_true, y_min_det)
x_max_inter = min(x_max_true, x_max_det)
y_max_inter = min(y_max_true, y_max_det)
inter_w = max(0, inter_x2 - inter_x1)
inter_h = max(0, inter_y2 - inter_y1)
inter_w = max(0.0, x_max_inter - x_min_inter)
inter_h = max(0.0, y_max_inter - y_min_inter)
inter_area = inter_w * inter_h
area_inter = inter_w * inter_h
area_true = (box_true[2] - box_true[0]) * (box_true[3] - box_true[1])
area_detection = (box_detection[2] - box_detection[0]) * (
box_detection[3] - box_detection[1]
)
area_true = (x_max_true - x_min_true) * (y_max_true - y_min_true)
area_det = (x_max_det - x_min_det) * (y_max_det - y_min_det)
union_area = area_true + area_detection - inter_area
if overlap_metric == OverlapMetric.IOU:
area_norm = area_true + area_det - area_inter
elif overlap_metric == OverlapMetric.IOS:
area_norm = min(area_true, area_det)
else:
raise ValueError(
f"overlap_metric {overlap_metric} is not supported, "
"only 'IOU' and 'IOS' are supported"
)
return inter_area / union_area + 1e-6
if area_norm <= 0.0:
return 0.0
return float(area_inter / area_norm)
def box_iou_batch(
boxes_true: np.ndarray,
boxes_detection: np.ndarray,
overlap_metric: OverlapMetric = OverlapMetric.IOU,
overlap_metric: OverlapMetric | str = OverlapMetric.IOU,
) -> np.ndarray:
"""
Compute Intersection over Union (IoU) of two sets of bounding boxes -
`boxes_true` and `boxes_detection`. Both sets
of boxes are expected to be in `(x_min, y_min, x_max, y_max)` format.
Compute pairwise overlap scores between batches of bounding boxes.
Note:
Use `box_iou` when computing IoU between two individual boxes.
For comparing multiple boxes (arrays of boxes), use `box_iou_batch` for better
performance.
Supports standard IOU (intersection-over-union) and IOS
(intersection-over-smaller-area) metrics for all `boxes_true` and
`boxes_detection` pairs. Returns a matrix of overlap values in range
`[0, 1]`, matching each box from the first batch to each from the second.
Args:
boxes_true (np.ndarray): 2D `np.ndarray` representing ground-truth boxes.
`shape = (N, 4)` where `N` is number of true objects.
boxes_detection (np.ndarray): 2D `np.ndarray` representing detection boxes.
`shape = (M, 4)` where `M` is number of detected objects.
overlap_metric (OverlapMetric): Metric used for matching detections in slices.
boxes_true (`numpy.array`): Array of reference boxes in
shape `(N, 4)` as `(x_min, y_min, x_max, y_max)`.
boxes_detection (`numpy.array`): Array of detected boxes in
shape `(M, 4)` as `(x_min, y_min, x_max, y_max)`.
overlap_metric (`OverlapMetric` or `str`): Overlap type.
Use `OverlapMetric.IOU` for intersection-over-union,
`OverlapMetric.IOS` for intersection-over-smaller-area.
Defaults to `OverlapMetric.IOU`.
Returns:
np.ndarray: Pairwise IoU of boxes from `boxes_true` and `boxes_detection`.
`shape = (N, M)` where `N` is number of true objects and
`M` is number of detected objects.
(`numpy.array`): Overlap matrix of shape `(N, M)`, where entry
`[i, j]` is the overlap between `boxes_true[i]` and
`boxes_detection[j]`.
Raises:
ValueError: If `overlap_metric` is not IOU or IOS.
Examples:
```python
@@ -185,49 +202,57 @@ def box_iou_batch(
[320, 320, 420, 420]
])
sv.box_iou_batch(boxes_true=boxes_true, boxes_detection=boxes_detection)
# array([
# [0.14285714, 0. ],
# [0. , 0.47058824]
# ])
sv.box_iou_batch(boxes_true, boxes_detection, overlap_metric=OverlapMetric.IOU)
# array([[0.14285715, 0. ],
# [0. , 0.47058824]])
sv.box_iou_batch(boxes_true, boxes_detection, overlap_metric=OverlapMetric.IOS)
# array([[0.25, 0. ],
# [0. , 0.64]])
```
"""
overlap_metric = OverlapMetric.from_value(overlap_metric)
x_min_true, y_min_true, x_max_true, y_max_true = boxes_true.T
x_min_det, y_min_det, x_max_det, y_max_det = boxes_detection.T
count_true, count_det = boxes_true.shape[0], boxes_detection.shape[0]
def box_area(box):
return (box[2] - box[0]) * (box[3] - box[1])
if count_true == 0 or count_det == 0:
return np.empty((count_true, count_det), dtype=np.float32)
area_true = box_area(boxes_true.T)
area_detection = box_area(boxes_detection.T)
x_min_inter = np.empty((count_true, count_det), dtype=np.float32)
x_max_inter = np.empty_like(x_min_inter)
y_min_inter = np.empty_like(x_min_inter)
y_max_inter = np.empty_like(x_min_inter)
top_left = np.maximum(boxes_true[:, None, :2], boxes_detection[:, :2])
bottom_right = np.minimum(boxes_true[:, None, 2:], boxes_detection[:, 2:])
np.maximum(x_min_true[:, None], x_min_det[None, :], out=x_min_inter)
np.minimum(x_max_true[:, None], x_max_det[None, :], out=x_max_inter)
np.maximum(y_min_true[:, None], y_min_det[None, :], out=y_min_inter)
np.minimum(y_max_true[:, None], y_max_det[None, :], out=y_max_inter)
area_inter = np.prod(np.clip(bottom_right - top_left, a_min=0, a_max=None), 2)
# we reuse x_max_inter and y_max_inter to store inter_w and inter_h
np.subtract(x_max_inter, x_min_inter, out=x_max_inter) # inter_w
np.subtract(y_max_inter, y_min_inter, out=y_max_inter) # inter_h
np.clip(x_max_inter, 0.0, None, out=x_max_inter)
np.clip(y_max_inter, 0.0, None, out=y_max_inter)
area_inter = x_max_inter * y_max_inter # inter_w * inter_h
area_true = (x_max_true - x_min_true) * (y_max_true - y_min_true)
area_det = (x_max_det - x_min_det) * (y_max_det - y_min_det)
if overlap_metric == OverlapMetric.IOU:
union_area = area_true[:, None] + area_detection - area_inter
ious = np.divide(
area_inter,
union_area,
out=np.zeros_like(area_inter, dtype=float),
where=union_area != 0,
)
area_norm = area_true[:, None] + area_det[None, :] - area_inter
elif overlap_metric == OverlapMetric.IOS:
small_area = np.minimum(area_true[:, None], area_detection)
ious = np.divide(
area_inter,
small_area,
out=np.zeros_like(area_inter, dtype=float),
where=small_area != 0,
)
area_norm = np.minimum(area_true[:, None], area_det[None, :])
else:
raise ValueError(
f"overlap_metric {overlap_metric} is not supported, "
"only 'IOU' and 'IOS' are supported"
)
ious = np.nan_to_num(ious)
return ious
out = np.zeros_like(area_inter, dtype=np.float32)
np.divide(area_inter, area_norm, out=out, where=area_norm > 0)
return out
def _jaccard(box_a: list[float], box_b: list[float], is_crowd: bool) -> float:
@@ -381,7 +406,8 @@ def _mask_iou_batch_split(
Args:
masks_true (np.ndarray): 3D `np.ndarray` representing ground-truth masks.
masks_detection (np.ndarray): 3D `np.ndarray` representing detection masks.
overlap_metric (OverlapMetric): Metric used for matching detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of overlap
between pairs of masks (e.g., IoU, IoS).
Returns:
np.ndarray: Pairwise IoU of masks from `masks_true` and `masks_detection`.
@@ -433,7 +459,8 @@ def mask_iou_batch(
Args:
masks_true (np.ndarray): 3D `np.ndarray` representing ground-truth masks.
masks_detection (np.ndarray): 3D `np.ndarray` representing detection masks.
overlap_metric (OverlapMetric): Metric used for matching detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of overlap
between pairs of masks (e.g., IoU, IoS).
memory_limit (int): memory limit in MB, default is 1024 * 5 MB (5GB).
Returns:
@@ -492,7 +519,8 @@ def mask_non_max_suppression(
dimensions of each mask.
iou_threshold (float): The intersection-over-union threshold
to use for non-maximum suppression.
overlap_metric (OverlapMetric): Metric used for matching detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of overlap
between pairs of masks (e.g., IoU, IoS).
mask_dimension (int): The dimension to which the masks should be
resized before computing IOU values. Defaults to 640.
@@ -543,7 +571,8 @@ def box_non_max_suppression(
or `(x_min, y_min, x_max, y_max, score, class)`.
iou_threshold (float): The intersection-over-union threshold
to use for non-maximum suppression.
overlap_metric (OverlapMetric): Metric used for matching detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of overlap
between pairs of boxes (e.g., IoU, IoS).
Returns:
np.ndarray: A boolean array indicating which predictions to keep after n
@@ -603,7 +632,8 @@ def _group_overlapping_masks(
the predictions.
iou_threshold (float): The intersection-over-union threshold
to use for non-maximum suppression. Defaults to 0.5.
overlap_metric (OverlapMetric): Metric used for matching detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of overlap
between pairs of masks (e.g., IoU, IoS).
Returns:
list[list[int]]: Groups of prediction indices be merged.
@@ -664,7 +694,8 @@ def mask_non_max_merge(
to use for non-maximum suppression.
mask_dimension (int): The dimension to which the masks should be
resized before computing IOU values. Defaults to 640.
overlap_metric (OverlapMetric): Metric used for matching detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of overlap
between pairs of masks (e.g., IoU, IoS).
Returns:
np.ndarray: A boolean array indicating which predictions to keep after
@@ -717,7 +748,8 @@ def _group_overlapping_boxes(
and the confidence scores.
iou_threshold (float): The intersection-over-union threshold
to use for non-maximum suppression. Defaults to 0.5.
overlap_metric (OverlapMetric): Metric used for matching detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of overlap
between pairs of boxes (e.g., IoU, IoS).
Returns:
list[list[int]]: Groups of prediction indices be merged.
@@ -765,7 +797,8 @@ def box_non_max_merge(
detections of different classes to be merged.
iou_threshold (float): The intersection-over-union threshold
to use for non-maximum suppression. Defaults to 0.5.
overlap_metric (OverlapMetric): Metric used for matching detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of overlap
between pairs of boxes (e.g., IoU, IoS).
Returns:
list[list[int]]: Groups of prediction indices be merged.
+139 -1
View File
@@ -1,5 +1,7 @@
from __future__ import annotations
from typing import Literal
import cv2
import numpy as np
import numpy.typing as npt
@@ -98,7 +100,7 @@ def calculate_masks_centroids(masks: np.ndarray) -> np.ndarray:
A 2D NumPy array of shape (num_masks, 2), where each row contains the x and y
coordinates (in that order) of the centroid of the corresponding mask.
"""
num_masks, height, width = masks.shape
_num_masks, height, width = masks.shape
total_pixels = masks.sum(axis=(1, 2))
# offset for 1-based indexing
@@ -260,3 +262,139 @@ def resize_masks(masks: np.ndarray, max_dimension: int = 640) -> np.ndarray:
resized_masks = masks[:, yv, xv]
return resized_masks.reshape(masks.shape[0], new_height, new_width)
def filter_segments_by_distance(
mask: npt.NDArray[np.bool_],
absolute_distance: float | None = 100.0,
relative_distance: float | None = None,
connectivity: int = 8,
mode: Literal["edge", "centroid"] = "edge",
) -> npt.NDArray[np.bool_]:
"""
Keep the largest connected component and any other components within a distance
threshold.
Distance can be absolute in pixels or relative to the image diagonal.
Args:
mask: Boolean mask HxW.
absolute_distance: Max allowed distance in pixels to the main component.
Ignored if `relative_distance` is provided.
relative_distance: Fraction of the diagonal. If set, threshold = fraction * sqrt(H^2 + W^2).
connectivity: Defines which neighboring pixels are considered connected.
- 4-connectedness: Only orthogonal neighbors.
```
[ ][X][ ]
[X][O][X]
[ ][X][ ]
```
- 8-connectedness: Includes diagonal neighbors.
```
[X][X][X]
[X][O][X]
[X][X][X]
```
Default is 8.
mode: Defines how distance between components is measured.
- "edge": Uses distance between nearest edges (via distance transform).
- "centroid": Uses distance between component centroids.
Returns:
Boolean mask after filtering.
Examples:
```python
import numpy as np
import supervision as sv
mask = np.array([
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
], dtype=bool)
sv.filter_segments_by_distance(
mask,
absolute_distance=2,
mode="edge",
connectivity=8
).astype(int)
# np.array([
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# ], dtype=bool)
# The nearby 2×2 block at columns 6–7 is kept because its edge distance
# is within 2 pixels. The distant block at columns 9-10 is removed.
```
""" # noqa E501 // docs
if mask.dtype != bool:
raise TypeError("mask must be boolean")
height, width = mask.shape
if not np.any(mask):
return mask.copy()
image = mask.astype(np.uint8)
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(
image, connectivity=connectivity
)
if num_labels <= 1:
return mask.copy()
areas = stats[1:, cv2.CC_STAT_AREA]
main_label = 1 + int(np.argmax(areas))
if relative_distance is not None:
diagonal = float(np.hypot(height, width))
threshold = float(relative_distance) * diagonal
else:
threshold = float(absolute_distance)
keep_labels = np.zeros(num_labels, dtype=bool)
keep_labels[main_label] = True
if mode == "centroid":
differences = centroids[1:] - centroids[main_label]
distances = np.sqrt(np.sum(differences**2, axis=1))
nearby = 1 + np.where(distances <= threshold)[0]
keep_labels[nearby] = True
elif mode == "edge":
main_mask = (labels == main_label).astype(np.uint8)
inverse = 1 - main_mask
distance_transform = cv2.distanceTransform(inverse, cv2.DIST_L2, 3)
for label in range(1, num_labels):
if label == main_label:
continue
component = labels == label
if not np.any(component):
continue
min_distance = float(distance_transform[component].min())
if min_distance <= threshold:
keep_labels[label] = True
else:
raise ValueError("mode must be 'edge' or 'centroid'")
return keep_labels[labels]
+105
View File
@@ -0,0 +1,105 @@
from __future__ import annotations
def edit_distance(string_1: str, string_2: str, case_sensitive: bool = True) -> int:
"""
Calculates the minimum number of single-character edits required
to transform one string into another. Allowed operations are insertion,
deletion, and substitution.
Args:
string_1 (str): The source string to be transformed.
string_2 (str): The target string to transform into.
case_sensitive (bool, optional): Whether comparison should be case-sensitive.
Defaults to True.
Returns:
int: The minimum number of edits required to convert `string_1`
into `string_2`.
Examples:
```python
import supervision as sv
sv.edit_distance("hello", "hello")
# 0
sv.edit_distance("Test", "test", case_sensitive=True)
# 1
sv.edit_distance("abc", "xyz")
# 3
sv.edit_distance("hello", "")
# 5
sv.edit_distance("", "")
# 0
sv.edit_distance("hello world", "helloworld")
# 1
```
"""
if not case_sensitive:
string_1 = string_1.lower()
string_2 = string_2.lower()
if len(string_1) < len(string_2):
string_1, string_2 = string_2, string_1
prev_row = list(range(len(string_2) + 1))
curr_row = [0] * (len(string_2) + 1)
for i in range(1, len(string_1) + 1):
curr_row[0] = i
for j in range(1, len(string_2) + 1):
if string_1[i - 1] == string_2[j - 1]:
substitution_cost = 0
else:
substitution_cost = 1
curr_row[j] = min(
prev_row[j] + 1,
curr_row[j - 1] + 1,
prev_row[j - 1] + substitution_cost,
)
prev_row, curr_row = curr_row, prev_row
return prev_row[len(string_2)]
def fuzzy_match_index(
candidates: list[str],
query: str,
threshold: int,
case_sensitive: bool = True,
) -> int | None:
"""
Searches for the first string in `candidates` whose edit distance
to `query` is less than or equal to `threshold`.
Args:
candidates (list[str]): List of strings to search.
query (str): String to compare against the candidates.
threshold (int): Maximum allowed edit distance for a match.
case_sensitive (bool, optional): Whether matching should be case-sensitive.
Returns:
Optional[int]: Index of the first matching string in candidates,
or None if no match is found.
Examples:
```python
fuzzy_match_index(["cat", "dog", "rat"], "dat", threshold=1)
# 0
fuzzy_match_index(["alpha", "beta", "gamma"], "bata", threshold=1)
# 1
fuzzy_match_index(["one", "two", "three"], "ten", threshold=2)
# None
```
"""
for idx, candidate in enumerate(candidates):
if edit_distance(candidate, query, case_sensitive=case_sensitive) <= threshold:
return idx
return None
+218 -47
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import ast
import base64
import io
import json
@@ -27,7 +28,8 @@ class LMM(Enum):
Attributes:
PALIGEMMA: Google's PaliGemma vision-language model.
FLORENCE_2: Microsoft's Florence-2 vision-language model.
QWEN_2_5_VL: Qwen2.5-VL open vision-language model from Alibaba.
QWEN_2_5_VL: Qwen2.5-VL open vision-language model from Alibaba.\
QWEN_3_VL: Qwen3-VL open vision-language model from Alibaba.
GOOGLE_GEMINI_2_0: Google Gemini 2.0 vision-language model.
GOOGLE_GEMINI_2_5: Google Gemini 2.5 vision-language model.
MOONDREAM: The Moondream vision-language model.
@@ -36,6 +38,8 @@ class LMM(Enum):
PALIGEMMA = "paligemma"
FLORENCE_2 = "florence_2"
QWEN_2_5_VL = "qwen_2_5_vl"
QWEN_3_VL = "qwen_3_vl"
DEEPSEEK_VL_2 = "deepseek_vl_2"
GOOGLE_GEMINI_2_0 = "gemini_2_0"
GOOGLE_GEMINI_2_5 = "gemini_2_5"
MOONDREAM = "moondream"
@@ -68,6 +72,7 @@ class VLM(Enum):
PALIGEMMA: Google's PaliGemma vision-language model.
FLORENCE_2: Microsoft's Florence-2 vision-language model.
QWEN_2_5_VL: Qwen2.5-VL open vision-language model from Alibaba.
QWEN_3_VL: Qwen3-VL open vision-language model from Alibaba.
GOOGLE_GEMINI_2_0: Google Gemini 2.0 vision-language model.
GOOGLE_GEMINI_2_5: Google Gemini 2.5 vision-language model.
MOONDREAM: The Moondream vision-language model.
@@ -76,6 +81,8 @@ class VLM(Enum):
PALIGEMMA = "paligemma"
FLORENCE_2 = "florence_2"
QWEN_2_5_VL = "qwen_2_5_vl"
QWEN_3_VL = "qwen_3_vl"
DEEPSEEK_VL_2 = "deepseek_vl_2"
GOOGLE_GEMINI_2_0 = "gemini_2_0"
GOOGLE_GEMINI_2_5 = "gemini_2_5"
MOONDREAM = "moondream"
@@ -104,6 +111,8 @@ RESULT_TYPES: dict[VLM, type] = {
VLM.PALIGEMMA: str,
VLM.FLORENCE_2: dict,
VLM.QWEN_2_5_VL: str,
VLM.QWEN_3_VL: str,
VLM.DEEPSEEK_VL_2: str,
VLM.GOOGLE_GEMINI_2_0: str,
VLM.GOOGLE_GEMINI_2_5: str,
VLM.MOONDREAM: dict,
@@ -113,6 +122,8 @@ REQUIRED_ARGUMENTS: dict[VLM, list[str]] = {
VLM.PALIGEMMA: ["resolution_wh"],
VLM.FLORENCE_2: ["resolution_wh"],
VLM.QWEN_2_5_VL: ["input_wh", "resolution_wh"],
VLM.QWEN_3_VL: ["resolution_wh"],
VLM.DEEPSEEK_VL_2: ["resolution_wh"],
VLM.GOOGLE_GEMINI_2_0: ["resolution_wh"],
VLM.GOOGLE_GEMINI_2_5: ["resolution_wh"],
VLM.MOONDREAM: ["resolution_wh"],
@@ -122,6 +133,8 @@ ALLOWED_ARGUMENTS: dict[VLM, list[str]] = {
VLM.PALIGEMMA: ["resolution_wh", "classes"],
VLM.FLORENCE_2: ["resolution_wh"],
VLM.QWEN_2_5_VL: ["input_wh", "resolution_wh", "classes"],
VLM.QWEN_3_VL: ["resolution_wh", "classes"],
VLM.DEEPSEEK_VL_2: ["resolution_wh", "classes"],
VLM.GOOGLE_GEMINI_2_0: ["resolution_wh", "classes"],
VLM.GOOGLE_GEMINI_2_5: ["resolution_wh", "classes"],
VLM.MOONDREAM: ["resolution_wh"],
@@ -230,6 +243,51 @@ def from_paligemma(
return xyxy, class_id, class_name
def recover_truncated_qwen_2_5_vl_response(text: str) -> Any | None:
"""
Attempt to recover and parse a truncated or malformed JSON snippet from Qwen-2.5-VL
output.
This utility extracts a JSON-like portion from a string that may be truncated or
malformed, cleans trailing commas, and attempts to parse it into a Python object.
Args:
text (str): Raw text containing the JSON snippet possibly truncated or
incomplete.
Returns:
Parsed Python object (usually list) if recovery and parsing succeed;
otherwise `None`.
"""
try:
first_bracket = text.find("[")
if first_bracket == -1:
return None
snippet = text[first_bracket:]
last_brace = snippet.rfind("}")
if last_brace == -1:
return None
snippet = snippet[: last_brace + 1]
prefix_end = snippet.find("[")
if prefix_end == -1:
return None
prefix = snippet[: prefix_end + 1]
body = snippet[prefix_end + 1 :].rstrip()
if body.endswith(","):
body = body[:-1].rstrip()
repaired = prefix + body + "]"
return json.loads(repaired)
except Exception:
return None
def from_qwen_2_5_vl(
result: str,
input_wh: tuple[int, int],
@@ -237,7 +295,7 @@ def from_qwen_2_5_vl(
classes: list[str] | None = None,
) -> tuple[np.ndarray, np.ndarray | None, np.ndarray]:
"""
Parse and scale bounding boxes from Qwen-2.5-VL style JSON output.
Parse and rescale bounding boxes and class labels from Qwen-2.5-VL JSON output.
The JSON is expected to be enclosed in triple backticks with the format:
```json
@@ -248,38 +306,52 @@ def from_qwen_2_5_vl(
```
Args:
result: String containing the JSON snippet enclosed by triple backticks.
input_wh: (input_width, input_height) describing the original bounding box
scale.
resolution_wh: (output_width, output_height) to which we rescale the boxes.
classes: Optional list of valid class names. If provided, returned boxes/labels
are filtered to only those classes found here.
result (str): String containing Qwen-2.5-VL JSON bounding box and label data.
input_wh (tuple[int, int]): Width and height of the coordinate space where boxes
are normalized.
resolution_wh (tuple[int, int]): Target width and height to scale bounding
boxes.
classes (list[str] or None): Optional list of valid class names to filter
results. If provided, only boxes with labels in this list are returned.
Returns:
xyxy (np.ndarray): An array of shape `(n, 4)` containing
the bounding boxes coordinates in format `[x1, y1, x2, y2]`
class_id (Optional[np.ndarray]): An array of shape `(n,)` containing
the class indices for each bounding box (or None if `classes` is not
provided)
class_name (np.ndarray): An array of shape `(n,)` containing
the class labels for each bounding box
xyxy (np.ndarray): Array of shape `(N, 4)` with rescaled bounding boxes in
`(x_min, y_min, x_max, y_max)` format.
class_id (np.ndarray or None): Array of shape `(N,)` with indices of classes,
or `None` if no filtering applied.
class_name (np.ndarray): Array of shape `(N,)` with class names as strings.
"""
in_w, in_h = validate_resolution(input_wh)
out_w, out_h = validate_resolution(resolution_wh)
pattern = re.compile(r"```json\s*(.*?)\s*```", re.DOTALL)
text = result.strip()
text = re.sub(r"^```(json)?", "", text, flags=re.IGNORECASE).strip()
text = re.sub(r"```$", "", text).strip()
match = pattern.search(result)
if not match:
return np.empty((0, 4)), None, np.empty((0,), dtype=str)
json_snippet = match.group(1)
start = text.find("[")
end = text.rfind("]")
if start != -1 and end != -1 and end > start:
text = text[start : end + 1].strip()
try:
data = json.loads(json_snippet)
data = json.loads(text)
except json.JSONDecodeError:
return np.empty((0, 4)), None, np.empty((0,), dtype=str)
repaired = recover_truncated_qwen_2_5_vl_response(text)
if repaired is not None:
data = repaired
else:
try:
data = ast.literal_eval(text)
except (ValueError, SyntaxError, TypeError):
return (
np.empty((0, 4)),
np.empty((0,), dtype=int),
np.empty((0,), dtype=str),
)
if not isinstance(data, list):
return (np.empty((0, 4)), np.empty((0,), dtype=int), np.empty((0,), dtype=str))
boxes_list = []
labels_list = []
@@ -291,7 +363,7 @@ def from_qwen_2_5_vl(
labels_list.append(item["label"])
if not boxes_list:
return np.empty((0, 4)), None, np.empty((0,), dtype=str)
return (np.empty((0, 4)), np.empty((0,), dtype=int), np.empty((0,), dtype=str))
xyxy = np.array(boxes_list, dtype=float)
class_name = np.array(labels_list, dtype=str)
@@ -310,6 +382,109 @@ def from_qwen_2_5_vl(
return xyxy, class_id, class_name
def from_qwen_3_vl(
result: str,
resolution_wh: tuple[int, int],
classes: list[str] | None = None,
) -> tuple[np.ndarray, np.ndarray | None, np.ndarray]:
"""
Parse and scale bounding boxes from Qwen-3-VL style JSON output.
Args:
result (str): String containing the Qwen-3-VL JSON output.
resolution_wh (tuple[int, int]): Target resolution `(width, height)` to
scale bounding boxes.
classes (list[str] or None): Optional list of valid classes to filter
results.
Returns:
xyxy (np.ndarray): Array of bounding boxes with shape `(N, 4)` in
`(x_min, y_min, x_max, y_max)` format scaled to `resolution_wh`.
class_id (np.ndarray or None): Array of class indices for each box, or
None if no filtering by classes.
class_name (np.ndarray): Array of class names as strings.
"""
return from_qwen_2_5_vl(
result=result,
input_wh=(1000, 1000),
resolution_wh=resolution_wh,
classes=classes,
)
def from_deepseek_vl_2(
result: str, resolution_wh: tuple[int, int], classes: list[str] | None = None
) -> tuple[np.ndarray, np.ndarray | None, np.ndarray]:
"""
Parse bounding boxes from deepseek-vl2-formatted text, scale them to the specified
resolution, and optionally filter by classes.
The DeepSeek-VL2 output typically contains pairs of <|ref|> ... <|/ref|> labels
and <|det|> ... <|/det|> bounding box definitions. Each <|det|> section may
contain one or more bounding boxes in the form [[x1, y1, x2, y2], [x1, y1, x2, y2], ...]
(scaled to 0..999). For example:
```
<|ref|>The giraffe at the back<|/ref|><|det|>[[580, 270, 999, 904]]<|/det|><|ref|>The giraffe at the front<|/ref|><|det|>[[26, 31, 632, 998]]<|/det|><|end▁of▁sentence|>
```
Args:
result: String containing deepseek-vl2-formatted locations and labels.
resolution_wh: Tuple (width, height) to which we scale the box coordinates.
classes: Optional list of valid class names. If provided, boxes and labels not
in this list are filtered out.
Returns:
xyxy (np.ndarray): An array of shape `(n, 4)` containing
the bounding boxes coordinates in format `[x1, y1, x2, y2]`.
class_id (Optional[np.ndarray]): An array of shape `(n,)` containing
the class indices for each bounding box (or `None` if classes is not
provided).
class_name (np.ndarray): An array of shape `(n,)` containing
the class labels for each bounding box.
""" # noqa: E501
width, height = resolution_wh
label_segments = re.findall(r"<\|ref\|>(.*?)<\|/ref\|>", result, flags=re.S)
detection_segments = re.findall(r"<\|det\|>(.*?)<\|/det\|>", result, flags=re.S)
if len(label_segments) != len(detection_segments):
raise ValueError(
f"Number of ref tags ({len(label_segments)}) "
f"and det tags ({len(detection_segments)}) in the result must be equal."
)
xyxy, class_name_list = [], []
for label, detection_blob in zip(label_segments, detection_segments):
current_class_name = label.strip()
for box in re.findall(r"\[(.*?)\]", detection_blob):
x1, y1, x2, y2 = map(float, box.strip("[]").split(","))
xyxy.append(
[
(x1 / 999 * width),
(y1 / 999 * height),
(x2 / 999 * width),
(y2 / 999 * height),
]
)
class_name_list.append(current_class_name)
xyxy = np.array(xyxy, dtype=np.float32)
class_name = np.array(class_name_list)
if classes is not None:
mask = np.array([name in classes for name in class_name], dtype=bool)
xyxy = xyxy[mask]
class_name = class_name[mask]
class_id = np.array([classes.index(name) for name in class_name])
else:
unique_classes = sorted(list(set(class_name)))
class_to_id = {name: i for i, name in enumerate(unique_classes)}
class_id = np.array([class_to_id[name] for name in class_name])
return xyxy, class_id, class_name
def from_florence_2(
result: dict, resolution_wh: tuple[int, int]
) -> tuple[np.ndarray, np.ndarray | None, np.ndarray | None, np.ndarray | None]:
@@ -400,7 +575,7 @@ def from_florence_2(
labels = np.array([result_string])
return xyxy, labels, None, None
assert False, f"Unimplemented task: {task}"
raise RuntimeError(f"Unimplemented task: {task}")
def from_google_gemini_2_0(
@@ -460,7 +635,7 @@ def from_google_gemini_2_0(
return np.empty((0, 4)), None, np.empty((0,), dtype=str)
labels = []
boxes_list = []
xyxy = []
for item in data:
if "box_2d" not in item or "label" not in item:
@@ -468,18 +643,16 @@ def from_google_gemini_2_0(
labels.append(item["label"])
box = item["box_2d"]
# Gemini bbox order is [y_min, x_min, y_max, x_max]
boxes_list.append(
denormalize_boxes(
np.array([box[1], box[0], box[3], box[2]]).astype(np.float64),
resolution_wh=(w, h),
normalization_factor=1000,
)
)
xyxy.append([box[1], box[0], box[3], box[2]])
if not boxes_list:
if len(xyxy) == 0:
return np.empty((0, 4)), None, np.empty((0,), dtype=str)
xyxy = np.array(boxes_list)
xyxy = denormalize_boxes(
np.array(xyxy, dtype=np.float64),
resolution_wh=(w, h),
normalization_factor=1000,
)
class_name = np.array(labels)
class_id = None
@@ -571,10 +744,10 @@ def from_google_gemini_2_5(
box = item["box_2d"]
# Gemini bbox order is [y_min, x_min, y_max, x_max]
absolute_bbox = denormalize_boxes(
np.array([box[1], box[0], box[3], box[2]]).astype(np.float64),
np.array([[box[1], box[0], box[3], box[2]]]).astype(np.float64),
resolution_wh=(w, h),
normalization_factor=1000,
)
)[0]
boxes_list.append(absolute_bbox)
if "mask" in item:
@@ -657,7 +830,7 @@ def from_google_gemini_2_5(
def from_moondream(
result: dict,
resolution_wh: tuple[int, int],
) -> tuple[np.ndarray]:
) -> np.ndarray:
"""
Parse and scale bounding boxes from moondream JSON output.
@@ -695,7 +868,7 @@ def from_moondream(
if "objects" not in result or not isinstance(result["objects"], list):
return np.empty((0, 4), dtype=float)
denormalize_xyxy = []
xyxy = []
for item in result["objects"]:
if not all(k in item for k in ["x_min", "y_min", "x_max", "y_max"]):
@@ -706,14 +879,12 @@ def from_moondream(
x_max = item["x_max"]
y_max = item["y_max"]
denormalize_xyxy.append(
denormalize_boxes(
np.array([x_min, y_min, x_max, y_max]).astype(np.float64),
resolution_wh=(w, h),
)
)
xyxy.append([x_min, y_min, x_max, y_max])
if not denormalize_xyxy:
if len(xyxy) == 0:
return np.empty((0, 4))
return np.array(denormalize_xyxy, dtype=float)
return denormalize_boxes(
np.array(xyxy).astype(np.float64),
resolution_wh=(w, h),
)
+13
View File
@@ -0,0 +1,13 @@
from typing import TypeVar
import numpy as np
from PIL import Image
ImageType = TypeVar("ImageType", np.ndarray, Image.Image)
"""
An image of type `np.ndarray` or `PIL.Image.Image`.
Unlike a `Union`, ensures the type remains consistent. If a function
takes an `ImageType` argument and returns an `ImageType`, when you
pass an `np.ndarray`, you will get an `np.ndarray` back.
"""
+32 -10
View File
@@ -346,28 +346,50 @@ def draw_image(
def calculate_optimal_text_scale(resolution_wh: tuple[int, int]) -> float:
"""
Calculate font scale based on the resolution of an image.
Calculate optimal font scale based on image resolution. Adjusts font scale
proportionally to the smallest dimension of the given image resolution for
consistent readability.
Parameters:
resolution_wh (Tuple[int, int]): A tuple representing the width and height
of the image.
Args:
resolution_wh (tuple[int, int]): (width, height) of the image in pixels
Returns:
float: The calculated font scale factor.
float: recommended font scale factor
Examples:
```python
import supervision as sv
sv.calculate_optimal_text_scale((1920, 1080))
# 1.08
sv.calculate_optimal_text_scale((640, 480))
# 0.48
```
"""
return min(resolution_wh) * 1e-3
def calculate_optimal_line_thickness(resolution_wh: tuple[int, int]) -> int:
"""
Calculate line thickness based on the resolution of an image.
Calculate optimal line thickness based on image resolution. Adjusts the line
thickness for readability depending on the smallest dimension of the provided
image resolution.
Parameters:
resolution_wh (Tuple[int, int]): A tuple representing the width and height
of the image.
Args:
resolution_wh (tuple[int, int]): (width, height) of the image in pixels
Returns:
int: The calculated line thickness in pixels.
int: recommended line thickness in pixels
Examples:
```python
import supervision as sv
sv.calculate_optimal_line_thickness((1920, 1080))
# 4
sv.calculate_optimal_line_thickness((640, 480))
# 2
```
"""
if min(resolution_wh) < 1080:
return 2
@@ -6,14 +6,14 @@ from logging import warn
import cv2
import numpy as np
from supervision.annotators.base import ImageType
from supervision.detection.utils.boxes import pad_boxes, spread_out_boxes
from supervision.draw.base import ImageType
from supervision.draw.color import Color
from supervision.draw.utils import draw_rounded_rectangle
from supervision.geometry.core import Rect
from supervision.keypoint.core import KeyPoints
from supervision.keypoint.skeletons import SKELETONS_BY_VERTEX_COUNT
from supervision.utils.conversion import ensure_cv2_image_for_annotation
from supervision.key_points.core import KeyPoints
from supervision.key_points.skeletons import SKELETONS_BY_VERTEX_COUNT
from supervision.utils.conversion import ensure_cv2_image_for_class_method
class BaseKeyPointAnnotator(ABC):
@@ -43,7 +43,7 @@ class VertexAnnotator(BaseKeyPointAnnotator):
self.color = color
self.radius = radius
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType:
"""
Annotates the given scene with skeleton vertices based on the provided key
@@ -120,7 +120,7 @@ class EdgeAnnotator(BaseKeyPointAnnotator):
self.thickness = thickness
self.edges = edges
@ensure_cv2_image_for_annotation
@ensure_cv2_image_for_class_method
def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType:
"""
Annotates the given scene by drawing lines between specified key points to form
@@ -10,7 +10,7 @@ import numpy.typing as npt
from supervision.config import CLASS_NAME_DATA_FIELD
from supervision.detection.core import Detections
from supervision.detection.utils.internal import get_data_item, is_data_equal
from supervision.validators import validate_keypoints_fields
from supervision.validators import validate_key_points_fields
@dataclass
@@ -23,7 +23,7 @@ class KeyPoints:
=== "Ultralytics"
Use [`sv.KeyPoints.from_ultralytics`](/latest/keypoint/core/#supervision.keypoint.core.KeyPoints.from_ultralytics)
Use [`sv.KeyPoints.from_ultralytics`](/latest/keypoint/core/#supervision.key_points.core.KeyPoints.from_ultralytics)
method, which accepts [YOLOv8-pose](https://docs.ultralytics.com/models/yolov8/), [YOLO11-pose](https://docs.ultralytics.com/models/yolo11/)
[pose](https://docs.ultralytics.com/tasks/pose/) result.
@@ -41,7 +41,7 @@ class KeyPoints:
=== "Inference"
Use [`sv.KeyPoints.from_inference`](/latest/keypoint/core/#supervision.keypoint.core.KeyPoints.from_inference)
Use [`sv.KeyPoints.from_inference`](/latest/keypoint/core/#supervision.key_points.core.KeyPoints.from_inference)
method, which accepts [Inference](https://inference.roboflow.com/) pose result.
```python
@@ -58,7 +58,7 @@ class KeyPoints:
=== "MediaPipe"
Use [`sv.KeyPoints.from_mediapipe`](/latest/keypoint/core/#supervision.keypoint.core.KeyPoints.from_mediapipe)
Use [`sv.KeyPoints.from_mediapipe`](/latest/keypoint/core/#supervision.key_points.core.KeyPoints.from_mediapipe)
method, which accepts [MediaPipe](https://github.com/google-ai-edge/mediapipe)
pose result.
@@ -89,10 +89,61 @@ class KeyPoints:
pose_landmarker_result, (image_width, image_height))
```
=== "Transformers"
Use [`sv.KeyPoints.from_transformers`](/latest/keypoint/core/#supervision.key_points.core.KeyPoints.from_transformers)
method, which accepts [ViTPose](https://huggingface.co/docs/transformers/en/model_doc/vitpose) result.
```python
from PIL import Image
import requests
import supervision as sv
import torch
from transformers import (
AutoProcessor,
RTDetrForObjectDetection,
VitPoseForPoseEstimation,
)
device = "cuda" if torch.cuda.is_available() else "cpu"
image = Image.open(<SOURCE_IMAGE_PATH>)
DETECTION_MODEL_ID = "PekingU/rtdetr_r50vd_coco_o365"
detection_processor = AutoProcessor.from_pretrained(DETECTION_MODEL_ID, use_fast=True)
detection_model = RTDetrForObjectDetection.from_pretrained(DETECTION_MODEL_ID, device_map=DEVICE)
inputs = detection_processor(images=frame, return_tensors="pt").to(DEVICE)
with torch.no_grad():
outputs = detection_model(**inputs)
target_size = torch.tensor([(frame.height, frame.width)])
results = detection_processor.post_process_object_detection(
outputs, target_sizes=target_size, threshold=0.3)
detections = sv.Detections.from_transformers(results[0])
boxes = sv.xyxy_to_xywh(detections[detections.class_id == 0].xyxy)
POSE_ESTIMATION_MODEL_ID = "usyd-community/vitpose-base-simple"
pose_estimation_processor = AutoProcessor.from_pretrained(POSE_ESTIMATION_MODEL_ID)
pose_estimation_model = VitPoseForPoseEstimation.from_pretrained(
POSE_ESTIMATION_MODEL_ID, device_map=DEVICE)
inputs = pose_estimation_processor(frame, boxes=[boxes], return_tensors="pt").to(DEVICE)
with torch.no_grad():
outputs = pose_estimation_model(**inputs)
results = pose_estimation_processor.post_process_pose_estimation(outputs, boxes=[boxes])
key_point = sv.KeyPoints.from_transformers(results[0])
```
Attributes:
xy (np.ndarray): An array of shape `(n, m, 2)` containing
`n` detected objects, each composed of `m` equally-sized
sets of keypoints, where each point is `[x, y]`.
sets of key points, where each point is `[x, y]`.
class_id (Optional[np.ndarray]): An array of shape
`(n,)` containing the class ids of the detected objects.
confidence (Optional[np.ndarray]): An array of shape
@@ -109,7 +160,7 @@ class KeyPoints:
data: dict[str, npt.NDArray[Any] | list] = field(default_factory=dict)
def __post_init__(self):
validate_keypoints_fields(
validate_key_points_fields(
xy=self.xy,
confidence=self.confidence,
class_id=self.class_id,
@@ -514,13 +565,13 @@ class KeyPoints:
return cls.empty()
@classmethod
def from_transformers(cls, transfomers_results: Any) -> KeyPoints:
def from_transformers(cls, transformers_results: Any) -> KeyPoints:
"""
Create a `sv.KeyPoints` object from the
[Transformers](https://github.com/huggingface/transformers) inference result.
Args:
transfomers_results (Any): The output of a
transformers_results (Any): The output of a
Transformers model containing instances with prediction data.
Returns:
@@ -545,9 +596,9 @@ class KeyPoints:
DETECTION_MODEL_ID = "PekingU/rtdetr_r50vd_coco_o365"
detection_processor = AutoProcessor.from_pretrained(DETECTION_MODEL_ID, use_fast=True)
detection_model = RTDetrForObjectDetection.from_pretrained(DETECTION_MODEL_ID, device_map=DEVICE)
detection_model = RTDetrForObjectDetection.from_pretrained(DETECTION_MODEL_ID, device_map=device)
inputs = detection_processor(images=frame, return_tensors="pt").to(DEVICE)
inputs = detection_processor(images=frame, return_tensors="pt").to(device)
with torch.no_grad():
outputs = detection_model(**inputs)
@@ -563,9 +614,9 @@ class KeyPoints:
pose_estimation_processor = AutoProcessor.from_pretrained(POSE_ESTIMATION_MODEL_ID)
pose_estimation_model = VitPoseForPoseEstimation.from_pretrained(
POSE_ESTIMATION_MODEL_ID, device_map=DEVICE)
POSE_ESTIMATION_MODEL_ID, device_map=device)
inputs = pose_estimation_processor(frame, boxes=[boxes], return_tensors="pt").to(DEVICE)
inputs = pose_estimation_processor(frame, boxes=[boxes], return_tensors="pt").to(device)
with torch.no_grad():
outputs = pose_estimation_model(**inputs)
@@ -576,8 +627,8 @@ class KeyPoints:
""" # noqa: E501 // docs
if "keypoints" in transfomers_results[0]:
if transfomers_results[0]["keypoints"].cpu().numpy().size == 0:
if "keypoints" in transformers_results[0]:
if transformers_results[0]["keypoints"].cpu().numpy().size == 0:
return cls.empty()
result_data = [
@@ -585,7 +636,7 @@ class KeyPoints:
result["keypoints"].cpu().numpy(),
result["scores"].cpu().numpy(),
)
for result in transfomers_results
for result in transformers_results
]
xy, scores = zip(*result_data)
@@ -599,55 +650,68 @@ class KeyPoints:
return cls.empty()
def __getitem__(
self, index: int | slice | list[int] | np.ndarray | str
) -> KeyPoints | list | np.ndarray | None:
"""
Get a subset of the `sv.KeyPoints` object or access an item from its data field.
When provided with an integer, slice, list of integers, or a numpy array, this
method returns a new `sv.KeyPoints` object that represents a subset of the
original `sv.KeyPoints`. When provided with a string, it accesses the
corresponding item in the data dictionary.
Args:
index (Union[int, slice, List[int], np.ndarray, str]): The index, indices,
or key to access a subset of the `sv.KeyPoints` or an item from the
data.
Returns:
A subset of the `sv.KeyPoints` object or an item from the data field.
Examples:
```python
import supervision as sv
key_points = sv.KeyPoints()
# access the first keypoint using an integer index
key_points[0]
# access the first 10 keypoints using index slice
key_points[0:10]
# access selected keypoints using a list of indices
key_points[[0, 2, 4]]
# access keypoints with selected class_id
key_points[key_points.class_id == 0]
# access keypoints with confidence greater than 0.5
key_points[key_points.confidence > 0.5]
```
"""
self, index: int | slice | list[int] | np.ndarray | tuple | str
) -> KeyPoints | np.ndarray | list | None:
if isinstance(index, str):
return self.data.get(index)
if isinstance(index, int):
index = [index]
if not isinstance(index, tuple):
index = (index, slice(None))
i, j = index
if isinstance(i, int):
i = [i]
if isinstance(i, list) and all(isinstance(x, bool) for x in i):
i = np.array(i)
if isinstance(j, list) and all(isinstance(x, bool) for x in j):
j = np.array(j)
if isinstance(i, np.ndarray) and i.dtype == bool:
i = np.flatnonzero(i)
if isinstance(j, np.ndarray) and j.dtype == bool:
j = np.flatnonzero(j)
if (
isinstance(i, (list, np.ndarray))
and isinstance(j, (list, np.ndarray))
and not np.isscalar(i)
and not np.isscalar(j)
):
i, j = np.ix_(i, j)
xy_selected = self.xy[i, j]
conf_selected = self.confidence[i, j] if self.confidence is not None else None
class_id_selected = self.class_id[i] if self.class_id is not None else None
data_selected = get_data_item(self.data, i)
if xy_selected.ndim == 1:
xy_selected = xy_selected.reshape(1, 1, 2)
if conf_selected is not None:
conf_selected = conf_selected.reshape(1, 1)
elif xy_selected.ndim == 2:
if np.isscalar(index[0]) or (
isinstance(index[0], np.ndarray) and index[0].ndim == 0
):
xy_selected = xy_selected[np.newaxis, ...]
if conf_selected is not None:
conf_selected = conf_selected[np.newaxis, ...]
elif np.isscalar(index[1]) or (
isinstance(index[1], np.ndarray) and index[1].ndim == 0
):
xy_selected = xy_selected[:, np.newaxis, :]
if conf_selected is not None:
conf_selected = conf_selected[:, np.newaxis]
return KeyPoints(
xy=self.xy[index],
confidence=self.confidence[index] if self.confidence is not None else None,
class_id=self.class_id[index] if self.class_id is not None else None,
data=get_data_item(self.data, index),
xy=xy_selected,
confidence=conf_selected,
class_id=class_id_selected,
data=data_selected,
)
def __setitem__(self, key: str, value: np.ndarray | list):
@@ -668,12 +732,12 @@ class KeyPoints:
model = YOLO('yolov8s.pt')
result = model(image)[0]
keypoints = sv.KeyPoints.from_ultralytics(result)
key_points = sv.KeyPoints.from_ultralytics(result)
keypoints['class_name'] = [
key_points['class_name'] = [
model.model.names[class_id]
for class_id
in keypoints.class_id
in key_points.class_id
]
```
"""
@@ -688,7 +752,7 @@ class KeyPoints:
@classmethod
def empty(cls) -> KeyPoints:
"""
Create an empty Keypoints object with no keypoints.
Create an empty KeyPoints object with no key points.
Returns:
An empty `sv.KeyPoints` object.
@@ -706,9 +770,9 @@ class KeyPoints:
"""
Returns `True` if the `KeyPoints` object is considered empty.
"""
empty_keypoints = KeyPoints.empty()
empty_keypoints.data = self.data
return self == empty_keypoints
empty_key_points = KeyPoints.empty()
empty_key_points.data = self.data
return self == empty_key_points
def as_detections(
self, selected_keypoint_indices: Iterable[int] | None = None
@@ -716,21 +780,21 @@ class KeyPoints:
"""
Convert a KeyPoints object to a Detections object. This
approximates the bounding box of the detected object by
taking the bounding box that fits all keypoints.
taking the bounding box that fits all key points.
Arguments:
selected_keypoint_indices (Optional[Iterable[int]]): The
indices of the keypoints to include in the bounding box
calculation. This helps focus on a subset of keypoints,
e.g. when some are occluded. Captures all keypoints by default.
indices of the key points to include in the bounding box
calculation. This helps focus on a subset of key points,
e.g. when some are occluded. Captures all key points by default.
Returns:
detections (Detections): The converted detections object.
Examples:
```python
keypoints = sv.KeyPoints.from_inference(...)
detections = keypoints.as_detections()
key_points = sv.KeyPoints.from_inference(...)
detections = key_points.as_detections()
```
"""
if self.is_empty():
+67 -20
View File
@@ -57,7 +57,11 @@ class MeanAveragePrecisionResult:
@property
def map50_95(self) -> float:
return self.mAP_scores.mean()
valid_scores = self.mAP_scores[self.mAP_scores > -1]
if len(valid_scores) > 0:
return valid_scores.mean()
else:
return -1
@property
def map50(self) -> float:
@@ -98,12 +102,12 @@ class MeanAveragePrecisionResult:
f"maxDets=100 ] = {self.map50:.3f}\n"
f"Average Precision (AP) @[ IoU=0.75 | area= all | "
f"maxDets=100 ] = {self.map75:.3f}\n"
f"Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] "
f"= {self.small_objects.map50_95:.3f}\n"
f"Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] \
= {self.medium_objects.map50_95:.3f}\n"
f"Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] \
= {self.large_objects.map50_95:.3f}"
f"Average Precision (AP) @[ IoU=0.50:0.95 | area= small | "
f"maxDets=100 ] = {self.small_objects.map50_95:.3f}\n"
f"Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | "
f"maxDets=100 ] = {self.medium_objects.map50_95:.3f}\n"
f"Average Precision (AP) @[ IoU=0.50:0.95 | area= large | "
f"maxDets=100 ] = {self.large_objects.map50_95:.3f}"
)
def to_pandas(self) -> pd.DataFrame:
@@ -421,6 +425,11 @@ class EvaluationDataset:
if not isinstance(predictions, list):
raise ValueError("results must be a list")
# Handle empty predictions
if len(predictions) == 0:
predictions_dataset.dataset["annotations"] = []
return predictions_dataset
ids = [pred["image_id"] for pred in predictions]
# Make sure the image ids from predictions exist in the current dataset
@@ -909,6 +918,35 @@ class COCOEvaluator:
np.array(score_at_recall)
)
self.results = {
"params": self.params,
"counts": [
num_iou_thresholds,
num_recall_thresholds,
num_categories,
num_area_ranges,
num_max_detections,
],
"date": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"precision": precision,
"recall": recall,
"scores": scores,
}
# Helper function to compute average precision while handling -1 sentinel values
def compute_average_precision(precision_slice):
"""Compute average precision while handling -1 sentinel values."""
masked = np.ma.masked_equal(precision_slice, -1)
if masked.count() == 0:
# All values are -1 (no data)
return np.full(num_iou_thresholds, -1), np.full(
(num_categories, num_iou_thresholds), -1
)
else:
mAP_scores = np.ma.filled(masked.mean(axis=(1, 2)), -1)
ap_per_class = np.ma.filled(masked.mean(axis=1), -1).transpose(1, 0)
return mAP_scores, ap_per_class
# Average precision over all sizes, 100 max detections
area_range_idx = list(ObjectSize).index(ObjectSize.ALL)
max_100_dets_idx = self.params.max_dets.index(100)
@@ -917,10 +955,9 @@ class COCOEvaluator:
:, :, :, area_range_idx, max_100_dets_idx
]
# mAP over thresholds (dimension=num_thresholds)
mAP_scores_all_sizes = average_precision_all_sizes.mean(axis=(1, 2))
# AP per class
ap_per_class_all_sizes = average_precision_all_sizes.mean(axis=1).transpose(
1, 0
# Use masked array to exclude -1 values when computing mean
mAP_scores_all_sizes, ap_per_class_all_sizes = compute_average_precision(
average_precision_all_sizes
)
# Average precision for SMALL objects and 100 max detections
@@ -928,24 +965,27 @@ class COCOEvaluator:
average_precision_small = precision[
:, :, :, small_area_range_idx, max_100_dets_idx
]
mAP_scores_small = average_precision_small.mean(axis=(1, 2))
ap_per_class_small = average_precision_small.mean(axis=1).transpose(1, 0)
mAP_scores_small, ap_per_class_small = compute_average_precision(
average_precision_small
)
# Average precision for MEDIUM objects and 100 max detections
medium_area_range_idx = list(ObjectSize).index(ObjectSize.MEDIUM)
average_precision_medium = precision[
:, :, :, medium_area_range_idx, max_100_dets_idx
]
mAP_scores_medium = average_precision_medium.mean(axis=(1, 2))
ap_per_class_medium = average_precision_medium.mean(axis=1).transpose(1, 0)
mAP_scores_medium, ap_per_class_medium = compute_average_precision(
average_precision_medium
)
# Average precision for LARGE objects and 100 max detections
large_area_range_idx = list(ObjectSize).index(ObjectSize.LARGE)
average_precision_large = precision[
:, :, :, large_area_range_idx, max_100_dets_idx
]
mAP_scores_large = average_precision_large.mean(axis=(1, 2))
ap_per_class_large = average_precision_large.mean(axis=1).transpose(1, 0)
mAP_scores_large, ap_per_class_large = compute_average_precision(
average_precision_large
)
self.results = {
"params": self.params,
@@ -1221,7 +1261,7 @@ class MeanAveragePrecision(Metric):
for image_id, image_targets in enumerate(targets):
if self._image_indices is not None:
image_id = self._image_indices[image_id]
for target in image_targets:
for target_idx, target in enumerate(image_targets):
xyxy = target[0] # or xyxy = prediction[0]; xyxy[2:4] -= xyxy[0:2]
xywh = [xyxy[0], xyxy[1], xyxy[2] - xyxy[0], xyxy[3] - xyxy[1]]
# Get "area" and "iscrowd" (default 0) from data
@@ -1231,13 +1271,20 @@ class MeanAveragePrecision(Metric):
category_id = self._class_mapping[target[3].item()]
else:
category_id = target[3].item()
# Use area from data if available (e.g., COCO datasets)
# Otherwise use Detections.area property
area = data.get("area") if data else None
if area is None:
area = image_targets.area[target_idx]
dict_annotation = {
"area": data.get("area", 0),
"area": area,
"iscrowd": data.get("iscrowd", 0),
"image_id": image_id,
"bbox": xywh,
"category_id": category_id,
"id": len(annotations), # incrementally increase the id
"id": len(annotations) + 1, # Start IDs from 1 (0 means no match)
}
annotations.append(dict_annotation)
# Category list
+1 -1
View File
@@ -247,7 +247,7 @@ class ByteTrack:
if strack_pool[i].state == TrackState.Tracked
]
dists = matching.iou_distance(r_tracked_stracks, detections_second)
matches, u_track, u_detection_second = matching.linear_assignment(
matches, u_track, _u_detection_second = matching.linear_assignment(
dists, thresh=0.5
)
for itracked, idet in matches:
+4 -4
View File
@@ -4,10 +4,10 @@ import cv2
import numpy as np
from PIL import Image
from supervision.annotators.base import ImageType
from supervision.draw.base import ImageType
def ensure_cv2_image_for_annotation(annotate_func):
def ensure_cv2_image_for_class_method(annotate_func):
"""
Decorates `BaseAnnotator.annotate` implementations, converts scene to
an image type used internally by the annotators, converts back when annotation
@@ -32,7 +32,7 @@ def ensure_cv2_image_for_annotation(annotate_func):
return wrapper
def ensure_cv2_image_for_processing(image_processing_fun):
def ensure_cv2_image_for_standalone_function(image_processing_fun):
"""
Decorates image processing functions that accept np.ndarray, converting `image` to
np.ndarray, converts back when processing is complete.
@@ -55,7 +55,7 @@ def ensure_cv2_image_for_processing(image_processing_fun):
return wrapper
def ensure_pil_image_for_annotation(annotate_func):
def ensure_pil_image_for_class_method(annotate_func):
"""
Decorates image processing functions that accept np.ndarray, converting `image` to
PIL image, converts back when processing is complete.
+309 -469
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -4,7 +4,7 @@ import cv2
import matplotlib.pyplot as plt
from PIL import Image
from supervision.annotators.base import ImageType
from supervision.draw.base import ImageType
from supervision.utils.conversion import pillow_to_cv2
@@ -98,7 +98,7 @@ def plot_images_grid(
" or reduce the number of images."
)
fig, axes = plt.subplots(nrows=nrows, ncols=ncols, figsize=size)
_fig, axes = plt.subplots(nrows=nrows, ncols=ncols, figsize=size)
for idx, ax in enumerate(axes.flat):
if idx < len(images):
+104 -39
View File
@@ -1,9 +1,11 @@
from __future__ import annotations
import threading
import time
from collections import deque
from collections.abc import Callable, Generator
from dataclasses import dataclass
from queue import Queue
import cv2
import numpy as np
@@ -196,63 +198,126 @@ def process_video(
source_path: str,
target_path: str,
callback: Callable[[np.ndarray, int], np.ndarray],
*,
max_frames: int | None = None,
prefetch: int = 32,
writer_buffer: int = 32,
show_progress: bool = False,
progress_message: str = "Processing video",
) -> None:
"""
Process a video file by applying a callback function on each frame
and saving the result to a target video file.
Process video frames asynchronously using a threaded pipeline.
This function orchestrates a three-stage pipeline to optimize video processing
throughput:
1. Reader thread: Continuously reads frames from the source video file and
enqueues them into a bounded queue (`frame_read_queue`). The queue size is
limited by the `prefetch` parameter to control memory usage.
2. Main thread (Processor): Dequeues frames from `frame_read_queue`, applies the
user-defined `callback` function to process each frame, then enqueues the
processed frames into another bounded queue (`frame_write_queue`) for writing.
The processing happens in the main thread, simplifying use of stateful objects
without synchronization.
3. Writer thread: Dequeues processed frames from `frame_write_queue` and writes
them sequentially to the output video file.
Args:
source_path (str): The path to the source video file.
target_path (str): The path to the target video file.
callback (Callable[[np.ndarray, int], np.ndarray]): A function that takes in
a numpy ndarray representation of a video frame and an
int index of the frame and returns a processed numpy ndarray
representation of the frame.
max_frames (Optional[int]): The maximum number of frames to process.
show_progress (bool): Whether to show a progress bar.
progress_message (str): The message to display in the progress bar.
source_path (str): Path to the input video file.
target_path (str): Path where the processed video will be saved.
callback (Callable[[numpy.ndarray, int], numpy.ndarray]): Function called for
each frame, accepting the frame as a numpy array and its zero-based index,
returning the processed frame.
max_frames (int | None): Optional maximum number of frames to process.
If None, the entire video is processed (default).
prefetch (int): Maximum number of frames buffered by the reader thread.
Controls memory use; default is 32.
writer_buffer (int): Maximum number of frames buffered before writing.
Controls output buffer size; default is 32.
show_progress (bool): Whether to display a tqdm progress bar during processing.
Default is False.
progress_message (str): Description shown in the progress bar.
Examples:
Returns:
None
Example:
```python
import cv2
import supervision as sv
from rfdetr import RFDETRMedium
def callback(scene: np.ndarray, index: int) -> np.ndarray:
...
model = RFDETRMedium()
def callback(frame, frame_index):
return model.predict(frame)
process_video(
source_path=<SOURCE_VIDEO_PATH>,
target_path=<TARGET_VIDEO_PATH>,
callback=callback
source_path="source.mp4",
target_path="target.mp4",
callback=frame_callback,
)
```
"""
source_video_info = VideoInfo.from_video_path(video_path=source_path)
video_frames_generator = get_video_frames_generator(
source_path=source_path, end=max_frames
video_info = VideoInfo.from_video_path(video_path=source_path)
total_frames = (
min(video_info.total_frames, max_frames)
if max_frames is not None
else video_info.total_frames
)
with VideoSink(target_path=target_path, video_info=source_video_info) as sink:
total_frames = (
min(source_video_info.total_frames, max_frames)
if max_frames is not None
else source_video_info.total_frames
frame_read_queue: Queue[tuple[int, np.ndarray] | None] = Queue(maxsize=prefetch)
frame_write_queue: Queue[np.ndarray | None] = Queue(maxsize=writer_buffer)
def reader_thread() -> None:
frame_generator = get_video_frames_generator(
source_path=source_path,
end=max_frames,
)
for index, frame in enumerate(
tqdm(
video_frames_generator,
total=total_frames,
disable=not show_progress,
desc=progress_message,
)
):
result_frame = callback(frame, index)
sink.write_frame(frame=result_frame)
else:
for index, frame in enumerate(video_frames_generator):
result_frame = callback(frame, index)
sink.write_frame(frame=result_frame)
for frame_index, frame in enumerate(frame_generator):
frame_read_queue.put((frame_index, frame))
frame_read_queue.put(None)
def writer_thread(video_sink: VideoSink) -> None:
while True:
frame = frame_write_queue.get()
if frame is None:
break
video_sink.write_frame(frame=frame)
reader_worker = threading.Thread(target=reader_thread, daemon=True)
with VideoSink(target_path=target_path, video_info=video_info) as video_sink:
writer_worker = threading.Thread(
target=writer_thread,
args=(video_sink,),
daemon=True,
)
reader_worker.start()
writer_worker.start()
progress_bar = tqdm(
total=total_frames,
disable=not show_progress,
desc=progress_message,
)
try:
while True:
read_item = frame_read_queue.get()
if read_item is None:
break
frame_index, frame = read_item
processed_frame = callback(frame, frame_index)
frame_write_queue.put(processed_frame)
progress_bar.update(1)
finally:
frame_write_queue.put(None)
reader_worker.join()
writer_worker.join()
progress_bar.close()
class FPSMonitor:
+3 -3
View File
@@ -53,7 +53,7 @@ def validate_confidence(confidence: Any, n: int) -> None:
)
def validate_keypoint_confidence(confidence: Any, n: int, m: int) -> None:
def validate_key_point_confidence(confidence: Any, n: int, m: int) -> None:
expected_shape = f"({n, m})"
actual_shape = str(getattr(confidence, "shape", None))
@@ -126,7 +126,7 @@ def validate_detections_fields(
validate_data(data, n)
def validate_keypoints_fields(
def validate_key_points_fields(
xy: Any,
class_id: Any,
confidence: Any,
@@ -136,7 +136,7 @@ def validate_keypoints_fields(
m = len(xy[0]) if len(xy) > 0 else 0
validate_xy(xy, n, m)
validate_class_id(class_id, n)
validate_keypoint_confidence(confidence, n, m)
validate_key_point_confidence(confidence, n, m)
validate_data(data, n)
+67 -3
View File
@@ -5,13 +5,13 @@ from contextlib import ExitStack as DoesNotRaise
import numpy as np
import pytest
from supervision.annotators.utils import ColorLookup, resolve_color_idx
from supervision.annotators.utils import ColorLookup, resolve_color_idx, wrap_text
from supervision.detection.core import Detections
from test.test_utils import mock_detections
@pytest.mark.parametrize(
"detections, detection_idx, color_lookup, expected_result, exception",
("detections", "detection_idx", "color_lookup", "expected_result", "exception"),
[
(
mock_detections(
@@ -97,7 +97,7 @@ from test.test_utils import mock_detections
def test_resolve_color_idx(
detections: Detections,
detection_idx: int,
color_lookup: ColorLookup,
color_lookup: ColorLookup | np.ndarray,
expected_result: int | None,
exception: Exception,
) -> None:
@@ -108,3 +108,67 @@ def test_resolve_color_idx(
color_lookup=color_lookup,
)
assert result == expected_result
@pytest.mark.parametrize(
("text", "max_line_length", "expected_result", "exception"),
[
(None, None, [""], DoesNotRaise()), # text is None
("", None, [""], DoesNotRaise()), # empty string
(" \t ", 3, [""], DoesNotRaise()), # whitespace-only (spaces + tab)
(12345, None, ["12345"], DoesNotRaise()), # plain integer
(-6789, None, ["-6789"], DoesNotRaise()), # negative integer
(np.int64(1000), None, ["1000"], DoesNotRaise()), # NumPy int64
([1, 2, 3], None, ["[1, 2, 3]"], DoesNotRaise()), # list to string
(
"When you play the game of thrones, you win or you die.\nFear cuts deeper than swords.\nA mind needs books as a sword needs a whetstone.", # noqa: E501
None,
[
"When you play the game of thrones, you win or you die.",
"Fear cuts deeper than swords.",
"A mind needs books as a sword needs a whetstone.",
],
DoesNotRaise(),
), # Game-of-Thrones quotes, multiline
("\n", None, [""], DoesNotRaise()), # single newline
(
"valarmorghulisvalardoharis",
6,
["valarm", "orghul", "isvala", "rdohar", "is"],
DoesNotRaise(),
), # long Valyrian phrase, wrapped
(
"Winter is coming\nFire and blood",
10,
[
"Winter is",
"coming",
"Fire and",
"blood",
],
DoesNotRaise(),
), # mix of short/long with newline
(
"What is dead may never die",
0,
None,
pytest.raises(ValueError),
), # width 0 - invalid
(
"A Lannister always pays his debts",
-1,
None,
pytest.raises(ValueError),
), # width -1 - invalid
(None, 10, [""], DoesNotRaise()), # text None, width set
],
)
def test_wrap_text(
text: object,
max_line_length: int | None,
expected_result: list[str],
exception: Exception,
) -> None:
with exception:
result = wrap_text(text=text, max_line_length=max_line_length)
assert result == expected_result
+1 -1
View File
@@ -9,7 +9,7 @@ from supervision.classification.core import Classifications
@pytest.mark.parametrize(
"class_id, confidence, k, expected_result, exception",
("class_id", "confidence", "k", "expected_result", "exception"),
[
(
np.array([0, 1, 2, 3, 4]),
+13 -7
View File
@@ -39,7 +39,7 @@ def mock_coco_annotation(
@pytest.mark.parametrize(
"coco_categories, expected_result, exception",
("coco_categories", "expected_result", "exception"),
[
([], [], DoesNotRaise()), # empty coco categories
(
@@ -87,7 +87,7 @@ def test_coco_categories_to_classes(
@pytest.mark.parametrize(
"classes, exception",
("classes", "exception"),
[
([], DoesNotRaise()), # empty classes
(["baseball cap"], DoesNotRaise()), # single class
@@ -104,7 +104,7 @@ def test_classes_to_coco_categories_and_back_to_classes(
@pytest.mark.parametrize(
"coco_annotations, expected_result, exception",
("coco_annotations", "expected_result", "exception"),
[
([], {}, DoesNotRaise()), # empty coco annotations
(
@@ -163,8 +163,14 @@ def test_group_coco_annotations_by_image_id(
@pytest.mark.parametrize(
"image_annotations, resolution_wh, with_masks, use_iscrowd, "
"expected_result, exception",
(
"image_annotations",
"resolution_wh",
"with_masks",
"use_iscrowd",
"expected_result",
"exception",
),
[
(
[],
@@ -601,7 +607,7 @@ def test_coco_annotations_to_detections(
@pytest.mark.parametrize(
"coco_categories, target_classes, expected_result, exception",
("coco_categories", "target_classes", "expected_result", "exception"),
[
([], [], {}, DoesNotRaise()), # empty coco categories
(
@@ -660,7 +666,7 @@ def test_build_coco_class_index_mapping(
@pytest.mark.parametrize(
"detections, image_id, annotation_id, expected_result, exception",
("detections", "image_id", "annotation_id", "expected_result", "exception"),
[
(
Detections(
+10 -3
View File
@@ -31,7 +31,7 @@ def are_xml_elements_equal(elem1, elem2):
@pytest.mark.parametrize(
"xyxy, name, polygon, expected_result, exception",
("xyxy", "name", "polygon", "expected_result", "exception"),
[
(
np.array([0, 0, 10, 10]),
@@ -71,7 +71,7 @@ def test_object_to_pascal_voc(
@pytest.mark.parametrize(
"polygon_element, expected_result, exception",
("polygon_element", "expected_result", "exception"),
[
(
ElementTree.fromstring(
@@ -116,7 +116,14 @@ NO_DETECTIONS = """<annotation></annotation>"""
@pytest.mark.parametrize(
"xml_string, classes, resolution_wh, force_masks, expected_result, exception",
(
"xml_string",
"classes",
"resolution_wh",
"force_masks",
"expected_result",
"exception",
),
[
(
ONE_CLASS_ONE_BBOX,
+4 -5
View File
@@ -31,7 +31,7 @@ def _arrays_almost_equal(
@pytest.mark.parametrize(
"lines, expected_result, exception",
("lines", "expected_result", "exception"),
[
([], False, DoesNotRaise()), # empty yolo annotation file
(
@@ -44,7 +44,6 @@ def _arrays_almost_equal(
False,
DoesNotRaise(),
), # yolo annotation file with two lines with box
(["0 0.5 0.5 0.2 0.2"], False, DoesNotRaise()),
(
["0 0.4 0.4 0.6 0.4 0.6 0.6 0.4 0.6"],
True,
@@ -66,7 +65,7 @@ def test_with_mask(
@pytest.mark.parametrize(
"lines, resolution_wh, with_masks, expected_result, exception",
("lines", "resolution_wh", "with_masks", "expected_result", "exception"),
[
(
[],
@@ -190,7 +189,7 @@ def test_yolo_annotations_to_detections(
@pytest.mark.parametrize(
"image_name, expected_result, exception",
("image_name", "expected_result", "exception"),
[
("image.png", "image.txt", DoesNotRaise()), # simple png image
("image.jpeg", "image.txt", DoesNotRaise()), # simple jpeg image
@@ -211,7 +210,7 @@ def test_image_name_to_annotation_name(
@pytest.mark.parametrize(
"xyxy, class_id, image_shape, polygon, expected_result, exception",
("xyxy", "class_id", "image_shape", "polygon", "expected_result", "exception"),
[
(
np.array([100, 100, 200, 200], dtype=np.float32),
+1 -1
View File
@@ -9,7 +9,7 @@ from test.test_utils import mock_detections
@pytest.mark.parametrize(
"dataset_list, expected_result, exception",
("dataset_list", "expected_result", "exception"),
[
(
[],
+6 -6
View File
@@ -22,7 +22,7 @@ T = TypeVar("T")
@pytest.mark.parametrize(
"data, train_ratio, random_state, shuffle, expected_result, exception",
("data", "train_ratio", "random_state", "shuffle", "expected_result", "exception"),
[
([], 0.5, None, False, ([], []), DoesNotRaise()), # empty data
(
@@ -94,7 +94,7 @@ def test_train_test_split(
@pytest.mark.parametrize(
"class_lists, expected_result, exception",
("class_lists", "expected_result", "exception"),
[
([], [], DoesNotRaise()), # empty class lists
(
@@ -128,7 +128,7 @@ def test_merge_class_maps(
@pytest.mark.parametrize(
"source_classes, target_classes, expected_result, exception",
("source_classes", "target_classes", "expected_result", "exception"),
[
([], [], {}, DoesNotRaise()), # empty class lists
([], ["dog", "person"], {}, DoesNotRaise()), # empty source class list
@@ -178,7 +178,7 @@ def test_build_class_index_mapping(
@pytest.mark.parametrize(
"source_to_target_mapping, detections, expected_result, exception",
("source_to_target_mapping", "detections", "expected_result", "exception"),
[
(
{},
@@ -238,7 +238,7 @@ def test_map_detections_class_id(
@pytest.mark.parametrize(
"mask, expected_rle, exception",
("mask", "expected_rle", "exception"),
[
(
np.zeros((3, 3)).astype(bool),
@@ -297,7 +297,7 @@ def test_mask_to_rle(
@pytest.mark.parametrize(
"rle, resolution_wh, expected_mask, exception",
("rle", "resolution_wh", "expected_mask", "exception"),
[
(
np.array([9]),
+5 -5
View File
@@ -130,7 +130,7 @@ TEST_DET_DIFFERENT_METADATA = Detections(
@pytest.mark.parametrize(
"detections, index, expected_result, exception",
("detections", "index", "expected_result", "exception"),
[
(
DETECTIONS,
@@ -244,7 +244,7 @@ def test_getitem(
@pytest.mark.parametrize(
"detections_list, expected_result, exception",
("detections_list", "expected_result", "exception"),
[
([], Detections.empty(), DoesNotRaise()), # empty detections list
(
@@ -516,7 +516,7 @@ def test_merge(
@pytest.mark.parametrize(
"detections, anchor, expected_result, exception",
("detections", "anchor", "expected_result", "exception"),
[
(
Detections.empty(),
@@ -598,7 +598,7 @@ def test_get_anchor_coordinates(
@pytest.mark.parametrize(
"detections_a, detections_b, expected_result",
("detections_a", "detections_b", "expected_result"),
[
(
Detections.empty(),
@@ -649,7 +649,7 @@ def test_equal(
@pytest.mark.parametrize(
"detection_1, detection_2, expected_result, exception",
("detection_1", "detection_2", "expected_result", "exception"),
[
(
mock_detections(
+16 -6
View File
@@ -9,9 +9,14 @@ from test.test_utils import mock_detections
@pytest.mark.parametrize(
"detections, custom_data, "
"second_detections, second_custom_data, "
"file_name, expected_result",
(
"detections",
"custom_data",
"second_detections",
"second_custom_data",
"file_name",
"expected_result",
),
[
(
mock_detections(
@@ -206,9 +211,14 @@ def test_csv_sink(
@pytest.mark.parametrize(
"detections, custom_data, "
"second_detections, second_custom_data, "
"file_name, expected_result",
(
"detections",
"custom_data",
"second_detections",
"second_custom_data",
"file_name",
"expected_result",
),
[
(
mock_detections(
+8 -3
View File
@@ -9,9 +9,14 @@ from test.test_utils import mock_detections
@pytest.mark.parametrize(
"detections, custom_data, "
"second_detections, second_custom_data, "
"file_name, expected_result",
(
"detections",
"custom_data",
"second_detections",
"second_custom_data",
"file_name",
"expected_result",
),
[
(
mock_detections(
+36 -11
View File
@@ -10,7 +10,7 @@ from test.test_utils import mock_detections
@pytest.mark.parametrize(
"vector, expected_result, exception",
("vector", "expected_result", "exception"),
[
(
Vector(start=Point(x=0.0, y=0.0), end=Point(x=0.0, y=0.0)),
@@ -75,7 +75,7 @@ def test_calculate_region_of_interest_limits(
@pytest.mark.parametrize(
"vector, xyxy_sequence, expected_crossed_in, expected_crossed_out",
("vector", "xyxy_sequence", "expected_crossed_in", "expected_crossed_out"),
[
( # Vertical line, simple crossing
Vector(Point(0, 0), Point(0, 10)),
@@ -260,8 +260,13 @@ def test_line_zone_one_detection_default_anchors(
@pytest.mark.parametrize(
"vector, xyxy_sequence, triggering_anchors, expected_crossed_in, "
"expected_crossed_out",
(
"vector",
"xyxy_sequence",
"triggering_anchors",
"expected_crossed_in",
"expected_crossed_out",
),
[
( # Scrape line, left side, corner anchors
Vector(Point(0, 0), Point(10, 0)),
@@ -425,8 +430,14 @@ def test_line_zone_one_detection(
@pytest.mark.parametrize(
"vector, xyxy_sequence, anchors, expected_crossed_in, "
"expected_crossed_out, exception",
(
"vector",
"xyxy_sequence",
"anchors",
"expected_crossed_in",
"expected_crossed_out",
"exception",
),
[
( # One stays, one crosses
Vector(Point(0, 0), Point(10, 0)),
@@ -494,8 +505,14 @@ def test_line_zone_multiple_detections(
@pytest.mark.parametrize(
"vector, xyxy_sequence, triggering_anchors, minimum_crossing_threshold, "
"expected_crossed_in, expected_crossed_out",
(
"vector",
"xyxy_sequence",
"triggering_anchors",
"minimum_crossing_threshold",
"expected_crossed_in",
"expected_crossed_out",
),
[
( # Detection lingers around line, all crosses counted
Vector(Point(0, 0), Point(10, 0)),
@@ -610,9 +627,17 @@ def test_line_zone_one_detection_long_horizon(
@pytest.mark.parametrize(
"vector, xyxy_sequence, anchors, minimum_crossing_threshold, "
"expected_crossed_in, expected_crossed_out, expected_count_in, "
"expected_count_out, exception",
(
"vector",
"xyxy_sequence",
"anchors",
"minimum_crossing_threshold",
"expected_crossed_in",
"expected_crossed_out",
"expected_count_in",
"expected_count_out",
"exception",
),
[
( # One stays, one crosses, one disappears before crossing
Vector(Point(0, 0), Point(10, 0)),
@@ -22,7 +22,7 @@ ANNOTATED_SCENE_0_5_OPACITY = sv.draw_filled_polygon(
@pytest.mark.parametrize(
"scene, polygon_zone_annotator, expected_results",
("scene", "polygon_zone_annotator", "expected_results"),
[
(
SCENE,
+2 -2
View File
@@ -29,7 +29,7 @@ POLYGON = np.array([[100, 100], [200, 100], [200, 200], [100, 200]])
@pytest.mark.parametrize(
"detections, polygon_zone, expected_results, exception",
("detections", "polygon_zone", "expected_results", "exception"),
[
(
DETECTIONS,
@@ -90,7 +90,7 @@ def test_polygon_zone_trigger(
@pytest.mark.parametrize(
"polygon, triggering_anchors, exception",
("polygon", "triggering_anchors", "exception"),
[
(POLYGON, [sv.Position.CENTER], DoesNotRaise()),
(
+157 -42
View File
@@ -6,7 +6,10 @@ from contextlib import nullcontext as does_not_raise
import numpy as np
import pytest
from supervision.config import CLASS_NAME_DATA_FIELD
from supervision.detection.core import Detections
from supervision.detection.vlm import (
VLM,
from_florence_2,
from_google_gemini_2_0,
from_google_gemini_2_5,
@@ -17,7 +20,7 @@ from supervision.detection.vlm import (
@pytest.mark.parametrize(
"exception, result, resolution_wh, classes, expected_results",
("exception", "result", "resolution_wh", "classes", "expected_results"),
[
(
does_not_raise(),
@@ -199,7 +202,7 @@ def test_from_paligemma(
@pytest.mark.parametrize(
"exception, result, input_wh, resolution_wh, classes, expected_results",
("exception", "result", "input_wh", "resolution_wh", "classes", "expected_results"),
[
(
does_not_raise(),
@@ -317,6 +320,43 @@ def test_from_paligemma(
np.array(["dog"], dtype=str),
),
), # out-of-bounds box
(
does_not_raise(),
"""[
{'bbox_2d': [10, 20, 110, 120], 'label': 'cat'}
]""",
(640, 640),
(1280, 720),
None,
(
np.array([[20.0, 22.5, 220.0, 135.0]]),
None,
np.array(["cat"], dtype=str),
),
), # python-style list, single quotes, no fences
(
does_not_raise(),
"""```json
[
{"bbox_2d": [0, 0, 64, 64], "label": "dog"},
{"bbox_2d": [10, 20, 110, 120], "label": "cat"},
{"bbox_2d": [30, 40, 130, 140], "label":
""",
(640, 640),
(640, 640),
None,
(
np.array(
[
[0.0, 0.0, 64.0, 64.0],
[10.0, 20.0, 110.0, 120.0],
],
dtype=float,
),
None,
np.array(["dog", "cat"], dtype=str),
),
), # truncated response, last object unfinished, previous ones recovered
(
pytest.raises(ValueError),
"""```json
@@ -327,8 +367,8 @@ def test_from_paligemma(
(0, 640),
(1280, 720),
None,
None, # won't be compared because we expect an exception
), # zero input width -> ValueError
None, # invalid input_wh
),
(
pytest.raises(ValueError),
"""```json
@@ -339,8 +379,8 @@ def test_from_paligemma(
(640, 640),
(1280, -100),
None,
None,
), # negative resolution height -> ValueError
None, # invalid resolution_wh
),
],
)
def test_from_qwen_2_5_vl(
@@ -365,7 +405,7 @@ def test_from_qwen_2_5_vl(
@pytest.mark.parametrize(
"exception, result, resolution_wh, classes, expected_results",
("exception", "result", "resolution_wh", "classes", "expected_results"),
[
(
does_not_raise(),
@@ -504,7 +544,7 @@ def test_from_google_gemini(
@pytest.mark.parametrize(
"exception, result, resolution_wh, expected_results",
("exception", "result", "resolution_wh", "expected_results"),
[
(
does_not_raise(),
@@ -604,7 +644,7 @@ def test_from_moondream(
@pytest.mark.parametrize(
"florence_result, resolution_wh, expected_results, exception",
("florence_result", "resolution_wh", "expected_results", "exception"),
[
( # Object detection: empty
{"<OD>": {"bboxes": [], "labels": []}},
@@ -752,38 +792,6 @@ def test_from_moondream(
),
DoesNotRaise(),
),
( # Referring Expression Segmentation
{
"<REFERRING_EXPRESSION_SEGMENTATION>": {
"polygons": [[[1, 1, 2, 1, 2, 2, 1, 2]]],
"labels": [""],
}
},
(10, 10),
(
np.array([[1.0, 1.0, 2.0, 2.0]], dtype=np.float32),
None,
np.array(
[
[
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
]
],
dtype=bool,
),
None,
),
DoesNotRaise(),
),
( # OCR: unsupported
{"<OCR>": "A"},
(10, 10),
@@ -888,7 +896,7 @@ def test_florence_2(
@pytest.mark.parametrize(
"exception, result, resolution_wh, classes, expected_results",
("exception", "result", "resolution_wh", "classes", "expected_results"),
[
(
does_not_raise(),
@@ -1122,3 +1130,110 @@ def test_from_google_gemini_2_5(
assert masks is not None
assert masks.shape == expected_results[4].shape
assert np.array_equal(masks, expected_results[4])
@pytest.mark.parametrize(
("exception", "result", "resolution_wh", "classes", "expected_detections"),
[
(
pytest.raises(ValueError),
"",
(100, 100),
None,
None,
), # empty text
(
pytest.raises(ValueError),
"random text",
(100, 100),
None,
None,
), # random text
(
does_not_raise(),
"<|ref|>cat<|/ref|><|det|>[[100, 200, 300, 400]]<|/det|>",
(1000, 1000),
None,
Detections(
xyxy=np.array([[100.1, 200.2, 300.3, 400.4]]),
class_id=np.array([0]),
data={CLASS_NAME_DATA_FIELD: np.array(["cat"])},
),
), # single box, no classes
(
does_not_raise(),
"<|ref|>cat<|/ref|><|det|>[[100, 200, 300, 400]]<|/det|>",
(1000, 1000),
["cat", "dog"],
Detections(
xyxy=np.array([[100.1, 200.2, 300.3, 400.4]]),
class_id=np.array([0]),
data={CLASS_NAME_DATA_FIELD: np.array(["cat"])},
),
), # single box, with classes
(
does_not_raise(),
"<|ref|>person<|/ref|><|det|>[[100, 200, 300, 400]]<|/det|>",
(1000, 1000),
["cat", "dog"],
Detections.empty(),
), # single box, wrong class
(
does_not_raise(),
(
"<|ref|>cat<|/ref|><|det|>[[100, 200, 300, 400]]<|/det|>"
"<|ref|>dog<|/ref|><|det|>[[500, 600, 700, 800]]<|/det|>"
),
(1000, 1000),
["cat"],
Detections(
xyxy=np.array([[100.1, 200.2, 300.3, 400.4]]),
class_id=np.array([0]),
data={CLASS_NAME_DATA_FIELD: np.array(["cat"])},
),
), # multiple boxes, one class correct
(
pytest.raises(ValueError),
"<|ref|>cat<|/ref|>",
(100, 100),
None,
None,
), # only ref
(
pytest.raises(ValueError),
"<|det|>[[100, 200, 300, 400]]<|/det|>",
(100, 100),
None,
None,
), # only det
],
)
def test_from_deepseek_vl_2(
exception,
result: str,
resolution_wh: tuple[int, int],
classes: list[str] | None,
expected_detections: Detections,
):
with exception:
detections = Detections.from_vlm(
vlm=VLM.DEEPSEEK_VL_2,
result=result,
resolution_wh=resolution_wh,
classes=classes,
)
if expected_detections is None:
return
assert len(detections) == len(expected_detections)
if len(detections) == 0:
return
assert np.allclose(detections.xyxy, expected_detections.xyxy, atol=1e-1)
assert np.array_equal(detections.class_id, expected_detections.class_id)
assert np.array_equal(
detections.data[CLASS_NAME_DATA_FIELD],
expected_detections.data[CLASS_NAME_DATA_FIELD],
)
+108 -97
View File
@@ -1,13 +1,10 @@
from __future__ import annotations
from contextlib import ExitStack as DoesNotRaise
import numpy as np
import pytest
from supervision.detection.core import Detections
from supervision.detection.tools.inference_slicer import InferenceSlicer
from supervision.detection.utils.iou_and_nms import OverlapFilter
@pytest.fixture
@@ -21,53 +18,9 @@ def mock_callback():
@pytest.mark.parametrize(
"slice_wh, overlap_ratio_wh, overlap_wh, expected_overlap, exception",
("resolution_wh", "slice_wh", "overlap_wh", "expected_offsets"),
[
# Valid case: explicit overlap_wh in pixels
((128, 128), None, (26, 26), (26, 26), DoesNotRaise()),
# Valid case: overlap_wh in pixels
((128, 128), None, (20, 20), (20, 20), DoesNotRaise()),
# Invalid case: negative overlap_wh, should raise ValueError
((128, 128), None, (-10, 20), None, pytest.raises(ValueError)),
# Invalid case: no overlaps defined
((128, 128), None, None, None, pytest.raises(ValueError)),
# Valid case: overlap_wh = 50 pixels
((256, 256), None, (50, 50), (50, 50), DoesNotRaise()),
# Valid case: overlap_wh = 60 pixels
((200, 200), None, (60, 60), (60, 60), DoesNotRaise()),
# Valid case: small overlap_wh values
((100, 100), None, (0.1, 0.1), (0.1, 0.1), DoesNotRaise()),
# Invalid case: negative overlap_wh values
((128, 128), None, (-10, -10), None, pytest.raises(ValueError)),
# Invalid case: overlap_wh greater than slice size
((128, 128), None, (150, 150), (150, 150), DoesNotRaise()),
# Valid case: zero overlap
((128, 128), None, (0, 0), (0, 0), DoesNotRaise()),
],
)
def test_inference_slicer_overlap(
mock_callback,
slice_wh: tuple[int, int],
overlap_ratio_wh: tuple[float, float] | None,
overlap_wh: tuple[int, int] | None,
expected_overlap: tuple[int, int] | None,
exception: Exception,
) -> None:
with exception:
slicer = InferenceSlicer(
callback=mock_callback,
slice_wh=slice_wh,
overlap_ratio_wh=overlap_ratio_wh,
overlap_wh=overlap_wh,
overlap_filter=OverlapFilter.NONE,
)
assert slicer.overlap_wh == expected_overlap
@pytest.mark.parametrize(
"resolution_wh, slice_wh, overlap_wh, expected_offsets",
[
# Case 1: No overlap, exact slices fit within image dimensions
# Case 1: Square image, square slices, no overlap
(
(256, 256),
(128, 128),
@@ -81,7 +34,7 @@ def test_inference_slicer_overlap(
]
),
),
# Case 2: Overlap of 64 pixels in both directions
# Case 2: Square image, square slices, non-zero overlap
(
(256, 256),
(128, 128),
@@ -91,96 +44,154 @@ def test_inference_slicer_overlap(
[0, 0, 128, 128],
[64, 0, 192, 128],
[128, 0, 256, 128],
[192, 0, 256, 128],
[0, 64, 128, 192],
[64, 64, 192, 192],
[128, 64, 256, 192],
[192, 64, 256, 192],
[0, 128, 128, 256],
[64, 128, 192, 256],
[128, 128, 256, 256],
[192, 128, 256, 256],
[0, 192, 128, 256],
[64, 192, 192, 256],
[128, 192, 256, 256],
[192, 192, 256, 256],
]
),
),
# Case 3: Image not perfectly divisible by slice size (no overlap)
# Case 3: Rectangle image (horizontal), square slices, no overlap
(
(300, 300),
(128, 128),
(192, 128),
(64, 64),
(0, 0),
np.array(
[
[0, 0, 128, 128],
[128, 0, 256, 128],
[256, 0, 300, 128],
[0, 128, 128, 256],
[128, 128, 256, 256],
[256, 128, 300, 256],
[0, 256, 128, 300],
[128, 256, 256, 300],
[256, 256, 300, 300],
[0, 0, 64, 64],
[64, 0, 128, 64],
[128, 0, 192, 64],
[0, 64, 64, 128],
[64, 64, 128, 128],
[128, 64, 192, 128],
]
),
),
# Case 4: Overlap of 32 pixels, image not perfectly divisible by slice size
# Case 4: Rectangle image (horizontal), square slices, non-zero overlap
(
(300, 300),
(128, 128),
(192, 128),
(64, 64),
(32, 32),
np.array(
[
[0, 0, 128, 128],
[96, 0, 224, 128],
[192, 0, 300, 128],
[288, 0, 300, 128],
[0, 96, 128, 224],
[96, 96, 224, 224],
[192, 96, 300, 224],
[288, 96, 300, 224],
[0, 192, 128, 300],
[96, 192, 224, 300],
[192, 192, 300, 300],
[288, 192, 300, 300],
[0, 288, 128, 300],
[96, 288, 224, 300],
[192, 288, 300, 300],
[288, 288, 300, 300],
[0, 0, 64, 64],
[32, 0, 96, 64],
[64, 0, 128, 64],
[96, 0, 160, 64],
[128, 0, 192, 64],
[0, 32, 64, 96],
[32, 32, 96, 96],
[64, 32, 128, 96],
[96, 32, 160, 96],
[128, 32, 192, 96],
[0, 64, 64, 128],
[32, 64, 96, 128],
[64, 64, 128, 128],
[96, 64, 160, 128],
[128, 64, 192, 128],
]
),
),
# Case 5: Image smaller than slice size (no overlap)
# Case 5: Rectangle image (vertical), square slices, no overlap
(
(100, 100),
(128, 128),
(128, 192),
(64, 64),
(0, 0),
np.array(
[
[0, 0, 100, 100],
[0, 0, 64, 64],
[64, 0, 128, 64],
[0, 64, 64, 128],
[64, 64, 128, 128],
[0, 128, 64, 192],
[64, 128, 128, 192],
]
),
),
# Case 6: Rectangle image (vertical), square slices, non-zero overlap
(
(128, 192),
(64, 64),
(32, 32),
np.array(
[
[0, 0, 64, 64],
[32, 0, 96, 64],
[64, 0, 128, 64],
[0, 32, 64, 96],
[32, 32, 96, 96],
[64, 32, 128, 96],
[0, 64, 64, 128],
[32, 64, 96, 128],
[64, 64, 128, 128],
[0, 96, 64, 160],
[32, 96, 96, 160],
[64, 96, 128, 160],
[0, 128, 64, 192],
[32, 128, 96, 192],
[64, 128, 128, 192],
]
),
),
# Case 7: Square image, rectangular slices (horizontal), no overlap
(
(160, 160),
(80, 40),
(0, 0),
np.array(
[
[0, 0, 80, 40],
[80, 0, 160, 40],
[0, 40, 80, 80],
[80, 40, 160, 80],
[0, 80, 80, 120],
[80, 80, 160, 120],
[0, 120, 80, 160],
[80, 120, 160, 160],
]
),
),
# Case 8: Square image, rectangular slices (vertical), non-zero overlap
(
(160, 160),
(40, 80),
(10, 20),
np.array(
[
[0, 0, 40, 80],
[30, 0, 70, 80],
[60, 0, 100, 80],
[90, 0, 130, 80],
[120, 0, 160, 80],
[0, 60, 40, 140],
[30, 60, 70, 140],
[60, 60, 100, 140],
[90, 60, 130, 140],
[120, 60, 160, 140],
[0, 80, 40, 160],
[30, 80, 70, 160],
[60, 80, 100, 160],
[90, 80, 130, 160],
[120, 80, 160, 160],
]
),
),
# Case 6: Overlap_wh is greater than the slice size
((256, 256), (128, 128), (150, 150), np.array([]).reshape(0, 4)),
],
)
def test_generate_offset(
resolution_wh: tuple[int, int],
slice_wh: tuple[int, int],
overlap_wh: tuple[int, int] | None,
overlap_wh: tuple[int, int],
expected_offsets: np.ndarray,
) -> None:
offsets = InferenceSlicer._generate_offset(
resolution_wh=resolution_wh,
slice_wh=slice_wh,
overlap_ratio_wh=None,
overlap_wh=overlap_wh,
)
# Verify that the generated offsets match the expected offsets
assert np.array_equal(offsets, expected_offsets), (
f"Expected {expected_offsets}, got {offsets}"
)
+94 -4
View File
@@ -5,11 +5,16 @@ from contextlib import ExitStack as DoesNotRaise
import numpy as np
import pytest
from supervision.detection.utils.boxes import clip_boxes, move_boxes, scale_boxes
from supervision.detection.utils.boxes import (
clip_boxes,
denormalize_boxes,
move_boxes,
scale_boxes,
)
@pytest.mark.parametrize(
"xyxy, resolution_wh, expected_result",
("xyxy", "resolution_wh", "expected_result"),
[
(
np.empty(shape=(0, 4)),
@@ -53,7 +58,7 @@ def test_clip_boxes(
@pytest.mark.parametrize(
"xyxy, offset, expected_result, exception",
("xyxy", "offset", "expected_result", "exception"),
[
(
np.empty(shape=(0, 4)),
@@ -99,7 +104,7 @@ def test_move_boxes(
@pytest.mark.parametrize(
"xyxy, factor, expected_result, exception",
("xyxy", "factor", "expected_result", "exception"),
[
(
np.empty(shape=(0, 4)),
@@ -142,3 +147,88 @@ def test_scale_boxes(
with exception:
result = scale_boxes(xyxy=xyxy, factor=factor)
assert np.array_equal(result, expected_result)
@pytest.mark.parametrize(
("xyxy", "resolution_wh", "normalization_factor", "expected_result", "exception"),
[
(
np.empty(shape=(0, 4)),
(1280, 720),
1.0,
np.empty(shape=(0, 4)),
DoesNotRaise(),
), # empty array
(
np.array([[0.1, 0.2, 0.5, 0.6]]),
(1280, 720),
1.0,
np.array([[128.0, 144.0, 640.0, 432.0]]),
DoesNotRaise(),
), # single box with default normalization
(
np.array([[0.1, 0.2, 0.5, 0.6], [0.3, 0.4, 0.7, 0.8]]),
(1280, 720),
1.0,
np.array([[128.0, 144.0, 640.0, 432.0], [384.0, 288.0, 896.0, 576.0]]),
DoesNotRaise(),
), # two boxes with default normalization
(
np.array(
[[0.1, 0.2, 0.5, 0.6], [0.3, 0.4, 0.7, 0.8], [0.2, 0.1, 0.6, 0.5]]
),
(1280, 720),
1.0,
np.array(
[
[128.0, 144.0, 640.0, 432.0],
[384.0, 288.0, 896.0, 576.0],
[256.0, 72.0, 768.0, 360.0],
]
),
DoesNotRaise(),
), # three boxes - regression test for issue #1959
(
np.array([[10.0, 20.0, 50.0, 60.0]]),
(100, 200),
100.0,
np.array([[10.0, 40.0, 50.0, 120.0]]),
DoesNotRaise(),
), # single box with custom normalization factor
(
np.array([[10.0, 20.0, 50.0, 60.0], [30.0, 40.0, 70.0, 80.0]]),
(100, 200),
100.0,
np.array([[10.0, 40.0, 50.0, 120.0], [30.0, 80.0, 70.0, 160.0]]),
DoesNotRaise(),
), # two boxes with custom normalization factor
(
np.array([[0.0, 0.0, 1.0, 1.0]]),
(1920, 1080),
1.0,
np.array([[0.0, 0.0, 1920.0, 1080.0]]),
DoesNotRaise(),
), # full frame box
(
np.array([[0.5, 0.5, 0.5, 0.5]]),
(640, 480),
1.0,
np.array([[320.0, 240.0, 320.0, 240.0]]),
DoesNotRaise(),
), # zero-area box (point)
],
)
def test_denormalize_boxes(
xyxy: np.ndarray,
resolution_wh: tuple[int, int],
normalization_factor: float,
expected_result: np.ndarray,
exception: Exception,
) -> None:
with exception:
result = denormalize_boxes(
xyxy=xyxy,
resolution_wh=resolution_wh,
normalization_factor=normalization_factor,
)
assert np.allclose(result, expected_result)
+176 -4
View File
@@ -6,13 +6,14 @@ import pytest
from supervision.detection.utils.converters import (
xcycwh_to_xyxy,
xywh_to_xyxy,
xyxy_to_mask,
xyxy_to_xcycarh,
xyxy_to_xywh,
)
@pytest.mark.parametrize(
"xywh, expected_result",
("xywh", "expected_result"),
[
(np.array([[10, 20, 30, 40]]), np.array([[10, 20, 40, 60]])), # standard case
(np.array([[0, 0, 0, 0]]), np.array([[0, 0, 0, 0]])), # zero size bounding box
@@ -35,7 +36,7 @@ def test_xywh_to_xyxy(xywh: np.ndarray, expected_result: np.ndarray) -> None:
@pytest.mark.parametrize(
"xyxy, expected_result",
("xyxy", "expected_result"),
[
(np.array([[10, 20, 40, 60]]), np.array([[10, 20, 30, 40]])), # standard case
(np.array([[0, 0, 0, 0]]), np.array([[0, 0, 0, 0]])), # zero size bounding box
@@ -58,7 +59,7 @@ def test_xyxy_to_xywh(xyxy: np.ndarray, expected_result: np.ndarray) -> None:
@pytest.mark.parametrize(
"xyxy, expected_result",
("xyxy", "expected_result"),
[
# Empty and zero cases
(np.array([]).reshape(0, 4), np.array([]).reshape(0, 4)), # empty array
@@ -109,7 +110,7 @@ def test_xyxy_to_xcycarh(xyxy: np.ndarray, expected_result: np.ndarray) -> None:
@pytest.mark.parametrize(
"xcycwh, expected_result",
("xcycwh", "expected_result"),
[
(np.array([[50, 50, 20, 30]]), np.array([[40, 35, 60, 65]])), # standard case
(np.array([[0, 0, 0, 0]]), np.array([[0, 0, 0, 0]])), # zero size bounding box
@@ -129,3 +130,174 @@ def test_xyxy_to_xcycarh(xyxy: np.ndarray, expected_result: np.ndarray) -> None:
def test_xcycwh_to_xyxy(xcycwh: np.ndarray, expected_result: np.ndarray) -> None:
result = xcycwh_to_xyxy(xcycwh)
np.testing.assert_array_equal(result, expected_result)
@pytest.mark.parametrize(
("boxes", "resolution_wh", "expected"),
[
# 0) Empty input
(
np.array([], dtype=float).reshape(0, 4),
(5, 4),
np.array([], dtype=bool).reshape(0, 4, 5),
),
# 1) Single pixel box
(
np.array([[2, 1, 2, 1]], dtype=float),
(5, 4),
np.array(
[
[
[False, False, False, False, False],
[False, False, True, False, False],
[False, False, False, False, False],
[False, False, False, False, False],
]
],
dtype=bool,
),
),
# 2) Horizontal line, inclusive bounds
(
np.array([[1, 2, 3, 2]], dtype=float),
(5, 4),
np.array(
[
[
[False, False, False, False, False],
[False, False, False, False, False],
[False, True, True, True, False],
[False, False, False, False, False],
]
],
dtype=bool,
),
),
# 3) Vertical line, inclusive bounds
(
np.array([[3, 0, 3, 2]], dtype=float),
(5, 4),
np.array(
[
[
[False, False, False, True, False],
[False, False, False, True, False],
[False, False, False, True, False],
[False, False, False, False, False],
]
],
dtype=bool,
),
),
# 4) Proper rectangle fill
(
np.array([[1, 1, 3, 2]], dtype=float),
(5, 4),
np.array(
[
[
[False, False, False, False, False],
[False, True, True, True, False],
[False, True, True, True, False],
[False, False, False, False, False],
]
],
dtype=bool,
),
),
# 5) Negative coordinates clipped to [0, 0]
(
np.array([[-2, -1, 1, 1]], dtype=float),
(5, 4),
np.array(
[
[
[True, True, False, False, False],
[True, True, False, False, False],
[False, False, False, False, False],
[False, False, False, False, False],
]
],
dtype=bool,
),
),
# 6) Overflow coordinates clipped to width-1 and height-1
(
np.array([[3, 2, 10, 10]], dtype=float),
(5, 4),
np.array(
[
[
[False, False, False, False, False],
[False, False, False, False, False],
[False, False, False, True, True],
[False, False, False, True, True],
]
],
dtype=bool,
),
),
# 7) Invalid box where max < min after ints, mask stays empty
(
np.array([[3, 2, 1, 4]], dtype=float),
(5, 4),
np.array(
[
[
[False, False, False, False, False],
[False, False, False, False, False],
[False, False, False, False, False],
[False, False, False, False, False],
]
],
dtype=bool,
),
),
# 8) Fractional coordinates are floored by int conversion
# (0.2,0.2)-(2.8,1.9) -> (0,0)-(2,1)
(
np.array([[0.2, 0.2, 2.8, 1.9]], dtype=float),
(5, 4),
np.array(
[
[
[True, True, True, False, False],
[True, True, True, False, False],
[False, False, False, False, False],
[False, False, False, False, False],
]
],
dtype=bool,
),
),
# 9) Multiple boxes, separate masks
(
np.array([[0, 0, 1, 0], [2, 1, 4, 3]], dtype=float),
(5, 4),
np.array(
[
# Box 0: row 0, cols 0..1
[
[True, True, False, False, False],
[False, False, False, False, False],
[False, False, False, False, False],
[False, False, False, False, False],
],
# Box 1: rows 1..3, cols 2..4
[
[False, False, False, False, False],
[False, False, True, True, True],
[False, False, True, True, True],
[False, False, True, True, True],
],
],
dtype=bool,
),
),
],
)
def test_xyxy_to_mask(boxes: np.ndarray, resolution_wh, expected: np.ndarray) -> None:
result = xyxy_to_mask(boxes, resolution_wh)
assert result.dtype == np.bool_
assert result.shape == expected.shape
np.testing.assert_array_equal(result, expected)
+5 -5
View File
@@ -19,7 +19,7 @@ TEST_MASK[:, 300:351, 200:251] = True
@pytest.mark.parametrize(
"roboflow_result, expected_result, exception",
("roboflow_result", "expected_result", "exception"),
[
(
{"predictions": [], "image": {"width": 1000, "height": 1000}},
@@ -251,7 +251,7 @@ def test_process_roboflow_result(
@pytest.mark.parametrize(
"data_list, expected_result, exception",
("data_list", "expected_result", "exception"),
[
(
[],
@@ -445,7 +445,7 @@ def test_merge_data(
with exception:
result = merge_data(data_list=data_list)
if expected_result is None:
assert False, f"Expected an error, but got result {result}"
pytest.fail(f"Expected an error, but got result {result}")
for key in result:
if isinstance(result[key], np.ndarray):
@@ -459,7 +459,7 @@ def test_merge_data(
@pytest.mark.parametrize(
"data, index, expected_result, exception",
("data", "index", "expected_result", "exception"),
[
({}, 0, {}, DoesNotRaise()), # empty data dict
(
@@ -632,7 +632,7 @@ def test_get_data_item(
@pytest.mark.parametrize(
"metadata_list, expected_result, exception",
("metadata_list", "expected_result", "exception"),
[
# Identical metadata with a single key
([{"key1": "value1"}, {"key1": "value1"}], {"key1": "value1"}, DoesNotRaise()),
+501 -4
View File
@@ -6,15 +6,19 @@ import numpy as np
import pytest
from supervision.detection.utils.iou_and_nms import (
OverlapMetric,
_group_overlapping_boxes,
box_iou,
box_iou_batch,
box_non_max_suppression,
mask_non_max_merge,
mask_non_max_suppression,
)
from test.test_utils import random_boxes
@pytest.mark.parametrize(
"predictions, iou_threshold, expected_result, exception",
("predictions", "iou_threshold", "expected_result", "exception"),
[
(
np.empty(shape=(0, 5), dtype=float),
@@ -141,7 +145,7 @@ def test_group_overlapping_boxes(
@pytest.mark.parametrize(
"predictions, iou_threshold, expected_result, exception",
("predictions", "iou_threshold", "expected_result", "exception"),
[
(
np.empty(shape=(0, 5)),
@@ -246,7 +250,7 @@ def test_box_non_max_suppression(
@pytest.mark.parametrize(
"predictions, masks, iou_threshold, expected_result, exception",
("predictions", "masks", "iou_threshold", "expected_result", "exception"),
[
(
np.empty((0, 6)),
@@ -452,7 +456,7 @@ def test_mask_non_max_suppression(
@pytest.mark.parametrize(
"predictions, masks, iou_threshold, expected_result, exception",
("predictions", "masks", "iou_threshold", "expected_result", "exception"),
[
(
np.empty((0, 6)),
@@ -631,3 +635,496 @@ def test_mask_non_max_merge(
sorted_result = sorted([sorted(group) for group in result])
sorted_expected_result = sorted([sorted(group) for group in expected_result])
assert sorted_result == sorted_expected_result
@pytest.mark.parametrize(
("box_true", "box_detection", "overlap_metric", "expected_overlap", "exception"),
[
(
[100.0, 100.0, 200.0, 200.0],
[150.0, 150.0, 250.0, 250.0],
OverlapMetric.IOU,
0.14285714285714285,
DoesNotRaise(),
), # partial overlap, IOU
(
[100.0, 100.0, 200.0, 200.0],
[150.0, 150.0, 250.0, 250.0],
OverlapMetric.IOS,
0.25,
DoesNotRaise(),
), # partial overlap, IOS
(
np.array([0.0, 0.0, 10.0, 10.0], dtype=np.float32),
np.array([0.0, 0.0, 10.0, 10.0], dtype=np.float32),
OverlapMetric.IOU,
1.0,
DoesNotRaise(),
), # identical boxes, both boxes are arrays, IOU
(
np.array([0.0, 0.0, 10.0, 10.0], dtype=np.float32),
np.array([0.0, 0.0, 10.0, 10.0], dtype=np.float32),
OverlapMetric.IOS,
1.0,
DoesNotRaise(),
), # identical boxes, both boxes are arrays, IOS
(
[0.0, 0.0, 10.0, 10.0],
[0.0, 0.0, 10.0, 10.0],
"iou",
1.0,
DoesNotRaise(),
), # identical boxes, both boxes are arrays, IOU as lowercase string
(
[0.0, 0.0, 10.0, 10.0],
[0.0, 0.0, 10.0, 10.0],
"ios",
1.0,
DoesNotRaise(),
), # identical boxes, both boxes are arrays, IOS as lowercase string
(
[0.0, 0.0, 10.0, 10.0],
[0.0, 0.0, 10.0, 10.0],
"IOU",
1.0,
DoesNotRaise(),
), # identical boxes, both boxes are arrays, IOU as uppercase string
(
[0.0, 0.0, 10.0, 10.0],
[20.0, 20.0, 30.0, 30.0],
OverlapMetric.IOU,
0.0,
DoesNotRaise(),
), # no overlap, IOU
(
[0.0, 0.0, 10.0, 10.0],
[20.0, 20.0, 30.0, 30.0],
OverlapMetric.IOS,
0.0,
DoesNotRaise(),
), # no overlap, IOS
(
[0.0, 0.0, 10.0, 10.0],
[10.0, 0.0, 20.0, 10.0],
OverlapMetric.IOU,
0.0,
DoesNotRaise(),
), # boxes touch at edge, zero intersection, IOU
(
[0.0, 0.0, 10.0, 10.0],
[10.0, 0.0, 20.0, 10.0],
OverlapMetric.IOS,
0.0,
DoesNotRaise(),
), # boxes touch at edge, zero intersection, IOU
(
[0.0, 0.0, 10.0, 10.0],
[2.0, 2.0, 8.0, 8.0],
OverlapMetric.IOU,
0.36,
DoesNotRaise(),
), # one box inside another, IOU
(
[0.0, 0.0, 10.0, 10.0],
[2.0, 2.0, 8.0, 8.0],
OverlapMetric.IOS,
1.0,
DoesNotRaise(),
), # one box inside another, IOS
(
[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 10.0, 10.0],
OverlapMetric.IOU,
0.0,
DoesNotRaise(),
), # degenerate true box with zero area, IOU
(
[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 10.0, 10.0],
OverlapMetric.IOS,
0.0,
DoesNotRaise(),
), # degenerate true box with zero area, IOS
(
[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0],
OverlapMetric.IOU,
0.0,
DoesNotRaise(),
), # both boxes fully degenerate, IOU
(
[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0],
OverlapMetric.IOS,
0.0,
DoesNotRaise(),
), # both boxes fully degenerate, IOS
(
[-5.0, 0.0, 5.0, 10.0],
[0.0, 0.0, 10.0, 10.0],
OverlapMetric.IOU,
1.0 / 3.0,
DoesNotRaise(),
), # negative x_min, overlapping boxes, IOU is 1/3
(
[-5.0, 0.0, 5.0, 10.0],
[0.0, 0.0, 10.0, 10.0],
OverlapMetric.IOS,
0.5,
DoesNotRaise(),
), # negative x_min, overlapping boxes, IOS is 0.5
(
[0.0, 0.0, 1.0, 1.0],
[0.5, 0.5, 1.5, 1.5],
OverlapMetric.IOU,
0.14285714285714285,
DoesNotRaise(),
), # partial overlap with fractional coordinates, IOU
(
[0.0, 0.0, 1.0, 1.0],
[0.5, 0.5, 1.5, 1.5],
OverlapMetric.IOS,
0.25,
DoesNotRaise(),
), # partial overlap with fractional coordinates, IOS
],
)
def test_box_iou(
box_true: list[float] | np.ndarray,
box_detection: list[float] | np.ndarray,
overlap_metric: str | OverlapMetric,
expected_overlap: float,
exception: Exception,
) -> None:
with exception:
result = box_iou(
box_true=box_true,
box_detection=box_detection,
overlap_metric=overlap_metric,
)
assert result == pytest.approx(expected_overlap, rel=1e-6, abs=1e-12)
@pytest.mark.parametrize(
(
"boxes_true",
"boxes_detection",
"overlap_metric",
"expected_overlap",
"exception",
),
[
# both inputs empty
(
np.empty((0, 4), dtype=np.float32),
np.empty((0, 4), dtype=np.float32),
OverlapMetric.IOU,
np.empty((0, 0), dtype=np.float32),
DoesNotRaise(),
),
# one true box, no detections
(
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
np.empty((0, 4), dtype=np.float32),
OverlapMetric.IOU,
np.empty((1, 0), dtype=np.float32),
DoesNotRaise(),
),
# no true boxes, one detection
(
np.empty((0, 4), dtype=np.float32),
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
OverlapMetric.IOU,
np.empty((0, 1), dtype=np.float32),
DoesNotRaise(),
),
# 1x1 partial overlap, IOU
(
np.array([[100.0, 100.0, 200.0, 200.0]], dtype=np.float32),
np.array([[150.0, 150.0, 250.0, 250.0]], dtype=np.float32),
OverlapMetric.IOU,
np.array([[0.14285715]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 partial overlap, IOS
(
np.array([[100.0, 100.0, 200.0, 200.0]], dtype=np.float32),
np.array([[150.0, 150.0, 250.0, 250.0]], dtype=np.float32),
OverlapMetric.IOS,
np.array([[0.25]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 identical boxes, IOU as lowercase string
(
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
"iou",
np.array([[1.0]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 identical boxes, IOS as lowercase string
(
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
"ios",
np.array([[1.0]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 identical boxes, IOU as uppercase string
(
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
"IOU",
np.array([[1.0]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 identical boxes, IOS as uppercase string
(
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
"IOS",
np.array([[1.0]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 no overlap, IOU
(
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
np.array([[20.0, 20.0, 30.0, 30.0]], dtype=np.float32),
OverlapMetric.IOU,
np.array([[0.0]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 no overlap, IOS
(
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
np.array([[20.0, 20.0, 30.0, 30.0]], dtype=np.float32),
OverlapMetric.IOS,
np.array([[0.0]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 touching at edge, zero intersection, IOU
(
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
np.array([[10.0, 0.0, 20.0, 10.0]], dtype=np.float32),
OverlapMetric.IOU,
np.array([[0.0]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 touching at edge, zero intersection, IOS
(
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
np.array([[10.0, 0.0, 20.0, 10.0]], dtype=np.float32),
OverlapMetric.IOS,
np.array([[0.0]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 box inside another, IOU
(
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
np.array([[2.0, 2.0, 8.0, 8.0]], dtype=np.float32),
OverlapMetric.IOU,
np.array([[0.36]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 box inside another, IOS
(
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
np.array([[2.0, 2.0, 8.0, 8.0]], dtype=np.float32),
OverlapMetric.IOS,
np.array([[1.0]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 degenerate true box, IOU
(
np.array([[0.0, 0.0, 0.0, 0.0]], dtype=np.float32),
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
OverlapMetric.IOU,
np.array([[0.0]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 degenerate true box, IOS
(
np.array([[0.0, 0.0, 0.0, 0.0]], dtype=np.float32),
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
OverlapMetric.IOS,
np.array([[0.0]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 both boxes degenerate, IOU
(
np.array([[0.0, 0.0, 0.0, 0.0]], dtype=np.float32),
np.array([[0.0, 0.0, 0.0, 0.0]], dtype=np.float32),
OverlapMetric.IOU,
np.array([[0.0]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 both boxes degenerate, IOS
(
np.array([[0.0, 0.0, 0.0, 0.0]], dtype=np.float32),
np.array([[0.0, 0.0, 0.0, 0.0]], dtype=np.float32),
OverlapMetric.IOS,
np.array([[0.0]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 negative coordinate, partial overlap, IOU
(
np.array([[-5.0, 0.0, 5.0, 10.0]], dtype=np.float32),
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
OverlapMetric.IOU,
np.array([[1.0 / 3.0]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 negative coordinate, partial overlap, IOS
(
np.array([[-5.0, 0.0, 5.0, 10.0]], dtype=np.float32),
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
OverlapMetric.IOS,
np.array([[0.5]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 fractional coordinates, partial overlap, IOU
(
np.array([[0.0, 0.0, 1.0, 1.0]], dtype=np.float32),
np.array([[0.5, 0.5, 1.5, 1.5]], dtype=np.float32),
OverlapMetric.IOU,
np.array([[0.14285715]], dtype=np.float32),
DoesNotRaise(),
),
# 1x1 fractional coordinates, partial overlap, IOS
(
np.array([[0.0, 0.0, 1.0, 1.0]], dtype=np.float32),
np.array([[0.5, 0.5, 1.5, 1.5]], dtype=np.float32),
OverlapMetric.IOS,
np.array([[0.25]], dtype=np.float32),
DoesNotRaise(),
),
# true batch case, 2x2, IOU
(
np.array(
[
[0.0, 0.0, 10.0, 10.0],
[10.0, 10.0, 20.0, 20.0],
],
dtype=np.float32,
),
np.array(
[
[0.0, 0.0, 10.0, 10.0],
[5.0, 5.0, 15.0, 15.0],
],
dtype=np.float32,
),
OverlapMetric.IOU,
np.array(
[
[1.0, 0.14285715],
[0.0, 0.14285715],
],
dtype=np.float32,
),
DoesNotRaise(),
),
# true batch case, 2x2, IOS
(
np.array(
[
[0.0, 0.0, 10.0, 10.0],
[10.0, 10.0, 20.0, 20.0],
],
dtype=np.float32,
),
np.array(
[
[0.0, 0.0, 10.0, 10.0],
[5.0, 5.0, 15.0, 15.0],
],
dtype=np.float32,
),
OverlapMetric.IOS,
np.array(
[
[1.0, 0.25],
[0.0, 0.25],
],
dtype=np.float32,
),
DoesNotRaise(),
),
# invalid overlap_metric
(
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
"invalid",
None,
pytest.raises(ValueError),
),
],
)
def test_box_iou_batch(
boxes_true: np.ndarray,
boxes_detection: np.ndarray,
overlap_metric: str | OverlapMetric,
expected_overlap: np.ndarray | None,
exception: Exception,
) -> None:
with exception:
result = box_iou_batch(
boxes_true=boxes_true,
boxes_detection=boxes_detection,
overlap_metric=overlap_metric,
)
assert isinstance(result, np.ndarray)
assert result.shape == expected_overlap.shape
assert np.allclose(
result,
expected_overlap,
rtol=1e-6,
atol=1e-12,
)
@pytest.mark.parametrize(
("num_true", "num_det"),
[
(5, 5),
(5, 10),
(10, 5),
(10, 10),
(20, 30),
(30, 20),
(50, 50),
(100, 100),
],
)
@pytest.mark.parametrize(
"overlap_metric",
[OverlapMetric.IOU, OverlapMetric.IOS],
)
def test_box_iou_batch_symmetric_large(
num_true: int,
num_det: int,
overlap_metric: OverlapMetric,
) -> None:
boxes_true = random_boxes(num_true)
boxes_det = random_boxes(num_det)
result_ab = box_iou_batch(
boxes_true=boxes_true,
boxes_detection=boxes_det,
overlap_metric=overlap_metric,
)
result_ba = box_iou_batch(
boxes_true=boxes_det,
boxes_detection=boxes_true,
overlap_metric=overlap_metric,
)
assert result_ab.shape == (num_true, num_det)
assert result_ba.shape == (num_det, num_true)
assert np.allclose(
result_ab,
result_ba.T,
rtol=1e-6,
atol=1e-12,
)
+238 -4
View File
@@ -10,12 +10,13 @@ from supervision.detection.utils.masks import (
calculate_masks_centroids,
contains_holes,
contains_multiple_segments,
filter_segments_by_distance,
move_masks,
)
@pytest.mark.parametrize(
"masks, offset, resolution_wh, expected_result, exception",
("masks", "offset", "resolution_wh", "expected_result", "exception"),
[
(
np.array(
@@ -277,7 +278,7 @@ def test_move_masks(
@pytest.mark.parametrize(
"masks, expected_result, exception",
("masks", "expected_result", "exception"),
[
(
np.array(
@@ -368,7 +369,7 @@ def test_calculate_masks_centroids(
@pytest.mark.parametrize(
"mask, expected_result, exception",
("mask", "expected_result", "exception"),
[
(
np.array([[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 0, 0], [0, 1, 1, 0]]).astype(
@@ -423,7 +424,7 @@ def test_contains_holes(
@pytest.mark.parametrize(
"mask, connectivity, expected_result, exception",
("mask", "connectivity", "expected_result", "exception"),
[
(
np.array([[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 0, 0], [0, 1, 1, 0]]).astype(
@@ -500,3 +501,236 @@ def test_contains_multiple_segments(
with exception:
result = contains_multiple_segments(mask=mask, connectivity=connectivity)
assert result == expected_result
@pytest.mark.parametrize(
(
"mask",
"connectivity",
"mode",
"absolute_distance",
"relative_distance",
"expected_result",
"exception",
),
[
# single component, unchanged
(
np.array(
[
[0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 0],
[0, 1, 1, 1, 0, 0],
[0, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
],
dtype=bool,
),
8,
"edge",
2.0,
None,
np.array(
[
[0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 0],
[0, 1, 1, 1, 0, 0],
[0, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
],
dtype=bool,
),
DoesNotRaise(),
),
# two components, edge distance 2, kept with abs=1
(
np.array(
[
[0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 1],
[0, 1, 1, 1, 0, 1],
[0, 1, 1, 1, 0, 1],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
],
dtype=bool,
),
8,
"edge",
2.0,
None,
np.array(
[
[0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 1],
[0, 1, 1, 1, 0, 1],
[0, 1, 1, 1, 0, 1],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
],
dtype=bool,
),
DoesNotRaise(),
),
# centroid mode, far centroids, dropped with small relative threshold
(
np.array(
[
[1, 1, 1, 0, 0, 0],
[1, 1, 1, 0, 0, 0],
[1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1],
],
dtype=bool,
),
8,
"centroid",
None,
0.3, # diagonal ~8.49, threshold ~2.55, centroid gap ~4.24
np.array(
[
[1, 1, 1, 0, 0, 0],
[1, 1, 1, 0, 0, 0],
[1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
],
dtype=bool,
),
DoesNotRaise(),
),
# centroid mode, larger relative threshold, kept
(
np.array(
[
[1, 1, 1, 0, 0, 0],
[1, 1, 1, 0, 0, 0],
[1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1],
],
dtype=bool,
),
8,
"centroid",
None,
0.6, # diagonal ~8.49, threshold ~5.09, centroid gap ~4.24
np.array(
[
[1, 1, 1, 0, 0, 0],
[1, 1, 1, 0, 0, 0],
[1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1],
],
dtype=bool,
),
DoesNotRaise(),
),
# empty mask
(
np.zeros((4, 4), dtype=bool),
4,
"edge",
2.0,
None,
np.zeros((4, 4), dtype=bool),
DoesNotRaise(),
),
# full mask
(
np.ones((4, 4), dtype=bool),
8,
"centroid",
None,
0.2,
np.ones((4, 4), dtype=bool),
DoesNotRaise(),
),
# two components, pixel distance = 2, kept with abs=2
(
np.array(
[
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 1, 1, 1],
[0, 1, 1, 1, 0, 1, 1, 1],
[0, 1, 1, 1, 0, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
],
dtype=bool,
),
8,
"edge",
2.0, # was 1.0
None,
np.array(
[
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 1, 1, 1],
[0, 1, 1, 1, 0, 1, 1, 1],
[0, 1, 1, 1, 0, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
],
dtype=bool,
),
DoesNotRaise(),
),
# two components, pixel distance = 3, dropped with abs=2
(
np.array(
[
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 0, 0, 1, 1],
[0, 1, 1, 1, 0, 0, 0, 1, 1],
[0, 1, 1, 1, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
],
dtype=bool,
),
8,
"edge",
2.0, # keep threshold below 3 so the right blob is removed
None,
np.array(
[
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
],
dtype=bool,
),
DoesNotRaise(),
),
],
)
def test_filter_segments_by_distance_sweep(
mask: npt.NDArray,
connectivity: int,
mode: str,
absolute_distance: float | None,
relative_distance: float | None,
expected_result: npt.NDArray | None,
exception: Exception,
) -> None:
with exception:
result = filter_segments_by_distance(
mask=mask,
connectivity=connectivity,
mode=mode, # type: ignore[arg-type]
absolute_distance=absolute_distance,
relative_distance=relative_distance,
)
assert np.array_equal(result, expected_result)
+1 -1
View File
@@ -9,7 +9,7 @@ from supervision.detection.utils.polygons import filter_polygons_by_area
@pytest.mark.parametrize(
"polygons, min_area, max_area, expected_result, exception",
("polygons", "min_area", "max_area", "expected_result", "exception"),
[
(
[np.array([[0, 0], [0, 10], [10, 10], [10, 0]])],

Some files were not shown because too many files have changed in this diff Show More