Compare commits

..
709 Commits
Author SHA1 Message Date
Piotr Skalski 1bddf26f34 Merge pull request #404 from roboflow/develop
`supervision-0.15.0` release
2023-10-05 09:23:31 +02:00
SkalskiP 73ed3f5f5e 👊 bump develop version to 0.15.0 2023-10-05 09:21:05 +02:00
SkalskiP 88e7fb76d2 Fixed link targeting in changelog documentation. 2023-10-04 23:57:03 +02:00
Piotr Skalski 09c5407347 Merge pull request #410 from roboflow/feature/blur-annotator-docs-update
Refactor BlurAnnotator class and update documentation
2023-10-04 23:54:48 +02:00
pre-commit-ci[bot] bfcf6469d4 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-10-04 21:52:50 +00:00
SkalskiP ec72f81749 Refactor BlurAnnotator class and update documentation 2023-10-04 23:50:47 +02:00
Piotr Skalski 3a52f7b082 Merge pull request #405 from Rajarshi-Misra/new1
Create sv.BlurAnnotator #395
2023-10-04 23:29:26 +02:00
SkalskiP 5efb3e2df1 👊 bump develop version to 0.15.0rc3 2023-10-04 19:43:03 +02:00
SkalskiP 6e33cb04ca 🪵 supervision-0.15.0 changelog.md update 2023-10-04 19:35:01 +02:00
Rajarshi-Misra 2455ef7cfe Merge branch 'roboflow:develop' into new1 2023-10-04 22:24:53 +05:30
Rajarshi-Misra 1d0ffeba72 Comments corrected 2023-10-04 22:23:54 +05:30
Piotr Skalski 55d14cf33e Merge pull request #407 from roboflow/feature/color_map_strategy_support_for_trace_annotator
Add color mapping strategy to InferenceSlicer
2023-10-04 18:49:45 +02:00
pre-commit-ci[bot] a66d835a57 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-10-04 16:37:40 +00:00
SkalskiP 440be5d416 Add color mapping strategy to InferenceSlicer
Added a new argument, 'color_map' in InferenceSlicer to provide more options for color coding while annotating. This includes mapping colors by 'index', 'class' or 'track'. Also refactored the way colors are resolved by separating the responsibility into a new function 'resolve_color_idx' and 'resolve_color'.
2023-10-04 18:36:30 +02:00
Hardik Dava 4fd1ba4dd3 Update var type 2023-10-04 18:34:51 +02:00
Piotr Skalski 61108048e1 Merge pull request #361 from roboflow/add-concurrent-slicer
Add concurrent processing for `sv.InferenceSlicer`
2023-10-04 18:17:52 +02:00
pre-commit-ci[bot] a0b64342ca fix(pre_commit): 🎨 auto format pre-commit hooks 2023-10-04 16:17:35 +00:00
Rajarshi-Misra 7cf593b55d Replaced with blur filter 2023-10-04 21:47:24 +05:30
SkalskiP 1c1936e411 Refactor args description in InferenceSlicer 2023-10-04 18:15:08 +02:00
pre-commit-ci[bot] 331ccb8dde fix(pre_commit): 🎨 auto format pre-commit hooks 2023-10-04 16:09:38 +00:00
Rajarshi-Misra 7366ba9036 Error resolved 2023-10-04 21:38:44 +05:30
pre-commit-ci[bot] 9d5e53030a fix(pre_commit): 🎨 auto format pre-commit hooks 2023-10-04 14:25:37 +00:00
Piotr Skalski d67fa3ae9b Merge branch 'develop' into new1 2023-10-04 16:24:13 +02:00
Piotr Skalski 10ad27d726 Merge pull request #406 from roboflow/feature/annotators-refactor-before-0.15.0-release
Refactor TraceAnnotator to separate utility class
2023-10-04 16:18:49 +02:00
SkalskiP 8511bbb762 Add Tracker ID warning and image to TraceAnnotator doc
Added a warning about the requirement of `tracker_id` for TraceAnnotator in the `core.py`. Also, included an example image in the TraceAnnotator initialization for better understanding in both `core.py` and `annotators.md`.
2023-10-04 16:15:38 +02:00
SkalskiP 14388e56a2 Merge remote-tracking branch 'origin/feature/annotators-refactor-before-0.15.0-release' into feature/annotators-refactor-before-0.15.0-release 2023-10-04 15:35:17 +02:00
SkalskiP 1eb74ed88a Add TraceAnnotator description to the documentation
The documentation has been updated to include the `TraceAnnotator` class.
2023-10-04 15:35:07 +02:00
pre-commit-ci[bot] 87d830d3a1 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-10-04 13:31:17 +00:00
SkalskiP d7a5624d60 Refactor TraceAnnotator to separate utility class 2023-10-04 15:28:00 +02:00
Rajarshi-Misra d3c71c2119 Merge branch 'roboflow:develop' into new1 2023-10-04 18:01:13 +05:30
Rajarshi-Misra 9ad5e50ae9 Addition of gaussian blur filter 2023-10-04 17:59:03 +05:30
Piotr Skalski 67fa66c7e7 Merge pull request #399 from akashAD98/update_detection_sv
Remove class list dependency for roboflow to detection conversion
2023-10-04 12:08:04 +02:00
dependabot[bot] 492f033882 chore(deps): bump pillow from 9.5.0 to 10.0.1 (#403)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.5.0 to 10.0.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/9.5.0...10.0.1)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-04 13:07:50 +03:00
SkalskiP e911516180 Change prediction class handling from text to ids
Updated the prediction parsing logic in supervision/detection/utils.py and supervision/detection/core.py to use 'class_id' instead of 'class' names. Adapted tests in test/detection/test_utils.py accordingly to reflect the same change.
2023-10-04 10:44:49 +02:00
pre-commit-ci[bot] ed30ef68af fix(pre_commit): 🎨 auto format pre-commit hooks 2023-10-03 17:47:44 +00:00
Hardik Dava fefd273a0f removing class list from test inputs 2023-10-03 19:45:38 +02:00
pre-commit-ci[bot] 8746dd3d0e fix(pre_commit): 🎨 auto format pre-commit hooks 2023-10-03 17:34:30 +00:00
Hardik Dava 7cb57c9cc9 Fixing int conversion bug 2023-10-03 19:33:56 +02:00
James ddc036ea3a Update CODE_OF_CONDUCT.md (#400) 2023-10-03 19:40:56 +03:00
pre-commit-ci[bot] c83959bb16 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-10-03 16:18:29 +00:00
Akash A Desai 4f513ce275 Update core.py
remoed class_list arg from core.py file
2023-10-03 21:41:13 +05:30
Piotr Skalski 6f30c87612 Merge pull request #392 from roboflow/docs/documentation-refactoring
Refactor documentation structure and improve annotator details
2023-10-03 11:53:09 +02:00
SkalskiP 93a8110908 Refactor documentation structure and improve annotator details
Update the file and section structure of the project documentation for better readability. Removed 'annotate.md' and replaced it with an expanded 'annotators.md'. The new document provides examples of each annotator and includes preview images for better understanding. The API reference structure was flattened for simplicity and related documents 'core.py' and 'mkdocs.yml' were updated accordingly. The core tracker and dataset documents were renamed for consistency.
2023-10-03 11:38:28 +02:00
dependabot[bot] 410b7eb4fc chore(deps-dev): bump urllib3 from 2.0.4 to 2.0.6 (#389)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.4 to 2.0.6.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.0.4...2.0.6)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-03 12:07:45 +03:00
Piotr Skalski e6e1397f56 Merge pull request #386 from onuralpszr/feature/circle_annotator
feat(annotator): 🚀 introduce circle annotator
2023-10-02 23:56:24 +02:00
Piotr Skalski 0ca9a3ecdf Merge pull request #384 from roboflow/code-of-conduct
docs: 📝 code of conduct added
2023-10-02 23:54:36 +02:00
pre-commit-ci[bot] c5016bbd8f chore(pre_commit): ⬆ pre_commit autoupdate (#388)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.291 → v0.0.292](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.291...v0.0.292)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-10-03 00:16:20 +03:00
Onuralp SEZER 9fbd0ddf62 docs(annotator): 📝 circle annotator example picture added
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-10-03 00:03:20 +03:00
Onuralp SEZER a630870968 feat(annotator): 🚀 introduce circle annotator
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-10-01 20:51:46 +03:00
Onuralp SEZER 06416607cc docs: 📝 code of conduct added
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-09-29 14:54:56 +03:00
SkalskiP 1b449294c4 👊 bump develop version to 0.15.0rc2 2023-09-29 13:46:39 +02:00
Onuralp SEZER 36f95d060d Merge pull request #170 from roboflow/feature/redesigned_annotators
redesigned annotators
2023-09-29 14:36:43 +03:00
Onuralp SEZER 07c344e050 fix: 🐛 add missing TraceAnnotator (traffic_analysis fix) and sv version adjusted
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-09-29 13:25:26 +03:00
pre-commit-ci[bot] f3bdb6efa2 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-28 21:55:12 +00:00
SkalskiP f4f9e7ddc7 ready for review 2023-09-28 23:54:34 +02:00
pre-commit-ci[bot] 53f2b4fd45 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-28 21:24:53 +00:00
SkalskiP c7582f7c07 Merge remote-tracking branch 'origin/feature/redesigned_annotators' into feature/redesigned_annotators 2023-09-28 23:24:39 +02:00
SkalskiP 3e8906d830 📘 docs updates for LabelAnnotator 2023-09-28 23:24:22 +02:00
pre-commit-ci[bot] 6521f9f4ed fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-28 19:47:18 +00:00
SkalskiP 560a35b75f 🧪 test LabelAnnotator supporting more Position values 2023-09-28 21:46:02 +02:00
pre-commit-ci[bot] 405a4c73f9 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-28 16:16:11 +00:00
SkalskiP 87fd10c366 Merge remote-tracking branch 'origin/feature/redesigned_annotators' into feature/redesigned_annotators 2023-09-28 18:15:57 +02:00
SkalskiP 8141cb357f Merge remote-tracking branch 'origin/feature/redesigned_annotators' into feature/redesigned_annotators 2023-09-28 18:15:45 +02:00
SkalskiP 87fb336eb6 Merge remote-tracking branch 'origin/feature/redesigned_annotators' into feature/redesigned_annotators 2023-09-28 18:13:32 +02:00
SkalskiP 28d279bec3 Merge remote-tracking branch 'origin/feature/redesigned_annotators' into feature/redesigned_annotators 2023-09-28 18:12:43 +02:00
pre-commit-ci[bot] 1b0bcea5a6 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-28 16:10:18 +00:00
SkalskiP 507e000bca Merge remote-tracking branch 'origin/feature/redesigned_annotators' into feature/redesigned_annotators 2023-09-28 18:08:45 +02:00
SkalskiP f7bf766a60 🧪 Test of LabelAnnotator alternative annotate method implementation. 2023-09-28 18:08:01 +02:00
pre-commit-ci[bot] 1e0f18f792 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-28 15:11:28 +00:00
SkalskiP be5c1b1aa1 Merge remote-tracking branch 'origin/feature/redesigned_annotators' into feature/redesigned_annotators 2023-09-28 17:11:14 +02:00
SkalskiP c35a7714d2 🧪 Test of BoxCornerAnnotator alternative annotate method implementation. 2023-09-28 17:11:06 +02:00
pre-commit-ci[bot] a8cc5a68bb fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-28 14:48:53 +00:00
SkalskiP de4056192e 🛠️ Refactor of BoxCornerAnnotator docs and annotate method implementation. 2023-09-28 16:48:03 +02:00
SkalskiP d337a03d28 Merge remote-tracking branch 'origin/feature/redesigned_annotators' into feature/redesigned_annotators 2023-09-28 16:27:07 +02:00
SkalskiP 00b67588d8 🛠️ EllipseAnnotator.end_angle fix 2023-09-28 16:25:35 +02:00
pre-commit-ci[bot] e10e0f21df fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-28 14:14:41 +00:00
SkalskiP 4fb6760d2c Merge remote-tracking branch 'origin/feature/redesigned_annotators' into feature/redesigned_annotators 2023-09-28 16:14:28 +02:00
SkalskiP de7ed5e115 🔨 EllipseAnnotator refactor. Changes in annotate method + new docstrings. 2023-09-28 16:14:17 +02:00
pre-commit-ci[bot] d929b86c9f fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-28 13:52:31 +00:00
SkalskiP 0b1627f835 Merge remote-tracking branch 'origin/feature/redesigned_annotators' into feature/redesigned_annotators 2023-09-28 15:51:06 +02:00
SkalskiP 20b1b21774 🧹 Removed old MaskAnnotator implementation as it is no longer used. Moved ColorMap, resolve_color_idx, resolve_color and BaseAnnotator to supervision/annotators/base. Removed BoxMaskAnnotator as it can be achieved by BoundingBoxAnnotator and MaskAnnotator composition. 2023-09-28 15:50:55 +02:00
pre-commit-ci[bot] e5eaf26137 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-28 13:37:05 +00:00
SkalskiP be3e67949c Merge remote-tracking branch 'origin/feature/redesigned_annotators' into feature/redesigned_annotators 2023-09-28 15:36:28 +02:00
SkalskiP 2587fdfefd ☝️ updating MaskAnnotator docs and code to be consistent with BoundingBoxAnnotator 2023-09-28 15:36:18 +02:00
pre-commit-ci[bot] 08a88a8c0c fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-28 11:47:28 +00:00
SkalskiP 181c0b833b 🔨 small names refactor 2023-09-28 13:46:52 +02:00
pre-commit-ci[bot] 11601e3f2d fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-28 10:14:38 +00:00
SkalskiP 1b8ff617c3 📘 docs updates for Position and get_anchor_coordinates 2023-09-28 12:13:52 +02:00
SkalskiP 5bc83bf6cc Merge branch 'develop' into feature/redesigned_annotators 2023-09-27 12:08:07 +02:00
fatihandpre-commit-ci[bot] dd80753b2b add sahi paper url into docstring (#382)
* add sahi paper url into docstring
* fix(pre_commit): 🎨 auto format pre-commit hooks

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-09-27 00:59:57 +03:00
Piotr Skalski b2bb69bebb Merge pull request #381 from Killua7362/utils-fix
Adding Fourcc codecs option to VideoSink
2023-09-26 14:32:44 +02:00
pre-commit-ci[bot] 9c081b0e11 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-26 05:40:22 +00:00
Akshay 7f613c6d27 Merge branch 'utils-fix' of https://github.com/Killua7362/supervision into utils-fix 2023-09-26 08:40:01 +03:00
Akshay 9736eba9e6 FIX: Added type error exception in videosink 2023-09-26 08:38:46 +03:00
pre-commit-ci[bot] 53dfeac19b fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-26 05:25:00 +00:00
Akshay 5c25e58e39 Added docstrings for codec 2023-09-26 08:22:07 +03:00
Akshay 52949f2fd1 Support for all the codes 2023-09-26 08:12:18 +03:00
pre-commit-ci[bot] 56c511b5d8 chore(pre_commit): ⬆ pre_commit autoupdate (#379)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.290 → v0.0.291](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.290...v0.0.291)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-09-26 00:20:59 +03:00
Piotr Skalski 62014186a2 Update README.md with latest supervision YouTube tutorial 2023-09-22 13:43:56 +02:00
dependabot[bot] 6cd7cf0209 Bump cryptography from 41.0.3 to 41.0.4 (#375)
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.3 to 41.0.4.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.3...41.0.4)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-22 10:17:28 +03:00
Onuralp SEZER a39c92016f docs(example): 📝 example LabelAnnotator doc fix
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-09-21 15:45:04 +03:00
Onuralp SEZER 4e88e26d91 chore(formatting): pre-commit fixes
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-09-21 15:43:07 +03:00
pre-commit-ci[bot] 484e53324e fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-21 12:36:21 +00:00
Onuralp SEZER e5c4d8e3b9 docs(example): 📝 Update tracking example with new annotators
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-09-21 15:34:17 +03:00
Onuralp SEZER 52ef3fadcb fix(redefinition): 🐞 fix redefinition for MaskAnnotator TraceAnnotator
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-09-21 14:46:35 +03:00
pre-commit-ci[bot] 472ddd8d0e fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-21 11:35:41 +00:00
Onuralp SEZER 90b23430d2 Merge branch 'develop' into feature/redesigned_annotators
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-09-21 14:35:24 +03:00
pre-commit-ci[bot] e91f0cb496 chore(pre_commit): ⬆ pre_commit autoupdate (#366)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.288 → v0.0.290](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.288...v0.0.290)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-09-19 16:34:16 +03:00
pre-commit-ci[bot] fa6238dbc4 chore(pre_commit): ⬆ pre_commit autoupdate (#362)
updates:
- [github.com/psf/black: 23.7.0 → 23.9.1](https://github.com/psf/black/compare/23.7.0...23.9.1)
- [github.com/astral-sh/ruff-pre-commit: v0.0.287 → v0.0.288](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.287...v0.0.288)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-09-12 00:32:39 +03:00
Onuralp SEZER 1a7306c4ae ci(test): 🔥 removal formatters from test ci (#317)
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-09-10 23:29:45 +03:00
Onuralp SEZER c6b8fbed31 fix(pre-commit): formatting fix
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-09-08 21:29:37 +03:00
Onuralp SEZER b9598f54b6 feat: ✨ control thread worker and set default 1
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-09-08 15:08:37 +03:00
pre-commit-ci[bot] 21e5d5139c fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-08 12:04:17 +00:00
Onuralp SEZER 8f583404ce fix(import): 📦 import only ThreadPoolExecutor,as_completed
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-09-08 15:03:59 +03:00
pre-commit-ci[bot] bb42753e23 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-08 11:46:17 +00:00
James Gallagher ea0ec29c81 add concurrent processing for inference slicer 2023-09-08 12:43:20 +01:00
Onuralp SEZER 3d4326c79b Merge pull request #349 from onuralpszr/empty_box_checks
fix: 🐛 Empty check for xyxy results for prevent ValueError and return as empty detection
2023-09-05 20:17:17 +03:00
pre-commit-ci[bot] ca212b70c8 chore(pre_commit): ⬆ pre_commit autoupdate (#355)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.286 → v0.0.287](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.286...v0.0.287)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-09-05 20:15:19 +03:00
Piotr Skalski 0008b33eb1 Add traffic analysis demo to README.md 2023-09-05 17:52:29 +02:00
SkalskiP 77190f287c 📍 Pin supervision==0.15.0rc1 in examples/traffic_analysis 2023-09-04 23:08:12 +02:00
Piotr Skalski 121d4f31bb 👊 supervision version to `0.15.0rc1 in pyproject.toml` 2023-09-04 23:06:02 +02:00
Piotr Skalski 7c97e87e79 Merge pull request #354 from roboflow/example/traffic_analysis
example/traffic_analysis
2023-09-04 23:03:40 +02:00
SkalskiP 9640f1d756 TraceAnnotator documentation 2023-09-04 22:56:42 +02:00
SkalskiP 2419d42224 TraceAnnotator documentation 2023-09-04 22:52:47 +02:00
Piotr Skalski c6517de143 Update README.md 2023-09-04 22:36:26 +02:00
pre-commit-ci[bot] 2706304a73 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-04 20:18:46 +00:00
SkalskiP 8538c26a22 Add TraceAnnotator, refactor traffic analysis code and improve documentation
Introduced TraceAnnotator in the supervision package, refactored traffic analysis script for improved readability, and added quality documentation.
2023-09-04 22:16:37 +02:00
SkalskiP a0c60fe043 Refactor video stream and traffic analysis process
Restructuring the video stream and traffics analysis codes resulted in two new key classes, VideoProcessor, and DetectionsManager. The main aim behind this was to improve code readability and reusability. VideoProcessor class handles video processing as an object, while DetectionsManager is responsible for managing detections and trackings. This change has also led to the removal of the process_frame function, as its functionality is now encapsulated within VideoProcessor class methods. Moreover, the code now identifies zones in and out separately, thereby handling detections more distinctly and efficiently. Lastly, Polygon Zones are no longer differentiated by color.
2023-09-04 17:28:19 +02:00
SkalskiP 01d8b0316c "Improve detection and annotation in the supervision module
The change modifies the polygon zone verification method in `polygon_zone.py` to ensure `self.current_count` always returns an integer, preventing possible type errors in downstream processing. Two classes, `Trace` and `TraceAnnotator`, were added to `annotate.py`, providing functionality for trace annotation during detection. In `script.py`, optimized the trace annotation process by replacing the previous class implementations with the new ones from `annotate.py`, and also cleaned up and simplified polygon zone initiation and frame processing. This provides more efficient and robust video processing and zone-based detection features."
2023-09-03 18:25:28 +02:00
SkalskiP 6356a8ac3d Add Trace and TraceAnnotator classes to traffic_analysis script
Added two new classes to enhance traffic analysis over video frames. Trace class stores the positional information of different objects detected in each video frame for a specified length. TraceAnnotator class uses the information stored in Trace instance to annotate the motion path of detected objects on the video frames. Also, corrected the incorrect numpy import statement in the dependence code.
2023-09-03 17:11:25 +02:00
pre-commit-ci[bot] bbd9a955f8 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-09-02 19:45:43 +00:00
Onuralp SEZER 7b2bcf00ad fix: 🐛 sam xyxy check added
Signed-off-by: Onuralp SEZER <thunderbirdtr@fedoraproject.org>
2023-09-02 22:45:29 +03:00
Onuralp SEZER 775a3f2795 fix: 🐛 cpu().numpy() converts not require empty check
Signed-off-by: Onuralp SEZER <thunderbirdtr@fedoraproject.org>
2023-09-02 22:43:27 +03:00
Onuralp SEZER 5dae02e93e fix: 🐛 deepsparse model_stub to model_path docs fix
Signed-off-by: Onuralp SEZER <thunderbirdtr@fedoraproject.org>
2023-09-02 20:40:35 +03:00
Onuralp SEZER 666427df08 fix: 🐛 empty check for xyxy results for prevent ValueError and return as empty detection
Signed-off-by: Onuralp SEZER <thunderbirdtr@fedoraproject.org>
2023-09-02 20:36:00 +03:00
Hardik Davaandpre-commit-ci[bot] 78411b14b5 fixed empty detections for deepsparse (#348)
* fixed empty detections for deepsparse
* fix(pre_commit): 🎨 auto format pre-commit hooks

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-09-02 20:08:30 +03:00
SkalskiP 69ae5c6cd9 Add traffic analysis script
Introduced a python script for video processing using YOLO and ByteTrack. The script processes videos frame by frame and allows annotation based on model's detections. Additionally, the requirements.txt file was added along with .gitignore to exclude the data/ directory. The requirements file lists the necessary packages for the script to run.
2023-09-01 18:06:09 +02:00
dependabot[bot] 94d08bc9b5 Bump cryptography from 41.0.2 to 41.0.3 (#345)
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.2 to 41.0.3.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.2...41.0.3)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-01 12:27:48 +03:00
Piotr Skalski 693465d74d Merge pull request #342 from roboflow/main
Merge pull request #337 from roboflow/develop
2023-08-31 17:54:46 +02:00
Piotr Skalski f82f0fade6 Merge pull request #337 from roboflow/develop
`supervision-0.14.0` release
2023-08-31 15:18:14 +02:00
Piotr Skalski 474a4db248 Merge pull request #312 from onuralpszr/ci-release-clean
ci(pypi): 🚧 release pypi action automatic release per tag and main branch
2023-08-31 15:16:13 +02:00
SkalskiP 89a9307855 😊 make pre-commit happy 2023-08-31 15:13:11 +02:00
SkalskiP 5f94b6c23a 👊 supervision version to 0.14.0 2023-08-31 15:11:33 +02:00
SkalskiP 18c972e422 🪵 supervision-0.14.0 changelog update 2023-08-31 15:10:33 +02:00
Piotr Skalski 426c4498df Merge pull request #341 from roboflow/example/tracking
tracking supervision example
2023-08-31 14:57:18 +02:00
pre-commit-ci[bot] a3596584c7 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-08-31 12:54:34 +00:00
SkalskiP d8787080ec Merge remote-tracking branch 'origin/example/tracking' into example/tracking
# Conflicts:
#	examples/tracking/README.md
2023-08-31 14:54:18 +02:00
SkalskiP c9e10b8a04 Ready fo merge 💪 2023-08-31 14:54:02 +02:00
pre-commit-ci[bot] 597e12d723 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-08-31 11:13:06 +00:00
SkalskiP bb2d2c9f80 Initial commit with tracking supervision example 2023-08-31 13:06:27 +02:00
Onuralp SEZER c2a29d6577 ci: 🚧 pypi_test_username added and 'v' letter removed from tags
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-08-30 19:24:00 +03:00
Piotr Skalski 7a6da63a69 Update package version to 0.14.0b2 in pyproject.toml 2023-08-30 18:01:45 +02:00
Onuralp SEZER 796c17f085 ci: 🔥 branch push rule removed (#340)
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-08-30 19:00:28 +03:00
Onuralp SEZER ddd400ed24 ci(pypi): 🚧 test(a,b,rc) release to pypi github action added (#311)
* ci(pypi): 🚧 test(a,b,rc) release to pypi github action added

Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>

* ci: 🚧 pypi_test_username added

Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>

* chore: 🔥 v letter removed from tag for consistency to old tags

Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>

---------

Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-08-30 18:42:31 +03:00
Piotr Skalski 38b6ac9935 Update package version to 0.14.0b1 in pyproject.toml 2023-08-30 17:40:54 +02:00
dependabot[bot] 4a450ce2ed Bump tornado from 6.3.2 to 6.3.3 (#339)
Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.3.2 to 6.3.3.
- [Changelog](https://github.com/tornadoweb/tornado/blob/master/docs/releases.rst)
- [Commits](https://github.com/tornadoweb/tornado/compare/v6.3.2...v6.3.3)

---
updated-dependencies:
- dependency-name: tornado
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 18:40:20 +03:00
dependabot[bot] 9c40f6f4aa Bump certifi from 2023.5.7 to 2023.7.22 (#338)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.5.7 to 2023.7.22.
- [Commits](https://github.com/certifi/python-certifi/compare/2023.05.07...2023.07.22)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 18:39:43 +03:00
dependabot[bot] f6766cb116 Bump jupyter-server from 2.7.0 to 2.7.2 (#332)
Bumps [jupyter-server](https://github.com/jupyter-server/jupyter_server) from 2.7.0 to 2.7.2.
- [Release notes](https://github.com/jupyter-server/jupyter_server/releases)
- [Changelog](https://github.com/jupyter-server/jupyter_server/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jupyter-server/jupyter_server/compare/v2.7.0...v2.7.2)

---
updated-dependencies:
- dependency-name: jupyter-server
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 18:34:45 +03:00
Piotr Skalski 52f5d9ef63 Merge pull request #336 from roboflow/slicer_refactor
slicer refactor
2023-08-30 16:16:52 +02:00
SkalskiP 02117dd8ae Small fix in InferenceSlicer.__call__ 2023-08-30 16:08:56 +02:00
SkalskiP bddd32dbb5 refactor(inference_slicer): Update class and method docstrings 2023-08-30 15:56:15 +02:00
pre-commit-ci[bot] 07c4472eac fix(pre_commit): 🎨 auto format pre-commit hooks 2023-08-30 13:17:14 +00:00
SkalskiP 11541fe271 fix crop_image circular imports 2023-08-30 15:16:25 +02:00
SkalskiP 6d7de56c25 slicer refactor 2023-08-30 15:08:07 +02:00
Piotr Skalski 85de9b255b Merge pull request #282 from hardikdava/slicer
Slicer for small object detection
2023-08-30 14:48:46 +02:00
Piotr Skalski a3a19d93bc Merge pull request #329 from roboflow/add-links
Add explore more Roboflow open source projects section
2023-08-29 18:30:09 +02:00
Piotr Skalski 1eaccd3e87 Update README.md 2023-08-29 18:28:20 +02:00
James Gallagher eb1155cc17 commit feedback 2023-08-29 12:06:22 +01:00
Onuralp SEZER beb3a2ae9f docs: 📝 cosmetic fix and remove supervision itself from list
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-08-29 13:44:33 +03:00
James Gallagher b4efe69d1e add open source projects links 2023-08-29 11:28:45 +01:00
pre-commit-ci[bot] 3d6b5e75a8 chore(pre_commit): ⬆ pre_commit autoupdate (#328)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.285 → v0.0.286](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.285...v0.0.286)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-08-28 23:45:36 +03:00
Onuralp SEZER a7edf46717 docs: 📝 code quality duplication removal and improvement in current (#318)
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-08-24 22:09:15 +03:00
Tiago De Gaspari cd6a7fd186 Fix SAM video URL on README (#314) 2023-08-23 17:25:14 +03:00
Onuralp SEZER 82729a61b0 ci: 💚 pre-commit autoupdate (#307)
Signed-off-by: Onuralp SEZER <thunderbirdtr@fedoraproject.org>
2023-08-23 13:28:24 +03:00
Onuralp SEZER 30a51c72d6 ci(pypi): 🚧 release pypi action automatic release per tag and main branch
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-08-22 23:43:13 +03:00
Piotr Skalski 6b0be9c55b Merge pull request #296 from roboflow/fix/yoloexplorer
Image Path as keys for DetectionDataset
2023-08-22 16:47:41 +02:00
SkalskiP cf2c8cb364 Merge remote-tracking branch 'origin/fix/yoloexplorer' into fix/yoloexplorer 2023-08-22 16:38:11 +02:00
pre-commit-ci[bot] 7f8d091f50 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-08-22 16:38:02 +02:00
pre-commit-ci[bot] 36e03f3707 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-08-22 14:36:06 +00:00
SkalskiP f8baad3065 small simplifications 2023-08-22 16:34:54 +02:00
SkalskiP 18f7cad081 Merge remote-tracking branch 'origin/fix/yoloexplorer' into fix/yoloexplorer 2023-08-22 16:03:29 +02:00
SkalskiP bebb427a76 Merge remote-tracking branch 'origin/develop' into fix/yoloexplorer 2023-08-22 16:03:22 +02:00
Piotr Skalski 6f03b7ffd0 Merge pull request #309 from roboflow/add-cname
Create CNAME
2023-08-22 14:10:48 +02:00
James 06f41e83dd Create CNAME 2023-08-22 13:08:10 +01:00
Piotr Skalski 2ec33dbe9a Merge pull request #300 from ShubhamKanitkar32/null-check-fillPoly
Fix issue #299
2023-08-22 12:33:01 +02:00
pre-commit-ci[bot] b86f8ccdb8 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-08-22 10:25:41 +00:00
SkalskiP 7de50e6b0b 🛠️ fix ready for merging 2023-08-22 12:23:39 +02:00
Shubham Kanitkar 0f889935fd filtering out invalid point key 2023-08-21 16:38:30 -07:00
pre-commit-ci[bot] 61f972db79 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-08-21 19:08:35 +00:00
Hardik Dava 4b419fa19e pascal voc saving updated and unified 2023-08-21 21:08:24 +02:00
Hardik Dava 738d00cc89 yolo save methods updated 2023-08-21 20:57:30 +02:00
Piotr Skalski 9c53bc16ac Merge pull request #301 from onuralpszr/docs-detections-typo-fix
fix(docs): 📚🐛 deprecated for "from_yolov8" desc fix for detection
2023-08-20 21:29:04 +02:00
Onuralp SEZER 8a84463aaa fix(docs): 📚🐛 deprecated for "from_yolov8" desc fix 2023-08-20 16:01:23 +03:00
Shubham Kanitkar 9ca1204a4c Empty numpy array check 2023-08-18 13:09:55 -07:00
Derrick Mwiti 6a2203c07a Support DeepSparse Model Detection Results (#297)
* add Deepsparse model detection results and make sure results are arrays
2023-08-18 14:08:53 +03:00
pre-commit-ci[bot] 04df220737 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-08-18 10:48:25 +00:00
Hardik Dava 981bcc71ea Update coco.py 2023-08-18 12:48:09 +02:00
Hardik Dava 1d42992e6b Update core.py 2023-08-18 12:44:35 +02:00
pre-commit-ci[bot] 1c0434c6d8 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-08-18 10:42:33 +00:00
Hardik Dava b40eb5b47f Update utils.py 2023-08-18 12:42:23 +02:00
pre-commit-ci[bot] 98f12b60ea fix(pre_commit): 🎨 auto format pre-commit hooks 2023-08-18 10:39:49 +00:00
Hardik Dava 2c988ac269 Update core.py 2023-08-18 12:39:38 +02:00
Piotr Skalski f53c51ac8d Merge pull request #294 from arjun-234/develop
refactor: multi_predict()
2023-08-18 11:40:27 +02:00
Piotr Skalski 7426d52c4a Merge pull request #288 from roboflow/update-contributing
Update CONTRIBUTING.md file
2023-08-18 09:36:49 +02:00
Hardik Dava ad26e13704 Update core.py 2023-08-17 21:35:33 +02:00
Hardik Dava d8aecc4fe7 Update pascal_voc.py 2023-08-17 21:28:35 +02:00
Hardik Dava 7dbf279477 Update pascal_voc.py 2023-08-17 21:27:47 +02:00
Hardik Dava d59f9c4c74 Update coco.py 2023-08-17 21:26:36 +02:00
Hardik Dava 0d991009f7 changing image path as keys 2023-08-17 21:21:55 +02:00
pre-commit-ci[bot] 3a6de392fc fix(pre_commit): 🎨 auto format pre-commit hooks 2023-08-17 10:28:27 +00:00
James Gallagher 5bd78d11cc add license section 2023-08-17 11:28:14 +01:00
pre-commit-ci[bot] cd8541f948 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-08-17 10:18:18 +00:00
arjun-234 77a1faa7de refactor: multi_predict()
In this version, I have modified the code by combining three loops into a single loop within the same method. This enhancement has resulted in improved efficiency.
2023-08-17 15:45:53 +05:30
Piotr Skalski b47bc808f7 🛠️ Add "build with supervision" section to README.md 2023-08-16 20:11:04 +02:00
Piotr Skalski 16f3a61431 Merge pull request #235 from mayankagarwals/vocXMLOffsetFix
Fix Pascal VOC Offset
2023-08-16 13:26:46 +02:00
SkalskiP a051be0920 Merge remote-tracking branch 'origin/vocXMLOffsetFix' into vocXMLOffsetFix 2023-08-16 13:24:12 +02:00
SkalskiP 7d449abee2 🛠️ small fix 2023-08-16 13:23:10 +02:00
pre-commit-ci[bot] 12ec48105b fix(pre_commit): 🎨 auto format pre-commit hooks 2023-08-16 11:21:15 +00:00
SkalskiP afc240e6be 🛠️ small fix 2023-08-16 13:20:58 +02:00
Piotr Skalski c2b9745fe5 Merge pull request #289 from rizavelioglu/develop
fixes #285
2023-08-16 12:38:47 +02:00
Mayank Agarwal 4c5c15fcf4 flake format 2023-08-16 12:48:14 +05:30
Mayank Agarwal 47268c893c merge develop 2023-08-16 12:44:55 +05:30
Piotr Skalski 512f98f074 Merge pull request #278 from mayankagarwals/removeYoloV8Support
Remove usage `from_yolov8` from documentation and `README.md`
2023-08-15 15:16:59 +02:00
Mayank Agarwal 1f0b50645c demo changes 2023-08-15 18:01:24 +05:30
Mayank Agarwal 3c5f5f7741 Merge Develop 2023-08-15 17:58:31 +05:30
rizavelioglu 01956a2735 fixes #285 2023-08-15 14:22:37 +02:00
James Gallagher a2b8cf50d6 add docs 2023-08-15 13:21:50 +01:00
Piotr Skalski bfe65013ab Merge pull request #281 from mayankagarwals/classificationUltralytics
Feature/ultralytics all models for Classification
2023-08-15 12:52:25 +02:00
SkalskiP 824b2f6dc2 🛠️ small fix 2023-08-15 12:40:30 +02:00
8f70513edc Fix citation.cff file (#287)
* fix citation.cff
* fix(pre_commit): 🎨 auto format pre-commit hooks

---------

Co-authored-by: Onuralp SEZER <thunderbirdtr@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-08-15 12:21:36 +03:00
James Gallagher 9167101e92 add develop branch guidance 2023-08-15 10:05:56 +01:00
pre-commit-ci[bot] b064356280 fix(pre_commit): 🎨 auto format pre-commit hooks 2023-08-15 09:03:18 +00:00
James Gallagher 4e5f87686f update contributing file 2023-08-15 10:02:22 +01:00
Mayank Agarwal 7bd6a107ce fix pre-commit-ci flake8 test 2023-08-15 11:02:02 +05:30
Mayank Agarwal a71d8c254a fix pre-commit-ci flake8 test 2023-08-15 11:01:24 +05:30
Mayank Agarwal 755365a92d Merge remote-tracking branch 'origin/classificationUltralytics' into classificationUltralytics 2023-08-15 10:57:55 +05:30
Mayank Agarwal 8c70197734 flake tests 2023-08-15 10:57:42 +05:30
pre-commit-ci[bot] 735de7de7e fix(pre_commit): 🎨 auto format pre-commit hooks 2023-08-15 05:21:04 +00:00
Mayank Agarwal dacce7aa5f embellish comment 2023-08-15 10:50:49 +05:30
Mayank Agarwal a5c6fe57f6 embellish comment 2023-08-15 10:47:16 +05:30
Onuralp SEZER b69d264ffe Merge pull request #286 from roboflow/paddle_typo_doc
docs: 📝 typo fix for PaddleDetection in args
2023-08-14 22:29:45 +03:00
Hardik Dava 92413d3cc2 Merge pull request #277 from hardikdava/feature/trace_annotators
tracer in consistent format
2023-08-14 21:20:56 +02:00
Mayank Agarwal 5f314cf21f remove redundant imports from comments 2023-08-15 00:48:12 +05:30
Onuralp SEZER bc4ba7f67c docs: 📝 typo fix for PaddleDetection in args
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-08-14 22:16:52 +03:00
Mayank Agarwal 47385f5a97 Merge remote-tracking branch 'origin/classificationUltralytics' into classificationUltralytics 2023-08-12 17:37:34 +05:30
Mayank Agarwal 234f6fa43b change examples to classification models 2023-08-12 17:37:00 +05:30
Mayank Agarwal 26402386aa Merge branch 'develop' into classificationUltralytics 2023-08-12 17:31:26 +05:30
Piotr Skalski 6f427b9ca4 Merge pull request #241 from onuralpszr/pre-commit-toml-file
feat: 🚀 pre-commit and formatted files
2023-08-12 13:43:20 +02:00
Hardik Dava 6a40516371 style fixing 2023-08-11 19:42:04 +02:00
Hardik Dava 1e1ce3c060 reverting mkdocs change 2023-08-11 19:38:32 +02:00
Hardik Dava 8fb8432e8b fixed style 2023-08-11 19:33:09 +02:00
Hardik Dava 0ed404602f recommit everything 2023-08-11 19:31:33 +02:00
Piotr Skalski d81d187da6 Merge branch 'develop' into pre-commit-toml-file 2023-08-11 15:21:28 +02:00
Mayank Agarwal b90f9a1677 doc fix 2023-08-11 18:27:04 +05:30
Mayank Agarwal 67a81afcfa format fix 2023-08-11 17:55:13 +05:30
Mayank Agarwal f0bc628eaf classification ultralytics 2023-08-11 17:47:17 +05:30
SkalskiP f38c438c3c 🛠️ fix update_with_detections documentation 2023-08-11 11:22:55 +02:00
Mayank Agarwal d37c62d0cf remove yolo v8 support 2023-08-10 17:35:51 +05:30
Hardik Dava 8b764218eb tracer in consistent format 2023-08-10 10:00:26 +02:00
Piotr Skalski 4f9d8c02c7 Merge pull request #275 from roboflow/main
`supervision-0.13.0` release
2023-08-08 11:09:35 +02:00
Piotr Skalski 4f79d29934 supervision-0.13.0 release
`supervision-0.13.0` release
2023-08-08 11:07:24 +02:00
Piotr Skalski d86623600a Merge pull request #274 from roboflow/v0.13.0-changelog
v0.13.0 changelog
2023-08-07 22:53:04 +02:00
SkalskiP 475b2ceeea 🪵 changelog.md update 2023-08-07 22:50:57 +02:00
Onuralp SEZER b1325132d7 fix(docs): 🐛 doc formatting fix and exclude docs from black formatting
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-08-07 23:49:44 +03:00
Onuralp SEZER 3d3b3e6436 Merge additonal features and changes from 'origin/develop'
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-08-07 23:23:23 +03:00
Piotr Skalski 95661f3bb1 Merge pull request #273 from hardikdava/feature/ultralytics_tracker
ultralytics tracker support added
2023-08-07 22:11:23 +02:00
James Gallagher e6ab865a0c add changelog updates 2023-08-07 20:55:55 +01:00
Hardik Dava 63669e587a docstrings updated 2023-08-07 21:23:28 +02:00
Hardik Dava 2166bc23bc ultralytics tracker support added 2023-08-07 21:19:29 +02:00
SkalskiP 867dd78f58 📝 small ByteTrack docs updates 2023-08-07 17:51:08 +02:00
SkalskiP 255b34a183 Merge remote-tracking branch 'origin/develop' into develop 2023-08-07 16:43:02 +02:00
SkalskiP f7b59870ed 👊 bump supervision version 2023-08-07 16:42:44 +02:00
Piotr Skalski 7948234f94 Merge pull request #256 from roboflow/tracker_latest
Tracker latest
2023-08-07 16:29:44 +02:00
Piotr Skalski b5e83fa953 Merge pull request #271 from roboflow/tracker_cleanup
🧹 tracker cleanup
2023-08-07 16:16:54 +02:00
SkalskiP de536a3f8e 🖤 make black happy 2023-08-07 16:08:44 +02:00
SkalskiP 6741ae642a 🧹 more cleanup - remove more dead code + update docstring 2023-08-07 16:04:58 +02:00
SkalskiP 3834e5cdf9 🧹 more cleanup - refactor of joint_tracks, sub_tracks and remove_duplicate_tracks functions + documentation updates 2023-08-07 15:41:36 +02:00
SkalskiP aa77c396f7 🧹 more cleanup - update docs, remove dead if branches 2023-08-07 15:18:17 +02:00
SkalskiP bee6dec22c 🧹 more cleanup - solve poetry.lock and pyproject.toml 2023-08-07 14:59:39 +02:00
SkalskiP db2eede55b 🧹 more cleanup - remove more unused code, documentation improvements 2023-08-07 14:39:25 +02:00
SkalskiP ba880402ad 🧹 more cleanup - remove unused code 2023-08-07 14:20:36 +02:00
SkalskiP 62e097b559 🧹 more cleanup - dropping example scripts, plugging in ByteTrack docs, renaming variables 2023-08-07 14:11:51 +02:00
SkalskiP 451bb5e513 🧹 more cleanup 2023-08-07 13:40:51 +02:00
SkalskiP 330167fc73 🖤 make black happy 2023-08-07 13:30:21 +02:00
SkalskiP 1c23e806ba 🧹 initial cleanup 2023-08-07 13:28:45 +02:00
Piotr Skalski 2c31f8ec94 Add mAP example to README.md 2023-08-07 12:03:38 +02:00
Piotr Skalski a6c73f70ec Merge pull request #270 from roboflow/feature/map_reformat
🧹 small cleanup
2023-08-07 11:53:34 +02:00
SkalskiP 13ee396b20 🧹 small cleanup 2023-08-07 11:51:04 +02:00
SkalskiP 0f222375e2 🧹 small cleanup 2023-08-07 11:43:08 +02:00
SkalskiP a63f65fff3 🧹 small cleanup 2023-08-07 11:31:52 +02:00
Piotr Skalski 94f15a6e50 Merge branch 'develop' into tracker_latest 2023-08-07 10:07:36 +02:00
Piotr Skalski 525e1b00ec Merge pull request #236 from hardikdava/feature/map
Feature MeanAveragePrecision 🔥
2023-08-07 10:03:33 +02:00
SkalskiP 7f4662cf3d Final changes before merge 2023-08-07 09:58:46 +02:00
SkalskiP 3a31ea2986 More small changes 2023-08-06 13:24:51 +02:00
Hardik Dava e0ca4ca5e7 added docstring and minor reformatting 2023-08-06 12:30:13 +02:00
Hardik Dava 0fcc17ee11 Docstrings updated, return types fixed. 2023-08-06 12:23:00 +02:00
SkalskiP 52cbe684d4 🖤 MAke black happy 2023-08-06 11:57:20 +02:00
SkalskiP fce6e660d9 Small fixes and improvements. 2023-08-06 11:52:48 +02:00
SkalskiP c6f2bf27c1 🖤 make black happy 2023-08-06 11:12:42 +02:00
Piotr Skalski 523d76f86d Merge pull request #262 from danigarciaoca/patch-2
Update core.py (n, W, H) -> (n, H, W)
2023-08-04 19:39:40 +02:00
Daniel M. García-Ocaña 1ba1b67b47 Update core.py (n, W, H) -> (n, H, W)
Fix mask array shape in docstrings (n, W, H) -> (n, H, W) #254
2023-08-02 18:51:49 +02:00
dbroboflow a45cc865dd lint fixed 2023-08-02 14:59:08 +00:00
dbroboflow 4942d3d817 Merge pull request #260 from hardikdava/hot_fix/tracker
tracker optimized
2023-08-02 17:31:42 +03:00
hardik 514f61b885 initial optimized working version 2023-08-02 16:04:28 +02:00
Onuralp SEZER fc154e34fe chore(merge): merge latest develop
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-08-02 15:46:58 +03:00
Onuralp SEZER 6f0f3bc3bc chore(cleanup): 🧹 removing old code-formatting CI
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-08-02 15:44:56 +03:00
SkalskiP b40e2f228a 🧹 small cleanup 2023-08-02 14:40:16 +02:00
Onuralp SEZER 21062b22a4 fix: 🐛 test_pascal_voc data format fix for pytest
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-08-02 15:35:34 +03:00
Onuralp SEZER f9d3aaa67e fix(formatting): 🐛 merge latest code from develop for formatting
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-08-02 15:29:29 +03:00
Mayank Agarwal 58156fe61e rename test 2023-08-02 09:44:05 +05:30
Mayank Agarwal d170cbeda0 fix tests according to offset 2023-08-02 09:42:18 +05:30
Mayank Agarwal e131f8b197 fix polygon loading as per new PR 2023-08-01 23:32:40 +05:30
Mayank Agarwal f372fb69e9 reformat according to makefile 2023-08-01 23:29:09 +05:30
Mayank Agarwal 9f8291405a Merge branch 'develop' into vocXMLOffsetFix 2023-08-01 23:07:10 +05:30
dbroboflow da9d50b4e2 docs updated 2023-08-01 16:33:39 +00:00
dbroboflow 8f437107b8 scipy fixed 2023-08-01 13:23:42 +00:00
dbroboflow 2a9a1f05bb scipy fixed 2023-08-01 13:21:39 +00:00
dbroboflow 414e0327bc scipy added to pyproject.toml 2023-08-01 13:18:22 +00:00
dbroboflow f2d2827eee lint fixed again 2023-08-01 13:13:27 +00:00
dbroboflow 8eaa42abc6 lint fixed 2023-08-01 13:10:36 +00:00
dbroboflow aa50052ff5 mot20 deleted 2023-08-01 12:59:16 +00:00
dbroboflow a59e4dfe73 updated tracker 2023-08-01 12:30:52 +00:00
Piotr Skalski d6d4760fce Merge pull request #245 from kirilllzaitsev/feature/load_voc_segmentation
Add support for loading segmentation datasets in Pascal VOC format
2023-08-01 00:33:05 +02:00
Piotr Skalski f2bbb7c38c Merge pull request #222 from hardikdava/feature/ultralytics_all_models
Feature/ultralytics all models
2023-07-31 23:23:20 +02:00
SkalskiP fa44884ea7 💬 update YOLOv8 deprecated message 2023-07-31 23:16:20 +02:00
kirilllzaitsev 6fdabf0a96 extend tests for test_detections_from_xml_obj 2023-07-31 20:10:20 +02:00
kirilllzaitsev 26289e3743 drop load_pascal_voc_annotations_v1 2023-07-31 19:43:32 +02:00
kirilllzaitsev e2d1fe7695 remove artifacts 2023-07-31 19:42:53 +02:00
Hardik Dava 36d3e94a76 removing unwanted code 2023-07-31 18:51:02 +02:00
kirilllzaitsev c0c292ef82 lint 2023-07-31 15:19:39 +02:00
kirilllzaitsev 9462a3058d cast masks to bool only when creating a Detection obj 2023-07-31 13:15:40 +02:00
kirilllzaitsev 77c2afe559 fix mask dtype 2023-07-31 13:13:32 +02:00
kirilllzaitsev 95c8576aaf fix mask shape (N, W, H) -> (N, H, W) 2023-07-31 13:09:35 +02:00
kirilllzaitsev 12b3def032 upd docstring 2023-07-29 23:42:59 +02:00
kirilllzaitsev e8f616ba2f cleanup imports 2023-07-29 23:41:02 +02:00
kirilllzaitsev f8005b268b add docstrings 2023-07-29 23:39:42 +02:00
kirilllzaitsev e4ef57ec2d add test_detections_from_xml_obj 2023-07-29 23:27:33 +02:00
kirilllzaitsev 1642432541 add test_parse_polygon_points 2023-07-29 23:08:50 +02:00
kirilllzaitsev 1febd715e2 remove test_detections_to_pascal_voc. conversion to XML is tested, and approx of masks should be in another test suite 2023-07-29 22:53:24 +02:00
kirilllzaitsev 9a9d0c5259 refactor class_id assignment to Detections in VOC 2023-07-29 00:39:11 +02:00
kirilllzaitsev 446ae23d99 fix registering of empty detection 2023-07-29 00:38:08 +02:00
kirilllzaitsev a181826527 add test_object_to_pascal_voc 2023-07-28 23:13:11 +02:00
kirilllzaitsev 0a1212f23d fix with_masks defined but not used 2023-07-28 22:22:03 +02:00
kirilllzaitsev b0e0d074af fixes patch 2023-07-28 22:21:47 +02:00
Piotr Skalski 27e8e2d91b Merge pull request #191 from hardikdava/paddledet_support
Paddledet support
2023-07-28 14:57:21 +02:00
Piotr Skalski f4c17f09c8 Merge pull request #248 from roboflow/fix/pillow_version_update
📦 update `Pillow` version
2023-07-28 14:17:02 +02:00
SkalskiP b219961f6e 📦 update Pillow version 2023-07-28 13:45:29 +02:00
Onuralp SEZER 1645ecf4bc feat(pre-commit): flake8 and flake8 toml support extension added into pre-commit config and flake8 rules added into toml
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-27 09:36:19 +03:00
Onuralp SEZER d4001a5bfa feat(pre-commit): black and isort added
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-27 09:25:12 +03:00
hardik 70fe357a4e fixed docstring 2023-07-26 18:13:24 +02:00
hardik 4b621c3c8d update 2023-07-26 13:58:53 +02:00
kirilllzaitsev 277a9cb6b9 import polygon_to_mask from supervision 2023-07-26 12:36:49 +02:00
kirilllzaitsev 6dde70eb0c move fixing of class_ids to load_pascal_voc_annotations 2023-07-26 10:46:31 +02:00
kirilllzaitsev a91118322b update docstrings 2023-07-26 10:45:46 +02:00
kirilllzaitsev 86b33ed558 add test_pascal template 2023-07-26 10:39:45 +02:00
kirilllzaitsev 3723eae50c update from_pascal_voc to match v2 loader 2023-07-26 10:21:17 +02:00
kirilllzaitsev d7e08d42eb add load_pascal_voc_annotations v2 2023-07-26 10:20:47 +02:00
Onuralp SEZER af82b4e2d2 fix: 🐛 200+ E501 doc error lines are fixed.
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-25 23:09:14 +03:00
hardik 9fef53f8a3 Merge remote-tracking branch 'origin/feature/map' into feature/map 2023-07-25 18:15:33 +02:00
hardik 8d8703a1c6 update 2023-07-25 18:15:26 +02:00
Hardik Dava 84a59a7e01 Merge branch 'roboflow:develop' into feature/map 2023-07-25 17:59:32 +02:00
hardik dec982200b update 2023-07-25 17:53:07 +02:00
hardik da6a324c1f update 2023-07-25 14:32:19 +02:00
Mayank Agarwal 7e391d0861 reformat 2023-07-25 16:35:36 +05:30
Mayank Agarwal c2018fa1ba increment polygon 2023-07-25 16:31:26 +05:30
Mayank Agarwal 6f7a50f41b increment polygon 2023-07-25 16:29:27 +05:30
Mayank Agarwal 204a04a85f refactor 2023-07-25 16:16:11 +05:30
Mayank Agarwal 5727fb5cb2 refactor 2023-07-25 16:15:20 +05:30
Mayank Agarwal a9dce586c2 refactor 2023-07-25 16:12:50 +05:30
Onuralp SEZER 57e17c34a0 feat: 🚀 pre-commit and initial formatted files
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-25 13:02:26 +03:00
Piotr Skalski 36cdcf1b12 Merge pull request #229 from hardikdava/docs/colab_request
Contributing guidelines added for colab request
2023-07-25 11:48:03 +02:00
Mayank Agarwal 3c64871c87 move increment within object_to_pascal_voc 2023-07-25 15:08:15 +05:30
Piotr Skalski 7ec6bcd916 Merge pull request #237 from onuralpszr/toml-license-classifer-fix
docs: 📝 MIT license classifier added
2023-07-25 11:08:34 +02:00
Hardik Dava da13366d0a Merge branch 'roboflow:develop' into feature/map 2023-07-25 10:46:58 +02:00
Onuralp SEZER 771a47e15d docs: 📝 MIT license classifier added
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-25 08:30:37 +03:00
Mayank Agarwal fe00da70bf remove separate function 2023-07-25 09:29:32 +05:30
Piotr Skalski 7067741310 © bring back MIT as license in pyproject.toml 2023-07-24 23:31:55 +02:00
SkalskiP 117ba2fcc0 🧪 initial API tests 2023-07-24 23:17:21 +02:00
hardik f4dc515df1 update 2023-07-24 18:20:01 +02:00
hardik 0754cb75bd update 2023-07-24 17:53:19 +02:00
hardik 44a174f889 removed extra blank lines 2023-07-24 16:33:42 +02:00
hardik e9fbcf5dcb updated docstrings 2023-07-24 16:33:10 +02:00
hardik 9b6b74dc77 updated docstrings 2023-07-24 16:30:54 +02:00
hardik e249ea8644 update 2023-07-24 16:11:16 +02:00
Mayank Agarwal 64c446ae44 format code 2023-07-24 19:20:53 +05:30
Mayank Agarwal dfd26545f8 remove redundant comment 2023-07-24 19:16:52 +05:30
Mayank Agarwal a28755bbdb modularize 2023-07-24 19:16:24 +05:30
Mayank Agarwal 06d0646fb8 fix bbox for pascal voc loading and saving 2023-07-24 19:01:21 +05:30
Mayank Agarwal 8afd54a1cc format code 2023-07-24 18:23:06 +05:30
Mayank Agarwal cddab1cbe6 fix pascal voc offset 2023-07-24 18:08:39 +05:30
Piotr Skalski 8662671e6b 🛠️ fix coco segmentation data for polygon should be [polygon,] not polygon
Format coco output data as per standard
2023-07-24 14:07:10 +02:00
Hardik Dava c7c8e67ffd Update CONTRIBUTING.md 2023-07-23 22:56:57 +02:00
Hardik Dava b2b75f1105 Update CONTRIBUTING.md 2023-07-23 22:56:20 +02:00
hd 2f726ca795 Initial working version 2023-07-23 22:37:44 +02:00
hd 40a6fe1b5f update 2023-07-23 22:35:51 +02:00
hd 0cfad8407c update 2023-07-23 22:27:47 +02:00
Piotr Skalski 1d28b8ae26 Merge pull request #171 from hardikdava/advanced_box_annotator
cornered box annotator
2023-07-23 20:44:29 +02:00
SkalskiP cc2e08ac72 🖤 make black happy 2023-07-23 20:40:46 +02:00
SkalskiP ba03fefbd8 🧹‍💨 cleanup in progress 2023-07-23 20:39:12 +02:00
SkalskiP 4cd9f05fab 🧪 reformatted tests 2023-07-23 20:08:34 +02:00
SkalskiP 8066125f03 🧹‍💨 clean up 2023-07-23 19:43:51 +02:00
SkalskiP aa772dde8d 🧹‍💨 clean up 2023-07-23 19:42:47 +02:00
Mayank Agarwal 6587f39b0f accomodate for no polygon indices 2023-07-23 20:52:07 +05:30
hd 0f95c5d8b3 update 2023-07-23 16:44:14 +02:00
Piotr Skalski 800e39d64c 🪵 supervision-0.12.0 changelog updates
Add 0.12.0 changelog
2023-07-23 15:34:44 +02:00
SkalskiP 926f935bad 🪵 supervision-0.12.0 changelog updates 2023-07-23 15:31:45 +02:00
Piotr Skalski d3da6afa07 Merge pull request #223 from roboflow/fix/confusion_matrix_plot_bug
🛠️ #212 bug fix
2023-07-23 15:00:58 +02:00
Piotr Skalski 97ae155b1e Merge pull request #226 from hardikdava/fix/yolo_loading_bug
Fix yolo dataset background instance loading
2023-07-23 14:51:11 +02:00
Piotr Skalski 24f013757f Update README.md 2023-07-23 14:12:16 +02:00
SkalskiP 532245640b ⚡ Make convert_detections_to_tensor static method 2023-07-23 13:41:57 +02:00
Hardik Dava 864764e665 Update core.py 2023-07-23 13:37:50 +02:00
hd ca348401d5 Fix for loading background image loading 2023-07-23 13:33:01 +02:00
hd 6def59444d initial commit 2023-07-23 13:27:21 +02:00
Mayank Agarwal bc0a38500d Format coco output data as per standard 2023-07-23 15:15:09 +05:30
hd 300ebea8ec Merge branch 'main' of github.com:hardikdava/supervision into main 2023-07-23 11:37:43 +02:00
SkalskiP 5cc7da289e 🛠️ #212 bug fix 2023-07-23 01:22:03 +02:00
hd 5edd58d6de ready for review 2023-07-22 20:45:41 +02:00
Hardik Dava 5729f8ffc3 Merge branch 'roboflow:main' into feature/ultralytics_all_models 2023-07-22 20:09:07 +02:00
Piotr Skalski 7a07517750 🧮 Add confusion matrix to README.md 2023-07-22 18:12:26 +02:00
Piotr Skalski 699fc01a0c Merge pull request #218 from onuralpszr/devtools
feat: 🛠  adding python dev tools into toml file
2023-07-22 17:42:58 +02:00
Piotr Skalski 43339e380a Merge pull request #220 from onuralpszr/docs-py-ver-fix
docs: 📝 Update python version for supervision version 0.12.0
2023-07-22 17:37:11 +02:00
Piotr Skalski 89a33cda8a Merge pull request #219 from onuralpszr/development-version-213
fix: 🐛 package version control if PackageNotFoundError occured
2023-07-22 17:26:55 +02:00
Piotr Skalski e84302b43f Merge pull request #217 from roboflow/fix/confusion_matrix_errors
🛠️ fix `detections_to_tensor`
2023-07-22 17:22:55 +02:00
Hardik Dava 10d3e3d48f Merge branch 'roboflow:main' into feature/ultralytics_all_models 2023-07-22 16:14:07 +02:00
hd 3c59618152 ready for review 2023-07-22 16:12:31 +02:00
hd e882c398b7 Added method for complete ultralytics model usage 2023-07-22 16:10:31 +02:00
Onuralp SEZER 93ab21dda7 docs: 📝 python 3.11 pypi classifier added
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-22 16:16:57 +03:00
Onuralp SEZER effa1415b5 docs: 📝 Update python version for supervision version 0.12.0
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-22 16:14:03 +03:00
hd e9e075acaf Merge branch 'main' of github.com:hardikdava/supervision into main 2023-07-22 10:44:12 +02:00
Onuralp SEZER 61e9584f5a fix: 🐛 package version control if PackageNotFoundError occured
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-22 10:40:17 +03:00
Onuralp SEZER 2a640d3ab6 feat: 🛠 adding develop branch into CIs
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-22 09:31:07 +03:00
Onuralp SEZER 2f478f4d06 feat: 🛠 adding python dev tools into toml file
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-22 09:24:28 +03:00
SkalskiP 0c20830be5 🖤 make black happy 2023-07-22 01:05:07 +02:00
SkalskiP 6c51c4a1b4 🛠️ fix detections_to_tensor 2023-07-22 00:42:26 +02:00
Piotr Skalski d6883caadc Merge pull request #214 from onuralpszr/readme-installation-docs
docs:  📝 update installation and source installation methods
2023-07-21 23:52:51 +02:00
SkalskiP f1f2728e0d 📝 updated install guide 2023-07-21 23:11:03 +02:00
Onuralp SEZER b6b7663ea7 docs: 📝 update installation and source installation methods
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-21 23:46:59 +03:00
SkalskiP 3d074fa213 🧮 Confusion Matrix docs update 2023-07-21 19:51:19 +02:00
James Gallagher 9fd267acb2 add changelog updates 2023-07-21 13:30:56 +01:00
Piotr Skalski 727451b382 Merge pull request #207 from onuralpszr/docs-ci-action-version-update
chore: ✨ update github action versions (actions/checkout@v3, actions/setup-python@v4)
2023-07-21 14:17:09 +02:00
Piotr Skalski 1b5e6db196 Merge pull request #159 from Ucag/feature/unicode-label-support
Add unicode label support
2023-07-21 14:13:40 +02:00
Onuralp SEZER 5f432fbc93 chore: ✨ update github action versions (actions/checkout@v3, actions/setup-python@v4)
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-21 14:46:20 +03:00
Piotr Skalski ea4638f5f5 Merge pull request #177 from kirilllzaitsev/feature/conf_matrix
Add ConfusionMatrix to EvaluationAPI
2023-07-21 13:31:26 +02:00
hd fc4bb27aa8 removing extra spacing 2023-07-21 12:54:44 +02:00
hd ace6ce363f Tests added for anchor fn 2023-07-21 12:54:19 +02:00
kirilllzaitsev 4b38634c63 add comments to _drop_extra_matches 2023-07-21 12:34:43 +02:00
hd 985608146c Docstrings updated 2023-07-21 12:13:46 +02:00
hd f0cef99de7 ready for review 2023-07-21 11:53:44 +02:00
kirilllzaitsev 04b7badcbe remove mock detection dataset 2023-07-21 11:44:28 +02:00
kirilllzaitsev 54524a4a1f drop docstring in _drop_extra_matches 2023-07-21 11:43:57 +02:00
kirilllzaitsev 3f55e7a79a updates patch 2023-07-21 11:42:50 +02:00
kirilllzaitsev 7a782b1ac3 cleanup test_detection 2023-07-20 15:59:50 +02:00
kirilllzaitsev 6016338a4f remove test_from_detections 2023-07-20 15:48:38 +02:00
kirilllzaitsev 8d131ad3e8 add convert_detections_to_tensor 2023-07-20 15:48:23 +02:00
kirilllzaitsev ae55e9c908 add test_convert_detections_to_tensor 2023-07-20 15:48:08 +02:00
kirilllzaitsev 5cb3c603cd remove test_benchmark 2023-07-20 15:39:55 +02:00
kirilllzaitsev 56e0b77211 add _validate_input_tensors 2023-07-20 15:35:59 +02:00
kirilllzaitsev bc9fe6f690 fix CM plot colors 2023-07-20 13:24:25 +02:00
kirilllzaitsev fb211a6160 add IoU tests 2023-07-20 10:14:06 +02:00
kirilllzaitsev 37b7224380 black 2023-07-20 10:13:58 +02:00
kirilllzaitsev 314454c7a7 add test cases to test_from_tensors 2023-07-20 09:57:44 +02:00
kirilllzaitsev ac39bdd487 add empty (preds,targets) test 2023-07-20 09:27:29 +02:00
kirilllzaitsev fc1e899575 fix failing tests (reduce threshold to 0.2) 2023-07-20 09:17:47 +02:00
kirilllzaitsev fccbe6baa2 add test_from_tensors 2023-07-20 00:22:12 +02:00
kirilllzaitsev d21154e882 fix _evaluate_detection_batch. use np.ndarray 2023-07-20 00:21:06 +02:00
kirilllzaitsev 1b3af74756 add ConfusionMatrix.from_tensors 2023-07-19 23:57:05 +02:00
kirilllzaitsev dc1083711a remove ConfusionMatrix.from_matrix 2023-07-19 23:13:19 +02:00
kirilllzaitsev 8df5c5b0c4 rm sans-serif from CM plot 2023-07-19 23:07:28 +02:00
kirilllzaitsev abbc0e44b0 fixes patch 2023-07-19 23:01:50 +02:00
kirilllzaitsev 950744111a fix type annotations 2023-07-19 22:54:39 +02:00
hd c4d296fbc6 Ready for review 2023-07-19 10:58:39 +02:00
hd 9c4555a800 Added extra anchor points calculation 2023-07-19 09:47:49 +02:00
Piotr Skalski 20954a2996 Merge pull request #180 from onuralpszr/python-toml-file-convert
Introduce pyproject toml file with optional install method and dropping python 3.7 support
2023-07-18 22:59:55 +02:00
Piotr Skalski 22d82c306f Merge pull request #173 from hardikdava/mmdetection_support
Mmdetection support
2023-07-18 15:27:30 +02:00
Onuralp SEZER 399121b65e fix: 🐛 lower pillow version for google-colab environment
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-18 16:24:17 +03:00
Piotr Skalski 1b9c65bdf8 🛠️ Merge pull request #192 from hardikdava/fix/coco_empty_fix
🛠️ Fixing empty detection coco dataset
2023-07-14 15:26:28 +02:00
SkalskiP 3a8c4d63ea 🧪 tests added 2023-07-14 15:06:55 +02:00
hardik bfe058c307 Fixing empty dataset 2023-07-13 20:19:55 +02:00
hd 12ce5feab7 paddledet refactored 2023-07-10 18:13:50 +02:00
hd 186bfc44fa paddledet initial support added 2023-07-10 18:03:56 +02:00
kirilllzaitsev 2c0905ba28 reformat 2023-07-08 12:29:50 +02:00
kirilllzaitsev fd717ea749 extend docs 2023-07-08 12:29:16 +02:00
kirilllzaitsev 40853350fd make plot() return figure 2023-07-08 12:11:24 +02:00
kirilllzaitsev fdd8127293 add source 2023-07-08 11:58:51 +02:00
kirilllzaitsev 21a480b483 refactor, remove duplicated 2023-07-08 11:54:25 +02:00
kirilllzaitsev 1b013f85e5 remove num_classes attr of ConfusionMatrix 2023-07-08 11:40:35 +02:00
kirilllzaitsev 36b48def89 rename vars, refactor ternary 2023-07-08 11:40:07 +02:00
kirilllzaitsev b7f72b79de add test_benchmark 2023-07-08 10:06:14 +02:00
kirilllzaitsev 141428310a add dummy_detection_dataset to utils 2023-07-08 10:05:52 +02:00
kirilllzaitsev 8f39df2fc0 add test_from_matrix 2023-07-07 22:54:22 +02:00
kirilllzaitsev 5107dd359f add test_drop_extra_matches 2023-07-07 22:49:47 +02:00
hd 4b423fd402 Fixed typo in annotator 2023-07-07 22:40:30 +02:00
kirilllzaitsev d480ff5be2 add test_evaluate_detection_batch 2023-07-07 22:34:23 +02:00
kirilllzaitsev 2b876bdd64 add from_matrix() 2023-07-07 22:29:17 +02:00
kirilllzaitsev 476be527e5 add test_from_detections 2023-07-07 22:28:21 +02:00
Onuralp SEZER f78ac90464 feat(package): 📦 opencv-python-headless set as default
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-07 20:53:58 +03:00
hd 27efbc044d Adding EllipseAnotator and added backward compatibility for box and mask annotator 2023-07-07 19:18:12 +02:00
hd 2a2c259964 🔥 Consistent code style 2023-07-07 09:37:03 +02:00
kirilllzaitsev d4ea295f09 add empty test templates 2023-07-06 22:46:45 +02:00
kirilllzaitsev 9c5c296fb6 docs 2023-07-06 22:43:43 +02:00
kirilllzaitsev 85c4d5a644 rm extra colorbar configs 2023-07-06 22:30:37 +02:00
kirilllzaitsev ad06937653 set figsize as plot() arg 2023-07-06 22:29:22 +02:00
kirilllzaitsev e01e495c92 rm test setup from plot() 2023-07-06 22:27:53 +02:00
kirilllzaitsev 6784c78b4f refine plot() 2023-07-06 22:27:21 +02:00
kirilllzaitsev 4375e19245 add benchmark() 2023-07-06 09:37:00 +02:00
Onuralp SEZER a75a92708c feat(package): 📦 pillow library added into toml file as deps
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-06 09:38:55 +03:00
Onuralp SEZER c83e57c638 fix: 📝 rename extras group into desktop and headless
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-06 09:35:27 +03:00
hd fcde1cea9c 🔥 working 2023-07-05 18:37:05 +02:00
hd 8614984f8f Initial commit for Detection Dataset Accumulator 2023-07-05 18:33:17 +02:00
hd c705d50e74 Marked as ready to review 2023-07-05 15:39:22 +02:00
hd d5b8a3aa1a Adding cornered box annotator and pillow label annotator 2023-07-05 15:01:16 +02:00
Onuralp SEZER cc81ae723b feat(version): ✨ dynamic version from __package__
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-05 15:37:50 +03:00
Onuralp SEZER f7a4a788ed docs: 📝 removing python 3.7 metadata pyproject
docs: 📝 change min python 3.7 to 3.8 in readme

Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-05 15:21:35 +03:00
kirilllzaitsev 35d9ee7bad non-int ticks 2023-07-05 14:08:39 +02:00
Piotr Skalski abbde887dc Merge pull request #176 from roboflow/fix/coco_incorrect_class_id_processing
🛠️ fix incorrect COCO class id loading
2023-07-05 11:40:34 +02:00
SkalskiP 4f5f87acb8 1️⃣ changing logic of to_coco to start image_id and annotation_id from 1 2023-07-05 11:37:44 +02:00
hd 3f78150a1d Fixing docstrings and model instructions 2023-07-05 10:11:55 +02:00
hd 183ba1e809 Fixing conversion 2023-07-05 10:09:04 +02:00
hd 597c776c39 Fixing typo in variable 2023-07-05 10:06:23 +02:00
hd 5291706bc1 revert to original method 2023-07-05 09:59:16 +02:00
hd a50e47e7c0 tracked detection method added along with trackstorage 2023-07-05 09:20:50 +02:00
Onuralp SEZER c9d85477f1 ci(pypa-action): introduce pypa github action for uploading to pypi and pypi-test
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-05 01:39:47 +03:00
Onuralp SEZER 19271f362e fix: toml file version fix and removal of setup.py and adjusment on makefile
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-05 00:33:15 +03:00
Onuralp SEZER 9e1e9fc2fc ci(removal): 🧪 removing python 3.7 from test and update other action versions
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-05 00:04:41 +03:00
Onuralp SEZER 629744dee1 feat(pyproject): 🐍 introduce pyproject toml file
Signed-off-by: Onuralp SEZER <thunderbirdtr@gmail.com>
2023-07-05 00:04:04 +03:00
kirilllzaitsev c9d2b52900 minor doc update 2023-07-04 22:07:18 +02:00
kirilllzaitsev 43cca52a58 add _drop_extra_matches from onemetric 2023-07-04 22:04:38 +02:00
kirilllzaitsev e1ee80cb37 add docstring to from_detections 2023-07-04 22:03:10 +02:00
kirilllzaitsev 6a8f3fc03b add detection module 2023-07-04 21:47:56 +02:00
hd d9f6c5c73c tracked detection method added along with trackstorage 2023-07-04 20:42:33 +02:00
SkalskiP c4b33fe1d6 ✅ ready for test and review 2023-07-04 17:31:33 +02:00
Hardik Dava 65a93f8287 Merge pull request #2 from hardikdava/redesigned_annotators
updated code
2023-07-04 16:16:53 +02:00
hd 1bcf8346dc updated code 2023-07-04 16:12:30 +02:00
Hardik Dava aba6ff57a9 Merge pull request #1 from hardikdava/redesigned_annotators
Redesigned annotators
2023-07-04 13:47:15 +02:00
hd b5e1342038 Updating refactored annotators 2023-07-04 13:38:10 +02:00
hd ebfaee22eb updated codebase 2023-07-03 17:30:53 +02:00
hd 9c062aac29 Support for openmmlab detections 2023-07-03 17:28:40 +02:00
SkalskiP 64d223b76d 🖤 make black happy 2023-07-02 23:28:28 +02:00
SkalskiP 21968e53c6 ✏️initial PR for redesigned annotators 2023-07-02 23:25:43 +02:00
hd 9b77b2a1c6 initial commit for advanced annotator 2023-06-30 18:25:13 +02:00
SkalskiP 3e4cb6df42 🪵 updating change log before 0.11.1 release 2023-06-29 15:01:35 +02:00
Piotr Skalski 5fdbb70c60 Merge pull request #165 from roboflow/fix-classification
Fix top_k processing in as_folder_structure
2023-06-29 14:53:43 +02:00
James Gallagher 9aee232d04 fix top_k processing in as_folder_structure 2023-06-29 13:50:47 +01:00
SkalskiP 9adc953780 📚 small documentation update 2023-06-28 23:01:32 +02:00
Piotr Skalski 4ace93f9ab Merge pull request #161 from roboflow/0.11.0-release
`0.11.0` release
2023-06-28 22:50:05 +02:00
SkalskiP c1d3ab2944 🪵 update changelog.md 2023-06-28 22:43:08 +02:00
SkalskiP 2072eeac12 Merge branch 'main' into 0.11.0-release 2023-06-28 22:35:31 +02:00
Piotr Skalski c74efc4908 Merge pull request #162 from hardikdava/video_generator_extended
video generator with start and end position feature
2023-06-28 22:34:14 +02:00
SkalskiP 08f399cf19 🖤 make black happy 2023-06-28 20:07:16 +02:00
SkalskiP 17020b23de try alternative implementation to make it faster 2023-06-28 20:02:03 +02:00
SkalskiP cd929525f6 fixed code break when end was None 2023-06-28 19:56:40 +02:00
SkalskiP bb18de8dce ready for tests 2023-06-28 19:47:23 +02:00
SkalskiP dec96aec4d ready for tests 2023-06-28 19:45:31 +02:00
hd ee45c684d0 video generator with start and end position feature 2023-06-28 18:58:46 +02:00
SkalskiP ea517642f0 🖤 make black happy 2023-06-28 17:12:24 +02:00
SkalskiP cdc08c0455 0.11.0 release 2023-06-28 17:07:02 +02:00
SkalskiP a0c808cfdf 📚 documentation cleanup 2023-06-28 14:52:13 +02:00
Piotr Skalski f146e88ceb Merge pull request #158 from hardikdava/dataset_merge
Dataset merge
2023-06-28 14:19:15 +02:00
SkalskiP 66d29015d6 🖤 make black happy 2023-06-28 14:17:35 +02:00
SkalskiP 46cb82797e ready for merge 2023-06-28 14:16:59 +02:00
SkalskiP 194ff0aaad small fix 2023-06-28 14:07:27 +02:00
SkalskiP a579ed46c6 ready for final tests 2023-06-28 13:36:10 +02:00
dong.li ddabe04409 add unicode label support 2023-06-28 09:56:53 +08:00
Piotr Skalski 8918783702 Update Development Status in setup.py 2023-06-28 01:14:05 +02:00
Piotr Skalski 6adb0f019d 🪪 fix incorrect license in setup.py 2023-06-28 01:04:18 +02:00
Piotr Skalski 84ae12768a Merge pull request #157 from hardikdava/yolo_class_name_fix
Yolo class name fix
2023-06-28 00:54:25 +02:00
SkalskiP 9cbec9fc94 ready for final tests 2023-06-28 00:39:21 +02:00
hd ed12daf77d Updated docstrings 2023-06-27 22:27:16 +02:00
hd 84cfa9609b Initial dataset merging code 2023-06-27 21:58:14 +02:00
hd f2eccae88e Fixed docstring 2023-06-27 17:45:43 +02:00
hd 55c8295755 Fixed data.yaml store and format issue 2023-06-27 17:26:07 +02:00
hd 7849033da3 Fixed yaml file reading code 2023-06-27 16:46:28 +02:00
Piotr Skalski 2e443c80c7 Merge pull request #154 from roboflow/fix/149_typo_in_filename
🛠️ fix #149 - typo in filename
2023-06-27 15:28:40 +02:00
SkalskiP 006caa0b9b 🛠️ fix #149 - typo in filename 2023-06-27 15:25:24 +02:00
Piotr Skalski 34e45bed5e 🥥 add from_coco and as_coco to README.md 2023-06-27 14:06:29 +02:00
Piotr Skalski 0d39de898a Merge pull request #150 from hardikdava/main
🥥 COCO dataset import / export support
2023-06-27 13:58:34 +02:00
SkalskiP 782e7e5a39 ready for final tests 2023-06-27 13:53:27 +02:00
SkalskiP b40265db9e small fix 2023-06-27 03:02:57 +02:00
SkalskiP de1ceff065 small fix 2023-06-27 03:02:24 +02:00
SkalskiP 5bf78e5505 naming refactor 2023-06-27 02:52:20 +02:00
SkalskiP f63336a824 save_coco_annotations logic simplification 2023-06-27 02:44:40 +02:00
SkalskiP 4d82460358 naming refactor 2023-06-27 02:09:56 +02:00
SkalskiP 540efc80ce classes_to_coco_categories, coco_categories_to_classes refactor + tests 2023-06-27 01:50:58 +02:00
hd beea21a97d Resolved issue, code quality checked, ready for review 2023-06-26 20:05:15 +02:00
hd 484e3d4afd COCO segmentation support added 2023-06-26 13:08:33 +02:00
hd ab9728cca2 Updated doc-strings 2023-06-25 13:40:41 +02:00
hd 769b6b967c Tested and working version 2023-06-25 13:31:55 +02:00
hd 253113fa88 refactored code 2023-06-24 18:33:18 +02:00
hd 3b2902c87c COCO dataset import export interface,
working for object detection
2023-06-24 15:48:57 +02:00
Piotr Skalski 9cff624e15 Merge pull request #136 from roboflow/0.10.0-release
`0.10.0` release
2023-06-14 16:50:24 +02:00
SkalskiP 584c4fef82 🪵 changelog.md updates before 0.10.0 release 2023-06-14 16:49:10 +02:00
Piotr Skalski 282aa2c5d3 Merge pull request #110 from roboflow/rf-polygon-loading
Add semantic segmentation polygon loading from Roboflow
2023-06-14 16:40:49 +02:00
SkalskiP 453d4df068 🧪 refactor + some tests 2023-06-14 16:38:08 +02:00
SkalskiP 4d4b38b292 🖤 make black happy 2023-06-14 15:54:40 +02:00
SkalskiP 13e8afa996 👌 final changes before 0.10.0 release 2023-06-14 15:52:20 +02:00
James Gallagher dd4cae328b add missing import from merge, run linters 2023-06-14 14:21:15 +01:00
James ed7103851a Merge branch 'main' into rf-polygon-loading 2023-06-14 14:14:03 +01:00
James Gallagher ca40b001dc fix bug in mask loading 2023-06-14 14:08:53 +01:00
Piotr Skalski de3ccc5c5d Merge pull request #125 from roboflow/add-classification
Add ClassificationDataset API
2023-06-14 14:11:27 +02:00
SkalskiP 1b6d02898c 8️⃣ Classifications.from_yolov8 added 2023-06-14 14:07:14 +02:00
SkalskiP 5ac92bc43b 🧪 ready for final tests 2023-06-14 13:15:28 +02:00
Piotr Skalski 86d9293770 Merge pull request #135 from dankresio/patch-1
Update stride definition
2023-06-14 11:08:11 +02:00
Daniel M. García-Ocaña 0e943e673e Update stride definition 2023-06-14 10:53:06 +02:00
James Gallagher 234e376fca respond to feedback 2023-06-14 09:48:00 +01:00
SkalskiP 54b7cbeb70 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	README.md
2023-06-13 17:47:25 +02:00
SkalskiP d000292eb2 ✨ new demo notebook 2023-06-13 17:45:22 +02:00
SkalskiP b551e6d410 ✨ new demo notebook 2023-06-13 17:34:37 +02:00
James Gallagher 138c271af0 update as_multiclass logic 2023-06-13 14:39:32 +01:00
James Gallagher d22940ce30 add classifications to table of contents 2023-06-13 14:36:31 +01:00
James Gallagher 06da0e70d2 remove unused files 2023-06-13 14:35:52 +01:00
James Gallagher 5d8b378f35 add test cases for get_top_k, add docs for Classifications and ClassificationsDataset 2023-06-13 14:35:32 +01:00
James Gallagher a4830389de fix import 2023-06-13 13:24:11 +01:00
James Gallagher d9a2efc64b remove unused imports, fix top k bug, fix import paths 2023-06-13 13:16:48 +01:00
James Gallagher f91b844b7d fix ClassificationDataset import 2023-06-13 13:12:04 +01:00
James Gallagher dc45d47e7e run linter 2023-06-13 12:48:34 +01:00
James Gallagher b5dedf09cd refactor code, respond to feedback 2023-06-13 12:48:07 +01:00
James Gallagher eb1ae1b790 respond to feedback 2023-06-13 12:00:46 +01:00
James Gallagher f2f8a564e0 run linter 2023-06-13 11:38:19 +01:00
James Gallagher 140a219091 generalise as_multiclass_folder_structure 2023-06-13 11:38:02 +01:00
James Gallagher 86af9b9dce continue work on classifications 2023-06-13 11:13:38 +01:00
Piotr Skalski 4366e4480f 💜Built with supervision added to README.md 2023-06-12 19:06:08 +02:00
James Gallagher b66e9c8c32 change val to valid, remove example code 2023-06-08 11:51:41 +01:00
James Gallagher d0d5626156 run linter 2023-06-07 16:29:52 +01:00
James Gallagher c661bf250d add classification dataset modules 2023-06-07 16:25:22 +01:00
SkalskiP ccb32f8aa1 🪵 Update changelog. 2023-06-07 12:49:53 +02:00
Piotr Skalski 97b9edcbec Merge pull request #122 from roboflow/feature/75_crop_bounding_boxes_as_separate_image
feature/75_crop_bounding_boxes_as_separate_image
2023-06-07 12:20:00 +02:00
SkalskiP 6b52863c9f 📚 Documentation update 2023-06-07 12:15:14 +02:00
SkalskiP 2fcc035413 🖼️ Add utility to crop image based on xyxy box 2023-06-07 12:12:39 +02:00
Piotr Skalski 7c8f16bf0f Merge pull request #121 from roboflow/fix/106_better_handling_of_wrong_data_types_of_mask
🛠️ Fix #106 - now you can pass polygon with int and float vertices.
2023-06-07 11:58:43 +02:00
SkalskiP 1870670c77 🛠️ Fix #106 - now you can pass polygon with int and float vertices. 2023-06-07 11:54:50 +02:00
SkalskiP 802347f4d8 🧹 cleanup before supervision 0.9.0 release 2023-06-07 11:47:34 +02:00
Piotr Skalski 920019c887 Merge pull request #120 from roboflow/feature/new_video_utils
feature/new_video_utils
2023-06-07 00:26:05 +02:00
SkalskiP 477b9a8f38 🛠️ fix ImageSink.__enter__ 2023-06-06 21:58:26 +02:00
SkalskiP 14f62deacf 📚 ImageSink documentation updated 2023-06-06 20:13:33 +02:00
SkalskiP 08eb513594 📚 ImageSink documentation added 2023-06-06 19:58:30 +02:00
SkalskiP a1615fd275 🎬 small VideoSink documentation update. + 🖼️ ImageSink added 2023-06-06 19:54:18 +02:00
SkalskiP ce979216f2 🐍 small setup.py and README.md updates 2023-06-06 18:01:06 +02:00
SkalskiP 95f94319f8 😷 Move YOLOv8 mask processing code to separate extract_yolov8_masks util 2023-06-06 17:50:14 +02:00
SkalskiP 98f60c24d3 😷 Add stride to get_video_frames_generator function. Initial from_yolov8 with masks refactor. 2023-06-06 17:35:40 +02:00
Piotr Skalski 1c043d6cc7 Merge pull request #101 from hardikdava/main
Adding yolov8 masks to detections
2023-06-06 10:52:40 +02:00
Piotr Skalski 86aee5ed98 Merge pull request #118 from roboflow/feature/115_how_to_select_detections_by_index
feature/115_how_to_select_detections_by_index
2023-06-05 11:34:35 +02:00
SkalskiP 0d38b95da7 📚update documentation + 🖤 make black happy 2023-06-05 11:31:52 +02:00
SkalskiP 97892e3138 ⛏️pick Detections by index 2023-06-05 11:01:14 +02:00
Piotr Skalski 17a42bc728 Merge pull request #117 from lomnes-atlast-food/update-readme-loop-dataset
Update README.md example for looping through dataset
2023-06-02 08:25:17 +02:00
Stephen Lomnes be65ba7370 Update README.md
Example in README documentation out of date for v.0.8.0.  __iter__() yields a length 3 tuple.  Example shows a length 2 tuple.

Change proposed: add image_name to example.

Reference: From API docs

```
__iter__() ¶
Iterate over the images and annotations in the dataset.

Yields:

Type	Description
Iterator[Tuple[str, np.ndarray, Detections]]	
Iterator[Tuple[str, np.ndarray, Detections]]: An iterator that yields tuples containing the image name, the image data, and its corresponding annotation.
```
2023-06-01 09:08:33 -04:00
Hardik Dava 72d86ba180 Fixing for uneven image size 2023-05-24 17:27:35 +02:00
Hardik Dava fba83ec371 Fixed minor typo bug 2023-05-24 17:22:16 +02:00
Hardik Dava 4a81a17ddb yolov8 masks plot using with letterbox concept
- gain and pads are calculated using yolov8 results.
- resize masks to input image size
2023-05-24 15:02:00 +02:00
Hardik Dava 30d253d5cc Fixed import error 2023-05-23 18:40:48 +02:00
James Gallagher 544f9cb0e2 incorporate feedback 2023-05-23 13:02:58 +01:00
James Gallagher 84928a2da9 add rf segmentation polygon loading 2023-05-23 12:02:51 +01:00
Hardik Dava 08ae0b2fc7 using basic functions from cv2 2023-05-20 19:22:30 +02:00
Piotr Skalski 0261bec2ce Merge pull request #104 from roboflow/SkalskiP-patch-1
📚 Update README.md
2023-05-18 01:10:05 +02:00
Piotr Skalski 5a4cb1b302 Update README.md 2023-05-18 01:08:49 +02:00
Piotr Skalski 91655f9609 🖌️ final README.md updates 2023-05-18 01:07:36 +02:00
Piotr Skalski 42d4079b23 🎬 video clap section 2023-05-18 01:04:12 +02:00
Piotr Skalski f03a47a5df 🔥 quick start section 2023-05-18 00:59:01 +02:00
Piotr Skalski c01efeaaca 📚 Update README.md 2023-05-17 23:08:28 +02:00
SkalskiP 5489d405c6 📚 DetectionDataset documentation update 2023-05-17 21:46:45 +02:00
SkalskiP 5f97eac9a9 📚 DetectionDataset documentation update 2023-05-17 21:35:33 +02:00
SkalskiP 30f105f267 🛡️ add badges to README.md 2023-05-17 20:47:42 +02:00
SkalskiP a34e64866e 📚 add DetectionDataset.split documentation 2023-05-17 20:39:36 +02:00
Piotr Skalski c02f69428e Merge pull request #102 from roboflow/feature/dataset_split
feature/dataset_split
2023-05-17 20:22:30 +02:00
SkalskiP 55f4f00c0c 🧪 tests for train_test_split 2023-05-17 20:20:28 +02:00
SkalskiP 91100e8393 🚀 initial commit 2023-05-17 19:53:21 +02:00
Hardik Dava b474a47065 Adding yolov8 masks to detections 2023-05-17 18:38:40 +02:00
Piotr Skalski 83d357d973 Merge pull request #100 from roboflow/feature/support_for_yolo_dataset_export
feature/support for yolo dataset export
2023-05-17 18:14:55 +02:00
SkalskiP b536c6c889 📚 small documentation updates 2023-05-17 16:27:31 +02:00
SkalskiP df579c866c 🌱 change default value of approximation_percentage parameter from 0.75 to 0.0 in DetectionDataset.as_yolo and DetectionDataset.as_pascal_voc. 2023-05-17 16:13:32 +02:00
SkalskiP f224bceb6f 🛠️ Bug fixing 2023-05-17 16:05:02 +02:00
SkalskiP bd3503e7d5 🪵 0.8.0 release changelog 2023-05-17 14:53:51 +02:00
SkalskiP f63f5acb2f ⭐ PR ready for review 2023-05-17 02:38:14 +02:00
SkalskiP 2f5b1b0f0d 🛠️ small fix 2023-05-17 02:21:14 +02:00
SkalskiP 3af32800d3 😷 initial support for masks 2023-05-17 02:13:08 +02:00
SkalskiP 8222bee9dd 🛠️ Small fix 2023-05-17 01:52:18 +02:00
SkalskiP 06ea737336 📦 YOLO box save ready for tests 2023-05-17 01:35:11 +02:00
SkalskiP f616bda40f 🐚 shell of YOLO format processing added 2023-05-17 00:58:17 +02:00
SkalskiP 0e835f1b0a 🐚 shell of YOLO format processing added 2023-05-17 00:40:33 +02:00
SkalskiP 4c024214b2 🐚 shell of YOLO format processing added 2023-05-16 23:51:08 +02:00
SkalskiP 6a15093277 👨‍👩‍👦 basic support for Dataset inheritance + adding pyyaml to dependencies 2023-05-16 23:25:58 +02:00
SkalskiP abcf453119 🎉 initial commit 2023-05-16 16:35:59 +02:00
SkalskiP ba2926688f 🪵 0.7.0 release changelog 2023-05-11 00:39:07 +02:00
SkalskiP 3da433f90f 🪵 0.7.0 release changelog 2023-05-11 00:31:46 +02:00
Piotr Skalski a911cbd554 Merge pull request #44 from iPoe/feature/custom_msg_for_line_zone
Add custom text for the in & out tags
2023-05-10 18:44:52 +02:00
SkalskiP d10618a8b3 🛠️ add __len__ and __iter__ to Dataset 2023-05-10 14:02:08 +02:00
Piotr Skalski eaf5b7e95f Merge pull request #86 from roboflow/feature/support_yolo_dataset_loading
support for yolo dataset loading and saving
2023-05-09 23:02:26 +02:00
Piotr Skalski 4ae8ac1db0 Merge pull request #91 from roboflow/feature/add_yolo_nas_support
💜 Initial YOLO-NAS support
2023-05-09 19:31:56 +02:00
SkalskiP 74efca7c22 💜 Initial YOLO-NAS support 2023-05-09 17:19:40 +02:00
SkalskiP 3063dfb1f5 📚 Improve BoxAnnotator.annotate documentation and add example 2023-05-09 12:14:37 +02:00
SkalskiP 6c81d21a03 Unit tests for yolo loader added 2023-05-08 21:34:36 +02:00
SkalskiP f4b91d0175 Fix load_yolo_annotations 2023-05-08 19:46:13 +02:00
SkalskiP 45fdb1e805 Fix load_yolo_annotations 2023-05-08 19:33:44 +02:00
SkalskiP 8c0a294ca1 First fully working version of from_yolo. Ready for test. 2023-05-08 17:13:59 +02:00
SkalskiP cf7f003133 Merge remote-tracking branch 'origin/feature/support_yolo_dataset_loading' into feature/support_yolo_dataset_loading
# Conflicts:
#	supervision/dataset/formats/yolo.py
2023-05-08 14:08:00 +02:00
SkalskiP 1bebbb14a6 yolo_annotations_to_detections function full implementation 2023-05-08 14:07:43 +02:00
SkalskiP 3d0aec03e0 yolo_annotations_to_detections function full implementation 2023-05-08 13:51:49 +02:00
SkalskiP 306d7bd27e Merge remote-tracking branch 'origin/feature/support_yolo_dataset_loading' into feature/support_yolo_dataset_loading
# Conflicts:
#	supervision/dataset/formats/yolo.py
2023-05-08 10:57:28 +02:00
SkalskiP 43fc5d6205 yolo annotations box_to_polygon initial implementation 2023-05-08 10:55:58 +02:00
SkalskiP d13238bc42 yolo annotations box_to_polygon initial implementation 2023-05-07 16:38:53 +02:00
SkalskiP 77243f1752 initial yolo annotations file parsing 2023-05-07 13:16:42 +02:00
SkalskiP e16cca4349 🚀 - add quick start section to Supervision docs page with first topic - advanced detections filtering 2023-05-04 17:48:39 +02:00
Piotr Skalski e592a44bea Merge pull request #84 from roboflow/feature/20230504_add_merge_detections_method
➕ Add `Detections.merge` method
2023-05-04 15:55:46 +02:00
SkalskiP c2833bc8ca 🤍 / 🖤 - light mode / datk mode switch added 2023-05-04 10:13:11 +02:00
SkalskiP feb08d10b2 ➕ Add Detections.merge method 2023-05-04 09:30:48 +02:00
Piotr Skalski 74bd403dc0 Merge pull request #83 from roboflow/feature/20230504_major_documentation_improvements
major documentation improvements
2023-05-04 00:37:42 +02:00
SkalskiP 6a013382d3 - 📚 Detections documentation improvements
- `list_files_with_extensions` documentation improvements
- internal `deprecated` function added
- `plot_image` and `plot_images_grid` documentation improvements
2023-05-04 00:33:17 +02:00
Piotr Skalski 92290979c7 Merge pull request #81 from hardikdava/patch-2
minor bug fixed
2023-05-03 16:06:00 +02:00
Hardik Dava 696b6f9276 minor bug fixed
annotation function should return frame but it was not. So added return value
2023-05-01 12:24:30 +02:00
iPoe ac61598ad4 Add custom text for the in & out tags. 2023-03-29 14:01:35 -05:00
106 changed files with 14854 additions and 872 deletions
+1
View File
@@ -0,0 +1 @@
*.ipynb linguist-vendored
+1 -1
View File
@@ -62,4 +62,4 @@ body:
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.
options:
- label: Yes I'd like to help by submitting a PR!
- label: Yes I'd like to help by submitting a PR!
+1 -1
View File
@@ -46,4 +46,4 @@ body:
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.
options:
- label: Yes I'd like to help by submitting a PR!
- label: Yes I'd like to help by submitting a PR!
+1 -1
View File
@@ -30,4 +30,4 @@ body:
- type: textarea
attributes:
label: Additional
description: Anything else you would like to share?
description: Anything else you would like to share?
+4 -3
View File
@@ -5,14 +5,15 @@ on:
branches:
- master
- main
- develop
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.x
- run: pip install mkdocs-material
- run: pip install "mkdocstrings[python]"
- run: mkdocs gh-deploy --force
- run: mkdocs gh-deploy --force
+41
View File
@@ -0,0 +1,41 @@
name: Supervision Test Releases to PyPi
on:
push:
tags:
- '[0-9]+.[0-9]+[0-9]+.[0-9]+a[0-9]'
- '[0-9]+.[0-9]+[0-9]+.[0-9]+b[0-9]'
- '[0-9]+.[0-9]+[0-9]+.[0-9]+rc[0-9]'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
build-n-publish:
name: Build and publish to PyPI
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v3
- name: 🐍 Set up Python 3.8 environment for build
uses: actions/setup-python@v4
with:
python-version: "3.8"
- name: 🏗️ Build source and wheel distributions
run: |
python -m pip install --upgrade build twine
python -m build
twine check --strict dist/*
- name: 🚀 Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: ${{ secrets.PYPI_USERNAME }}
password: ${{ secrets.PYPI_PASSWORD }}
- name: 🚀 Publish to Test-PyPi
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
user: ${{ secrets.PYPI_TEST_USERNAME }}
password: ${{ secrets.PYPI_TEST_PASSWORD }}
+25 -14
View File
@@ -1,8 +1,13 @@
name: Publish WorkFlow
name: Supervision Releases to PyPi
on:
release:
types: [created]
push:
tags:
- '[0-9]+.[0-9]+[0-9]+.[0-9]'
- '[0-9]+.[0-9]+[0-9]+.[0-9]'
- '[0-9]+.[0-9]+[0-9]+.[0-9]'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
build:
@@ -16,17 +21,23 @@ jobs:
with:
ref: ${{ github.head_ref }}
- name: 🐍 Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: 🦾 Install dependencies
- name: 🏗️ Build source and wheel distributions
run: |
python -m pip install --upgrade pip
pip install ".[dev]"
python -m pip install --upgrade build twine
python -m build
twine check --strict dist/*
- name: 🚀 Publish to PyPi
env:
PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }}
PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
PYPI_TEST_PASSWORD: ${{ secrets.PYPI_TEST_PASSWORD }}
run: |
make publish -e PYPI_USERNAME=$PYPI_USERNAME -e PYPI_PASSWORD=$PYPI_PASSWORD -e PYPI_TEST_PASSWORD=$PYPI_TEST_PASSWORD
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: ${{ secrets.PYPI_USERNAME }}
password: ${{ secrets.PYPI_PASSWORD }}
- name: 🚀 Publish to Test-PyPi
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
user: ${{ secrets.PYPI_TEST_USERNAME }}
password: ${{ secrets.PYPI_TEST_PASSWORD }}
+4 -12
View File
@@ -2,21 +2,19 @@ name: Test WorkFlow
on:
pull_request:
branches: [main]
branches: [main, develop]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8, 3.9, '3.10']
python-version: ["3.8", "3.9", "3.10","3.11"]
steps:
- name: 🛎️ Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.head_ref }}
- name: 🐍 Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: 🦾 Install dependencies
@@ -24,11 +22,5 @@ jobs:
python -m pip install --upgrade pip
pip install -e .
pip install pytest
pip install isort
pip install flake8
pip install "black==22.3.0"
- name: 🧹 Lint with flake8
run: |
make check_code_quality
- name: 🧪 Test
run: "python -m pytest ./test"
run: "python -m pytest ./test"
+1 -1
View File
@@ -15,4 +15,4 @@ jobs:
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: "Hello there, thank you for opening an Issue ! 🙏🏻 The team was notified and they will get back to you asap."
pr-message: "Hello there, thank you for opening an PR ! 🙏🏻 The team was notified and they will get back to you asap."
pr-message: "Hello there, thank you for opening an PR ! 🙏🏻 The team was notified and they will get back to you asap."
+1
View File
@@ -54,6 +54,7 @@ coverage.xml
*.py,cover
.hypothesis/
.pytest_cache/
.ruff_cache/
# Translations
*.mo
+80
View File
@@ -0,0 +1,80 @@
ci:
autofix_prs: true
autoupdate_schedule: weekly
autofix_commit_msg: "fix(pre_commit): 🎨 auto format pre-commit hooks"
autoupdate_commit_msg: "chore(pre_commit): ⬆ pre_commit autoupdate"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-yaml
- id: check-docstring-first
- id: check-executables-have-shebangs
- id: check-toml
- id: check-case-conflict
- id: check-added-large-files
args: ['--maxkb=2048']
exclude: ^logo/
- id: detect-private-key
- id: forbid-new-submodules
- id: pretty-format-json
args: ['--autofix', '--no-sort-keys', '--indent=4']
- id: end-of-file-fixer
- id: mixed-line-ending
# - repo: https://github.com/asottile/pyupgrade
# rev: v3.9.0
# hooks:
# - id: pyupgrade
# name: Upgrade code
# args: [--py38-plus]
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
name: Sort imports
- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
hooks:
- id: flake8
name: Flake8 Checks
entry: flake8
additional_dependencies: [Flake8-pyproject]
- repo: https://github.com/PyCQA/bandit
rev: '1.7.5'
hooks:
- id: bandit
args: ["-c", "pyproject.toml"]
additional_dependencies: ["bandit[toml]"]
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
name: isort (python)
- id: isort
name: isort (cython)
types: [cython]
- id: isort
name: isort (pyi)
types: [pyi]
- repo: https://github.com/psf/black
rev: 23.9.1
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.292
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
+19 -8
View File
@@ -1,12 +1,23 @@
# This CITATION.cff file was generated with cffinit.
# Visit https://bit.ly/cffinit to generate yours today!
cff-version: 1.2.0
author: Roboflow
message: If you use this software, please cite it as below.
title: supervision
version: 0.1.0
date-released: 2023-01-19
license: MIT
repository-code: https://github.com/roboflow/supervision
title: Supervision
message: >-
If you use this software, please cite it using the
metadata from this file.
type: software
authors:
- given-names: Roboflow
email: support@roboflow.com
repository-code: 'https://github.com/roboflow/supervision'
url: 'https://roboflow.github.io/supervision/'
abstract: >-
supervision features a range of utilities for use in
computer vision projects, from detections processing and
filtering to confusion matrix calcuation.
keywords:
- computer vision
- image processing
- video processing
- video processing
license: MIT
+134
View File
@@ -0,0 +1,134 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* 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
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
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
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
community-reports@roboflow.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
+80 -9
View File
@@ -14,7 +14,7 @@ We welcome contributions to:
4. Submit a request for a new feature.
5. Improve our test coverage.
### Contributing Features
### Contributing Features ✨
Supervision is designed to provide generic utilities to solve problems. Thus, we focus on contributions that can have an impact on a wide range of projects.
@@ -24,7 +24,72 @@ Before you contribute a new feature, consider submitting an Issue to discuss the
## How to Contribute Changes
First, fork this repository to your own GitHub account. Create a new branch that describes your changes (i.e. `line-counter-docs`). Push your changes to the branch on your fork and then submit a pull request to this repository.
First, fork this repository to your own GitHub account. Click "fork" in the top corner of the `supervision` repository to get started:
![Forking the repository](https://media.roboflow.com/fork.png)
![Creating a repository fork](https://media.roboflow.com/create_fork.png)
Then, run `git clone` to download the project code to your computer.
Move to a new branch using the `git checkout` command:
```bash
git checkout -b <your_branch_name>
```
The name you choose for your branch should describe the change you want to make (i.e. `line-counter-docs`).
Make any changes you want to the project code, then run the following commands to commit your changes:
```bash
git add .
git commit -m "Your commit message"
git push -u origin main
```
## 🎨 Code quality
### Pre-commit tool
This project utilizes the [pre-commit](https://pre-commit.com/) tool to maintain code quality and consistency. Before submitting a pull request or making any commits, it is important to run the pre-commit tool to ensure that your changes meet the project's guidelines.
Furthermore, we have integrated a pre-commit GitHub Action into our workflow. This means that with every pull request opened, the pre-commit checks will be automatically enforced, streamlining the code review process and ensuring that all contributions adhere to our quality standards.
To run the pre-commit tool, follow these steps:
1. Install pre-commit by running the following command: `poetry install`. It will not only install pre-commit but also install all the deps and dev-deps of project
2. Once pre-commit is installed, navigate to the project's root directory.
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.
4. You can also install pre-commit as a git hook by execute `pre-commit install`. Every time you made `git commit` pre-commit run automatically for you.
### Docstrings
All new functions and classes in `supervision` should include docstrings. This is a prerequisite for any new functions and classes to be added to the library.
`supervision` adheres to the [Google Python docstring style](https://google.github.io/styleguide/pyguide.html#383-functions-and-methods). Please refer to the style guide while writing docstrings for your contribution.
### Type checking
So far, **there is no type checking with mypy**. See [issue](https://github.com/roboflow-ai/template-python/issues/4).
Then, go back to your fork of the `supervision` repository, click "Pull Requests", and click "New Pull Request".
![Opening a pull request](https://media.roboflow.com/open_pr.png)
Make sure the `base` branch is `develop` before submitting your PR.
On the next page, review your changes then click "Create pull request":
![Configuring a pull request](https://media.roboflow.com/create_pr_submit.png)
Next, write a description for your pull request, and click "Create pull request" again to submit it for review:
![Submitting a pull request](https://media.roboflow.com/write_pr.png)
When creating new functions, please ensure you have the following:
@@ -32,20 +97,26 @@ 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.
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.
All pull requests will be reviewed by the maintainers of the project. We will provide feedback and ask for changes if necessary.
PRs must pass all tests and linting requirements before they can be merged.
## 🧹 code quality
## 📝 documentation
We provide two handy commands inside the `Makefile`, namely:
The `supervision` documentation is stored in a folder called `docs`. The project documentation is built using `mkdocs`.
- `make style` to format the code
- `make check_code_quality` to check code quality (PEP8 basically)
To run the documentation, install the project requirements with `poetry install dev`. Then, run `mkdocs serve` to start the documentation server.
So far, **there is no types checking with mypy**. See [issue](https://github.com/roboflow-ai/template-python/issues/4).
You can learn more about mkdocs on the [mkdocs website](https://www.mkdocs.org/).
## 🧪 tests
## 🧪 tests
[`pytests`](https://docs.pytest.org/en/7.1.x/) is used to run our tests.
[`pytests`](https://docs.pytest.org/en/7.1.x/) is used to run our tests.
## 📄 license
By contributing, you agree that your contributions will be licensed under an [MIT license](https://github.com/roboflow/supervision/blob/develop/LICENSE.md).
+5 -5
View File
@@ -12,11 +12,11 @@ check_code_quality:
isort --check-only --profile black $(check_dirs)
# stop the build if there are Python syntax errors or undefined names
flake8 $(check_dirs) --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. E203 for black, E501 for docstring, W503 for line breaks before logical operators
# exit-zero treats all errors as warnings. E203 for black, E501 for docstring, W503 for line breaks before logical operators
flake8 $(check_dirs) --count --max-line-length=88 --exit-zero --ignore=D --extend-ignore=E203,E501,W503 --statistics
publish:
python setup.py sdist bdist_wheel
twine upload -r testpypi dist/* -u ${PYPI_USERNAME} -p ${PYPI_TEST_PASSWORD} --verbose
poetry build
twine upload -r testpypi dist/* -u ${PYPI_USERNAME} -p ${PYPI_TEST_PASSWORD} --verbose
twine check dist/*
twine upload dist/* -u ${PYPI_USERNAME} -p ${PYPI_PASSWORD} --verbose
twine upload dist/* -u ${PYPI_USERNAME} -p ${PYPI_PASSWORD} --verbose
+299 -69
View File
@@ -10,6 +10,305 @@
<br>
[notebooks](https://github.com/roboflow/notebooks) | [inference](https://github.com/roboflow/inference) | [autodistill](https://github.com/autodistill/autodistill) | [collect](https://github.com/roboflow/roboflow-collect)
<br>
[![version](https://badge.fury.io/py/supervision.svg)](https://badge.fury.io/py/supervision)
[![downloads](https://img.shields.io/pypi/dm/supervision)](https://pypistats.org/packages/supervision)
[![license](https://img.shields.io/pypi/l/supervision)](https://github.com/roboflow/supervision/blob/main/LICENSE.md)
[![python-version](https://img.shields.io/pypi/pyversions/supervision)](https://badge.fury.io/py/supervision)
[![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/roboflow/supervision/blob/main/demo.ipynb)
</div>
## 👋 hello
**We write your reusable computer vision tools.** Whether you need to load your dataset from your hard drive, draw detections on an image or video, or count how many detections are in a zone. You can count on us! 🤝
## 💻 install
Pip install the supervision package in a
[**3.11>=Python>=3.8**](https://www.python.org/) environment.
```bash
pip install supervision[desktop]
```
Read more about desktop, headless, and local installation in our [guide](https://roboflow.github.io/supervision/).
## 🔥 quickstart
### [detections processing](https://roboflow.github.io/supervision/detection/core/)
```python
>>> import supervision as sv
>>> from ultralytics import YOLO
>>> model = YOLO('yolov8s.pt')
>>> result = model(IMAGE)[0]
>>> detections = sv.Detections.from_ultralytics(result)
>>> len(detections)
5
```
<details close>
<summary>👉 more detections utils</summary>
- Easily switch inference pipeline between supported object detection/instance segmentation models
```python
>>> import supervision as sv
>>> from segment_anything import sam_model_registry, SamAutomaticMaskGenerator
>>> sam = sam_model_registry[MODEL_TYPE](checkpoint=CHECKPOINT_PATH).to(device=DEVICE)
>>> mask_generator = SamAutomaticMaskGenerator(sam)
>>> sam_result = mask_generator.generate(IMAGE)
>>> detections = sv.Detections.from_sam(sam_result=sam_result)
```
- [Advanced filtering](https://roboflow.github.io/supervision/quickstart/detections/)
```python
>>> detections = detections[detections.class_id == 0]
>>> detections = detections[detections.confidence > 0.5]
>>> detections = detections[detections.area > 1000]
```
- Image annotation
```python
>>> import supervision as sv
>>> box_annotator = sv.BoxAnnotator()
>>> annotated_frame = box_annotator.annotate(
... scene=IMAGE,
... detections=detections
... )
```
</details>
### [datasets processing](https://roboflow.github.io/supervision/dataset/core/)
```python
>>> import supervision as sv
>>> dataset = sv.DetectionDataset.from_yolo(
... images_directory_path='...',
... annotations_directory_path='...',
... data_yaml_path='...'
... )
>>> dataset.classes
['dog', 'person']
>>> len(dataset)
1000
```
<details close>
<summary>👉 more dataset utils</summary>
- Load object detection/instance segmentation datasets in one of the supported formats
```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_coco(
... images_directory_path='...',
... annotations_path='...'
... )
```
- Loop over dataset entries
```python
>>> for name, image, labels in dataset:
... print(labels.xyxy)
array([[404. , 719. , 538. , 884.5 ],
[155. , 497. , 404. , 833.5 ],
[ 20.154999, 347.825 , 416.125 , 915.895 ]], dtype=float32)
```
- Split dataset for training, testing, and validation
```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)
```
- Merge multiple datasets
```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_merged = sv.DetectionDataset.merge([ds_1, ds_2])
>>> len(ds_merged)
300
>>> ds_merged.classes
['cat', 'dog', 'person']
```
- Save object detection/instance segmentation datasets in one of the supported formats
```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_coco(
... images_directory_path='...',
... annotations_path='...'
... )
```
- Convert labels between supported formats
```python
>>> sv.DetectionDataset.from_yolo(
... images_directory_path='...',
... annotations_directory_path='...',
... data_yaml_path='...'
... ).as_pascal_voc(
... images_directory_path='...',
... annotations_directory_path='...'
... )
```
- Load classification datasets in one of the supported formats
```python
>>> cs = sv.ClassificationDataset.from_folder_structure(
... root_directory_path='...'
... )
```
- Save classification datasets in one of the supported formats
```python
>>> cs.as_folder_structure(
... root_directory_path='...'
... )
```
</details>
### [model evaluation](https://roboflow.github.io/supervision/metrics/detection/)
```python
>>> import supervision as sv
>>> dataset = sv.DetectionDataset.from_yolo(...)
>>> def callback(image: np.ndarray) -> sv.Detections:
... ...
>>> confusion_matrix = sv.ConfusionMatrix.benchmark(
... dataset = dataset,
... callback = callback
... )
>>> confusion_matrix.matrix
array([
[0., 0., 0., 0.],
[0., 1., 0., 1.],
[0., 1., 1., 0.],
[1., 1., 0., 0.]
])
```
<details close>
<summary>👉 more metrics</summary>
- Mean average precision (mAP) for object detection tasks.
```python
>>> import supervision as sv
>>> dataset = sv.DetectionDataset.from_yolo(...)
>>> def callback(image: np.ndarray) -> sv.Detections:
... ...
>>> mean_average_precision = sv.MeanAveragePrecision.benchmark(
... dataset = dataset,
... callback = callback
... )
>>> mean_average_precision.map50_95
0.433
```
</details>
## 🎬 tutorials
<p align="left">
<a href="https://youtu.be/4Q3ut7vqD5o" title="Traffic Analysis with YOLOv8 and ByteTrack - Vehicle Detection and Tracking"><img src="https://github.com/roboflow/supervision/assets/26109316/54afdf1c-218c-4451-8f12-627fb85f1682" alt="Traffic Analysis with YOLOv8 and ByteTrack - Vehicle Detection and Tracking" width="300px" align="left" /></a>
<a href="https://youtu.be/4Q3ut7vqD5o" title="Traffic Analysis with YOLOv8 and ByteTrack - Vehicle Detection and Tracking"><strong>Traffic Analysis with YOLOv8 and ByteTrack - Vehicle Detection and Tracking</strong></a>
<div><strong>Created: 6 Sep 2023</strong> | <strong>Updated: 6 Sep 2023</strong></div>
<br/> In this video, we explore real-time traffic analysis using YOLOv8 and ByteTrack to detect and track vehicles on aerial images. Harnessing the power of Python and Supervision, we delve deep into assigning cars to specific entry zones and understanding their direction of movement. By visualizing their paths, we gain insights into traffic flow across bustling roundabouts... </p>
<br/>
<p align="left">
<a href="https://youtu.be/D-D6ZmadzPE" title="SAM - Segment Anything Model by Meta AI: Complete Guide"><img src="https://github.com/SkalskiP/SkalskiP/assets/26109316/6913ff11-53c6-4341-8d90-eaff3023c3fd" alt="SAM - Segment Anything Model by Meta AI: Complete Guide" width="300px" align="left" /></a>
<a href="https://youtu.be/D-D6ZmadzPE" title="SAM - Segment Anything Model by Meta AI: Complete Guide"><strong>SAM - Segment Anything Model by Meta AI: Complete Guide</strong></a>
<div><strong>Created: 11 Apr 2023</strong> | <strong>Updated: 11 Apr 2023</strong></div>
<br/> Discover the incredible potential of Meta AI's Segment Anything Model (SAM)! We dive into SAM, an efficient and promptable model for image segmentation, which has revolutionized computer vision tasks. With over 1 billion masks on 11M licensed and privacy-respecting images, SAM's zero-shot performance is often competitive with or even superior to prior fully supervised results... </p>
## 💜 built with supervision
Did you build something cool using supervision? [Let us know!](https://github.com/roboflow/supervision/discussions/categories/built-with-supervision)
https://user-images.githubusercontent.com/26109316/207858600-ee862b22-0353-440b-ad85-caa0c4777904.mp4
https://github.com/roboflow/supervision/assets/26109316/c9436828-9fbf-4c25-ae8c-60e9c81b3900
## 📚 documentation
Visit our [documentation](https://roboflow.github.io/supervision) page to learn how supervision can help you build computer vision applications faster and more reliably.
## 🏆 contribution
We love your input! Please see our [contributing guide](https://github.com/roboflow/supervision/blob/main/CONTRIBUTING.md) to get started. Thank you 🙏 to all our contributors!
<br>
<div align="center">
<div align="center">
<a href="https://youtube.com/roboflow">
<img
@@ -54,73 +353,4 @@
</a>
</div>
<br>
[![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/roboflow-ai/notebooks/blob/main/notebooks/how-to-detect-and-count-objects-in-polygon-zone.ipynb)
</div>
## 👋 hello
A set of easy-to-use utils that will come in handy in any computer vision project. **Supervision is still in
pre-release stage. 🚧 Keep your eyes open for potential bugs and be aware that at this stage our API is still fluid
and may change.**
## 💻 install
Pip install the supervision package in a
[**3.10>=Python>=3.7**](https://www.python.org/) environment.
```bash
pip install supervision
```
<details close>
<summary>Install from source</summary>
```bash
# clone repository and navigate to root directory
git clone https://github.com/roboflow/supervision.git
cd supervision
# setup python environment and activate it
python3 -m venv venv
source venv/bin/activate
# install
pip install -e ".[dev]"
```
</details>
## 📖 documentation
Curious how Supervision can help you solve problems on your project? Visit our [documentation](https://roboflow.github.io/supervision) page!
## 🎬 tutorials
🔥 [Subscribe](https://www.youtube.com/@Roboflow) and stay up to date with the latest changes to the Supervision library.
<p align="center">
<a href="https://youtu.be/l_kf9CfZ_8M">
<img
width="90%"
src="https://user-images.githubusercontent.com/26109316/217950212-311de186-1862-4b4c-a86e-89cafd68b233.jpg"
alt="count-objects-in-zone"
>
</a>
</p>
<p align="center">
<a href="https://youtu.be/OS5qI9YBkfk">
<img
width="90%"
src="https://user-images.githubusercontent.com/26109316/213702005-ddd568f0-b902-46c2-9af9-e6ff33db23bf.jpg"
alt="count-objects-crossing-line"
>
</a>
</p>
## 🏆 contribution
We love your input! Please see our [contributing guide](https://github.com/roboflow/supervision/blob/main/CONTRIBUTING.md) to get started. Thank you 🙏 to all our contributors!
Vendored
+1325
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
supervision.roboflow.com
+199
View File
@@ -0,0 +1,199 @@
=== "BoundingBox"
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> bounding_box_annotator = sv.BoundingBoxAnnotator()
>>> annotated_frame = bounding_box_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
<div class="result" markdown>
![bounding-box-annotator-example](https://media.roboflow.com/supervision-annotator-examples/bounding-box-annotator-example.png){ align=center width="800" }
</div>
=== "Mask"
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> mask_annotator = sv.MaskAnnotator()
>>> annotated_frame = mask_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
<div class="result" markdown>
![mask-annotator-example](https://media.roboflow.com/supervision-annotator-examples/mask-annotator-example.png){ align=center width="800" }
</div>
=== "Ellipse"
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> ellipse_annotator = sv.EllipseAnnotator()
>>> annotated_frame = ellipse_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
<div class="result" markdown>
![ellipse-annotator-example](https://media.roboflow.com/supervision-annotator-examples/ellipse-annotator-example.png){ align=center width="800" }
</div>
=== "BoxCorner"
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> corner_annotator = sv.BoxCornerAnnotator()
>>> annotated_frame = corner_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
<div class="result" markdown>
![box-corner-annotator-example](https://media.roboflow.com/supervision-annotator-examples/box-corner-annotator-example.png){ align=center width="800" }
</div>
=== "Circle"
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> circle_annotator = sv.CircleAnnotator()
>>> annotated_frame = circle_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
<div class="result" markdown>
![circle-annotator-example](https://media.roboflow.com/supervision-annotator-examples/circle-annotator-example.png){ align=center width="800" }
</div>
=== "Label"
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> label_annotator = sv.LabelAnnotator(text_position=sv.Position.CENTER)
>>> annotated_frame = label_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
<div class="result" markdown>
![label-annotator-example](https://media.roboflow.com/supervision-annotator-examples/label-annotator-example-2.png){ align=center width="800" }
</div>
=== "Blur"
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> blur_annotator = sv.BlurAnnotator()
>>> annotated_frame = blur_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
<div class="result" markdown>
![blur-annotator-example](https://media.roboflow.com/supervision-annotator-examples/blur-annotator-example-2.png){ align=center width="800" }
</div>
=== "Trace"
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> trace_annotator = sv.TraceAnnotator()
>>> annotated_frame = trace_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
<div class="result" markdown>
![trace-annotator-example](https://media.roboflow.com/supervision-annotator-examples/trace-annotator-example.png){ align=center width="800" }
</div>
## BoundingBoxAnnotator
:::supervision.annotators.core.BoundingBoxAnnotator
## MaskAnnotator
:::supervision.annotators.core.MaskAnnotator
## EllipseAnnotator
:::supervision.annotators.core.EllipseAnnotator
## BoxCornerAnnotator
:::supervision.annotators.core.BoxCornerAnnotator
## CircleAnnotator
:::supervision.annotators.core.CircleAnnotator
## LabelAnnotator
:::supervision.annotators.core.LabelAnnotator
## TraceAnnotator
:::supervision.annotators.core.TraceAnnotator
## BlurAnnotator
:::supervision.annotators.core.BlurAnnotator
+335 -27
View File
@@ -1,14 +1,322 @@
### 0.15.0 <small>October 5, 2023</small>
- Added [#170](https://github.com/roboflow/supervision/pull/170): [`sv.BoundingBoxAnnotator`](https://supervision.roboflow.com/annotators/#supervision.annotators.core.BoundingBoxAnnotator) allowing to annotate images and videos with bounding boxes.
- Added [#170](https://github.com/roboflow/supervision/pull/170): [`sv.BoxCornerAnnotator `](https://supervision.roboflow.com/annotators/#supervision.annotators.core.BoxCornerAnnotator) allowing to annotate images and videos with just bounding box corners.
- Added [#170](https://github.com/roboflow/supervision/pull/170): [`sv.MaskAnnotator`](https://supervision.roboflow.com/annotators/#supervision.annotators.core.MaskAnnotator) allowing to annotate images and videos with segmentation masks.
- Added [#170](https://github.com/roboflow/supervision/pull/170): [`sv.EllipseAnnotator`](https://supervision.roboflow.com/annotators/#supervision.annotators.core.EllipseAnnotator) allowing to annotate images and videos with ellipses (sports game style).
- Added [#386](https://github.com/roboflow/supervision/pull/386): [`sv.CircleAnnotator`](https://supervision.roboflow.com/annotators/#supervision.annotators.core.CircleAnnotator) allowing to annotate images and videos with circles.
- Added [#354](https://github.com/roboflow/supervision/pull/354): [`sv.TraceAnnotator`](https://supervision.roboflow.com/annotators/#supervision.annotators.core.TraceAnnotator) allowing to draw path of moving objects on videos.
- Added [#405](https://github.com/roboflow/supervision/pull/405): [`sv.BlurAnnotator`](https://supervision.roboflow.com/annotators/#supervision.annotators.core.BlurAnnotator) allowing to blur objects on images and videos.
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> bounding_box_annotator = sv.BoundingBoxAnnotator()
>>> annotated_frame = bounding_box_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
- Added [#354](https://github.com/roboflow/supervision/pull/354): Supervision usage [example](https://github.com/roboflow/supervision/tree/develop/examples/traffic_analysis). You can now learn how to perform traffic flow analysis with Supervision.
- Changed [#399](https://github.com/roboflow/supervision/pull/399): [`sv.Detections.from_roboflow`](https://supervision.roboflow.com/detection/core/#supervision.detection.core.Detections.from_roboflow) now does not require `class_list` to be specified. The `class_id` value can be extracted directly from the [inference](https://github.com/roboflow/inference) response.
- Changed [#381](https://github.com/roboflow/supervision/pull/381): [`sv.VideoSink`](https://supervision.roboflow.com/utils/video/#videosink) now allows to customize the output codec.
- Changed [#361](https://github.com/roboflow/supervision/pull/361): [`sv.InferenceSlicer`](https://supervision.roboflow.com/detection/tools/inference_slicer/#supervision.detection.tools.inference_slicer.InferenceSlicer) can now operate in multithreading mode.
- Fixed [#348](https://github.com/roboflow/supervision/pull/348): [`sv.Detections.from_deepsparse`](https://supervision.roboflow.com/detection/core/#supervision.detection.core.Detections.from_deepsparse) to allow processing empty [deepsparse](https://github.com/neuralmagic/deepsparse) result object.
### 0.14.0 <small>August 31, 2023</small>
- Added [#282](https://github.com/roboflow/supervision/pull/282): support for SAHI inference technique with [`sv.InferenceSlicer`](https://supervision.roboflow.com/detection/tools/inference_slicer).
```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)
>>> detections = slicer(image)
```
- Added [#297](https://github.com/roboflow/supervision/pull/297): [`Detections.from_deepsparse`](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.from_deepsparse) to enable seamless integration with [DeepSparse](https://github.com/neuralmagic/deepsparse) framework.
- Added [#281](https://github.com/roboflow/supervision/pull/281): [`sv.Classifications.from_ultralytics`](https://supervision.roboflow.com/classification/core/#supervision.classification.core.Classifications.from_ultralytics) to enable seamless integration with [Ultralytics](https://github.com/ultralytics/ultralytics) framework. This will enable you to use supervision with all [models](https://docs.ultralytics.com/models/) that Ultralytics supports.
!!! warning
[sv.Detections.from_yolov8](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.from_yolov8) and [sv.Classifications.from_yolov8](https://supervision.roboflow.com/classification/core/#supervision.classification.core.Classifications.from_yolov8) are now deprecated and will be removed with supervision-0.16.0 release.
- Added [#341](https://github.com/roboflow/supervision/pull/341): First supervision usage example script showing how to detect and track objects on video using YOLOv8 + Supervision.
- Changed [#296](https://github.com/roboflow/supervision/pull/296): [`sv.ClassificationDataset`](https://supervision.roboflow.com/dataset/core/#supervision.dataset.core.ClassificationDataset) and [`sv.DetectionDataset`](https://supervision.roboflow.com/dataset/core/#supervision.dataset.core.DetectionDataset) now use image path (not image name) as dataset keys.
- Fixed [#300](https://github.com/roboflow/supervision/pull/300): [`Detections.from_roboflow`](https://supervision.roboflow.com/detection/core/#supervision.detection.core.Detections.from_roboflow) to filter out polygons with less than 3 points.
### 0.13.0 <small>August 8, 2023</small>
- Added [#236](https://github.com/roboflow/supervision/pull/236): support for mean average precision (mAP) for object detection models with [`sv.MeanAveragePrecision`](https://roboflow.github.io/supervision/metrics/detection/#meanaverageprecision).
```python
>>> import supervision as sv
>>> from ultralytics import YOLO
>>> dataset = sv.DetectionDataset.from_yolo(...)
>>> model = YOLO(...)
>>> def callback(image: np.ndarray) -> sv.Detections:
... result = model(image)[0]
... return sv.Detections.from_yolov8(result)
>>> mean_average_precision = sv.MeanAveragePrecision.benchmark(
... dataset = dataset,
... callback = callback
... )
>>> mean_average_precision.map50_95
0.433
```
- Added [#256](https://github.com/roboflow/supervision/pull/256): support for ByteTrack for object tracking with [`sv.ByteTrack`](https://roboflow.github.io/supervision/tracker/core/#bytetrack).
- Added [#222](https://github.com/roboflow/supervision/pull/222): [`sv.Detections.from_ultralytics`](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.from_ultralytics) to enable seamless integration with [Ultralytics](https://github.com/ultralytics/ultralytics) framework. This will enable you to use `supervision` with all [models](https://docs.ultralytics.com/models/) that Ultralytics supports.
!!! warning
[`sv.Detections.from_yolov8`](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.from_yolov8) is now deprecated and will be removed with `supervision-0.15.0` release.
- Added [#191](https://github.com/roboflow/supervision/pull/191): [`sv.Detections.from_paddledet`](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.from_paddledet) to enable seamless integration with [PaddleDetection](https://github.com/PaddlePaddle/PaddleDetection) framework.
- Added [#245](https://github.com/roboflow/supervision/pull/245): support for loading PASCAL VOC segmentation datasets with [`sv.DetectionDataset.`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.from_pascal_voc).
### 0.12.0 <small>July 24, 2023</small>
!!! warning
With the `supervision-0.12.0` release, we are terminating official support for Python 3.7.
- Added [#177](https://github.com/roboflow/supervision/pull/177): initial support for object detection model benchmarking with [`sv.ConfusionMatrix`](https://roboflow.github.io/supervision/metrics/detection/#confusionmatrix).
```python
>>> import supervision as sv
>>> from ultralytics import YOLO
>>> dataset = sv.DetectionDataset.from_yolo(...)
>>> model = YOLO(...)
>>> def callback(image: np.ndarray) -> sv.Detections:
... result = model(image)[0]
... return sv.Detections.from_yolov8(result)
>>> confusion_matrix = sv.ConfusionMatrix.benchmark(
... dataset = dataset,
... callback = callback
... )
>>> confusion_matrix.matrix
array([
[0., 0., 0., 0.],
[0., 1., 0., 1.],
[0., 1., 1., 0.],
[1., 1., 0., 0.]
])
```
- Added [#173](https://github.com/roboflow/supervision/pull/173): [`Detections.from_mmdetection`](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.from_mmdetection) to enable seamless integration with [MMDetection](https://github.com/open-mmlab/mmdetection) framework.
- Added [#130](https://github.com/roboflow/supervision/issues/130): ability to [install](https://roboflow.github.io/supervision/) package in `headless` or `desktop` mode.
- Changed [#180](https://github.com/roboflow/supervision/pull/180): packing method from `setup.py` to `pyproject.toml`.
- Fixed [#188](https://github.com/roboflow/supervision/issues/188): [`sv.DetectionDataset.from_cooc`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.from_coco) can't be loaded when there are images without annotations.
- Fixed [#226](https://github.com/roboflow/supervision/issues/226): [`sv.DetectionDataset.from_yolo`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.from_yolo) can't load background instances.
### 0.11.1 <small>June 29, 2023</small>
- Fix [#165](https://github.com/roboflow/supervision/pull/165): [`as_folder_structure`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.ClassificationDataset.as_folder_structure) fails to save [`sv.ClassificationDataset`](https://roboflow.github.io/supervision/dataset/core/#classificationdataset) when it is result of inference.
### 0.11.0 <small>June 28, 2023</small>
- Added [#150](https://github.com/roboflow/supervision/pull/150): ability to load and save [`sv.DetectionDataset`](https://roboflow.github.io/supervision/dataset/core/#detectiondataset) in COCO format using [`as_coco`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.as_coco) and [`from_coco`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.from_coco) methods.
```python
>>> import supervision as sv
>>> ds = sv.DetectionDataset.from_coco(
... images_directory_path='...',
... annotations_path='...'
... )
>>> ds.as_coco(
... images_directory_path='...',
... annotations_path='...'
... )
```
- Added [#158](https://github.com/roboflow/supervision/pull/158): ability to marge multiple [`sv.DetectionDataset`](https://roboflow.github.io/supervision/dataset/core/#detectiondataset) together using [`merge`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.merge) method.
```python
>>> import supervision as sv
>>> 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_merged = sv.DetectionDataset.merge([ds_1, ds_2])
>>> len(ds_merged)
300
>>> ds_merged.classes
['cat', 'dog', 'person']
```
- Added [#162](https://github.com/roboflow/supervision/pull/162): additional `start` and `end` arguments to [`sv.get_video_frames_generator`](https://roboflow.github.io/supervision/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`.
### 0.10.0 <small>June 14, 2023</small>
- Added [#125](https://github.com/roboflow/supervision/pull/125): ability to load and save [`sv.ClassificationDataset`](https://roboflow.github.io/supervision/dataset/core/#classificationdataset) in a folder structure format.
```python
>>> import supervision as sv
>>> cs = sv.ClassificationDataset.from_folder_structure(
... root_directory_path='...'
... )
>>> cs.as_folder_structure(
... root_directory_path='...'
... )
```
- Added [#125](https://github.com/roboflow/supervision/pull/125): support for [`sv.ClassificationDataset.split`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.ClassificationDataset.split) allowing to divide `sv.ClassificationDataset` into two parts.
- Added [#110](https://github.com/roboflow/supervision/pull/110): ability to extract masks from Roboflow API results using [`sv.Detections.from_roboflow`](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.from_roboflow).
- Added [commit hash](https://github.com/roboflow/supervision/commit/d000292eb2f2342544e0947b65528082e60fb8d6): Supervision Quickstart [notebook](https://colab.research.google.com/github/roboflow/supervision/blob/main/demo.ipynb) where you can learn more about Detection, Dataset and Video APIs.
- Changed [#135](https://github.com/roboflow/supervision/pull/135): `sv.get_video_frames_generator` documentation to better describe actual behavior.
### 0.9.0 <small>June 7, 2023</small>
- Added [#118](https://github.com/roboflow/supervision/pull/118): ability to select [`sv.Detections`](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.__getitem__) by index, list of indexes or slice. Here is an example illustrating the new selection methods.
```python
>>> import supervision as sv
>>> detections = sv.Detections(...)
>>> len(detections[0])
1
>>> len(detections[[0, 1]])
2
>>> len(detections[0:2])
2
```
- Added [#101](https://github.com/roboflow/supervision/pull/101): ability to extract masks from YOLOv8 result using [`sv.Detections.from_yolov8`](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.from_yolov8). Here is an example illustrating how to extract boolean masks from the result of the YOLOv8 model inference.
- Added [#122](https://github.com/roboflow/supervision/pull/122): ability to crop image using [`sv.crop`](https://roboflow.github.io/supervision/utils/image/#crop). Here is an example showing how to get a separate crop for each detection in `sv.Detections`.
- Added [#120](https://github.com/roboflow/supervision/pull/120): ability to conveniently save multiple images into directory using [`sv.ImageSink`](https://roboflow.github.io/supervision/utils/image/#imagesink). Here is an example showing how to save every tenth video frame as a separate image.
```python
>>> import supervision as sv
>>> with sv.ImageSink(target_dir_path='target/directory/path') as sink:
... for image in sv.get_video_frames_generator(source_path='source_video.mp4', stride=10):
... sink.save_image(image=image)
```
- Fixed [#106](https://github.com/roboflow/supervision/issues/106): inconvenient handling of [`sv.PolygonZone`](https://roboflow.github.io/supervision/detection/tools/polygon_zone/#polygonzone) coordinates. Now `sv.PolygonZone` accepts coordinates in the form of `[[x1, y1], [x2, y2], ...]` that can be both integers and floats.
### 0.8.0 <small>May 17, 2023</small>
- Added [#100](https://github.com/roboflow/supervision/pull/100): support for dataset inheritance. The current `Dataset` got renamed to `DetectionDataset`. Now [`DetectionDataset`](https://roboflow.github.io/supervision/dataset/core/#detectiondataset) inherits from `BaseDataset`. This change was made to enforce the future consistency of APIs of different types of computer vision datasets.
- Added [#100](https://github.com/roboflow/supervision/pull/100): ability to save datasets in YOLO format using [`DetectionDataset.as_yolo`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.as_yolo).
```python
>>> import roboflow
>>> from roboflow import Roboflow
>>> import supervision as sv
>>> roboflow.login()
>>> rf = Roboflow()
>>> project = rf.workspace(WORKSPACE_ID).project(PROJECT_ID)
>>> dataset = project.version(PROJECT_VERSION).download("yolov5")
>>> ds = sv.DetectionDataset.from_yolo(
... images_directory_path=f"{dataset.location}/train/images",
... annotations_directory_path=f"{dataset.location}/train/labels",
... data_yaml_path=f"{dataset.location}/data.yaml"
... )
>>> ds.classes
['dog', 'person']
```
- Added [#102](https://github.com/roboflow/supervision/pull/103): support for [`DetectionDataset.split`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.split) allowing to divide `DetectionDataset` into two parts.
```python
>>> import supervision as sv
>>> ds = sv.DetectionDataset(...)
>>> train_ds, test_ds = ds.split(split_ratio=0.7, random_state=42, shuffle=True)
>>> len(train_ds), len(test_ds)
(700, 300)
```
- Changed [#100](https://github.com/roboflow/supervision/pull/100): default value of `approximation_percentage` parameter from `0.75` to `0.0` in `DetectionDataset.as_yolo` and `DetectionDataset.as_pascal_voc`.
### 0.7.0 <small>May 11, 2023</small>
- Added [#91](https://github.com/roboflow/supervision/pull/91): `Detections.from_yolo_nas` to enable seamless integration with [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) model.
- Added [#86](https://github.com/roboflow/supervision/pull/86): ability to load datasets in YOLO format using `Dataset.from_yolo`.
- Added [#84](https://github.com/roboflow/supervision/pull/84): `Detections.merge` to merge multiple `Detections` objects together.
- Fixed [#81](https://github.com/roboflow/supervision/pull/81): `LineZoneAnnotator.annotate` does not return annotated frame.
- Changed [#44](https://github.com/roboflow/supervision/pull/44): `LineZoneAnnotator.annotate` to allow for custom text for the in and out tags.
### 0.6.0 <small>April 19, 2023</small>
- Added [[#71](https://github.com/roboflow/supervision/pull/71)]: initial `Dataset` support and ability to save `Detections` in Pascal VOC XML format.
- Added [[#71](https://github.com/roboflow/supervision/pull/71)]: new `mask_to_polygons`, `filter_polygons_by_area`, `polygon_to_xyxy` and `approximate_polygon` utilities.
- Added [[#72](https://github.com/roboflow/supervision/pull/72)]: ability to load Pascal VOC XML **object detections** dataset as `Dataset`.
- Changed [[#70](https://github.com/roboflow/supervision/pull/70)]: order of `Detections` attributes to make it consistent with order of objects in `__iter__` tuple.
- Changed [[#71](https://github.com/roboflow/supervision/pull/71)]: `generate_2d_mask` to `polygon_to_mask`.
- Added [#71](https://github.com/roboflow/supervision/pull/71): initial `Dataset` support and ability to save `Detections` in Pascal VOC XML format.
- Added [#71](https://github.com/roboflow/supervision/pull/71): new `mask_to_polygons`, `filter_polygons_by_area`, `polygon_to_xyxy` and `approximate_polygon` utilities.
- Added [#72](https://github.com/roboflow/supervision/pull/72): ability to load Pascal VOC XML **object detections** dataset as `Dataset`.
- Changed [#70](https://github.com/roboflow/supervision/pull/70): order of `Detections` attributes to make it consistent with order of objects in `__iter__` tuple.
- Changed [#71](https://github.com/roboflow/supervision/pull/71): `generate_2d_mask` to `polygon_to_mask`.
### 0.5.2 <small>April 13, 2023</small>
- Fixed [[#63](https://github.com/roboflow/supervision/pull/63)]: `LineZone.trigger` function expects 4 values instead of 5.
- Fixed [#63](https://github.com/roboflow/supervision/pull/63): `LineZone.trigger` function expects 4 values instead of 5.
### 0.5.1 <small>April 12, 2023</small>
@@ -17,41 +325,41 @@
### 0.5.0 <small>April 10, 2023</small>
- Added [[#58](https://github.com/roboflow/supervision/pull/58)]: `Detections.mask` to enable segmentation support.
- Added [[#58](https://github.com/roboflow/supervision/pull/58)]: `MaskAnnotator` to allow easy `Detections.mask` annotation.
- Added [[#58](https://github.com/roboflow/supervision/pull/58)]: `Detections.from_sam` to enable native Segment Anything Model (SAM) support.
- Changed [[#58](https://github.com/roboflow/supervision/pull/58)]: `Detections.area` behaviour to work not only with boxes but also with masks.
- Added [#58](https://github.com/roboflow/supervision/pull/58): `Detections.mask` to enable segmentation support.
- Added [#58](https://github.com/roboflow/supervision/pull/58): `MaskAnnotator` to allow easy `Detections.mask` annotation.
- Added [#58](https://github.com/roboflow/supervision/pull/58): `Detections.from_sam` to enable native Segment Anything Model (SAM) support.
- Changed [#58](https://github.com/roboflow/supervision/pull/58): `Detections.area` behaviour to work not only with boxes but also with masks.
### 0.4.0 <small>April 5, 2023</small>
### 0.4.0 <small>April 5, 2023</small>
- Added [[#46](https://github.com/roboflow/supervision/discussions/48)]: `Detections.empty` to allow easy creation of empty `Detections` objects.
- Added [[#56](https://github.com/roboflow/supervision/pull/56)]: `Detections.from_roboflow` to allow easy creation of `Detections` objects from Roboflow API inference results.
- Added [[#56](https://github.com/roboflow/supervision/pull/56)]: `plot_images_grid` to allow easy plotting of multiple images on single plot.
- Added [[#56](https://github.com/roboflow/supervision/pull/56)]: initial support for Pascal VOC XML format with `detections_to_voc_xml` method.
- Changed [[#56](https://github.com/roboflow/supervision/pull/56)]: `show_frame_in_notebook` refactored and renamed to `plot_image`.
- Added [#46](https://github.com/roboflow/supervision/discussions/48): `Detections.empty` to allow easy creation of empty `Detections` objects.
- Added [#56](https://github.com/roboflow/supervision/pull/56): `Detections.from_roboflow` to allow easy creation of `Detections` objects from Roboflow API inference results.
- Added [#56](https://github.com/roboflow/supervision/pull/56): `plot_images_grid` to allow easy plotting of multiple images on single plot.
- Added [#56](https://github.com/roboflow/supervision/pull/56): initial support for Pascal VOC XML format with `detections_to_voc_xml` method.
- Changed [#56](https://github.com/roboflow/supervision/pull/56): `show_frame_in_notebook` refactored and renamed to `plot_image`.
### 0.3.2 <small>March 23, 2023</small>
### 0.3.2 <small>March 23, 2023</small>
- Changed [[#50](https://github.com/roboflow/supervision/issues/50)]: Allow `Detections.class_id` to be `None`.
- Changed [#50](https://github.com/roboflow/supervision/issues/50): Allow `Detections.class_id` to be `None`.
### 0.3.1 <small>March 6, 2023</small>
### 0.3.1 <small>March 6, 2023</small>
- Fixed [[#41](https://github.com/roboflow/supervision/issues/41)]: `PolygonZone` throws an exception when the object touches the bottom edge of the image.
- Fixed [[#42](https://github.com/roboflow/supervision/issues/42)]: `Detections.wth_nms` method throws an exception when `Detections` is empty.
- Changed [[#36](https://github.com/roboflow/supervision/pull/36)]: `Detections.wth_nms` support class agnostic and non-class agnostic case.
- Fixed [#41](https://github.com/roboflow/supervision/issues/41): `PolygonZone` throws an exception when the object touches the bottom edge of the image.
- Fixed [#42](https://github.com/roboflow/supervision/issues/42): `Detections.wth_nms` method throws an exception when `Detections` is empty.
- Changed [#36](https://github.com/roboflow/supervision/pull/36): `Detections.wth_nms` support class agnostic and non-class agnostic case.
### 0.3.0 <small>March 6, 2023</small>
### 0.3.0 <small>March 6, 2023</small>
- Changed: Allow `Detections.confidence` to be `None`.
- Added: `Detections.from_transformers` and `Detections.from_detectron2` to enable seamless integration with Transformers and Detectron2 models.
- Added: `Detections.from_transformers` and `Detections.from_detectron2` to enable seamless integration with Transformers and Detectron2 models.
- Added: `Detections.area` to dynamically calculate bounding box area.
- Added: `Detections.wth_nms` to filter out double detections with NMS. Initial - only class agnostic - implementation.
- Added: `Detections.wth_nms` to filter out double detections with NMS. Initial - only class agnostic - implementation.
### 0.2.0 <small>February 2, 2023</small>
### 0.2.0 <small>February 2, 2023</small>
- Added: Advanced `Detections` filtering with pandas-like API.
- Added: `Detections.from_yolov5` and `Detections.from_yolov8` to enable seamless integration with YOLOv5 and YOLOv8 models.
### 0.1.0 <small>January 19, 2023</small>
### 0.1.0 <small>January 19, 2023</small>
Say hello to Supervision 👋
+3
View File
@@ -0,0 +1,3 @@
## Classifications
:::supervision.classification.core.Classifications
-8
View File
@@ -1,8 +0,0 @@
!!! warning
`Dataset` API is still fluid and may change. If you use Dataset in your project until further notice, freeze the
`supervision` version in your `requirements.txt`.
## Dataset
:::supervision.dataset.core.Dataset
+12
View File
@@ -0,0 +1,12 @@
!!! warning
Dataset API is still fluid and may change. If you use Dataset API in your project until further notice, freeze the
`supervision` version in your `requirements.txt` or `setup.py`.
## DetectionDataset
:::supervision.dataset.core.DetectionDataset
## ClassificationDataset
:::supervision.dataset.core.ClassificationDataset
-7
View File
@@ -1,7 +0,0 @@
## BoxAnnotator
:::supervision.detection.annotate.BoxAnnotator
## MaskAnnotator
:::supervision.detection.annotate.MaskAnnotator
+3
View File
@@ -0,0 +1,3 @@
## InferenceSlicer
:::supervision.detection.tools.inference_slicer.InferenceSlicer
+1 -1
View File
@@ -4,4 +4,4 @@
## PolygonZoneAnnotator
:::supervision.detection.tools.polygon_zone.PolygonZoneAnnotator
:::supervision.detection.tools.polygon_zone.PolygonZoneAnnotator
+1 -1
View File
@@ -24,4 +24,4 @@
## filter_polygons_by_area
:::supervision.detection.utils.filter_polygons_by_area
:::supervision.detection.utils.filter_polygons_by_area
+1 -1
View File
@@ -16,4 +16,4 @@
## draw_text
:::supervision.draw.utils.draw_text
:::supervision.draw.utils.draw_text
+3
View File
@@ -0,0 +1,3 @@
## Position
:::supervision.geometry.core.Position
+54 -18
View File
@@ -9,29 +9,65 @@
</p>
</div>
## 👋 Welcome
## 👋 Hello
Supervision is a set of easy-to-use utilities that will come in handy in any computer vision project.
We write your reusable computer vision tools. Whether you need to load your dataset from your hard drive, draw detections on an image or video, or count how many detections are in a zone. You can count on us!
**Supervision** is still in
pre-release stage 🚧 Keep your eyes open for potential bugs and be aware that at this stage our API is still fluid and may change.
## 💻 Install
## 💻 How to Install
You can install `supervision` with pip in a
[**3.11>=Python>=3.8**](https://www.python.org/) environment.
You can install `supervision` with pip in a
[**3.10>=Python>=3.7**](https://www.python.org/) environment.
!!! example "pip install (recommended)"
!!! example "Pip install method (recommended)"
=== "headless"
The headless installation of `supervision` is designed for environments where graphical user interfaces (GUI) are not needed, making it more lightweight and suitable for server-side applications.
```bash
pip install supervision
```
```bash
pip install supervision
```
!!! example "Git clone method (for development)"
=== "desktop"
If you require the full version of `supervision` with GUI support you can install the desktop version. This version includes the GUI components of OpenCV, allowing you to display images and videos on the screen.
```bash
git https://github.com/roboflow/supervision.git
cd supervision
pip install -e '.[dev]'
```
See contributing section to know more about contributing to the project
```bash
pip install supervision[desktop]
```
!!! example "git clone (for development)"
=== "virtualenv"
```bash
# clone repository and navigate to root directory
git clone https://github.com/roboflow/supervision.git
cd supervision
# setup python environment and activate it
python3 -m venv venv
source venv/bin/activate
# headless install
pip install -e "."
# desktop install
pip install -e ".[desktop]"
```
=== "poetry"
```bash
# clone repository and navigate to root directory
git clone https://github.com/roboflow/supervision.git
cd supervision
# setup python environment and activate it
poetry env use python 3.10
poetry shell
# headless install
poetry install
# desktop install
poetry install --extras "desktop"
```
+12
View File
@@ -0,0 +1,12 @@
!!! warning
Evaluation API is still fluid and may change. If you use Evaluation API in your project until further notice, freeze the
`supervision` version in your `requirements.txt` or `setup.py`.
## ConfusionMatrix
:::supervision.metrics.detection.ConfusionMatrix
## MeanAveragePrecision
:::supervision.metrics.detection.MeanAveragePrecision
-7
View File
@@ -1,7 +0,0 @@
## plot_image
:::supervision.notebook.utils.plot_image
## plot_images_grid
:::supervision.notebook.utils.plot_images_grid
+312
View File
@@ -0,0 +1,312 @@
## advanced filtering
The advanced filtering capabilities of the `Detections` class offer users a versatile and efficient way to narrow down
and refine object detections. This section outlines various filtering methods, including filtering by specific class
or a set of classes, confidence, object area, bounding box area, relative area, box dimensions, and designated zones.
Each method is demonstrated with concise code examples to provide users with a clear understanding of how to implement
the filters in their applications.
### by specific class
Allows you to select detections that belong only to one selected class.
=== "After"
```python
import supervision as sv
detections = sv.Detections(...)
detections = detections[detections.class_id == 0]
```
<div class="result" markdown>
![by-specific-class](https://media.roboflow.com/open-source/supervision/supervision-detection-by-specific-class.png){ align=center width="800" }
</div>
=== "Before"
```python
import supervision as sv
detections = sv.Detections(...)
detections = detections[detections.class_id == 0]
```
<div class="result" markdown>
![original](https://media.roboflow.com/open-source/supervision/supervision-detection-original.png){ align=center width="800" }
</div>
### by set of classes
Allows you to select detections that belong only to selected set of classes.
=== "After"
```python
import numpy as np
import supervision as sv
selected_classes = [0, 2, 3]
detections = sv.Detections(...)
detections = detections[np.isin(detections.class_id, selected_classes)]
```
<div class="result" markdown>
![by-set-of-classes](https://media.roboflow.com/open-source/supervision/supervision-detection-by-set-of-classes.png){ align=center width="800" }
</div>
=== "Before"
```python
import numpy as np
import supervision as sv
class_id = [0, 2, 3]
detections = sv.Detections(...)
detections = detections[np.isin(detections.class_id, class_id)]
```
<div class="result" markdown>
![original](https://media.roboflow.com/open-source/supervision/supervision-detection-original.png){ align=center width="800" }
</div>
### by confidence
Allows you to select detections with specific confidence value, for example higher than selected threshold.
=== "After"
```python
import supervision as sv
detections = sv.Detections(...)
detections = detections[detections.confidence > 0.5]
```
<div class="result" markdown>
![by-set-of-classes](https://media.roboflow.com/open-source/supervision/supervision-detection-by-confidence.png){ align=center width="800" }
</div>
=== "Before"
```python
import supervision as sv
detections = sv.Detections(...)
detections = detections[detections.confidence > 0.5]
```
<div class="result" markdown>
![original](https://media.roboflow.com/open-source/supervision/supervision-detection-original.png){ align=center width="800" }
</div>
### by area
Allows you to select detections based on their size. We define the area as the number of pixels occupied by the
detection in the image. In the example below, we have sifted out the detections that are too small.
=== "After"
```python
import supervision as sv
detections = sv.Detections(...)
detections = detections[detections.area > 1000]
```
<div class="result" markdown>
![by-area](https://media.roboflow.com/open-source/supervision/supervision-detection-by-area.png){ align=center width="800" }
</div>
=== "Before"
```python
import supervision as sv
detections = sv.Detections(...)
detections = detections[detections.area > 1000]
```
<div class="result" markdown>
![original](https://media.roboflow.com/open-source/supervision/supervision-detection-original.png){ align=center width="800" }
</div>
### by relative area
Allows you to select detections based on their size in relation to the size of whole image. Sometimes the concept of
detection size changes depending on the image. Detection occupying 10000 square px can be large on a 1280x720 image
but small on a 3840x2160 image. In such cases, we can filter out detections based on the percentage of the image area
occupied by them. In the example below, we remove too large detections.
=== "After"
```python
import supervision as sv
image = ...
height, width, channels = image.shape
image_area = height * width
detections = sv.Detections(...)
detections = detections[(detections.area / image_area) < 0.8]
```
<div class="result" markdown>
![by-relative-area](https://media.roboflow.com/open-source/supervision/supervision-detection-by-relative-area.png?updatedAt=1683207183434){ align=center width="800" }
</div>
=== "Before"
```python
import supervision as sv
image = ...
height, width, channels = image.shape
image_area = height * width
detections = sv.Detections(...)
detections = detections[(detections.area / image_area) < 0.8]
```
<div class="result" markdown>
![original](https://media.roboflow.com/open-source/supervision/supervision-detection-original.png){ align=center width="800" }
</div>
### by box dimensions
Allows you to select detections based on their dimensions. The size of the bounding box, as well as its coordinates,
can be criteria for rejecting detection. Implementing such filtering requires a bit of custom code but is relatively
simple and fast.
=== "After"
```python
import supervision as sv
detections = sv.Detections(...)
w = detections.xyxy[:, 2] - detections.xyxy[:, 0]
h = detections.xyxy[:, 3] - detections.xyxy[:, 1]
detections = detections[(w > 200) & (h > 200)]
```
<div class="result" markdown>
![by-box-dimensions](https://media.roboflow.com/open-source/supervision/supervision-detection-by-box-dimensions.png){ align=center width="800" }
</div>
=== "Before"
```python
import supervision as sv
detections = sv.Detections(...)
w = detections.xyxy[:, 2] - detections.xyxy[:, 0]
h = detections.xyxy[:, 3] - detections.xyxy[:, 1]
detections = detections[(w > 200) & (h > 200)]
```
<div class="result" markdown>
![original](https://media.roboflow.com/open-source/supervision/supervision-detection-original.png){ align=center width="800" }
</div>
### by `PolygonZone`
Allows you to use `Detections` in combination with `PolygonZone` to weed out bounding boxes that are in and out of the
zone. In the example below you can see how to filter out all detections located in the lower part of the image.
=== "After"
```python
import supervision as sv
zone = sv.PolygonZone(...)
detections = sv.Detections(...)
mask = zone.trigger(detections=detections)
detections = detections[mask]
```
<div class="result" markdown>
![by-polygon-zone](https://media.roboflow.com/open-source/supervision/supervision-detection-by-polygon-zone.png?updatedAt=1683211380445){ align=center width="800" }
</div>
=== "Before"
```python
import supervision as sv
zone = sv.PolygonZone(...)
detections = sv.Detections(...)
mask = zone.trigger(detections=detections)
detections = detections[mask]
```
<div class="result" markdown>
![original](https://media.roboflow.com/open-source/supervision/supervision-detection-original.png){ align=center width="800" }
</div>
### by mixed conditions
`Detections`' greatest strength, however, is that you can build arbitrarily complex logical conditions by simply combining separate conditions using `&` or `|`.
=== "After"
```python
import supervision as sv
zone = sv.PolygonZone(...)
detections = sv.Detections(...)
mask = zone.trigger(detections=detections)
detections = detections[(detections.confidence > 0.7) & mask]
```
<div class="result" markdown>
![by-mixed-conditions](https://media.roboflow.com/open-source/supervision/supervision-detection-by-mixed-conditions.png){ align=center width="800" }
</div>
=== "Before"
```python
import supervision as sv
zone = sv.PolygonZone(...)
detections = sv.Detections(...)
mask = zone.trigger(detections=detections)
detections = detections[mask]
```
<div class="result" markdown>
![original](https://media.roboflow.com/open-source/supervision/supervision-detection-original.png){ align=center width="800" }
</div>
+4
View File
@@ -0,0 +1,4 @@
:root {
--md-primary-fg-color: #8315F9;
--md-accent-fg-color: #00FFCE;
}
+3
View File
@@ -0,0 +1,3 @@
## ByteTrack
:::supervision.tracker.byte_tracker.core.ByteTrack
+3
View File
@@ -0,0 +1,3 @@
## list_files_with_extensions
:::supervision.utils.file.list_files_with_extensions
+7
View File
@@ -0,0 +1,7 @@
## ImageSink
:::supervision.utils.image.ImageSink
## crop
:::supervision.utils.image.crop_image
+7
View File
@@ -0,0 +1,7 @@
## plot_image
:::supervision.utils.notebook.plot_image
## plot_images_grid
:::supervision.utils.notebook.plot_images_grid
+15
View File
@@ -0,0 +1,15 @@
## VideoInfo
:::supervision.utils.video.VideoInfo
## VideoSink
:::supervision.utils.video.VideoSink
## get_video_frames_generator
:::supervision.utils.video.get_video_frames_generator
## process_video
:::supervision.utils.video.process_video
-15
View File
@@ -1,15 +0,0 @@
## VideoInfo
:::supervision.video.VideoInfo
## VideoSink
:::supervision.video.VideoSink
## get_video_frames_generator
:::supervision.video.get_video_frames_generator
## process_video
:::supervision.video.process_video
+20
View File
@@ -0,0 +1,20 @@
## 👋 hello
This script provides functionality for processing videos using YOLOv8 for object
detection and Supervision for tracking and annotation.
## 💻 install
```bash
pip install -r requirements.txt
```
## ⚙️ parameters
| parameter | required | description |
|:-------------------------|:--------:|:----------------------------------------------------------------------------------|
| `--source_weights_path` | ✓ | Path to the source weights file for YOLOv8. |
| `--source_video_path` | ✓ | Path to the source video file to be processed. |
| `--target_video_path` | ✓ | Path to the target video file (output). |
| `--confidence_threshold` | ✗ | Confidence threshold for YOLO model detection. Default is 0.3. |
| `--iou_threshold` | ✗ | IOU (Intersection over Union) threshold for YOLO model detection. Default is 0.7. |
+3
View File
@@ -0,0 +1,3 @@
supervision
tqdm
ultralytics
+83
View File
@@ -0,0 +1,83 @@
import argparse
from tqdm import tqdm
from ultralytics import YOLO
import supervision as sv
def process_video(
source_weights_path: str,
source_video_path: str,
target_video_path: str,
confidence_threshold: float = 0.3,
iou_threshold: float = 0.7,
) -> None:
model = YOLO(source_weights_path)
tracker = sv.ByteTrack()
box_annotator = sv.BoundingBoxAnnotator()
label_annotator = sv.LabelAnnotator(classes=model.names)
frame_generator = sv.get_video_frames_generator(source_path=source_video_path)
video_info = sv.VideoInfo.from_video_path(video_path=source_video_path)
with sv.VideoSink(target_path=target_video_path, video_info=video_info) as sink:
for frame in tqdm(frame_generator, total=video_info.total_frames):
results = model(
frame, verbose=False, conf=confidence_threshold, iou=iou_threshold
)[0]
detections = sv.Detections.from_ultralytics(results)
detections = tracker.update_with_detections(detections)
annotated_frame = box_annotator.annotate(
scene=frame.copy(), detections=detections
)
annotated_labeled_frame = label_annotator.annotate(
scene=annotated_frame, detections=detections
)
sink.write_frame(frame=annotated_labeled_frame)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Video Processing with YOLO and ByteTrack"
)
parser.add_argument(
"--source_weights_path",
required=True,
help="Path to the source weights file",
type=str,
)
parser.add_argument(
"--source_video_path",
required=True,
help="Path to the source video file",
type=str,
)
parser.add_argument(
"--target_video_path",
required=True,
help="Path to the target video file (output)",
type=str,
)
parser.add_argument(
"--confidence_threshold",
default=0.3,
help="Confidence threshold for the model",
type=float,
)
parser.add_argument(
"--iou_threshold", default=0.7, help="IOU threshold for the model", type=float
)
args = parser.parse_args()
process_video(
source_weights_path=args.source_weights_path,
source_video_path=args.source_video_path,
target_video_path=args.target_video_path,
confidence_threshold=args.confidence_threshold,
iou_threshold=args.iou_threshold,
)
+1
View File
@@ -0,0 +1 @@
data/
+44
View File
@@ -0,0 +1,44 @@
## 👋 hello
This script performs traffic flow analysis using YOLOv8, an object-detection method and ByteTrack, a simple yet effective online multi-object tracking method. It uses the supervision package for multiple tasks such as tracking, annotations, etc.
https://github.com/roboflow/supervision/assets/26109316/c9436828-9fbf-4c25-ae8c-60e9c81b3900
## 💻 install
- clone repository and navigate to example directory
```bash
git clone https://github.com/roboflow/supervision.git
cd supervision/examples/traffic_analysis
```
- setup python environment and activate it [optional]
```bash
python3 -m venv venv
source venv/bin/activate
```
- install required dependencies
```bash
pip install -r requirements.txt
```
- download `traffic_analysis.pt` and `traffic_analysis.mov` files
```bash
./setup.sh
```
## ⚙️ run
```bash
python script.py \
--source_weights_path data/traffic_analysis.pt \
--source_video_path data/traffic_analysis.mov \
--confidence_threshold 0.3 \
--iou_threshold 0.5 \
--target_video_path data/traffic_analysis_result.mov
```
@@ -0,0 +1,4 @@
supervision>=0.15.0rc1
tqdm
ultralytics
gdown
+219
View File
@@ -0,0 +1,219 @@
import argparse
from typing import Dict, List, Set, Tuple
import cv2
import numpy as np
from tqdm import tqdm
from ultralytics import YOLO
import supervision as sv
COLORS = sv.ColorPalette.default()
ZONE_IN_POLYGONS = [
np.array([[592, 282], [900, 282], [900, 82], [592, 82]]),
np.array([[950, 860], [1250, 860], [1250, 1060], [950, 1060]]),
np.array([[592, 582], [592, 860], [392, 860], [392, 582]]),
np.array([[1250, 282], [1250, 530], [1450, 530], [1450, 282]]),
]
ZONE_OUT_POLYGONS = [
np.array([[950, 282], [1250, 282], [1250, 82], [950, 82]]),
np.array([[592, 860], [900, 860], [900, 1060], [592, 1060]]),
np.array([[592, 282], [592, 550], [392, 550], [392, 282]]),
np.array([[1250, 860], [1250, 560], [1450, 560], [1450, 860]]),
]
class DetectionsManager:
def __init__(self) -> None:
self.tracker_id_to_zone_id: Dict[int, int] = {}
self.counts: Dict[int, Dict[int, Set[int]]] = {}
def update(
self,
detections_all: sv.Detections,
detections_in_zones: List[sv.Detections],
detections_out_zones: List[sv.Detections],
) -> sv.Detections:
for zone_in_id, detections_in_zone in enumerate(detections_in_zones):
for tracker_id in detections_in_zone.tracker_id:
self.tracker_id_to_zone_id.setdefault(tracker_id, zone_in_id)
for zone_out_id, detections_out_zone in enumerate(detections_out_zones):
for tracker_id in detections_out_zone.tracker_id:
if tracker_id in self.tracker_id_to_zone_id:
zone_in_id = self.tracker_id_to_zone_id[tracker_id]
self.counts.setdefault(zone_out_id, {})
self.counts[zone_out_id].setdefault(zone_in_id, set())
self.counts[zone_out_id][zone_in_id].add(tracker_id)
detections_all.class_id = np.vectorize(
lambda x: self.tracker_id_to_zone_id.get(x, -1)
)(detections_all.tracker_id)
return detections_all[detections_all.class_id != -1]
def initiate_polygon_zones(
polygons: List[np.ndarray],
frame_resolution_wh: Tuple[int, int],
triggering_position: sv.Position = sv.Position.CENTER,
) -> List[sv.PolygonZone]:
return [
sv.PolygonZone(
polygon=polygon,
frame_resolution_wh=frame_resolution_wh,
triggering_position=triggering_position,
)
for polygon in polygons
]
class VideoProcessor:
def __init__(
self,
source_weights_path: str,
source_video_path: str,
target_video_path: str = None,
confidence_threshold: float = 0.3,
iou_threshold: float = 0.7,
) -> None:
self.conf_threshold = confidence_threshold
self.iou_threshold = iou_threshold
self.source_video_path = source_video_path
self.target_video_path = target_video_path
self.model = YOLO(source_weights_path)
self.tracker = sv.ByteTrack()
self.video_info = sv.VideoInfo.from_video_path(source_video_path)
self.zones_in = initiate_polygon_zones(
ZONE_IN_POLYGONS, self.video_info.resolution_wh, sv.Position.CENTER
)
self.zones_out = initiate_polygon_zones(
ZONE_OUT_POLYGONS, self.video_info.resolution_wh, sv.Position.CENTER
)
self.box_annotator = sv.BoxAnnotator(color=COLORS)
self.trace_annotator = sv.TraceAnnotator(
color=COLORS, position=sv.Position.CENTER, trace_length=100, thickness=2
)
self.detections_manager = DetectionsManager()
def process_video(self):
frame_generator = sv.get_video_frames_generator(
source_path=self.source_video_path
)
if self.target_video_path:
with sv.VideoSink(self.target_video_path, self.video_info) as sink:
for frame in tqdm(frame_generator, total=self.video_info.total_frames):
annotated_frame = self.process_frame(frame)
sink.write_frame(annotated_frame)
else:
for frame in tqdm(frame_generator, total=self.video_info.total_frames):
annotated_frame = self.process_frame(frame)
cv2.imshow("Processed Video", annotated_frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cv2.destroyAllWindows()
def annotate_frame(
self, frame: np.ndarray, detections: sv.Detections
) -> np.ndarray:
annotated_frame = frame.copy()
for i, (zone_in, zone_out) in enumerate(zip(self.zones_in, self.zones_out)):
annotated_frame = sv.draw_polygon(
annotated_frame, zone_in.polygon, COLORS.colors[i]
)
annotated_frame = sv.draw_polygon(
annotated_frame, zone_out.polygon, COLORS.colors[i]
)
labels = [f"#{tracker_id}" for tracker_id in detections.tracker_id]
annotated_frame = self.trace_annotator.annotate(annotated_frame, detections)
annotated_frame = self.box_annotator.annotate(
annotated_frame, detections, labels
)
for zone_out_id, zone_out in enumerate(self.zones_out):
zone_center = sv.get_polygon_center(polygon=zone_out.polygon)
if zone_out_id in self.detections_manager.counts:
counts = self.detections_manager.counts[zone_out_id]
for i, zone_in_id in enumerate(counts):
count = len(self.detections_manager.counts[zone_out_id][zone_in_id])
text_anchor = sv.Point(x=zone_center.x, y=zone_center.y + 40 * i)
annotated_frame = sv.draw_text(
scene=annotated_frame,
text=str(count),
text_anchor=text_anchor,
background_color=COLORS.colors[zone_in_id],
)
return annotated_frame
def process_frame(self, frame: np.ndarray) -> np.ndarray:
results = self.model(
frame, verbose=False, conf=self.conf_threshold, iou=self.iou_threshold
)[0]
detections = sv.Detections.from_ultralytics(results)
detections.class_id = np.zeros(len(detections))
detections = self.tracker.update_with_detections(detections)
detections_in_zones = []
detections_out_zones = []
for i, (zone_in, zone_out) in enumerate(zip(self.zones_in, self.zones_out)):
detections_in_zone = detections[zone_in.trigger(detections=detections)]
detections_in_zones.append(detections_in_zone)
detections_out_zone = detections[zone_out.trigger(detections=detections)]
detections_out_zones.append(detections_out_zone)
detections = self.detections_manager.update(
detections, detections_in_zones, detections_out_zones
)
return self.annotate_frame(frame, detections)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Traffic Flow Analysis with YOLO and ByteTrack"
)
parser.add_argument(
"--source_weights_path",
required=True,
help="Path to the source weights file",
type=str,
)
parser.add_argument(
"--source_video_path",
required=True,
help="Path to the source video file",
type=str,
)
parser.add_argument(
"--target_video_path",
default=None,
help="Path to the target video file (output)",
type=str,
)
parser.add_argument(
"--confidence_threshold",
default=0.3,
help="Confidence threshold for the model",
type=float,
)
parser.add_argument(
"--iou_threshold", default=0.7, help="IOU threshold for the model", type=float
)
args = parser.parse_args()
processor = VideoProcessor(
source_weights_path=args.source_weights_path,
source_video_path=args.source_video_path,
target_video_path=args.target_video_path,
confidence_threshold=args.confidence_threshold,
iou_threshold=args.iou_threshold,
)
processor.process_video()
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
# Get the directory where the script is located
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# Check if 'data' directory does not exist and then create it
if [[ ! -e $DIR/data ]]; then
mkdir "$DIR/data"
else
echo "'data' directory already exists."
fi
# Download the traffic_analysis.mov file from Google Drive
gdown -O "$DIR/data/traffic_analysis.mov" "https://drive.google.com/uc?id=1qadBd7lgpediafCpL_yedGjQPk-FLK-W"
# Download the traffic_analysis.pt file from Google Drive
gdown -O "$DIR/data/traffic_analysis.pt" "https://drive.google.com/uc?id=1y-IfToCjRXa3ZdC1JpnKRopC7mcQW-5z"
+44 -17
View File
@@ -21,22 +21,34 @@ extra:
provider: google
property: G-P7ZG0Y19G5
extra_css:
- stylesheets/extra.css
nav:
- Home: index.md
- API reference:
- Detection:
- Core: detection/core.md
- Annotate: detection/annotate.md
- Utils: detection/utils.md
- Tools:
- Polygon Zone: detection/tools/polygon_zone.md
- Dataset:
- Core: dataset/core.md
- Draw:
- Utils: draw/utils.md
- Notebook:
- Utils: notebook/utils.md
- Video: video.md
- Quickstart:
- Detections: quickstart/detections.md
- Classifications:
- Core: classification/core.md
- Detections:
- Core: detection/core.md
- Annotate: detection/annotate.md
- Utils: detection/utils.md
- Tools:
- Polygon Zone: detection/tools/polygon_zone.md
- Inference Slicer: detection/tools/inference_slicer.md
- Annotators: annotators.md
- Trackers: trackers.md
- Datasets: datasets.md
- Metrics:
- Object Detection: metrics/detection.md
- Draw:
- Utils: draw/utils.md
- Utils:
- Video: utils/video.md
- Image: utils/image.md
- Notebook: utils/notebook.md
- File: utils/file.md
- Changelog: changelog.md
theme:
@@ -44,8 +56,19 @@ theme:
logo: https://media.roboflow.com/open-source/supervision/supervision-lenny.png
favicon: https://media.roboflow.com/open-source/supervision/supervision-lenny.png
palette:
primary: 'deep purple'
accent: 'teal'
# Palette for light mode
- scheme: default
primary: 'custom'
toggle:
icon: material/brightness-7
name: Switch to dark mode
# Palette toggle for dark mode
- scheme: slate
primary: 'custom'
toggle:
icon: material/brightness-4
name: Switch to light mode
font:
text: Roboto
code: Roboto Mono
@@ -58,5 +81,9 @@ markdown_extensions:
- admonition
- pymdownx.details
- pymdownx.superfences
- attr_list
- md_in_html
- pymdownx.tabbed:
alternate_style: true
- toc:
permalink: true
permalink: true
Generated
+3523
View File
File diff suppressed because it is too large Load Diff
+183
View File
@@ -0,0 +1,183 @@
[tool.poetry]
name = "supervision"
version = "0.15.0"
description = "A set of easy-to-use utils that will come in handy in any Computer Vision project"
authors = ["Piotr Skalski <piotr.skalski92@gmail.com>"]
maintainers = ["Piotr Skalski <piotr.skalski92@gmail.com>"]
readme = "README.md"
license = "MIT"
packages = [{include = "supervision"}]
homepage = "https://github.com/roboflow/supervision"
repository = "https://github.com/roboflow/supervision"
documentation = "https://github.com/roboflow/supervision/blob/main/README.md"
keywords = ["machine-learning", "deep-learning", "vision", "ML", "DL", "AI", "YOLOv5", "YOLOv8", "Roboflow"]
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.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'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'
]
[tool.poetry.dependencies]
python = ">=3.8,<3.12.0"
numpy = "^1.20.0"
matplotlib = "^3.7.1"
pyyaml = "^6.0"
pillow = ">=9.4,<11.0"
opencv-python = { version = "^4.8.0.74", optional = true }
opencv-python-headless = "^4.8.0.74"
scipy = "^1.9.0"
[tool.poetry.extras]
desktop = ["opencv-python"]
[tool.poetry.group.dev.dependencies]
twine = "^4.0.2"
pytest = "^7.2.2"
wheel = "^0.40.0"
notebook = "^6.5.3"
build = "^0.10.0"
ruff = "^0.0.280"
isort = "^5.12.0"
black = "^23.7.0"
mypy = "^1.4.1"
pre-commit = "^3.3.3"
flake8 = { version = "*", python = ">=3.8.1,<3.12.0" }
[tool.poetry.group.docs.dependencies]
mkdocs-material = "^9.1.4"
mkdocstrings = {extras = ["python"], version = "^0.20.0"}
[tool.flake8]
exclude = ".venv"
max-complexity = 10
max-line-length = 88
extend-ignore = """
W503,
E203,
E701,
C901,
"""
per-file-ignores = """
__init__.py: F401
"""
[tool.isort]
line_length = 88
profile = "black"
[tool.bandit]
target = ["test", "supervision"]
tests = ["B201", "B301"]
[tool.autoflake]
check = true
imports = ["cv2", "supervision"]
[tool.black]
target-version = ["py38"]
line-length = 88
include = '\.pyi?$'
exclude = '''
/(
\.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
| docs
)/
'''
[tool.ruff]
target-version = "py38"
# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
select = ["E", "F"]
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"]
unfixable = []
# 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",
]
# Same as Black.
line-length = 88
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.flake8-quotes]
inline-quotes = "double"
multiline-quotes = "double"
docstring-quotes = "double"
[tool.ruff.pydocstyle]
convention = "google"
[tool.ruff.per-file-ignores]
"__init__.py" = ["E402","F401"]
[tool.ruff.pylint]
max-args = 20
[tool.setuptools]
include-package-data = false
[tool.setuptools.packages.find]
exclude = ["docs*", "test*"]
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
-67
View File
@@ -1,67 +0,0 @@
import setuptools
from setuptools import find_packages
import re
from pathlib import Path
FILE = Path(__file__).resolve()
PARENT = FILE.parent # root directory
README = (PARENT / "README.md").read_text(encoding="utf-8")
def get_version():
file = PARENT / 'supervision/__init__.py'
return re.search(r'^__version__ = [\'"]([^\'"]*)[\'"]', file.read_text(encoding="utf-8"), re.M)[1]
setuptools.setup(
name='supervision',
version=get_version(),
author='Piotr Skalski',
author_email='piotr.skalski92@gmail.com',
license='MIT',
description='A set of easy-to-use utils that will come in handy in any Computer Vision project',
long_description=README,
long_description_content_type='text/markdown',
url='https://github.com/roboflow/supervision',
install_requires=[
'numpy>=1.20.0',
'opencv-python',
'matplotlib'
],
packages=find_packages(exclude=("tests",)),
extras_require={
'dev': [
'flake8',
'black==22.3.0',
'isort',
'twine',
'pytest',
'wheel',
'notebook',
'mkdocs-material',
'mkdocstrings[python]'
],
},
classifiers=[
'Intended Audience :: Developers',
'Intended Audience :: Science/Research',
'License :: OSI Approved :: BSD License',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'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',
'Operating System :: Unix',
'Operating System :: MacOS'
],
keywords="machine-learning, deep-learning, vision, ML, DL, AI, YOLOv5, YOLOv8, Roboflow",
python_requires='>=3.7',
)
+31 -6
View File
@@ -1,9 +1,31 @@
__version__ = "0.6.0"
import importlib.metadata as importlib_metadata
from supervision.dataset.core import Dataset
from supervision.detection.annotate import BoxAnnotator, MaskAnnotator
try:
# This will read version from pyproject.toml
__version__ = importlib_metadata.version(__package__ or __name__)
except importlib_metadata.PackageNotFoundError:
__version__ = "development"
from supervision.annotators.core import (
BlurAnnotator,
BoundingBoxAnnotator,
BoxCornerAnnotator,
CircleAnnotator,
EllipseAnnotator,
LabelAnnotator,
MaskAnnotator,
TraceAnnotator,
)
from supervision.classification.core import Classifications
from supervision.dataset.core import (
BaseDataset,
ClassificationDataset,
DetectionDataset,
)
from supervision.detection.annotate import BoxAnnotator
from supervision.detection.core import Detections
from supervision.detection.line_counter import LineZone, LineZoneAnnotator
from supervision.detection.tools.inference_slicer import InferenceSlicer
from supervision.detection.tools.polygon_zone import PolygonZone, PolygonZoneAnnotator
from supervision.detection.utils import (
box_iou_batch,
@@ -16,11 +38,14 @@ from supervision.detection.utils import (
)
from supervision.draw.color import Color, ColorPalette
from supervision.draw.utils import draw_filled_rectangle, draw_polygon, draw_text
from supervision.file import list_files_with_extensions
from supervision.geometry.core import Point, Position, Rect
from supervision.geometry.utils import get_polygon_center
from supervision.notebook.utils import plot_image, plot_images_grid
from supervision.video import (
from supervision.metrics.detection import ConfusionMatrix, MeanAveragePrecision
from supervision.tracker.byte_tracker.core import ByteTrack
from supervision.utils.file import list_files_with_extensions
from supervision.utils.image import ImageSink, crop_image
from supervision.utils.notebook import plot_image, plot_images_grid
from supervision.utils.video import (
VideoInfo,
VideoSink,
get_video_frames_generator,
+11
View File
@@ -0,0 +1,11 @@
from abc import ABC, abstractmethod
import numpy as np
from supervision.detection.core import Detections
class BaseAnnotator(ABC):
@abstractmethod
def annotate(self, scene: np.ndarray, detections: Detections) -> np.ndarray:
pass
+707
View File
@@ -0,0 +1,707 @@
from math import sqrt
from typing import List, Optional, Tuple, Union
import cv2
import numpy as np
from supervision.annotators.base import BaseAnnotator
from supervision.annotators.utils import (
ColorMap,
Trace,
resolve_color,
resolve_color_idx,
)
from supervision.detection.core import Detections
from supervision.draw.color import Color, ColorPalette
from supervision.geometry.core import Position
class BoundingBoxAnnotator(BaseAnnotator):
"""
A class for drawing bounding boxes on an image using provided detections.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
thickness: int = 2,
color_map: str = "class",
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating detections.
thickness (int): Thickness of the bounding box lines.
color_map (str): Strategy for mapping colors to annotations.
Options are `index`, `class`, or `track`.
"""
self.color: Union[Color, ColorPalette] = color
self.thickness: int = thickness
self.color_map: ColorMap = ColorMap(color_map)
def annotate(self, scene: np.ndarray, detections: Detections) -> np.ndarray:
"""
Annotates the given scene with bounding boxes based on the provided detections.
Args:
scene (np.ndarray): The image where bounding boxes will be drawn.
detections (Detections): Object detections to annotate.
Returns:
np.ndarray: The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> bounding_box_annotator = sv.BoundingBoxAnnotator()
>>> annotated_frame = bounding_box_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![bounding-box-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/bounding-box-annotator-example.png)
"""
for detection_idx in range(len(detections)):
x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
idx = resolve_color_idx(
detections=detections,
detection_idx=detection_idx,
color_map=self.color_map,
)
color = resolve_color(color=self.color, idx=idx)
cv2.rectangle(
img=scene,
pt1=(x1, y1),
pt2=(x2, y2),
color=color.as_bgr(),
thickness=self.thickness,
)
return scene
class MaskAnnotator(BaseAnnotator):
"""
A class for drawing masks on an image using provided detections.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
opacity: float = 0.5,
color_map: str = "class",
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating detections.
opacity (float): Opacity of the overlay mask. Must be between `0` and `1`.
color_map (str): Strategy for mapping colors to annotations.
Options are `index`, `class`, or `track`.
"""
self.color: Union[Color, ColorPalette] = color
self.opacity = opacity
self.color_map: ColorMap = ColorMap(color_map)
def annotate(self, scene: np.ndarray, detections: Detections) -> np.ndarray:
"""
Annotates the given scene with masks based on the provided detections.
Args:
scene (np.ndarray): The image where masks will be drawn.
detections (Detections): Object detections to annotate.
Returns:
np.ndarray: The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> mask_annotator = sv.MaskAnnotator()
>>> annotated_frame = mask_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![mask-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/mask-annotator-example.png)
"""
if detections.mask is None:
return scene
for detection_idx in np.flip(np.argsort(detections.area)):
idx = resolve_color_idx(
detections=detections,
detection_idx=detection_idx,
color_map=self.color_map,
)
color = resolve_color(color=self.color, idx=idx)
mask = detections.mask[detection_idx]
colored_mask = np.zeros_like(scene, dtype=np.uint8)
colored_mask[:] = color.as_bgr()
scene = np.where(
np.expand_dims(mask, axis=-1),
np.uint8(self.opacity * colored_mask + (1 - self.opacity) * scene),
scene,
)
return scene
class EllipseAnnotator(BaseAnnotator):
"""
A class for drawing ellipses on an image using provided detections.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
thickness: int = 2,
start_angle: int = -45,
end_angle: int = 235,
color_map: str = "class",
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating detections.
thickness (int): Thickness of the ellipse lines.
start_angle (int): Starting angle of the ellipse.
end_angle (int): Ending angle of the ellipse.
color_map (str): Strategy for mapping colors to annotations.
Options are `index`, `class`, or `track`.
"""
self.color: Union[Color, ColorPalette] = color
self.thickness: int = thickness
self.start_angle: int = start_angle
self.end_angle: int = end_angle
self.color_map: ColorMap = ColorMap(color_map)
def annotate(self, scene: np.ndarray, detections: Detections) -> np.ndarray:
"""
Annotates the given scene with ellipses based on the provided detections.
Args:
scene (np.ndarray): The image where ellipses will be drawn.
detections (Detections): Object detections to annotate.
Returns:
np.ndarray: The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> ellipse_annotator = sv.EllipseAnnotator()
>>> annotated_frame = ellipse_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![ellipse-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/ellipse-annotator-example.png)
"""
for detection_idx in range(len(detections)):
x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
idx = resolve_color_idx(
detections=detections,
detection_idx=detection_idx,
color_map=self.color_map,
)
color = resolve_color(color=self.color, idx=idx)
center = (int((x1 + x2) / 2), y2)
width = x2 - x1
cv2.ellipse(
scene,
center=center,
axes=(int(width), int(0.35 * width)),
angle=0.0,
startAngle=self.start_angle,
endAngle=self.end_angle,
color=color.as_bgr(),
thickness=self.thickness,
lineType=cv2.LINE_4,
)
return scene
class BoxCornerAnnotator(BaseAnnotator):
"""
A class for drawing box corners on an image using provided detections.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
thickness: int = 4,
corner_length: int = 25,
color_map: str = "class",
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating detections.
thickness (int): Thickness of the corner lines.
corner_length (int): Length of each corner line.
color_map (str): Strategy for mapping colors to annotations.
Options are `index`, `class`, or `track`.
"""
self.color: Union[Color, ColorPalette] = color
self.thickness: int = thickness
self.corner_length: int = corner_length
self.color_map: ColorMap = ColorMap(color_map)
def annotate(self, scene: np.ndarray, detections: Detections) -> np.ndarray:
"""
Annotates the given scene with box corners based on the provided detections.
Args:
scene (np.ndarray): The image where box corners will be drawn.
detections (Detections): Object detections to annotate.
Returns:
np.ndarray: The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> corner_annotator = sv.BoxCornerAnnotator()
>>> annotated_frame = corner_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![box-corner-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/box-corner-annotator-example.png)
"""
for detection_idx in range(len(detections)):
x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
idx = resolve_color_idx(
detections=detections,
detection_idx=detection_idx,
color_map=self.color_map,
)
color = resolve_color(color=self.color, idx=idx)
corners = [(x1, y1), (x2, y1), (x1, y2), (x2, y2)]
for x, y in corners:
x_end = x + self.corner_length if x == x1 else x - self.corner_length
cv2.line(
scene, (x, y), (x_end, y), color.as_bgr(), thickness=self.thickness
)
y_end = y + self.corner_length if y == y1 else y - self.corner_length
cv2.line(
scene, (x, y), (x, y_end), color.as_bgr(), thickness=self.thickness
)
return scene
class CircleAnnotator(BaseAnnotator):
"""
A class for drawing circle on an image using provided detections.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
thickness: int = 4,
color_map: str = "class",
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating detections.
thickness (int): Thickness of the circle line.
color_map (str): Strategy for mapping colors to annotations.
Options are `index`, `class`, or `track`.
"""
self.color: Union[Color, ColorPalette] = color
self.thickness: int = thickness
self.color_map: ColorMap = ColorMap(color_map)
def annotate(
self,
scene: np.ndarray,
detections: Detections,
) -> np.ndarray:
"""
Annotates the given scene with circles based on the provided detections.
Args:
scene (np.ndarray): The image where box corners will be drawn.
detections (Detections): Object detections to annotate.
Returns:
np.ndarray: The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> circle_annotator = sv.CircleAnnotator()
>>> annotated_frame = circle_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![circle-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/circle-annotator-example.png)
"""
for detection_idx in range(len(detections)):
x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
center = ((x1 + x2) // 2, (y1 + y2) // 2)
distance = sqrt((x1 - center[0]) ** 2 + (y1 - center[1]) ** 2)
idx = resolve_color_idx(
detections=detections,
detection_idx=detection_idx,
color_map=self.color_map,
)
color = (
self.color.by_idx(idx)
if isinstance(self.color, ColorPalette)
else self.color
)
cv2.circle(
img=scene,
center=center,
radius=int(distance),
color=color.as_bgr(),
thickness=self.thickness,
)
return scene
class LabelAnnotator:
"""
A class for annotating labels on an image using provided detections.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
text_color: Color = Color.black(),
text_scale: float = 0.5,
text_thickness: int = 1,
text_padding: int = 10,
text_position: Position = Position.TOP_LEFT,
color_map: str = "class",
):
"""
Args:
color (Union[Color, ColorPalette]): The color or color palette to use for
annotating the text background.
text_color (Color): The color 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.
color_map (str): Strategy for mapping colors to annotations.
Options are `index`, `class`, or `track`.
"""
self.color: Union[Color, ColorPalette] = color
self.text_color: Color = text_color
self.text_scale: float = text_scale
self.text_thickness: int = text_thickness
self.text_padding: int = text_padding
self.text_position: Position = text_position
self.color_map: ColorMap = ColorMap(color_map)
@staticmethod
def resolve_text_background_xyxy(
detection_xyxy: Tuple[int, int, int, int],
text_wh: Tuple[int, int],
text_padding: int,
position: Position,
) -> Tuple[int, int, int, int]:
padded_text_wh = (text_wh[0] + 2 * text_padding, text_wh[1] + 2 * text_padding)
x1, y1, x2, y2 = detection_xyxy
center_x = (x1 + x2) // 2
center_y = (y1 + y2) // 2
if position == Position.TOP_LEFT:
return x1, y1 - padded_text_wh[1], x1 + padded_text_wh[0], y1
elif position == Position.TOP_RIGHT:
return x2 - padded_text_wh[0], y1 - padded_text_wh[1], x2, y1
elif position == Position.TOP_CENTER:
return (
center_x - padded_text_wh[0] // 2,
y1 - padded_text_wh[1],
center_x + padded_text_wh[0] // 2,
y1,
)
elif position == Position.CENTER:
return (
center_x - padded_text_wh[0] // 2,
center_y - padded_text_wh[1] // 2,
center_x + padded_text_wh[0] // 2,
center_y + padded_text_wh[1] // 2,
)
elif position == Position.BOTTOM_LEFT:
return x1, y2, x1 + padded_text_wh[0], y2 + padded_text_wh[1]
elif position == Position.BOTTOM_RIGHT:
return x2 - padded_text_wh[0], y2, x2, y2 + padded_text_wh[1]
elif position == Position.BOTTOM_CENTER:
return (
center_x - padded_text_wh[0] // 2,
y2,
center_x + padded_text_wh[0] // 2,
y2 + padded_text_wh[1],
)
def annotate(
self,
scene: np.ndarray,
detections: Detections,
labels: List[str] = None,
) -> np.ndarray:
"""
Annotates the given scene with labels based on the provided detections.
Args:
scene (np.ndarray): The image where labels will be drawn.
detections (Detections): Object detections to annotate.
labels (List[str]): Optional. Custom labels for each detection.
Returns:
np.ndarray: The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> label_annotator = sv.LabelAnnotator(text_position=sv.Position.CENTER)
>>> annotated_frame = label_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![label-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/label-annotator-example-2.png)
"""
font = cv2.FONT_HERSHEY_SIMPLEX
for detection_idx in range(len(detections)):
detection_xyxy = detections.xyxy[detection_idx].astype(int)
idx = resolve_color_idx(
detections=detections,
detection_idx=detection_idx,
color_map=self.color_map,
)
color = resolve_color(color=self.color, idx=idx)
text = (
f"{detections.class_id[detection_idx]}"
if (labels is None or len(detections) != len(labels))
else labels[detection_idx]
)
text_wh = cv2.getTextSize(
text=text,
fontFace=font,
fontScale=self.text_scale,
thickness=self.text_thickness,
)[0]
text_background_xyxy = self.resolve_text_background_xyxy(
detection_xyxy=detection_xyxy,
text_wh=text_wh,
text_padding=self.text_padding,
position=self.text_position,
)
text_x = text_background_xyxy[0] + self.text_padding
text_y = text_background_xyxy[1] + self.text_padding + text_wh[1]
cv2.rectangle(
img=scene,
pt1=(text_background_xyxy[0], text_background_xyxy[1]),
pt2=(text_background_xyxy[2], text_background_xyxy[3]),
color=color.as_bgr(),
thickness=cv2.FILLED,
)
cv2.putText(
img=scene,
text=text,
org=(text_x, text_y),
fontFace=font,
fontScale=self.text_scale,
color=self.text_color.as_rgb(),
thickness=self.text_thickness,
lineType=cv2.LINE_AA,
)
return scene
class BlurAnnotator(BaseAnnotator):
"""
A class for blurring regions in an image using provided detections.
"""
def __init__(self, kernel_size: int = 15):
"""
Args:
kernel_size (int): The size of the average pooling kernel used for blurring.
"""
self.kernel_size: int = kernel_size
def annotate(
self,
scene: np.ndarray,
detections: Detections,
) -> np.ndarray:
"""
Annotates the given scene by blurring regions based on the provided detections.
Args:
scene (np.ndarray): The image where blurring will be applied.
detections (Detections): Object detections to annotate.
Returns:
np.ndarray: The annotated image.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> blur_annotator = sv.BlurAnnotator()
>>> annotated_frame = blur_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![blur-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/blur-annotator-example-2.png)
"""
for detection_idx in range(len(detections)):
x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
roi = scene[y1:y2, x1:x2]
roi = cv2.blur(roi, (self.kernel_size, self.kernel_size))
scene[y1:y2, x1:x2] = roi
return scene
class TraceAnnotator:
"""
A class for drawing trace paths on an image based on detection coordinates.
!!! warning
This annotator utilizes the `tracker_id`. Read
[here](https://supervision.roboflow.com/trackers/) to learn how to plug
tracking into your inference pipeline.
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
position: Optional[Position] = Position.CENTER,
trace_length: int = 30,
thickness: int = 2,
color_map: str = "class",
):
"""
Args:
color (Union[Color, ColorPalette]): The color to draw the trace, can be
a single color or a color palette.
position (Optional[Position]): The position of the trace.
Defaults to `CENTER`.
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`.
color_map (str): Strategy for mapping colors to annotations.
Options are `index`, `class`, or `track`.
"""
self.color: Union[Color, ColorPalette] = color
self.position = position
self.trace = Trace(max_size=trace_length)
self.thickness = thickness
self.color_map: ColorMap = ColorMap(color_map)
def annotate(self, scene: np.ndarray, detections: Detections) -> np.ndarray:
"""
Draws trace paths on the frame based on the detection coordinates provided.
Args:
scene (np.ndarray): The image on which the traces will be drawn.
detections (Detections): The detections which include coordinates for
which the traces will be drawn.
Returns:
np.ndarray: The image with the trace paths drawn on it.
Example:
```python
>>> import supervision as sv
>>> image = ...
>>> detections = sv.Detections(...)
>>> trace_annotator = sv.TraceAnnotator()
>>> annotated_frame = trace_annotator.annotate(
... scene=image.copy(),
... detections=detections
... )
```
![trace-annotator-example](https://media.roboflow.com/
supervision-annotator-examples/trace-annotator-example.png)
"""
self.trace.put(detections)
for detection_idx in range(len(detections)):
tracker_id = int(detections.tracker_id[detection_idx])
idx = resolve_color_idx(
detections=detections,
detection_idx=detection_idx,
color_map=self.color_map,
)
color = resolve_color(color=self.color, idx=idx)
xy = self.trace.get(tracker_id=tracker_id)
if len(xy) > 1:
scene = cv2.polylines(
scene,
[xy.astype(np.int32)],
False,
color=color.as_bgr(),
thickness=self.thickness,
)
return scene
+89
View File
@@ -0,0 +1,89 @@
from enum import Enum
from typing import Optional, Union
import numpy as np
from supervision.detection.core import Detections
from supervision.draw.color import Color, ColorPalette
from supervision.geometry.core import Position
class ColorMap(Enum):
"""
Enum for annotator color mapping.
"""
INDEX = "index"
CLASS = "class"
TRACK = "track"
def resolve_color_idx(
detections: Detections, detection_idx: int, color_map: ColorMap = ColorMap.CLASS
) -> int:
if detection_idx >= len(detections):
raise ValueError(
f"Detection index {detection_idx}"
f"is out of bounds for detections of length {len(detections)}"
)
if color_map == ColorMap.INDEX:
return detection_idx
elif color_map == ColorMap.CLASS:
if detections.class_id is None:
raise ValueError(
"Could not resolve color by class because"
"Detections do not have class_id"
)
return detections.class_id[detection_idx]
elif color_map == ColorMap.TRACK:
if detections.tracker_id is None:
raise ValueError(
"Could not resolve color by track because"
"Detections do not have tracker_id"
)
return detections.tracker_id[detection_idx]
def resolve_color(color: Union[Color, ColorPalette], idx: int) -> Color:
if isinstance(color, ColorPalette):
return color.by_idx(idx)
return color
class Trace:
def __init__(
self,
max_size: Optional[int] = None,
start_frame_id: int = 0,
anchor: Position = Position.CENTER,
) -> None:
self.current_frame_id = start_frame_id
self.max_size = max_size
self.anchor = anchor
self.frame_id = np.array([], dtype=int)
self.xy = np.empty((0, 2), dtype=np.float32)
self.tracker_id = np.array([], dtype=int)
def put(self, detections: Detections) -> None:
frame_id = np.full(len(detections), self.current_frame_id, dtype=int)
self.frame_id = np.concatenate([self.frame_id, frame_id])
self.xy = np.concatenate(
[self.xy, detections.get_anchor_coordinates(self.anchor)]
)
self.tracker_id = np.concatenate([self.tracker_id, detections.tracker_id])
unique_frame_id = np.unique(self.frame_id)
if 0 < self.max_size < len(unique_frame_id):
max_allowed_frame_id = self.current_frame_id - self.max_size + 1
filtering_mask = self.frame_id >= max_allowed_frame_id
self.frame_id = self.frame_id[filtering_mask]
self.xy = self.xy[filtering_mask]
self.tracker_id = self.tracker_id[filtering_mask]
self.current_frame_id += 1
def get(self, tracker_id: int) -> np.ndarray:
return self.xy[self.tracker_id == tracker_id]
+140
View File
@@ -0,0 +1,140 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Optional, Tuple
import numpy as np
from supervision.utils.internal import deprecated
def _validate_class_ids(class_id: Any, n: int) -> None:
"""
Ensure that class_id is a 1d np.ndarray with (n, ) shape.
"""
is_valid = isinstance(class_id, np.ndarray) and class_id.shape == (n,)
if not is_valid:
raise ValueError("class_id must be 1d np.ndarray with (n, ) shape")
def _validate_confidence(confidence: Any, n: int) -> None:
"""
Ensure that confidence is a 1d np.ndarray with (n, ) shape.
"""
if confidence is not None:
is_valid = isinstance(confidence, np.ndarray) and confidence.shape == (n,)
if not is_valid:
raise ValueError("confidence must be 1d np.ndarray with (n, ) shape")
@dataclass
class Classifications:
class_id: np.ndarray
confidence: Optional[np.ndarray] = None
def __post_init__(self) -> None:
"""
Validate the classification inputs.
"""
n = len(self.class_id)
_validate_class_ids(self.class_id, n)
_validate_confidence(self.confidence, n)
@classmethod
@deprecated(
"""
This method is deprecated and removed in 0.16.0 release.
Use sv.Classifications.from_ultralytics() instead as it is more generic and
can be used for detections from any ultralytics.engine.results.Results Object
"""
)
def from_yolov8(cls, yolov8_results) -> Classifications:
"""
Creates a Classifications instance from a
[YOLOv8](https://github.com/ultralytics/ultralytics) inference result.
Args:
yolov8_results (ultralytics.yolo.engine.results.Results):
The output Results instance from YOLOv8
Returns:
Classifications: A new Classifications object.
Example:
```python
>>> import cv2
>>> from ultralytics import YOLO
>>> import supervision as sv
>>> image = cv2.imread(SOURCE_IMAGE_PATH)
>>> model = YOLO('yolov8s-cls.pt')
>>> result = model(image)[0]
>>> classifications = sv.Classifications.from_yolov8(result)
```
"""
confidence = yolov8_results.probs.data.cpu().numpy()
return cls(class_id=np.arange(confidence.shape[0]), confidence=confidence)
@classmethod
def from_ultralytics(cls, ultralytics_results) -> Classifications:
"""
Creates a Classifications instance from a
(https://github.com/ultralytics/ultralytics) inference result.
Args:
ultralytics_results (ultralytics.engine.results.Results):
The output Results instance from ultralytics model
Returns:
Classifications: A new Classifications object.
Example:
```python
>>> import cv2
>>> from ultralytics import YOLO
>>> import supervision as sv
>>> image = cv2.imread(SOURCE_IMAGE_PATH)
>>> model = YOLO('yolov8n-cls.pt')
>>> model = YOLO('yolov8s-cls.pt')
>>> result = model(image)[0]
>>> classifications = sv.Classifications.from_ultralytics(result)
```
"""
confidence = ultralytics_results.probs.data.cpu().numpy()
return cls(class_id=np.arange(confidence.shape[0]), confidence=confidence)
def get_top_k(self, k: int) -> Tuple[np.ndarray, np.ndarray]:
"""
Retrieve the top k class IDs and confidences,
ordered in descending order by confidence.
Args:
k (int): The number of top class IDs and confidences to retrieve.
Returns:
Tuple[np.ndarray, np.ndarray]: A tuple containing
the top k class IDs and confidences.
Example:
```python
>>> import supervision as sv
>>> classifications = sv.Classifications(...)
>>> classifications.get_top_k(1)
(array([1]), array([0.9]))
```
"""
if self.confidence is None:
raise ValueError("top_k could not be calculated, confidence is None")
order = np.argsort(self.confidence)[::-1]
top_k_order = order[:k]
top_k_class_id = self.class_id[top_k_order]
top_k_confidence = self.confidence[top_k_order]
return top_k_class_id, top_k_confidence
+607 -55
View File
@@ -1,75 +1,201 @@
from __future__ import annotations
import os
from abc import ABC, abstractmethod
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from typing import Dict, Iterator, List, Optional, Tuple
import cv2
import numpy as np
from supervision.classification.core import Classifications
from supervision.dataset.formats.coco import (
load_coco_annotations,
save_coco_annotations,
)
from supervision.dataset.formats.pascal_voc import (
detections_to_pascal_voc,
load_pascal_voc_annotations,
)
from supervision.dataset.formats.yolo import (
load_yolo_annotations,
save_data_yaml,
save_yolo_annotations,
)
from supervision.dataset.utils import (
build_class_index_mapping,
map_detections_class_id,
merge_class_lists,
save_dataset_images,
train_test_split,
)
from supervision.detection.core import Detections
from supervision.file import list_files_with_extensions
@dataclass
class Dataset:
class BaseDataset(ABC):
@abstractmethod
def __len__(self) -> int:
pass
@abstractmethod
def split(
self, split_ratio=0.8, random_state=None, shuffle: bool = True
) -> Tuple[BaseDataset, BaseDataset]:
pass
@dataclass
class DetectionDataset(BaseDataset):
"""
Dataclass containing information about the dataset.
Dataclass containing information about object detection dataset.
Attributes:
classes (List[str]): List containing dataset class names.
images (Dict[str, np.ndarray]): Dictionary mapping image name to image.
annotations (Dict[str, Detections]): Dictionary mapping image name to annotations.
annotations (Dict[str, Detections]): Dictionary mapping
image name to annotations.
"""
classes: List[str]
images: Dict[str, np.ndarray]
annotations: Dict[str, Detections]
def __len__(self) -> int:
"""
Return the number of images in the dataset.
Returns:
int: The number of images.
"""
return len(self.images)
def __iter__(self) -> Iterator[Tuple[str, np.ndarray, Detections]]:
"""
Iterate over the images and annotations in the dataset.
Yields:
Iterator[Tuple[str, np.ndarray, Detections]]:
An iterator that yields tuples containing the image name,
the image data, and its corresponding annotation.
"""
for image_name, image in self.images.items():
yield image_name, image, self.annotations.get(image_name, None)
def __eq__(self, other):
if not isinstance(other, DetectionDataset):
return False
if set(self.classes) != set(other.classes):
return False
for key in self.images:
if not np.array_equal(self.images[key], other.images[key]):
return False
if not self.annotations[key] == other.annotations[key]:
return False
return True
def split(
self, split_ratio=0.8, random_state=None, shuffle: bool = True
) -> Tuple[DetectionDataset, DetectionDataset]:
"""
Splits the dataset into two parts (training and testing)
using the provided split_ratio.
Args:
split_ratio (float, optional): The ratio of the training
set to the entire dataset.
random_state (int, optional): The seed for the random number generator.
This is used for reproducibility.
shuffle (bool, optional): Whether to shuffle the data before splitting.
Returns:
Tuple[DetectionDataset, DetectionDataset]: A tuple containing
the training and testing datasets.
Example:
```python
>>> import supervision as sv
>>> ds = sv.DetectionDataset(...)
>>> train_ds, test_ds = ds.split(split_ratio=0.7,
... random_state=42, shuffle=True)
>>> len(train_ds), len(test_ds)
(700, 300)
```
"""
image_names = list(self.images.keys())
train_names, test_names = train_test_split(
data=image_names,
train_ratio=split_ratio,
random_state=random_state,
shuffle=shuffle,
)
train_dataset = DetectionDataset(
classes=self.classes,
images={name: self.images[name] for name in train_names},
annotations={name: self.annotations[name] for name in train_names},
)
test_dataset = DetectionDataset(
classes=self.classes,
images={name: self.images[name] for name in test_names},
annotations={name: self.annotations[name] for name in test_names},
)
return train_dataset, test_dataset
def as_pascal_voc(
self,
images_directory_path: Optional[str] = None,
annotations_directory_path: Optional[str] = None,
min_image_area_percentage: float = 0.0,
max_image_area_percentage: float = 1.0,
approximation_percentage: float = 0.75,
approximation_percentage: float = 0.0,
) -> None:
"""
Exports the dataset to PASCAL VOC format. This method saves the images and their corresponding annotations in
PASCAL VOC format, which consists of XML files. The method allows filtering the detections based on their area
percentage.
Exports the dataset to PASCAL VOC format. This method saves the images
and their corresponding annotations in PASCAL VOC format.
Args:
images_directory_path (Optional[str]): The path to the directory where the images should be saved.
images_directory_path (Optional[str]): The path to the directory
where the images should be saved.
If not provided, images will not be saved.
annotations_directory_path (Optional[str]): The path to the directory where the annotations in
PASCAL VOC format should be saved. If not provided, annotations will not be saved.
min_image_area_percentage (float): The minimum percentage of detection area relative to
annotations_directory_path (Optional[str]): The path to
the directory where the annotations in
PASCAL VOC format should be saved. If not provided,
annotations will not be saved.
min_image_area_percentage (float): The minimum percentage of
detection area relative to
the image area for a detection to be included.
max_image_area_percentage (float): The maximum percentage of detection area relative to
Argument is used only for segmentation datasets.
max_image_area_percentage (float): The maximum percentage
of detection area relative to
the image area for a detection to be included.
approximation_percentage (float): The percentage of polygon points to be removed from the input polygon, in the range [0, 1).
Argument is used only for segmentation datasets.
approximation_percentage (float): The percentage of
polygon points to be removed from the input polygon,
in the range [0, 1). Argument is used only for segmentation datasets.
"""
if images_directory_path:
images_path = Path(images_directory_path)
images_path.mkdir(parents=True, exist_ok=True)
save_dataset_images(
images_directory_path=images_directory_path, images=self.images
)
if annotations_directory_path:
annotations_path = Path(annotations_directory_path)
annotations_path.mkdir(parents=True, exist_ok=True)
Path(annotations_directory_path).mkdir(parents=True, exist_ok=True)
for image_name, image in self.images.items():
detections = self.annotations[image_name]
if images_directory_path:
cv2.imwrite(str(images_path / image_name), image)
for image_path, image in self.images.items():
detections = self.annotations[image_path]
if annotations_directory_path:
annotation_name = Path(image_name).stem
annotation_name = Path(image_path).stem
annotations_path = os.path.join(
annotations_directory_path, f"{annotation_name}.xml"
)
image_name = Path(image_path).name
pascal_voc_xml = detections_to_pascal_voc(
detections=detections,
classes=self.classes,
@@ -80,49 +206,475 @@ class Dataset:
approximation_percentage=approximation_percentage,
)
with open(annotations_path / f"{annotation_name}.xml", "w") as f:
with open(annotations_path, "w") as f:
f.write(pascal_voc_xml)
@classmethod
def from_pascal_voc(
cls, images_directory_path: str, annotations_directory_path: str
) -> Dataset:
cls,
images_directory_path: str,
annotations_directory_path: str,
force_masks: bool = False,
) -> DetectionDataset:
"""
Creates a Dataset instance from PASCAL VOC formatted data.
Args:
images_directory_path (str): The path to the directory containing the images.
annotations_directory_path (str): The path to the directory containing the PASCAL VOC XML annotations.
images_directory_path (str): Path to the directory containing the images.
annotations_directory_path (str): Path to the directory
containing the PASCAL VOC XML annotations.
force_masks (bool, optional): If True, forces masks to
be loaded for all annotations, regardless of whether they are present.
Returns:
Dataset: A Dataset instance containing the loaded images and annotations.
DetectionDataset: A DetectionDataset instance containing
the loaded images and annotations.
Example:
```python
>>> import roboflow
>>> from roboflow import Roboflow
>>> import supervision as sv
>>> roboflow.login()
>>> rf = Roboflow()
>>> project = rf.workspace(WORKSPACE_ID).project(PROJECT_ID)
>>> dataset = project.version(PROJECT_VERSION).download("voc")
>>> ds = sv.DetectionDataset.from_pascal_voc(
... images_directory_path=f"{dataset.location}/train/images",
... annotations_directory_path=f"{dataset.location}/train/labels"
... )
>>> ds.classes
['dog', 'person']
```
"""
image_paths = list_files_with_extensions(
directory=images_directory_path, extensions=["jpg", "jpeg", "png"]
)
annotation_paths = list_files_with_extensions(
directory=annotations_directory_path, extensions=["xml"]
classes, images, annotations = load_pascal_voc_annotations(
images_directory_path=images_directory_path,
annotations_directory_path=annotations_directory_path,
force_masks=force_masks,
)
raw_annotations: List[Tuple[str, Detections, List[str]]] = [
load_pascal_voc_annotations(annotation_path=str(annotation_path))
for annotation_path in annotation_paths
]
return DetectionDataset(classes=classes, images=images, annotations=annotations)
classes = []
for annotation in raw_annotations:
classes.extend(annotation[2])
classes = list(set(classes))
@classmethod
def from_yolo(
cls,
images_directory_path: str,
annotations_directory_path: str,
data_yaml_path: str,
force_masks: bool = False,
) -> DetectionDataset:
"""
Creates a Dataset instance from YOLO formatted data.
for annotation in raw_annotations:
class_id = [classes.index(class_name) for class_name in annotation[2]]
annotation[1].class_id = np.array(class_id)
Args:
images_directory_path (str): The path to the
directory containing the images.
annotations_directory_path (str): The path to the directory
containing the YOLO annotation files.
data_yaml_path (str): The path to the data
YAML file containing class information.
force_masks (bool, optional): If True, forces
masks to be loaded for all annotations,
regardless of whether they are present.
images = {
image_path.name: cv2.imread(str(image_path)) for image_path in image_paths
}
Returns:
DetectionDataset: A DetectionDataset instance
containing the loaded images and annotations.
annotations = {
image_name: detections for image_name, detections, _ in raw_annotations
}
return Dataset(classes=classes, images=images, annotations=annotations)
Example:
```python
>>> import roboflow
>>> from roboflow import Roboflow
>>> import supervision as sv
>>> roboflow.login()
>>> rf = Roboflow()
>>> project = rf.workspace(WORKSPACE_ID).project(PROJECT_ID)
>>> dataset = project.version(PROJECT_VERSION).download("yolov5")
>>> ds = sv.DetectionDataset.from_yolo(
... images_directory_path=f"{dataset.location}/train/images",
... annotations_directory_path=f"{dataset.location}/train/labels",
... data_yaml_path=f"{dataset.location}/data.yaml"
... )
>>> ds.classes
['dog', 'person']
```
"""
classes, images, annotations = load_yolo_annotations(
images_directory_path=images_directory_path,
annotations_directory_path=annotations_directory_path,
data_yaml_path=data_yaml_path,
force_masks=force_masks,
)
return DetectionDataset(classes=classes, images=images, annotations=annotations)
def as_yolo(
self,
images_directory_path: Optional[str] = None,
annotations_directory_path: Optional[str] = None,
data_yaml_path: Optional[str] = None,
min_image_area_percentage: float = 0.0,
max_image_area_percentage: float = 1.0,
approximation_percentage: float = 0.0,
) -> None:
"""
Exports the dataset to YOLO format. This method saves the
images and their corresponding annotations in YOLO format.
Args:
images_directory_path (Optional[str]): The path to the
directory where the images should be saved.
If not provided, images will not be saved.
annotations_directory_path (Optional[str]): The path to the
directory where the annotations in
YOLO format should be saved. If not provided,
annotations will not be saved.
data_yaml_path (Optional[str]): The path where the data.yaml
file should be saved.
If not provided, the file will not be saved.
min_image_area_percentage (float): The minimum percentage of
detection area relative to
the image area for a detection to be included.
Argument is used only for segmentation datasets.
max_image_area_percentage (float): The maximum percentage
of detection area relative to
the image area for a detection to be included.
Argument is used only for segmentation datasets.
approximation_percentage (float): The percentage of polygon points to
be removed from the input polygon, in the range [0, 1).
This is useful for simplifying the annotations.
Argument is used only for segmentation datasets.
"""
if images_directory_path is not None:
save_dataset_images(
images_directory_path=images_directory_path, images=self.images
)
if annotations_directory_path is not None:
save_yolo_annotations(
annotations_directory_path=annotations_directory_path,
images=self.images,
annotations=self.annotations,
min_image_area_percentage=min_image_area_percentage,
max_image_area_percentage=max_image_area_percentage,
approximation_percentage=approximation_percentage,
)
if data_yaml_path is not None:
save_data_yaml(data_yaml_path=data_yaml_path, classes=self.classes)
@classmethod
def from_coco(
cls,
images_directory_path: str,
annotations_path: str,
force_masks: bool = False,
) -> DetectionDataset:
"""
Creates a Dataset instance from COCO formatted data.
Args:
images_directory_path (str): The path to the
directory containing the images.
annotations_path (str): The path to the json annotation files.
force_masks (bool, optional): If True,
forces masks to be loaded for all annotations,
regardless of whether they are present.
Returns:
DetectionDataset: A DetectionDataset instance containing
the loaded images and annotations.
Example:
```python
>>> import roboflow
>>> from roboflow import Roboflow
>>> import supervision as sv
>>> roboflow.login()
>>> rf = Roboflow()
>>> project = rf.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",
... annotations_path=f"{dataset.location}/train/_annotations.coco.json",
... )
>>> ds.classes
['dog', 'person']
```
"""
classes, images, annotations = load_coco_annotations(
images_directory_path=images_directory_path,
annotations_path=annotations_path,
force_masks=force_masks,
)
return DetectionDataset(classes=classes, images=images, annotations=annotations)
def as_coco(
self,
images_directory_path: Optional[str] = None,
annotations_path: Optional[str] = None,
min_image_area_percentage: float = 0.0,
max_image_area_percentage: float = 1.0,
approximation_percentage: float = 0.0,
) -> None:
"""
Exports the dataset to COCO format. This method saves the
images and their corresponding annotations in COCO format.
Args:
images_directory_path (Optional[str]): The path to the directory
where the images should be saved.
If not provided, images will not be saved.
annotations_path (Optional[str]): The path to COCO annotation file.
min_image_area_percentage (float): The minimum percentage of
detection area relative to
the image area for a detection to be included.
Argument is used only for segmentation datasets.
max_image_area_percentage (float): The maximum percentage of
detection area relative to
the image area for a detection to be included.
Argument is used only for segmentation datasets.
approximation_percentage (float): The percentage of polygon points
to be removed from the input polygon,
in the range [0, 1). This is useful for simplifying the annotations.
Argument is used only for segmentation datasets.
"""
if images_directory_path is not None:
save_dataset_images(
images_directory_path=images_directory_path, images=self.images
)
if annotations_path is not None:
save_coco_annotations(
annotation_path=annotations_path,
images=self.images,
annotations=self.annotations,
classes=self.classes,
min_image_area_percentage=min_image_area_percentage,
max_image_area_percentage=max_image_area_percentage,
approximation_percentage=approximation_percentage,
)
@classmethod
def merge(cls, dataset_list: List[DetectionDataset]) -> DetectionDataset:
"""
Merge a list of `DetectionDataset` objects into a single
`DetectionDataset` object.
This method takes a list of `DetectionDataset` objects and combines
their respective fields (`classes`, `images`,
`annotations`) into a single `DetectionDataset` object.
Args:
dataset_list (List[DetectionDataset]): A list of `DetectionDataset`
objects to merge.
Returns:
(DetectionDataset): A single `DetectionDataset` object containing
the merged data from the input list.
Example:
```python
>>> import supervision as sv
>>> 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_merged = sv.DetectionDataset.merge([ds_1, ds_2])
>>> len(ds_merged)
300
>>> ds_merged.classes
['cat', 'dog', 'person']
```
"""
merged_images, merged_annotations = {}, {}
class_lists = [dataset.classes for dataset in dataset_list]
merged_classes = merge_class_lists(class_lists=class_lists)
for dataset in dataset_list:
class_index_mapping = build_class_index_mapping(
source_classes=dataset.classes, target_classes=merged_classes
)
for image_name, image, detections in dataset:
if image_name in merged_annotations:
raise ValueError(
f"Image name {image_name} is not unique across datasets."
)
merged_images[image_name] = image
merged_annotations[image_name] = map_detections_class_id(
source_to_target_mapping=class_index_mapping,
detections=detections,
)
return cls(
classes=merged_classes, images=merged_images, annotations=merged_annotations
)
@dataclass
class ClassificationDataset(BaseDataset):
"""
Dataclass containing information about a classification dataset.
Attributes:
classes (List[str]): List containing dataset class names.
images (Dict[str, np.ndarray]): Dictionary mapping image name to image.
annotations (Dict[str, Detections]): Dictionary mapping
image name to annotations.
"""
classes: List[str]
images: Dict[str, np.ndarray]
annotations: Dict[str, Classifications]
def __len__(self) -> int:
return len(self.images)
def split(
self, split_ratio=0.8, random_state=None, shuffle: bool = True
) -> Tuple[ClassificationDataset, ClassificationDataset]:
"""
Splits the dataset into two parts (training and testing)
using the provided split_ratio.
Args:
split_ratio (float, optional): The ratio of the training
set to the entire dataset.
random_state (int, optional): The seed for the
random number generator.
This is used for reproducibility.
shuffle (bool, optional): Whether to shuffle the data before splitting.
Returns:
Tuple[ClassificationDataset, ClassificationDataset]: A tuple containing
the training and testing datasets.
Example:
```python
>>> import supervision as sv
>>> cd = sv.ClassificationDataset(...)
>>> train_cd,test_cd = cd.split(split_ratio=0.7,
... random_state=42,shuffle=True)
>>> len(train_cd), len(test_cd)
(700, 300)
```
"""
image_names = list(self.images.keys())
train_names, test_names = train_test_split(
data=image_names,
train_ratio=split_ratio,
random_state=random_state,
shuffle=shuffle,
)
train_dataset = ClassificationDataset(
classes=self.classes,
images={name: self.images[name] for name in train_names},
annotations={name: self.annotations[name] for name in train_names},
)
test_dataset = ClassificationDataset(
classes=self.classes,
images={name: self.images[name] for name in test_names},
annotations={name: self.annotations[name] for name in test_names},
)
return train_dataset, test_dataset
def as_folder_structure(self, root_directory_path: str) -> None:
"""
Saves the dataset as a multi-class folder structure.
Args:
root_directory_path (str): The path to the directory
where the dataset will be saved.
"""
os.makedirs(root_directory_path, exist_ok=True)
for class_name in self.classes:
os.makedirs(os.path.join(root_directory_path, class_name), exist_ok=True)
for image_path in self.images:
classification = self.annotations[image_path]
image = self.images[image_path]
image_name = Path(image_path).name
class_id = (
classification.class_id[0]
if classification.confidence is None
else classification.get_top_k(1)[0][0]
)
class_name = self.classes[class_id]
image_path = os.path.join(root_directory_path, class_name, image_name)
cv2.imwrite(image_path, image)
@classmethod
def from_folder_structure(cls, root_directory_path: str) -> ClassificationDataset:
"""
Load data from a multiclass folder structure into a ClassificationDataset.
Args:
root_directory_path (str): The path to the dataset directory.
Returns:
ClassificationDataset: The dataset.
Example:
```python
>>> import roboflow
>>> from roboflow import Roboflow
>>> import supervision as sv
>>> roboflow.login()
>>> rf = Roboflow()
>>> project = rf.workspace(WORKSPACE_ID).project(PROJECT_ID)
>>> dataset = project.version(PROJECT_VERSION).download("folder")
>>> cd = sv.ClassificationDataset.from_folder_structure(
... root_directory_path=f"{dataset.location}/train"
... )
```
"""
classes = os.listdir(root_directory_path)
classes = sorted(set(classes))
images = {}
annotations = {}
for class_name in classes:
class_id = classes.index(class_name)
for image in os.listdir(os.path.join(root_directory_path, class_name)):
image_path = str(os.path.join(root_directory_path, class_name, image))
images[image_path] = cv2.imread(image_path)
annotations[image_path] = Classifications(
class_id=np.array([class_id]),
)
return cls(
classes=classes,
images=images,
annotations=annotations,
)
+237
View File
@@ -0,0 +1,237 @@
import os
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Tuple
import cv2
import numpy as np
from supervision.dataset.utils import (
approximate_mask_with_polygons,
map_detections_class_id,
)
from supervision.detection.core import Detections
from supervision.detection.utils import polygon_to_mask
from supervision.utils.file import read_json_file, save_json_file
def coco_categories_to_classes(coco_categories: List[dict]) -> List[str]:
return [
category["name"]
for category in sorted(coco_categories, key=lambda category: category["id"])
]
def build_coco_class_index_mapping(
coco_categories: List[dict], target_classes: List[str]
) -> Dict[int, int]:
source_class_to_index = {
category["name"]: category["id"] for category in coco_categories
}
return {
source_class_to_index[target_class_name]: target_class_index
for target_class_index, target_class_name in enumerate(target_classes)
}
def classes_to_coco_categories(classes: List[str]) -> List[dict]:
return [
{
"id": class_id,
"name": class_name,
"supercategory": "common-objects",
}
for class_id, class_name in enumerate(classes)
]
def group_coco_annotations_by_image_id(
coco_annotations: List[dict],
) -> Dict[int, List[dict]]:
annotations = {}
for annotation in coco_annotations:
image_id = annotation["image_id"]
if image_id not in annotations:
annotations[image_id] = []
annotations[image_id].append(annotation)
return annotations
def _polygons_to_masks(
polygons: List[np.ndarray], resolution_wh: Tuple[int, int]
) -> np.ndarray:
return np.array(
[
polygon_to_mask(polygon=polygon, resolution_wh=resolution_wh)
for polygon in polygons
],
dtype=bool,
)
def coco_annotations_to_detections(
image_annotations: List[dict], resolution_wh: Tuple[int, int], with_masks: bool
) -> Detections:
if not image_annotations:
return Detections.empty()
class_ids = [
image_annotation["category_id"] for image_annotation in image_annotations
]
xyxy = [image_annotation["bbox"] for image_annotation in image_annotations]
xyxy = np.asarray(xyxy)
xyxy[:, 2:4] += xyxy[:, 0:2]
if with_masks:
polygons = [
np.reshape(
np.asarray(image_annotation["segmentation"], dtype=np.int32), (-1, 2)
)
for image_annotation in image_annotations
]
mask = _polygons_to_masks(polygons=polygons, resolution_wh=resolution_wh)
return Detections(
class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=mask
)
return Detections(xyxy=xyxy, class_id=np.asarray(class_ids, dtype=int))
def detections_to_coco_annotations(
detections: Detections,
image_id: int,
annotation_id: int,
min_image_area_percentage: float = 0.0,
max_image_area_percentage: float = 1.0,
approximation_percentage: float = 0.75,
) -> Tuple[List[Dict], int]:
coco_annotations = []
for xyxy, mask, _, class_id, _ in detections:
box_width, box_height = xyxy[2] - xyxy[0], xyxy[3] - xyxy[1]
polygon = []
if mask is not None:
polygon = list(
approximate_mask_with_polygons(
mask=mask,
min_image_area_percentage=min_image_area_percentage,
max_image_area_percentage=max_image_area_percentage,
approximation_percentage=approximation_percentage,
)[0].flatten()
)
coco_annotation = {
"id": annotation_id,
"image_id": image_id,
"category_id": int(class_id),
"bbox": [xyxy[0], xyxy[1], box_width, box_height],
"area": box_width * box_height,
"segmentation": [polygon] if polygon else [],
"iscrowd": 0,
}
coco_annotations.append(coco_annotation)
annotation_id += 1
return coco_annotations, annotation_id
def load_coco_annotations(
images_directory_path: str,
annotations_path: str,
force_masks: bool = False,
) -> Tuple[List[str], Dict[str, np.ndarray], Dict[str, Detections]]:
coco_data = read_json_file(file_path=annotations_path)
classes = coco_categories_to_classes(coco_categories=coco_data["categories"])
class_index_mapping = build_coco_class_index_mapping(
coco_categories=coco_data["categories"], target_classes=classes
)
coco_images = coco_data["images"]
coco_annotations_groups = group_coco_annotations_by_image_id(
coco_annotations=coco_data["annotations"]
)
images = {}
annotations = {}
for coco_image in coco_images:
image_name, image_width, image_height = (
coco_image["file_name"],
coco_image["width"],
coco_image["height"],
)
image_annotations = coco_annotations_groups.get(coco_image["id"], [])
image_path = os.path.join(images_directory_path, image_name)
image = cv2.imread(image_path)
annotation = coco_annotations_to_detections(
image_annotations=image_annotations,
resolution_wh=(image_width, image_height),
with_masks=force_masks,
)
annotation = map_detections_class_id(
source_to_target_mapping=class_index_mapping,
detections=annotation,
)
images[image_path] = image
annotations[image_path] = annotation
return classes, images, annotations
def save_coco_annotations(
annotation_path: str,
images: Dict[str, np.ndarray],
annotations: Dict[str, Detections],
classes: List[str],
min_image_area_percentage: float = 0.0,
max_image_area_percentage: float = 1.0,
approximation_percentage: float = 0.75,
) -> None:
Path(annotation_path).parent.mkdir(parents=True, exist_ok=True)
info = {}
licenses = [
{
"id": 1,
"url": "https://creativecommons.org/licenses/by/4.0/",
"name": "CC BY 4.0",
}
]
coco_annotations = []
coco_images = []
coco_categories = classes_to_coco_categories(classes=classes)
image_id, annotation_id = 1, 1
for image_path, image in images.items():
image_height, image_width, _ = image.shape
image_name = f"{Path(image_path).stem}{Path(image_path).suffix}"
coco_image = {
"id": image_id,
"license": 1,
"file_name": image_name,
"height": image_height,
"width": image_width,
"date_captured": datetime.now().strftime("%m/%d/%Y,%H:%M:%S"),
}
coco_images.append(coco_image)
detections = annotations[image_path]
coco_annotation, annotation_id = detections_to_coco_annotations(
detections=detections,
image_id=image_id,
annotation_id=annotation_id,
min_image_area_percentage=min_image_area_percentage,
max_image_area_percentage=max_image_area_percentage,
approximation_percentage=approximation_percentage,
)
coco_annotations.extend(coco_annotation)
image_id += 1
annotation_dict = {
"info": info,
"licenses": licenses,
"categories": coco_categories,
"images": coco_images,
"annotations": coco_annotations,
}
save_json_file(annotation_dict, file_path=annotation_path)
+156 -43
View File
@@ -1,16 +1,16 @@
from typing import List, Optional, Tuple
import os
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from xml.dom.minidom import parseString
from xml.etree.ElementTree import Element, SubElement, parse, tostring
import cv2
import numpy as np
from supervision.dataset.utils import approximate_mask_with_polygons
from supervision.detection.core import Detections
from supervision.detection.utils import (
approximate_polygon,
filter_polygons_by_area,
mask_to_polygons,
polygon_to_xyxy,
)
from supervision.detection.utils import polygon_to_mask, polygon_to_xyxy
from supervision.utils.file import list_files_with_extensions
def object_to_pascal_voc(
@@ -21,6 +21,9 @@ def object_to_pascal_voc(
object_name = SubElement(root, "name")
object_name.text = name
# https://github.com/roboflow/supervision/issues/144
xyxy += 1
bndbox = SubElement(root, "bndbox")
xmin = SubElement(bndbox, "xmin")
xmin.text = str(int(xyxy[0]))
@@ -32,6 +35,8 @@ def object_to_pascal_voc(
ymax.text = str(int(xyxy[3]))
if polygon is not None:
# https://github.com/roboflow/supervision/issues/144
polygon += 1
object_polygon = SubElement(root, "polygon")
for index, point in enumerate(polygon, start=1):
x_coordinate, y_coordinate = point
@@ -56,20 +61,23 @@ def detections_to_pascal_voc(
Converts Detections object to Pascal VOC XML format.
Args:
detections (Detections): A Detections object containing bounding boxes, class ids, and other relevant information.
classes (List[str]): A list of class names corresponding to the class ids in the Detections object.
detections (Detections): A Detections object containing bounding boxes,
class ids, and other relevant information.
classes (List[str]): A list of class names corresponding to the
class ids in the Detections object.
filename (str): The name of the image file associated with the detections.
image_shape (Tuple[int, int, int]): The shape of the image file associated with the detections.
min_image_area_percentage (float): Minimum detection area relative to area of image associated with it.
max_image_area_percentage (float): Maximum detection area relative to area of image associated with it.
approximation_percentage (float): The percentage of polygon points to be removed from the input polygon, in the range [0, 1).
image_shape (Tuple[int, int, int]): The shape of the image
file associated with the detections.
min_image_area_percentage (float): Minimum detection area
relative to area of image associated with it.
max_image_area_percentage (float): Maximum detection area
relative to area of image associated with it.
approximation_percentage (float): The percentage of
polygon points to be removed from the input polygon, in the range [0, 1).
Returns:
str: An XML string in Pascal VOC format representing the detections.
"""
height, width, depth = image_shape
image_area = height * width
minimum_detection_area = min_image_area_percentage * image_area
maximum_detection_area = max_image_area_percentage * image_area
# Create root element
annotation = Element("annotation")
@@ -104,24 +112,16 @@ def detections_to_pascal_voc(
for xyxy, mask, _, class_id, _ in detections:
name = classes[class_id]
if mask is not None:
polygons = mask_to_polygons(mask=mask)
if len(polygons) == 1:
polygons = filter_polygons_by_area(
polygons=polygons, min_area=None, max_area=maximum_detection_area
)
else:
polygons = filter_polygons_by_area(
polygons=polygons,
min_area=minimum_detection_area,
max_area=maximum_detection_area,
)
polygons = approximate_mask_with_polygons(
mask=mask,
min_image_area_percentage=min_image_area_percentage,
max_image_area_percentage=max_image_area_percentage,
approximation_percentage=approximation_percentage,
)
for polygon in polygons:
approx_polygon = approximate_polygon(
polygon=polygon, percentage=approximation_percentage
)
xyxy = polygon_to_xyxy(polygon=approx_polygon)
xyxy = polygon_to_xyxy(polygon=polygon)
next_object = object_to_pascal_voc(
xyxy=xyxy, name=name, polygon=approx_polygon
xyxy=xyxy, name=name, polygon=polygon
)
annotation.append(next_object)
else:
@@ -135,24 +135,101 @@ def detections_to_pascal_voc(
def load_pascal_voc_annotations(
annotation_path: str,
) -> Tuple[str, Detections, List[str]]:
images_directory_path: str,
annotations_directory_path: str,
force_masks: bool = False,
) -> Tuple[List[str], Dict[str, np.ndarray], Dict[str, Detections]]:
"""
Loads PASCAL VOC XML annotations and returns the image name, a Detections instance, and a list of class names.
Loads PASCAL VOC XML annotations and returns the image name,
a Detections instance, and a list of class names.
Args:
annotation_path (str): The path to the PASCAL VOC XML annotations file.
images_directory_path (str): The path to the directory containing the images.
annotations_directory_path (str): The path to the directory containing the
PASCAL VOC annotation files.
force_masks (bool, optional): If True, forces masks to be loaded for all
annotations, regardless of whether they are present.
Returns:
Tuple[str, Detections, List[str]]: A tuple containing the image name, a Detections instance, and a list of class names of objects in the detections.
Tuple[List[str], Dict[str, np.ndarray], Dict[str, Detections]]: A tuple
containing a list of class names,
a dictionary with image names as keys and
images as values, and a dictionary with image names as
keys and corresponding Detections instances as values.
"""
tree = parse(annotation_path)
root = tree.getroot()
image_name = root.find("filename").text
image_paths = list_files_with_extensions(
directory=images_directory_path, extensions=["jpg", "jpeg", "png"]
)
classes = []
images = {}
annotations = {}
for image_path in image_paths:
image_name = Path(image_path).stem
image_path = str(image_path)
image = cv2.imread(image_path)
annotation_path = os.path.join(annotations_directory_path, f"{image_name}.xml")
if not os.path.exists(annotation_path):
images[image_path] = image
annotations[image_path] = Detections.empty()
continue
tree = parse(annotation_path)
root = tree.getroot()
resolution_wh = (image.shape[1], image.shape[0])
annotation, classes = detections_from_xml_obj(
root, classes, resolution_wh, force_masks
)
images[image_path] = image
annotations[image_path] = annotation
return classes, images, annotations
def detections_from_xml_obj(
root: Element, classes: List[str], resolution_wh, force_masks: bool = False
) -> Tuple[Detections, List[str]]:
"""
Converts an XML object in Pascal VOC format to a Detections object.
Expected XML format:
<annotation>
...
<object>
<name>dog</name>
<bndbox>
<xmin>48</xmin>
<ymin>240</ymin>
<xmax>195</xmax>
<ymax>371</ymax>
</bndbox>
<polygon>
<x1>48</x1>
<y1>240</y1>
<x2>195</x2>
<y2>240</y2>
<x3>195</x3>
<y3>371</y3>
<x4>48</x4>
<y4>371</y4>
</polygon>
</object>
</annotation>
Returns:
Tuple[Detections, List[str]]: A tuple containing a Detections object and an
updated list of class names, extended with the class names
from the XML object.
"""
xyxy = []
class_names = []
masks = []
with_masks = False
extended_classes = classes[:]
for obj in root.findall("object"):
class_name = obj.find("name").text
class_names.append(class_name)
@@ -165,7 +242,43 @@ def load_pascal_voc_annotations(
xyxy.append([x1, y1, x2, y2])
xyxy = np.array(xyxy)
detections = Detections(xyxy=xyxy)
with_masks = obj.find("polygon") is not None
with_masks = force_masks if force_masks else with_masks
return image_name, detections, class_names
for polygon in obj.findall("polygon"):
polygon = parse_polygon_points(polygon)
# https://github.com/roboflow/supervision/issues/144
polygon -= 1
mask_from_polygon = polygon_to_mask(
polygon=polygon,
resolution_wh=resolution_wh,
)
masks.append(mask_from_polygon)
xyxy = np.array(xyxy) if len(xyxy) > 0 else np.empty((0, 4))
# https://github.com/roboflow/supervision/issues/144
xyxy -= 1
for k in set(class_names):
if k not in extended_classes:
extended_classes.append(k)
class_id = np.array(
[extended_classes.index(class_name) for class_name in class_names]
)
if with_masks:
annotation = Detections(
xyxy=xyxy, mask=np.array(masks).astype(bool), class_id=class_id
)
else:
annotation = Detections(xyxy=xyxy, class_id=class_id)
return annotation, extended_classes
def parse_polygon_points(polygon: Element) -> np.ndarray:
coordinates = [int(coord.text) for coord in polygon.findall(".//*")]
return np.array(
[(coordinates[i], coordinates[i + 1]) for i in range(0, len(coordinates), 2)]
)
+251
View File
@@ -0,0 +1,251 @@
import os
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import cv2
import numpy as np
from supervision.dataset.utils import approximate_mask_with_polygons
from supervision.detection.core import Detections
from supervision.detection.utils import polygon_to_mask, polygon_to_xyxy
from supervision.utils.file import (
list_files_with_extensions,
read_txt_file,
read_yaml_file,
save_text_file,
save_yaml_file,
)
def _parse_box(values: List[str]) -> np.ndarray:
x_center, y_center, width, height = values
return np.array(
[
float(x_center) - float(width) / 2,
float(y_center) - float(height) / 2,
float(x_center) + float(width) / 2,
float(y_center) + float(height) / 2,
],
dtype=np.float32,
)
def _box_to_polygon(box: np.ndarray) -> np.ndarray:
return np.array(
[[box[0], box[1]], [box[2], box[1]], [box[2], box[3]], [box[0], box[3]]]
)
def _parse_polygon(values: List[str]) -> np.ndarray:
return np.array(values, dtype=np.float32).reshape(-1, 2)
def _polygons_to_masks(
polygons: List[np.ndarray], resolution_wh: Tuple[int, int]
) -> np.ndarray:
return np.array(
[
polygon_to_mask(polygon=polygon, resolution_wh=resolution_wh)
for polygon in polygons
],
dtype=bool,
)
def _with_mask(lines: List[str]) -> bool:
return any([len(line.split()) > 5 for line in lines])
def _extract_class_names(file_path: str) -> List[str]:
data = read_yaml_file(file_path=file_path)
names = data["names"]
if isinstance(names, dict):
names = [names[key] for key in sorted(names.keys())]
return names
def _image_name_to_annotation_name(image_name: str) -> str:
base_name, _ = os.path.splitext(image_name)
return base_name + ".txt"
def yolo_annotations_to_detections(
lines: List[str], resolution_wh: Tuple[int, int], with_masks: bool
) -> Detections:
if len(lines) == 0:
return Detections.empty()
class_id, relative_xyxy, relative_polygon = [], [], []
w, h = resolution_wh
for line in lines:
values = line.split()
class_id.append(int(values[0]))
if len(values) == 5:
box = _parse_box(values=values[1:])
relative_xyxy.append(box)
if with_masks:
relative_polygon.append(_box_to_polygon(box=box))
elif len(values) > 5:
polygon = _parse_polygon(values=values[1:])
relative_xyxy.append(polygon_to_xyxy(polygon=polygon))
if with_masks:
relative_polygon.append(polygon)
class_id = np.array(class_id, dtype=int)
relative_xyxy = np.array(relative_xyxy, dtype=np.float32)
xyxy = relative_xyxy * np.array([w, h, w, h], dtype=np.float32)
if not with_masks:
return Detections(class_id=class_id, xyxy=xyxy)
polygons = [
(polygon * np.array(resolution_wh)).astype(int) for polygon in relative_polygon
]
mask = _polygons_to_masks(polygons=polygons, resolution_wh=resolution_wh)
return Detections(class_id=class_id, xyxy=xyxy, mask=mask)
def load_yolo_annotations(
images_directory_path: str,
annotations_directory_path: str,
data_yaml_path: str,
force_masks: bool = False,
) -> Tuple[List[str], Dict[str, np.ndarray], Dict[str, Detections]]:
"""
Loads YOLO annotations and returns class names, images,
and their corresponding detections.
Args:
images_directory_path (str): The path to the directory containing the images.
annotations_directory_path (str): The path to the directory
containing the YOLO annotation files.
data_yaml_path (str): The path to the data
YAML file containing class information.
force_masks (bool, optional): If True, forces masks to be loaded
for all annotations, regardless of whether they are present.
Returns:
Tuple[List[str], Dict[str, np.ndarray], Dict[str, Detections]]:
A tuple containing a list of class names, a dictionary with
image names as keys and images as values, and a dictionary
with image names as keys and corresponding Detections instances as values.
"""
image_paths = list_files_with_extensions(
directory=images_directory_path, extensions=["jpg", "jpeg", "png"]
)
classes = _extract_class_names(file_path=data_yaml_path)
images = {}
annotations = {}
for image_path in image_paths:
image_stem = Path(image_path).stem
image_path = str(image_path)
image = cv2.imread(image_path)
annotation_path = os.path.join(annotations_directory_path, f"{image_stem}.txt")
if not os.path.exists(annotation_path):
images[image_path] = image
annotations[image_path] = Detections.empty()
continue
lines = read_txt_file(str(annotation_path))
h, w, _ = image.shape
resolution_wh = (w, h)
with_masks = _with_mask(lines=lines)
with_masks = force_masks if force_masks else with_masks
annotation = yolo_annotations_to_detections(
lines=lines, resolution_wh=resolution_wh, with_masks=with_masks
)
images[image_path] = image
annotations[image_path] = annotation
return classes, images, annotations
def object_to_yolo(
xyxy: np.ndarray,
class_id: int,
image_shape: Tuple[int, int, int],
polygon: Optional[np.ndarray] = None,
) -> str:
h, w, _ = image_shape
if polygon is None:
xyxy_relative = xyxy / np.array([w, h, w, h], dtype=np.float32)
x_min, y_min, x_max, y_max = xyxy_relative
x_center = (x_min + x_max) / 2
y_center = (y_min + y_max) / 2
width = x_max - x_min
height = y_max - y_min
return f"{int(class_id)} {x_center:.5f} {y_center:.5f} {width:.5f} {height:.5f}"
else:
polygon_relative = polygon / np.array([w, h], dtype=np.float32)
polygon_relative = polygon_relative.reshape(-1)
polygon_parsed = " ".join([f"{value:.5f}" for value in polygon_relative])
return f"{int(class_id)} {polygon_parsed}"
def detections_to_yolo_annotations(
detections: Detections,
image_shape: Tuple[int, int, int],
min_image_area_percentage: float = 0.0,
max_image_area_percentage: float = 1.0,
approximation_percentage: float = 0.75,
) -> List[str]:
annotation = []
for xyxy, mask, _, class_id, _ in detections:
if mask is not None:
polygons = approximate_mask_with_polygons(
mask=mask,
min_image_area_percentage=min_image_area_percentage,
max_image_area_percentage=max_image_area_percentage,
approximation_percentage=approximation_percentage,
)
for polygon in polygons:
xyxy = polygon_to_xyxy(polygon=polygon)
next_object = object_to_yolo(
xyxy=xyxy,
class_id=class_id,
image_shape=image_shape,
polygon=polygon,
)
annotation.append(next_object)
else:
next_object = object_to_yolo(
xyxy=xyxy, class_id=class_id, image_shape=image_shape
)
annotation.append(next_object)
return annotation
def save_yolo_annotations(
annotations_directory_path: str,
images: Dict[str, np.ndarray],
annotations: Dict[str, Detections],
min_image_area_percentage: float = 0.0,
max_image_area_percentage: float = 1.0,
approximation_percentage: float = 0.75,
) -> None:
Path(annotations_directory_path).mkdir(parents=True, exist_ok=True)
for image_path, image in images.items():
detections = annotations[image_path]
image_name = Path(image_path).name
yolo_annotations_name = _image_name_to_annotation_name(image_name=image_name)
yolo_annotations_path = os.path.join(
annotations_directory_path, yolo_annotations_name
)
lines = detections_to_yolo_annotations(
detections=detections,
image_shape=image.shape,
min_image_area_percentage=min_image_area_percentage,
max_image_area_percentage=max_image_area_percentage,
approximation_percentage=approximation_percentage,
)
save_text_file(lines=lines, file_path=yolo_annotations_path)
def save_data_yaml(data_yaml_path: str, classes: List[str]) -> None:
data = {"nc": len(classes), "names": classes}
Path(data_yaml_path).parent.mkdir(parents=True, exist_ok=True)
save_yaml_file(data=data, file_path=data_yaml_path)
+131
View File
@@ -0,0 +1,131 @@
import copy
import os
import random
from pathlib import Path
from typing import Dict, List, Optional, Tuple, TypeVar
import cv2
import numpy as np
from supervision.detection.core import Detections
from supervision.detection.utils import (
approximate_polygon,
filter_polygons_by_area,
mask_to_polygons,
)
T = TypeVar("T")
def approximate_mask_with_polygons(
mask: np.ndarray,
min_image_area_percentage: float = 0.0,
max_image_area_percentage: float = 1.0,
approximation_percentage: float = 0.75,
) -> List[np.ndarray]:
height, width = mask.shape
image_area = height * width
minimum_detection_area = min_image_area_percentage * image_area
maximum_detection_area = max_image_area_percentage * image_area
polygons = mask_to_polygons(mask=mask)
if len(polygons) == 1:
polygons = filter_polygons_by_area(
polygons=polygons, min_area=None, max_area=maximum_detection_area
)
else:
polygons = filter_polygons_by_area(
polygons=polygons,
min_area=minimum_detection_area,
max_area=maximum_detection_area,
)
return [
approximate_polygon(polygon=polygon, percentage=approximation_percentage)
for polygon in polygons
]
def merge_class_lists(class_lists: List[List[str]]) -> List[str]:
unique_classes = set()
for class_list in class_lists:
for class_name in class_list:
unique_classes.add(class_name.lower())
return sorted(list(unique_classes))
def build_class_index_mapping(
source_classes: List[str], target_classes: List[str]
) -> Dict[int, int]:
index_mapping = {}
for i, class_name in enumerate(source_classes):
if class_name not in target_classes:
raise ValueError(
f"Class {class_name} not found in target classes. "
"source_classes must be a subset of target_classes."
)
corresponding_index = target_classes.index(class_name)
index_mapping[i] = corresponding_index
return index_mapping
def map_detections_class_id(
source_to_target_mapping: Dict[int, int], detections: Detections
) -> Detections:
if detections.class_id is None:
raise ValueError("Detections must have class_id attribute.")
if set(np.unique(detections.class_id)) - set(source_to_target_mapping.keys()):
raise ValueError(
"Detections class_id must be a subset of source_to_target_mapping keys."
)
detections_copy = copy.deepcopy(detections)
if len(detections) > 0:
detections_copy.class_id = np.vectorize(source_to_target_mapping.get)(
detections_copy.class_id
)
return detections_copy
def save_dataset_images(
images_directory_path: str, images: Dict[str, np.ndarray]
) -> None:
Path(images_directory_path).mkdir(parents=True, exist_ok=True)
for image_path, image in images.items():
image_name = Path(image_path).name
target_image_path = os.path.join(images_directory_path, image_name)
cv2.imwrite(target_image_path, image)
def train_test_split(
data: List[T],
train_ratio: float = 0.8,
random_state: Optional[int] = None,
shuffle: bool = True,
) -> Tuple[List[T], List[T]]:
"""
Splits the data into two parts using the provided train_ratio.
Args:
data (List[T]): The data to split.
train_ratio (float): The ratio of the training set to the entire dataset.
random_state (Optional[int]): The seed for the random number generator.
shuffle (bool): Whether to shuffle the data before splitting.
Returns:
Tuple[List[T], List[T]]: The split data.
"""
if random_state is not None:
random.seed(random_state)
if shuffle:
random.shuffle(data)
split_index = int(len(data) * train_ratio)
return data[:split_index], data[split_index:]
+34 -62
View File
@@ -12,12 +12,15 @@ class BoxAnnotator:
A class for drawing bounding boxes on an image using detections provided.
Attributes:
color (Union[Color, ColorPalette]): The color to draw the bounding box, can be a single color or a color palette
color (Union[Color, ColorPalette]): The color to draw the bounding box,
can be a single color or a color palette
thickness (int): The thickness of the bounding box lines, default is 2
text_color (Color): The color of the text on the bounding box, default is white
text_scale (float): The scale of the text on the bounding box, default is 0.5
text_thickness (int): The thickness of the text on the bounding box, default is 1
text_padding (int): The padding around the text on the bounding box, default is 5
text_thickness (int): The thickness of the text on the bounding box,
default is 1
text_padding (int): The padding around the text on the bounding box,
default is 5
"""
@@ -47,13 +50,37 @@ class BoxAnnotator:
"""
Draws bounding boxes on the frame using the detections provided.
Parameters:
Args:
scene (np.ndarray): The image on which the bounding boxes will be drawn
detections (Detections): The detections for which the bounding boxes will be drawn
labels (Optional[List[str]]): An optional list of labels corresponding to each detection. If labels is provided, the confidence score of the detection will be replaced with the label.
skip_label (bool): Is set to True, skips bounding box label annotation.
detections (Detections): The detections for which the
bounding boxes will be drawn
labels (Optional[List[str]]): An optional list of labels
corresponding to each detection. If `labels` are not provided,
corresponding `class_id` will be used as label.
skip_label (bool): Is set to `True`, skips bounding box label annotation.
Returns:
np.ndarray: The image with the bounding boxes drawn on it
Example:
```python
>>> import supervision as sv
>>> classes = ['person', ...]
>>> image = ...
>>> detections = sv.Detections(...)
>>> box_annotator = sv.BoxAnnotator()
>>> labels = [
... f"{classes[class_id]} {confidence:0.2f}"
... for _, _, confidence, class_id, _
... in detections
... ]
>>> annotated_frame = box_annotator.annotate(
... scene=image.copy(),
... detections=detections,
... labels=labels
... )
```
"""
font = cv2.FONT_HERSHEY_SIMPLEX
for i in range(len(detections)):
@@ -117,58 +144,3 @@ class BoxAnnotator:
lineType=cv2.LINE_AA,
)
return scene
class MaskAnnotator:
"""
A class for overlaying masks on an image using detections provided.
Attributes:
color (Union[Color, ColorPalette]): The color to fill the mask, can be a single color or a color palette
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
):
self.color: Union[Color, ColorPalette] = color
def annotate(
self, scene: np.ndarray, detections: Detections, opacity: float = 0.5
) -> np.ndarray:
"""
Overlays the masks on the given image based on the provided detections, with a specified opacity.
Parameters:
scene (np.ndarray): The image on which the masks will be overlaid
detections (Detections): The detections for which the masks will be overlaid
opacity (float): The opacity of the masks, between 0 and 1, default is 0.5
Returns:
np.ndarray: The image with the masks overlaid
"""
if detections.mask is None:
return scene
for i in np.flip(np.argsort(detections.area)):
class_id = (
detections.class_id[i] if detections.class_id is not None else None
)
idx = class_id if class_id is not None else i
color = (
self.color.by_idx(idx)
if isinstance(self.color, ColorPalette)
else self.color
)
mask = detections.mask[i]
colored_mask = np.zeros_like(scene, dtype=np.uint8)
colored_mask[:] = color.as_bgr()
scene = np.where(
np.expand_dims(mask, axis=-1),
np.uint8(opacity * colored_mask + (1 - opacity) * scene),
scene,
)
return scene
+504 -85
View File
@@ -1,12 +1,18 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Iterator, List, Optional, Tuple
from dataclasses import astuple, dataclass
from typing import Any, Iterator, List, Optional, Tuple, Union
import numpy as np
from supervision.detection.utils import non_max_suppression, xywh_to_xyxy
from supervision.detection.utils import (
extract_ultralytics_masks,
non_max_suppression,
process_roboflow_result,
xywh_to_xyxy,
)
from supervision.geometry.core import Position
from supervision.utils.internal import deprecated
def _validate_xyxy(xyxy: Any, n: int) -> None:
@@ -20,7 +26,14 @@ def _validate_mask(mask: Any, n: int) -> None:
isinstance(mask, np.ndarray) and len(mask.shape) == 3 and mask.shape[0] == n
)
if not is_valid:
raise ValueError("mask must be 3d np.ndarray with (n, W, H) shape")
raise ValueError("mask must be 3d np.ndarray with (n, H, W) shape")
def validate_inference_callback(callback) -> None:
tmp_img = np.zeros((256, 256, 3), dtype=np.uint8)
res = callback(tmp_img)
if not isinstance(res, Detections):
raise ValueError("Callback function must return sv.Detection type")
def _validate_class_id(class_id: Any, n: int) -> None:
@@ -52,15 +65,20 @@ class Detections:
"""
Data class containing information about the detections in a video frame.
Attributes:
xyxy (np.ndarray): An array of shape `(n, 4)` containing the bounding boxes coordinates in format `[x1, y1, x2, y2]`
mask: (Optional[np.ndarray]): An array of shape `(n, W, H)` containing the segmentation masks.
confidence (Optional[np.ndarray]): An array of shape `(n,)` containing the confidence scores of the detections.
class_id (Optional[np.ndarray]): An array of shape `(n,)` containing the class ids of the detections.
tracker_id (Optional[np.ndarray]): An array of shape `(n,)` containing the tracker ids of the detections.
xyxy (np.ndarray): An array of shape `(n, 4)` containing
the bounding boxes coordinates in format `[x1, y1, x2, y2]`
mask: (Optional[np.ndarray]): An array of shape
`(n, H, W)` containing the segmentation masks.
confidence (Optional[np.ndarray]): An array of shape
`(n,)` containing the confidence scores of the detections.
class_id (Optional[np.ndarray]): An array of shape
`(n,)` containing the class ids of the detections.
tracker_id (Optional[np.ndarray]): An array of shape
`(n,)` containing the tracker ids of the detections.
"""
xyxy: np.ndarray
mask: np.Optional[np.ndarray] = None
mask: Optional[np.ndarray] = None
confidence: Optional[np.ndarray] = None
class_id: Optional[np.ndarray] = None
tracker_id: Optional[np.ndarray] = None
@@ -91,7 +109,8 @@ class Detections:
]
]:
"""
Iterates over the Detections object and yield a tuple of `(xyxy, mask, confidence, class_id, tracker_id)` for each detection.
Iterates over the Detections object and yield a tuple of
`(xyxy, mask, confidence, class_id, tracker_id)` for each detection.
"""
for i in range(len(self.xyxy)):
yield (
@@ -136,25 +155,30 @@ class Detections:
@classmethod
def from_yolov5(cls, yolov5_results) -> Detections:
"""
Creates a Detections instance from a YOLOv5 output Detections
Creates a Detections instance from a
[YOLOv5](https://github.com/ultralytics/yolov5) inference result.
Args:
yolov5_results (yolov5.models.common.Detections): The output Detections instance from YOLOv5
yolov5_results (yolov5.models.common.Detections):
The output Detections instance from YOLOv5
Returns:
Detections: A new Detections object.
Example:
```python
>>> import cv2
>>> import torch
>>> from supervision import Detections
>>> import supervision as sv
>>> image = cv2.imread(SOURCE_IMAGE_PATH)
>>> model = torch.hub.load('ultralytics/yolov5', 'yolov5s')
>>> results = model(IMAGE)
>>> detections = Detections.from_yolov5(results)
>>> result = model(image)
>>> detections = sv.Detections.from_yolov5(result)
```
"""
yolov5_detections_predictions = yolov5_results.pred[0].cpu().cpu().numpy()
return cls(
xyxy=yolov5_detections_predictions[:, :4],
confidence=yolov5_detections_predictions[:, 4],
@@ -162,40 +186,205 @@ class Detections:
)
@classmethod
@deprecated(
"""
This method is deprecated and removed in 0.15.0 release.
Use sv.Detections.from_ultralytics() instead as it is more generic and
can be used for detections from any ultralytics.engine.results.Results Object
"""
)
def from_yolov8(cls, yolov8_results) -> Detections:
"""
Creates a Detections instance from a YOLOv8 output Results
Creates a Detections instance from a
[YOLOv8](https://github.com/ultralytics/ultralytics) inference result.
Args:
yolov8_results (ultralytics.yolo.engine.results.Results): The output Results instance from YOLOv8
yolov8_results (ultralytics.yolo.engine.results.Results):
The output Results instance from YOLOv8
Returns:
Detections: A new Detections object.
Example:
```python
>>> import cv2
>>> from ultralytics import YOLO
>>> from supervision import Detections
>>> import supervision as sv
>>> image = cv2.imread(SOURCE_IMAGE_PATH)
>>> model = YOLO('yolov8s.pt')
>>> yolov8_results = model(IMAGE)[0]
>>> detections = Detections.from_yolov8(yolov8_results)
>>> result = model(image)[0]
>>> detections = sv.Detections.from_yolov8(result)
```
"""
return cls(
xyxy=yolov8_results.boxes.xyxy.cpu().numpy(),
confidence=yolov8_results.boxes.conf.cpu().numpy(),
class_id=yolov8_results.boxes.cls.cpu().numpy().astype(int),
mask=extract_ultralytics_masks(yolov8_results),
)
@classmethod
def from_ultralytics(cls, ultralytics_results) -> Detections:
"""
Creates a Detections instance from a
[YOLOv8](https://github.com/ultralytics/ultralytics) inference result.
Args:
ultralytics_results (ultralytics.yolo.engine.results.Results):
The output Results instance from YOLOv8
Returns:
Detections: A new Detections object.
Example:
```python
>>> import cv2
>>> from ultralytics import YOLO, FastSAM, SAM, RTDETR
>>> import supervision as sv
>>> image = cv2.imread(SOURCE_IMAGE_PATH)
>>> model = YOLO('yolov8s.pt')
>>> model = SAM('sam_b.pt')
>>> model = SAM('mobile_sam.pt')
>>> model = FastSAM('FastSAM-s.pt')
>>> model = RTDETR('rtdetr-l.pt')
>>> # model inferences
>>> result = model(image)[0]
>>> # if tracker is enabled
>>> result = model.track(image)[0]
>>> detections = sv.Detections.from_ultralytics(result)
```
"""
return cls(
xyxy=ultralytics_results.boxes.xyxy.cpu().numpy(),
confidence=ultralytics_results.boxes.conf.cpu().numpy(),
class_id=ultralytics_results.boxes.cls.cpu().numpy().astype(int),
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,
)
@classmethod
def from_yolo_nas(cls, yolo_nas_results) -> Detections:
"""
Creates a Detections instance from a
[YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md)
inference result.
Args:
yolo_nas_results (ImageDetectionPrediction):
The output Results instance from YOLO-NAS
ImageDetectionPrediction is coming from
'super_gradients.training.models.prediction_results'
Returns:
Detections: A new Detections object.
Example:
```python
>>> import cv2
>>> from super_gradients.training import models
>>> import supervision as sv
>>> image = cv2.imread(SOURCE_IMAGE_PATH)
>>> model = models.get('yolo_nas_l', pretrained_weights="coco")
>>> result = list(model.predict(image, conf=0.35))[0]
>>> detections = sv.Detections.from_yolo_nas(result)
```
"""
if np.asarray(yolo_nas_results.bboxes_xyxy).shape[0] == 0:
return cls.empty()
return cls(
xyxy=yolo_nas_results.prediction.bboxes_xyxy,
confidence=yolo_nas_results.prediction.confidence,
class_id=yolo_nas_results.prediction.labels.astype(int),
)
@classmethod
def from_deepsparse(cls, deepsparse_results) -> Detections:
"""
Creates a Detections instance from a
[DeepSparse](https://github.com/neuralmagic/deepsparse)
inference result.
Args:
deepsparse_results (deepsparse.yolo.schemas.YOLOOutput):
The output Results instance from DeepSparse.
Returns:
Detections: A new Detections object.
Example:
```python
>>> from deepsparse import Pipeline
>>> import supervision as sv
>>> yolo_pipeline = Pipeline.create(
... task="yolo",
... model_path = "zoo:cv/detection/yolov5-l/pytorch/" \
... "ultralytics/coco/pruned80_quant-none"
>>> pipeline_outputs = yolo_pipeline(SOURCE_IMAGE_PATH,
... iou_thres=0.6, conf_thres=0.001)
>>> detections = sv.Detections.from_deepsparse(result)
```
"""
if np.asarray(deepsparse_results.boxes[0]).shape[0] == 0:
return cls.empty()
return cls(
xyxy=np.array(deepsparse_results.boxes[0]),
confidence=np.array(deepsparse_results.scores[0]),
class_id=np.array(deepsparse_results.labels[0]).astype(float).astype(int),
)
@classmethod
def from_mmdetection(cls, mmdet_results) -> Detections:
"""
Creates a Detections instance from
a [mmdetection](https://github.com/open-mmlab/mmdetection) inference result.
Also supported for [mmyolo](https://github.com/open-mmlab/mmyolo)
Args:
mmdet_results (mmdet.structures.DetDataSample):
The output Results instance from MMDetection.
Returns:
Detections: A new Detections object.
Example:
```python
>>> import cv2
>>> import supervision as sv
>>> from mmdet.apis import DetInferencer
>>> inferencer = DetInferencer(model_name, checkpoint, device)
>>> mmdet_result = inferencer(SOURCE_IMAGE_PATH, out_dir='./output',
... return_datasample=True)["predictions"][0]
>>> detections = sv.Detections.from_mmdet(mmdet_result)
```
"""
return cls(
xyxy=mmdet_results.pred_instances.bboxes.cpu().numpy(),
confidence=mmdet_results.pred_instances.scores.cpu().numpy(),
class_id=mmdet_results.pred_instances.labels.cpu().numpy().astype(int),
)
@classmethod
def from_transformers(cls, transformers_results: dict) -> Detections:
"""
Creates a Detections instance from Object Detection Transformer output Results
Creates a Detections instance from object detection
[transformer](https://github.com/huggingface/transformers) inference result.
Returns:
Detections: A new Detections object.
"""
return cls(
xyxy=transformers_results["boxes"].cpu().numpy(),
confidence=transformers_results["scores"].cpu().numpy(),
@@ -204,6 +393,35 @@ class Detections:
@classmethod
def from_detectron2(cls, detectron2_results) -> Detections:
"""
Create a Detections object from the
[Detectron2](https://github.com/facebookresearch/detectron2) inference result.
Args:
detectron2_results: The output of a
Detectron2 model containing instances with prediction data.
Returns:
(Detections): A Detections object containing the bounding boxes,
class IDs, and confidences of the predictions.
Example:
```python
>>> import cv2
>>> from detectron2.engine import DefaultPredictor
>>> from detectron2.config import get_cfg
>>> import supervision as sv
>>> image = cv2.imread(SOURCE_IMAGE_PATH)
>>> cfg = get_cfg()
>>> cfg.merge_from_file("path/to/config.yaml")
>>> cfg.MODEL.WEIGHTS = "path/to/model_weights.pth"
>>> predictor = DefaultPredictor(cfg)
>>> result = predictor(image)
>>> detections = sv.Detections.from_detectron2(result)
```
"""
return cls(
xyxy=detectron2_results["instances"].pred_boxes.tensor.cpu().numpy(),
confidence=detectron2_results["instances"].scores.cpu().numpy(),
@@ -214,34 +432,61 @@ class Detections:
)
@classmethod
def from_roboflow(cls, roboflow_result: dict, class_list: List[str]) -> Detections:
xyxy = []
confidence = []
class_id = []
def from_roboflow(cls, roboflow_result: dict) -> Detections:
"""
Create a Detections object from the [Roboflow](https://roboflow.com/)
API inference result.
for prediction in roboflow_result["predictions"]:
x = prediction["x"]
y = prediction["y"]
width = prediction["width"]
height = prediction["height"]
x_min = x - width / 2
y_min = y - height / 2
x_max = x_min + width
y_max = y_min + height
xyxy.append([x_min, y_min, x_max, y_max])
class_id.append(class_list.index(prediction["class"]))
confidence.append(prediction["confidence"])
Args:
roboflow_result (dict): The result from the
Roboflow API containing predictions.
return Detections(
xyxy=np.array(xyxy),
confidence=np.array(confidence),
class_id=np.array(class_id).astype(int),
Returns:
(Detections): A Detections object containing the bounding boxes, class IDs,
and confidences of the predictions.
Example:
```python
>>> import supervision as sv
>>> roboflow_result = {
... "predictions": [
... {
... "x": 0.5,
... "y": 0.5,
... "width": 0.2,
... "height": 0.3,
... "class_id": 0,
... "class": "person",
... "confidence": 0.9
... },
... # ... more predictions ...
... ]
... }
>>> detections = sv.Detections.from_roboflow(roboflow_result)
```
"""
xyxy, confidence, class_id, masks = process_roboflow_result(
roboflow_result=roboflow_result
)
if np.asarray(xyxy).shape[0] == 0:
return cls.empty()
return cls(
xyxy=xyxy,
confidence=confidence,
class_id=class_id,
mask=masks,
)
@classmethod
def from_sam(cls, sam_result: List[dict]) -> Detections:
"""
Creates a Detections instance from Segment Anything Model (SAM) by Meta AI.
Creates a Detections instance from
[Segment Anything Model](https://github.com/facebookresearch/segment-anything)
inference result.
Args:
sam_result (List[dict]): The output Results instance from SAM
@@ -251,15 +496,20 @@ class Detections:
Example:
```python
>>> from segment_anything import sam_model_registry, SamAutomaticMaskGenerator
>>> import supervision as sv
>>> from segment_anything import (
... sam_model_registry,
... SamAutomaticMaskGenerator
... )
>>> sam = sam_model_registry[MODEL_TYPE](checkpoint=CHECKPOINT_PATH).to(device=DEVICE)
>>> sam_model_reg = sam_model_registry[MODEL_TYPE]
>>> sam = sam_model_reg(checkpoint=CHECKPOINT_PATH).to(device=DEVICE)
>>> mask_generator = SamAutomaticMaskGenerator(sam)
>>> sam_result = mask_generator.generate(IMAGE)
>>> detections = sv.Detections.from_sam(sam_result=sam_result)
```
"""
sorted_generated_masks = sorted(
sam_result, key=lambda x: x["area"], reverse=True
)
@@ -267,36 +517,148 @@ class Detections:
xywh = np.array([mask["bbox"] for mask in sorted_generated_masks])
mask = np.array([mask["segmentation"] for mask in sorted_generated_masks])
return Detections(xyxy=xywh_to_xyxy(boxes_xywh=xywh), mask=mask)
if np.asarray(xywh).shape[0] == 0:
return cls.empty()
xyxy = xywh_to_xyxy(boxes_xywh=xywh)
return cls(xyxy=xyxy, mask=mask)
@classmethod
def from_coco_annotations(cls, coco_annotation: dict) -> Detections:
xyxy, class_id = [], []
def from_paddledet(cls, paddledet_result) -> Detections:
"""
Creates a Detections instance from
[PaddleDetection](https://github.com/PaddlePaddle/PaddleDetection)
inference result.
for annotation in coco_annotation:
x_min, y_min, width, height = annotation["bbox"]
xyxy.append([x_min, y_min, x_min + width, y_min + height])
class_id.append(annotation["category_id"])
Args:
paddledet_result (List[dict]): The output Results instance from PaddleDet
return cls(xyxy=np.array(xyxy), class_id=np.array(class_id))
Returns:
Detections: A new Detections object.
Example:
```python
>>> import supervision as sv
>>> import paddle
>>> from ppdet.engine import Trainer
>>> from ppdet.core.workspace import load_config
>>> weights = (...)
>>> config = (...)
>>> cfg = load_config(config)
>>> trainer = Trainer(cfg, mode='test')
>>> trainer.load_weights(weights)
>>> paddledet_result = trainer.predict([images])[0]
>>> detections = sv.Detections.from_paddledet(paddledet_result)
```
"""
if np.asarray(paddledet_result["bbox"][:, 2:6]).shape[0] == 0:
return cls.empty()
return cls(
xyxy=paddledet_result["bbox"][:, 2:6],
confidence=paddledet_result["bbox"][:, 1],
class_id=paddledet_result["bbox"][:, 0].astype(int),
)
@classmethod
def empty(cls) -> Detections:
"""
Create an empty Detections object with no bounding boxes,
confidences, or class IDs.
Returns:
(Detections): An empty Detections object.
Example:
```python
>>> from supervision import Detections
>>> empty_detections = Detections.empty()
```
"""
return cls(
xyxy=np.empty((0, 4), dtype=np.float32),
confidence=np.array([], dtype=np.float32),
class_id=np.array([], dtype=int),
)
def get_anchor_coordinates(self, anchor: Position) -> np.ndarray:
@classmethod
def merge(cls, detections_list: List[Detections]) -> Detections:
"""
Returns the bounding box coordinates for a specific anchor.
Merge a list of Detections objects into a single Detections object.
This method takes a list of Detections objects and combines their
respective fields (`xyxy`, `mask`, `confidence`, `class_id`, and `tracker_id`)
into a single Detections object. If all elements in a field are not
`None`, the corresponding field will be stacked.
Otherwise, the field will be set to `None`.
Args:
anchor (Position): Position of bounding box anchor for which to return the coordinates.
detections_list (List[Detections]): A list of Detections objects to merge.
Returns:
np.ndarray: An array of shape `(n, 2)` containing the bounding box anchor coordinates in format `[x, y]`.
(Detections): A single Detections object containing
the merged data from the input list.
Example:
```python
>>> from supervision import Detections
>>> detections_1 = Detections(...)
>>> detections_2 = Detections(...)
>>> merged_detections = Detections.merge([detections_1, detections_2])
```
"""
if len(detections_list) == 0:
return Detections.empty()
detections_tuples_list = [astuple(detection) for detection in detections_list]
xyxy, mask, confidence, class_id, tracker_id = [
list(field) for field in zip(*detections_tuples_list)
]
def __all_not_none(item_list: List[Any]):
return all(x is not None for x in item_list)
xyxy = np.vstack(xyxy)
mask = np.vstack(mask) if __all_not_none(mask) else None
confidence = np.hstack(confidence) if __all_not_none(confidence) else None
class_id = np.hstack(class_id) if __all_not_none(class_id) else None
tracker_id = np.hstack(tracker_id) if __all_not_none(tracker_id) else None
return cls(
xyxy=xyxy,
mask=mask,
confidence=confidence,
class_id=class_id,
tracker_id=tracker_id,
)
def get_anchor_coordinates(self, anchor: Position) -> np.ndarray:
"""
Calculates and returns the coordinates of a specific anchor point
within the bounding boxes defined by the `xyxy` attribute. The anchor
point can be any of the predefined positions in the `Position` enum,
such as `CENTER`, `CENTER_LEFT`, `BOTTOM_RIGHT`, etc.
Args:
anchor (Position): An enum specifying the position of the anchor point
within the bounding box. Supported positions are defined in the
`Position` enum.
Returns:
np.ndarray: An array of shape `(n, 2)`, where `n` is the number of bounding
boxes. Each row contains the `[x, y]` coordinates of the specified
anchor point for the corresponding bounding box.
Raises:
ValueError: If the provided `anchor` is not supported.
"""
if anchor == Position.CENTER:
return np.array(
@@ -305,40 +667,90 @@ class Detections:
(self.xyxy[:, 1] + self.xyxy[:, 3]) / 2,
]
).transpose()
elif anchor == Position.CENTER_LEFT:
return np.array(
[
self.xyxy[:, 0],
(self.xyxy[:, 1] + self.xyxy[:, 3]) / 2,
]
).transpose()
elif anchor == Position.CENTER_RIGHT:
return np.array(
[
self.xyxy[:, 2],
(self.xyxy[:, 1] + self.xyxy[:, 3]) / 2,
]
).transpose()
elif anchor == Position.BOTTOM_CENTER:
return np.array(
[(self.xyxy[:, 0] + self.xyxy[:, 2]) / 2, self.xyxy[:, 3]]
).transpose()
elif anchor == Position.BOTTOM_LEFT:
return np.array([self.xyxy[:, 0], self.xyxy[:, 3]]).transpose()
elif anchor == Position.BOTTOM_RIGHT:
return np.array([self.xyxy[:, 2], self.xyxy[:, 3]]).transpose()
elif anchor == Position.TOP_CENTER:
return np.array(
[(self.xyxy[:, 0] + self.xyxy[:, 2]) / 2, self.xyxy[:, 1]]
).transpose()
elif anchor == Position.TOP_LEFT:
return np.array([self.xyxy[:, 0], self.xyxy[:, 1]]).transpose()
elif anchor == Position.TOP_RIGHT:
return np.array([self.xyxy[:, 2], self.xyxy[:, 1]]).transpose()
raise ValueError(f"{anchor} is not supported.")
def __getitem__(self, index: np.ndarray) -> Detections:
if isinstance(index, np.ndarray) and (
index.dtype == bool or index.dtype == int
):
return Detections(
xyxy=self.xyxy[index],
mask=self.mask[index] if self.mask is not None else None,
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,
tracker_id=self.tracker_id[index]
if self.tracker_id is not None
else None,
)
raise TypeError(
f"Detections.__getitem__ not supported for index of type {type(index)}."
def __getitem__(
self, index: Union[int, slice, List[int], np.ndarray]
) -> Detections:
"""
Get a subset of the Detections object.
Args:
index (Union[int, slice, List[int], np.ndarray]):
The index or indices of the subset of the Detections
Returns:
(Detections): A subset of the Detections object.
Example:
```python
>>> import supervision as sv
>>> detections = sv.Detections(...)
>>> first_detection = detections[0]
>>> first_10_detections = detections[0:10]
>>> some_detections = detections[[0, 2, 4]]
>>> class_0_detections = detections[detections.class_id == 0]
>>> high_confidence_detections = detections[detections.confidence > 0.5]
```
"""
if isinstance(index, int):
index = [index]
return Detections(
xyxy=self.xyxy[index],
mask=self.mask[index] if self.mask is not None else None,
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,
tracker_id=self.tracker_id[index] if self.tracker_id is not None else None,
)
@property
def area(self) -> np.ndarray:
"""
Calculate the area of each detection in the set of object detections. If masks field is defined property
returns are of each mask. If only box is given property return area of each box.
Calculate the area of each detection in the set of object detections.
If masks field is defined property returns are of each mask.
If only box is given property return area of each box.
Returns:
np.ndarray: An array of floats containing the area of each detection in the format of `(area_1, area_2, ..., area_n)`, where n is the number of detections.
np.ndarray: An array of floats containing the area of each detection
in the format of `(area_1, area_2, ..., area_n)`,
where n is the number of detections.
"""
if self.mask is not None:
return np.array([np.sum(mask) for mask in self.mask])
@@ -351,7 +763,9 @@ class Detections:
Calculate the area of each bounding box in the set of object detections.
Returns:
np.ndarray: An array of floats containing the area of each bounding box in the format of `(area_1, area_2, ..., area_n)`, where n is the number of detections.
np.ndarray: An array of floats containing the area of each bounding
box in the format of `(area_1, area_2, ..., area_n)`,
where n is the number of detections.
"""
return (self.xyxy[:, 3] - self.xyxy[:, 1]) * (self.xyxy[:, 2] - self.xyxy[:, 0])
@@ -362,21 +776,26 @@ class Detections:
Perform non-maximum suppression on the current set of object detections.
Args:
threshold (float, optional): The intersection-over-union threshold to use for non-maximum suppression. Defaults to 0.5.
class_agnostic (bool, optional): Whether to perform class-agnostic non-maximum suppression. If True, the class_id of each detection will be ignored. Defaults to False.
threshold (float, optional): The intersection-over-union threshold
to use for non-maximum suppression. Defaults to 0.5.
class_agnostic (bool, optional): Whether to perform class-agnostic
non-maximum suppression. If True, the class_id of each detection
will be ignored. Defaults to False.
Returns:
Detections: A new Detections object containing the subset of detections after non-maximum suppression.
Detections: A new Detections object containing the subset of detections
after non-maximum suppression.
Raises:
AssertionError: If `confidence` is None and class_agnostic is False. If `class_id` is None and class_agnostic is False.
AssertionError: If `confidence` is None and class_agnostic is False.
If `class_id` is None and class_agnostic is False.
"""
if len(self) == 0:
return self
assert (
self.confidence is not None
), f"Detections confidence must be given for NMS to be executed."
), "Detections confidence must be given for NMS to be executed."
if class_agnostic:
predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1)))
@@ -386,8 +805,8 @@ class Detections:
return self[indices]
assert self.class_id is not None, (
f"Detections class_id must be given for NMS to be executed. If you intended to perform class agnostic "
f"NMS set class_agnostic=True."
"Detections class_id must be given for NMS to be executed. If you intended"
" to perform class agnostic NMS set class_agnostic=True."
)
predictions = np.hstack(
+18 -4
View File
@@ -1,4 +1,4 @@
from typing import Dict
from typing import Dict, Optional
import cv2
import numpy as np
@@ -81,6 +81,8 @@ class LineZoneAnnotator:
text_scale: float = 0.5,
text_offset: float = 1.5,
text_padding: int = 10,
custom_in_text: Optional[str] = None,
custom_out_text: Optional[str] = None,
):
"""
Initialize the LineCounterAnnotator object with default values.
@@ -102,6 +104,8 @@ class LineZoneAnnotator:
self.text_scale: float = text_scale
self.text_offset: float = text_offset
self.text_padding: int = text_padding
self.custom_in_text: str = custom_in_text
self.custom_out_text: str = custom_out_text
def annotate(self, frame: np.ndarray, line_counter: LineZone) -> np.ndarray:
"""
@@ -109,7 +113,8 @@ class LineZoneAnnotator:
Attributes:
frame (np.ndarray): The image on which the line will be drawn.
line_counter (LineCounter): The line counter that will be used to draw the line.
line_counter (LineCounter): The line counter
that will be used to draw the line.
Returns:
np.ndarray: The image with the line drawn on it.
@@ -141,8 +146,16 @@ class LineZoneAnnotator:
lineType=cv2.LINE_AA,
)
in_text = f"in: {line_counter.in_count}"
out_text = f"out: {line_counter.out_count}"
in_text = (
f"{self.custom_in_text}: {line_counter.in_count}"
if self.custom_in_text is not None
else f"in: {line_counter.in_count}"
)
out_text = (
f"{self.custom_out_text}: {line_counter.out_count}"
if self.custom_out_text is not None
else f"out: {line_counter.out_count}"
)
(in_text_width, in_text_height), _ = cv2.getTextSize(
in_text, cv2.FONT_HERSHEY_SIMPLEX, self.text_scale, self.text_thickness
@@ -219,3 +232,4 @@ class LineZoneAnnotator:
self.text_thickness,
cv2.LINE_AA,
)
return frame
@@ -0,0 +1,181 @@
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Callable, Optional, Tuple
import numpy as np
from supervision.detection.core import Detections, validate_inference_callback
from supervision.detection.utils import move_boxes
from supervision.utils.image import crop_image
def move_detections(detections: Detections, offset: np.array) -> Detections:
"""
Args:
detections (sv.Detections): Detections object to be moved.
offset (np.array): An array of shape `(2,)` containing offset values in format
is `[dx, dy]`.
Returns:
(sv.Detections) repositioned Detections object.
"""
detections.xyxy = move_boxes(xyxy=detections.xyxy, offset=offset)
return 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.
Args:
slice_wh (Tuple[int, int]): Dimensions of each slice in the format
`(width, height)`.
overlap_ratio_wh (Tuple[float, float]): Overlap ratio between consecutive
slices in the format `(width_ratio, height_ratio)`.
iou_threshold (Optional[float]): Intersection over Union (IoU) threshold
used for non-max suppression.
callback (Callable): A function that performs inference on a given image
slice and returns detections.
thread_workers (int): Number of threads for parallel execution.
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.
"""
def __init__(
self,
callback: Callable[[np.ndarray], Detections],
slice_wh: Tuple[int, int] = (320, 320),
overlap_ratio_wh: Tuple[float, float] = (0.2, 0.2),
iou_threshold: Optional[float] = 0.5,
thread_workers: int = 1,
):
self.slice_wh = slice_wh
self.overlap_ratio_wh = overlap_ratio_wh
self.iou_threshold = iou_threshold
self.callback = callback
self.thread_workers = thread_workers
validate_inference_callback(callback=callback)
def __call__(self, image: np.ndarray) -> Detections:
"""
Performs slicing-based inference on the provided image using the specified
callback.
Args:
image (np.ndarray): The input image on which inference needs to be
performed. The image should be in the format
`(height, width, channels)`.
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)
>>> detections = slicer(image)
```
"""
detections_list = []
resolution_wh = (image.shape[1], image.shape[0])
offsets = self._generate_offset(
resolution_wh=resolution_wh,
slice_wh=self.slice_wh,
overlap_ratio_wh=self.overlap_ratio_wh,
)
with ThreadPoolExecutor(max_workers=self.thread_workers) as executor:
futures = [
executor.submit(self._run_callback, image, offset) for offset in offsets
]
for future in as_completed(futures):
detections_list.append(future.result())
return Detections.merge(detections_list=detections_list).with_nms(
threshold=self.iou_threshold
)
def _run_callback(self, image, offset) -> Detections:
"""
Run the provided callback on a slice of an image.
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.
Returns:
Detections: A collection of detections for the slice.
"""
image_slice = crop_image(image=image, xyxy=offset)
detections = self.callback(image_slice)
detections = move_detections(detections=detections, offset=offset[:2])
return detections
@staticmethod
def _generate_offset(
resolution_wh: Tuple[int, int],
slice_wh: Tuple[int, int],
overlap_ratio_wh: Tuple[float, float],
) -> np.ndarray:
"""
Generate offset coordinates for slicing an image based on the given resolution,
slice dimensions, and overlap ratios.
Args:
resolution_wh (Tuple[int, int]): A tuple representing the width and height
of the image to be sliced.
slice_wh (Tuple[int, int]): A tuple representing the desired width and
height of each slice.
overlap_ratio_wh (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.
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.
"""
slice_width, slice_height = slice_wh
image_width, image_height = resolution_wh
overlap_ratio_width, overlap_ratio_height = overlap_ratio_wh
width_stride = slice_width - int(overlap_ratio_width * slice_width)
height_stride = slice_height - int(overlap_ratio_height * slice_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)
return offsets
+16 -9
View File
@@ -17,9 +17,11 @@ class PolygonZone:
A class for defining a polygon-shaped zone within a frame for detecting objects.
Attributes:
polygon (np.ndarray): A numpy array defining the polygon vertices
polygon (np.ndarray): A polygon represented by a numpy array of shape
`(N, 2)`, containing the `x`, `y` coordinates of the points.
frame_resolution_wh (Tuple[int, int]): The frame resolution (width, height)
triggering_position (Position): The position within the bounding box that triggers the zone (default: Position.BOTTOM_CENTER)
triggering_position (Position): The position within the bounding
box that triggers the zone (default: Position.BOTTOM_CENTER)
current_count (int): The current count of detected objects within the zone
mask (np.ndarray): The 2D bool mask for the polygon zone
"""
@@ -30,7 +32,7 @@ class PolygonZone:
frame_resolution_wh: Tuple[int, int],
triggering_position: Position = Position.BOTTOM_CENTER,
):
self.polygon = polygon
self.polygon = polygon.astype(int)
self.frame_resolution_wh = frame_resolution_wh
self.triggering_position = triggering_position
self.current_count = 0
@@ -45,10 +47,12 @@ class PolygonZone:
Determines if the detections are within the polygon zone.
Parameters:
detections (Detections): The detections to be checked against the polygon zone
detections (Detections): The detections
to be checked against the polygon zone
Returns:
np.ndarray: A boolean numpy array indicating if each detection is within the polygon zone
np.ndarray: A boolean numpy array indicating
if each detection is within the polygon zone
"""
clipped_xyxy = clip_boxes(
@@ -59,13 +63,14 @@ class PolygonZone:
clipped_detections.get_anchor_coordinates(anchor=self.triggering_position)
).astype(int)
is_in_zone = self.mask[clipped_anchors[:, 1], clipped_anchors[:, 0]]
self.current_count = np.sum(is_in_zone)
self.current_count = int(np.sum(is_in_zone))
return is_in_zone.astype(bool)
class PolygonZoneAnnotator:
"""
A class for annotating a polygon-shaped zone within a frame with a count of detected objects.
A class for annotating a polygon-shaped zone within a
frame with a count of detected objects.
Attributes:
zone (PolygonZone): The polygon zone to be annotated
@@ -75,7 +80,8 @@ class PolygonZoneAnnotator:
text_scale (float): The scale of the text on the polygon, default is 0.5
text_thickness (int): The thickness of the text on the polygon, default is 1
text_padding (int): The padding around the text on the polygon, default is 10
font (int): The font type for the text on the polygon, default is cv2.FONT_HERSHEY_SIMPLEX
font (int): The font type for the text on the polygon,
default is cv2.FONT_HERSHEY_SIMPLEX
center (Tuple[int, int]): The center of the polygon for text placement
"""
@@ -105,7 +111,8 @@ class PolygonZoneAnnotator:
Parameters:
scene (np.ndarray): The image on which the polygon zone will be annotated
label (Optional[str]): An optional label for the count of detected objects within the polygon zone (default: None)
label (Optional[str]): An optional label for the count of detected objects
within the polygon zone (default: None)
Returns:
np.ndarray: The image with the polygon zone and count of detected objects
+179 -41
View File
@@ -10,29 +10,37 @@ def polygon_to_mask(polygon: np.ndarray, resolution_wh: Tuple[int, int]) -> np.n
"""Generate a mask from a polygon.
Args:
polygon (np.ndarray): The polygon for which the mask should be generated, given as a list of vertices.
polygon (np.ndarray): The polygon for which the mask should be generated,
given as a list of vertices.
resolution_wh (Tuple[int, int]): The width and height of the desired resolution.
Returns:
np.ndarray: The generated 2D mask, where the polygon is marked with `1`'s and the rest is filled with `0`'s.
np.ndarray: The generated 2D mask, where the polygon is marked with
`1`'s and the rest is filled with `0`'s.
"""
width, height = resolution_wh
mask = np.zeros((height, width), dtype=np.uint8)
mask = np.zeros((height, width))
cv2.fillPoly(mask, [polygon], color=1)
return mask
def box_iou_batch(boxes_true: np.ndarray, boxes_detection: np.ndarray) -> 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 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.
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.
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.
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.
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.
"""
def box_area(box):
@@ -55,17 +63,22 @@ def non_max_suppression(
Perform Non-Maximum Suppression (NMS) on object detection predictions.
Args:
predictions (np.ndarray): An array of object detection predictions in the format of `(x_min, y_min, x_max, y_max, score)` or `(x_min, y_min, x_max, y_max, score, class)`.
iou_threshold (float, optional): The intersection-over-union threshold to use for non-maximum suppression.
predictions (np.ndarray): An array of object detection predictions in
the format of `(x_min, y_min, x_max, y_max, score)`
or `(x_min, y_min, x_max, y_max, score, class)`.
iou_threshold (float, optional): The intersection-over-union threshold
to use for non-maximum suppression.
Returns:
np.ndarray: A boolean array indicating which predictions to keep after non-maximum suppression.
np.ndarray: A boolean array indicating which predictions to keep after n
on-maximum suppression.
Raises:
AssertionError: If `iou_threshold` is not within the closed range from `0` to `1`.
AssertionError: If `iou_threshold` is not within the
closed range from `0` to `1`.
"""
assert 0 <= iou_threshold <= 1, (
f"Value of `iou_threshold` must be in the closed range from 0 to 1, "
"Value of `iou_threshold` must be in the closed range from 0 to 1, "
f"{iou_threshold} given."
)
rows, columns = predictions.shape
@@ -89,7 +102,8 @@ def non_max_suppression(
if not keep[index]:
continue
# drop detections with iou > iou_threshold and same category as current detections
# drop detections with iou > iou_threshold and
# same category as current detections
condition = (iou > iou_threshold) & (categories == category)
keep = keep & ~condition
@@ -103,14 +117,16 @@ def clip_boxes(
Clips bounding boxes coordinates to fit within the frame resolution.
Args:
boxes_xyxy (np.ndarray): A numpy array of shape `(N, 4)` where each row corresponds to a bounding box in
boxes_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)`.
frame_resolution_wh (Tuple[int, int]): A tuple of the form `(width, height)` representing the resolution of the
frame.
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 corresponds to a bounding box with coordinates
clipped to fit within the frame resolution.
np.ndarray: A numpy array of shape `(N, 4)` where each row
corresponds to a bounding box with coordinates clipped to fit
within the frame resolution.
"""
result = np.copy(boxes_xyxy)
width, height = frame_resolution_wh
@@ -131,10 +147,12 @@ def mask_to_xyxy(masks: np.ndarray) -> np.ndarray:
Converts a 3D `np.array` of 2D bool masks into a 2D `np.array` of bounding boxes.
Parameters:
masks (np.ndarray): A 3D `np.array` of shape `(N, W, H)` containing 2D bool masks
masks (np.ndarray): A 3D `np.array` of shape `(N, W, H)`
containing 2D bool masks
Returns:
np.ndarray: A 2D `np.array` of shape `(N, 4)` containing the bounding boxes `(x_min, y_min, x_max, y_max)` for each mask
np.ndarray: A 2D `np.array` of shape `(N, 4)` containing the bounding boxes
`(x_min, y_min, x_max, y_max)` for each mask
"""
n = masks.shape[0]
bboxes = np.zeros((n, 4), dtype=int)
@@ -155,12 +173,14 @@ def mask_to_polygons(mask: np.ndarray) -> List[np.ndarray]:
Converts a binary mask to a list of polygons.
Parameters:
mask (np.ndarray): A binary mask represented as a 2D NumPy array of shape `(H, W)`,
where H and W are the height and width of the mask, respectively.
mask (np.ndarray): A binary mask represented as a 2D NumPy array of
shape `(H, W)`, where H and W are the height and width of
the mask, respectively.
Returns:
List[np.ndarray]: A list of polygons, where each polygon is represented by a NumPy array of shape `(N, 2)`,
containing the `x`, `y` coordinates of the points. Polygons with fewer points than `MIN_POLYGON_POINT_COUNT = 3`
List[np.ndarray]: A list of polygons, where each polygon is represented by a
NumPy array of shape `(N, 2)`, containing the `x`, `y` coordinates
of the points. Polygons with fewer points than `MIN_POLYGON_POINT_COUNT = 3`
are excluded from the output.
"""
@@ -183,15 +203,21 @@ def filter_polygons_by_area(
Filters a list of polygons based on their area.
Parameters:
polygons (List[np.ndarray]): A list of polygons, where each polygon is represented by a NumPy array of shape `(N, 2)`,
polygons (List[np.ndarray]): A list of polygons, where each polygon is
represented by a NumPy array of shape `(N, 2)`,
containing the `x`, `y` coordinates of the points.
min_area (Optional[float]): The minimum area threshold. Only polygons with an area greater than or equal to this value
will be included in the output. If set to None, no minimum area constraint will be applied.
max_area (Optional[float]): The maximum area threshold. Only polygons with an area less than or equal to this value
will be included in the output. If set to None, no maximum area constraint will be applied.
min_area (Optional[float]): The minimum area threshold.
Only polygons with an area greater than or equal to this value
will be included in the output. If set to None,
no minimum area constraint will be applied.
max_area (Optional[float]): The maximum area threshold.
Only polygons with an area less than or equal to this value
will be included in the output. If set to None,
no maximum area constraint will be applied.
Returns:
List[np.ndarray]: A new list of polygons containing only those with areas within the specified thresholds.
List[np.ndarray]: A new list of polygons containing only those with
areas within the specified thresholds.
"""
if min_area is None and max_area is None:
return polygons
@@ -213,26 +239,37 @@ def polygon_to_xyxy(polygon: np.ndarray) -> np.ndarray:
containing the `x`, `y` coordinates of the points.
Returns:
np.ndarray: A 1D NumPy array containing the bounding box `(x_min, y_min, x_max, y_max)` of the input polygon.
np.ndarray: A 1D NumPy array containing the bounding box
`(x_min, y_min, x_max, y_max)` of the input polygon.
"""
x_min, y_min = np.min(polygon, axis=0)
x_max, y_max = np.max(polygon, axis=0)
return np.array([x_min, y_min, x_max, y_max])
def approximate_polygon(polygon: np.ndarray, percentage: float) -> np.ndarray:
def approximate_polygon(
polygon: np.ndarray, percentage: float, epsilon_step: float = 0.05
) -> np.ndarray:
"""
Approximates a given polygon by reducing a certain percentage of points.
This function uses the Ramer-Douglas-Peucker algorithm to simplify the input polygon by reducing the number of points
while preserving the general shape.
This function uses the Ramer-Douglas-Peucker algorithm to simplify the input
polygon by reducing the number of points
while preserving the general shape.
Parameters:
polygon (np.ndarray): A 2D NumPy array of shape (N, 2) containing the x, y coordinates of the input polygon's points.
percentage (float): The percentage of points to be removed from the input polygon, in the range [0, 1).
polygon (np.ndarray): A 2D NumPy array of shape `(N, 2)` containing
the `x`, `y` coordinates of the input polygon's points.
percentage (float): The percentage of points to be removed from the
input polygon, in the range `[0, 1)`.
epsilon_step (float): Approximation accuracy step.
Epsilon is the maximum distance between the original curve
and its approximation.
Returns:
np.ndarray: A new 2D NumPy array of shape (M, 2), where M <= N * (1 - percentage), containing the x, y coordinates of the
np.ndarray: A new 2D NumPy array of shape `(M, 2)`,
where `M <= N * (1 - percentage)`, containing
the `x`, `y` coordinates of the
approximated polygon's points.
"""
@@ -246,8 +283,109 @@ def approximate_polygon(polygon: np.ndarray, percentage: float) -> np.ndarray:
epsilon = 0
approximated_points = polygon
while len(approximated_points) > target_points:
epsilon += 0.1
approximated_points = cv2.approxPolyDP(polygon, epsilon, closed=True)
while True:
epsilon += epsilon_step
new_approximated_points = cv2.approxPolyDP(polygon, epsilon, closed=True)
if len(new_approximated_points) > target_points:
approximated_points = new_approximated_points
else:
break
return np.squeeze(approximated_points, axis=1)
def extract_ultralytics_masks(yolov8_results) -> Optional[np.ndarray]:
if not yolov8_results.masks:
return None
orig_shape = yolov8_results.orig_shape
inference_shape = tuple(yolov8_results.masks.data.shape[1:])
gain = 0
pad = (0, 0)
if inference_shape != orig_shape:
gain = min(
inference_shape[0] / orig_shape[0],
inference_shape[1] / orig_shape[1],
)
pad = (
(inference_shape[1] - orig_shape[1] * gain) / 2,
(inference_shape[0] - orig_shape[0] * gain) / 2,
)
top, left = int(pad[1]), int(pad[0])
bottom, right = int(inference_shape[0] - pad[1]), int(inference_shape[1] - pad[0])
mask_maps = []
masks = yolov8_results.masks.data.cpu().numpy()
for i in range(masks.shape[0]):
mask = masks[i]
mask = mask[top:bottom, left:right]
if mask.shape != orig_shape:
mask = cv2.resize(mask, (orig_shape[1], orig_shape[0]))
mask_maps.append(mask)
return np.asarray(mask_maps, dtype=bool)
def process_roboflow_result(
roboflow_result: dict,
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, Optional[np.ndarray]]:
if not roboflow_result["predictions"]:
return np.empty((0, 4)), np.empty(0), np.empty(0), None
xyxy = []
confidence = []
class_id = []
masks = []
image_width = int(roboflow_result["image"]["width"])
image_height = int(roboflow_result["image"]["height"])
for prediction in roboflow_result["predictions"]:
x = prediction["x"]
y = prediction["y"]
width = prediction["width"]
height = prediction["height"]
x_min = x - width / 2
y_min = y - height / 2
x_max = x_min + width
y_max = y_min + height
if "points" not in prediction:
xyxy.append([x_min, y_min, x_max, y_max])
class_id.append(prediction["class_id"])
confidence.append(prediction["confidence"])
elif len(prediction["points"]) >= 3:
polygon = np.array(
[[point["x"], point["y"]] for point in prediction["points"]], dtype=int
)
mask = polygon_to_mask(polygon, resolution_wh=(image_width, image_height))
xyxy.append([x_min, y_min, x_max, y_max])
class_id.append(prediction["class_id"])
confidence.append(prediction["confidence"])
masks.append(mask)
xyxy = np.array(xyxy) if len(xyxy) > 0 else np.empty((0, 4))
confidence = np.array(confidence) if len(confidence) > 0 else np.empty(0)
class_id = np.array(class_id).astype(int) if len(class_id) > 0 else np.empty(0)
masks = np.array(masks, dtype=bool) if len(masks) > 0 else None
return xyxy, confidence, class_id, masks
def move_boxes(xyxy: np.ndarray, offset: np.ndarray) -> np.ndarray:
"""
Args:
xyxy (np.ndarray): An array of shape `(n, 4)` containing the bounding boxes
coordinates in format `[x1, y1, x2, y2]`
offset (np.array): An array of shape `(2,)` containing offset values in format
is `[dx, dy]`.
Returns:
(np.ndarray) repositioned bounding boxes
"""
return xyxy + np.hstack([offset, offset])
+4 -2
View File
@@ -42,7 +42,8 @@ class Color:
"""
Creates a Color instance from a color hex string
:param color_hex: str : The color hex string in the format of "fff", "ffffff", "#fff", or "#ffffff"
:param color_hex: str : The color hex string in the format
of "fff", "ffffff", "#fff", or "#ffffff"
:return: Color : A Color instance representing the color
Example:
@@ -105,7 +106,8 @@ class ColorPalette:
"""
Creates a ColorPalette instance from a list of color hex strings
:param color_hex_list: List[str] : A list of color hex strings in the format of "fff", "ffffff", "#fff", or "#ffffff"
:param color_hex_list: List[str] : A list of color hex strings in the
format of "fff", "ffffff", "#fff", or "#ffffff"
:return: ColorPalette : A ColorPalette instance representing the color palette
Example:
+10 -6
View File
@@ -115,15 +115,19 @@ def draw_text(
Draw text with background on a scene.
Parameters:
scene (np.ndarray): A 2-dimensional numpy ndarray representing an image or scene.
scene (np.ndarray): A 2-dimensional numpy ndarray representing an image or scene
text (str): The text to be drawn.
text_anchor (Point): The anchor point for the text, represented as a Point object with x and y attributes.
text_anchor (Point): The anchor point for the text, represented as a
Point object with x and y attributes.
text_color (Color, optional): The color of the text. Defaults to black.
text_scale (float, optional): The scale of the text. Defaults to 0.5.
text_thickness (int, optional): The thickness of the text. Defaults to 1.
text_padding (int, optional): The amount of padding to add around the text when drawing a rectangle in the background. Defaults to 10.
text_font (int, optional): The font to use for the text. Defaults to cv2.FONT_HERSHEY_SIMPLEX.
background_color (Color, optional): The color of the background rectangle, if one is to be drawn. Defaults to None.
text_padding (int, optional): The amount of padding to add around the text
when drawing a rectangle in the background. Defaults to 10.
text_font (int, optional): The font to use for the text.
Defaults to cv2.FONT_HERSHEY_SIMPLEX.
background_color (Color, optional): The color of the background rectangle,
if one is to be drawn. Defaults to None.
Returns:
np.ndarray: The input scene with the text drawn on it.
@@ -132,7 +136,7 @@ def draw_text(
```python
>>> scene = np.zeros((100, 100, 3), dtype=np.uint8)
>>> text_anchor = Point(x=50, y=50)
>>> scene = draw_text(scene=scene, text="Hello, world!", text_anchor=text_anchor)
>>> scene = draw_text(scene=scene, text="Hello, world!",text_anchor=text_anchor)
```
"""
text_width, text_height = cv2.getTextSize(
-22
View File
@@ -1,22 +0,0 @@
from pathlib import Path
from typing import List, Optional, Union
def write_text_to_file(content: str, output_path: str) -> None:
with open(output_path, "w") as file:
file.write(content)
def list_files_with_extensions(
directory: Union[str, Path], extensions: Optional[List[str]] = None
) -> List[Path]:
directory = Path(directory)
files_with_extensions = []
if extensions is not None:
for ext in extensions:
files_with_extensions.extend(directory.glob(f"*.{ext}"))
else:
files_with_extensions.extend(directory.glob("*"))
return files_with_extensions
+11
View File
@@ -6,8 +6,19 @@ from typing import Tuple
class Position(Enum):
"""
Enum representing the position of an anchor point.
"""
CENTER = "CENTER"
CENTER_LEFT = "CENTER_LEFT"
CENTER_RIGHT = "CENTER_RIGHT"
TOP_CENTER = "TOP_CENTER"
TOP_LEFT = "TOP_LEFT"
TOP_RIGHT = "TOP_RIGHT"
BOTTOM_LEFT = "BOTTOM_LEFT"
BOTTOM_CENTER = "BOTTOM_CENTER"
BOTTOM_RIGHT = "BOTTOM_RIGHT"
@classmethod
def list(cls):
+8 -3
View File
@@ -7,13 +7,18 @@ def get_polygon_center(polygon: np.ndarray) -> Point:
"""
Calculate the center of a polygon.
This function takes in a polygon as a 2-dimensional numpy ndarray and returns the center of the polygon as a Point object. The center is calculated as the mean of the polygon's vertices along each axis, and is rounded down to the nearest integer.
This function takes in a polygon as a 2-dimensional numpy ndarray and
returns the center of the polygon as a Point object.
The center is calculated as the mean of the polygon's vertices along each axis,
and is rounded down to the nearest integer.
Parameters:
polygon (np.ndarray): A 2-dimensional numpy ndarray representing the vertices of the polygon.
polygon (np.ndarray): A 2-dimensional numpy ndarray representing the
vertices of the polygon.
Returns:
Point: The center of the polygon, represented as a Point object with x and y attributes.
Point: The center of the polygon, represented as a
Point object with x and y attributes.
Examples:
```python
View File
+843
View File
@@ -0,0 +1,843 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable, List, Optional, Tuple
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from supervision.dataset.core import DetectionDataset
from supervision.detection.core import Detections
from supervision.detection.utils import box_iou_batch
def detections_to_tensor(
detections: Detections, with_confidence: bool = False
) -> np.ndarray:
"""
Convert Supervision Detections to numpy tensors for further computation
Args:
detections (sv.Detections): Detections/Targets in the format of sv.Detections
with_confidence (bool): Whether to include confidence in the tensor
Returns:
(np.ndarray): Detections as numpy tensors as in (xyxy, class_id,
confidence) order
"""
if detections.class_id is None:
raise ValueError(
"ConfusionMatrix can only be calculated for Detections with class_id"
)
arrays_to_concat = [detections.xyxy, np.expand_dims(detections.class_id, 1)]
if with_confidence:
if detections.confidence is None:
raise ValueError(
"ConfusionMatrix can only be calculated for Detections with confidence"
)
arrays_to_concat.append(np.expand_dims(detections.confidence, 1))
return np.concatenate(arrays_to_concat, axis=1)
def validate_input_tensors(predictions: List[np.ndarray], targets: List[np.ndarray]):
"""
Checks for shape consistency of input tensors.
"""
if len(predictions) != len(targets):
raise ValueError(
f"Number of predictions ({len(predictions)}) and"
f"targets ({len(targets)}) must be equal."
)
if len(predictions) > 0:
if not isinstance(predictions[0], np.ndarray) or not isinstance(
targets[0], np.ndarray
):
raise ValueError(
f"Predictions and targets must be lists of numpy arrays."
f"Got {type(predictions[0])} and {type(targets[0])} instead."
)
if predictions[0].shape[1] != 6:
raise ValueError(
f"Predictions must have shape (N, 6)."
f"Got {predictions[0].shape} instead."
)
if targets[0].shape[1] != 5:
raise ValueError(
f"Targets must have shape (N, 5). Got {targets[0].shape} instead."
)
@dataclass
class ConfusionMatrix:
"""
Confusion matrix for object detection tasks.
Attributes:
matrix (np.ndarray): An 2D `np.ndarray` of shape
`(len(classes) + 1, len(classes) + 1)`
containing the number of `TP`, `FP`, `FN` and `TN` for each class.
classes (List[str]): Model class names.
conf_threshold (float): Detection confidence threshold between `0` and `1`.
Detections with lower confidence will be excluded from the matrix.
iou_threshold (float): Detection IoU threshold between `0` and `1`.
Detections with lower IoU will be classified as `FP`.
"""
matrix: np.ndarray
classes: List[str]
conf_threshold: float
iou_threshold: float
@classmethod
def from_detections(
cls,
predictions: List[Detections],
targets: List[Detections],
classes: List[str],
conf_threshold: float = 0.3,
iou_threshold: float = 0.5,
) -> ConfusionMatrix:
"""
Calculate confusion matrix based on predicted and ground-truth detections.
Args:
targets (List[Detections]): Detections objects from ground-truth.
predictions (List[Detections]): Detections objects predicted by the model.
classes (List[str]): Model class names.
conf_threshold (float): Detection confidence threshold between `0` and `1`.
Detections with lower confidence will be excluded.
iou_threshold (float): Detection IoU threshold between `0` and `1`.
Detections with lower IoU will be classified as `FP`.
Returns:
ConfusionMatrix: New instance of ConfusionMatrix.
Example:
```python
>>> import supervision as sv
>>> targets = [
... sv.Detections(...),
... sv.Detections(...)
... ]
>>> predictions = [
... sv.Detections(...),
... sv.Detections(...)
... ]
>>> confusion_matrix = sv.ConfusionMatrix.from_detections(
... predictions=predictions,
... targets=target,
... classes=['person', ...]
... )
>>> confusion_matrix.matrix
array([
[0., 0., 0., 0.],
[0., 1., 0., 1.],
[0., 1., 1., 0.],
[1., 1., 0., 0.]
])
```
"""
prediction_tensors = []
target_tensors = []
for prediction, target in zip(predictions, targets):
prediction_tensors.append(
detections_to_tensor(prediction, with_confidence=True)
)
target_tensors.append(detections_to_tensor(target, with_confidence=False))
return cls.from_tensors(
predictions=prediction_tensors,
targets=target_tensors,
classes=classes,
conf_threshold=conf_threshold,
iou_threshold=iou_threshold,
)
@classmethod
def from_tensors(
cls,
predictions: List[np.ndarray],
targets: List[np.ndarray],
classes: List[str],
conf_threshold: float = 0.3,
iou_threshold: float = 0.5,
) -> ConfusionMatrix:
"""
Calculate confusion matrix based on predicted and ground-truth detections.
Args:
predictions (List[np.ndarray]): Each element of the list describes a single
image and has `shape = (M, 6)` where `M` is the number of detected
objects. Each row is expected to be in
`(x_min, y_min, x_max, y_max, class, conf)` format.
targets (List[np.ndarray]): Each element of the list describes a single
image and has `shape = (N, 5)` where `N` is the number of
ground-truth objects. Each row is expected to be in
`(x_min, y_min, x_max, y_max, class)` format.
classes (List[str]): Model class names.
conf_threshold (float): Detection confidence threshold between `0` and `1`.
Detections with lower confidence will be excluded.
iou_threshold (float): Detection iou threshold between `0` and `1`.
Detections with lower iou will be classified as `FP`.
Returns:
ConfusionMatrix: New instance of ConfusionMatrix.
Example:
```python
>>> import supervision as sv
>>> targets = (
... [
... array(
... [
... [0.0, 0.0, 3.0, 3.0, 1],
... [2.0, 2.0, 5.0, 5.0, 1],
... [6.0, 1.0, 8.0, 3.0, 2],
... ]
... ),
... array([1.0, 1.0, 2.0, 2.0, 2]),
... ]
... )
>>> predictions = [
... array(
... [
... [0.0, 0.0, 3.0, 3.0, 1, 0.9],
... [0.1, 0.1, 3.0, 3.0, 0, 0.9],
... [6.0, 1.0, 8.0, 3.0, 1, 0.8],
... [1.0, 6.0, 2.0, 7.0, 1, 0.8],
... ]
... ),
... array([[1.0, 1.0, 2.0, 2.0, 2, 0.8]])
... ]
>>> confusion_matrix = sv.ConfusionMatrix.from_tensors(
... predictions=predictions,
... targets=targets,
... classes=['person', ...]
... )
>>> confusion_matrix.matrix
array([
[0., 0., 0., 0.],
[0., 1., 0., 1.],
[0., 1., 1., 0.],
[1., 1., 0., 0.]
])
```
"""
validate_input_tensors(predictions, targets)
num_classes = len(classes)
matrix = np.zeros((num_classes + 1, num_classes + 1))
for true_batch, detection_batch in zip(targets, predictions):
matrix += cls.evaluate_detection_batch(
predictions=detection_batch,
targets=true_batch,
num_classes=num_classes,
conf_threshold=conf_threshold,
iou_threshold=iou_threshold,
)
return cls(
matrix=matrix,
classes=classes,
conf_threshold=conf_threshold,
iou_threshold=iou_threshold,
)
@staticmethod
def evaluate_detection_batch(
predictions: np.ndarray,
targets: np.ndarray,
num_classes: int,
conf_threshold: float,
iou_threshold: float,
) -> np.ndarray:
"""
Calculate confusion matrix for a batch of detections for a single image.
Args:
predictions (np.ndarray): Batch prediction. Describes a single image and
has `shape = (M, 6)` where `M` is the number of detected objects.
Each row is expected to be in
`(x_min, y_min, x_max, y_max, class, conf)` format.
targets (np.ndarray): Batch target labels. Describes a single image and
has `shape = (N, 5)` where `N` is the number of ground-truth objects.
Each row is expected to be in
`(x_min, y_min, x_max, y_max, class)` format.
num_classes (int): Number of classes.
conf_threshold (float): Detection confidence threshold between `0` and `1`.
Detections with lower confidence will be excluded.
iou_threshold (float): Detection iou threshold between `0` and `1`.
Detections with lower iou will be classified as `FP`.
Returns:
np.ndarray: Confusion matrix based on a single image.
"""
result_matrix = np.zeros((num_classes + 1, num_classes + 1))
conf_idx = 5
confidence = predictions[:, conf_idx]
detection_batch_filtered = predictions[confidence > conf_threshold]
class_id_idx = 4
true_classes = np.array(targets[:, class_id_idx], dtype=np.int16)
detection_classes = np.array(
detection_batch_filtered[:, class_id_idx], dtype=np.int16
)
true_boxes = targets[:, :class_id_idx]
detection_boxes = detection_batch_filtered[:, :class_id_idx]
iou_batch = box_iou_batch(
boxes_true=true_boxes, boxes_detection=detection_boxes
)
matched_idx = np.asarray(iou_batch > iou_threshold).nonzero()
if matched_idx[0].shape[0]:
matches = np.stack(
(matched_idx[0], matched_idx[1], iou_batch[matched_idx]), axis=1
)
matches = ConfusionMatrix._drop_extra_matches(matches=matches)
else:
matches = np.zeros((0, 3))
matched_true_idx, matched_detection_idx, _ = matches.transpose().astype(
np.int16
)
for i, true_class_value in enumerate(true_classes):
j = matched_true_idx == i
if matches.shape[0] > 0 and sum(j) == 1:
result_matrix[
true_class_value, detection_classes[matched_detection_idx[j]]
] += 1 # TP
else:
result_matrix[true_class_value, num_classes] += 1 # FN
for i, detection_class_value in enumerate(detection_classes):
if not any(matched_detection_idx == i):
result_matrix[num_classes, detection_class_value] += 1 # FP
return result_matrix
@staticmethod
def _drop_extra_matches(matches: np.ndarray) -> np.ndarray:
"""
Deduplicate matches. If there are multiple matches for the same true or
predicted box, only the one with the highest IoU is kept.
"""
if matches.shape[0] > 0:
matches = matches[matches[:, 2].argsort()[::-1]]
matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
matches = matches[matches[:, 2].argsort()[::-1]]
matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
return matches
@classmethod
def benchmark(
cls,
dataset: DetectionDataset,
callback: Callable[[np.ndarray], Detections],
conf_threshold: float = 0.3,
iou_threshold: float = 0.5,
) -> ConfusionMatrix:
"""
Calculate confusion matrix from dataset and callback function.
Args:
dataset (DetectionDataset): Object detection dataset used for evaluation.
callback (Callable[[np.ndarray], Detections]): Function that takes an image
as input and returns Detections object.
conf_threshold (float): Detection confidence threshold between `0` and `1`.
Detections with lower confidence will be excluded.
iou_threshold (float): Detection IoU threshold between `0` and `1`.
Detections with lower IoU will be classified as `FP`.
Returns:
ConfusionMatrix: New instance of ConfusionMatrix.
Example:
```python
>>> import supervision as sv
>>> from ultralytics import YOLO
>>> dataset = sv.DetectionDataset.from_yolo(...)
>>> model = YOLO(...)
>>> def callback(image: np.ndarray) -> sv.Detections:
... result = model(image)[0]
... return sv.Detections.from_ultralytics(result)
>>> confusion_matrix = sv.ConfusionMatrix.benchmark(
... dataset = dataset,
... callback = callback
... )
>>> confusion_matrix.matrix
array([
[0., 0., 0., 0.],
[0., 1., 0., 1.],
[0., 1., 1., 0.],
[1., 1., 0., 0.]
])
```
"""
predictions, targets = [], []
for img_name, img in dataset.images.items():
predictions_batch = callback(img)
predictions.append(predictions_batch)
targets_batch = dataset.annotations[img_name]
targets.append(targets_batch)
return cls.from_detections(
predictions=predictions,
targets=targets,
classes=dataset.classes,
conf_threshold=conf_threshold,
iou_threshold=iou_threshold,
)
def plot(
self,
save_path: Optional[str] = None,
title: Optional[str] = None,
classes: Optional[List[str]] = None,
normalize: bool = False,
fig_size: Tuple[int, int] = (12, 10),
) -> matplotlib.figure.Figure:
"""
Create confusion matrix plot and save it at selected location.
Args:
save_path (Optional[str]): Path to save the plot. If not provided,
plot will be displayed.
title (Optional[str]): Title of the plot.
classes (Optional[List[str]]): List of classes to be displayed on the plot.
If not provided, all classes will be displayed.
normalize (bool): If True, normalize the confusion matrix.
fig_size (Tuple[int, int]): Size of the plot.
Returns:
matplotlib.figure.Figure: Confusion matrix plot.
"""
array = self.matrix.copy()
if normalize:
eps = 1e-8
array = array / (array.sum(0).reshape(1, -1) + eps)
array[array < 0.005] = np.nan
fig, ax = plt.subplots(figsize=fig_size, tight_layout=True, facecolor="white")
class_names = classes if classes is not None else self.classes
use_labels_for_ticks = class_names is not None and (0 < len(class_names) < 99)
if use_labels_for_ticks:
x_tick_labels = class_names + ["FN"]
y_tick_labels = class_names + ["FP"]
num_ticks = len(x_tick_labels)
else:
x_tick_labels = None
y_tick_labels = None
num_ticks = len(array)
im = ax.imshow(array, cmap="Blues")
cbar = ax.figure.colorbar(im, ax=ax)
cbar.mappable.set_clim(vmin=0, vmax=np.nanmax(array))
if x_tick_labels is None:
tick_interval = 2
else:
tick_interval = 1
ax.set_xticks(np.arange(0, num_ticks, tick_interval), labels=x_tick_labels)
ax.set_yticks(np.arange(0, num_ticks, tick_interval), labels=y_tick_labels)
plt.setp(ax.get_xticklabels(), rotation=90, ha="right", rotation_mode="default")
labelsize = 10 if num_ticks < 50 else 8
ax.tick_params(axis="both", which="both", labelsize=labelsize)
if num_ticks < 30:
for i in range(array.shape[0]):
for j in range(array.shape[1]):
n_preds = array[i, j]
if not np.isnan(n_preds):
ax.text(
j,
i,
f"{n_preds:.2f}" if normalize else f"{n_preds:.0f}",
ha="center",
va="center",
color="black"
if n_preds < 0.5 * np.nanmax(array)
else "white",
)
if title:
ax.set_title(title, fontsize=20)
ax.set_xlabel("Predicted")
ax.set_ylabel("True")
ax.set_facecolor("white")
if save_path:
fig.savefig(
save_path, dpi=250, facecolor=fig.get_facecolor(), transparent=True
)
return fig
@dataclass(frozen=True)
class MeanAveragePrecision:
"""
Mean Average Precision for object detection tasks.
Attributes:
map50_95 (float): Mean Average Precision (mAP) calculated over IoU thresholds
ranging from `0.50` to `0.95` with a step size of `0.05`.
map50 (float): Mean Average Precision (mAP) calculated specifically at
an IoU threshold of `0.50`.
map75 (float): Mean Average Precision (mAP) calculated specifically at
an IoU threshold of `0.75`.
per_class_ap50_95 (np.ndarray): Average Precision (AP) values calculated over
IoU thresholds ranging from `0.50` to `0.95` with a step size of `0.05`,
provided for each individual class.
"""
map50_95: float
map50: float
map75: float
per_class_ap50_95: np.ndarray
@classmethod
def from_detections(
cls,
predictions: List[Detections],
targets: List[Detections],
) -> MeanAveragePrecision:
"""
Calculate mean average precision based on predicted and ground-truth detections.
Args:
targets (List[Detections]): Detections objects from ground-truth.
predictions (List[Detections]): Detections objects predicted by the model.
Returns:
MeanAveragePrecision: New instance of ConfusionMatrix.
Example:
```python
>>> import supervision as sv
>>> targets = [
... sv.Detections(...),
... sv.Detections(...)
... ]
>>> predictions = [
... sv.Detections(...),
... sv.Detections(...)
... ]
>>> mean_average_precision = sv.MeanAveragePrecision.from_detections(
... predictions=predictions,
... targets=target,
... )
>>> mean_average_precison.map50_95
0.2899
```
"""
prediction_tensors = []
target_tensors = []
for prediction, target in zip(predictions, targets):
prediction_tensors.append(
detections_to_tensor(prediction, with_confidence=True)
)
target_tensors.append(detections_to_tensor(target, with_confidence=False))
return cls.from_tensors(
predictions=prediction_tensors,
targets=target_tensors,
)
@classmethod
def benchmark(
cls,
dataset: DetectionDataset,
callback: Callable[[np.ndarray], Detections],
) -> MeanAveragePrecision:
"""
Calculate mean average precision from dataset and callback function.
Args:
dataset (DetectionDataset): Object detection dataset used for evaluation.
callback (Callable[[np.ndarray], Detections]): Function that takes
an image as input and returns Detections object.
Returns:
MeanAveragePrecision: New instance of MeanAveragePrecision.
Example:
```python
>>> import supervision as sv
>>> from ultralytics import YOLO
>>> dataset = sv.DetectionDataset.from_yolo(...)
>>> model = YOLO(...)
>>> def callback(image: np.ndarray) -> sv.Detections:
... result = model(image)[0]
... return sv.Detections.from_ultralytics(result)
>>> mean_average_precision = sv.MeanAveragePrecision.benchmark(
... dataset = dataset,
... callback = callback
... )
>>> mean_average_precision.map50_95
0.433
```
"""
predictions, targets = [], []
for img_name, img in dataset.images.items():
predictions_batch = callback(img)
predictions.append(predictions_batch)
targets_batch = dataset.annotations[img_name]
targets.append(targets_batch)
return cls.from_detections(
predictions=predictions,
targets=targets,
)
@classmethod
def from_tensors(
cls,
predictions: List[np.ndarray],
targets: List[np.ndarray],
) -> MeanAveragePrecision:
"""
Calculate Mean Average Precision based on predicted and ground-truth
detections at different threshold.
Args:
predictions (List[np.ndarray]): Each element of the list describes
a single image and has `shape = (M, 6)` where `M` is
the number of detected objects. Each row is expected to be
in `(x_min, y_min, x_max, y_max, class, conf)` format.
targets (List[np.ndarray]): Each element of the list describes a single
image and has `shape = (N, 5)` where `N` is the
number of ground-truth objects. Each row is expected to be in
`(x_min, y_min, x_max, y_max, class)` format.
Returns:
MeanAveragePrecision: New instance of MeanAveragePrecision.
Example:
```python
>>> import supervision as sv
>>> targets = (
... [
... array(
... [
... [0.0, 0.0, 3.0, 3.0, 1],
... [2.0, 2.0, 5.0, 5.0, 1],
... [6.0, 1.0, 8.0, 3.0, 2],
... ]
... ),
... array([1.0, 1.0, 2.0, 2.0, 2]),
... ]
... )
>>> predictions = [
... array(
... [
... [0.0, 0.0, 3.0, 3.0, 1, 0.9],
... [0.1, 0.1, 3.0, 3.0, 0, 0.9],
... [6.0, 1.0, 8.0, 3.0, 1, 0.8],
... [1.0, 6.0, 2.0, 7.0, 1, 0.8],
... ]
... ),
... array([[1.0, 1.0, 2.0, 2.0, 2, 0.8]])
... ]
>>> mean_average_precison = sv.MeanAveragePrecision.from_tensors(
... predictions=predictions,
... targets=targets,
... )
>>> mean_average_precison.map50_95
0.2899
```
"""
validate_input_tensors(predictions, targets)
iou_thresholds = np.linspace(0.5, 0.95, 10)
stats = []
# Gather matching stats for predictions and targets
for true_objs, predicted_objs in zip(targets, predictions):
if predicted_objs.shape[0] == 0:
if true_objs.shape[0]:
stats.append(
(
np.zeros((0, iou_thresholds.size), dtype=bool),
*np.zeros((2, 0)),
true_objs[:, 4],
)
)
continue
if true_objs.shape[0]:
matches = cls._match_detection_batch(
predicted_objs, true_objs, iou_thresholds
)
stats.append(
(
matches,
predicted_objs[:, 5],
predicted_objs[:, 4],
true_objs[:, 4],
)
)
# Compute average precisions if any matches exist
if stats:
concatenated_stats = [np.concatenate(items, 0) for items in zip(*stats)]
average_precisions = cls._average_precisions_per_class(*concatenated_stats)
map50 = average_precisions[:, 0].mean()
map75 = average_precisions[:, 5].mean()
map50_95 = average_precisions.mean()
else:
map50, map75, map50_95 = 0, 0, 0
average_precisions = []
return cls(
map50_95=map50_95,
map50=map50,
map75=map75,
per_class_ap50_95=average_precisions,
)
@staticmethod
def compute_average_precision(recall: np.ndarray, precision: np.ndarray) -> float:
"""
Compute the average precision using 101-point interpolation (COCO), given
the recall and precision curves.
Args:
recall (np.ndarray): The recall curve.
precision (np.ndarray): The precision curve.
Returns:
float: Average precision.
"""
extended_recall = np.concatenate(([0.0], recall, [1.0]))
extended_precision = np.concatenate(([1.0], precision, [0.0]))
max_accumulated_precision = np.flip(
np.maximum.accumulate(np.flip(extended_precision))
)
interpolated_recall_levels = np.linspace(0, 1, 101)
interpolated_precision = np.interp(
interpolated_recall_levels, extended_recall, max_accumulated_precision
)
average_precision = np.trapz(interpolated_precision, interpolated_recall_levels)
return average_precision
@staticmethod
def _match_detection_batch(
predictions: np.ndarray, targets: np.ndarray, iou_thresholds: np.ndarray
) -> np.ndarray:
"""
Match predictions with target labels based on IoU levels.
Args:
predictions (np.ndarray): Batch prediction. Describes a single image and
has `shape = (M, 6)` where `M` is the number of detected objects.
Each row is expected to be in
`(x_min, y_min, x_max, y_max, class, conf)` format.
targets (np.ndarray): Batch target labels. Describes a single image and
has `shape = (N, 5)` where `N` is the number of ground-truth objects.
Each row is expected to be in
`(x_min, y_min, x_max, y_max, class)` format.
iou_thresholds (np.ndarray): Array contains different IoU thresholds.
Returns:
np.ndarray: Matched prediction with target labels result.
"""
num_predictions, num_iou_levels = predictions.shape[0], iou_thresholds.shape[0]
correct = np.zeros((num_predictions, num_iou_levels), dtype=bool)
iou = box_iou_batch(targets[:, :4], predictions[:, :4])
correct_class = targets[:, 4:5] == predictions[:, 4]
for i, iou_level in enumerate(iou_thresholds):
matched_indices = np.where((iou >= iou_level) & correct_class)
if matched_indices[0].shape[0]:
combined_indices = np.stack(matched_indices, axis=1)
iou_values = iou[matched_indices][:, None]
matches = np.hstack([combined_indices, iou_values])
if matched_indices[0].shape[0] > 1:
matches = matches[matches[:, 2].argsort()[::-1]]
matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
correct[matches[:, 1].astype(int), i] = True
return correct
@staticmethod
def _average_precisions_per_class(
matches: np.ndarray,
prediction_confidence: np.ndarray,
prediction_class_ids: np.ndarray,
true_class_ids: np.ndarray,
eps: float = 1e-16,
) -> np.ndarray:
"""
Compute the average precision, given the recall and precision curves.
Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
Args:
matches (np.ndarray): True positives.
prediction_confidence (np.ndarray): Objectness value from 0-1.
prediction_class_ids (np.ndarray): Predicted object classes.
true_class_ids (np.ndarray): True object classes.
eps (float, optional): Small value to prevent division by zero.
Returns:
np.ndarray: Average precision for different IoU levels.
"""
sorted_indices = np.argsort(-prediction_confidence)
matches = matches[sorted_indices]
prediction_class_ids = prediction_class_ids[sorted_indices]
unique_classes, class_counts = np.unique(true_class_ids, return_counts=True)
num_classes = unique_classes.shape[0]
average_precisions = np.zeros((num_classes, matches.shape[1]))
for class_idx, class_id in enumerate(unique_classes):
is_class = prediction_class_ids == class_id
total_true = class_counts[class_idx]
total_prediction = is_class.sum()
if total_prediction == 0 or total_true == 0:
continue
false_positives = (1 - matches[is_class]).cumsum(0)
true_positives = matches[is_class].cumsum(0)
recall = true_positives / (total_true + eps)
precision = true_positives / (true_positives + false_positives)
for iou_level_idx in range(matches.shape[1]):
average_precisions[
class_idx, iou_level_idx
] = MeanAveragePrecision.compute_average_precision(
recall[:, iou_level_idx], precision[:, iou_level_idx]
)
return average_precisions
View File
@@ -0,0 +1,55 @@
from collections import OrderedDict
from enum import Enum
import numpy as np
class TrackState(Enum):
New = 0
Tracked = 1
Lost = 2
Removed = 3
class BaseTrack:
_count = 0
def __init__(self):
self.track_id = 0
self.is_activated = False
self.state = TrackState.New
self.history = OrderedDict()
self.features = []
self.curr_feature = None
self.score = 0
self.start_frame = 0
self.frame_id = 0
self.time_since_update = 0
# multi-camera
self.location = (np.inf, np.inf)
@property
def end_frame(self) -> int:
return self.frame_id
@staticmethod
def next_id() -> int:
BaseTrack._count += 1
return BaseTrack._count
def activate(self, *args):
raise NotImplementedError
def predict(self):
raise NotImplementedError
def update(self, *args, **kwargs):
raise NotImplementedError
def mark_lost(self):
self.state = TrackState.Lost
def mark_removed(self):
self.state = TrackState.Removed
+475
View File
@@ -0,0 +1,475 @@
from typing import List, Tuple
import numpy as np
from supervision.detection.core import Detections
from supervision.tracker.byte_tracker import matching
from supervision.tracker.byte_tracker.basetrack import BaseTrack, TrackState
from supervision.tracker.byte_tracker.kalman_filter import KalmanFilter
class STrack(BaseTrack):
shared_kalman = KalmanFilter()
def __init__(self, tlwh, score, class_ids):
# wait activate
self._tlwh = np.asarray(tlwh, dtype=np.float32)
self.kalman_filter = None
self.mean, self.covariance = None, None
self.is_activated = False
self.score = score
self.class_ids = class_ids
self.tracklet_len = 0
def predict(self):
mean_state = self.mean.copy()
if self.state != TrackState.Tracked:
mean_state[7] = 0
self.mean, self.covariance = self.kalman_filter.predict(
mean_state, self.covariance
)
@staticmethod
def multi_predict(stracks):
if len(stracks) > 0:
multi_mean = []
multi_covariance = []
for i, st in enumerate(stracks):
multi_mean.append(st.mean.copy())
multi_covariance.append(st.covariance)
if st.state != TrackState.Tracked:
multi_mean[i][7] = 0
multi_mean, multi_covariance = STrack.shared_kalman.multi_predict(
np.asarray(multi_mean), np.asarray(multi_covariance)
)
for i, (mean, cov) in enumerate(zip(multi_mean, multi_covariance)):
stracks[i].mean = mean
stracks[i].covariance = cov
def activate(self, kalman_filter, frame_id):
"""Start a new tracklet"""
self.kalman_filter = kalman_filter
self.track_id = self.next_id()
self.mean, self.covariance = self.kalman_filter.initiate(
self.tlwh_to_xyah(self._tlwh)
)
self.tracklet_len = 0
self.state = TrackState.Tracked
if frame_id == 1:
self.is_activated = True
self.frame_id = frame_id
self.start_frame = frame_id
def re_activate(self, new_track, frame_id, new_id=False):
self.mean, self.covariance = self.kalman_filter.update(
self.mean, self.covariance, self.tlwh_to_xyah(new_track.tlwh)
)
self.tracklet_len = 0
self.state = TrackState.Tracked
self.is_activated = True
self.frame_id = frame_id
if new_id:
self.track_id = self.next_id()
self.score = new_track.score
def update(self, new_track, frame_id):
"""
Update a matched track
:type new_track: STrack
:type frame_id: int
:type update_feature: bool
:return:
"""
self.frame_id = frame_id
self.tracklet_len += 1
new_tlwh = new_track.tlwh
self.mean, self.covariance = self.kalman_filter.update(
self.mean, self.covariance, self.tlwh_to_xyah(new_tlwh)
)
self.state = TrackState.Tracked
self.is_activated = True
self.score = new_track.score
@property
def tlwh(self):
"""Get current position in bounding box format `(top left x, top left y,
width, height)`.
"""
if self.mean is None:
return self._tlwh.copy()
ret = self.mean[:4].copy()
ret[2] *= ret[3]
ret[:2] -= ret[2:] / 2
return ret
@property
def tlbr(self):
"""Convert bounding box to format `(min x, min y, max x, max y)`, i.e.,
`(top left, bottom right)`.
"""
ret = self.tlwh.copy()
ret[2:] += ret[:2]
return ret
@staticmethod
def tlwh_to_xyah(tlwh):
"""Convert bounding box to format `(center x, center y, aspect ratio,
height)`, where the aspect ratio is `width / height`.
"""
ret = np.asarray(tlwh).copy()
ret[:2] += ret[2:] / 2
ret[2] /= ret[3]
return ret
def to_xyah(self):
return self.tlwh_to_xyah(self.tlwh)
@staticmethod
def tlbr_to_tlwh(tlbr):
ret = np.asarray(tlbr).copy()
ret[2:] -= ret[:2]
return ret
@staticmethod
def tlwh_to_tlbr(tlwh):
ret = np.asarray(tlwh).copy()
ret[2:] += ret[:2]
return ret
def __repr__(self):
return "OT_{}_({}-{})".format(self.track_id, self.start_frame, self.end_frame)
def detections2boxes(detections: Detections) -> np.ndarray:
"""
Convert Supervision Detections to numpy tensors for further computation.
Args:
detections (Detections): Detections/Targets in the format of sv.Detections.
Returns:
(np.ndarray): Detections as numpy tensors as in
`(x_min, y_min, x_max, y_max, confidence, class_id)` order.
"""
return np.hstack(
(
detections.xyxy,
detections.confidence[:, np.newaxis],
detections.class_id[:, np.newaxis],
)
)
class ByteTrack:
"""
Initialize the ByteTrack object.
Parameters:
track_thresh (float, optional): Detection confidence threshold
for track activation.
track_buffer (int, optional): Number of frames to buffer when a track is lost.
match_thresh (float, optional): Threshold for matching tracks with detections.
frame_rate (int, optional): The frame rate of the video.
"""
def __init__(
self,
track_thresh: float = 0.25,
track_buffer: int = 30,
match_thresh: float = 0.8,
frame_rate: int = 30,
):
self.track_thresh = track_thresh
self.match_thresh = match_thresh
self.frame_id = 0
self.det_thresh = self.track_thresh + 0.1
self.max_time_lost = int(frame_rate / 30.0 * track_buffer)
self.kalman_filter = KalmanFilter()
self.tracked_tracks: List[STrack] = []
self.lost_tracks: List[STrack] = []
self.removed_tracks: List[STrack] = []
def update_with_detections(self, detections: Detections) -> Detections:
"""
Updates the tracker with the provided detections and
returns the updated detection results.
Parameters:
detections: The new detections to update with.
Returns:
Detection: The updated detection results that now include tracking IDs.
Example:
```python
>>> import supervision as sv
>>> from ultralytics import YOLO
>>> model = YOLO(...)
>>> byte_tracker = sv.ByteTrack()
>>> annotator = sv.BoxAnnotator()
>>> def callback(frame: np.ndarray, index: int) -> np.ndarray:
... results = model(frame)[0]
... detections = sv.Detections.from_ultralytics(results)
... detections = byte_tracker.update_with_detections(detections)
... labels = [
... f"#{tracker_id} {model.model.names[class_id]} {confidence:0.2f}"
... for _, _, confidence, class_id, tracker_id
... in detections
... ]
... return annotator.annotate(scene=frame.copy(),
... detections=detections, labels=labels)
>>> sv.process_video(
... source_path='...',
... target_path='...',
... callback=callback
... )
```
"""
tracks = self.update_with_tensors(
tensors=detections2boxes(detections=detections)
)
detections = Detections.empty()
if len(tracks) > 0:
detections.xyxy = np.array(
[track.tlbr for track in tracks], dtype=np.float32
)
detections.class_id = np.array(
[int(t.class_ids) for t in tracks], dtype=int
)
detections.tracker_id = np.array(
[int(t.track_id) for t in tracks], dtype=int
)
detections.confidence = np.array(
[t.score for t in tracks], dtype=np.float32
)
return detections
def update_with_tensors(self, tensors: np.ndarray) -> List[STrack]:
"""
Updates the tracker with the provided tensors and returns the updated tracks.
Parameters:
tensors: The new tensors to update with.
Returns:
List[STrack]: Updated tracks.
"""
self.frame_id += 1
activated_starcks = []
refind_stracks = []
lost_stracks = []
removed_stracks = []
class_ids = tensors[:, 5]
scores = tensors[:, 4]
bboxes = tensors[:, :4]
remain_inds = scores > self.track_thresh
inds_low = scores > 0.1
inds_high = scores < self.track_thresh
inds_second = np.logical_and(inds_low, inds_high)
dets_second = bboxes[inds_second]
dets = bboxes[remain_inds]
scores_keep = scores[remain_inds]
scores_second = scores[inds_second]
class_ids_keep = class_ids[remain_inds]
class_ids_second = class_ids[inds_second]
if len(dets) > 0:
"""Detections"""
detections = [
STrack(STrack.tlbr_to_tlwh(tlbr), s, c)
for (tlbr, s, c) in zip(dets, scores_keep, class_ids_keep)
]
else:
detections = []
""" Add newly detected tracklets to tracked_stracks"""
unconfirmed = []
tracked_stracks = [] # type: list[STrack]
for track in self.tracked_tracks:
if not track.is_activated:
unconfirmed.append(track)
else:
tracked_stracks.append(track)
""" Step 2: First association, with high score detection boxes"""
strack_pool = joint_tracks(tracked_stracks, self.lost_tracks)
# Predict the current location with KF
STrack.multi_predict(strack_pool)
dists = matching.iou_distance(strack_pool, detections)
dists = matching.fuse_score(dists, detections)
matches, u_track, u_detection = matching.linear_assignment(
dists, thresh=self.match_thresh
)
for itracked, idet in matches:
track = strack_pool[itracked]
det = detections[idet]
if track.state == TrackState.Tracked:
track.update(detections[idet], self.frame_id)
activated_starcks.append(track)
else:
track.re_activate(det, self.frame_id, new_id=False)
refind_stracks.append(track)
""" Step 3: Second association, with low score detection boxes"""
# association the untrack to the low score detections
if len(dets_second) > 0:
"""Detections"""
detections_second = [
STrack(STrack.tlbr_to_tlwh(tlbr), s, c)
for (tlbr, s, c) in zip(dets_second, scores_second, class_ids_second)
]
else:
detections_second = []
r_tracked_stracks = [
strack_pool[i]
for i in u_track
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(
dists, thresh=0.5
)
for itracked, idet in matches:
track = r_tracked_stracks[itracked]
det = detections_second[idet]
if track.state == TrackState.Tracked:
track.update(det, self.frame_id)
activated_starcks.append(track)
else:
track.re_activate(det, self.frame_id, new_id=False)
refind_stracks.append(track)
for it in u_track:
track = r_tracked_stracks[it]
if not track.state == TrackState.Lost:
track.mark_lost()
lost_stracks.append(track)
"""Deal with unconfirmed tracks, usually tracks with only one beginning frame"""
detections = [detections[i] for i in u_detection]
dists = matching.iou_distance(unconfirmed, detections)
dists = matching.fuse_score(dists, detections)
matches, u_unconfirmed, u_detection = matching.linear_assignment(
dists, thresh=0.7
)
for itracked, idet in matches:
unconfirmed[itracked].update(detections[idet], self.frame_id)
activated_starcks.append(unconfirmed[itracked])
for it in u_unconfirmed:
track = unconfirmed[it]
track.mark_removed()
removed_stracks.append(track)
""" Step 4: Init new stracks"""
for inew in u_detection:
track = detections[inew]
if track.score < self.det_thresh:
continue
track.activate(self.kalman_filter, self.frame_id)
activated_starcks.append(track)
""" Step 5: Update state"""
for track in self.lost_tracks:
if self.frame_id - track.end_frame > self.max_time_lost:
track.mark_removed()
removed_stracks.append(track)
self.tracked_tracks = [
t for t in self.tracked_tracks if t.state == TrackState.Tracked
]
self.tracked_tracks = joint_tracks(self.tracked_tracks, activated_starcks)
self.tracked_tracks = joint_tracks(self.tracked_tracks, refind_stracks)
self.lost_tracks = sub_tracks(self.lost_tracks, self.tracked_tracks)
self.lost_tracks.extend(lost_stracks)
self.lost_tracks = sub_tracks(self.lost_tracks, self.removed_tracks)
self.removed_tracks.extend(removed_stracks)
self.tracked_tracks, self.lost_tracks = remove_duplicate_tracks(
self.tracked_tracks, self.lost_tracks
)
output_stracks = [track for track in self.tracked_tracks if track.is_activated]
return output_stracks
def joint_tracks(
track_list_a: List[STrack], track_list_b: List[STrack]
) -> List[STrack]:
"""
Joins two lists of tracks, ensuring that the resulting list does not
contain tracks with duplicate track_id values.
Parameters:
track_list_a: First list of tracks (with track_id attribute).
track_list_b: Second list of tracks (with track_id attribute).
Returns:
Combined list of tracks from track_list_a and track_list_b
without duplicate track_id values.
"""
seen_track_ids = set()
result = []
for track in track_list_a + track_list_b:
if track.track_id not in seen_track_ids:
seen_track_ids.add(track.track_id)
result.append(track)
return result
def sub_tracks(track_list_a: List, track_list_b: List) -> List[int]:
"""
Returns a list of tracks from track_list_a after removing any tracks
that share the same track_id with tracks in track_list_b.
Parameters:
track_list_a: List of tracks (with track_id attribute).
track_list_b: List of tracks (with track_id attribute) to
be subtracted from track_list_a.
Returns:
List of remaining tracks from track_list_a after subtraction.
"""
tracks = {track.track_id: track for track in track_list_a}
track_ids_b = {track.track_id for track in track_list_b}
for track_id in track_ids_b:
tracks.pop(track_id, None)
return list(tracks.values())
def remove_duplicate_tracks(tracks_a: List, tracks_b: List) -> Tuple[List, List]:
pairwise_distance = matching.iou_distance(tracks_a, tracks_b)
matching_pairs = np.where(pairwise_distance < 0.15)
duplicates_a, duplicates_b = set(), set()
for track_index_a, track_index_b in zip(*matching_pairs):
time_a = tracks_a[track_index_a].frame_id - tracks_a[track_index_a].start_frame
time_b = tracks_b[track_index_b].frame_id - tracks_b[track_index_b].start_frame
if time_a > time_b:
duplicates_b.add(track_index_b)
else:
duplicates_a.add(track_index_a)
result_a = [
track for index, track in enumerate(tracks_a) if index not in duplicates_a
]
result_b = [
track for index, track in enumerate(tracks_b) if index not in duplicates_b
]
return result_a, result_b
@@ -0,0 +1,205 @@
from typing import Tuple
import numpy as np
import scipy.linalg
class KalmanFilter:
"""
A simple Kalman filter for tracking bounding boxes in image space.
The 8-dimensional state space
x, y, a, h, vx, vy, va, vh
contains the bounding box center position (x, y), aspect ratio a, height h,
and their respective velocities.
Object motion follows a constant velocity model. The bounding box location
(x, y, a, h) is taken as direct observation of the state space (linear
observation model).
"""
def __init__(self):
ndim, dt = 4, 1.0
self._motion_mat = np.eye(2 * ndim, 2 * ndim)
for i in range(ndim):
self._motion_mat[i, ndim + i] = dt
self._update_mat = np.eye(ndim, 2 * ndim)
self._std_weight_position = 1.0 / 20
self._std_weight_velocity = 1.0 / 160
def initiate(self, measurement: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
"""
Create track from an unassociated measurement.
Args:
measurement (ndarray): Bounding box coordinates (x, y, a, h) with
center position (x, y), aspect ratio a, and height h.
Returns:
Tuple[ndarray, ndarray]: Returns the mean vector (8 dimensional) and
covariance matrix (8x8 dimensional) of the new track.
Unobserved velocities are initialized to 0 mean.
"""
mean_pos = measurement
mean_vel = np.zeros_like(mean_pos)
mean = np.r_[mean_pos, mean_vel]
std = [
2 * self._std_weight_position * measurement[3],
2 * self._std_weight_position * measurement[3],
1e-2,
2 * self._std_weight_position * measurement[3],
10 * self._std_weight_velocity * measurement[3],
10 * self._std_weight_velocity * measurement[3],
1e-5,
10 * self._std_weight_velocity * measurement[3],
]
covariance = np.diag(np.square(std))
return mean, covariance
def predict(
self, mean: np.ndarray, covariance: np.ndarray
) -> Tuple[np.ndarray, np.ndarray]:
"""
Run Kalman filter prediction step.
Args:
mean (ndarray): The 8 dimensional mean vector of the object
state at the previous time step.
covariance (ndarray): The 8x8 dimensional covariance matrix of
the object state at the previous time step.
Returns:
Tuple[ndarray, ndarray]: Returns the mean vector and
covariance matrix of the predicted state.
Unobserved velocities are initialized to 0 mean.
"""
std_pos = [
self._std_weight_position * mean[3],
self._std_weight_position * mean[3],
1e-2,
self._std_weight_position * mean[3],
]
std_vel = [
self._std_weight_velocity * mean[3],
self._std_weight_velocity * mean[3],
1e-5,
self._std_weight_velocity * mean[3],
]
motion_cov = np.diag(np.square(np.r_[std_pos, std_vel]))
mean = np.dot(mean, self._motion_mat.T)
covariance = (
np.linalg.multi_dot((self._motion_mat, covariance, self._motion_mat.T))
+ motion_cov
)
return mean, covariance
def project(
self, mean: np.ndarray, covariance: np.ndarray
) -> Tuple[np.ndarray, np.ndarray]:
"""
Project state distribution to measurement space.
Args:
mean (ndarray): The state's mean vector (8 dimensional array).
covariance (ndarray): The state's covariance matrix (8x8 dimensional).
Returns:
Tuple[ndarray, ndarray]: Returns the projected mean and
covariance matrix of the given state estimate.
"""
std = [
self._std_weight_position * mean[3],
self._std_weight_position * mean[3],
1e-1,
self._std_weight_position * mean[3],
]
innovation_cov = np.diag(np.square(std))
mean = np.dot(self._update_mat, mean)
covariance = np.linalg.multi_dot(
(self._update_mat, covariance, self._update_mat.T)
)
return mean, covariance + innovation_cov
def multi_predict(
self, mean: np.ndarray, covariance: np.ndarray
) -> Tuple[np.ndarray, np.ndarray]:
"""
Run Kalman filter prediction step (Vectorized version).
Args:
mean (ndarray): The Nx8 dimensional mean matrix
of the object states at the previous time step.
covariance (ndarray): The Nx8x8 dimensional covariance matrices
of the object states at the previous time step.
Returns:
Tuple[ndarray, ndarray]: Returns the mean vector and
covariance matrix of the predicted state.
Unobserved velocities are initialized to 0 mean.
"""
std_pos = [
self._std_weight_position * mean[:, 3],
self._std_weight_position * mean[:, 3],
1e-2 * np.ones_like(mean[:, 3]),
self._std_weight_position * mean[:, 3],
]
std_vel = [
self._std_weight_velocity * mean[:, 3],
self._std_weight_velocity * mean[:, 3],
1e-5 * np.ones_like(mean[:, 3]),
self._std_weight_velocity * mean[:, 3],
]
sqr = np.square(np.r_[std_pos, std_vel]).T
motion_cov = []
for i in range(len(mean)):
motion_cov.append(np.diag(sqr[i]))
motion_cov = np.asarray(motion_cov)
mean = np.dot(mean, self._motion_mat.T)
left = np.dot(self._motion_mat, covariance).transpose((1, 0, 2))
covariance = np.dot(left, self._motion_mat.T) + motion_cov
return mean, covariance
def update(
self, mean: np.ndarray, covariance: np.ndarray, measurement: np.ndarray
) -> Tuple[np.ndarray, np.ndarray]:
"""
Run Kalman filter correction step.
Args:
mean (ndarray): The predicted state's mean vector (8 dimensional).
covariance (ndarray): The state's covariance matrix (8x8 dimensional).
measurement (ndarray): The 4-dimensional measurement vector (x, y, a, h),
where (x, y) is the center position, a the aspect ratio,
and h the height of the bounding box.
Returns:
Tuple[ndarray, ndarray]: Returns the measurement-corrected
state distribution.
"""
projected_mean, projected_cov = self.project(mean, covariance)
chol_factor, lower = scipy.linalg.cho_factor(
projected_cov, lower=True, check_finite=False
)
kalman_gain = scipy.linalg.cho_solve(
(chol_factor, lower),
np.dot(covariance, self._update_mat.T).T,
check_finite=False,
).T
innovation = measurement - projected_mean
new_mean = mean + np.dot(innovation, kalman_gain.T)
new_covariance = covariance - np.linalg.multi_dot(
(kalman_gain, projected_cov, kalman_gain.T)
)
return new_mean, new_covariance
@@ -0,0 +1,64 @@
from typing import List, Tuple
import numpy as np
from scipy.optimize import linear_sum_assignment
from supervision.detection.utils import box_iou_batch
def indices_to_matches(
cost_matrix: np.ndarray, indices: np.ndarray, thresh: float
) -> Tuple[np.ndarray, tuple, tuple]:
matched_cost = cost_matrix[tuple(zip(*indices))]
matched_mask = matched_cost <= thresh
matches = indices[matched_mask]
unmatched_a = tuple(set(range(cost_matrix.shape[0])) - set(matches[:, 0]))
unmatched_b = tuple(set(range(cost_matrix.shape[1])) - set(matches[:, 1]))
return matches, unmatched_a, unmatched_b
def linear_assignment(
cost_matrix: np.ndarray, thresh: float
) -> [np.ndarray, Tuple[int], Tuple[int, int]]:
if cost_matrix.size == 0:
return (
np.empty((0, 2), dtype=int),
tuple(range(cost_matrix.shape[0])),
tuple(range(cost_matrix.shape[1])),
)
cost_matrix[cost_matrix > thresh] = thresh + 1e-4
row_ind, col_ind = linear_sum_assignment(cost_matrix)
indices = np.column_stack((row_ind, col_ind))
return indices_to_matches(cost_matrix, indices, thresh)
def iou_distance(atracks: List, btracks: List) -> np.ndarray:
if (len(atracks) > 0 and isinstance(atracks[0], np.ndarray)) or (
len(btracks) > 0 and isinstance(btracks[0], np.ndarray)
):
atlbrs = atracks
btlbrs = btracks
else:
atlbrs = [track.tlbr for track in atracks]
btlbrs = [track.tlbr for track in btracks]
_ious = np.zeros((len(atlbrs), len(btlbrs)), dtype=np.float32)
if _ious.size != 0:
_ious = box_iou_batch(np.asarray(atlbrs), np.asarray(btlbrs))
cost_matrix = 1 - _ious
return cost_matrix
def fuse_score(cost_matrix: np.ndarray, detections: List) -> np.ndarray:
if cost_matrix.size == 0:
return cost_matrix
iou_sim = 1 - cost_matrix
det_scores = np.array([det.score for det in detections])
det_scores = np.expand_dims(det_scores, axis=0).repeat(cost_matrix.shape[0], axis=0)
fuse_sim = iou_sim * det_scores
fuse_cost = 1 - fuse_sim
return fuse_cost
View File
+144
View File
@@ -0,0 +1,144 @@
import json
from pathlib import Path
from typing import List, Optional, Union
import numpy as np
import yaml
class NumpyJsonEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, np.integer):
return int(obj)
if isinstance(obj, np.floating):
return float(obj)
if isinstance(obj, np.ndarray):
return obj.tolist()
return super(NumpyJsonEncoder, self).default(obj)
def list_files_with_extensions(
directory: Union[str, Path], extensions: Optional[List[str]] = None
) -> List[Path]:
"""
List files in a directory with specified extensions or
all files if no extensions are provided.
Args:
directory (Union[str, Path]): The directory path as a string or Path object.
extensions (Optional[List[str]]): A list of file extensions to filter.
Default is None, which lists all files.
Returns:
(List[Path]): A list of Path objects for the matching files.
Examples:
```python
>>> import supervision as sv
>>> # List all files in the directory
>>> files = sv.list_files_with_extensions(directory='my_directory')
>>> # List only files with '.txt' and '.md' extensions
>>> files = sv.list_files_with_extensions(
... directory='my_directory', extensions=['txt', 'md'])
```
"""
directory = Path(directory)
files_with_extensions = []
if extensions is not None:
for ext in extensions:
files_with_extensions.extend(directory.glob(f"*.{ext}"))
else:
files_with_extensions.extend(directory.glob("*"))
return files_with_extensions
def read_txt_file(file_path: str) -> List[str]:
"""
Read a text file and return a list of strings without newline characters.
Args:
file_path (str): The path to the text file.
Returns:
List[str]: A list of strings representing the lines in the text file.
"""
with open(file_path, "r") as file:
lines = file.readlines()
lines = [line.rstrip("\n") for line in lines]
return lines
def save_text_file(lines: List[str], file_path: str):
"""
Write a list of strings to a text file, each string on a new line.
Args:
lines (List[str]): The list of strings to be written to the file.
file_path (str): The path to the text file.
"""
with open(file_path, "w") as file:
for line in lines:
file.write(line + "\n")
def read_json_file(file_path: str) -> dict:
"""
Read a json file and return a dict.
Args:
file_path (str): The path to the json file.
Returns:
dict: A dict of annotations information
"""
with open(file_path, "r") as file:
data = json.load(file)
return data
def save_json_file(data: dict, file_path: str, indent: int = 3) -> None:
"""
Write a dict to a json file.
Args:
indent:
data (dict): dict with unique keys and value as pair.
file_path (str): The path to the json file.
"""
with open(file_path, "w") as fp:
json.dump(data, fp, cls=NumpyJsonEncoder, indent=indent)
def read_yaml_file(file_path: str) -> dict:
"""
Read a yaml file and return a dict.
Args:
file_path (str): The path to the yaml file.
Returns:
dict: A dict of content information
"""
with open(file_path, "r") as file:
data = yaml.safe_load(file)
return data
def save_yaml_file(data: dict, file_path: str) -> None:
"""
Save a dict to a json file.
Args:
indent:
data (dict): dict with unique keys and value as pair.
file_path (str): The path to the json file.
"""
with open(file_path, "w") as outfile:
yaml.dump(data, outfile, sort_keys=False, default_flow_style=None)
+101
View File
@@ -0,0 +1,101 @@
import os
import shutil
from typing import Optional
import cv2
import numpy as np
def crop_image(image: np.ndarray, xyxy: np.ndarray) -> np.ndarray:
"""
Crops the given image based on the given bounding box.
Args:
image (np.ndarray): The image to be cropped, represented as a numpy array.
xyxy (np.ndarray): A numpy array containing the bounding box coordinates
in the format (x1, y1, x2, y2).
Returns:
(np.ndarray): The cropped image as a numpy array.
Examples:
```python
>>> import supervision as sv
>>> detection = sv.Detections(...)
>>> with sv.ImageSink(target_dir_path='target/directory/path') as sink:
... for xyxy in detection.xyxy:
... cropped_image = sv.crop_image(image=image, xyxy=xyxy)
... sink.save_image(image=image)
```
"""
xyxy = np.round(xyxy).astype(int)
x1, y1, x2, y2 = xyxy
cropped_img = image[y1:y2, x1:x2]
return cropped_img
class ImageSink:
def __init__(
self,
target_dir_path: str,
overwrite: bool = False,
image_name_pattern: str = "image_{:05d}.png",
):
"""
Initialize a context manager for saving images.
Args:
target_dir_path (str): The target directory where images will be saved.
overwrite (bool, optional): Whether to overwrite the existing directory.
Defaults to False.
image_name_pattern (str, optional): The image file name pattern.
Defaults to "image_{:05d}.png".
Examples:
```python
>>> import supervision as sv
>>> with sv.ImageSink(target_dir_path='target/directory/path',
... overwrite=True) as sink:
... for image in sv.get_video_frames_generator(
... source_path='source_video.mp4', stride=2):
... sink.save_image(image=image)
```
"""
self.target_dir_path = target_dir_path
self.overwrite = overwrite
self.image_name_pattern = image_name_pattern
self.image_count = 0
def __enter__(self):
if os.path.exists(self.target_dir_path):
if self.overwrite:
shutil.rmtree(self.target_dir_path)
os.makedirs(self.target_dir_path)
else:
os.makedirs(self.target_dir_path)
return self
def save_image(self, image: np.ndarray, image_name: Optional[str] = None):
"""
Save a given image in the target directory.
Args:
image (np.ndarray): The image to be saved.
image_name (str, optional): The name to use for the saved image.
If not provided, a name will be
generated using the `image_name_pattern`.
"""
if image_name is None:
image_name = self.image_name_pattern.format(self.image_count)
image_path = os.path.join(self.target_dir_path, image_name)
cv2.imwrite(image_path, image)
self.image_count += 1
def __exit__(self, exc_type, exc_value, exc_traceback):
pass
+18
View File
@@ -0,0 +1,18 @@
import functools
import warnings
def deprecated(reason: str):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
warnings.warn(
f"{func.__name__} is deprecated: {reason}",
category=DeprecationWarning,
stacklevel=2,
)
return func(*args, **kwargs)
return wrapper
return decorator
@@ -24,7 +24,7 @@ def plot_image(
>>> image = cv2.imread("path/to/image.jpg")
%matplotlib inline
>>> sv.plot_image(image, (16, 16))
>>> sv.plot_image(image=image, size=(16, 16))
```
"""
plt.figure(figsize=size)
@@ -50,9 +50,12 @@ def plot_images_grid(
Args:
images (List[np.ndarray]): A list of images as numpy arrays.
grid_size (Tuple[int, int]): A tuple specifying the number of rows and columns for the grid.
titles (Optional[List[str]]): A list of titles for each image. Defaults to None.
size (Tuple[int, int]): A tuple specifying the width and height of the entire plot in inches.
grid_size (Tuple[int, int]): A tuple specifying the number
of rows and columns for the grid.
titles (Optional[List[str]]): A list of titles for each image.
Defaults to None.
size (Tuple[int, int]): A tuple specifying the width and
height of the entire plot in inches.
cmap (str): the colormap to use for single channel images.
Raises:
@@ -71,14 +74,15 @@ def plot_images_grid(
>>> titles = ["Image 1", "Image 2", "Image 3"]
%matplotlib inline
>>> plot_images_grid(images, grid_size=(2, 2), titles=titles, figsize=(16, 16))
>>> plot_images_grid(images, grid_size=(2, 2), titles=titles, size=(16, 16))
```
"""
nrows, ncols = grid_size
if len(images) > nrows * ncols:
raise ValueError(
"The number of images exceeds the grid size. Please increase the grid size or reduce the number of images."
"The number of images exceeds the grid size. Please increase the grid size"
" or reduce the number of images."
)
fig, axes = plt.subplots(nrows=nrows, ncols=ncols, figsize=size)
@@ -10,19 +10,21 @@ import numpy as np
@dataclass
class VideoInfo:
"""
A class to store video information, including width, height, fps and total number of frames.
A class to store video information, including width, height, fps and
total number of frames.
Attributes:
width (int): width of the video in pixels
height (int): height of the video in pixels
fps (int): frames per second of the video
total_frames (int, optional): total number of frames in the video, default is None
total_frames (int, optional): total number of frames in the video,
default is None
Examples:
```python
>>> from supervision import VideoInfo
>>> import supervision as sv
>>> video_info = VideoInfo.from_video_path(video_path='video.mp4')
>>> video_info = sv.VideoInfo.from_video_path(video_path='video.mp4')
>>> video_info
VideoInfo(width=3840, height=2160, fps=25, total_frames=538)
@@ -61,27 +63,37 @@ class VideoSink:
Attributes:
target_path (str): The path to the output file where the video will be saved.
video_info (VideoInfo): Information about the video resolution, fps, and total frame count.
video_info (VideoInfo): Information about the video resolution, fps,
and total frame count.
codec (str): FOURCC code for video format
Examples:
Example:
```python
>>> from supervision import VideoInfo, VideoSink
>>> import supervision as sv
>>> video_info = VideoInfo.from_video_path(video_path='source_video.mp4')
>>> video_info = sv.VideoInfo.from_video_path(video_path='source_video.mp4')
>>> with VideoSink(target_path='target_video.mp4', video_info=video_info) as s:
... frame = ...
... s.write_frame(frame=frame)
>>> with sv.VideoSink(target_path='target_video.mp4',
... video_info=video_info,
codec='H264') as sink:
... for frame in get_video_frames_generator(source_path='source_video.mp4',
... stride=2):
... sink.write_frame(frame=frame)
```
"""
def __init__(self, target_path: str, video_info: VideoInfo):
def __init__(self, target_path: str, video_info: VideoInfo, codec: str = "mp4v"):
self.target_path = target_path
self.video_info = video_info
self.__fourcc = cv2.VideoWriter_fourcc(*"mp4v")
self.__codec = codec
self.__writer = None
def __enter__(self):
try:
self.__fourcc = cv2.VideoWriter_fourcc(*self.__codec)
except TypeError as e:
print(str(e) + ". Defaulting to mp4v...")
self.__fourcc = cv2.VideoWriter_fourcc(*"mp4v")
self.__writer = cv2.VideoWriter(
self.target_path,
self.__fourcc,
@@ -93,35 +105,62 @@ class VideoSink:
def write_frame(self, frame: np.ndarray):
self.__writer.write(frame)
def __exit__(self, exc_type, exc_val, exc_tb):
def __exit__(self, exc_type, exc_value, exc_traceback):
self.__writer.release()
def get_video_frames_generator(source_path: str) -> Generator[np.ndarray, None, None]:
def _validate_and_setup_video(source_path: str, start: int, end: Optional[int]):
video = cv2.VideoCapture(source_path)
if not video.isOpened():
raise Exception(f"Could not open video at {source_path}")
total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
if end is not None and end > total_frames:
raise Exception("Requested frames are outbound")
start = max(start, 0)
end = min(end, total_frames) if end is not None else total_frames
video.set(cv2.CAP_PROP_POS_FRAMES, start)
return video, start, end
def get_video_frames_generator(
source_path: str, stride: int = 1, start: int = 0, end: Optional[int] = None
) -> Generator[np.ndarray, None, None]:
"""
Get a generator that yields the frames of the video.
Args:
source_path (str): The path of the video file.
stride (int): Indicates the interval at which frames are returned,
skipping stride - 1 frames between each.
start (int): Indicates the starting position from which
video should generate frames
end (Optional[int]): Indicates the ending position at which video
should stop generating frames. If None, video will be read to the end.
Returns:
(Generator[np.ndarray, None, None]): A generator that yields the frames of the video.
(Generator[np.ndarray, None, None]): A generator that yields the
frames of the video.
Examples:
```python
>>> from supervision import get_video_frames_generator
>>> import supervision as sv
>>> for frame in get_video_frames_generator(source_path='source_video.mp4'):
>>> for frame in sv.get_video_frames_generator(source_path='source_video.mp4'):
... ...
```
"""
video = cv2.VideoCapture(source_path)
if not video.isOpened():
raise Exception(f"Could not open video at {source_path}")
success, frame = video.read()
while success:
yield frame
video, start, end = _validate_and_setup_video(source_path, start, end)
frame_position = start
while True:
success, frame = video.read()
if not success or frame_position >= end:
break
yield frame
for _ in range(stride - 1):
success = video.grab()
if not success:
break
frame_position += stride
video.release()
@@ -131,24 +170,28 @@ def process_video(
callback: Callable[[np.ndarray, int], np.ndarray],
) -> None:
"""
Process a video file by applying a callback function on each frame and saving the result to a target video file.
Process a video file by applying a callback function on each frame
and saving the result to a target 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.
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.
Examples:
```python
>>> from supervision import process_video
>>> import supervision as sv
>>> def process_frame(scene: np.ndarray) -> np.ndarray:
>>> def callback(scene: np.ndarray, index: int) -> np.ndarray:
... ...
>>> process_video(
... source_path='source_video.mp4',
... target_path='target_video.mp4',
... callback=process_frame
... source_path='...',
... target_path='...',
... callback=callback
... )
```
"""
View File
+94
View File
@@ -0,0 +1,94 @@
from contextlib import ExitStack as DoesNotRaise
from test.utils import mock_detections
from typing import Optional
import pytest
from supervision.annotators.utils import ColorMap, resolve_color_idx
from supervision.detection.core import Detections
@pytest.mark.parametrize(
"detections, detection_idx, color_map, expected_result, exception",
[
(
mock_detections(
xyxy=[[10, 10, 20, 20], [20, 20, 30, 30]],
class_id=[5, 3],
tracker_id=[2, 6],
),
0,
ColorMap.INDEX,
0,
DoesNotRaise(),
), # multiple detections; index mapping
(
mock_detections(
xyxy=[[10, 10, 20, 20], [20, 20, 30, 30]],
class_id=[5, 3],
tracker_id=[2, 6],
),
0,
ColorMap.CLASS,
5,
DoesNotRaise(),
), # multiple detections; class mapping
(
mock_detections(
xyxy=[[10, 10, 20, 20], [20, 20, 30, 30]],
class_id=[5, 3],
tracker_id=[2, 6],
),
0,
ColorMap.TRACK,
2,
DoesNotRaise(),
), # multiple detections; track mapping
(
Detections.empty(),
0,
ColorMap.INDEX,
None,
pytest.raises(ValueError),
), # no detections; index mapping; out of bounds
(
mock_detections(
xyxy=[[10, 10, 20, 20], [20, 20, 30, 30]],
class_id=[5, 3],
tracker_id=[2, 6],
),
2,
ColorMap.INDEX,
None,
pytest.raises(ValueError),
), # multiple detections; index mapping; out of bounds
(
mock_detections(xyxy=[[10, 10, 20, 20], [20, 20, 30, 30]]),
0,
ColorMap.CLASS,
None,
pytest.raises(ValueError),
), # multiple detections; class mapping; no class_id
(
mock_detections(xyxy=[[10, 10, 20, 20], [20, 20, 30, 30]]),
0,
ColorMap.TRACK,
None,
pytest.raises(ValueError),
), # multiple detections; class mapping; no track_id
],
)
def test_resolve_color_idx(
detections: Detections,
detection_idx: int,
color_map: ColorMap,
expected_result: Optional[int],
exception: Exception,
) -> None:
with exception:
result = resolve_color_idx(
detections=detections,
detection_idx=detection_idx,
color_map=color_map,
)
assert result == expected_result
View File
+63
View File
@@ -0,0 +1,63 @@
from contextlib import ExitStack as DoesNotRaise
from typing import Optional, Tuple
import numpy as np
import pytest
from supervision.classification.core import Classifications
@pytest.mark.parametrize(
"class_id, confidence, k, expected_result, exception",
[
(
np.array([0, 1, 2, 3, 4]),
np.array([0.1, 0.2, 0.9, 0.4, 0.5]),
5,
(np.array([2, 4, 3, 1, 0]), np.array([0.9, 0.5, 0.4, 0.2, 0.1])),
DoesNotRaise(),
), # class_id with 5 numbers and 5 confidences
(
np.array([5, 1, 2, 3, 4]),
np.array([0.1, 0.2, 0.9, 0.4, 0.5]),
1,
(np.array([2]), np.array([0.9])),
DoesNotRaise(),
), # class_id with 5 numbers and 5 confidences, retrieve where k = 1
(
np.array([4, 1, 2, 3, 6, 5]),
np.array([0.8, 0.2, 0.9, 0.4, 0.5, 0.1]),
2,
(np.array([2, 4]), np.array([0.9, 0.8])),
DoesNotRaise(),
), # class_id with 5 numbers and 5 confidences, retrieve where k = 3
(
np.array([0, 1, 2, 3, 4]),
np.array([]),
5,
None,
pytest.raises(ValueError),
), # class_id with 5 numbers and 0 confidences
(
[0, 1, 2, 3, 4],
[0.1, 0.2, 0.3, 0.4],
5,
None,
pytest.raises(ValueError),
), # class_id with 5 numbers and 4 confidences
],
)
def test_top_k(
class_id: np.ndarray,
confidence: Optional[np.ndarray],
k: int,
expected_result: Optional[Tuple[np.ndarray, np.ndarray]],
exception: Exception,
) -> None:
with exception:
result = Classifications(
class_id=np.array(class_id), confidence=np.array(confidence)
).get_top_k(k)
assert np.array_equal(result[0], expected_result[0])
assert np.array_equal(result[1], expected_result[1])
View File
View File
+303
View File
@@ -0,0 +1,303 @@
from contextlib import ExitStack as DoesNotRaise
from typing import Dict, List, Tuple
import numpy as np
import pytest
from supervision import Detections
from supervision.dataset.formats.coco import (
build_coco_class_index_mapping,
classes_to_coco_categories,
coco_annotations_to_detections,
coco_categories_to_classes,
group_coco_annotations_by_image_id,
)
def mock_cock_coco_annotation(
annotation_id: int = 0,
image_id: int = 0,
category_id: int = 0,
bbox: Tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
area: float = 0.0,
) -> dict:
return {
"id": annotation_id,
"image_id": image_id,
"category_id": category_id,
"bbox": list(bbox),
"area": area,
"iscrowd": 0,
}
@pytest.mark.parametrize(
"coco_categories, expected_result, exception",
[
([], [], DoesNotRaise()), # empty coco categories
(
[{"id": 0, "name": "fashion-assistant", "supercategory": "none"}],
["fashion-assistant"],
DoesNotRaise(),
), # single coco category with supercategory == "none"
(
[
{"id": 0, "name": "fashion-assistant", "supercategory": "none"},
{"id": 1, "name": "baseball cap", "supercategory": "fashion-assistant"},
],
["fashion-assistant", "baseball cap"],
DoesNotRaise(),
), # two coco categories; one with supercategory == "none" and
# one with supercategory != "none"
(
[
{"id": 0, "name": "fashion-assistant", "supercategory": "none"},
{"id": 1, "name": "baseball cap", "supercategory": "fashion-assistant"},
{"id": 2, "name": "hoodie", "supercategory": "fashion-assistant"},
],
["fashion-assistant", "baseball cap", "hoodie"],
DoesNotRaise(),
), # three coco categories; one with supercategory == "none" and
# two with supercategory != "none"
(
[
{"id": 0, "name": "fashion-assistant", "supercategory": "none"},
{"id": 2, "name": "hoodie", "supercategory": "fashion-assistant"},
{"id": 1, "name": "baseball cap", "supercategory": "fashion-assistant"},
],
["fashion-assistant", "baseball cap", "hoodie"],
DoesNotRaise(),
), # three coco categories; one with supercategory == "none" and
# two with supercategory != "none" (different order)
],
)
def test_coco_categories_to_classes(
coco_categories: List[dict], expected_result: List[str], exception: Exception
) -> None:
with exception:
result = coco_categories_to_classes(coco_categories=coco_categories)
assert result == expected_result
@pytest.mark.parametrize(
"classes, exception",
[
([], DoesNotRaise()), # empty classes
(["baseball cap"], DoesNotRaise()), # single class
(["baseball cap", "hoodie"], DoesNotRaise()), # two classes
],
)
def test_classes_to_coco_categories_and_back_to_classes(
classes: List[str], exception: Exception
) -> None:
with exception:
coco_categories = classes_to_coco_categories(classes=classes)
result = coco_categories_to_classes(coco_categories=coco_categories)
assert result == classes
@pytest.mark.parametrize(
"coco_annotations, expected_result, exception",
[
([], {}, DoesNotRaise()), # empty coco annotations
(
[mock_cock_coco_annotation(annotation_id=0, image_id=0, category_id=0)],
{
0: [
mock_cock_coco_annotation(
annotation_id=0, image_id=0, category_id=0
)
]
},
DoesNotRaise(),
), # single coco annotation
(
[
mock_cock_coco_annotation(annotation_id=0, image_id=0, category_id=0),
mock_cock_coco_annotation(annotation_id=1, image_id=1, category_id=0),
],
{
0: [
mock_cock_coco_annotation(
annotation_id=0, image_id=0, category_id=0
)
],
1: [
mock_cock_coco_annotation(
annotation_id=1, image_id=1, category_id=0
)
],
},
DoesNotRaise(),
), # two coco annotations
(
[
mock_cock_coco_annotation(annotation_id=0, image_id=0, category_id=0),
mock_cock_coco_annotation(annotation_id=1, image_id=1, category_id=1),
mock_cock_coco_annotation(annotation_id=2, image_id=1, category_id=2),
mock_cock_coco_annotation(annotation_id=3, image_id=2, category_id=3),
mock_cock_coco_annotation(annotation_id=4, image_id=3, category_id=1),
mock_cock_coco_annotation(annotation_id=5, image_id=3, category_id=2),
mock_cock_coco_annotation(annotation_id=5, image_id=3, category_id=3),
],
{
0: [
mock_cock_coco_annotation(
annotation_id=0, image_id=0, category_id=0
),
],
1: [
mock_cock_coco_annotation(
annotation_id=1, image_id=1, category_id=1
),
mock_cock_coco_annotation(
annotation_id=2, image_id=1, category_id=2
),
],
2: [
mock_cock_coco_annotation(
annotation_id=3, image_id=2, category_id=3
),
],
3: [
mock_cock_coco_annotation(
annotation_id=4, image_id=3, category_id=1
),
mock_cock_coco_annotation(
annotation_id=5, image_id=3, category_id=2
),
mock_cock_coco_annotation(
annotation_id=5, image_id=3, category_id=3
),
],
},
DoesNotRaise(),
), # two coco annotations
],
)
def test_group_coco_annotations_by_image_id(
coco_annotations: List[dict], expected_result: dict, exception: Exception
) -> None:
with exception:
result = group_coco_annotations_by_image_id(coco_annotations=coco_annotations)
assert result == expected_result
@pytest.mark.parametrize(
"image_annotations, resolution_wh, with_masks, expected_result, exception",
[
(
[],
(1000, 1000),
False,
Detections.empty(),
DoesNotRaise(),
), # empty image annotations
(
[
mock_cock_coco_annotation(
category_id=0, bbox=(0, 0, 100, 100), area=100 * 100
)
],
(1000, 1000),
False,
Detections(
xyxy=np.array([[0, 0, 100, 100]], dtype=np.float32),
class_id=np.array([0], dtype=int),
),
DoesNotRaise(),
), # single image annotations
(
[
mock_cock_coco_annotation(
category_id=0, bbox=(0, 0, 100, 100), area=100 * 100
),
mock_cock_coco_annotation(
category_id=0, bbox=(100, 100, 100, 100), area=100 * 100
),
],
(1000, 1000),
False,
Detections(
xyxy=np.array(
[[0, 0, 100, 100], [100, 100, 200, 200]], dtype=np.float32
),
class_id=np.array([0, 0], dtype=int),
),
DoesNotRaise(),
), # two image annotations
],
)
def test_coco_annotations_to_detections(
image_annotations: List[dict],
resolution_wh: Tuple[int, int],
with_masks: bool,
expected_result: Detections,
exception: Exception,
) -> None:
with exception:
result = coco_annotations_to_detections(
image_annotations=image_annotations,
resolution_wh=resolution_wh,
with_masks=with_masks,
)
assert result == expected_result
@pytest.mark.parametrize(
"coco_categories, target_classes, expected_result, exception",
[
([], [], {}, DoesNotRaise()), # empty coco categories
(
[{"id": 0, "name": "fashion-assistant", "supercategory": "none"}],
["fashion-assistant"],
{0: 0},
DoesNotRaise(),
), # single coco category starting from 0
(
[{"id": 1, "name": "fashion-assistant", "supercategory": "none"}],
["fashion-assistant"],
{1: 0},
DoesNotRaise(),
), # single coco category starting from 1
(
[
{"id": 0, "name": "fashion-assistant", "supercategory": "none"},
{"id": 2, "name": "hoodie", "supercategory": "fashion-assistant"},
{"id": 1, "name": "baseball cap", "supercategory": "fashion-assistant"},
],
["fashion-assistant", "baseball cap", "hoodie"],
{0: 0, 1: 1, 2: 2},
DoesNotRaise(),
), # three coco categories
(
[
{"id": 2, "name": "hoodie", "supercategory": "fashion-assistant"},
{"id": 1, "name": "baseball cap", "supercategory": "fashion-assistant"},
],
["baseball cap", "hoodie"],
{2: 1, 1: 0},
DoesNotRaise(),
), # two coco categories
(
[
{"id": 3, "name": "hoodie", "supercategory": "fashion-assistant"},
{"id": 1, "name": "baseball cap", "supercategory": "fashion-assistant"},
],
["baseball cap", "hoodie"],
{3: 1, 1: 0},
DoesNotRaise(),
), # two coco categories with missing category
],
)
def test_build_coco_class_index_mapping(
coco_categories: List[dict],
target_classes: List[str],
expected_result: Dict[int, int],
exception: Exception,
) -> None:
with exception:
result = build_coco_class_index_mapping(
coco_categories=coco_categories, target_classes=target_classes
)
assert result == expected_result
+164
View File
@@ -0,0 +1,164 @@
import xml.etree.ElementTree as ET
from contextlib import ExitStack as DoesNotRaise
from test.utils import mock_detections
from typing import List, Optional
import numpy as np
import pytest
from supervision.dataset.formats.pascal_voc import (
detections_from_xml_obj,
object_to_pascal_voc,
parse_polygon_points,
)
def are_xml_elements_equal(elem1, elem2):
if (
elem1.tag != elem2.tag
or elem1.attrib != elem2.attrib
or elem1.text != elem2.text
or len(elem1) != len(elem2)
):
return False
for child1, child2 in zip(elem1, elem2):
if not are_xml_elements_equal(child1, child2):
return False
return True
@pytest.mark.parametrize(
"xyxy, name, polygon, expected_result, exception",
[
(
np.array([0, 0, 10, 10]),
"test",
None,
ET.fromstring(
"""<object><name>test</name><bndbox><xmin>1</xmin><ymin>1</ymin>
<xmax>11</xmax><ymax>11</ymax></bndbox></object>"""
),
DoesNotRaise(),
),
(
np.array([0, 0, 10, 10]),
"test",
np.array([[0, 0], [10, 0], [10, 10], [0, 10]]),
ET.fromstring(
"""<object><name>test</name><bndbox><xmin>1</xmin><ymin>1</ymin>
<xmax>11</xmax><ymax>11</ymax>
</bndbox><polygon><x1>1</x1><y1>1</y1><x2>11</x2>
<y2>1</y2><x3>11</x3><y3>11</y3><x4>1</x4><y4>11</y4>
</polygon></object>"""
),
DoesNotRaise(),
),
],
)
def test_object_to_pascal_voc(
xyxy: np.ndarray,
name: str,
polygon: Optional[np.ndarray],
expected_result,
exception: Exception,
):
with exception:
result = object_to_pascal_voc(xyxy=xyxy, name=name, polygon=polygon)
assert are_xml_elements_equal(result, expected_result)
@pytest.mark.parametrize(
"polygon_element, expected_result, exception",
[
(
ET.fromstring(
"""<polygon><x1>0</x1><y1>0</y1><x2>10</x2><y2>0</y2><x3>10</x3>
<y3>10</y3><x4>0</x4><y4>10</y4></polygon>"""
),
np.array([[0, 0], [10, 0], [10, 10], [0, 10]]),
DoesNotRaise(),
)
],
)
def test_parse_polygon_points(
polygon_element,
expected_result: List[list],
exception,
):
with exception:
result = parse_polygon_points(polygon_element)
assert np.array_equal(result, expected_result)
ONE_CLASS_N_BBOX = """<annotation><object><name>test</name><bndbox><xmin>1</xmin>
<ymin>1</ymin><xmax>11</xmax><ymax>11</ymax>
</bndbox></object><object><name>test</name><bndbox><xmin>11</xmin><ymin>11</ymin>
<xmax>21</xmax><ymax>21</ymax></bndbox></object></annotation>"""
ONE_CLASS_ONE_BBOX = """<annotation><object><name>test</name><bndbox>
<xmin>1</xmin><ymin>1</ymin><xmax>11</xmax><ymax>11</ymax></bndbox></object>
</annotation>"""
N_CLASS_N_BBOX = """<annotation><object><name>test</name><bndbox><xmin>1</xmin>
<ymin>1</ymin><xmax>11</xmax><ymax>11</ymax>
</bndbox></object><object><name>test</name><bndbox>
<xmin>21</xmin><ymin>31</ymin><xmax>31</xmax><ymax>41</ymax></bndbox>
</object><object><name>test2</name><bndbox><xmin>
11</xmin><ymin>11</ymin><xmax>21</xmax><ymax>
21</ymax></bndbox></object></annotation>"""
NO_DETECTIONS = """<annotation></annotation>"""
@pytest.mark.parametrize(
"xml_string, classes, resolution_wh, force_masks, expected_result, exception",
[
(
ONE_CLASS_ONE_BBOX,
["test"],
(100, 100),
False,
mock_detections(np.array([[0, 0, 10, 10]]), None, [0]),
DoesNotRaise(),
),
(
ONE_CLASS_N_BBOX,
["test"],
(100, 100),
False,
mock_detections(np.array([[0, 0, 10, 10], [10, 10, 20, 20]]), None, [0, 0]),
DoesNotRaise(),
),
(
N_CLASS_N_BBOX,
["test", "test2"],
(100, 100),
False,
mock_detections(
np.array([[0, 0, 10, 10], [20, 30, 30, 40], [10, 10, 20, 20]]),
None,
[0, 0, 1],
),
DoesNotRaise(),
),
(
NO_DETECTIONS,
[],
(100, 100),
False,
mock_detections(np.empty((0, 4)), None, []),
DoesNotRaise(),
),
],
)
def test_detections_from_xml_obj(
xml_string, classes, resolution_wh, force_masks, expected_result, exception
):
with exception:
root = ET.fromstring(xml_string)
result, _ = detections_from_xml_obj(root, classes, resolution_wh, force_masks)
assert result == expected_result
+279
View File
@@ -0,0 +1,279 @@
from contextlib import ExitStack as DoesNotRaise
from typing import List, Optional, Tuple
import numpy as np
import pytest
from supervision.dataset.formats.yolo import (
_image_name_to_annotation_name,
_with_mask,
object_to_yolo,
yolo_annotations_to_detections,
)
from supervision.detection.core import Detections
def _mock_simple_mask(resolution_wh: Tuple[int, int], box: List[int]) -> np.array:
x_min, y_min, x_max, y_max = box
mask = np.full(resolution_wh, False, dtype=bool)
mask[y_min:y_max, x_min:x_max] = True
return mask
# The result of _mock_simple_mask is a little different from the result produced by cv2.
def _arrays_almost_equal(
arr1: np.ndarray, arr2: np.ndarray, threshold: float = 0.99
) -> bool:
equal_elements = np.equal(arr1, arr2)
proportion_equal = np.mean(equal_elements)
return proportion_equal >= threshold
@pytest.mark.parametrize(
"lines, expected_result, exception",
[
([], False, DoesNotRaise()), # empty yolo annotation file
(
["0 0.5 0.5 0.2 0.2"],
False,
DoesNotRaise(),
), # yolo annotation file with single line with box
(
["0 0.50 0.50 0.20 0.20", "1 0.11 0.47 0.22 0.30"],
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,
DoesNotRaise(),
), # yolo annotation file with single line with polygon
(
["0 0.4 0.4 0.6 0.4 0.6 0.6 0.4 0.6", "1 0.11 0.47 0.22 0.30"],
True,
DoesNotRaise(),
), # yolo annotation file with two lines - one box and one polygon
],
)
def test_with_mask(
lines: List[str], expected_result: Optional[bool], exception: Exception
) -> None:
with exception:
result = _with_mask(lines=lines)
assert result == expected_result
@pytest.mark.parametrize(
"lines, resolution_wh, with_masks, expected_result, exception",
[
(
[],
(1000, 1000),
False,
Detections.empty(),
DoesNotRaise(),
), # empty yolo annotation file
(
["0 0.5 0.5 0.2 0.2"],
(1000, 1000),
False,
Detections(
xyxy=np.array([[400, 400, 600, 600]], dtype=np.float32),
class_id=np.array([0], dtype=int),
),
DoesNotRaise(),
), # yolo annotation file with single line with box
(
["0 0.50 0.50 0.20 0.20", "1 0.11 0.47 0.22 0.30"],
(1000, 1000),
False,
Detections(
xyxy=np.array(
[[400, 400, 600, 600], [0, 320, 220, 620]], dtype=np.float32
),
class_id=np.array([0, 1], dtype=int),
),
DoesNotRaise(),
), # yolo annotation file with two lines with box
(
["0 0.5 0.5 0.2 0.2"],
(1000, 1000),
True,
Detections(
xyxy=np.array([[400, 400, 600, 600]], dtype=np.float32),
class_id=np.array([0], dtype=int),
mask=np.array(
[
_mock_simple_mask(
resolution_wh=(1000, 1000), box=[400, 400, 600, 600]
)
],
dtype=bool,
),
),
DoesNotRaise(),
), # yolo annotation file with single line with box in with_masks mode
(
["0 0.4 0.4 0.6 0.4 0.6 0.6 0.4 0.6"],
(1000, 1000),
True,
Detections(
xyxy=np.array([[400, 400, 600, 600]], dtype=np.float32),
class_id=np.array([0], dtype=int),
mask=np.array(
[
_mock_simple_mask(
resolution_wh=(1000, 1000), box=[400, 400, 600, 600]
)
],
dtype=bool,
),
),
DoesNotRaise(),
), # yolo annotation file with single line with polygon
(
["0 0.4 0.4 0.6 0.4 0.6 0.6 0.4 0.6", "1 0.11 0.47 0.22 0.30"],
(1000, 1000),
True,
Detections(
xyxy=np.array(
[[400, 400, 600, 600], [0, 320, 220, 620]], dtype=np.float32
),
class_id=np.array([0, 1], dtype=int),
mask=np.array(
[
_mock_simple_mask(
resolution_wh=(1000, 1000), box=[400, 400, 600, 600]
),
_mock_simple_mask(
resolution_wh=(1000, 1000), box=[0, 320, 220, 620]
),
],
dtype=bool,
),
),
DoesNotRaise(),
), # yolo annotation file with two lines -
# one box and one polygon in with_masks mode
(
["0 0.4 0.4 0.6 0.4 0.6 0.6 0.4 0.6", "1 0.11 0.47 0.22 0.30"],
(1000, 1000),
False,
Detections(
xyxy=np.array(
[[400, 400, 600, 600], [0, 320, 220, 620]], dtype=np.float32
),
class_id=np.array([0, 1], dtype=int),
),
DoesNotRaise(),
), # yolo annotation file with two lines - one box and one polygon
],
)
def test_yolo_annotations_to_detections(
lines: List[str],
resolution_wh: Tuple[int, int],
with_masks: bool,
expected_result: Optional[Detections],
exception: Exception,
) -> None:
with exception:
result = yolo_annotations_to_detections(
lines=lines, resolution_wh=resolution_wh, with_masks=with_masks
)
assert np.array_equal(result.xyxy, expected_result.xyxy)
assert np.array_equal(result.class_id, expected_result.class_id)
assert (
result.mask is None and expected_result.mask is None
) or _arrays_almost_equal(result.mask, expected_result.mask)
@pytest.mark.parametrize(
"image_name, expected_result, exception",
[
("image.png", "image.txt", DoesNotRaise()), # simple png image
("image.jpeg", "image.txt", DoesNotRaise()), # simple jpeg image
("image.jpg", "image.txt", DoesNotRaise()), # simple jpg image
(
"image.000.jpg",
"image.000.txt",
DoesNotRaise(),
), # jpg image with multiple dots in name
],
)
def test_image_name_to_annotation_name(
image_name: str, expected_result: Optional[str], exception: Exception
) -> None:
with exception:
result = _image_name_to_annotation_name(image_name=image_name)
assert result == expected_result
@pytest.mark.parametrize(
"xyxy, class_id, image_shape, polygon, expected_result, exception",
[
(
np.array([100, 100, 200, 200], dtype=np.float32),
1,
(1000, 1000, 3),
None,
"1 0.15000 0.15000 0.10000 0.10000",
DoesNotRaise(),
), # square bounding box on square image
(
np.array([100, 100, 200, 200], dtype=np.float32),
1,
(800, 1000, 3),
None,
"1 0.15000 0.18750 0.10000 0.12500",
DoesNotRaise(),
), # square bounding box on horizontal image
(
np.array([100, 100, 200, 200], dtype=np.float32),
1,
(1000, 800, 3),
None,
"1 0.18750 0.15000 0.12500 0.10000",
DoesNotRaise(),
), # square bounding box on vertical image
(
np.array([100, 200, 200, 400], dtype=np.float32),
1,
(1000, 1000, 3),
None,
"1 0.15000 0.30000 0.10000 0.20000",
DoesNotRaise(),
), # horizontal bounding box on square image
(
np.array([200, 100, 400, 200], dtype=np.float32),
1,
(1000, 1000, 3),
None,
"1 0.30000 0.15000 0.20000 0.10000",
DoesNotRaise(),
), # vertical bounding box on square image
(
np.array([100, 100, 200, 200], dtype=np.float32),
1,
(1000, 1000, 3),
np.array(
[[100, 100], [200, 100], [200, 200], [100, 100]], dtype=np.float32
),
"1 0.10000 0.10000 0.20000 0.10000 0.20000 0.20000 0.10000 0.10000",
DoesNotRaise(),
), # square mask on square image
],
)
def test_object_to_yolo(
xyxy: np.ndarray,
class_id: int,
image_shape: Tuple[int, int, int],
polygon: Optional[np.ndarray],
expected_result: Optional[str],
exception: Exception,
) -> None:
with exception:
result = object_to_yolo(
xyxy=xyxy, class_id=class_id, image_shape=image_shape, polygon=polygon
)
assert result == expected_result
+195
View File
@@ -0,0 +1,195 @@
from contextlib import ExitStack as DoesNotRaise
from test.utils import mock_detections
from typing import List, Optional
import numpy as np
import pytest
from supervision import DetectionDataset
@pytest.mark.parametrize(
"dataset_list, expected_result, exception",
[
(
[],
DetectionDataset(classes=[], images={}, annotations={}),
DoesNotRaise(),
), # empty dataset list
(
[DetectionDataset(classes=[], images={}, annotations={})],
DetectionDataset(classes=[], images={}, annotations={}),
DoesNotRaise(),
), # single empty dataset
(
[
DetectionDataset(classes=["dog", "person"], images={}, annotations={}),
DetectionDataset(classes=["dog", "person"], images={}, annotations={}),
],
DetectionDataset(classes=["dog", "person"], images={}, annotations={}),
DoesNotRaise(),
), # two datasets; no images and annotations, the same classes
(
[
DetectionDataset(classes=["dog", "person"], images={}, annotations={}),
DetectionDataset(classes=["cat"], images={}, annotations={}),
],
DetectionDataset(
classes=["cat", "dog", "person"], images={}, annotations={}
),
DoesNotRaise(),
), # two datasets; no images and annotations, different classes
(
[
DetectionDataset(
classes=["dog", "person"],
images={
"image-1.png": np.zeros((100, 100, 3), dtype=np.uint8),
"image-2.png": np.zeros((100, 100, 3), dtype=np.uint8),
},
annotations={
"image-1.png": mock_detections(
xyxy=[[0, 0, 10, 10]], class_id=[0]
),
"image-2.png": mock_detections(
xyxy=[[0, 0, 10, 10]], class_id=[1]
),
},
),
DetectionDataset(classes=[], images={}, annotations={}),
],
DetectionDataset(
classes=["dog", "person"],
images={
"image-1.png": np.zeros((100, 100, 3), dtype=np.uint8),
"image-2.png": np.zeros((100, 100, 3), dtype=np.uint8),
},
annotations={
"image-1.png": mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[0]),
"image-2.png": mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[1]),
},
),
DoesNotRaise(),
), # two datasets; images and annotations, the same classes
(
[
DetectionDataset(
classes=["dog", "person"],
images={
"image-1.png": np.zeros((100, 100, 3), dtype=np.uint8),
"image-2.png": np.zeros((100, 100, 3), dtype=np.uint8),
},
annotations={
"image-1.png": mock_detections(
xyxy=[[0, 0, 10, 10]], class_id=[0]
),
"image-2.png": mock_detections(
xyxy=[[0, 0, 10, 10]], class_id=[1]
),
},
),
DetectionDataset(classes=["cat"], images={}, annotations={}),
],
DetectionDataset(
classes=["cat", "dog", "person"],
images={
"image-1.png": np.zeros((100, 100, 3), dtype=np.uint8),
"image-2.png": np.zeros((100, 100, 3), dtype=np.uint8),
},
annotations={
"image-1.png": mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[1]),
"image-2.png": mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[2]),
},
),
DoesNotRaise(),
), # two datasets; images and annotations, different classes
(
[
DetectionDataset(
classes=["dog", "person"],
images={
"image-1.png": np.zeros((100, 100, 3), dtype=np.uint8),
"image-2.png": np.zeros((100, 100, 3), dtype=np.uint8),
},
annotations={
"image-1.png": mock_detections(
xyxy=[[0, 0, 10, 10]], class_id=[0]
),
"image-2.png": mock_detections(
xyxy=[[0, 0, 10, 10]], class_id=[1]
),
},
),
DetectionDataset(
classes=["cat"],
images={
"image-3.png": np.zeros((100, 100, 3), dtype=np.uint8),
},
annotations={
"image-3.png": mock_detections(
xyxy=[[0, 0, 10, 10]], class_id=[0]
),
},
),
],
DetectionDataset(
classes=["cat", "dog", "person"],
images={
"image-1.png": np.zeros((100, 100, 3), dtype=np.uint8),
"image-2.png": np.zeros((100, 100, 3), dtype=np.uint8),
"image-3.png": np.zeros((100, 100, 3), dtype=np.uint8),
},
annotations={
"image-1.png": mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[1]),
"image-2.png": mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[2]),
"image-3.png": mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[0]),
},
),
DoesNotRaise(),
), # two datasets; images and annotations, different classes
(
[
DetectionDataset(
classes=["dog", "person"],
images={
"image-1.png": np.zeros((100, 100, 3), dtype=np.uint8),
"image-2.png": np.zeros((100, 100, 3), dtype=np.uint8),
},
annotations={
"image-1.png": mock_detections(
xyxy=[[0, 0, 10, 10]], class_id=[0]
),
"image-2.png": mock_detections(
xyxy=[[0, 0, 10, 10]], class_id=[1]
),
},
),
DetectionDataset(
classes=["dog", "person"],
images={
"image-2.png": np.zeros((100, 100, 3), dtype=np.uint8),
"image-3.png": np.zeros((100, 100, 3), dtype=np.uint8),
},
annotations={
"image-2.png": mock_detections(
xyxy=[[0, 0, 10, 10]], class_id=[0]
),
"image-3.png": mock_detections(
xyxy=[[0, 0, 10, 10]], class_id=[1]
),
},
),
],
None,
pytest.raises(ValueError),
),
],
)
def test_dataset_merge(
dataset_list: List[DetectionDataset],
expected_result: Optional[DetectionDataset],
exception: Exception,
) -> None:
with exception:
result = DetectionDataset.merge(dataset_list=dataset_list)
assert result == expected_result
+231
View File
@@ -0,0 +1,231 @@
from contextlib import ExitStack as DoesNotRaise
from test.utils import mock_detections
from typing import Dict, List, Optional, Tuple, TypeVar
import pytest
from supervision import Detections
from supervision.dataset.utils import (
build_class_index_mapping,
map_detections_class_id,
merge_class_lists,
train_test_split,
)
T = TypeVar("T")
@pytest.mark.parametrize(
"data, train_ratio, random_state, shuffle, expected_result, exception",
[
([], 0.5, None, False, ([], []), DoesNotRaise()), # empty data
(
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
0.5,
None,
False,
([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]),
DoesNotRaise(),
), # data with 10 numbers and 50% train split
(
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
1.0,
None,
False,
([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], []),
DoesNotRaise(),
), # data with 10 numbers and 100% train split
(
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
0.0,
None,
False,
([], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
DoesNotRaise(),
), # data with 10 numbers and 0% train split
(
["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"],
0.5,
None,
False,
(["a", "b", "c", "d", "e"], ["f", "g", "h", "i", "j"]),
DoesNotRaise(),
), # data with 10 chars and 50% train split
(
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
0.5,
23,
True,
([7, 8, 5, 6, 3], [2, 9, 0, 1, 4]),
DoesNotRaise(),
), # data with 10 numbers and 50% train split with 23 random seed
(
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
0.5,
32,
True,
([4, 6, 0, 8, 9], [5, 7, 2, 3, 1]),
DoesNotRaise(),
), # data with 10 numbers and 50% train split with 23 random seed
],
)
def test_train_test_split(
data: List[T],
train_ratio: float,
random_state: int,
shuffle: bool,
expected_result: Optional[Tuple[List[T], List[T]]],
exception: Exception,
) -> None:
with exception:
result = train_test_split(
data=data,
train_ratio=train_ratio,
random_state=random_state,
shuffle=shuffle,
)
assert result == expected_result
@pytest.mark.parametrize(
"class_lists, expected_result, exception",
[
([], [], DoesNotRaise()), # empty class lists
(
[["dog", "person"]],
["dog", "person"],
DoesNotRaise(),
), # single class list; already alphabetically sorted
(
[["person", "dog"]],
["dog", "person"],
DoesNotRaise(),
), # single class list; not alphabetically sorted
(
[["dog", "person"], ["dog", "person"]],
["dog", "person"],
DoesNotRaise(),
), # two class lists; the same classes; already alphabetically sorted
(
[["dog", "person"], ["cat"]],
["cat", "dog", "person"],
DoesNotRaise(),
), # two class lists; different classes; already alphabetically sorted
],
)
def test_merge_class_maps(
class_lists: List[List[str]], expected_result: List[str], exception: Exception
) -> None:
with exception:
result = merge_class_lists(class_lists=class_lists)
assert result == expected_result
@pytest.mark.parametrize(
"source_classes, target_classes, expected_result, exception",
[
([], [], {}, DoesNotRaise()), # empty class lists
([], ["dog", "person"], {}, DoesNotRaise()), # empty source class list
(
["dog", "person"],
[],
None,
pytest.raises(ValueError),
), # empty target class list
(
["dog", "person"],
["dog", "person"],
{0: 0, 1: 1},
DoesNotRaise(),
), # same class lists
(
["dog", "person"],
["person", "dog"],
{0: 1, 1: 0},
DoesNotRaise(),
), # same class lists but not alphabetically sorted
(
["dog", "person"],
["cat", "dog", "person"],
{0: 1, 1: 2},
DoesNotRaise(),
), # source class list is a subset of target class list
(
["dog", "person"],
["cat", "dog"],
None,
pytest.raises(ValueError),
), # source class list is not a subset of target class list
],
)
def test_build_class_index_mapping(
source_classes: List[str],
target_classes: List[str],
expected_result: Optional[Dict[int, int]],
exception: Exception,
) -> None:
with exception:
result = build_class_index_mapping(
source_classes=source_classes, target_classes=target_classes
)
assert result == expected_result
@pytest.mark.parametrize(
"source_to_target_mapping, detections, expected_result, exception",
[
(
{},
mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[0]),
None,
pytest.raises(ValueError),
), # empty mapping
(
{0: 1},
mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[0]),
mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[1]),
DoesNotRaise(),
), # single mapping
(
{0: 1, 1: 2},
Detections.empty(),
Detections.empty(),
DoesNotRaise(),
), # empty detections
(
{0: 1, 1: 2},
mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[0]),
mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[1]),
DoesNotRaise(),
), # multiple mappings
(
{0: 1, 1: 2},
mock_detections(xyxy=[[0, 0, 10, 10], [0, 0, 10, 10]], class_id=[0, 1]),
mock_detections(xyxy=[[0, 0, 10, 10], [0, 0, 10, 10]], class_id=[1, 2]),
DoesNotRaise(),
), # multiple mappings
(
{0: 1, 1: 2},
mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[2]),
None,
pytest.raises(ValueError),
), # class_id not in mapping
(
{0: 1, 1: 2},
mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[0], confidence=[0.5]),
mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[1], confidence=[0.5]),
DoesNotRaise(),
), # confidence is not None
],
)
def test_map_detections_class_id(
source_to_target_mapping: Dict[int, int],
detections: Detections,
expected_result: Optional[Detections],
exception: Exception,
) -> None:
with exception:
result = map_detections_class_id(
source_to_target_mapping=source_to_target_mapping, detections=detections
)
assert result == expected_result

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