Compare commits

..
154 Commits
Author SHA1 Message Date
Piotr Skalski 29450139f9 Merge pull request #72 from roboflow/feature/add_pascal_voc_xml_load_support
feature/add_pascal_voc_xml_load_support
2023-04-19 23:02:07 +02:00
SkalskiP ea400feb6a 🪵 update changelog.md and 🖤 make black happy 2023-04-19 22:12:27 +02:00
SkalskiP 05495aca75 Initial version of from_pascal_voc 2023-04-19 21:15:23 +02:00
SkalskiP a3bea9ae37 🛠️ small fix 2023-04-19 20:36:57 +02:00
SkalskiP 651690f838 load_pascal_voc_annotations function added 2023-04-19 20:34:17 +02:00
SkalskiP d87761f75d 🎉 initial commit 2023-04-19 16:19:19 +02:00
Piotr Skalski b20e4a6c19 Merge pull request #71 from roboflow/feature/initial_dataset_support
feature/initial dataset support
2023-04-19 14:56:10 +02:00
SkalskiP 0ef2ab7273 🧪 test new approximate_polygon function 2023-04-19 14:44:43 +02:00
SkalskiP d1bb169593 🛠️ small bug fixing 2023-04-19 14:17:56 +02:00
SkalskiP c8f3f59a45 🛠️ small bug fixing 2023-04-19 14:09:36 +02:00
SkalskiP 12523afe9a 🧪 PoC of approximate_polygon 2023-04-19 13:49:38 +02:00
SkalskiP 68412500fa 📄 docs update 2023-04-19 13:09:53 +02:00
SkalskiP 49d4441419 📄 docs update 2023-04-19 12:59:29 +02:00
SkalskiP 0394a3f545 small update in detections_to_pascal_voc function 2023-04-18 22:25:26 +02:00
SkalskiP 2bcd1e1dfd 🛠️small fixes 2023-04-18 20:36:52 +02:00
SkalskiP e62937c8ce list_files_with_extensions utility added 2023-04-18 20:08:55 +02:00
SkalskiP b37aca52e2 ⬇️Initial Dataset implementation 2023-04-18 19:31:36 +02:00
SkalskiP 790d2f6c5b polygon_to_xyxy function added 2023-04-18 17:09:37 +02:00
SkalskiP 0cb722e91e mask_to_polygons and filter_polygons_by_area added 2023-04-18 14:48:25 +02:00
SkalskiP 55cd04d0aa 🛠️ small fixes 2023-04-18 12:26:51 +02:00
SkalskiP 58181e6a7a 🛠️ small fixes 2023-04-18 12:22:35 +02:00
SkalskiP ab00d2db0d 🛠️ small fixes 2023-04-18 12:02:58 +02:00
SkalskiP 6a76c96df5 🛠️ small fixes 2023-04-18 11:59:41 +02:00
SkalskiP d4a6a1a85b 🛠️ small fixes 2023-04-18 11:55:11 +02:00
SkalskiP 7c78feb4b8 Merge branch 'main' into feature/initial_dataset_support
# Conflicts:
#	supervision/detection/core.py
2023-04-18 11:08:06 +02:00
Piotr Skalski 73a7983719 Merge pull request #70 from alexandercarruthers/feature/add-detections-consistancy
Make Detection docs consistent with __iter__
2023-04-18 10:59:21 +02:00
SkalskiP 1d6c2bc080 ⬇️ initial version of Pascal VOC XML sink 2023-04-18 10:56:41 +02:00
alexandercarruthers 889461574f Make Detection docs consistent with __iter__ 2023-04-17 21:26:04 +01:00
SkalskiP f95426540b 🎭 mask annotator updates 2023-04-17 17:31:48 +02:00
SkalskiP c16599269c 🛠️ fix imports 2023-04-17 16:58:25 +02:00
SkalskiP 888edfbc67 🎉 Initial commit 2023-04-17 16:36:27 +02:00
Piotr Skalski 7497f8d96a Merge pull request #64 from roboflow/feature/small_housekeeping_before_release
🧹 Small housekeeping before release
2023-04-13 10:56:13 +02:00
SkalskiP b34ecd53cf 🧹 Small housekeeping before release 2023-04-13 10:50:20 +02:00
Piotr Skalski e9649acd90 Merge pull request #63 from ChaseDDevelopment/line-zone-detections-fix
Fix issue where the LineZone.trigger function expects 4 values instead of 5
2023-04-13 10:44:48 +02:00
Chase Dubauskas 2cf7a9b620 Fix issue where the LineZone.trigger function expects 4 values instead of 5. 2023-04-12 14:02:29 -06:00
SkalskiP e091f9c959 📚 Docs changelog.md update 2023-04-12 17:54:06 +02:00
SkalskiP f12504b0dd 🛠️ fix Detections.__getitem__ and Detections.area bugs 2023-04-11 13:50:42 +02:00
Piotr Skalski dba4d9f8ad Merge pull request #58 from roboflow/feature/update_to_support_sam
feature/update_to_support_sam
2023-04-10 23:52:35 +02:00
SkalskiP ddc8a8ccce 🖤 Make black happy 2023-04-10 23:16:04 +02:00
SkalskiP c1ccff474f Documentation for PolygonZone and PolygonZoneAnnotator 2023-04-10 23:15:35 +02:00
SkalskiP d02bac75c4 Document changes in docs 2023-04-10 23:07:53 +02:00
SkalskiP f3c4a966c4 changelog added 2023-04-10 22:57:08 +02:00
SkalskiP 5bc436b1c5 add area and box_area to Detections object 2023-04-10 19:58:44 +02:00
SkalskiP c37590ef3a add area and box_area to Detections object 2023-04-10 19:17:53 +02:00
SkalskiP a14bb94b46 😷 to 📦 initial version of mask_to_xyxy util 2023-04-10 18:58:07 +02:00
SkalskiP aab7e238e4 😷 to 📦 initial version of mask_to_xyxy util 2023-04-10 18:46:13 +02:00
SkalskiP 34dba17641 🖤 Make Black happy 2023-04-10 12:00:00 +02:00
SkalskiP ef5ee71f0b Update sv.Detections.from_sam API 2023-04-10 11:58:09 +02:00
SkalskiP bed0e41cb7 👊 version bump 2023-04-09 23:24:10 +02:00
SkalskiP e439b29f07 Initial from_sam connector for Detections 2023-04-09 23:21:04 +02:00
SkalskiP 511375f24b 😷 Top level import of MaskAnnotator 2023-04-09 23:05:56 +02:00
SkalskiP ff2788945d 😷 Initial implementation of MaskAnnotator 2023-04-09 22:57:29 +02:00
SkalskiP 5b9eef363d small fix :) 2023-04-09 22:16:12 +02:00
SkalskiP bcc4b25eb9 🖤bake black happy 2023-04-08 19:05:07 +02:00
SkalskiP d5d2035048 😷add mask support to Detection object 2023-04-08 19:01:48 +02:00
SkalskiP 14795a6626 🏞 updated plot_image and plot_images_grid 2023-04-08 17:31:52 +02:00
Piotr Skalski bc12a8ea0b Merge pull request #56 from roboflow/feature/rafactor_of_functions_in_notebooks_package_and_intitial_version_of_voc_xml_support
feature/rafactor_of_functions_in_notebooks_package_and_intitial_version_of_voc_xml_support
2023-04-05 17:18:38 +02:00
SkalskiP 2be4981c76 🖤 Make black happy 2023-04-05 16:27:22 +02:00
SkalskiP fe68327b75 Changelog updated 2023-04-05 16:25:29 +02:00
SkalskiP 141809ae92 - 📓 Refactor of notebooks utilities
- Initial voc xml sing support
- Documentation improvements
2023-04-05 15:48:21 +02:00
SkalskiP 1fa4a1c72e 📜 Supervision docs changelog added 2023-03-29 15:59:02 +02:00
Piotr Skalski 2df4261355 Merge pull request #50 from roboflow/feature/drop_requirement_for_class_id_in_detections
feature/drop_requirement_for_class_id_in_detections
2023-03-23 14:42:52 +01:00
SkalskiP a88eff868e 📖 improve documentation 2023-03-23 14:41:03 +01:00
SkalskiP 8bf686e23b 🖤 make black happy 2023-03-23 14:18:46 +01:00
SkalskiP b2f6520b87 🛠️ fix idx error in annotate 2023-03-23 13:43:03 +01:00
SkalskiP 420dae9e70 🛠️ fix circular import 2023-03-23 13:02:52 +01:00
SkalskiP 550c913dbb 💧 drop requirement for class_id in Detections constructor to make it more flexible 2023-03-23 12:31:23 +01:00
SkalskiP 4dcca5309c 🔥 docs and README.md changes 2023-03-21 13:43:40 +01:00
SkalskiP d30cc180d0 👊 bump version before release 2023-03-14 13:34:46 +01:00
Piotr Skalski ace8301b12 Merge pull request #46 from roboflow/fix/041_index_error_on_polygon_zones
fix/041_index_error_on_polygon_zones
2023-03-14 13:09:22 +01:00
SkalskiP b91bc91897 🖤 make black happy 2023-03-14 12:41:12 +01:00
SkalskiP e04c531c05 Merge remote-tracking branch 'origin/fix/041_index_error_on_polygon_zones' into fix/041_index_error_on_polygon_zones 2023-03-14 12:36:33 +01:00
SkalskiP 63fd5a3f8a 🧪 ready for tests 2023-03-14 12:36:06 +01:00
SkalskiP 38f52a04ae 🧪 ready for tests 2023-03-14 11:33:31 +01:00
Piotr Skalski 65090f612c Merge pull request #45 from roboflow/fix/042_fix_with_nms_when_detections_is_empty
🧪 ready for tests
2023-03-13 22:08:15 +01:00
SkalskiP 9401210aa5 🧪 ready for tests 2023-03-13 21:45:12 +01:00
Piotr Skalski 59d8655014 Merge pull request #36 from roboflow/feature/add_class_agnostic_nms
feature/add_class_agnostic_nms
2023-03-10 13:44:11 +01:00
SkalskiP 4dde484141 🖤 make black happy 2023-03-08 11:31:18 +01:00
SkalskiP e6f250dbfb 🧹 docs refinement and clean up 2023-03-08 11:28:54 +01:00
SkalskiP e6090be9eb 📄 small type changes in docs 2023-03-08 11:08:07 +01:00
SkalskiP 25c19b4422 👋 initial commit 2023-03-07 22:35:09 +01:00
Piotr Skalski ac1658268c Merge pull request #32 from roboflow/feature/detections_api_refinement
feature/detections_api_refinement
2023-03-01 12:38:19 +01:00
SkalskiP 8e7228313b make black happy 🖤 2023-03-01 12:36:15 +01:00
SkalskiP a27377aaf3 add area property to Detections 2023-02-28 15:27:08 +01:00
SkalskiP eefc5ddf7d fix redundant assert 2023-02-27 22:09:41 +01:00
SkalskiP 96d8badf1a 🥥 add from_coco_annotations support 2023-02-27 17:35:07 +01:00
SkalskiP c0ae62a460 making score/confidence optional to allow for using Detections to process labels 2023-02-27 15:12:33 +01:00
SkalskiP 1c2d946527 nms support 2023-02-27 12:35:07 +01:00
SkalskiP 95b44db2bd from_transformers added 2023-02-27 11:50:17 +01:00
SkalskiP ba732330a5 🫵 allow integer array indexing 2023-02-27 11:43:01 +01:00
Piotr Skalski 3ea95b3966 Merge pull request #31 from dceejay/main
Update __init__.py to include linezone
2023-02-24 17:00:38 +01:00
Dave Conway-Jones c2a5161ff9 Update __init__.py
Add export of linezone
2023-02-24 14:32:51 +00:00
SkalskiP c3eec02b6c 👊 bump version from 0.2.0 to 0.2.1 2023-02-12 23:08:04 +01:00
Piotr Skalski 8c68999324 Merge pull request #23 from roboflow/fix/22-uses-deprecated-alias-bool
🛠️ fix `core.py uses deprecated alias np.bool` #22 + tests
2023-02-12 23:05:37 +01:00
SkalskiP 4a43f4ec69 🖤 make black happy 2023-02-12 23:01:40 +01:00
SkalskiP 78d267e169 🛠️ fix core.py uses deprecated alias np.bool #22 + tests 2023-02-12 22:54:54 +01:00
Piotr Skalski 7780308533 Update README.md 2023-02-09 23:08:41 +01:00
Piotr Skalski 2e76e3db0f Merge pull request #15 from roboflow/feature/polygon-zone
feature/polygon-zone
2023-02-07 19:28:51 +01:00
SkalskiP a0eb6c3237 - make linter happy 2023-02-07 19:03:25 +01:00
SkalskiP fd35ad9653 - set supervision version to 0.2.0 2023-02-07 19:02:19 +01:00
SkalskiP 14d23eb9df - fix bug in BoxAnnotator 2023-02-07 14:14:04 +01:00
SkalskiP 602d3af009 - fix bug in VideoSink 2023-02-07 13:42:42 +01:00
SkalskiP b8b08cacd3 - make it simpler to import process_video function 2023-02-07 13:24:52 +01:00
SkalskiP c8868c3412 - small API fixes in PolygonZoneAnnotator and BoxAnnotator 2023-02-07 12:34:59 +01:00
SkalskiP 52a8193b4a - default value for triggering_position in PolygonZone added 2023-02-07 12:24:56 +01:00
SkalskiP 51a1655bd4 - improve ease of import of supervision components 2023-02-07 12:06:56 +01:00
SkalskiP 38bf4b0f54 - matplotlib added to setup.py 2023-02-07 11:52:07 +01:00
SkalskiP 05705bd4f2 - Fix linter issues 2023-02-07 11:49:16 +01:00
SkalskiP 940bcb8bc5 - BoxAnnotator.annotate() fixed 2023-02-07 11:39:11 +01:00
SkalskiP a64a252ae3 - Detections.from_yolov8() added 2023-02-07 11:31:37 +01:00
SkalskiP 4afc650eb2 - skip_label argument added to BoxAnnotator.annotate() 2023-02-07 11:18:00 +01:00
SkalskiP 8d091a7a00 - Rename LineCounter to LineZone
- Rename LineCounter.update() to LineZone.trigger() to make the API consistent with LineZone
- Rename LineCounterAnnotator to LineZoneAnnotator
2023-02-07 09:30:53 +01:00
SkalskiP ccafa3339d - PolygonZone and PolygonZoneAnnotator added 2023-02-06 23:05:17 +01:00
SkalskiP 807785a820 - draw_text added 2023-02-06 22:35:00 +01:00
SkalskiP 66f81ceeb9 - ColorPalette.default() added
- Detections.from_yolov5() updated
- Detection.__getitem__ added
- get_polygon_center added
2023-02-06 22:26:44 +01:00
SkalskiP b7c1791236 🕵️ update Detection API 2023-02-06 16:14:24 +01:00
SkalskiP 0ac184b01d 👊 bump Supervision version to 0.2.0.dev0 2023-02-06 15:23:20 +01:00
SkalskiP 307ec8a042 📄 Documentation updates before 0.2.0 release 2023-02-06 14:49:09 +01:00
SkalskiP d5d7187fe4 Detection core API reformat 2023-02-06 14:28:16 +01:00
SkalskiP 0e3851506c Update VideoInfo API 🎬 2023-02-06 14:20:56 +01:00
SkalskiP 301f1df386 🎉 initial commit 0.2.0 API commit 2023-02-06 13:55:57 +01:00
Piotr Skalski ceabb83ac6 Merge pull request #11 from roboflow/add-citation
Create CITATION.cff file
2023-02-06 12:36:15 +01:00
Piotr Skalski dfb757b8cd Merge pull request #10 from roboflow/add-contributing
Create CONTRIBUTING.md file
2023-02-06 12:33:10 +01:00
James Gallagher ed0fd7b768 move analytics to main extra section in yaml 2023-02-02 09:29:10 +00:00
James Gallagher 5b2f8e4dcc create citation.cff file 2023-02-02 09:24:08 +00:00
James Gallagher 465830f3f6 create contribution guidelines 2023-02-01 16:26:08 +00:00
Piotr Skalski ae84cdeb02 Merge pull request #8 from roboflow/documentation-improvements
Refactor documentation for Supervision
2023-02-01 16:07:37 +01:00
James Gallagher 4fbec12b06 run linter 2023-02-01 14:37:54 +00:00
Piotr Skalski b5e33c5b30 Merge pull request #9 from roboflow/add-google-analytics
Add Google Analytics to documentation
2023-02-01 15:05:54 +01:00
James Gallagher 5a3df85fc6 add ga script 2023-02-01 10:33:30 +00:00
James Gallagher 61a2f4e0e6 add requirements.txt 2023-02-01 10:29:00 +00:00
James Gallagher 90a26747b6 create stub doc files, refactor docstrings, add placeholders for examples 2023-02-01 10:28:49 +00:00
Piotr Skalski 507f08f27f Update README.md 2023-01-31 13:58:04 +01:00
Piotr Skalski fcf30b79d4 Merge pull request #7 from roboflow/feature/github-issue-templates
🐞 Bug Report, 🤩 Feature Request and ❓ Question issue templates
2023-01-31 13:51:39 +01:00
SkalskiP a7b26abe0e 🧪 python 3.7 test still fail 2023-01-31 13:50:36 +01:00
SkalskiP 9590f3845e 🧪 python 3.7 test still fail 2023-01-31 13:49:28 +01:00
SkalskiP 8d8bd3b997 🧪 python 3.7 test still fail 2023-01-31 13:48:31 +01:00
SkalskiP 6b11a1fdad 🧪 python 3.7 test still fail 2023-01-31 13:31:28 +01:00
SkalskiP cb485878cc 🧪 python 3.7 test still fail 2023-01-31 13:29:47 +01:00
SkalskiP 8e70a3cd46 🧪 python 3.7 test still fail 2023-01-31 13:21:55 +01:00
SkalskiP 123dd77ba4 🧪 python 3.7 test fail 2023-01-31 13:19:34 +01:00
SkalskiP 7687d4a873 🐞 Bug Report, 🤩 Feature Request and ❓ Question issue templates 2023-01-31 13:09:53 +01:00
Piotr Skalski d941fe256d Merge pull request #6 from roboflow/feature/add-initial-docs
feature/add-initial-docs
2023-01-31 10:04:29 +01:00
SkalskiP d4a2adde9e Merge remote-tracking branch 'origin/feature/add-initial-docs' into feature/add-initial-docs
# Conflicts:
#	.github/workflows/test.yml
2023-01-31 10:02:09 +01:00
SkalskiP 8b229427e5 🧪 tests 2023-01-31 10:01:52 +01:00
SkalskiP b4641245c1 🧪 tests 2023-01-31 09:57:41 +01:00
SkalskiP bfcc3e40b2 🖤 make black happy 2023-01-31 09:44:32 +01:00
SkalskiP 1561bd0027 🎬 poc of video utils documentation 2023-01-31 09:36:39 +01:00
SkalskiP 8d14a4d93b Merge branch 'main' into feature/add-initial-docs 2023-01-30 22:44:45 +01:00
SkalskiP bd889ae3cc 🚩 add roboflow logo to docs 2023-01-30 22:40:51 +01:00
SkalskiP 02c20e81a9 📚 initial documentation commit 2023-01-30 19:50:25 +01:00
Piotr Skalski c236bb71d1 Update README.md 2023-01-20 14:08:53 +01:00
SkalskiP 22fa6f0a3a 📚 update README.md 2023-01-19 02:22:50 +01:00
SkalskiP c2ac59f992 👊 bump package version to 0.1.0 2023-01-19 02:02:42 +01:00
52 changed files with 2773 additions and 430 deletions
+65
View File
@@ -0,0 +1,65 @@
name: 🐞 Bug Report
# title: " "
description: Problems with Supervision
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thank you for submitting a Supervision 🐞 Bug Report!
- type: checkboxes
attributes:
label: Search before asking
description: >
Please search the [issues](https://github.com/roboflow/supervision/issues) to see if a similar bug report already exists.
options:
- label: >
I have searched the Supervision [issues](https://github.com/roboflow/supervision/issues) and found no similar bug report.
required: true
- type: textarea
attributes:
label: Bug
description: Provide console output with error messages and/or screenshots of the bug.
placeholder: |
💡 ProTip! Include as much information as possible (screenshots, logs, tracebacks etc.) to receive the most helpful response.
validations:
required: true
- type: textarea
attributes:
label: Environment
description: Please specify the software and hardware you used to produce the bug.
placeholder: |
- Supervision: 0.1.0
- OS: Ubuntu 20.04
- Python: 3.8.10
validations:
required: false
- type: textarea
attributes:
label: Minimal Reproducible Example
description: >
When asking a question, people will be better able to provide help if you provide code that they can easily understand and use to **reproduce** the problem.
This is referred to by community members as creating a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example).
placeholder: |
```
# Code to reproduce your issue here
```
validations:
required: false
- type: textarea
attributes:
label: Additional
description: Anything else you would like to share?
- type: checkboxes
attributes:
label: Are you willing to submit a PR?
description: >
(Optional) We encourage you to submit a [Pull Request](https://github.com/roboflow/supervision/pulls) (PR) to help improve Supervision for everyone, especially if you have a good understanding of how to implement a fix or feature.
options:
- label: Yes I'd like to help by submitting a PR!
@@ -0,0 +1,49 @@
name: 🤩 Feature Request
description: Suggest a Supervision idea
# title: " "
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
Thank you for submitting a Supervision 🤩 Feature Request!
- type: checkboxes
attributes:
label: Search before asking
description: >
Please search the [issues](https://github.com/roboflow/supervision/issues) to see if a similar feature request already exists.
options:
- label: >
I have searched the Supervision [issues](https://github.com/roboflow/supervision/issues) and found no similar feature requests.
required: true
- type: textarea
attributes:
label: Description
description: A short description of your feature.
placeholder: |
What new feature would you like to see in Supervision?
validations:
required: true
- type: textarea
attributes:
label: Use case
description: |
Describe the use case of your feature request. It will help us understand and prioritize the feature request.
placeholder: |
How would this feature be used, and who would use it?
- type: textarea
attributes:
label: Additional
description: Anything else you would like to share?
- type: checkboxes
attributes:
label: Are you willing to submit a PR?
description: >
(Optional) We encourage you to submit a [Pull Request](https://github.com/roboflow/supervision/pulls) (PR) to help improve Supervision for everyone, especially if you have a good understanding of how to implement a fix or feature.
options:
- label: Yes I'd like to help by submitting a PR!
+33
View File
@@ -0,0 +1,33 @@
name: ❓ Question
description: Ask a Supervision question
# title: " "
labels: [question]
body:
- type: markdown
attributes:
value: |
Thank you for asking a Supervision ❓ Question!
- type: checkboxes
attributes:
label: Search before asking
description: >
Please search the [issues](https://github.com/roboflow/supervision/issues) to see if a similar feature request already exists.
options:
- label: >
I have searched the Supervision [issues](https://github.com/roboflow/supervision/issues) and found no similar feature requests.
required: true
- type: textarea
attributes:
label: Question
description: What is your question?
placeholder: |
💡 ProTip! Include as much information as possible (screenshots, logs, tracebacks etc.) to receive the most helpful response.
validations:
required: true
- type: textarea
attributes:
label: Additional
description: Anything else you would like to share?
+18
View File
@@ -0,0 +1,18 @@
name: Docs WorkFlow
on:
push:
branches:
- master
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.x
- run: pip install mkdocs-material
- run: pip install "mkdocstrings[python]"
- run: mkdocs gh-deploy --force
+6 -2
View File
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8, 3.9]
python-version: [3.7, 3.8, 3.9, '3.10']
steps:
- name: 🛎️ Checkout
uses: actions/checkout@v3
@@ -22,7 +22,11 @@ jobs:
- name: 🦾 Install dependencies
run: |
python -m pip install --upgrade pip
pip install ".[dev]"
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
+2
View File
@@ -1,3 +1,5 @@
name: Welcome WorkFlow
on:
issues:
types: [opened]
+12
View File
@@ -0,0 +1,12 @@
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
keywords:
- computer vision
- image processing
- video processing
+51
View File
@@ -0,0 +1,51 @@
# Contributing to Supervision 🛠️
Thank you for your interest in contributing to Supervision!
We are actively improving this library to reduce the amount of work you need to do to solve common computer vision problems.
## Contribution Guidelines
We welcome contributions to:
1. Add a new feature to the library (guidance below).
2. Improve our documentation and add examples to make it clear how to leverage the supervision library.
3. Report bugs and issues in the project.
4. Submit a request for a new feature.
5. Improve our test coverage.
### 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.
For example, counting objects that cross a line anywhere on an image is a common problem in computer vision, but counting objects that cross a line 75% of the way through is less useful.
Before you contribute a new feature, consider submitting an Issue to discuss the feature so the community can weigh in and assist.
## 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.
When creating new functions, please ensure you have the following:
1. Docstrings for the function and all parameters.
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.
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
We provide two handy commands inside the `Makefile`, namely:
- `make style` to format the code
- `make check_code_quality` to check code quality (PEP8 basically)
So far, **there is no types checking with mypy**. See [issue](https://github.com/roboflow-ai/template-python/issues/4).
## 🧪 tests
[`pytests`](https://docs.pytest.org/en/7.1.x/) is used to run our tests.
+54 -18
View File
@@ -2,11 +2,12 @@
<p>
<a align="center" href="" target="_blank">
<img
width="850"
src="https://media.roboflow.com/open-source/supervision/roboflow-supervision-banner.png?ik-sdk-version=javascript-1.4.3&updatedAt=1674062891088"
width="100%"
src="https://media.roboflow.com/open-source/supervision/rf-supervision-banner.png?updatedAt=1678995927529"
>
</a>
</p>
<br>
<div align="center">
@@ -16,34 +17,34 @@
width="3%"
/>
</a>
<img src="https://github.com/SkalskiP/SkalskiP/blob/master/icons/transparent.png" width="3%"/>
<img src="https://raw.githubusercontent.com/ultralytics/assets/main/social/logo-transparent.png" width="3%"/>
<a href="https://roboflow.com">
<img
src="https://media.roboflow.com/notebooks/template/icons/purple/roboflow-app.png?ik-sdk-version=javascript-1.4.3&updatedAt=1672949746649"
width="3%"
/>
</a>
<img src="https://github.com/SkalskiP/SkalskiP/blob/master/icons/transparent.png" width="3%"/>
<img src="https://raw.githubusercontent.com/ultralytics/assets/main/social/logo-transparent.png" width="3%"/>
<a href="https://www.linkedin.com/company/roboflow-ai/">
<img
src="https://media.roboflow.com/notebooks/template/icons/purple/linkedin.png?ik-sdk-version=javascript-1.4.3&updatedAt=1672949633691"
width="3%"
/>
</a>
<img src="https://github.com/SkalskiP/SkalskiP/blob/master/icons/transparent.png" width="3%"/>
<img src="https://raw.githubusercontent.com/ultralytics/assets/main/social/logo-transparent.png" width="3%"/>
<a href="https://docs.roboflow.com">
<img
src="https://media.roboflow.com/notebooks/template/icons/purple/knowledge.png?ik-sdk-version=javascript-1.4.3&updatedAt=1672949634511"
width="3%"
/>
</a>
<img src="https://github.com/SkalskiP/SkalskiP/blob/master/icons/transparent.png" width="3%"/>
<img src="https://raw.githubusercontent.com/ultralytics/assets/main/social/logo-transparent.png" width="3%"/>
<a href="https://disuss.roboflow.com">
<img
src="https://media.roboflow.com/notebooks/template/icons/purple/forum.png?ik-sdk-version=javascript-1.4.3&updatedAt=1672949633584"
width="3%"
/>
<img src="https://github.com/SkalskiP/SkalskiP/blob/master/icons/transparent.png" width="3%"/>
<img src="https://raw.githubusercontent.com/ultralytics/assets/main/social/logo-transparent.png" width="3%"/>
<a href="https://blog.roboflow.com">
<img
src="https://media.roboflow.com/notebooks/template/icons/purple/blog.png?ik-sdk-version=javascript-1.4.3&updatedAt=1672949633605"
@@ -53,19 +54,33 @@
</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
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.
and may change.**
## 💻 install
```console
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 git@github.com:roboflow/supervision.git
git clone https://github.com/roboflow/supervision.git
cd supervision
# setup python environment and activate it
@@ -76,15 +91,36 @@ source venv/bin/activate
pip install -e ".[dev]"
```
## 🧹 Code Quality
</details>
We provide two handy commands inside the `Makefile`, namely:
## 📖 documentation
- `make style` to format the code
- `make check_code_quality` to check code quality (PEP8 basically)
Curious how Supervision can help you solve problems on your project? Visit our [documentation](https://roboflow.github.io/supervision) page!
So far, **there is no types checking with mypy**. See [issue](https://github.com/roboflow-ai/template-python/issues/4).
## 🎬 tutorials
## 🧪 Tests
🔥 [Subscribe](https://www.youtube.com/@Roboflow) and stay up to date with the latest changes to the Supervision library.
[`pytests`](https://docs.pytest.org/en/7.1.x/) is used to run our tests.
<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!
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 600 600" style="enable-background:new 0 0 600 600;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#6706CE;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
</style>
<g>
<circle class="st0" cx="300" cy="300" r="300"/>
</g>
<g>
<path class="st1" d="M279.34,267.67c27.7-21.58,15.55-51.11-30.24-45.4c-53.03,6.6-120.62,58.45-137.46,120.7
c-1.38,5.09-2.45,10.27-3.19,15.5c-0.73,5.15-1.12,10.35-1.17,15.55c-0.05,5.15,0.25,10.3,0.89,15.4
c0.64,5.11,1.64,10.17,2.98,15.14c1.36,5.03,3.06,9.97,5.09,14.76c2.09,4.92,4.51,9.68,7.24,14.26c2.83,4.76,5.98,9.31,9.4,13.64
c11.67,14.78,26.65,27.16,42.63,37.02c3.46,2.14,7.18,4.21,11.25,4.41c5.94,0.28,10.44-4.44,10.68-10.23
c0.09-2.18-0.46-4.24-1.46-6c-13.42-23.67-24.52-58.81-22.24-80.99C179.43,336.29,235.28,301.98,279.34,267.67z"/>
<path class="st1" d="M500.28,363.84c-2.9-6.91-10.16-10.81-17.51-10.15c-5.02,0.45-9.17,3.13-12.55,6.68
c-5.49,4.97-9.31,11.89-13.43,17.91c-4.86,7.08-9.94,14.02-15.41,20.63c-10.8,13.04-23.24,24.79-37.76,33.61
c-24.63,14.95-47.17,22.12-74.27,8.31c-19.83-15.86-13.13-34.6,3.29-49.87c28.08-26.1,61.93-44.42,88.52-72.44
c25-26.34,47.05-59.11,52.24-95.78c1.46-10.27,1.48-20.74-0.17-31c-7.8-48.31-51.48-78.97-95.59-92.01
C321.47,83.13,259.6,88,206.48,112.56c-21.86,10.11-42.23,23.65-59.94,39.98c-7.85,7.24-15.24,15.01-22.1,23.2
c-11.1,13.27-23.73,28.77-25.89,46.57c-1.33,10.99,8.58,18.59,18.99,17.18c6.98-0.94,13.16-5.15,18.3-9.74
c5.97-5.33,11.45-11.25,17.21-16.82c17.38-16.82,35.94-32.47,58.01-42.79c28.11-13.15,59.61-19.55,90.6-16.32
c31.43,3.27,69.67,17.44,81.15,49.98c11.97,33.97-13.73,67.36-37.18,89.06c-38.29,35.44-110.99,71.63-102.98,134.19
c4.29,33.51,28.4,62.19,59.24,74.96c53.66,22.23,111.74-4.84,148.89-46.18c12.46-13.24,30.38-35.82,46.68-69.46
C500.61,379.84,503.23,370.87,500.28,363.84z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 600 600" style="enable-background:new 0 0 600 600;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#6706CE;}
</style>
<g>
<circle class="st0" cx="300" cy="300" r="300"/>
</g>
<g>
<path class="st1" d="M279.34,267.67c27.7-21.58,15.55-51.11-30.24-45.4c-53.03,6.6-120.62,58.45-137.46,120.7
c-1.38,5.09-2.45,10.27-3.19,15.5c-0.73,5.15-1.12,10.35-1.17,15.55c-0.05,5.15,0.25,10.3,0.89,15.4
c0.64,5.11,1.64,10.17,2.98,15.14c1.36,5.03,3.06,9.97,5.09,14.76c2.09,4.92,4.51,9.68,7.24,14.26c2.83,4.76,5.98,9.31,9.4,13.64
c11.67,14.78,26.65,27.16,42.63,37.02c3.46,2.14,7.18,4.21,11.25,4.41c5.94,0.28,10.44-4.44,10.68-10.23
c0.09-2.18-0.46-4.24-1.46-6c-13.42-23.67-24.52-58.81-22.24-80.99C179.43,336.29,235.28,301.98,279.34,267.67z"/>
<path class="st1" d="M500.28,363.84c-2.9-6.91-10.16-10.81-17.51-10.15c-5.02,0.45-9.17,3.13-12.55,6.68
c-5.49,4.97-9.31,11.89-13.43,17.91c-4.86,7.08-9.94,14.02-15.41,20.63c-10.8,13.04-23.24,24.79-37.76,33.61
c-24.63,14.95-47.17,22.12-74.27,8.31c-19.83-15.86-13.13-34.6,3.29-49.87c28.08-26.1,61.93-44.42,88.52-72.44
c25-26.34,47.05-59.11,52.24-95.78c1.46-10.27,1.48-20.74-0.17-31c-7.8-48.31-51.48-78.97-95.59-92.01
C321.47,83.13,259.6,88,206.48,112.56c-21.86,10.11-42.23,23.65-59.94,39.98c-7.85,7.24-15.24,15.01-22.1,23.2
c-11.1,13.27-23.73,28.77-25.89,46.57c-1.33,10.99,8.58,18.59,18.99,17.18c6.98-0.94,13.16-5.15,18.3-9.74
c5.97-5.33,11.45-11.25,17.21-16.82c17.38-16.82,35.94-32.47,58.01-42.79c28.11-13.15,59.61-19.55,90.6-16.32
c31.43,3.27,69.67,17.44,81.15,49.98c11.97,33.97-13.73,67.36-37.18,89.06c-38.29,35.44-110.99,71.63-102.98,134.19
c4.29,33.51,28.4,62.19,59.24,74.96c53.66,22.23,111.74-4.84,148.89-46.18c12.46-13.24,30.38-35.82,46.68-69.46
C500.61,379.84,503.23,370.87,500.28,363.84z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 600 600" style="enable-background:new 0 0 600 600;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
</style>
<path class="st0" d="M300,0C134.31,0,0,134.31,0,300s134.31,300,300,300s300-134.31,300-300S465.69,0,300,0z M195.98,472.42
c1,1.76,1.55,3.82,1.46,6c-0.24,5.79-4.74,10.51-10.68,10.23c-4.07-0.19-7.78-2.27-11.25-4.41c-15.98-9.86-30.96-22.24-42.63-37.02
c-3.42-4.33-6.57-8.89-9.4-13.64c-2.72-4.58-5.15-9.34-7.24-14.26c-2.03-4.79-3.74-9.72-5.09-14.76
c-1.34-4.97-2.33-10.03-2.98-15.14c-0.64-5.1-0.94-10.25-0.89-15.4c0.05-5.2,0.45-10.4,1.17-15.55c0.74-5.23,1.81-10.41,3.19-15.5
c16.84-62.24,84.43-114.09,137.46-120.7c45.79-5.71,57.94,23.81,30.24,45.4c-44.06,34.32-99.92,68.63-105.6,123.77
C171.46,413.61,182.55,448.75,195.98,472.42z M497.44,386.39c-16.3,33.65-34.22,56.22-46.68,69.46
c-37.14,41.34-95.23,68.4-148.89,46.18c-30.83-12.77-54.94-41.45-59.24-74.96c-8.02-62.55,64.69-98.74,102.98-134.19
c23.45-21.71,49.16-55.1,37.18-89.06c-11.47-32.55-49.72-46.71-81.15-49.98c-30.99-3.23-62.49,3.17-90.6,16.32
c-22.07,10.32-40.62,25.97-58.01,42.79c-5.75,5.57-11.24,11.49-17.21,16.82c-5.14,4.59-11.31,8.79-18.29,9.74
c-10.41,1.41-20.32-6.18-18.99-17.18c2.16-17.8,14.79-33.29,25.89-46.57c6.86-8.19,14.25-15.96,22.1-23.2
c17.71-16.34,38.08-29.87,59.94-39.98C259.6,88,321.47,83.13,377.64,99.73c44.11,13.04,87.79,43.7,95.59,92.01
c1.65,10.25,1.63,20.72,0.17,31c-5.19,36.67-27.24,69.44-52.24,95.78c-26.59,28.02-60.44,46.35-88.52,72.44
c-16.43,15.27-23.13,34.01-3.29,49.87c27.1,13.81,49.64,6.64,74.27-8.31c14.52-8.82,26.96-20.57,37.76-33.61
c5.48-6.61,10.56-13.55,15.41-20.63c4.13-6.02,7.94-12.94,13.43-17.91c3.38-3.55,7.53-6.23,12.55-6.68
c7.35-0.66,14.61,3.23,17.51,10.15C503.23,370.87,500.61,379.84,497.44,386.39z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+57
View File
@@ -0,0 +1,57 @@
### 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`.
### 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.
### 0.5.1 <small>April 12, 2023</small>
- Fixed `Detections.__getitem__` method did not return mask for selected item.
- Fixed `Detections.area` crashed for mask detections.
### 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.
### 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`.
### 0.3.2 <small>March 23, 2023</small>
- Changed [[#50](https://github.com/roboflow/supervision/issues/50)]: Allow `Detections.class_id` to be `None`.
### 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.
### 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.area` to dynamically calculate bounding box area.
- Added: `Detections.wth_nms` to filter out double detections with NMS. Initial - only class agnostic - implementation.
### 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>
Say hello to Supervision 👋
+8
View File
@@ -0,0 +1,8 @@
!!! 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
+7
View File
@@ -0,0 +1,7 @@
## BoxAnnotator
:::supervision.detection.annotate.BoxAnnotator
## MaskAnnotator
:::supervision.detection.annotate.MaskAnnotator
+3
View File
@@ -0,0 +1,3 @@
## Detections
:::supervision.detection.core.Detections
+7
View File
@@ -0,0 +1,7 @@
## PolygonZone
:::supervision.detection.tools.polygon_zone.PolygonZone
## PolygonZoneAnnotator
:::supervision.detection.tools.polygon_zone.PolygonZoneAnnotator
+27
View File
@@ -0,0 +1,27 @@
## box_iou_batch
:::supervision.detection.utils.box_iou_batch
## non_max_suppression
:::supervision.detection.utils.non_max_suppression
## polygon_to_mask
:::supervision.detection.utils.polygon_to_mask
## mask_to_xyxy
:::supervision.detection.utils.mask_to_xyxy
## mask_to_polygons
:::supervision.detection.utils.mask_to_polygons
## polygon_to_xyxy
:::supervision.detection.utils.polygon_to_xyxy
## filter_polygons_by_area
:::supervision.detection.utils.filter_polygons_by_area
+19
View File
@@ -0,0 +1,19 @@
## draw_line
:::supervision.draw.utils.draw_line
## draw_rectangle
:::supervision.draw.utils.draw_rectangle
## draw_filled_rectangle
:::supervision.draw.utils.draw_filled_rectangle
## draw_polygon
:::supervision.draw.utils.draw_polygon
## draw_text
:::supervision.draw.utils.draw_text
+37
View File
@@ -0,0 +1,37 @@
<div align="center">
<p>
<a align="center" href="" target="_blank">
<img
width="850"
src="https://media.roboflow.com/open-source/supervision/roboflow-supervision-banner.png?ik-sdk-version=javascript-1.4.3&updatedAt=1674062891088"
>
</a>
</p>
</div>
## 👋 Welcome
Supervision is a set of easy-to-use utilities 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.
## 💻 How to Install
You can install `supervision` with pip in a
[**3.10>=Python>=3.7**](https://www.python.org/) environment.
!!! example "Pip install method (recommended)"
```bash
pip install supervision
```
!!! example "Git clone method (for development)"
```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
+7
View File
@@ -0,0 +1,7 @@
## plot_image
:::supervision.notebook.utils.plot_image
## plot_images_grid
:::supervision.notebook.utils.plot_images_grid
+15
View File
@@ -0,0 +1,15 @@
## VideoInfo
:::supervision.video.VideoInfo
## VideoSink
:::supervision.video.VideoSink
## get_video_frames_generator
:::supervision.video.get_video_frames_generator
## process_video
:::supervision.video.process_video
+62
View File
@@ -0,0 +1,62 @@
site_name: Supervision
site_url: https://roboflow.github.io/supervision
site_author: Roboflow
site_description: A set of easy-to-use utils that will come in handy in any Computer Vision project
repo_name: roboflow/supervision
repo_url: https://github.com/roboflow/supervision
edit_uri: https://github.com/roboflow/supervision/tree/main/docs
copyright: Roboflow 2023. All rights reserved.
extra:
social:
- icon: fontawesome/brands/github
link: https://github.com/roboflow
- icon: fontawesome/brands/youtube
link: https://www.youtube.com/roboflow
- icon: fontawesome/brands/linkedin
link: https://www.linkedin.com/company/roboflow-ai/mycompany/
- icon: fontawesome/brands/twitter
link: https://twitter.com/roboflow
analytics:
provider: google
property: G-P7ZG0Y19G5
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
- Changelog: changelog.md
theme:
name: 'material'
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'
font:
text: Roboto
code: Roboto Mono
plugins:
- mkdocstrings
- search
markdown_extensions:
- admonition
- pymdownx.details
- pymdownx.superfences
- toc:
permalink: true
+6 -3
View File
@@ -24,8 +24,9 @@ setuptools.setup(
long_description_content_type='text/markdown',
url='https://github.com/roboflow/supervision',
install_requires=[
'numpy',
'opencv-python'
'numpy>=1.20.0',
'opencv-python',
'matplotlib'
],
packages=find_packages(exclude=("tests",)),
extras_require={
@@ -36,7 +37,9 @@ setuptools.setup(
'twine',
'pytest',
'wheel',
'notebook'
'notebook',
'mkdocs-material',
'mkdocstrings[python]'
],
},
classifiers=[
+28 -1
View File
@@ -1 +1,28 @@
__version__ = "0.0.2"
__version__ = "0.6.0"
from supervision.dataset.core import Dataset
from supervision.detection.annotate import BoxAnnotator, MaskAnnotator
from supervision.detection.core import Detections
from supervision.detection.line_counter import LineZone, LineZoneAnnotator
from supervision.detection.tools.polygon_zone import PolygonZone, PolygonZoneAnnotator
from supervision.detection.utils import (
box_iou_batch,
filter_polygons_by_area,
mask_to_polygons,
mask_to_xyxy,
non_max_suppression,
polygon_to_mask,
polygon_to_xyxy,
)
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 (
VideoInfo,
VideoSink,
get_video_frames_generator,
process_video,
)
+128
View File
@@ -0,0 +1,128 @@
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import cv2
import numpy as np
from supervision.dataset.formats.pascal_voc import (
detections_to_pascal_voc,
load_pascal_voc_annotations,
)
from supervision.detection.core import Detections
from supervision.file import list_files_with_extensions
@dataclass
class Dataset:
"""
Dataclass containing information about the 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, Detections]
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,
) -> 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.
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
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
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).
"""
if images_directory_path:
images_path = Path(images_directory_path)
images_path.mkdir(parents=True, exist_ok=True)
if annotations_directory_path:
annotations_path = Path(annotations_directory_path)
annotations_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)
if annotations_directory_path:
annotation_name = Path(image_name).stem
pascal_voc_xml = detections_to_pascal_voc(
detections=detections,
classes=self.classes,
filename=image_name,
image_shape=image.shape,
min_image_area_percentage=min_image_area_percentage,
max_image_area_percentage=max_image_area_percentage,
approximation_percentage=approximation_percentage,
)
with open(annotations_path / f"{annotation_name}.xml", "w") as f:
f.write(pascal_voc_xml)
@classmethod
def from_pascal_voc(
cls, images_directory_path: str, annotations_directory_path: str
) -> Dataset:
"""
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.
Returns:
Dataset: A Dataset instance containing the loaded images and annotations.
"""
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"]
)
raw_annotations: List[Tuple[str, Detections, List[str]]] = [
load_pascal_voc_annotations(annotation_path=str(annotation_path))
for annotation_path in annotation_paths
]
classes = []
for annotation in raw_annotations:
classes.extend(annotation[2])
classes = list(set(classes))
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)
images = {
image_path.name: cv2.imread(str(image_path)) for image_path in image_paths
}
annotations = {
image_name: detections for image_name, detections, _ in raw_annotations
}
return Dataset(classes=classes, images=images, annotations=annotations)
+171
View File
@@ -0,0 +1,171 @@
from typing import List, Optional, Tuple
from xml.dom.minidom import parseString
from xml.etree.ElementTree import Element, SubElement, parse, tostring
import numpy as np
from supervision.detection.core import Detections
from supervision.detection.utils import (
approximate_polygon,
filter_polygons_by_area,
mask_to_polygons,
polygon_to_xyxy,
)
def object_to_pascal_voc(
xyxy: np.ndarray, name: str, polygon: Optional[np.ndarray] = None
) -> Element:
root = Element("object")
object_name = SubElement(root, "name")
object_name.text = name
bndbox = SubElement(root, "bndbox")
xmin = SubElement(bndbox, "xmin")
xmin.text = str(int(xyxy[0]))
ymin = SubElement(bndbox, "ymin")
ymin.text = str(int(xyxy[1]))
xmax = SubElement(bndbox, "xmax")
xmax.text = str(int(xyxy[2]))
ymax = SubElement(bndbox, "ymax")
ymax.text = str(int(xyxy[3]))
if polygon is not None:
object_polygon = SubElement(root, "polygon")
for index, point in enumerate(polygon, start=1):
x_coordinate, y_coordinate = point
x = SubElement(object_polygon, f"x{index}")
x.text = str(x_coordinate)
y = SubElement(object_polygon, f"y{index}")
y.text = str(y_coordinate)
return root
def detections_to_pascal_voc(
detections: Detections,
classes: List[str],
filename: str,
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,
) -> str:
"""
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.
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).
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")
# Add folder element
folder = SubElement(annotation, "folder")
folder.text = "VOC"
# Add filename element
file_name = SubElement(annotation, "filename")
file_name.text = filename
# Add source element
source = SubElement(annotation, "source")
database = SubElement(source, "database")
database.text = "roboflow.ai"
# Add size element
size = SubElement(annotation, "size")
w = SubElement(size, "width")
w.text = str(width)
h = SubElement(size, "height")
h.text = str(height)
d = SubElement(size, "depth")
d.text = str(depth)
# Add segmented element
segmented = SubElement(annotation, "segmented")
segmented.text = "0"
# Add object elements
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,
)
for polygon in polygons:
approx_polygon = approximate_polygon(
polygon=polygon, percentage=approximation_percentage
)
xyxy = polygon_to_xyxy(polygon=approx_polygon)
next_object = object_to_pascal_voc(
xyxy=xyxy, name=name, polygon=approx_polygon
)
annotation.append(next_object)
else:
next_object = object_to_pascal_voc(xyxy=xyxy, name=name)
annotation.append(next_object)
# Generate XML string
xml_string = parseString(tostring(annotation)).toprettyxml(indent=" ")
return xml_string
def load_pascal_voc_annotations(
annotation_path: str,
) -> Tuple[str, Detections, List[str]]:
"""
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.
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.
"""
tree = parse(annotation_path)
root = tree.getroot()
image_name = root.find("filename").text
xyxy = []
class_names = []
for obj in root.findall("object"):
class_name = obj.find("name").text
class_names.append(class_name)
bbox = obj.find("bndbox")
x1 = int(bbox.find("xmin").text)
y1 = int(bbox.find("ymin").text)
x2 = int(bbox.find("xmax").text)
y2 = int(bbox.find("ymax").text)
xyxy.append([x1, y1, x2, y2])
xyxy = np.array(xyxy)
detections = Detections(xyxy=xyxy)
return image_name, detections, class_names
View File
+174
View File
@@ -0,0 +1,174 @@
from typing import List, Optional, Union
import cv2
import numpy as np
from supervision.detection.core import Detections
from supervision.draw.color import Color, ColorPalette
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
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
"""
def __init__(
self,
color: Union[Color, ColorPalette] = ColorPalette.default(),
thickness: int = 2,
text_color: Color = Color.black(),
text_scale: float = 0.5,
text_thickness: int = 1,
text_padding: int = 10,
):
self.color: Union[Color, ColorPalette] = color
self.thickness: int = thickness
self.text_color: Color = text_color
self.text_scale: float = text_scale
self.text_thickness: int = text_thickness
self.text_padding: int = text_padding
def annotate(
self,
scene: np.ndarray,
detections: Detections,
labels: Optional[List[str]] = None,
skip_label: bool = False,
) -> np.ndarray:
"""
Draws bounding boxes on the frame using the detections provided.
Parameters:
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.
Returns:
np.ndarray: The image with the bounding boxes drawn on it
"""
font = cv2.FONT_HERSHEY_SIMPLEX
for i in range(len(detections)):
x1, y1, x2, y2 = detections.xyxy[i].astype(int)
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
)
cv2.rectangle(
img=scene,
pt1=(x1, y1),
pt2=(x2, y2),
color=color.as_bgr(),
thickness=self.thickness,
)
if skip_label:
continue
text = (
f"{class_id}"
if (labels is None or len(detections) != len(labels))
else labels[i]
)
text_width, text_height = cv2.getTextSize(
text=text,
fontFace=font,
fontScale=self.text_scale,
thickness=self.text_thickness,
)[0]
text_x = x1 + self.text_padding
text_y = y1 - self.text_padding
text_background_x1 = x1
text_background_y1 = y1 - 2 * self.text_padding - text_height
text_background_x2 = x1 + 2 * self.text_padding + text_width
text_background_y2 = y1
cv2.rectangle(
img=scene,
pt1=(text_background_x1, text_background_y1),
pt2=(text_background_x2, text_background_y2),
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 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
+397
View File
@@ -0,0 +1,397 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Iterator, List, Optional, Tuple
import numpy as np
from supervision.detection.utils import non_max_suppression, xywh_to_xyxy
from supervision.geometry.core import Position
def _validate_xyxy(xyxy: Any, n: int) -> None:
is_valid = isinstance(xyxy, np.ndarray) and xyxy.shape == (n, 4)
if not is_valid:
raise ValueError("xyxy must be 2d np.ndarray with (n, 4) shape")
def _validate_mask(mask: Any, n: int) -> None:
is_valid = mask is None or (
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")
def _validate_class_id(class_id: Any, n: int) -> None:
is_valid = class_id is None or (
isinstance(class_id, np.ndarray) and class_id.shape == (n,)
)
if not is_valid:
raise ValueError("class_id must be None or 1d np.ndarray with (n,) shape")
def _validate_confidence(confidence: Any, n: int) -> None:
is_valid = confidence is None or (
isinstance(confidence, np.ndarray) and confidence.shape == (n,)
)
if not is_valid:
raise ValueError("confidence must be None or 1d np.ndarray with (n,) shape")
def _validate_tracker_id(tracker_id: Any, n: int) -> None:
is_valid = tracker_id is None or (
isinstance(tracker_id, np.ndarray) and tracker_id.shape == (n,)
)
if not is_valid:
raise ValueError("tracker_id must be None or 1d np.ndarray with (n,) shape")
@dataclass
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
mask: np.Optional[np.ndarray] = None
confidence: Optional[np.ndarray] = None
class_id: Optional[np.ndarray] = None
tracker_id: Optional[np.ndarray] = None
def __post_init__(self):
n = len(self.xyxy)
_validate_xyxy(xyxy=self.xyxy, n=n)
_validate_mask(mask=self.mask, n=n)
_validate_class_id(class_id=self.class_id, n=n)
_validate_confidence(confidence=self.confidence, n=n)
_validate_tracker_id(tracker_id=self.tracker_id, n=n)
def __len__(self):
"""
Returns the number of detections in the Detections object.
"""
return len(self.xyxy)
def __iter__(
self,
) -> Iterator[
Tuple[
np.ndarray,
Optional[np.ndarray],
Optional[float],
Optional[int],
Optional[int],
]
]:
"""
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 (
self.xyxy[i],
self.mask[i] if self.mask is not None else None,
self.confidence[i] if self.confidence is not None else None,
self.class_id[i] if self.class_id is not None else None,
self.tracker_id[i] if self.tracker_id is not None else None,
)
def __eq__(self, other: Detections):
return all(
[
np.array_equal(self.xyxy, other.xyxy),
any(
[
self.mask is None and other.mask is None,
np.array_equal(self.mask, other.mask),
]
),
any(
[
self.class_id is None and other.class_id is None,
np.array_equal(self.class_id, other.class_id),
]
),
any(
[
self.confidence is None and other.confidence is None,
np.array_equal(self.confidence, other.confidence),
]
),
any(
[
self.tracker_id is None and other.tracker_id is None,
np.array_equal(self.tracker_id, other.tracker_id),
]
),
]
)
@classmethod
def from_yolov5(cls, yolov5_results) -> Detections:
"""
Creates a Detections instance from a YOLOv5 output Detections
Args:
yolov5_results (yolov5.models.common.Detections): The output Detections instance from YOLOv5
Returns:
Detections: A new Detections object.
Example:
```python
>>> import torch
>>> from supervision import Detections
>>> model = torch.hub.load('ultralytics/yolov5', 'yolov5s')
>>> results = model(IMAGE)
>>> detections = Detections.from_yolov5(results)
```
"""
yolov5_detections_predictions = yolov5_results.pred[0].cpu().cpu().numpy()
return cls(
xyxy=yolov5_detections_predictions[:, :4],
confidence=yolov5_detections_predictions[:, 4],
class_id=yolov5_detections_predictions[:, 5].astype(int),
)
@classmethod
def from_yolov8(cls, yolov8_results) -> Detections:
"""
Creates a Detections instance from a YOLOv8 output Results
Args:
yolov8_results (ultralytics.yolo.engine.results.Results): The output Results instance from YOLOv8
Returns:
Detections: A new Detections object.
Example:
```python
>>> from ultralytics import YOLO
>>> from supervision import Detections
>>> model = YOLO('yolov8s.pt')
>>> yolov8_results = model(IMAGE)[0]
>>> detections = Detections.from_yolov8(yolov8_results)
```
"""
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),
)
@classmethod
def from_transformers(cls, transformers_results: dict) -> Detections:
"""
Creates a Detections instance from Object Detection Transformer output Results
Returns:
Detections: A new Detections object.
"""
return cls(
xyxy=transformers_results["boxes"].cpu().numpy(),
confidence=transformers_results["scores"].cpu().numpy(),
class_id=transformers_results["labels"].cpu().numpy().astype(int),
)
@classmethod
def from_detectron2(cls, detectron2_results) -> Detections:
return cls(
xyxy=detectron2_results["instances"].pred_boxes.tensor.cpu().numpy(),
confidence=detectron2_results["instances"].scores.cpu().numpy(),
class_id=detectron2_results["instances"]
.pred_classes.cpu()
.numpy()
.astype(int),
)
@classmethod
def from_roboflow(cls, roboflow_result: dict, class_list: List[str]) -> Detections:
xyxy = []
confidence = []
class_id = []
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"])
return Detections(
xyxy=np.array(xyxy),
confidence=np.array(confidence),
class_id=np.array(class_id).astype(int),
)
@classmethod
def from_sam(cls, sam_result: List[dict]) -> Detections:
"""
Creates a Detections instance from Segment Anything Model (SAM) by Meta AI.
Args:
sam_result (List[dict]): The output Results instance from SAM
Returns:
Detections: A new Detections object.
Example:
```python
>>> from segment_anything import sam_model_registry, SamAutomaticMaskGenerator
>>> import supervision as sv
>>> 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)
```
"""
sorted_generated_masks = sorted(
sam_result, key=lambda x: x["area"], reverse=True
)
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)
@classmethod
def from_coco_annotations(cls, coco_annotation: dict) -> Detections:
xyxy, class_id = [], []
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"])
return cls(xyxy=np.array(xyxy), class_id=np.array(class_id))
@classmethod
def empty(cls) -> Detections:
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:
"""
Returns the bounding box coordinates for a specific anchor.
Args:
anchor (Position): Position of bounding box anchor for which to return the coordinates.
Returns:
np.ndarray: An array of shape `(n, 2)` containing the bounding box anchor coordinates in format `[x, y]`.
"""
if anchor == Position.CENTER:
return np.array(
[
(self.xyxy[:, 0] + self.xyxy[:, 2]) / 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()
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)}."
)
@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.
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.
"""
if self.mask is not None:
return np.array([np.sum(mask) for mask in self.mask])
else:
return self.box_area
@property
def box_area(self) -> np.ndarray:
"""
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.
"""
return (self.xyxy[:, 3] - self.xyxy[:, 1]) * (self.xyxy[:, 2] - self.xyxy[:, 0])
def with_nms(
self, threshold: float = 0.5, class_agnostic: bool = False
) -> 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.
Returns:
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.
"""
if len(self) == 0:
return self
assert (
self.confidence is not None
), f"Detections confidence must be given for NMS to be executed."
if class_agnostic:
predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1)))
indices = non_max_suppression(
predictions=predictions, iou_threshold=threshold
)
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."
)
predictions = np.hstack(
(self.xyxy, self.confidence.reshape(-1, 1), self.class_id.reshape(-1, 1))
)
indices = non_max_suppression(predictions=predictions, iou_threshold=threshold)
return self[indices]
@@ -3,31 +3,39 @@ from typing import Dict
import cv2
import numpy as np
from supervision.detection.core import Detections
from supervision.draw.color import Color
from supervision.geometry.dataclasses import Point, Rect, Vector
from supervision.tools.detections import Detections
from supervision.geometry.core import Point, Rect, Vector
class LineCounter:
class LineZone:
"""
Count the number of objects that cross a line.
"""
def __init__(self, start: Point, end: Point):
"""
Initialize a LineCounter object.
:param start: Point : The starting point of the line.
:param end: Point : The ending point of the line.
Attributes:
start (Point): The starting point of the line.
end (Point): The ending point of the line.
"""
self.vector = Vector(start=start, end=end)
self.tracker_state: Dict[str, bool] = {}
self.in_count: int = 0
self.out_count: int = 0
def update(self, detections: Detections):
def trigger(self, detections: Detections):
"""
Update the in_count and out_count for the detections that cross the line.
:param detections: Detections : The detections for which to update the counts.
Attributes:
detections (Detections): The detections for which to update the counts.
"""
for xyxy, confidence, class_id, tracker_id in detections:
for xyxy, _, confidence, class_id, tracker_id in detections:
# handle detections with no tracker_id
if tracker_id is None:
continue
@@ -63,7 +71,7 @@ class LineCounter:
self.out_count += 1
class LineCounterAnnotator:
class LineZoneAnnotator:
def __init__(
self,
thickness: float = 2,
@@ -77,13 +85,15 @@ class LineCounterAnnotator:
"""
Initialize the LineCounterAnnotator object with default values.
:param thickness: float : The thickness of the line that will be drawn.
:param color: Color : The color of the line that will be drawn.
:param text_thickness: float : The thickness of the text that will be drawn.
:param text_color: Color : The color of the text that will be drawn.
:param text_scale: float : The scale of the text that will be drawn.
:param text_offset: float : The offset of the text that will be drawn.
:param text_padding: int : The padding of the text that will be drawn.
Attributes:
thickness (float): The thickness of the line that will be drawn.
color (Color): The color of the line that will be drawn.
text_thickness (float): The thickness of the text that will be drawn.
text_color (Color): The color of the text that will be drawn.
text_scale (float): The scale of the text that will be drawn.
text_offset (float): The offset of the text that will be drawn.
text_padding (int): The padding of the text that will be drawn.
"""
self.thickness: float = thickness
self.color: Color = color
@@ -93,13 +103,17 @@ class LineCounterAnnotator:
self.text_offset: float = text_offset
self.text_padding: int = text_padding
def annotate(self, frame: np.ndarray, line_counter: LineCounter) -> np.ndarray:
def annotate(self, frame: np.ndarray, line_counter: LineZone) -> np.ndarray:
"""
Draws the line on the frame using the line_counter provided.
:param frame: np.ndarray : The image on which the line will be drawn
:param line_counter: LineCounter : The line counter that will be used to draw the line
:return: np.ndarray : The image with the line drawn on it
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.
Returns:
np.ndarray: The image with the line drawn on it.
"""
cv2.line(
frame,
+132
View File
@@ -0,0 +1,132 @@
from dataclasses import replace
from typing import Optional, Tuple
import cv2
import numpy as np
from supervision import Detections
from supervision.detection.utils import clip_boxes, polygon_to_mask
from supervision.draw.color import Color
from supervision.draw.utils import draw_polygon, draw_text
from supervision.geometry.core import Position
from supervision.geometry.utils import get_polygon_center
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
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)
current_count (int): The current count of detected objects within the zone
mask (np.ndarray): The 2D bool mask for the polygon zone
"""
def __init__(
self,
polygon: np.ndarray,
frame_resolution_wh: Tuple[int, int],
triggering_position: Position = Position.BOTTOM_CENTER,
):
self.polygon = polygon
self.frame_resolution_wh = frame_resolution_wh
self.triggering_position = triggering_position
self.current_count = 0
width, height = frame_resolution_wh
self.mask = polygon_to_mask(
polygon=polygon, resolution_wh=(width + 1, height + 1)
)
def trigger(self, detections: Detections) -> np.ndarray:
"""
Determines if the detections are within the polygon zone.
Parameters:
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
"""
clipped_xyxy = clip_boxes(
boxes_xyxy=detections.xyxy, frame_resolution_wh=self.frame_resolution_wh
)
clipped_detections = replace(detections, xyxy=clipped_xyxy)
clipped_anchors = np.ceil(
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)
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.
Attributes:
zone (PolygonZone): The polygon zone to be annotated
color (Color): The color to draw the polygon lines
thickness (int): The thickness of the polygon lines, default is 2
text_color (Color): The color of the text on the polygon, default is black
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
center (Tuple[int, int]): The center of the polygon for text placement
"""
def __init__(
self,
zone: PolygonZone,
color: Color,
thickness: int = 2,
text_color: Color = Color.black(),
text_scale: float = 0.5,
text_thickness: int = 1,
text_padding: int = 10,
):
self.zone = zone
self.color = color
self.thickness = thickness
self.text_color = text_color
self.text_scale = text_scale
self.text_thickness = text_thickness
self.text_padding = text_padding
self.font = cv2.FONT_HERSHEY_SIMPLEX
self.center = get_polygon_center(polygon=zone.polygon)
def annotate(self, scene: np.ndarray, label: Optional[str] = None) -> np.ndarray:
"""
Annotates the polygon zone within a frame with a count of detected objects.
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)
Returns:
np.ndarray: The image with the polygon zone and count of detected objects
"""
annotated_frame = draw_polygon(
scene=scene,
polygon=self.zone.polygon,
color=self.color,
thickness=self.thickness,
)
annotated_frame = draw_text(
scene=annotated_frame,
text=str(self.zone.current_count) if label is None else label,
text_anchor=self.center,
background_color=self.color,
text_color=self.text_color,
text_scale=self.text_scale,
text_thickness=self.text_thickness,
text_padding=self.text_padding,
text_font=self.font,
)
return annotated_frame
+253
View File
@@ -0,0 +1,253 @@
from typing import List, Optional, Tuple
import cv2
import numpy as np
MIN_POLYGON_POINT_COUNT = 3
def polygon_to_mask(polygon: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray:
"""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.
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.
"""
width, height = resolution_wh
mask = np.zeros((height, width), dtype=np.uint8)
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.
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.
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.
"""
def box_area(box):
return (box[2] - box[0]) * (box[3] - box[1])
area_true = box_area(boxes_true.T)
area_detection = box_area(boxes_detection.T)
top_left = np.maximum(boxes_true[:, None, :2], boxes_detection[:, :2])
bottom_right = np.minimum(boxes_true[:, None, 2:], boxes_detection[:, 2:])
area_inter = np.prod(np.clip(bottom_right - top_left, a_min=0, a_max=None), 2)
return area_inter / (area_true[:, None] + area_detection - area_inter)
def non_max_suppression(
predictions: np.ndarray, iou_threshold: float = 0.5
) -> np.ndarray:
"""
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.
Returns:
np.ndarray: A boolean array indicating which predictions to keep after non-maximum suppression.
Raises:
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, "
f"{iou_threshold} given."
)
rows, columns = predictions.shape
# add column #5 - category filled with zeros for agnostic nms
if columns == 5:
predictions = np.c_[predictions, np.zeros(rows)]
# sort predictions column #4 - score
sort_index = np.flip(predictions[:, 4].argsort())
predictions = predictions[sort_index]
boxes = predictions[:, :4]
categories = predictions[:, 5]
ious = box_iou_batch(boxes, boxes)
ious = ious - np.eye(rows)
keep = np.ones(rows, dtype=bool)
for index, (iou, category) in enumerate(zip(ious, categories)):
if not keep[index]:
continue
# drop detections with iou > iou_threshold and same category as current detections
condition = (iou > iou_threshold) & (categories == category)
keep = keep & ~condition
return keep[sort_index.argsort()]
def clip_boxes(
boxes_xyxy: np.ndarray, frame_resolution_wh: Tuple[int, int]
) -> np.ndarray:
"""
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
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.
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.
"""
result = np.copy(boxes_xyxy)
width, height = frame_resolution_wh
result[:, [0, 2]] = result[:, [0, 2]].clip(0, width)
result[:, [1, 3]] = result[:, [1, 3]].clip(0, height)
return result
def xywh_to_xyxy(boxes_xywh: np.ndarray) -> np.ndarray:
xyxy = boxes_xywh.copy()
xyxy[:, 2] = boxes_xywh[:, 0] + boxes_xywh[:, 2]
xyxy[:, 3] = boxes_xywh[:, 1] + boxes_xywh[:, 3]
return xyxy
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
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
"""
n = masks.shape[0]
bboxes = np.zeros((n, 4), dtype=int)
for i, mask in enumerate(masks):
rows, cols = np.where(mask)
if len(rows) > 0 and len(cols) > 0:
x_min, x_max = np.min(cols), np.max(cols)
y_min, y_max = np.min(rows), np.max(rows)
bboxes[i, :] = [x_min, y_min, x_max, y_max]
return bboxes
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.
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`
are excluded from the output.
"""
contours, _ = cv2.findContours(
mask.astype(np.uint8), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE
)
return [
np.squeeze(contour, axis=1)
for contour in contours
if contour.shape[0] >= MIN_POLYGON_POINT_COUNT
]
def filter_polygons_by_area(
polygons: List[np.ndarray],
min_area: Optional[float] = None,
max_area: Optional[float] = None,
) -> List[np.ndarray]:
"""
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)`,
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.
Returns:
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
ares = [cv2.contourArea(polygon) for polygon in polygons]
return [
polygon
for polygon, area in zip(polygons, ares)
if (min_area is None or area >= min_area)
and (max_area is None or area <= max_area)
]
def polygon_to_xyxy(polygon: np.ndarray) -> np.ndarray:
"""
Converts a polygon represented by a NumPy array into a bounding box.
Parameters:
polygon (np.ndarray): A polygon represented by a NumPy array of shape `(N, 2)`,
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.
"""
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:
"""
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.
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).
Returns:
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.
"""
if percentage < 0 or percentage >= 1:
raise ValueError("Percentage must be in the range [0, 1).")
target_points = max(int(len(polygon) * (1 - percentage)), 3)
if len(polygon) <= target_points:
return polygon
epsilon = 0
approximated_points = polygon
while len(approximated_points) > target_points:
epsilon += 0.1
approximated_points = cv2.approxPolyDP(polygon, epsilon, closed=True)
return np.squeeze(approximated_points, axis=1)
+6 -6
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import List, Tuple
DEFAULT_COLOR_PALETTE = [
@@ -94,11 +94,11 @@ class Color:
@dataclass
class ColorPalette:
colors: List[Color] = field(
default_factory=lambda: [
Color.from_hex(color_hex) for color_hex in DEFAULT_COLOR_PALETTE
]
)
colors: List[Color]
@classmethod
def default(cls) -> ColorPalette:
return ColorPalette.from_hex(color_hex_list=DEFAULT_COLOR_PALETTE)
@classmethod
def from_hex(cls, color_hex_list: List[str]):
+114 -17
View File
@@ -1,8 +1,10 @@
from typing import Optional
import cv2
import numpy as np
from supervision.commons.dataclasses import Point, Rect
from supervision.draw.color import Color
from supervision.geometry.core import Point, Rect
def draw_line(
@@ -11,12 +13,15 @@ def draw_line(
"""
Draws a line on a given scene.
:param scene: np.ndarray : The scene on which the line will be drawn
:param start: Point : The starting point of the line
:param end: Point : The end point of the line
:param color: Color : The color of the line
:param thickness: int : The thickness of the line
:return: np.ndarray : The scene with the line drawn on it
Parameters:
scene (np.ndarray): The scene on which the line will be drawn
start (Point): The starting point of the line
end (Point): The end point of the line
color (Color): The color of the line
thickness (int): The thickness of the line
Returns:
np.ndarray: The scene with the line drawn on it
"""
cv2.line(
scene,
@@ -34,11 +39,14 @@ def draw_rectangle(
"""
Draws a rectangle on an image.
:param scene: np.ndarray : The image on which to draw the rectangle.
:param rect: Rect : The rectangle to draw.
:param color: Color : The color of the rectangle.
:param thickness: int : The thickness of the rectangle border.
:return: np.ndarray : The image with the rectangle drawn on it.
Parameters:
scene (np.ndarray): The scene on which the rectangle will be drawn
rect (Rect): The rectangle to be drawn
color (Color): The color of the rectangle
thickness (int): The thickness of the rectangle border
Returns:
np.ndarray: The scene with the rectangle drawn on it
"""
cv2.rectangle(
scene,
@@ -52,12 +60,15 @@ def draw_rectangle(
def draw_filled_rectangle(scene: np.ndarray, rect: Rect, color: Color) -> np.ndarray:
"""
Draws a filled rectangle on the given scene.
Draws a filled rectangle on an image.
:param scene: np.ndarray : The scene on which to draw the rectangle.
:param rect: Rect : The rectangle to be drawn.
:param color: Color : The color of the rectangle.
:return: np.ndarray : The updated scene with the filled rectangle drawn on it.
Parameters:
scene (np.ndarray): The scene on which the rectangle will be drawn
rect (Rect): The rectangle to be drawn
color (Color): The color of the rectangle
Returns:
np.ndarray: The scene with the rectangle drawn on it
"""
cv2.rectangle(
scene,
@@ -67,3 +78,89 @@ def draw_filled_rectangle(scene: np.ndarray, rect: Rect, color: Color) -> np.nda
-1,
)
return scene
def draw_polygon(
scene: np.ndarray, polygon: np.ndarray, color: Color, thickness: int = 2
) -> np.ndarray:
"""Draw a polygon on a scene.
Parameters:
scene (np.ndarray): The scene to draw the polygon on.
polygon (np.ndarray): The polygon to be drawn, given as a list of vertices.
color (Color): The color of the polygon.
thickness (int, optional): The thickness of the polygon lines, by default 2.
Returns:
np.ndarray: The scene with the polygon drawn on it.
"""
cv2.polylines(
scene, [polygon], isClosed=True, color=color.as_bgr(), thickness=thickness
)
return scene
def draw_text(
scene: np.ndarray,
text: str,
text_anchor: Point,
text_color: Color = Color.black(),
text_scale: float = 0.5,
text_thickness: int = 1,
text_padding: int = 10,
text_font: int = cv2.FONT_HERSHEY_SIMPLEX,
background_color: Optional[Color] = None,
) -> np.ndarray:
"""
Draw text with background on a scene.
Parameters:
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_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.
Returns:
np.ndarray: The input scene with the text drawn on it.
Examples:
```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)
```
"""
text_width, text_height = cv2.getTextSize(
text=text,
fontFace=text_font,
fontScale=text_scale,
thickness=text_thickness,
)[0]
text_rect = Rect(
x=text_anchor.x - text_width // 2,
y=text_anchor.y - text_height // 2,
width=text_width,
height=text_height,
).pad(text_padding)
if background_color is not None:
scene = draw_filled_rectangle(
scene=scene, rect=text_rect, color=background_color
)
cv2.putText(
img=scene,
text=text,
org=(text_anchor.x - text_width // 2, text_anchor.y + text_height // 2),
fontFace=text_font,
fontScale=text_scale,
color=text_color.as_bgr(),
thickness=text_thickness,
lineType=cv2.LINE_AA,
)
return scene
+22
View File
@@ -0,0 +1,22 @@
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
@@ -1,9 +1,19 @@
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Tuple
class Position(Enum):
CENTER = "CENTER"
BOTTOM_CENTER = "BOTTOM_CENTER"
@classmethod
def list(cls):
return list(map(lambda c: c.value, cls))
@dataclass
class Point:
x: float
+28
View File
@@ -0,0 +1,28 @@
import numpy as np
from supervision.geometry.core import Point
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.
Parameters:
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.
Examples:
```python
>>> from supervision.geometry.utils import get_polygon_center
>>> vertices = np.array([[0, 0], [0, 1], [1, 1], [1, 0]])
>>> get_center(vertices)
Point(x=0.5, y=0.5)
```
"""
center = np.mean(polygon, axis=0).astype(int)
return Point(x=center[0], y=center[1])
+86 -13
View File
@@ -1,24 +1,97 @@
from typing import Tuple
from typing import List, Optional, Tuple
import cv2
import matplotlib.pyplot as plt
import numpy as np
def show_frame_in_notebook(
frame: np.ndarray, size: Tuple[int, int] = (10, 10), cmap: str = "gray"
):
def plot_image(
image: np.ndarray, size: Tuple[int, int] = (12, 12), cmap: Optional[str] = "gray"
) -> None:
"""
Display a frame in Jupyter Notebook using Matplotlib
Plots image using matplotlib.
:param frame: np.ndarray : The frame to be displayed.
:param size: Tuple[int, int] : The size of the plot. default:(10,10)
:param cmap: str : the colormap to use for single channel images. default:gray
Args:
image (np.ndarray): The frame to be displayed.
size (Tuple[int, int]): The size of the plot.
cmap (str): the colormap to use for single channel images.
Examples:
```python
>>> import cv2
>>> import supervision as sv
>>> image = cv2.imread("path/to/image.jpg")
%matplotlib inline
>>> sv.plot_image(image, (16, 16))
```
"""
if frame.ndim == 2:
plt.figure(figsize=size)
plt.imshow(frame, cmap=cmap)
plt.figure(figsize=size)
if image.ndim == 2:
plt.imshow(image, cmap=cmap)
else:
plt.figure(figsize=size)
plt.imshow(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
plt.axis("off")
plt.show()
def plot_images_grid(
images: List[np.ndarray],
grid_size: Tuple[int, int],
titles: Optional[List[str]] = None,
size: Tuple[int, int] = (12, 12),
cmap: Optional[str] = "gray",
) -> None:
"""
Plots images in a grid using matplotlib.
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.
cmap (str): the colormap to use for single channel images.
Raises:
ValueError: If the number of images exceeds the grid size.
Examples:
```python
>>> import cv2
>>> import supervision as sv
>>> image1 = cv2.imread("path/to/image1.jpg")
>>> image2 = cv2.imread("path/to/image2.jpg")
>>> image3 = cv2.imread("path/to/image3.jpg")
>>> images = [image1, image2, image3]
>>> titles = ["Image 1", "Image 2", "Image 3"]
%matplotlib inline
>>> plot_images_grid(images, grid_size=(2, 2), titles=titles, figsize=(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."
)
fig, axes = plt.subplots(nrows=nrows, ncols=ncols, figsize=size)
for idx, ax in enumerate(axes.flat):
if idx < len(images):
if images[idx].ndim == 2:
ax.imshow(images[idx], cmap=cmap)
else:
ax.imshow(cv2.cvtColor(images[idx], cv2.COLOR_BGR2RGB))
if titles is not None and idx < len(titles):
ax.set_title(titles[idx])
ax.axis("off")
plt.show()
-205
View File
@@ -1,205 +0,0 @@
from typing import List, Optional, Union
import cv2
import numpy as np
from supervision.draw.color import Color, ColorPalette
class Detections:
def __init__(
self,
xyxy: np.ndarray,
confidence: np.ndarray,
class_id: np.ndarray,
tracker_id: Optional[np.ndarray] = None,
):
"""
Data class containing information about the detections in a video frame.
:param xyxy: np.ndarray : An array of shape (n, 4) containing the bounding boxes coordinates in format [x1, y1, x2, y2]
:param confidence: np.ndarray : An array of shape (n,) containing the confidence scores of the detections.
:param class_id: np.ndarray : An array of shape (n,) containing the class ids of the detections.
:param tracker_id: Optional[np.ndarray] : An array of shape (n,) containing the tracker ids of the detections.
"""
self.xyxy: np.ndarray = xyxy
self.confidence: np.ndarray = confidence
self.class_id: np.ndarray = class_id
self.tracker_id: Optional[np.ndarray] = tracker_id
n = len(self.xyxy)
validators = [
(isinstance(self.xyxy, np.ndarray) and self.xyxy.shape == (n, 4)),
(isinstance(self.confidence, np.ndarray) and self.confidence.shape == (n,)),
(isinstance(self.class_id, np.ndarray) and self.class_id.shape == (n,)),
self.tracker_id is None
or (
isinstance(self.tracker_id, np.ndarray)
and self.tracker_id.shape == (n,)
),
]
if not all(validators):
raise ValueError(
"xyxy must be 2d np.ndarray with (n, 4) shape, "
"confidence must be 1d np.ndarray with (n,) shape, "
"class_id must be 1d np.ndarray with (n,) shape, "
"tracker_id must be None or 1d np.ndarray with (n,) shape"
)
def __len__(self):
"""
Returns the number of detections in the Detections object.
"""
return len(self.xyxy)
def __iter__(self):
"""
Iterates over the Detections object and yield a tuple of (xyxy, confidence, class_id, tracker_id) for each detection.
"""
for i in range(len(self.xyxy)):
yield (
self.xyxy[i],
self.confidence[i],
self.class_id[i],
self.tracker_id[i] if self.tracker_id is not None else None,
)
@classmethod
def from_yolov5(cls, yolov5_output: np.ndarray):
"""
Creates a Detections instance from a YOLOv5 output tensor
:param yolov5_output: np.ndarray : The output tensor from YOLOv5
:return: Detections : A Detections instance representing the detections in the frame
Example:
detections = Detections.from_yolov5(yolov5_output)
"""
xyxy = yolov5_output[:, :4]
confidence = yolov5_output[:, 4]
class_id = yolov5_output[:, 5].astype(int)
return cls(xyxy, confidence, class_id)
def filter(self, mask: np.ndarray, inplace: bool = False) -> Optional[np.ndarray]:
"""
Filter the detections by applying a mask
:param mask: np.ndarray : A mask of shape (n,) containing a boolean value for each detection indicating if it should be included in the filtered detections
:param inplace: bool : If True, the original data will be modified and self will be returned.
:return: Optional[np.ndarray] : A new instance of Detections with the filtered detections, if inplace is set to False. None otherwise.
"""
if inplace:
self.xyxy = self.xyxy[mask]
self.confidence = self.confidence[mask]
self.class_id = self.class_id[mask]
self.tracker_id = (
self.tracker_id[mask] if self.tracker_id is not None else None
)
return self
else:
return Detections(
xyxy=self.xyxy[mask],
confidence=self.confidence[mask],
class_id=self.class_id[mask],
tracker_id=self.tracker_id[mask]
if self.tracker_id is not None
else None,
)
class BoxAnnotator:
def __init__(
self,
color: Union[Color, ColorPalette],
thickness: int = 2,
text_color: Color = Color.black(),
text_scale: float = 0.5,
text_thickness: int = 1,
text_padding: int = 10,
):
"""
A class for drawing bounding boxes on an image using detections provided.
:param color: Union[Color, ColorPalette] : The color to draw the bounding box, can be a single color or a color palette
:param thickness: int : The thickness of the bounding box lines, default is 2
:param text_color: Color : The color of the text on the bounding box, default is white
:param text_scale: float : The scale of the text on the bounding box, default is 0.5
:param text_thickness: int : The thickness of the text on the bounding box, default is 1
:param text_padding: int : The padding around the text on the bounding box, default is 5
"""
self.color: Union[Color, ColorPalette] = color
self.thickness: int = thickness
self.text_color: Color = text_color
self.text_scale: float = text_scale
self.text_thickness: int = text_thickness
self.text_padding: int = text_padding
def annotate(
self,
frame: np.ndarray,
detections: Detections,
labels: Optional[List[str]] = None,
) -> np.ndarray:
"""
Draws bounding boxes on the frame using the detections provided.
:param frame: np.ndarray : The image on which the bounding boxes will be drawn
:param detections: Detections : The detections for which the bounding boxes will be drawn
:param 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.
:return: np.ndarray : The image with the bounding boxes drawn on it
"""
font = cv2.FONT_HERSHEY_SIMPLEX
for i, (xyxy, confidence, class_id, tracker_id) in enumerate(detections):
color = (
self.color.by_idx(class_id)
if isinstance(self.color, ColorPalette)
else self.color
)
text = (
f"{confidence:0.2f}"
if (labels is None or len(detections) != len(labels))
else labels[i]
)
x1, y1, x2, y2 = xyxy.astype(int)
text_width, text_height = cv2.getTextSize(
text=text,
fontFace=font,
fontScale=self.text_scale,
thickness=self.text_thickness,
)[0]
text_x = x1 + self.text_padding
text_y = y1 - self.text_padding
text_background_x1 = x1
text_background_y1 = y1 - 2 * self.text_padding - text_height
text_background_x2 = x1 + 2 * self.text_padding + text_width
text_background_y2 = y1
cv2.rectangle(
img=frame,
pt1=(x1, y1),
pt2=(x2, y2),
color=color.as_bgr(),
thickness=self.thickness,
)
cv2.rectangle(
img=frame,
pt1=(text_background_x1, text_background_y1),
pt2=(text_background_x2, text_background_y2),
color=color.as_bgr(),
thickness=cv2.FILLED,
)
cv2.putText(
img=frame,
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 frame
+161
View File
@@ -0,0 +1,161 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable, Generator, Optional, Tuple
import cv2
import numpy as np
@dataclass
class VideoInfo:
"""
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
Examples:
```python
>>> from supervision import VideoInfo
>>> video_info = VideoInfo.from_video_path(video_path='video.mp4')
>>> video_info
VideoInfo(width=3840, height=2160, fps=25, total_frames=538)
>>> video_info.resolution_wh
(3840, 2160)
```
"""
width: int
height: int
fps: int
total_frames: Optional[int] = None
@classmethod
def from_video_path(cls, video_path: str) -> VideoInfo:
video = cv2.VideoCapture(video_path)
if not video.isOpened():
raise Exception(f"Could not open video at {video_path}")
width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = int(video.get(cv2.CAP_PROP_FPS))
total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
video.release()
return VideoInfo(width, height, fps, total_frames)
@property
def resolution_wh(self) -> Tuple[int, int]:
return self.width, self.height
class VideoSink:
"""
Context manager that saves video frames to a file using OpenCV.
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.
Examples:
```python
>>> from supervision import VideoInfo, VideoSink
>>> video_info = 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)
```
"""
def __init__(self, target_path: str, video_info: VideoInfo):
self.target_path = target_path
self.video_info = video_info
self.__fourcc = cv2.VideoWriter_fourcc(*"mp4v")
self.__writer = None
def __enter__(self):
self.__writer = cv2.VideoWriter(
self.target_path,
self.__fourcc,
self.video_info.fps,
self.video_info.resolution_wh,
)
return self
def write_frame(self, frame: np.ndarray):
self.__writer.write(frame)
def __exit__(self, exc_type, exc_val, exc_tb):
self.__writer.release()
def get_video_frames_generator(source_path: str) -> 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.
Returns:
(Generator[np.ndarray, None, None]): A generator that yields the frames of the video.
Examples:
```python
>>> from supervision import get_video_frames_generator
>>> for frame in 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
success, frame = video.read()
video.release()
def process_video(
source_path: str,
target_path: str,
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.
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.
Examples:
```python
>>> from supervision import process_video
>>> def process_frame(scene: np.ndarray) -> np.ndarray:
... ...
>>> process_video(
... source_path='source_video.mp4',
... target_path='target_video.mp4',
... callback=process_frame
... )
```
"""
source_video_info = VideoInfo.from_video_path(video_path=source_path)
with VideoSink(target_path=target_path, video_info=source_video_info) as sink:
for index, frame in enumerate(
get_video_frames_generator(source_path=source_path)
):
result_frame = callback(frame, index)
sink.write_frame(frame=result_frame)
-46
View File
@@ -1,46 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional, Tuple
import cv2
@dataclass
class VideoInfo:
"""
Data class containing information about the video resolution, fps, and total frame count.
:param width: int : The width of the video frames in pixels.
:param height: int : The height of the video frames in pixels.
:param fps: int : The frames per second of the video.
:param total_frames: int : The total number of frames in the video.
"""
width: int
height: int
fps: int
total_frames: Optional[int] = None
@classmethod
def from_video_path(cls, video_path: str) -> VideoInfo:
"""
Returns a VideoInfo data class containing information about the video resolution, fps, and total frame count.
:param video_path: str : The path of the video file.
:return: VideoInfo : A data class containing information about the video resolution, fps, and total frame count.
"""
video = cv2.VideoCapture(video_path)
if not video.isOpened():
raise Exception(f"Could not open video at {video_path}")
width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = int(video.get(cv2.CAP_PROP_FPS))
total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
video.release()
return VideoInfo(width, height, fps, total_frames)
@property
def resolution(self) -> Tuple[int, int]:
return self.width, self.height
-48
View File
@@ -1,48 +0,0 @@
import cv2
import numpy as np
from supervision.video.dataclasses import VideoInfo
class VideoSink:
"""
A context manager that uses OpenCV to save video frames to a file.
:param output_path: str : The path to the output file where the video will be saved.
:param video_info: VideoInfo : An instance of VideoInfo containing information about the video resolution, fps, and total frame count.
"""
def __init__(self, output_path: str, video_info: VideoInfo):
"""
Initializes the VideoSink with the specified output path and video information.
"""
self.output_path = output_path
self.video_info = video_info
self.fourcc = cv2.VideoWriter_fourcc(*"mp4v")
self.writer = None
def __enter__(self):
"""
Opens the output file and returns the VideoSink instance.
"""
self.writer = cv2.VideoWriter(
self.output_path,
self.fourcc,
self.video_info.fps,
self.video_info.resolution,
)
return self
def write_frame(self, frame: np.ndarray):
"""
Writes a frame to the output video file.
:param frame: np.ndarray : The frame to be written.
"""
self.writer.write(frame)
def __exit__(self, exc_type, exc_val, exc_tb):
"""
Closes the output file.
"""
self.writer.release()
-20
View File
@@ -1,20 +0,0 @@
from typing import Generator
import cv2
def get_video_frames_generator(video_path: str) -> Generator[int, None, None]:
"""
Returns a generator that yields the frames of the video.
:param video_path: str : The path of the video file.
:return: Generator[int, None, None] : Generator that yields the frames of the video.
"""
video = cv2.VideoCapture(video_path)
if not video.isOpened():
raise Exception(f"Could not open video at {video_path}")
success, frame = video.read()
while success:
yield frame
success, frame = video.read()
video.release()
View File
+105
View File
@@ -0,0 +1,105 @@
from contextlib import ExitStack as DoesNotRaise
import pytest
from supervision import Detections
from typing import Optional, Union
import numpy as np
PREDICTIONS = np.array([
[ 2254, 906, 2447, 1353, 0.90538, 0],
[ 2049, 1133, 2226, 1371, 0.59002, 56],
[ 727, 1224, 838, 1601, 0.51119, 39],
[ 808, 1214, 910, 1564, 0.45287, 39],
[ 6, 52, 1131, 2133, 0.45057, 72],
[ 299, 1225, 512, 1663, 0.45029, 39],
[ 529, 874, 645, 945, 0.31101, 39],
[ 8, 47, 1935, 2135, 0.28192, 72],
[ 2265, 813, 2328, 901, 0.2714, 62]
], dtype=np.float32)
DETECTIONS = Detections(
xyxy=PREDICTIONS[:, :4],
confidence=PREDICTIONS[:, 4],
class_id=PREDICTIONS[:, 5].astype(int)
)
PREDICTIONS = np.array([
[ 2254, 906, 2447, 1353, 0.90538, 0],
[ 2049, 1133, 2226, 1371, 0.59002, 56],
[ 727, 1224, 838, 1601, 0.51119, 39],
], dtype=np.float32)
@pytest.mark.parametrize(
'detections, index, expected_result, exception',
[
(
DETECTIONS,
DETECTIONS.class_id == 0,
Detections(
xyxy=np.array([
[ 2254, 906, 2447, 1353]
], dtype=np.float32),
confidence=np.array([
0.90538
], dtype=np.float32),
class_id=np.array([
0
], dtype=int)
),
DoesNotRaise()
), # take only detections with class_id = 0
(
DETECTIONS,
DETECTIONS.confidence > 0.5,
Detections(
xyxy=np.array([
[ 2254, 906, 2447, 1353],
[ 2049, 1133, 2226, 1371],
[ 727, 1224, 838, 1601]
], dtype=np.float32),
confidence=np.array([
0.90538,
0.59002,
0.51119
], dtype=np.float32),
class_id=np.array([
0,
56,
39
], dtype=int)
),
DoesNotRaise()
), # take only detections with confidence > 0.5
(
DETECTIONS,
np.array([True, True, True, True, True, True, True, True, True], dtype=bool),
DETECTIONS,
DoesNotRaise()
), # take all detections
(
DETECTIONS,
np.array([False, False, False, False, False, False, False, False, False], dtype=bool),
Detections(
xyxy=np.empty((0, 4), dtype=np.float32),
confidence=np.array([], dtype=np.float32),
class_id=np.array([], dtype=int)
),
DoesNotRaise()
), # take no detections
]
)
def test_getitem(
detections: Detections,
index: Union[int, slice, np.ndarray],
expected_result: Optional[Detections],
exception: Exception
) -> None:
with exception:
result = detections[index]
assert result == expected_result
+278
View File
@@ -0,0 +1,278 @@
from contextlib import ExitStack as DoesNotRaise
from typing import Optional, Tuple, List
import pytest
import numpy as np
from supervision.detection.utils import non_max_suppression, clip_boxes, filter_polygons_by_area
@pytest.mark.parametrize(
"predictions, iou_threshold, expected_result, exception",
[
(
np.empty(shape=(0, 5)),
0.5,
np.array([]),
DoesNotRaise()
), # single box with no category
(
np.array([
[10.0, 10.0, 40.0, 40.0, 0.8]
]),
0.5,
np.array([
True
]),
DoesNotRaise()
), # single box with no category
(
np.array([
[10.0, 10.0, 40.0, 40.0, 0.8, 0]
]),
0.5,
np.array([
True
]),
DoesNotRaise()
), # single box with category
(
np.array([
[10.0, 10.0, 40.0, 40.0, 0.8],
[15.0, 15.0, 40.0, 40.0, 0.9],
]),
0.5,
np.array([
False,
True
]),
DoesNotRaise()
), # two boxes with no category
(
np.array([
[10.0, 10.0, 40.0, 40.0, 0.8, 0],
[15.0, 15.0, 40.0, 40.0, 0.9, 1],
]),
0.5,
np.array([
True,
True
]),
DoesNotRaise()
), # two boxes with different category
(
np.array([
[10.0, 10.0, 40.0, 40.0, 0.8, 0],
[15.0, 15.0, 40.0, 40.0, 0.9, 0],
]),
0.5,
np.array([
False,
True
]),
DoesNotRaise()
), # two boxes with same category
(
np.array([
[0.0, 0.0, 30.0, 40.0, 0.8],
[5.0, 5.0, 35.0, 45.0, 0.9],
[10.0, 10.0, 40.0, 50.0, 0.85],
]),
0.5,
np.array([
False,
True,
False
]),
DoesNotRaise()
), # three boxes with no category
(
np.array([
[0.0, 0.0, 30.0, 40.0, 0.8, 0],
[5.0, 5.0, 35.0, 45.0, 0.9, 1],
[10.0, 10.0, 40.0, 50.0, 0.85, 2],
]),
0.5,
np.array([
True,
True,
True
]),
DoesNotRaise()
), # three boxes with same category
(
np.array([
[0.0, 0.0, 30.0, 40.0, 0.8, 0],
[5.0, 5.0, 35.0, 45.0, 0.9, 0],
[10.0, 10.0, 40.0, 50.0, 0.85, 1],
]),
0.5,
np.array([
False,
True,
True
]),
DoesNotRaise()
), # three boxes with different category
]
)
def test_non_max_suppression(
predictions: np.ndarray,
iou_threshold: float,
expected_result: Optional[np.ndarray],
exception: Exception
) -> None:
with exception:
result = non_max_suppression(predictions=predictions, iou_threshold=iou_threshold)
assert np.array_equal(result, expected_result)
@pytest.mark.parametrize(
"boxes_xyxy, frame_resolution_wh, expected_result",
[
(
np.empty(shape=(0, 4)),
(1280, 720),
np.empty(shape=(0, 4)),
),
(
np.array([
[1.0, 1.0, 1279.0, 719.0]
]),
(1280, 720),
np.array([
[1.0, 1.0, 1279.0, 719.0]
]),
),
(
np.array([
[-1.0, 1.0, 1279.0, 719.0]
]),
(1280, 720),
np.array([
[0.0, 1.0, 1279.0, 719.0]
]),
),
(
np.array([
[1.0, -1.0, 1279.0, 719.0]
]),
(1280, 720),
np.array([
[1.0, 0.0, 1279.0, 719.0]
]),
),
(
np.array([
[1.0, 1.0, 1281.0, 719.0]
]),
(1280, 720),
np.array([
[1.0, 1.0, 1280.0, 719.0]
]),
),
(
np.array([
[1.0, 1.0, 1279.0, 721.0]
]),
(1280, 720),
np.array([
[1.0, 1.0, 1279.0, 720.0]
]),
),
]
)
def test_clip_boxes(boxes_xyxy: np.ndarray, frame_resolution_wh: Tuple[int, int], expected_result: np.ndarray) -> None:
result = clip_boxes(boxes_xyxy=boxes_xyxy, frame_resolution_wh=frame_resolution_wh)
assert np.array_equal(result, expected_result)
@pytest.mark.parametrize(
"polygons, min_area, max_area, expected_result, exception",
[
(
[np.array([[0, 0], [0, 10], [10, 10], [10, 0]])],
None,
None,
[np.array([[0, 0], [0, 10], [10, 10], [10, 0]])],
DoesNotRaise()
), # single polygon without area constraints
(
[np.array([[0, 0], [0, 10], [10, 10], [10, 0]])],
50,
None,
[np.array([[0, 0], [0, 10], [10, 10], [10, 0]])],
DoesNotRaise()
), # single polygon with min_area constraint
(
[np.array([[0, 0], [0, 10], [10, 10], [10, 0]])],
None,
50,
[],
DoesNotRaise()
), # single polygon with max_area constraint
(
[
np.array([[0, 0], [0, 10], [10, 10], [10, 0]]),
np.array([[0, 0], [0, 20], [20, 20], [20, 0]])
],
200,
None,
[np.array([[0, 0], [0, 20], [20, 20], [20, 0]])],
DoesNotRaise()
), # two polygons with min_area constraint
(
[
np.array([[0, 0], [0, 10], [10, 10], [10, 0]]),
np.array([[0, 0], [0, 20], [20, 20], [20, 0]])
],
None,
200,
[np.array([[0, 0], [0, 10], [10, 10], [10, 0]])],
DoesNotRaise()
), # two polygons with max_area constraint
(
[
np.array([[0, 0], [0, 10], [10, 10], [10, 0]]),
np.array([[0, 0], [0, 20], [20, 20], [20, 0]])
],
200,
200,
[],
DoesNotRaise()
), # two polygons with both area constraints
(
[
np.array([[0, 0], [0, 10], [10, 10], [10, 0]]),
np.array([[0, 0], [0, 20], [20, 20], [20, 0]])
],
100,
100,
[np.array([[0, 0], [0, 10], [10, 10], [10, 0]])],
DoesNotRaise()
), # two polygons with min_area and max_area equal to the area of the first polygon
(
[
np.array([[0, 0], [0, 10], [10, 10], [10, 0]]),
np.array([[0, 0], [0, 20], [20, 20], [20, 0]])
],
400,
400,
[np.array([[0, 0], [0, 20], [20, 20], [20, 0]])],
DoesNotRaise()
), # two polygons with min_area and max_area equal to the area of the second polygon
]
)
def test_filter_polygons_by_area(
polygons: List[np.ndarray],
min_area: Optional[float],
max_area: Optional[float],
expected_result: List[np.ndarray],
exception: Exception
) -> None:
with exception:
result = filter_polygons_by_area(polygons=polygons, min_area=min_area, max_area=max_area)
assert len(result) == len(expected_result)
for result_polygon, expected_result_polygon in zip(result, expected_result):
assert np.array_equal(result_polygon, expected_result_polygon)
+16 -18
View File
@@ -7,27 +7,25 @@ from supervision.draw.color import Color
@pytest.mark.parametrize(
'color_hex, expected_result, exception',
"color_hex, expected_result, exception",
[
('fff', Color.white(), DoesNotRaise()),
('#fff', Color.white(), DoesNotRaise()),
('ffffff', Color.white(), DoesNotRaise()),
('#ffffff', Color.white(), DoesNotRaise()),
('f00', Color.red(), DoesNotRaise()),
('0f0', Color.green(), DoesNotRaise()),
('00f', Color.blue(), DoesNotRaise()),
('#808000', Color(r=128, g=128, b=0), DoesNotRaise()),
('', None, pytest.raises(ValueError)),
('00', None, pytest.raises(ValueError)),
('0000', None, pytest.raises(ValueError)),
('0000000', None, pytest.raises(ValueError)),
('ffg', None, pytest.raises(ValueError)),
]
("fff", Color.white(), DoesNotRaise()),
("#fff", Color.white(), DoesNotRaise()),
("ffffff", Color.white(), DoesNotRaise()),
("#ffffff", Color.white(), DoesNotRaise()),
("f00", Color.red(), DoesNotRaise()),
("0f0", Color.green(), DoesNotRaise()),
("00f", Color.blue(), DoesNotRaise()),
("#808000", Color(r=128, g=128, b=0), DoesNotRaise()),
("", None, pytest.raises(ValueError)),
("00", None, pytest.raises(ValueError)),
("0000", None, pytest.raises(ValueError)),
("0000000", None, pytest.raises(ValueError)),
("ffg", None, pytest.raises(ValueError)),
],
)
def test_color_from_hex(
color_hex,
expected_result: Optional[Color],
exception: Exception
color_hex, expected_result: Optional[Color], exception: Exception
) -> None:
with exception:
result = Color.from_hex(color_hex=color_hex)
+4 -13
View File
@@ -1,40 +1,31 @@
import pytest
from supervision.geometry.dataclasses import Vector, Point
from supervision.geometry.core import Vector, Point
@pytest.mark.parametrize(
'vector, point, expected_result',
"vector, point, expected_result",
[
(Vector(start=Point(x=0, y=0), end=Point(x=5, y=5)), Point(x=-1, y=1), False),
(Vector(start=Point(x=0, y=0), end=Point(x=5, y=5)), Point(x=6, y=6), False),
(Vector(start=Point(x=0, y=0), end=Point(x=5, y=5)), Point(x=3, y=6), False),
(Vector(start=Point(x=5, y=5), end=Point(x=0, y=0)), Point(x=-1, y=1), True),
(Vector(start=Point(x=5, y=5), end=Point(x=0, y=0)), Point(x=6, y=6), False),
(Vector(start=Point(x=5, y=5), end=Point(x=0, y=0)), Point(x=3, y=6), True),
(Vector(start=Point(x=0, y=0), end=Point(x=1, y=0)), Point(x=0, y=0), False),
(Vector(start=Point(x=0, y=0), end=Point(x=1, y=0)), Point(x=0, y=-1), True),
(Vector(start=Point(x=0, y=0), end=Point(x=1, y=0)), Point(x=0, y=1), False),
(Vector(start=Point(x=1, y=0), end=Point(x=0, y=0)), Point(x=0, y=0), False),
(Vector(start=Point(x=1, y=0), end=Point(x=0, y=0)), Point(x=0, y=-1), False),
(Vector(start=Point(x=1, y=0), end=Point(x=0, y=0)), Point(x=0, y=1), True),
(Vector(start=Point(x=1, y=1), end=Point(x=1, y=3)), Point(x=0, y=0), False),
(Vector(start=Point(x=1, y=1), end=Point(x=1, y=3)), Point(x=1, y=4), False),
(Vector(start=Point(x=1, y=1), end=Point(x=1, y=3)), Point(x=2, y=4), True),
(Vector(start=Point(x=1, y=3), end=Point(x=1, y=1)), Point(x=0, y=0), True),
(Vector(start=Point(x=1, y=3), end=Point(x=1, y=1)), Point(x=1, y=4), False),
(Vector(start=Point(x=1, y=3), end=Point(x=1, y=1)), Point(x=2, y=4), False),
]
],
)
def test_vector_is_in(
vector: Vector,
point: Point,
expected_result: bool
) -> None:
def test_vector_is_in(vector: Vector, point: Point, expected_result: bool) -> None:
result = vector.is_in(point=point)
assert result == expected_result