Merge pull request #1660 from roboflow/fix/address-release-pr-comments
Address 0.25.0 release PR comments
This commit is contained in:
17 files changed
+982
-242
No files matched your search
@@ -21,7 +21,7 @@
|
||||
[](https://badge.fury.io/py/supervision)
|
||||
[](https://colab.research.google.com/github/roboflow/supervision/blob/main/demo.ipynb)
|
||||
[](https://huggingface.co/spaces/Roboflow/Annotators)
|
||||
[](https://discord.gg/GbfgXGJ8Bk)
|
||||
[](https://discord.gg/GbfgXGJ8Bk)
|
||||
[](https://squidfunk.github.io/mkdocs-material/)
|
||||
|
||||
<div align="center">
|
||||
@@ -135,88 +135,88 @@ for path, image, annotation in ds:
|
||||
|
||||
- load
|
||||
|
||||
```python
|
||||
dataset = sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
)
|
||||
```python
|
||||
dataset = sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
)
|
||||
|
||||
dataset = sv.DetectionDataset.from_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...
|
||||
)
|
||||
dataset = sv.DetectionDataset.from_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...
|
||||
)
|
||||
|
||||
dataset = sv.DetectionDataset.from_coco(
|
||||
images_directory_path=...,
|
||||
annotations_path=...
|
||||
)
|
||||
```
|
||||
dataset = sv.DetectionDataset.from_coco(
|
||||
images_directory_path=...,
|
||||
annotations_path=...
|
||||
)
|
||||
```
|
||||
|
||||
- split
|
||||
|
||||
```python
|
||||
train_dataset, test_dataset = dataset.split(split_ratio=0.7)
|
||||
test_dataset, valid_dataset = test_dataset.split(split_ratio=0.5)
|
||||
```python
|
||||
train_dataset, test_dataset = dataset.split(split_ratio=0.7)
|
||||
test_dataset, valid_dataset = test_dataset.split(split_ratio=0.5)
|
||||
|
||||
len(train_dataset), len(test_dataset), len(valid_dataset)
|
||||
# (700, 150, 150)
|
||||
```
|
||||
len(train_dataset), len(test_dataset), len(valid_dataset)
|
||||
# (700, 150, 150)
|
||||
```
|
||||
|
||||
- merge
|
||||
|
||||
```python
|
||||
ds_1 = sv.DetectionDataset(...)
|
||||
len(ds_1)
|
||||
# 100
|
||||
ds_1.classes
|
||||
# ['dog', 'person']
|
||||
```python
|
||||
ds_1 = sv.DetectionDataset(...)
|
||||
len(ds_1)
|
||||
# 100
|
||||
ds_1.classes
|
||||
# ['dog', 'person']
|
||||
|
||||
ds_2 = sv.DetectionDataset(...)
|
||||
len(ds_2)
|
||||
# 200
|
||||
ds_2.classes
|
||||
# ['cat']
|
||||
ds_2 = sv.DetectionDataset(...)
|
||||
len(ds_2)
|
||||
# 200
|
||||
ds_2.classes
|
||||
# ['cat']
|
||||
|
||||
ds_merged = sv.DetectionDataset.merge([ds_1, ds_2])
|
||||
len(ds_merged)
|
||||
# 300
|
||||
ds_merged.classes
|
||||
# ['cat', 'dog', 'person']
|
||||
```
|
||||
ds_merged = sv.DetectionDataset.merge([ds_1, ds_2])
|
||||
len(ds_merged)
|
||||
# 300
|
||||
ds_merged.classes
|
||||
# ['cat', 'dog', 'person']
|
||||
```
|
||||
|
||||
- save
|
||||
|
||||
```python
|
||||
dataset.as_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
)
|
||||
```python
|
||||
dataset.as_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
)
|
||||
|
||||
dataset.as_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...
|
||||
)
|
||||
dataset.as_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...
|
||||
)
|
||||
|
||||
dataset.as_coco(
|
||||
images_directory_path=...,
|
||||
annotations_path=...
|
||||
)
|
||||
```
|
||||
dataset.as_coco(
|
||||
images_directory_path=...,
|
||||
annotations_path=...
|
||||
)
|
||||
```
|
||||
|
||||
- convert
|
||||
|
||||
```python
|
||||
sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
).as_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...
|
||||
)
|
||||
```
|
||||
```python
|
||||
sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
).as_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...
|
||||
)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+284
-43
@@ -1,5 +1,6 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Track Objects
|
||||
@@ -13,6 +14,8 @@ take you through the steps to perform inference using the YOLOv8 model via eithe
|
||||
you'll discover how to track these objects efficiently and annotate your video content
|
||||
for a deeper analysis.
|
||||
|
||||
## Object Detection & Segmentation
|
||||
|
||||
To make it easier for you to follow our tutorial download the video we will use as an
|
||||
example. You can do this using
|
||||
[`supervision[assets]`](/latest/assets/) extension.
|
||||
@@ -21,14 +24,13 @@ example. You can do this using
|
||||
from supervision.assets import download_assets, VideoAssets
|
||||
|
||||
download_assets(VideoAssets.PEOPLE_WALKING)
|
||||
download_assets(VideoAssets.SKIING)
|
||||
```
|
||||
|
||||
<video controls>
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/people-walking.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
## Run Inference
|
||||
### Run Inference
|
||||
|
||||
First, you'll need to obtain predictions from your object detection or segmentation
|
||||
model. In this tutorial, we are using the YOLOv8 model as an example. However,
|
||||
@@ -41,6 +43,10 @@ by obtaining model predictions and then annotating the frame based on these pred
|
||||
This `callback` function will be essential in the subsequent steps of the tutorial, as
|
||||
it will be modified to include tracking, labeling, and trace annotations.
|
||||
|
||||
!!! tip
|
||||
|
||||
Both object detection and segmentation models are supported. Try it with `yolov8n.pt` or `yolov8n-640-seg`!
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py }
|
||||
@@ -89,7 +95,7 @@ it will be modified to include tracking, labeling, and trace annotations.
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/run-inference.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
## Tracking
|
||||
### Tracking
|
||||
|
||||
After running inference and obtaining predictions, the next step is to track the
|
||||
detected objects throughout the video. Utilizing Supervision’s
|
||||
@@ -145,7 +151,7 @@ enabling the continuous following of the object's motion path across different f
|
||||
)
|
||||
```
|
||||
|
||||
## Annotate Video with Tracking IDs
|
||||
### Annotate Video with Tracking IDs
|
||||
|
||||
Annotating the video with tracking IDs helps in distinguishing and following each object
|
||||
distinctly. With the
|
||||
@@ -227,7 +233,7 @@ offering a clear visual representation of each object's class and unique identif
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/annotate-video-with-tracking-ids.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
## Annotate Video with Traces
|
||||
### Annotate Video with Traces
|
||||
|
||||
Adding traces to the video involves overlaying the historical paths of the detected
|
||||
objects. This feature, powered by the
|
||||
@@ -315,9 +321,96 @@ movement patterns and interactions between objects in the video.
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/annotate-video-with-traces.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
## Tracking Key Points
|
||||
## Keypoints
|
||||
|
||||
Keypoint tracking is currently supported via the conversion of `KeyPoints` to `Detections`. This is achieved with the [`keypoints_to_detections`](/latest/utils/datatypes/#supervision.utils.datatypes.keypoints_to_detections) function. We'll use a different video as well as [`DetectionsSmoother`](/latest/detection/tools/smoother/) to stabilize the boxes.
|
||||
Models aren't limited to object detection and segmentation. Keypoint detection allows for detailed analysis of body joints and connections, especially valuable for applications like human pose estimation. This section introduces keypoint tracking. We'll walk through the steps of annotating keypoints, converting them into bounding box detections compatible with `ByteTrack`, and applying detection smoothing for enhanced stability.
|
||||
|
||||
To make it easier for you to follow our tutorial, let's download the video we will use as an
|
||||
example. You can do this using [`supervision[assets]`](/latest/assets/) extension.
|
||||
|
||||
```python
|
||||
from supervision.assets import download_assets, VideoAssets
|
||||
|
||||
download_assets(VideoAssets.SKIING)
|
||||
```
|
||||
|
||||
<video controls muted>
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/skiing-hd.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
### Keypoint Detection
|
||||
|
||||
First, you'll need to obtain predictions from your keypoint detection model. In this tutorial, we are using the YOLOv8 model as an example. However,
|
||||
Supervision is versatile and compatible with various models. Check this [link](/latest/keypoint/core/) for guidance on how to plug in other models.
|
||||
|
||||
We will define a `callback` function, which will process each frame of the video by obtaining model predictions and then annotating the frame based on these predictions.
|
||||
|
||||
Let's immediately visualize the results with our [`EdgeAnnotator`](/latest/keypoint/annotators/#supervision.keypoint.annotators.EdgeAnnotator) and [`VertexAnnotator`](https://supervision.roboflow.com/latest/keypoint/annotators/#supervision.keypoint.annotators.VertexAnnotator).
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="5 10-11" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from ultralytics import YOLO
|
||||
|
||||
model = YOLO("yolov8m-pose.pt")
|
||||
edge_annotator = sv.EdgeAnnotator()
|
||||
vertex_annotator = sv.VertexAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model(frame)[0]
|
||||
key_points = sv.KeyPoints.from_ultralytics(results)
|
||||
|
||||
annotated_frame = edge_annotator.annotate(
|
||||
frame.copy(), key_points=key_points)
|
||||
return vertex_annotator.annotate(
|
||||
annotated_frame, key_points=key_points)
|
||||
|
||||
sv.process_video(
|
||||
source_path="skiing.mp4",
|
||||
target_path="result.mp4",
|
||||
callback=callback
|
||||
)
|
||||
```
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="5-6 11-12" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from inference.models.utils import get_roboflow_model
|
||||
|
||||
model = get_roboflow_model(
|
||||
model_id="yolov8m-pose-640", api_key=<ROBOFLOW API KEY>)
|
||||
edge_annotator = sv.EdgeAnnotator()
|
||||
vertex_annotator = sv.VertexAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model.infer(frame)[0]
|
||||
key_points = sv.KeyPoints.from_inference(results)
|
||||
|
||||
annotated_frame = edge_annotator.annotate(
|
||||
frame.copy(), key_points=key_points)
|
||||
return vertex_annotator.annotate(
|
||||
annotated_frame, key_points=key_points)
|
||||
|
||||
sv.process_video(
|
||||
source_path="skiing.mp4",
|
||||
target_path="result.mp4",
|
||||
callback=callback
|
||||
)
|
||||
```
|
||||
|
||||
<video controls>
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/track-keypoints-only-keypoints.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
### Convert to Detections
|
||||
|
||||
Keypoint tracking is currently supported via the conversion of `KeyPoints` to `Detections`. This is achieved with the [`KeyPoints.as_detections()`](/latest/keypoint/core/#supervision.keypoint.core.KeyPoints.as_detections) function.
|
||||
|
||||
Let's convert to detections and visualize the results with our [`BoxAnnotator`](/latest/detection/annotators/#supervision.annotators.core.BoxAnnotator).
|
||||
|
||||
!!! tip
|
||||
|
||||
@@ -325,35 +418,102 @@ Keypoint tracking is currently supported via the conversion of `KeyPoints` to `D
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="5 7 14-15 17 33" }
|
||||
```{ .py hl_lines="8 13 19-20" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from ultralytics import YOLO
|
||||
|
||||
model = YOLO("yolov8m-pose.pt")
|
||||
edge_annotator = sv.EdgeAnnotator()
|
||||
vertex_annotator = sv.VertexAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model(frame)[0]
|
||||
key_points = sv.KeyPoints.from_ultralytics(results)
|
||||
detections = key_points.as_detections()
|
||||
|
||||
annotated_frame = edge_annotator.annotate(
|
||||
frame.copy(), key_points=key_points)
|
||||
annotated_frame = vertex_annotator.annotate(
|
||||
annotated_frame, key_points=key_points)
|
||||
return box_annotator.annotate(
|
||||
annotated_frame, detections=detections)
|
||||
|
||||
sv.process_video(
|
||||
source_path="skiing.mp4",
|
||||
target_path="result.mp4",
|
||||
callback=callback
|
||||
)
|
||||
```
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="9 14 20-21" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from inference.models.utils import get_roboflow_model
|
||||
|
||||
model = get_roboflow_model(
|
||||
model_id="yolov8m-pose-640", api_key=<ROBOFLOW API KEY>)
|
||||
edge_annotator = sv.EdgeAnnotator()
|
||||
vertex_annotator = sv.VertexAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model.infer(frame)[0]
|
||||
key_points = sv.KeyPoints.from_inference(results)
|
||||
detections = key_points.as_detections()
|
||||
|
||||
annotated_frame = edge_annotator.annotate(
|
||||
frame.copy(), key_points=key_points)
|
||||
annotated_frame = vertex_annotator.annotate(
|
||||
annotated_frame, key_points=key_points)
|
||||
return box_annotator.annotate(
|
||||
annotated_frame, detections=detections)
|
||||
|
||||
sv.process_video(
|
||||
source_path="skiing.mp4",
|
||||
target_path="result.mp4",
|
||||
callback=callback
|
||||
)
|
||||
```
|
||||
|
||||
<video controls>
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/track-keypoints-converted-to-detections.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
### Keypoint Tracking
|
||||
|
||||
Now that we have a `Detections` object, we can track it throughout the video. Utilizing Supervision’s [`sv.ByteTrack`](/latest/trackers/#supervision.tracker.byte_tracker.core.ByteTrack) functionality, each detected object is assigned a unique tracker ID, enabling the continuous following of the object's motion path across different frames. We shall visualize the result with `TraceAnnotator`.
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="10-11 17 25-26" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from ultralytics import YOLO
|
||||
|
||||
model = YOLO("yolov8m-pose.pt")
|
||||
edge_annotator = sv.EdgeAnnotator()
|
||||
vertex_annotator = sv.VertexAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
tracker = sv.ByteTrack()
|
||||
smoother = sv.DetectionsSmoother()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
trace_annotator = sv.TraceAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model(frame)[0]
|
||||
keypoints = sv.KeyPoints.from_ultralytics(results)
|
||||
detections = sv.keypoints_to_detections(keypoints)
|
||||
key_points = sv.KeyPoints.from_ultralytics(results)
|
||||
detections = key_points.as_detections()
|
||||
detections = tracker.update_with_detections(detections)
|
||||
detections = smoother.update_with_detections(detections)
|
||||
|
||||
labels = [
|
||||
f"#{tracker_id} {results.names[class_id]}"
|
||||
for class_id, tracker_id
|
||||
in zip(detections.class_id, detections.tracker_id)
|
||||
]
|
||||
|
||||
annotated_frame = edge_annotator.annotate(
|
||||
frame.copy(), key_points=key_points)
|
||||
annotated_frame = vertex_annotator.annotate(
|
||||
annotated_frame, key_points=key_points)
|
||||
annotated_frame = box_annotator.annotate(
|
||||
frame.copy(), detections=detections)
|
||||
annotated_frame = label_annotator.annotate(
|
||||
annotated_frame, detections=detections, labels=labels)
|
||||
annotated_frame, detections=detections)
|
||||
return trace_annotator.annotate(
|
||||
annotated_frame, detections=detections)
|
||||
|
||||
@@ -366,36 +526,32 @@ Keypoint tracking is currently supported via the conversion of `KeyPoints` to `D
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="5-6 8 15-16 18 34" }
|
||||
```{ .py hl_lines="11-12 18 26-27" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from inference.models.utils import get_roboflow_model
|
||||
|
||||
model = get_roboflow_model(
|
||||
model_id="yolov8m-pose-640", api_key=<ROBOFLOW API KEY>)
|
||||
edge_annotator = sv.EdgeAnnotator()
|
||||
vertex_annotator = sv.VertexAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
tracker = sv.ByteTrack()
|
||||
smoother = sv.DetectionsSmoother()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
trace_annotator = sv.TraceAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model.infer(frame)[0]
|
||||
keypoints = sv.KeyPoints.from_inference(results)
|
||||
detections = sv.keypoints_to_detections(keypoints)
|
||||
key_points = sv.KeyPoints.from_inference(results)
|
||||
detections = key_points.as_detections()
|
||||
detections = tracker.update_with_detections(detections)
|
||||
detections = smoother.update_with_detections(detections)
|
||||
|
||||
labels = [
|
||||
f"#{tracker_id} {results.names[class_id]}"
|
||||
for class_id, tracker_id
|
||||
in zip(detections.class_id, detections.tracker_id)
|
||||
]
|
||||
|
||||
annotated_frame = edge_annotator.annotate(
|
||||
frame.copy(), key_points=key_points)
|
||||
annotated_frame = vertex_annotator.annotate(
|
||||
annotated_frame, key_points=key_points)
|
||||
annotated_frame = box_annotator.annotate(
|
||||
frame.copy(), detections=detections)
|
||||
annotated_frame = label_annotator.annotate(
|
||||
annotated_frame, detections=detections, labels=labels)
|
||||
annotated_frame, detections=detections)
|
||||
return trace_annotator.annotate(
|
||||
annotated_frame, detections=detections)
|
||||
|
||||
@@ -407,9 +563,94 @@ Keypoint tracking is currently supported via the conversion of `KeyPoints` to `D
|
||||
```
|
||||
|
||||
<video controls>
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/track-keypoints.mp4" type="video/mp4">
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/track-keypoints-with-tracking.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
This structured walkthrough should give a detailed pathway to annotate videos
|
||||
effectively using Supervision’s various functionalities, including object tracking and
|
||||
trace annotations.
|
||||
### Bonus: Smoothing
|
||||
|
||||
We could stop here as we have successfully tracked the object detected by the keypoint model. However, we can further enhance the stability of the boxes by applying [`DetectionsSmoother`](/latest/detection/tools/smoother/). This tool helps in stabilizing the boxes by smoothing the bounding box coordinates across frames. It is very simple to use:
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="11 19" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from ultralytics import YOLO
|
||||
|
||||
model = YOLO("yolov8m-pose.pt")
|
||||
edge_annotator = sv.EdgeAnnotator()
|
||||
vertex_annotator = sv.VertexAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
tracker = sv.ByteTrack()
|
||||
smoother = sv.DetectionsSmoother()
|
||||
trace_annotator = sv.TraceAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model(frame)[0]
|
||||
key_points = sv.KeyPoints.from_ultralytics(results)
|
||||
detections = key_points.as_detections()
|
||||
detections = tracker.update_with_detections(detections)
|
||||
detections = smoother.update_with_detections(detections)
|
||||
|
||||
annotated_frame = edge_annotator.annotate(
|
||||
frame.copy(), key_points=key_points)
|
||||
annotated_frame = vertex_annotator.annotate(
|
||||
annotated_frame, key_points=key_points)
|
||||
annotated_frame = box_annotator.annotate(
|
||||
annotated_frame, detections=detections)
|
||||
return trace_annotator.annotate(
|
||||
annotated_frame, detections=detections)
|
||||
|
||||
sv.process_video(
|
||||
source_path="skiing.mp4",
|
||||
target_path="result.mp4",
|
||||
callback=callback
|
||||
)
|
||||
```
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="12 20" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from inference.models.utils import get_roboflow_model
|
||||
|
||||
model = get_roboflow_model(
|
||||
model_id="yolov8m-pose-640", api_key=<ROBOFLOW API KEY>)
|
||||
edge_annotator = sv.EdgeAnnotator()
|
||||
vertex_annotator = sv.VertexAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
tracker = sv.ByteTrack()
|
||||
smoother = sv.DetectionsSmoother()
|
||||
trace_annotator = sv.TraceAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model.infer(frame)[0]
|
||||
key_points = sv.KeyPoints.from_inference(results)
|
||||
detections = key_points.as_detections()
|
||||
detections = tracker.update_with_detections(detections)
|
||||
detections = smoother.update_with_detections(detections)
|
||||
|
||||
annotated_frame = edge_annotator.annotate(
|
||||
frame.copy(), key_points=key_points)
|
||||
annotated_frame = vertex_annotator.annotate(
|
||||
annotated_frame, key_points=key_points)
|
||||
annotated_frame = box_annotator.annotate(
|
||||
annotated_frame, detections=detections)
|
||||
return trace_annotator.annotate(
|
||||
annotated_frame, detections=detections)
|
||||
|
||||
sv.process_video(
|
||||
source_path="skiing.mp4",
|
||||
target_path="result.mp4",
|
||||
callback=callback
|
||||
)
|
||||
```
|
||||
|
||||
<video controls>
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/track-keypoints-with-smoothing.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
This structured walkthrough should give a detailed pathway to annotate videos effectively using Supervision’s various functionalities, including object tracking and trace annotations.
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Keypoint Detection
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Data Types Utils
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.utils.datatypes.keypoints_to_detections">keypoints_to_detections</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.utils.datatypes.keypoints_to_detections
|
||||
@@ -80,7 +80,6 @@ nav:
|
||||
- File: utils/file.md
|
||||
- Draw: utils/draw.md
|
||||
- Geometry: utils/geometry.md
|
||||
- Datatypes: utils/datatypes.md
|
||||
- Assets: assets.md
|
||||
- Cookbooks: cookbooks.md
|
||||
- Cheatsheet: https://roboflow.github.io/cheatsheet-supervision/
|
||||
|
||||
@@ -100,7 +100,6 @@ from supervision.keypoint.core import KeyPoints
|
||||
from supervision.metrics.detection import ConfusionMatrix, MeanAveragePrecision
|
||||
from supervision.tracker.byte_tracker.core import ByteTrack
|
||||
from supervision.utils.conversion import cv2_to_pillow, pillow_to_cv2
|
||||
from supervision.utils.datatypes import keypoints_to_detections
|
||||
from supervision.utils.file import list_files_with_extensions
|
||||
from supervision.utils.image import (
|
||||
ImageSink,
|
||||
|
||||
@@ -42,6 +42,10 @@ class LineZone:
|
||||
to inside.
|
||||
out_count (int): The number of objects that have crossed the line from inside
|
||||
to outside.
|
||||
in_count_per_class (Dict[int, int]): Number of objects of each class that have
|
||||
crossed the line from outside to inside.
|
||||
out_count_per_class (Dict[int, int]): Number of objects of each class that have
|
||||
crossed the line from inside to outside.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@@ -75,7 +79,7 @@ class LineZone:
|
||||
Position.BOTTOM_LEFT,
|
||||
Position.BOTTOM_RIGHT,
|
||||
),
|
||||
crossing_acceptance_threshold: int = 1,
|
||||
minimum_crossing_threshold: int = 1,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
@@ -86,7 +90,7 @@ class LineZone:
|
||||
to consider when deciding on whether the detection
|
||||
has passed the line counter or not. By default, this
|
||||
contains the four corners of the detection's bounding box
|
||||
crossing_acceptance_threshold (int): Detection needs to be seen
|
||||
minimum_crossing_threshold (int): Detection needs to be seen
|
||||
on the other side of the line for this many frames to be
|
||||
considered as having crossed the line. This is useful when
|
||||
dealing with unstable bounding boxes or when detections
|
||||
@@ -94,7 +98,7 @@ class LineZone:
|
||||
"""
|
||||
self.vector = Vector(start=start, end=end)
|
||||
self.limits = self._calculate_region_of_interest_limits(vector=self.vector)
|
||||
self.crossing_history_length = max(2, crossing_acceptance_threshold + 1)
|
||||
self.crossing_history_length = max(2, minimum_crossing_threshold + 1)
|
||||
self.crossing_state_history: Dict[int, Deque[bool]] = defaultdict(
|
||||
lambda: deque(maxlen=self.crossing_history_length)
|
||||
)
|
||||
@@ -107,34 +111,18 @@ class LineZone:
|
||||
|
||||
@property
|
||||
def in_count(self) -> int:
|
||||
"""
|
||||
Number of objects that have crossed the line from
|
||||
outside to inside.
|
||||
"""
|
||||
return sum(self._in_count_per_class.values())
|
||||
|
||||
@property
|
||||
def out_count(self) -> int:
|
||||
"""
|
||||
Number of objects that have crossed the line from
|
||||
inside to outside.
|
||||
"""
|
||||
return sum(self._out_count_per_class.values())
|
||||
|
||||
@property
|
||||
def in_count_per_class(self) -> Dict[int, int]:
|
||||
"""
|
||||
Number of objects of each class that have crossed
|
||||
the line from outside to inside.
|
||||
"""
|
||||
return dict(self._in_count_per_class)
|
||||
|
||||
@property
|
||||
def out_count_per_class(self) -> Dict[int, int]:
|
||||
"""
|
||||
Number of objects of each class that have crossed the line
|
||||
from inside to outside.
|
||||
"""
|
||||
return dict(self._out_count_per_class)
|
||||
|
||||
def trigger(self, detections: Detections) -> Tuple[np.ndarray, np.ndarray]:
|
||||
|
||||
@@ -919,11 +919,27 @@ def merge_metadata(metadata_list: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
merged_metadata: Dict[str, Any] = {}
|
||||
for metadata in metadata_list:
|
||||
for key, value in metadata.items():
|
||||
if key in merged_metadata:
|
||||
if key not in merged_metadata:
|
||||
merged_metadata[key] = value
|
||||
continue
|
||||
|
||||
other_value = merged_metadata[key]
|
||||
if isinstance(value, np.ndarray) and isinstance(other_value, np.ndarray):
|
||||
if not np.array_equal(merged_metadata[key], value):
|
||||
raise ValueError(
|
||||
f"Conflicting metadata for key: '{key}': "
|
||||
"{type(value)}, {type(other_value)}."
|
||||
)
|
||||
elif isinstance(value, np.ndarray) or isinstance(other_value, np.ndarray):
|
||||
# Since [] == np.array([]).
|
||||
raise ValueError(
|
||||
f"Conflicting metadata for key: '{key}': "
|
||||
"{type(value)}, {type(other_value)}."
|
||||
)
|
||||
else:
|
||||
print("hm")
|
||||
if merged_metadata[key] != value:
|
||||
raise ValueError(f"Conflicting metadata for key: '{key}'.")
|
||||
else:
|
||||
merged_metadata[key] = value
|
||||
|
||||
return merged_metadata
|
||||
|
||||
|
||||
@@ -2,12 +2,13 @@ from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
|
||||
from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from supervision.config import CLASS_NAME_DATA_FIELD
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.detection.utils import get_data_item, is_data_equal
|
||||
from supervision.validators import validate_keypoints_fields
|
||||
|
||||
@@ -620,3 +621,67 @@ class KeyPoints:
|
||||
empty_keypoints = KeyPoints.empty()
|
||||
empty_keypoints.data = self.data
|
||||
return self == empty_keypoints
|
||||
|
||||
def as_detections(
|
||||
self, selected_keypoint_indices: Optional[Iterable[int]] = None
|
||||
) -> Detections:
|
||||
"""
|
||||
Convert a KeyPoints object to a Detections object. This
|
||||
approximates the bounding box of the detected object by
|
||||
taking the bounding box that fits all keypoints.
|
||||
|
||||
Arguments:
|
||||
selected_keypoint_indices (Optional[Iterable[int]]): The
|
||||
indices of the keypoints to include in the bounding box
|
||||
calculation. This helps focus on a subset of keypoints,
|
||||
e.g. when some are occluded. Captures all keypoints by default.
|
||||
|
||||
Returns:
|
||||
detections (Detections): The converted detections object.
|
||||
|
||||
Example:
|
||||
```python
|
||||
keypoints = sv.KeyPoints.from_inference(...)
|
||||
detections = keypoints.as_detections()
|
||||
```
|
||||
"""
|
||||
if self.is_empty():
|
||||
return Detections.empty()
|
||||
|
||||
detections_list = []
|
||||
for i, xy in enumerate(self.xy):
|
||||
if selected_keypoint_indices:
|
||||
xy = xy[selected_keypoint_indices]
|
||||
|
||||
# [0, 0] used by some frameworks to indicate missing keypoints
|
||||
xy = xy[~np.all(xy == 0, axis=1)]
|
||||
if len(xy) == 0:
|
||||
xyxy = np.array([[0, 0, 0, 0]], dtype=np.float32)
|
||||
else:
|
||||
x_min = xy[:, 0].min()
|
||||
x_max = xy[:, 0].max()
|
||||
y_min = xy[:, 1].min()
|
||||
y_max = xy[:, 1].max()
|
||||
xyxy = np.array([[x_min, y_min, x_max, y_max]], dtype=np.float32)
|
||||
|
||||
if self.confidence is None:
|
||||
confidence = None
|
||||
else:
|
||||
confidence = self.confidence[i]
|
||||
if selected_keypoint_indices:
|
||||
confidence = confidence[selected_keypoint_indices]
|
||||
confidence = np.array([confidence.mean()], dtype=np.float32)
|
||||
|
||||
detections_list.append(
|
||||
Detections(
|
||||
xyxy=xyxy,
|
||||
confidence=confidence,
|
||||
)
|
||||
)
|
||||
|
||||
detections = Detections.merge(detections_list)
|
||||
detections.class_id = self.class_id
|
||||
detections.data = self.data
|
||||
detections = detections[detections.area > 0]
|
||||
|
||||
return detections
|
||||
@@ -47,10 +47,30 @@ class F1Score(Metric):
|
||||
f1_metric = F1Score()
|
||||
f1_result = f1_metric.update(predictions, targets).compute()
|
||||
|
||||
print(f1_result)
|
||||
print(f1_result.f1_50)
|
||||
print(f1_result.small_objects.f1_50)
|
||||
# 0.7618
|
||||
|
||||
print(f1_result)
|
||||
# F1ScoreResult:
|
||||
# Metric target: MetricTarget.BOXES
|
||||
# Averaging method: AveragingMethod.WEIGHTED
|
||||
# F1 @ 50: 0.7618
|
||||
# F1 @ 75: 0.7487
|
||||
# F1 @ thresh: [0.76175 0.76068 0.76068]
|
||||
# IoU thresh: [0.5 0.55 0.6 ...]
|
||||
# F1 per class:
|
||||
# 0: [0.70968 0.70968 0.70968 ...]
|
||||
# ...
|
||||
# Small objects: ...
|
||||
# Medium objects: ...
|
||||
# Large objects: ...
|
||||
|
||||
f1_result.plot()
|
||||
```
|
||||
|
||||
{ align=center width="800" }
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -456,11 +476,11 @@ class F1ScoreResult:
|
||||
matched_classes (np.ndarray): the class IDs of all matched classes.
|
||||
Corresponds to the rows of `f1_per_class`.
|
||||
small_objects (Optional[F1ScoreResult]): the F1 metric results
|
||||
for small objects.
|
||||
for small objects (area < 32²).
|
||||
medium_objects (Optional[F1ScoreResult]): the F1 metric results
|
||||
for medium objects.
|
||||
for medium objects (32² ≤ area < 96²).
|
||||
large_objects (Optional[F1ScoreResult]): the F1 metric results
|
||||
for large objects.
|
||||
for large objects (area ≥ 96²).
|
||||
"""
|
||||
|
||||
metric_target: MetricTarget
|
||||
@@ -490,6 +510,19 @@ class F1ScoreResult:
|
||||
Example:
|
||||
```python
|
||||
print(f1_result)
|
||||
# F1ScoreResult:
|
||||
# Metric target: MetricTarget.BOXES
|
||||
# Averaging method: AveragingMethod.WEIGHTED
|
||||
# F1 @ 50: 0.7618
|
||||
# F1 @ 75: 0.7487
|
||||
# F1 @ thresh: [0.76175 0.76068 0.76068]
|
||||
# IoU thresh: [0.5 0.55 0.6 ...]
|
||||
# F1 per class:
|
||||
# 0: [0.70968 0.70968 0.70968 ...]
|
||||
# ...
|
||||
# Small objects: ...
|
||||
# Medium objects: ...
|
||||
# Large objects: ...
|
||||
```
|
||||
"""
|
||||
out_str = (
|
||||
@@ -553,6 +586,10 @@ class F1ScoreResult:
|
||||
def plot(self):
|
||||
"""
|
||||
Plot the F1 results.
|
||||
|
||||
{ align=center width="800" }
|
||||
"""
|
||||
|
||||
labels = ["F1@50", "F1@75"]
|
||||
|
||||
@@ -42,10 +42,31 @@ class MeanAveragePrecision(Metric):
|
||||
map_metric = MeanAveragePrecision()
|
||||
map_result = map_metric.update(predictions, targets).compute()
|
||||
|
||||
print(map_result)
|
||||
print(map_result.map50_95)
|
||||
# 0.4674
|
||||
|
||||
print(map_result)
|
||||
# MeanAveragePrecisionResult:
|
||||
# Metric target: MetricTarget.BOXES
|
||||
# Class agnostic: False
|
||||
# mAP @ 50:95: 0.4674
|
||||
# mAP @ 50: 0.5048
|
||||
# mAP @ 75: 0.4796
|
||||
# mAP scores: [0.50485 0.50377 0.50377 ...]
|
||||
# IoU thresh: [0.5 0.55 0.6 ...]
|
||||
# AP per class:
|
||||
# 0: [0.67699 0.67699 0.67699 ...]
|
||||
# ...
|
||||
# Small objects: ...
|
||||
# Medium objects: ...
|
||||
# Large objects: ...
|
||||
|
||||
map_result.plot()
|
||||
```
|
||||
|
||||
{ align=center width="800" }
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -419,11 +440,11 @@ class MeanAveragePrecisionResult:
|
||||
matched_classes (np.ndarray): the class IDs of all matched classes.
|
||||
Corresponds to the rows of `ap_per_class`.
|
||||
small_objects (Optional[MeanAveragePrecisionResult]): the mAP results
|
||||
for small objects.
|
||||
for small objects (area < 32²).
|
||||
medium_objects (Optional[MeanAveragePrecisionResult]): the mAP results
|
||||
for medium objects.
|
||||
for medium objects (32² ≤ area < 96²).
|
||||
large_objects (Optional[MeanAveragePrecisionResult]): the mAP results
|
||||
for large objects.
|
||||
for large objects (area ≥ 96²).
|
||||
"""
|
||||
|
||||
metric_target: MetricTarget
|
||||
@@ -456,6 +477,20 @@ class MeanAveragePrecisionResult:
|
||||
Example:
|
||||
```python
|
||||
print(map_result)
|
||||
# MeanAveragePrecisionResult:
|
||||
# Metric target: MetricTarget.BOXES
|
||||
# Class agnostic: False
|
||||
# mAP @ 50:95: 0.4674
|
||||
# mAP @ 50: 0.5048
|
||||
# mAP @ 75: 0.4796
|
||||
# mAP scores: [0.50485 0.50377 0.50377 ...]
|
||||
# IoU thresh: [0.5 0.55 0.6 ...]
|
||||
# AP per class:
|
||||
# 0: [0.67699 0.67699 0.67699 ...]
|
||||
# ...
|
||||
# Small objects: ...
|
||||
# Medium objects: ...
|
||||
# Large objects: ...
|
||||
```
|
||||
"""
|
||||
|
||||
@@ -527,6 +562,10 @@ class MeanAveragePrecisionResult:
|
||||
def plot(self):
|
||||
"""
|
||||
Plot the mAP results.
|
||||
|
||||
{ align=center width="800" }
|
||||
"""
|
||||
|
||||
labels = ["mAP@50:95", "mAP@50", "mAP@75"]
|
||||
|
||||
@@ -50,10 +50,30 @@ class Precision(Metric):
|
||||
precision_metric = Precision()
|
||||
precision_result = precision_metric.update(predictions, targets).compute()
|
||||
|
||||
print(precision_result)
|
||||
print(precision_result.precision_at_50)
|
||||
# 0.8099
|
||||
|
||||
print(precision_result)
|
||||
# PrecisionResult:
|
||||
# Metric target: MetricTarget.BOXES
|
||||
# Averaging method: AveragingMethod.WEIGHTED
|
||||
# P @ 50: 0.8099
|
||||
# P @ 75: 0.7969
|
||||
# P @ thresh: [0.80992 0.80905 0.80905 ...]
|
||||
# IoU thresh: [0.5 0.55 0.6 ...]
|
||||
# Precision per class:
|
||||
# 0: [0.64706 0.64706 0.64706 ...]
|
||||
# ...
|
||||
# Small objects: ...
|
||||
# Medium objects: ...
|
||||
# Large objects: ...
|
||||
|
||||
print(precision_result.small_objects.precision_at_50)
|
||||
```
|
||||
|
||||
{ align=center width="800" }
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -459,11 +479,11 @@ class PrecisionResult:
|
||||
matched_classes (np.ndarray): the class IDs of all matched classes.
|
||||
Corresponds to the rows of `precision_per_class`.
|
||||
small_objects (Optional[PrecisionResult]): the Precision metric results
|
||||
for small objects.
|
||||
for small objects (area < 32²).
|
||||
medium_objects (Optional[PrecisionResult]): the Precision metric results
|
||||
for medium objects.
|
||||
for medium objects (32² ≤ area < 96²).
|
||||
large_objects (Optional[PrecisionResult]): the Precision metric results
|
||||
for large objects.
|
||||
for large objects (area ≥ 96²).
|
||||
"""
|
||||
|
||||
metric_target: MetricTarget
|
||||
@@ -493,6 +513,19 @@ class PrecisionResult:
|
||||
Example:
|
||||
```python
|
||||
print(precision_result)
|
||||
# PrecisionResult:
|
||||
# Metric target: MetricTarget.BOXES
|
||||
# Averaging method: AveragingMethod.WEIGHTED
|
||||
# P @ 50: 0.8099
|
||||
# P @ 75: 0.7969
|
||||
# P @ thresh: [0.80992 0.80905 0.80905 ...]
|
||||
# IoU thresh: [0.5 0.55 0.6 ...]
|
||||
# Precision per class:
|
||||
# 0: [0.64706 0.64706 0.64706 ...]
|
||||
# ...
|
||||
# Small objects: ...
|
||||
# Medium objects: ...
|
||||
# Large objects: ...
|
||||
```
|
||||
"""
|
||||
out_str = (
|
||||
@@ -558,6 +591,10 @@ class PrecisionResult:
|
||||
def plot(self):
|
||||
"""
|
||||
Plot the precision results.
|
||||
|
||||
{ align=center width="800" }
|
||||
"""
|
||||
|
||||
labels = ["Precision@50", "Precision@75"]
|
||||
|
||||
@@ -50,10 +50,31 @@ class Recall(Metric):
|
||||
recall_metric = Recall()
|
||||
recall_result = recall_metric.update(predictions, targets).compute()
|
||||
|
||||
print(recall_result)
|
||||
print(recall_result.recall_at_50)
|
||||
print(recall_result.small_objects.recall_at_50)
|
||||
# 0.7615
|
||||
|
||||
print(recall_result)
|
||||
# RecallResult:
|
||||
# Metric target: MetricTarget.BOXES
|
||||
# Averaging method: AveragingMethod.WEIGHTED
|
||||
# R @ 50: 0.7615
|
||||
# R @ 75: 0.7462
|
||||
# R @ thresh: [0.76151 0.76011 0.76011 0.75732 ...]
|
||||
# IoU thresh: [0.5 0.55 0.6 ...]
|
||||
# Recall per class:
|
||||
# 0: [0.78571 0.78571 0.78571 ...]
|
||||
# ...
|
||||
# Small objects: ...
|
||||
# Medium objects: ...
|
||||
# Large objects: ...
|
||||
|
||||
recall_result.plot()
|
||||
|
||||
```
|
||||
|
||||
{ align=center width="800" }
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -457,11 +478,11 @@ class RecallResult:
|
||||
matched_classes (np.ndarray): the class IDs of all matched classes.
|
||||
Corresponds to the rows of `recall_per_class`.
|
||||
small_objects (Optional[RecallResult]): the Recall metric results
|
||||
for small objects.
|
||||
for small objects (area < 32²).
|
||||
medium_objects (Optional[RecallResult]): the Recall metric results
|
||||
for medium objects.
|
||||
for medium objects (32² ≤ area < 96²).
|
||||
large_objects (Optional[RecallResult]): the Recall metric results
|
||||
for large objects.
|
||||
for large objects (area ≥ 96²).
|
||||
"""
|
||||
|
||||
metric_target: MetricTarget
|
||||
@@ -491,6 +512,19 @@ class RecallResult:
|
||||
Example:
|
||||
```python
|
||||
print(recall_result)
|
||||
# RecallResult:
|
||||
# Metric target: MetricTarget.BOXES
|
||||
# Averaging method: AveragingMethod.WEIGHTED
|
||||
# R @ 50: 0.7615
|
||||
# R @ 75: 0.7462
|
||||
# R @ thresh: [0.76151 0.76011 0.76011 0.75732 ...]
|
||||
# IoU thresh: [0.5 0.55 0.6 ...]
|
||||
# Recall per class:
|
||||
# 0: [0.78571 0.78571 0.78571 ...]
|
||||
# ...
|
||||
# Small objects: ...
|
||||
# Medium objects: ...
|
||||
# Large objects: ...
|
||||
```
|
||||
"""
|
||||
out_str = (
|
||||
@@ -556,6 +590,10 @@ class RecallResult:
|
||||
def plot(self):
|
||||
"""
|
||||
Plot the recall results.
|
||||
|
||||
{ align=center width="800" }
|
||||
"""
|
||||
|
||||
labels = ["Recall@50", "Recall@75"]
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
from typing import Iterable, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.keypoint.core import KeyPoints
|
||||
|
||||
|
||||
def keypoints_to_detections(
|
||||
keypoints: KeyPoints, selected_keypoint_indices: Optional[Iterable[int]] = None
|
||||
) -> Detections:
|
||||
"""
|
||||
Convert a KeyPoints object to a Detections object. This
|
||||
approximates the bounding box of the detected object by
|
||||
taking the bounding box that fits all keypoints.
|
||||
|
||||
Arguments:
|
||||
keypoints (KeyPoints): The keypoints to convert to detections.
|
||||
selected_keypoint_indices (Optional[Iterable[int]]): The
|
||||
indices of the keypoints to include in the bounding box
|
||||
calculation. This helps focus on a subset of keypoints,
|
||||
e.g. when some are occluded. Captures all keypoints by default.
|
||||
|
||||
Returns:
|
||||
detections (Detections): The converted detections object.
|
||||
|
||||
Example:
|
||||
```python
|
||||
keypoints = sv.KeyPoints.from_inference(...)
|
||||
detections = keypoints_to_detections(keypoints)
|
||||
```
|
||||
"""
|
||||
if keypoints.is_empty():
|
||||
return Detections.empty()
|
||||
|
||||
detections_list = []
|
||||
for i, xy in enumerate(keypoints.xy):
|
||||
if selected_keypoint_indices:
|
||||
xy = xy[selected_keypoint_indices]
|
||||
|
||||
# [0, 0] used by some frameworks to indicate missing keypoints
|
||||
xy = xy[~np.all(xy == 0, axis=1)]
|
||||
if len(xy) == 0:
|
||||
xyxy = np.array([[0, 0, 0, 0]], dtype=np.float32)
|
||||
else:
|
||||
x_min = xy[:, 0].min()
|
||||
x_max = xy[:, 0].max()
|
||||
y_min = xy[:, 1].min()
|
||||
y_max = xy[:, 1].max()
|
||||
xyxy = np.array([[x_min, y_min, x_max, y_max]], dtype=np.float32)
|
||||
|
||||
if keypoints.confidence is None:
|
||||
confidence = None
|
||||
else:
|
||||
confidence = keypoints.confidence[i]
|
||||
if selected_keypoint_indices:
|
||||
confidence = confidence[selected_keypoint_indices]
|
||||
confidence = np.array([confidence.mean()], dtype=np.float32)
|
||||
|
||||
detections_list.append(
|
||||
Detections(
|
||||
xyxy=xyxy,
|
||||
confidence=confidence,
|
||||
)
|
||||
)
|
||||
|
||||
detections = Detections.merge(detections_list)
|
||||
detections.class_id = keypoints.class_id
|
||||
detections.data = keypoints.data
|
||||
detections = detections[detections.area > 0]
|
||||
|
||||
return detections
|
||||
+205
-2
@@ -106,6 +106,26 @@ TEST_DET_DIFFERENT_DATA = Detections(
|
||||
"never_seen_key": [9],
|
||||
},
|
||||
)
|
||||
TEST_DET_WITH_METADATA = Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=np.array([1]),
|
||||
metadata={"source": "camera1"},
|
||||
)
|
||||
|
||||
TEST_DET_WITH_METADATA_2 = Detections(
|
||||
xyxy=np.array([[30, 30, 40, 40]]),
|
||||
class_id=np.array([2]),
|
||||
metadata={"source": "camera1"},
|
||||
)
|
||||
TEST_DET_NO_METADATA = Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=np.array([1]),
|
||||
)
|
||||
TEST_DET_DIFFERENT_METADATA = Detections(
|
||||
xyxy=np.array([[50, 50, 60, 60]]),
|
||||
class_id=np.array([3]),
|
||||
metadata={"source": "camera2"},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -258,6 +278,11 @@ def test_getitem(
|
||||
TEST_DET_1,
|
||||
DoesNotRaise(),
|
||||
), # Single detection and empty-array fields
|
||||
(
|
||||
[TEST_DET_ZERO_LENGTH, TEST_DET_ZERO_LENGTH],
|
||||
TEST_DET_ZERO_LENGTH,
|
||||
DoesNotRaise(),
|
||||
), # Zero-length fields across all Detections
|
||||
(
|
||||
[
|
||||
TEST_DET_1,
|
||||
@@ -287,12 +312,190 @@ def test_getitem(
|
||||
Detections.empty(),
|
||||
],
|
||||
mock_detections(
|
||||
xyxy=[[10, 10, 20, 20]],
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=[1],
|
||||
mask=[np.zeros((4, 4), dtype=bool)],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # Segmentation + Empty
|
||||
# Metadata
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=np.array([1]),
|
||||
metadata={"source": "camera1"},
|
||||
),
|
||||
Detections.empty(),
|
||||
],
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=np.array([1]),
|
||||
metadata={"source": "camera1"},
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # Metadata merge with empty detections
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=np.array([1]),
|
||||
metadata={"source": "camera1"},
|
||||
),
|
||||
Detections(xyxy=np.array([[30, 30, 40, 40]]), class_id=np.array([2])),
|
||||
],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
), # Empty and non-empty metadata
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=np.array([1]),
|
||||
metadata={"source": "camera1"},
|
||||
)
|
||||
],
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=np.array([1]),
|
||||
metadata={"source": "camera1"},
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # Single detection with metadata
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=np.array([1]),
|
||||
metadata={"source": "camera1"},
|
||||
),
|
||||
Detections(
|
||||
xyxy=np.array([[30, 30, 40, 40]]),
|
||||
class_id=np.array([2]),
|
||||
metadata={"source": "camera1"},
|
||||
),
|
||||
],
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20], [30, 30, 40, 40]]),
|
||||
class_id=np.array([1, 2]),
|
||||
metadata={"source": "camera1"},
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # Multiple metadata entries with identical values
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=np.array([1]),
|
||||
metadata={"source": "camera1"},
|
||||
),
|
||||
Detections(
|
||||
xyxy=np.array([[50, 50, 60, 60]]),
|
||||
class_id=np.array([3]),
|
||||
metadata={"source": "camera2"},
|
||||
),
|
||||
],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
), # Different metadata values
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
metadata={"source": "camera1", "resolution": "1080p"},
|
||||
),
|
||||
Detections(
|
||||
xyxy=np.array([[30, 30, 40, 40]]),
|
||||
metadata={"source": "camera1", "resolution": "1080p"},
|
||||
),
|
||||
],
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20], [30, 30, 40, 40]]),
|
||||
metadata={"source": "camera1", "resolution": "1080p"},
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # Large metadata with multiple identical entries
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]), metadata={"source": "camera1"}
|
||||
),
|
||||
Detections(
|
||||
xyxy=np.array([[30, 30, 40, 40]]), metadata={"source": ["camera1"]}
|
||||
),
|
||||
],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
), # Inconsistent types in metadata values
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]), metadata={"source": "camera1"}
|
||||
),
|
||||
Detections(
|
||||
xyxy=np.array([[30, 30, 40, 40]]), metadata={"location": "indoor"}
|
||||
),
|
||||
],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
), # Metadata key mismatch
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
metadata={
|
||||
"source": "camera1",
|
||||
"settings": {"resolution": "1080p", "fps": 30},
|
||||
},
|
||||
),
|
||||
Detections(
|
||||
xyxy=np.array([[30, 30, 40, 40]]),
|
||||
metadata={
|
||||
"source": "camera1",
|
||||
"settings": {"resolution": "1080p", "fps": 30},
|
||||
},
|
||||
),
|
||||
],
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20], [30, 30, 40, 40]]),
|
||||
metadata={
|
||||
"source": "camera1",
|
||||
"settings": {"resolution": "1080p", "fps": 30},
|
||||
},
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # multi-field metadata
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
metadata={"calibration_matrix": np.array([[1, 0], [0, 1]])},
|
||||
),
|
||||
Detections(
|
||||
xyxy=np.array([[30, 30, 40, 40]]),
|
||||
metadata={"calibration_matrix": np.array([[1, 0], [0, 1]])},
|
||||
),
|
||||
],
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20], [30, 30, 40, 40]]),
|
||||
metadata={"calibration_matrix": np.array([[1, 0], [0, 1]])},
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # Identical 2D numpy arrays in metadata
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
metadata={"calibration_matrix": np.array([[1, 0], [0, 1]])},
|
||||
),
|
||||
Detections(
|
||||
xyxy=np.array([[30, 30, 40, 40]]),
|
||||
metadata={"calibration_matrix": np.array([[2, 0], [0, 2]])},
|
||||
),
|
||||
],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
), # Mismatching 2D numpy arrays in metadata
|
||||
],
|
||||
)
|
||||
def test_merge(
|
||||
@@ -302,7 +505,7 @@ def test_merge(
|
||||
) -> None:
|
||||
with exception:
|
||||
result = Detections.merge(detections_list=detections_list)
|
||||
assert result == expected_result
|
||||
assert result == expected_result, f"Expected: {expected_result}, Got: {result}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -493,7 +493,7 @@ def test_line_zone_multiple_detections(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"vector, xyxy_sequence, triggering_anchors, crossing_acceptance_threshold, "
|
||||
"vector, xyxy_sequence, triggering_anchors, minimum_crossing_threshold, "
|
||||
"expected_crossed_in, expected_crossed_out",
|
||||
[
|
||||
( # Detection lingers around line, all crosses counted
|
||||
@@ -578,7 +578,7 @@ def test_line_zone_one_detection_long_horizon(
|
||||
vector: Vector,
|
||||
xyxy_sequence: List[List[float]],
|
||||
triggering_anchors: List[Position],
|
||||
crossing_acceptance_threshold: int,
|
||||
minimum_crossing_threshold: int,
|
||||
expected_crossed_in: List[bool],
|
||||
expected_crossed_out: List[bool],
|
||||
) -> None:
|
||||
@@ -586,7 +586,7 @@ def test_line_zone_one_detection_long_horizon(
|
||||
start=vector.start,
|
||||
end=vector.end,
|
||||
triggering_anchors=triggering_anchors,
|
||||
crossing_acceptance_threshold=crossing_acceptance_threshold,
|
||||
minimum_crossing_threshold=minimum_crossing_threshold,
|
||||
)
|
||||
|
||||
crossed_in_list = []
|
||||
@@ -609,7 +609,7 @@ def test_line_zone_one_detection_long_horizon(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"vector, xyxy_sequence, anchors, crossing_acceptance_threshold, "
|
||||
"vector, xyxy_sequence, anchors, minimum_crossing_threshold, "
|
||||
"expected_crossed_in, expected_crossed_out, expected_count_in, "
|
||||
"expected_count_out, exception",
|
||||
[
|
||||
@@ -743,7 +743,7 @@ def test_line_zone_long_horizon_disappearing_detections(
|
||||
vector: Vector,
|
||||
xyxy_sequence: List[List[Optional[List[float]]]],
|
||||
anchors: List[Position],
|
||||
crossing_acceptance_threshold: int,
|
||||
minimum_crossing_threshold: int,
|
||||
expected_crossed_in: List[List[bool]],
|
||||
expected_crossed_out: List[List[bool]],
|
||||
expected_count_in: List[int],
|
||||
@@ -755,7 +755,7 @@ def test_line_zone_long_horizon_disappearing_detections(
|
||||
start=vector.start,
|
||||
end=vector.end,
|
||||
triggering_anchors=anchors,
|
||||
crossing_acceptance_threshold=crossing_acceptance_threshold,
|
||||
minimum_crossing_threshold=minimum_crossing_threshold,
|
||||
)
|
||||
crossed_in_list = []
|
||||
crossed_out_list = []
|
||||
|
||||
@@ -14,6 +14,7 @@ from supervision.detection.utils import (
|
||||
filter_polygons_by_area,
|
||||
get_data_item,
|
||||
merge_data,
|
||||
merge_metadata,
|
||||
move_boxes,
|
||||
process_roboflow_result,
|
||||
scale_boxes,
|
||||
@@ -1138,3 +1139,163 @@ def test_xywh_to_xyxy(xywh: np.ndarray, expected_result: np.ndarray) -> None:
|
||||
def test_xcycwh_to_xyxy(xcycwh: np.ndarray, expected_result: np.ndarray) -> None:
|
||||
result = xcycwh_to_xyxy(xcycwh)
|
||||
np.testing.assert_array_equal(result, expected_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"metadata_list, expected_result, exception",
|
||||
[
|
||||
# Identical metadata with a single key
|
||||
([{"key1": "value1"}, {"key1": "value1"}], {"key1": "value1"}, DoesNotRaise()),
|
||||
# Identical metadata with multiple keys
|
||||
(
|
||||
[
|
||||
{"key1": "value1", "key2": "value2"},
|
||||
{"key1": "value1", "key2": "value2"},
|
||||
],
|
||||
{"key1": "value1", "key2": "value2"},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Conflicting values for the same key
|
||||
([{"key1": "value1"}, {"key1": "value2"}], None, pytest.raises(ValueError)),
|
||||
# Different sets of keys across dictionaries
|
||||
([{"key1": "value1"}, {"key2": "value2"}], None, pytest.raises(ValueError)),
|
||||
# Empty metadata list
|
||||
([], {}, DoesNotRaise()),
|
||||
# Empty metadata dictionaries
|
||||
([{}, {}], {}, DoesNotRaise()),
|
||||
# Different declaration order for keys
|
||||
(
|
||||
[
|
||||
{"key1": "value1", "key2": "value2"},
|
||||
{"key2": "value2", "key1": "value1"},
|
||||
],
|
||||
{"key1": "value1", "key2": "value2"},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Nested metadata dictionaries
|
||||
(
|
||||
[{"key1": {"sub_key": "sub_value"}}, {"key1": {"sub_key": "sub_value"}}],
|
||||
{"key1": {"sub_key": "sub_value"}},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Large metadata dictionaries with many keys
|
||||
(
|
||||
[
|
||||
{f"key{i}": f"value{i}" for i in range(100)},
|
||||
{f"key{i}": f"value{i}" for i in range(100)},
|
||||
],
|
||||
{f"key{i}": f"value{i}" for i in range(100)},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Mixed types in list metadata values
|
||||
(
|
||||
[{"key1": ["value1", 2, True]}, {"key1": ["value1", 2, True]}],
|
||||
{"key1": ["value1", 2, True]},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Identical lists across metadata dictionaries
|
||||
(
|
||||
[{"key1": [1, 2, 3]}, {"key1": [1, 2, 3]}],
|
||||
{"key1": [1, 2, 3]},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Identical numpy arrays across metadata dictionaries
|
||||
(
|
||||
[{"key1": np.array([1, 2, 3])}, {"key1": np.array([1, 2, 3])}],
|
||||
{"key1": np.array([1, 2, 3])},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Identical numpy arrays across metadata dictionaries, different datatype
|
||||
(
|
||||
[
|
||||
{"key1": np.array([1, 2, 3], dtype=np.int32)},
|
||||
{"key1": np.array([1, 2, 3], dtype=np.int64)},
|
||||
],
|
||||
{"key1": np.array([1, 2, 3])},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Conflicting lists for the same key
|
||||
([{"key1": [1, 2, 3]}, {"key1": [4, 5, 6]}], None, pytest.raises(ValueError)),
|
||||
# Conflicting numpy arrays for the same key
|
||||
(
|
||||
[{"key1": np.array([1, 2, 3])}, {"key1": np.array([4, 5, 6])}],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
),
|
||||
# Mixed data types: list and numpy array for the same key
|
||||
(
|
||||
[{"key1": [1, 2, 3]}, {"key1": np.array([1, 2, 3])}],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
),
|
||||
# Empty lists and numpy arrays for the same key
|
||||
([{"key1": []}, {"key1": np.array([])}], None, pytest.raises(ValueError)),
|
||||
# Identical multi-dimensional lists across metadata dictionaries
|
||||
(
|
||||
[{"key1": [[1, 2], [3, 4]]}, {"key1": [[1, 2], [3, 4]]}],
|
||||
{"key1": [[1, 2], [3, 4]]},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Identical multi-dimensional numpy arrays across metadata dictionaries
|
||||
(
|
||||
[
|
||||
{"key1": np.arange(4).reshape(2, 2)},
|
||||
{"key1": np.arange(4).reshape(2, 2)},
|
||||
],
|
||||
{"key1": np.arange(4).reshape(2, 2)},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Conflicting multi-dimensional lists for the same key
|
||||
(
|
||||
[{"key1": [[1, 2], [3, 4]]}, {"key1": [[5, 6], [7, 8]]}],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
),
|
||||
# Conflicting multi-dimensional numpy arrays for the same key
|
||||
(
|
||||
[
|
||||
{"key1": np.arange(4).reshape(2, 2)},
|
||||
{"key1": np.arange(4, 8).reshape(2, 2)},
|
||||
],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
),
|
||||
# Mixed types with multi-dimensional list and array for the same key
|
||||
(
|
||||
[{"key1": [[1, 2], [3, 4]]}, {"key1": np.arange(4).reshape(2, 2)}],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
),
|
||||
# Identical higher-dimensional (3D) numpy arrays across
|
||||
# metadata dictionaries
|
||||
(
|
||||
[
|
||||
{"key1": np.arange(8).reshape(2, 2, 2)},
|
||||
{"key1": np.arange(8).reshape(2, 2, 2)},
|
||||
],
|
||||
{"key1": np.arange(8).reshape(2, 2, 2)},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Differently-shaped higher-dimensional (3D) numpy arrays
|
||||
# across metadata dictionaries
|
||||
(
|
||||
[
|
||||
{"key1": np.arange(8).reshape(2, 2, 2)},
|
||||
{"key1": np.arange(8).reshape(4, 1, 2)},
|
||||
],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_merge_metadata(metadata_list, expected_result, exception):
|
||||
with exception:
|
||||
result = merge_metadata(metadata_list)
|
||||
if expected_result is None:
|
||||
assert result is None, f"Expected an error, but got a result {result}"
|
||||
for key, value in result.items():
|
||||
assert key in expected_result
|
||||
if isinstance(value, np.ndarray):
|
||||
np.testing.assert_array_equal(value, expected_result[key])
|
||||
else:
|
||||
assert value == expected_result[key]
|
||||
Reference in new issue
Block a user