Compare commits

...
57 Commits
Author SHA1 Message Date
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
41 changed files with 1292 additions and 405 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
+38
View File
@@ -0,0 +1,38 @@
# 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.
+36 -9
View File
@@ -16,34 +16,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"
@@ -63,9 +63,19 @@ 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,7 +86,24 @@ source venv/bin/activate
pip install -e ".[dev]"
```
## 🧹 Code Quality
</details>
## 📖 documentation
Curious how Supervision can help you solve problems on your project? Visit our [documentation](https://roboflow.github.io/supervision) page!
## 🎬 videos
Learn how to use YOLOv8, ByteTrack and **Supervision** to detect, track and count objects. 🔥
[Subscribe](https://www.youtube.com/@Roboflow), and stay up to date with our latest YouTube videos!
<p align="center">
<a href="https://youtu.be/OS5qI9YBkfk">
<img src="https://user-images.githubusercontent.com/26109316/213702005-ddd568f0-b902-46c2-9af9-e6ff33db23bf.jpg" alt="latest-roboflow-tutorial">
</a>
</p>
## 🧹 code quality
We provide two handy commands inside the `Makefile`, namely:
@@ -85,6 +112,6 @@ We provide two handy commands inside the `Makefile`, namely:
So far, **there is no types checking with mypy**. See [issue](https://github.com/roboflow-ai/template-python/issues/4).
## 🧪 Tests
## 🧪 tests
[`pytests`](https://docs.pytest.org/en/7.1.x/) is used to run our tests.
+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

+3
View File
@@ -0,0 +1,3 @@
## Detections
:::supervision.detection.core.Detections
+3
View File
@@ -0,0 +1,3 @@
## generate_2d_mask
:::supervision.detection.utils.generate_2d_mask
+7
View File
@@ -0,0 +1,7 @@
## draw_line
:::supervision.draw.utils.draw_line
## draw_rectangle
:::supervision.draw.utils.draw_rectangle
+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
+3
View File
@@ -0,0 +1,3 @@
## show_frame_in_notebook
:::supervision.notebook.utils.show_frame_in_notebook
+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
+53
View File
@@ -0,0 +1,53 @@
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:
- Video: video.md
- Detection:
- Core: detection_core.md
- Utils: detection_utils.md
- Draw: draw.md
- Geometry: geometry.md
- Notebook: notebook.md
theme:
name: 'material'
logo: https://raw.githubusercontent.com/roboflow/supervision/main/docs/assets/roboflow_logomark_white.svg
favicon: https://raw.githubusercontent.com/roboflow/supervision/main/docs/assets/roboflow_logomark_color.svg
palette:
primary: 'deep purple'
accent: 'teal'
font:
text: Roboto
code: Roboto Mono
plugins:
- mkdocstrings
- search
markdown_extensions:
- admonition
- pymdownx.details
- pymdownx.superfences
+30
View File
@@ -0,0 +1,30 @@
certifi==2022.12.7
charset-normalizer==3.0.1
click==8.1.3
colorama==0.4.6
ghp-import==2.1.0
griffe==0.25.4
idna==3.4
importlib-metadata==6.0.0
Jinja2==3.1.2
Markdown==3.3.7
MarkupSafe==2.1.2
mergedeep==1.3.4
mkdocs==1.4.2
mkdocs-autorefs==0.4.1
mkdocs-material==9.0.9
mkdocs-material-extensions==1.1.1
mkdocstrings==0.20.0
mkdocstrings-python==0.8.3
packaging==23.0
Pygments==2.14.0
pymdown-extensions==9.9.2
python-dateutil==2.8.2
PyYAML==6.0
pyyaml_env_tag==0.1
regex==2022.10.31
requests==2.28.2
six==1.16.0
urllib3==1.26.14
watchdog==2.2.1
zipp==3.12.0
+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',
'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=[
+16 -1
View File
@@ -1 +1,16 @@
__version__ = "0.0.2"
__version__ = "0.2.0"
from supervision.detection.core import BoxAnnotator, Detections
from supervision.detection.polygon_zone import PolygonZone, PolygonZoneAnnotator
from supervision.detection.utils import generate_2d_mask
from supervision.draw.color import Color, ColorPalette
from supervision.draw.utils import draw_filled_rectangle, draw_polygon, draw_text
from supervision.geometry.core import Point, Position, Rect
from supervision.geometry.utils import get_polygon_center
from supervision.notebook.utils import show_frame_in_notebook
from supervision.video import (
VideoInfo,
VideoSink,
get_video_frames_generator,
process_video,
)
+293
View File
@@ -0,0 +1,293 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import List, Optional, Union
import cv2
import numpy as np
from supervision.draw.color import Color, ColorPalette
from supervision.geometry.core import Position
@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]`
confidence (np.ndarray): An array of shape `(n,)` containing the confidence scores of the detections.
class_id (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
confidence: np.ndarray
class_id: np.ndarray
tracker_id: Optional[np.ndarray] = None
def __post_init__(self):
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_detections):
"""
Creates a Detections instance from a YOLOv5 output Detections
Attributes:
yolov5_detections (yolov5.models.common.Detections): The output Detections instance from YOLOv5
Returns:
Example:
```python
>>> import torch
>>> from supervision import Detections
>>> model = torch.hub.load('ultralytics/yolov5', 'yolov5s')
>>> results = model(frame)
>>> detections = Detections.from_yolov5(results)
```
"""
yolov5_detections_predictions = yolov5_detections.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):
"""
Creates a Detections instance from a YOLOv8 output Results
Attributes:
yolov8_results (ultralytics.yolo.engine.results.Results): The output Results instance from YOLOv8
Returns:
Example:
```python
>>> from ultralytics import YOLO
>>> from supervision import Detections
>>> model = YOLO('yolov8s.pt')
>>> results = model(frame)
>>> detections = Detections.from_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),
)
def filter(self, mask: np.ndarray, inplace: bool = False) -> Optional[Detections]:
"""
Filter the detections by applying a mask.
Attributes:
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
inplace (bool): If True, the original data will be modified and self will be returned.
Returns:
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,
)
def get_anchor_coordinates(self, anchor: Position) -> np.ndarray:
"""
Returns the bounding box coordinates for a specific anchor.
Properties:
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 == np.bool:
return Detections(
xyxy=self.xyxy[index],
confidence=self.confidence[index],
class_id=self.class_id[index],
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)}."
)
class BoxAnnotator:
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,
):
"""
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
"""
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, (xyxy, confidence, class_id, tracker_id) in enumerate(detections):
x1, y1, x2, y2 = xyxy.astype(int)
color = (
self.color.by_idx(class_id)
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"{confidence:0.2f}"
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
@@ -3,29 +3,37 @@ 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:
# handle detections with no tracker_id
@@ -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,
+80
View File
@@ -0,0 +1,80 @@
from typing import Optional, Tuple
import cv2
import numpy as np
from supervision import Detections
from supervision.detection.utils import generate_2d_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:
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.mask = generate_2d_mask(polygon=polygon, resolution_wh=frame_resolution_wh)
self.current_count = 0
def trigger(self, detections: Detections) -> np.ndarray:
anchors = (
np.ceil(
detections.get_anchor_coordinates(anchor=self.triggering_position)
).astype(int)
- 1
)
is_in_zone = self.mask[anchors[:, 1], anchors[:, 0]]
self.current_count = np.sum(is_in_zone)
return is_in_zone.astype(bool)
class PolygonZoneAnnotator:
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:
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
+20
View File
@@ -0,0 +1,20 @@
from typing import Tuple
import cv2
import numpy as np
def generate_2d_mask(polygon: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray:
"""Generate a 2D mask from a polygon.
Properties:
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
+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]):
+116 -12
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.
Attributes:
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,
@@ -58,6 +66,14 @@ def draw_filled_rectangle(scene: np.ndarray, rect: Rect, color: Color) -> np.nda
: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.
Attributes:
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 +83,91 @@ 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.
Attributes:
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 on a scene.
This function takes in a 2-dimensional numpy ndarray representing an image or scene, and draws text on it using OpenCV's putText function. The text is anchored at a specified Point, and its appearance can be customized using arguments such as color, scale, and font. An optional background color and padding can be specified to draw a rectangle behind the text.
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
@@ -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])
+12 -3
View File
@@ -11,9 +11,18 @@ def show_frame_in_notebook(
"""
Display a frame in Jupyter Notebook 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
Attributes:
frame (np.ndarray): The frame to be displayed.
size (Tuple[int, int]): The size of the plot. default:(10,10)
cmap (str): the colormap to use for single channel images. default:gray
Examples:
```python
>>> from supervision.notebook.utils import show_frame_in_notebook
%matplotlib inline
show_frame_in_notebook(frame, (16, 16))
```
"""
if frame.ndim == 2:
plt.figure(figsize=size)
-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
+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