Compare commits

..
27 Commits
Author SHA1 Message Date
SkalskiP 3da433f90f 🪵 0.7.0 release changelog 2023-05-11 00:31:46 +02:00
Piotr Skalski a911cbd554 Merge pull request #44 from iPoe/feature/custom_msg_for_line_zone
Add custom text for the in & out tags
2023-05-10 18:44:52 +02:00
SkalskiP d10618a8b3 🛠️ add __len__ and __iter__ to Dataset 2023-05-10 14:02:08 +02:00
Piotr Skalski eaf5b7e95f Merge pull request #86 from roboflow/feature/support_yolo_dataset_loading
support for yolo dataset loading and saving
2023-05-09 23:02:26 +02:00
Piotr Skalski 4ae8ac1db0 Merge pull request #91 from roboflow/feature/add_yolo_nas_support
💜 Initial YOLO-NAS support
2023-05-09 19:31:56 +02:00
SkalskiP 74efca7c22 💜 Initial YOLO-NAS support 2023-05-09 17:19:40 +02:00
SkalskiP 3063dfb1f5 📚 Improve BoxAnnotator.annotate documentation and add example 2023-05-09 12:14:37 +02:00
SkalskiP 6c81d21a03 Unit tests for yolo loader added 2023-05-08 21:34:36 +02:00
SkalskiP f4b91d0175 Fix load_yolo_annotations 2023-05-08 19:46:13 +02:00
SkalskiP 45fdb1e805 Fix load_yolo_annotations 2023-05-08 19:33:44 +02:00
SkalskiP 8c0a294ca1 First fully working version of from_yolo. Ready for test. 2023-05-08 17:13:59 +02:00
SkalskiP cf7f003133 Merge remote-tracking branch 'origin/feature/support_yolo_dataset_loading' into feature/support_yolo_dataset_loading
# Conflicts:
#	supervision/dataset/formats/yolo.py
2023-05-08 14:08:00 +02:00
SkalskiP 1bebbb14a6 yolo_annotations_to_detections function full implementation 2023-05-08 14:07:43 +02:00
SkalskiP 3d0aec03e0 yolo_annotations_to_detections function full implementation 2023-05-08 13:51:49 +02:00
SkalskiP 306d7bd27e Merge remote-tracking branch 'origin/feature/support_yolo_dataset_loading' into feature/support_yolo_dataset_loading
# Conflicts:
#	supervision/dataset/formats/yolo.py
2023-05-08 10:57:28 +02:00
SkalskiP 43fc5d6205 yolo annotations box_to_polygon initial implementation 2023-05-08 10:55:58 +02:00
SkalskiP d13238bc42 yolo annotations box_to_polygon initial implementation 2023-05-07 16:38:53 +02:00
SkalskiP 77243f1752 initial yolo annotations file parsing 2023-05-07 13:16:42 +02:00
SkalskiP e16cca4349 🚀 - add quick start section to Supervision docs page with first topic - advanced detections filtering 2023-05-04 17:48:39 +02:00
Piotr Skalski e592a44bea Merge pull request #84 from roboflow/feature/20230504_add_merge_detections_method
➕ Add `Detections.merge` method
2023-05-04 15:55:46 +02:00
SkalskiP c2833bc8ca 🤍 / 🖤 - light mode / datk mode switch added 2023-05-04 10:13:11 +02:00
SkalskiP feb08d10b2 ➕ Add Detections.merge method 2023-05-04 09:30:48 +02:00
Piotr Skalski 74bd403dc0 Merge pull request #83 from roboflow/feature/20230504_major_documentation_improvements
major documentation improvements
2023-05-04 00:37:42 +02:00
SkalskiP 6a013382d3 - 📚 Detections documentation improvements
- `list_files_with_extensions` documentation improvements
- internal `deprecated` function added
- `plot_image` and `plot_images_grid` documentation improvements
2023-05-04 00:33:17 +02:00
Piotr Skalski 92290979c7 Merge pull request #81 from hardikdava/patch-2
minor bug fixed
2023-05-03 16:06:00 +02:00
Hardik Dava 696b6f9276 minor bug fixed
annotation function should return frame but it was not. So added return value
2023-05-01 12:24:30 +02:00
iPoe ac61598ad4 Add custom text for the in & out tags. 2023-03-29 14:01:35 -05:00
19 changed files with 1214 additions and 55 deletions
+7
View File
@@ -1,3 +1,10 @@
### 0.7.0 <small>May 11, 2023</small>
- Added [[#91](https://github.com/roboflow/supervision/pull/91)]: `Detections.from_yolo_nas` to enable seamless integration with [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) model.
- Added [[#86](https://github.com/roboflow/supervision/pull/86)]: ability to load datasets in YOLO format using `Dataset.from_yolo`.
- Added [[#84](https://github.com/roboflow/supervision/pull/84)]: `Detections.merge` to merge multiple `Detections` objects together.
- Fixed [[#81](https://github.com/roboflow/supervision/pull/81)]: `LineZoneAnnotator.annotate` does not return annotated frame.
### 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.
+312
View File
@@ -0,0 +1,312 @@
## advanced filtering
The advanced filtering capabilities of the `Detections` class offer users a versatile and efficient way to narrow down
and refine object detections. This section outlines various filtering methods, including filtering by specific class
or a set of classes, confidence, object area, bounding box area, relative area, box dimensions, and designated zones.
Each method is demonstrated with concise code examples to provide users with a clear understanding of how to implement
the filters in their applications.
### by specific class
Allows you to select detections that belong only to one selected class.
=== "After"
```python
import supervision as sv
detections = sv.Detections(...)
detections = detections[detections.class_id == 0]
```
<div class="result" markdown>
![by-specific-class](https://media.roboflow.com/open-source/supervision/supervision-detection-by-specific-class.png){ align=center width="800" }
</div>
=== "Before"
```python
import supervision as sv
detections = sv.Detections(...)
detections = detections[detections.class_id == 0]
```
<div class="result" markdown>
![original](https://media.roboflow.com/open-source/supervision/supervision-detection-original.png){ align=center width="800" }
</div>
### by set of classes
Allows you to select detections that belong only to selected set of classes.
=== "After"
```python
import numpy as np
import supervision as sv
selected_classes = [0, 2, 3]
detections = sv.Detections(...)
detections = detections[np.isin(detections.class_id, selected_classes)]
```
<div class="result" markdown>
![by-set-of-classes](https://media.roboflow.com/open-source/supervision/supervision-detection-by-set-of-classes.png){ align=center width="800" }
</div>
=== "Before"
```python
import numpy as np
import supervision as sv
class_id = [0, 2, 3]
detections = sv.Detections(...)
detections = detections[np.isin(detections.class_id, class_id)]
```
<div class="result" markdown>
![original](https://media.roboflow.com/open-source/supervision/supervision-detection-original.png){ align=center width="800" }
</div>
### by confidence
Allows you to select detections with specific confidence value, for example higher than selected threshold.
=== "After"
```python
import supervision as sv
detections = sv.Detections(...)
detections = detections[detections.confidence > 0.5]
```
<div class="result" markdown>
![by-set-of-classes](https://media.roboflow.com/open-source/supervision/supervision-detection-by-confidence.png){ align=center width="800" }
</div>
=== "Before"
```python
import supervision as sv
detections = sv.Detections(...)
detections = detections[detections.confidence > 0.5]
```
<div class="result" markdown>
![original](https://media.roboflow.com/open-source/supervision/supervision-detection-original.png){ align=center width="800" }
</div>
### by area
Allows you to select detections based on their size. We define the area as the number of pixels occupied by the
detection in the image. In the example below, we have sifted out the detections that are too small.
=== "After"
```python
import supervision as sv
detections = sv.Detections(...)
detections = detections[detections.area > 1000]
```
<div class="result" markdown>
![by-area](https://media.roboflow.com/open-source/supervision/supervision-detection-by-area.png){ align=center width="800" }
</div>
=== "Before"
```python
import supervision as sv
detections = sv.Detections(...)
detections = detections[detections.area > 1000]
```
<div class="result" markdown>
![original](https://media.roboflow.com/open-source/supervision/supervision-detection-original.png){ align=center width="800" }
</div>
### by relative area
Allows you to select detections based on their size in relation to the size of whole image. Sometimes the concept of
detection size changes depending on the image. Detection occupying 10000 square px can be large on a 1280x720 image
but small on a 3840x2160 image. In such cases, we can filter out detections based on the percentage of the image area
occupied by them. In the example below, we remove too large detections.
=== "After"
```python
import supervision as sv
image = ...
height, width, channels = image.shape
image_area = height * width
detections = sv.Detections(...)
detections = detections[(detections.area / image_area) < 0.8]
```
<div class="result" markdown>
![by-relative-area](https://media.roboflow.com/open-source/supervision/supervision-detection-by-relative-area.png?updatedAt=1683207183434){ align=center width="800" }
</div>
=== "Before"
```python
import supervision as sv
image = ...
height, width, channels = image.shape
image_area = height * width
detections = sv.Detections(...)
detections = detections[(detections.area / image_area) < 0.8]
```
<div class="result" markdown>
![original](https://media.roboflow.com/open-source/supervision/supervision-detection-original.png){ align=center width="800" }
</div>
### by box dimensions
Allows you to select detections based on their dimensions. The size of the bounding box, as well as its coordinates,
can be criteria for rejecting detection. Implementing such filtering requires a bit of custom code but is relatively
simple and fast.
=== "After"
```python
import supervision as sv
detections = sv.Detections(...)
w = detections.xyxy[:, 2] - detections.xyxy[:, 0]
h = detections.xyxy[:, 3] - detections.xyxy[:, 1]
detections = detections[(w > 200) & (h > 200)]
```
<div class="result" markdown>
![by-box-dimensions](https://media.roboflow.com/open-source/supervision/supervision-detection-by-box-dimensions.png){ align=center width="800" }
</div>
=== "Before"
```python
import supervision as sv
detections = sv.Detections(...)
w = detections.xyxy[:, 2] - detections.xyxy[:, 0]
h = detections.xyxy[:, 3] - detections.xyxy[:, 1]
detections = detections[(w > 200) & (h > 200)]
```
<div class="result" markdown>
![original](https://media.roboflow.com/open-source/supervision/supervision-detection-original.png){ align=center width="800" }
</div>
### by `PolygonZone`
Allows you to use `Detections` in combination with `PolygonZone` to weed out bounding boxes that are in and out of the
zone. In the example below you can see how to filter out all detections located in the lower part of the image.
=== "After"
```python
import supervision as sv
zone = sv.PolygonZone(...)
detections = sv.Detections(...)
mask = zone.trigger(detections=detections)
detections = detections[mask]
```
<div class="result" markdown>
![by-polygon-zone](https://media.roboflow.com/open-source/supervision/supervision-detection-by-polygon-zone.png?updatedAt=1683211380445){ align=center width="800" }
</div>
=== "Before"
```python
import supervision as sv
zone = sv.PolygonZone(...)
detections = sv.Detections(...)
mask = zone.trigger(detections=detections)
detections = detections[mask]
```
<div class="result" markdown>
![original](https://media.roboflow.com/open-source/supervision/supervision-detection-original.png){ align=center width="800" }
</div>
### by mixed conditions
`Detections`' greatest strength, however, is that you can build arbitrarily complex logical conditions by simply combining separate conditions using `&` or `|`.
=== "After"
```python
import supervision as sv
zone = sv.PolygonZone(...)
detections = sv.Detections(...)
mask = zone.trigger(detections=detections)
detections = detections[(detections.confidence > 0.7) & mask]
```
<div class="result" markdown>
![by-mixed-conditions](https://media.roboflow.com/open-source/supervision/supervision-detection-by-mixed-conditions.png){ align=center width="800" }
</div>
=== "Before"
```python
import supervision as sv
zone = sv.PolygonZone(...)
detections = sv.Detections(...)
mask = zone.trigger(detections=detections)
detections = detections[mask]
```
<div class="result" markdown>
![original](https://media.roboflow.com/open-source/supervision/supervision-detection-original.png){ align=center width="800" }
</div>
+4
View File
@@ -0,0 +1,4 @@
:root {
--md-primary-fg-color: #8315F9;
--md-accent-fg-color: #00FFCE;
}
+25 -4
View File
@@ -21,10 +21,15 @@ extra:
provider: google
property: G-P7ZG0Y19G5
extra_css:
- stylesheets/extra.css
nav:
- Home: index.md
- Quickstart:
- Detections: quickstart/detections.md
- API reference:
- Detection:
- Detections:
- Core: detection/core.md
- Annotate: detection/annotate.md
- Utils: detection/utils.md
@@ -36,7 +41,8 @@ nav:
- Utils: draw/utils.md
- Notebook:
- Utils: notebook/utils.md
- Video: video.md
- Video:
- Utils: video.md
- Changelog: changelog.md
theme:
@@ -44,8 +50,19 @@ theme:
logo: https://media.roboflow.com/open-source/supervision/supervision-lenny.png
favicon: https://media.roboflow.com/open-source/supervision/supervision-lenny.png
palette:
primary: 'deep purple'
accent: 'teal'
# Palette for light mode
- scheme: default
primary: 'custom'
toggle:
icon: material/brightness-7
name: Switch to dark mode
# Palette toggle for dark mode
- scheme: slate
primary: 'custom'
toggle:
icon: material/brightness-4
name: Switch to light mode
font:
text: Roboto
code: Roboto Mono
@@ -58,5 +75,9 @@ markdown_extensions:
- admonition
- pymdownx.details
- pymdownx.superfences
- attr_list
- md_in_html
- pymdownx.tabbed:
alternate_style: true
- toc:
permalink: true
+1 -1
View File
@@ -1,4 +1,4 @@
__version__ = "0.6.0"
__version__ = "0.7.0"
from supervision.dataset.core import Dataset
from supervision.detection.annotate import BoxAnnotator, MaskAnnotator
+95 -1
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from typing import Dict, List, Optional, Tuple, Iterator
import cv2
import numpy as np
@@ -11,6 +11,7 @@ from supervision.dataset.formats.pascal_voc import (
detections_to_pascal_voc,
load_pascal_voc_annotations,
)
from supervision.dataset.formats.yolo import load_yolo_annotations
from supervision.detection.core import Detections
from supervision.file import list_files_with_extensions
@@ -30,6 +31,26 @@ class Dataset:
images: Dict[str, np.ndarray]
annotations: Dict[str, Detections]
def __len__(self) -> int:
"""
Return the number of images in the dataset.
Returns:
int: The number of images.
"""
return len(self.images)
def __iter__(self) -> Iterator[Tuple[str, np.ndarray, Detections]]:
"""
Iterate over the images and annotations in the dataset.
Yields:
Iterator[Tuple[str, np.ndarray, Detections]]: An iterator that yields tuples containing the image name,
the image data, and its corresponding annotation.
"""
for image_name, image in self.images.items():
yield image_name, image, self.annotations.get(image_name, None)
def as_pascal_voc(
self,
images_directory_path: Optional[str] = None,
@@ -96,6 +117,28 @@ class Dataset:
Returns:
Dataset: A Dataset instance containing the loaded images and annotations.
Example:
```python
>>> import roboflow
>>> from roboflow import Roboflow
>>> import supervision as sv
>>> roboflow.login()
>>> rf = Roboflow()
>>> project = rf.workspace(WORKSPACE_ID).project(PROJECT_ID)
>>> dataset = project.version(PROJECT_VERSION).download("voc")
>>> train_dataset = sv.Dataset.from_yolo(
... images_directory_path=f"{dataset.location}/train/images",
... annotations_directory_path=f"{dataset.location}/train/labels"
... )
>>> dataset.classes
['dog', 'person']
```
"""
image_paths = list_files_with_extensions(
directory=images_directory_path, extensions=["jpg", "jpeg", "png"]
@@ -126,3 +169,54 @@ class Dataset:
image_name: detections for image_name, detections, _ in raw_annotations
}
return Dataset(classes=classes, images=images, annotations=annotations)
@classmethod
def from_yolo(
cls,
images_directory_path: str,
annotations_directory_path: str,
data_yaml_path: str,
force_masks: bool = False,
) -> Dataset:
"""
Creates a Dataset instance from YOLO 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 YOLO annotation files.
data_yaml_path (str): The path to the data YAML file containing class information.
force_masks (bool, optional): If True, forces masks to be loaded for all annotations, regardless of whether they are present.
Returns:
Dataset: A Dataset instance containing the loaded images and annotations.
Example:
```python
>>> import roboflow
>>> from roboflow import Roboflow
>>> import supervision as sv
>>> roboflow.login()
>>> rf = Roboflow()
>>> project = rf.workspace(WORKSPACE_ID).project(PROJECT_ID)
>>> dataset = project.version(PROJECT_VERSION).download("yolov5")
>>> train_dataset = sv.Dataset.from_yolo(
... images_directory_path=f"{dataset.location}/train/images",
... annotations_directory_path=f"{dataset.location}/train/labels",
... data_yaml_path=f"{dataset.location}/data.yaml"
... )
>>> dataset.classes
['dog', 'person']
```
"""
classes, images, annotations = load_yolo_annotations(
images_directory_path=images_directory_path,
annotations_directory_path=annotations_directory_path,
data_yaml_path=data_yaml_path,
force_masks=force_masks,
)
return Dataset(classes=classes, images=images, annotations=annotations)
+10 -22
View File
@@ -4,13 +4,9 @@ from xml.etree.ElementTree import Element, SubElement, parse, tostring
import numpy as np
from supervision.dataset.ultils import approximate_mask_with_polygons
from supervision.detection.core import Detections
from supervision.detection.utils import (
approximate_polygon,
filter_polygons_by_area,
mask_to_polygons,
polygon_to_xyxy,
)
from supervision.detection.utils import polygon_to_xyxy
def object_to_pascal_voc(
@@ -104,24 +100,16 @@ def detections_to_pascal_voc(
for xyxy, mask, _, class_id, _ in detections:
name = classes[class_id]
if mask is not None:
polygons = mask_to_polygons(mask=mask)
if len(polygons) == 1:
polygons = filter_polygons_by_area(
polygons=polygons, min_area=None, max_area=maximum_detection_area
)
else:
polygons = filter_polygons_by_area(
polygons=polygons,
min_area=minimum_detection_area,
max_area=maximum_detection_area,
)
polygons = approximate_mask_with_polygons(
mask=mask,
min_image_area_percentage=min_image_area_percentage,
max_image_area_percentage=max_image_area_percentage,
approximation_percentage=approximation_percentage,
)
for polygon in polygons:
approx_polygon = approximate_polygon(
polygon=polygon, percentage=approximation_percentage
)
xyxy = polygon_to_xyxy(polygon=approx_polygon)
xyxy = polygon_to_xyxy(polygon=polygon)
next_object = object_to_pascal_voc(
xyxy=xyxy, name=name, polygon=approx_polygon
xyxy=xyxy, name=name, polygon=polygon
)
annotation.append(next_object)
else:
+161
View File
@@ -0,0 +1,161 @@
from pathlib import Path
from typing import Dict, List, Tuple, Union
import cv2
import numpy as np
from supervision.detection.core import Detections
from supervision.detection.utils import polygon_to_mask, polygon_to_xyxy
from supervision.file import list_files_with_extensions, read_txt_file
def _parse_box(values: List[str]) -> np.ndarray:
x_center, y_center, width, height = values
return np.array(
[
float(x_center) - float(width) / 2,
float(y_center) - float(height) / 2,
float(x_center) + float(width) / 2,
float(y_center) + float(height) / 2,
],
dtype=np.float32,
)
def _box_to_polygon(box: np.ndarray) -> np.ndarray:
return np.array(
[[box[0], box[1]], [box[2], box[1]], [box[2], box[3]], [box[0], box[3]]]
)
def _parse_polygon(values: List[str]) -> np.ndarray:
return np.array(values, dtype=np.float32).reshape(-1, 2)
def _polygons_to_masks(
polygons: List[np.ndarray], resolution_wh: Tuple[int, int]
) -> np.ndarray:
return np.array(
[
polygon_to_mask(polygon=polygon, resolution_wh=resolution_wh)
for polygon in polygons
],
dtype=bool,
)
def _pair_image_with_annotation(
image_paths: List[Union[str, Path]], annotation_paths: List[Union[str, Path]]
) -> List[Tuple[Union[str, Path], Union[str, Path]]]:
image_dict = {p.stem: p for p in image_paths}
return [
(image_dict[annotation_path.stem], annotation_path)
for annotation_path in annotation_paths
if annotation_path.stem in image_dict
]
def _with_mask(lines: List[str]) -> bool:
return any([len(line.split()) > 5 for line in lines])
def _extract_class_names(file_path: str) -> List[str]:
names = []
with open(file_path, "r") as file:
lines = file.readlines()
for line in lines:
if line.strip().startswith("names:"):
start_index = lines.index(line) + 1
break
for line in lines[start_index:]:
if line.strip().startswith("-"):
names.append(line.strip().replace("-", "").strip())
else:
break
return names
def yolo_annotations_to_detections(
lines: List[str], resolution_wh: Tuple[int, int], with_masks: bool
) -> Detections:
if len(lines) == 0:
return Detections.empty()
class_id, relative_xyxy, relative_polygon = [], [], []
w, h = resolution_wh
for line in lines:
values = line.split()
class_id.append(int(values[0]))
if len(values) == 5:
box = _parse_box(values=values[1:])
relative_xyxy.append(box)
if with_masks:
relative_polygon.append(_box_to_polygon(box=box))
elif len(values) > 5:
polygon = _parse_polygon(values=values[1:])
relative_xyxy.append(polygon_to_xyxy(polygon=polygon))
if with_masks:
relative_polygon.append(polygon)
class_id = np.array(class_id, dtype=int)
relative_xyxy = np.array(relative_xyxy, dtype=np.float32)
xyxy = relative_xyxy * np.array([w, h, w, h], dtype=np.float32)
if not with_masks:
return Detections(class_id=class_id, xyxy=xyxy)
polygons = [
(polygon * np.array(resolution_wh)).astype(int) for polygon in relative_polygon
]
mask = _polygons_to_masks(polygons=polygons, resolution_wh=resolution_wh)
return Detections(class_id=class_id, xyxy=xyxy, mask=mask)
def load_yolo_annotations(
images_directory_path: str,
annotations_directory_path: str,
data_yaml_path: str,
force_masks: bool = False,
) -> Tuple[List[str], Dict[str, np.ndarray], Dict[str, Detections]]:
"""
Loads YOLO annotations and returns class names, images, and their corresponding detections.
Args:
images_directory_path (str): The path to the directory containing the images.
annotations_directory_path (str): The path to the directory containing the YOLO annotation files.
data_yaml_path (str): The path to the data YAML file containing class information.
force_masks (bool, optional): If True, forces masks to be loaded for all annotations, regardless of whether they are present.
Returns:
Tuple[List[str], Dict[str, np.ndarray], Dict[str, Detections]]: A tuple containing a list of class names, a dictionary with image paths as keys and images as values, and a dictionary with image paths as keys and corresponding Detections instances as values.
"""
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=["txt"]
)
path_pairs = _pair_image_with_annotation(
image_paths=image_paths, annotation_paths=annotation_paths
)
classes = _extract_class_names(file_path=data_yaml_path)
images = {}
annotations = {}
for image_path, annotation_path in path_pairs:
image = cv2.imread(str(image_path))
lines = read_txt_file(str(annotation_path))
h, w, _ = image.shape
resolution_wh = (w, h)
with_masks = _with_mask(lines=lines)
with_masks = force_masks if force_masks else with_masks
annotation = yolo_annotations_to_detections(
lines=lines, resolution_wh=resolution_wh, with_masks=with_masks
)
images[str(image_path)] = image
annotations[str(image_path)] = annotation
return classes, images, annotations
+37
View File
@@ -0,0 +1,37 @@
from typing import List
import numpy as np
from supervision.detection.utils import (
approximate_polygon,
filter_polygons_by_area,
mask_to_polygons,
)
def approximate_mask_with_polygons(
mask: np.ndarray,
min_image_area_percentage: float = 0.0,
max_image_area_percentage: float = 1.0,
approximation_percentage: float = 0.75,
) -> List[np.ndarray]:
height, width = mask
image_area = height * width
minimum_detection_area = min_image_area_percentage * image_area
maximum_detection_area = max_image_area_percentage * image_area
polygons = mask_to_polygons(mask=mask)
if len(polygons) == 1:
polygons = filter_polygons_by_area(
polygons=polygons, min_area=None, max_area=maximum_detection_area
)
else:
polygons = filter_polygons_by_area(
polygons=polygons,
min_area=minimum_detection_area,
max_area=maximum_detection_area,
)
return [
approximate_polygon(polygon=polygon, percentage=approximation_percentage)
for polygon in polygons
]
+25 -4
View File
@@ -47,13 +47,34 @@ class BoxAnnotator:
"""
Draws bounding boxes on the frame using the detections provided.
Parameters:
Args:
scene (np.ndarray): The image on which the bounding boxes will be drawn
detections (Detections): The detections for which the bounding boxes will be drawn
labels (Optional[List[str]]): An optional list of labels corresponding to each detection. If labels is provided, the confidence score of the detection will be replaced with the label.
skip_label (bool): Is set to True, skips bounding box label annotation.
labels (Optional[List[str]]): An optional list of labels corresponding to each detection. If `labels` are not provided, corresponding `class_id` will be used as label.
skip_label (bool): Is set to `True`, skips bounding box label annotation.
Returns:
np.ndarray: The image with the bounding boxes drawn on it
Example:
```python
>>> import supervision as sv
>>> classes = ['person', ...]
>>> image = ...
>>> detections = sv.Detections(...)
>>> box_annotator = sv.BoxAnnotator()
>>> labels = [
... f"{classes[class_id]} {confidence:0.2f}"
... for _, _, confidence, class_id, _
... in detections
... ]
>>> annotated_frame = box_annotator.annotate(
... scene=image.copy(),
... detections=detections,
... labels=labels
... )
```
"""
font = cv2.FONT_HERSHEY_SIMPLEX
for i in range(len(detections)):
@@ -139,7 +160,7 @@ class MaskAnnotator:
"""
Overlays the masks on the given image based on the provided detections, with a specified opacity.
Parameters:
Args:
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
+169 -12
View File
@@ -1,12 +1,13 @@
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import astuple, 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
from supervision.internal import deprecated
def _validate_xyxy(xyxy: Any, n: int) -> None:
@@ -136,7 +137,7 @@ class Detections:
@classmethod
def from_yolov5(cls, yolov5_results) -> Detections:
"""
Creates a Detections instance from a YOLOv5 output Detections
Creates a Detections instance from a [YOLOv5](https://github.com/ultralytics/yolov5) inference result.
Args:
yolov5_results (yolov5.models.common.Detections): The output Detections instance from YOLOv5
@@ -146,12 +147,14 @@ class Detections:
Example:
```python
>>> import cv2
>>> import torch
>>> from supervision import Detections
>>> import supervision as sv
>>> image = cv2.imread(SOURCE_IMAGE_PATH)
>>> model = torch.hub.load('ultralytics/yolov5', 'yolov5s')
>>> results = model(IMAGE)
>>> detections = Detections.from_yolov5(results)
>>> result = model(image)
>>> detections = sv.Detections.from_yolov5(result)
```
"""
yolov5_detections_predictions = yolov5_results.pred[0].cpu().cpu().numpy()
@@ -164,7 +167,7 @@ class Detections:
@classmethod
def from_yolov8(cls, yolov8_results) -> Detections:
"""
Creates a Detections instance from a YOLOv8 output Results
Creates a Detections instance from a [YOLOv8](https://github.com/ultralytics/ultralytics) inference result.
Args:
yolov8_results (ultralytics.yolo.engine.results.Results): The output Results instance from YOLOv8
@@ -174,12 +177,14 @@ class Detections:
Example:
```python
>>> import cv2
>>> from ultralytics import YOLO
>>> from supervision import Detections
>>> import supervision as sv
>>> image = cv2.imread(SOURCE_IMAGE_PATH)
>>> model = YOLO('yolov8s.pt')
>>> yolov8_results = model(IMAGE)[0]
>>> detections = Detections.from_yolov8(yolov8_results)
>>> result = model(image)[0]
>>> detections = sv.Detections.from_yolov8(result)
```
"""
return cls(
@@ -188,10 +193,39 @@ class Detections:
class_id=yolov8_results.boxes.cls.cpu().numpy().astype(int),
)
@classmethod
def from_yolo_nas(cls, yolo_nas_results) -> Detections:
"""
Creates a Detections instance from a [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) inference result.
Args:
yolo_nas_results (super_gradients.training.models.prediction_results.ImageDetectionPrediction): The output Results instance from YOLO-NAS
Returns:
Detections: A new Detections object.
Example:
```python
>>> import cv2
>>> from super_gradients.training import models
>>> import supervision as sv
>>> image = cv2.imread(SOURCE_IMAGE_PATH)
>>> model = models.get('yolo_nas_l', pretrained_weights="coco")
>>> result = list(model.predict(image, conf=0.35))[0]
>>> detections = sv.Detections.from_yolo_nas(result)
```
"""
return cls(
xyxy=yolo_nas_results.prediction.bboxes_xyxy,
confidence=yolo_nas_results.prediction.confidence,
class_id=yolo_nas_results.prediction.labels.astype(int),
)
@classmethod
def from_transformers(cls, transformers_results: dict) -> Detections:
"""
Creates a Detections instance from Object Detection Transformer output Results
Creates a Detections instance from object detection [transformer](https://github.com/huggingface/transformers) inference result.
Returns:
Detections: A new Detections object.
@@ -204,6 +238,32 @@ class Detections:
@classmethod
def from_detectron2(cls, detectron2_results) -> Detections:
"""
Create a Detections object from the [Detectron2](https://github.com/facebookresearch/detectron2) inference result.
Args:
detectron2_results: The output of a Detectron2 model containing instances with prediction data.
Returns:
(Detections): A Detections object containing the bounding boxes, class IDs, and confidences of the predictions.
Example:
```python
>>> import cv2
>>> from detectron2.engine import DefaultPredictor
>>> from detectron2.config import get_cfg
>>> import supervision as sv
>>> image = cv2.imread(SOURCE_IMAGE_PATH)
>>> cfg = get_cfg()
>>> cfg.merge_from_file("path/to/config.yaml")
>>> cfg.MODEL.WEIGHTS = "path/to/model_weights.pth"
>>> predictor = DefaultPredictor(cfg)
>>> result = predictor(image)
>>> detections = sv.Detections.from_detectron2(result)
```
"""
return cls(
xyxy=detectron2_results["instances"].pred_boxes.tensor.cpu().numpy(),
confidence=detectron2_results["instances"].scores.cpu().numpy(),
@@ -215,6 +275,38 @@ class Detections:
@classmethod
def from_roboflow(cls, roboflow_result: dict, class_list: List[str]) -> Detections:
"""
Create a Detections object from the [Roboflow](https://roboflow.com/) API inference result.
Args:
roboflow_result (dict): The result from the Roboflow API containing predictions.
class_list (List[str]): A list of class names corresponding to the class IDs in the API result.
Returns:
(Detections): A Detections object containing the bounding boxes, class IDs, and confidences of the predictions.
Example:
```python
>>> import supervision as sv
>>> roboflow_result = {
... "predictions": [
... {
... "x": 0.5,
... "y": 0.5,
... "width": 0.2,
... "height": 0.3,
... "class": "person",
... "confidence": 0.9
... },
... # ... more predictions ...
... ]
... }
>>> class_list = ["person", "car", "dog"]
>>> detections = sv.Detections.from_roboflow(roboflow_result, class_list)
```
"""
xyxy = []
confidence = []
class_id = []
@@ -241,7 +333,7 @@ class Detections:
@classmethod
def from_sam(cls, sam_result: List[dict]) -> Detections:
"""
Creates a Detections instance from Segment Anything Model (SAM) by Meta AI.
Creates a Detections instance from [Segment Anything Model](https://github.com/facebookresearch/segment-anything) inference result.
Args:
sam_result (List[dict]): The output Results instance from SAM
@@ -251,8 +343,8 @@ class Detections:
Example:
```python
>>> from segment_anything import sam_model_registry, SamAutomaticMaskGenerator
>>> import supervision as sv
>>> from segment_anything import sam_model_registry, SamAutomaticMaskGenerator
>>> sam = sam_model_registry[MODEL_TYPE](checkpoint=CHECKPOINT_PATH).to(device=DEVICE)
>>> mask_generator = SamAutomaticMaskGenerator(sam)
@@ -270,6 +362,9 @@ class Detections:
return Detections(xyxy=xywh_to_xyxy(boxes_xywh=xywh), mask=mask)
@classmethod
@deprecated(
"Dataset loading and saving is going to be executed by supervision.dataset.core.Dataset"
)
def from_coco_annotations(cls, coco_annotation: dict) -> Detections:
xyxy, class_id = [], []
@@ -282,12 +377,74 @@ class Detections:
@classmethod
def empty(cls) -> Detections:
"""
Create an empty Detections object with no bounding boxes, confidences, or class IDs.
Returns:
(Detections): An empty Detections object.
Example:
```python
>>> from supervision import Detections
>>> empty_detections = Detections.empty()
```
"""
return cls(
xyxy=np.empty((0, 4), dtype=np.float32),
confidence=np.array([], dtype=np.float32),
class_id=np.array([], dtype=int),
)
@classmethod
def merge(cls, detections_list: List[Detections]) -> Detections:
"""
Merge a list of Detections objects into a single Detections object.
This method takes a list of Detections objects and combines their respective fields (`xyxy`, `mask`,
`confidence`, `class_id`, and `tracker_id`) into a single Detections object. If all elements in a field are not
`None`, the corresponding field will be stacked. Otherwise, the field will be set to `None`.
Args:
detections_list (List[Detections]): A list of Detections objects to merge.
Returns:
(Detections): A single Detections object containing the merged data from the input list.
Example:
```python
>>> from supervision import Detections
>>> detections_1 = Detections(...)
>>> detections_2 = Detections(...)
>>> merged_detections = Detections.merge([detections_1, detections_2])
```
"""
if len(detections_list) == 0:
return Detections.empty()
detections_tuples_list = [astuple(detection) for detection in detections_list]
xyxy, mask, confidence, class_id, tracker_id = [
list(field) for field in zip(*detections_tuples_list)
]
all_not_none = lambda l: all(x is not None for x in l)
xyxy = np.vstack(xyxy)
mask = np.vstack(mask) if all_not_none(mask) else None
confidence = np.hstack(confidence) if all_not_none(confidence) else None
class_id = np.hstack(class_id) if all_not_none(class_id) else None
tracker_id = np.hstack(tracker_id) if all_not_none(tracker_id) else None
return cls(
xyxy=xyxy,
mask=mask,
confidence=confidence,
class_id=class_id,
tracker_id=tracker_id,
)
def get_anchor_coordinates(self, anchor: Position) -> np.ndarray:
"""
Returns the bounding box coordinates for a specific anchor.
+9 -3
View File
@@ -6,6 +6,7 @@ import numpy as np
from supervision.detection.core import Detections
from supervision.draw.color import Color
from supervision.geometry.core import Point, Rect, Vector
from typing import Optional
class LineZone:
@@ -70,7 +71,6 @@ class LineZone:
else:
self.out_count += 1
class LineZoneAnnotator:
def __init__(
self,
@@ -81,6 +81,8 @@ class LineZoneAnnotator:
text_scale: float = 0.5,
text_offset: float = 1.5,
text_padding: int = 10,
custom_in_text: Optional[str] = None,
custom_out_text: Optional[str] = None,
):
"""
Initialize the LineCounterAnnotator object with default values.
@@ -102,6 +104,8 @@ class LineZoneAnnotator:
self.text_scale: float = text_scale
self.text_offset: float = text_offset
self.text_padding: int = text_padding
self.custom_in_text: str = custom_in_text
self.custom_out_text: str = custom_out_text
def annotate(self, frame: np.ndarray, line_counter: LineZone) -> np.ndarray:
"""
@@ -141,8 +145,9 @@ class LineZoneAnnotator:
lineType=cv2.LINE_AA,
)
in_text = f"in: {line_counter.in_count}"
out_text = f"out: {line_counter.out_count}"
in_text = f"{self.custom_in_text}: {line_counter.in_count}" if self.custom_in_text is not None else f"in: {line_counter.in_count}"
out_text = f"{self.custom_out_text}: {line_counter.out_count}" if self.custom_out_text is not None else f"out: {line_counter.out_count}"
(in_text_width, in_text_height), _ = cv2.getTextSize(
in_text, cv2.FONT_HERSHEY_SIMPLEX, self.text_scale, self.text_thickness
@@ -219,3 +224,4 @@ class LineZoneAnnotator:
self.text_thickness,
cv2.LINE_AA,
)
return frame
+38 -5
View File
@@ -2,14 +2,30 @@ 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]:
"""
List files in a directory with specified extensions or all files if no extensions are provided.
Args:
directory (Union[str, Path]): The directory path as a string or Path object.
extensions (Optional[List[str]]): A list of file extensions to filter. Default is None, which lists all files.
Returns:
(List[Path]): A list of Path objects for the matching files.
Examples:
```python
>>> from supervision import list_files_with_extensions
>>> # List all files in the directory
>>> files = list_files_with_extensions(directory='my_directory')
>>> # List only files with '.txt' and '.md' extensions
>>> files = list_files_with_extensions(directory='my_directory', extensions=['txt', 'md'])
```
"""
directory = Path(directory)
files_with_extensions = []
@@ -20,3 +36,20 @@ def list_files_with_extensions(
files_with_extensions.extend(directory.glob("*"))
return files_with_extensions
def read_txt_file(file_path: str) -> List[str]:
"""
Read a text file and return a list of strings without newline characters.
Args:
file_path (str): The path to the text file.
Returns:
List[str]: A list of strings representing the lines in the text file.
"""
with open(file_path, "r") as file:
lines = file.readlines()
lines = [line.rstrip("\n") for line in lines]
return lines
+18
View File
@@ -0,0 +1,18 @@
import functools
import warnings
def deprecated(reason: str):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
warnings.warn(
f"{func.__name__} is deprecated: {reason}",
category=DeprecationWarning,
stacklevel=2,
)
return func(*args, **kwargs)
return wrapper
return decorator
+2 -2
View File
@@ -24,7 +24,7 @@ def plot_image(
>>> image = cv2.imread("path/to/image.jpg")
%matplotlib inline
>>> sv.plot_image(image, (16, 16))
>>> sv.plot_image(image=image, size=(16, 16))
```
"""
plt.figure(figsize=size)
@@ -71,7 +71,7 @@ def plot_images_grid(
>>> titles = ["Image 1", "Image 2", "Image 3"]
%matplotlib inline
>>> plot_images_grid(images, grid_size=(2, 2), titles=titles, figsize=(16, 16))
>>> plot_images_grid(images, grid_size=(2, 2), titles=titles, size=(16, 16))
```
"""
nrows, ncols = grid_size
View File
View File
+204
View File
@@ -0,0 +1,204 @@
from contextlib import ExitStack as DoesNotRaise
from typing import List, Tuple, Optional
import pytest
import numpy as np
from supervision.detection.core import Detections
from supervision.dataset.formats.yolo import yolo_annotations_to_detections, _with_mask
def _mock_simple_mask(resolution_wh: Tuple[int, int], box: List[int]) -> np.array:
x_min, y_min, x_max, y_max = box
mask = np.full(resolution_wh, False, dtype=bool)
mask[y_min:y_max, x_min:x_max] = True
return mask
# The result of _mock_simple_mask is a little different from the result produced by cv2.
def _arrays_almost_equal(arr1: np.ndarray, arr2: np.ndarray, threshold: float = 0.99) -> bool:
equal_elements = np.equal(arr1, arr2)
proportion_equal = np.mean(equal_elements)
return proportion_equal >= threshold
@pytest.mark.parametrize(
'lines, expected_result, exception',
[
(
[],
False,
DoesNotRaise()
), # empty yolo annotation file
(
[
'0 0.5 0.5 0.2 0.2'
],
False,
DoesNotRaise()
), # yolo annotation file with single line with box
(
[
'0 0.50 0.50 0.20 0.20',
'1 0.11 0.47 0.22 0.30'
],
False,
DoesNotRaise()
), # yolo annotation file with two lines with box
(
[
'0 0.5 0.5 0.2 0.2'
],
False,
DoesNotRaise()
),
(
[
'0 0.4 0.4 0.6 0.4 0.6 0.6 0.4 0.6'
],
True,
DoesNotRaise()
), # yolo annotation file with single line with polygon
(
[
'0 0.4 0.4 0.6 0.4 0.6 0.6 0.4 0.6',
'1 0.11 0.47 0.22 0.30'
],
True,
DoesNotRaise()
), # yolo annotation file with two lines - one box and one polygon
]
)
def test_with_mask(lines: List[str], expected_result: Optional[bool], exception: Exception) -> None:
with exception:
result = _with_mask(lines=lines)
assert result == expected_result
@pytest.mark.parametrize(
'lines, resolution_wh, with_masks, expected_result, exception',
[
(
[],
(1000, 1000),
False,
Detections.empty(),
DoesNotRaise()
), # empty yolo annotation file
(
[
'0 0.5 0.5 0.2 0.2'
],
(1000, 1000),
False,
Detections(
xyxy=np.array([
[400, 400, 600, 600]
], dtype=np.float32),
class_id=np.array([0], dtype=int)
),
DoesNotRaise()
), # yolo annotation file with single line with box
(
[
'0 0.50 0.50 0.20 0.20',
'1 0.11 0.47 0.22 0.30'
],
(1000, 1000),
False,
Detections(
xyxy=np.array([
[400, 400, 600, 600],
[ 0, 320, 220, 620]
], dtype=np.float32),
class_id=np.array([0, 1], dtype=int)
),
DoesNotRaise()
), # yolo annotation file with two lines with box
(
[
'0 0.5 0.5 0.2 0.2'
],
(1000, 1000),
True,
Detections(
xyxy=np.array([
[400, 400, 600, 600]
], dtype=np.float32),
class_id=np.array([0], dtype=int),
mask=np.array([
_mock_simple_mask(resolution_wh=(1000, 1000), box=[400, 400, 600, 600])
], dtype=bool)
),
DoesNotRaise()
), # yolo annotation file with single line with box in with_masks mode
(
[
'0 0.4 0.4 0.6 0.4 0.6 0.6 0.4 0.6'
],
(1000, 1000),
True,
Detections(
xyxy=np.array([
[400, 400, 600, 600]
], dtype=np.float32),
class_id=np.array([0], dtype=int),
mask=np.array([
_mock_simple_mask(resolution_wh=(1000, 1000), box=[400, 400, 600, 600])
], dtype=bool)
),
DoesNotRaise()
), # yolo annotation file with single line with polygon
(
[
'0 0.4 0.4 0.6 0.4 0.6 0.6 0.4 0.6',
'1 0.11 0.47 0.22 0.30'
],
(1000, 1000),
True,
Detections(
xyxy=np.array([
[400, 400, 600, 600],
[ 0, 320, 220, 620]
], dtype=np.float32),
class_id=np.array([0, 1], dtype=int),
mask=np.array([
_mock_simple_mask(resolution_wh=(1000, 1000), box=[400, 400, 600, 600]),
_mock_simple_mask(resolution_wh=(1000, 1000), box=[ 0, 320, 220, 620])
], dtype=bool)
),
DoesNotRaise()
), # yolo annotation file with two lines - one box and one polygon in with_masks mode
(
[
'0 0.4 0.4 0.6 0.4 0.6 0.6 0.4 0.6',
'1 0.11 0.47 0.22 0.30'
],
(1000, 1000),
False,
Detections(
xyxy=np.array([
[400, 400, 600, 600],
[ 0, 320, 220, 620]
], dtype=np.float32),
class_id=np.array([0, 1], dtype=int)
),
DoesNotRaise()
), # yolo annotation file with two lines - one box and one polygon
]
)
def test_yolo_annotations_to_detections(
lines: List[str],
resolution_wh: Tuple[int, int],
with_masks: bool,
expected_result: Optional[Detections],
exception: Exception
) -> None:
with exception:
result = yolo_annotations_to_detections(
lines=lines,
resolution_wh=resolution_wh,
with_masks=with_masks)
assert np.array_equal(result.xyxy, expected_result.xyxy)
assert np.array_equal(result.class_id, expected_result.class_id)
assert (result.mask is None and expected_result.mask is None) or _arrays_almost_equal(result.mask, expected_result.mask)
+97 -1
View File
@@ -4,7 +4,7 @@ import pytest
from supervision import Detections
from typing import Optional, Union
from typing import Optional, Union, List
import numpy as np
@@ -35,6 +35,15 @@ PREDICTIONS = np.array([
], dtype=np.float32)
def mock_detections(xyxy, confidence = None, class_id = None, tracker_id = None) -> Detections:
return Detections(
xyxy = np.array(xyxy, dtype=np.float32),
confidence = confidence if confidence is None else np.array(confidence, dtype=np.float32),
class_id = class_id if class_id is None else np.array(class_id, dtype=int),
tracker_id = tracker_id if tracker_id is None else np.array(tracker_id, dtype=int),
)
@pytest.mark.parametrize(
'detections, index, expected_result, exception',
[
@@ -103,3 +112,90 @@ def test_getitem(
with exception:
result = detections[index]
assert result == expected_result
@pytest.mark.parametrize(
'detections_list, expected_result, exception',
[
(
[],
Detections.empty(),
DoesNotRaise()
), # empty detections list
(
[
Detections.empty()
],
Detections.empty(),
DoesNotRaise()
), # single empty detections
(
[
mock_detections(xyxy=[[10, 10, 20, 20]])
],
mock_detections(xyxy=[[10, 10, 20, 20]]),
DoesNotRaise()
), # single detection with xyxy field
(
[
mock_detections(xyxy=[[10, 10, 20, 20]]),
Detections.empty()
],
mock_detections(xyxy=[[10, 10, 20, 20]]),
DoesNotRaise()
), # single detection with xyxy field + empty detection
(
[
mock_detections(xyxy=[[10, 10, 20, 20]]),
mock_detections(xyxy=[[20, 20, 30, 30]])
],
mock_detections(
xyxy=[
[10, 10, 20, 20],
[20, 20, 30, 30]
]),
DoesNotRaise()
), # two detections with xyxy field
(
[
mock_detections(
xyxy=[[10, 10, 20, 20]],
class_id=[0]),
mock_detections(
xyxy=[[20, 20, 30, 30]])
],
mock_detections(
xyxy=[
[10, 10, 20, 20],
[20, 20, 30, 30]
]),
DoesNotRaise()
), # detection with xyxy, class_id fields + detection with xyxy field
(
[
mock_detections(
xyxy=[[10, 10, 20, 20]],
class_id=[0]),
mock_detections(
xyxy=[[20, 20, 30, 30]],
class_id=[1]),
],
mock_detections(
xyxy=[
[10, 10, 20, 20],
[20, 20, 30, 30]
],
class_id=[0, 1]
),
DoesNotRaise()
), # two detections with xyxy, class_id fields
]
)
def test_merge(
detections_list: List[Detections],
expected_result: Optional[Detections],
exception: Exception
) -> None:
with exception:
result = Detections.merge(detections_list=detections_list)
assert result == expected_result