Compare commits

..
49 Commits
Author SHA1 Message Date
Piotr Skalski 7ecfc9f475 Merge pull request #1908 from roboflow/develop
`supervision-0.26.1` release
2025-07-23 08:46:45 +02:00
Piotr Skalski 317ac0810e Merge pull request #1907 from roboflow/changelog/0.26.1
update: changelog for 0.26.1
2025-07-23 08:35:47 +02:00
SkalskiP cb6018ef04 final changes before supervision-0.26.1 2025-07-23 08:32:07 +02:00
pre-commit-ci[bot] 580fcdde4c fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-22 10:51:53 +00:00
soumik12345 ad4f44644c update: changelog for 0.26.1 2025-07-22 16:20:38 +05:30
Piotr Skalski 0c8a9d34a6 Merge pull request #1906 from roboflow/fix/border_thickness
fix: border thickness parameter usage for `PercentageBarAnnotator`
2025-07-22 12:27:26 +02:00
soumik12345 d067f32678 fix: border thickness parameter usage for PercentageBarAnnotator 2025-07-22 15:48:54 +05:30
Piotr Skalski d251677774 Merge pull request #1894 from roboflow/fix/mAP-area
fix: correct area handling in MeanAveragePrecision for size-specific mAP
2025-07-22 08:07:20 +02:00
SkalskiP e8cb962fcc bump version from 0.26.1 to 0.26.1rc0 2025-07-21 10:14:58 +02:00
Piotr Skalski baa0402d5a Merge pull request #1901 from roboflow/fix/inference-overlap-metric-fix
fix:  🐞 update inference_slicer.py for improved detection handling
2025-07-21 00:07:28 +02:00
pre-commit-ci[bot] c54d16c57e fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-20 22:02:42 +00:00
SkalskiP 1adb75520f bump version from 0.26.0 to 0.26.1; improve docstrings; unify naming; improve parsing 2025-07-21 00:02:14 +02:00
pre-commit-ci[bot] db51a9ff98 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-20 17:12:24 +00:00
Onuralp SEZER f044398c17 fix: 🐞 update inference_slicer.py for improved detection handling 2025-07-20 20:11:37 +03:00
pre-commit-ci[bot] 704652a0d2 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-18 15:19:38 +00:00
Balthasar 6e3001194d improve tests 2025-07-18 17:19:10 +02:00
Balthasar d7774381cc Merge branch 'develop' into fix/mAP-area 2025-07-18 12:11:05 +02:00
Soumik Rakshit 1c8ec0a287 Merge pull request #1898 from roboflow/fix/negative-mAP-values
fix: handle -1 sentinel values in mAP calculation to prevent negative results and mimic pycocotools behaviour
2025-07-18 15:32:21 +05:30
Balthasar 118e3a496c fix: line length 2025-07-18 10:09:53 +02:00
pre-commit-ci[bot] a139fb0b49 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-18 08:00:37 +00:00
Balthasar afdbb0a75c fix: keep it DRY 2025-07-18 09:41:06 +02:00
Balthasar 001a4254f8 fix: improve code, more fixes and add tests 2025-07-18 09:41:06 +02:00
Balthasar 2e72e70893 basic fix 2025-07-18 09:41:06 +02:00
pre-commit-ci[bot] 2907c40ecc fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-17 08:00:04 +00:00
Balthasar 2c67cee720 fix: tests and adjust comment 2025-07-17 09:57:55 +02:00
Balthasar 0ddbcb2010 fix: start id with 1 because 0 means no match 2025-07-17 09:49:21 +02:00
pre-commit-ci[bot] 40d4c6f554 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-16 18:07:52 +00:00
Balthasar bf88636487 Merge branch 'fix/mAP-area' of https://github.com/roboflow/supervision into fix/mAP-area 2025-07-16 20:07:30 +02:00
Balthasar a444b3bde8 fix: cleanup comment 2025-07-16 20:06:27 +02:00
pre-commit-ci[bot] 4009f9d886 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-16 18:00:47 +00:00
Balthasar 3f926c618a fix: cleanup comments 2025-07-16 19:51:59 +02:00
Balthasar 71d6670e7d fix(metrics): update area calculation in MeanAveragePrecision to use area property as fallback if it's not COCO 2025-07-16 19:50:37 +02:00
Piotr Skalski 08226e9199 Merge pull request #1893 from roboflow/main
Merge pull request #1742 from roboflow/develop
2025-07-16 02:50:11 +02:00
Piotr Skalski d8de58d7f3 Merge pull request #1742 from roboflow/develop
`supervision-0.26.0` release
2025-07-16 02:39:07 +02:00
SkalskiP a1e402987b bump version from 0.26.0rc8 to 0.26.0 2025-07-16 02:33:29 +02:00
Piotr Skalski 78e5cd56f3 Merge pull request #1891 from roboflow/docs/vlm-improvements
docs: 📝 enhance prompt engineering tips for Qwen2.5-VL and Moondream models
2025-07-16 02:32:39 +02:00
Onuralp SEZER 462146b34e docs: 📝 add prompt engineering tips for Qwen2.5-VL and Moondream models for from_lmm 2025-07-16 02:30:12 +02:00
Onuralp SEZER 12e38da1a0 docs: 📝 enhance prompt engineering tips for Qwen2.5-VL and Moondream models 2025-07-16 01:50:34 +02:00
Piotr Skalski 56a0ac5cbf Merge pull request #1888 from roboflow/docs/changelog-0.26.0
update(docs): changelog + deprecation for supervision 0.26.0
2025-07-16 01:07:51 +02:00
pre-commit-ci[bot] d1aa1726f5 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-15 23:05:19 +00:00
SkalskiP 0f56a405c6 updates before supervision-0.26.0 release 2025-07-16 01:04:51 +02:00
SkalskiP 4948612cd4 Merge branch 'develop' into docs/changelog-0.26.0 2025-07-16 00:06:50 +02:00
pre-commit-ci[bot] 6046e88eb1 fix(pre_commit): 🎨 auto format pre-commit hooks 2025-07-15 22:01:32 +00:00
soumik12345 77c45355ff chore: address feedback 2025-07-16 03:31:03 +05:30
soumik12345 3ff1414fc2 update: changelog 2025-07-16 02:20:01 +05:30
soumik12345 2c986abf5e add: remove_noisy_segments in the changelog 2025-07-15 12:20:23 +05:30
Onuralp SEZER 9443a433fc fix: 🐞quick typo fix. 2025-07-14 22:03:00 +03:00
soumik12345 b2776cadf7 chore: make pre-commit happy 2025-07-15 00:32:03 +05:30
soumik12345 1337b7ffd5 update: changelog + deprecation for supervision 0.26.0 2025-07-15 00:28:23 +05:30
13 changed files with 906 additions and 76 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# These owners will be the default owners for everything in
# the repo. They will be requested for review when someone
# opens a pull request.
* @SkalskiP @onuralpszr
* @SkalskiP @soumik12345
+192 -5
View File
@@ -1,5 +1,192 @@
# Changelog
### 0.26.1 <small>Jul 22, 2025</small>
- Fixed [1894](https://github.com/roboflow/supervision/pull/1894): Error in [`sv.MeanAveragePrecision`](https://supervision.roboflow.com/0.26.1/metrics/mean_average_precision/#supervision.metrics.mean_average_precision.MeanAveragePrecision) where the area used for size-specific evaluation (small / medium / large) was always zero unless explicitly provided in `sv.Detections.data`.
- Fixed [1895](https://github.com/roboflow/supervision/pull/1895): `ID=0` bug in [`sv.MeanAveragePrecision`](https://supervision.roboflow.com/0.26.1/metrics/mean_average_precision/#supervision.metrics.mean_average_precision.MeanAveragePrecision) where objects were getting `0.0` mAP despite perfect IoU matches due to a bug in annotation ID assignment.
- Fixed [1898](https://github.com/roboflow/supervision/pull/1898): Issue where [`sv.MeanAveragePrecision`](https://supervision.roboflow.com/0.26.1/metrics/mean_average_precision/#supervision.metrics.mean_average_precision.MeanAveragePrecision) could return negative values when certain object size categories have no data.
- Fixed [1901](https://github.com/roboflow/supervision/pull/1901): `match_metric` support for [`sv.Detections.with_nms`](https://supervision.roboflow.com/0.26.1/metrics/mean_average_precision/#supervision.detection.core.Detections.with_nms).
- Fixed [1906](https://github.com/roboflow/supervision/pull/1906): `border_thickness` parameter usage for [`sv.PercentageBarAnnotator`](https://supervision.roboflow.com/0.26.1/metrics/mean_average_precision/#supervision.annotators.core.PercentageBarAnnotator).
### 0.26.0 <small>Jul 16, 2025</small>
!!! failure "Removed"
`supervision-0.26.0` drops `python3.8` support and upgrade all codes to `python3.9` syntax style.
!!! info "Tip"
Supervision’s documentation theme now has a fresh look that is consistent with the documentations of all Roboflow open-source projects. ([#1858](https://github.com/roboflow/supervision/pull/1858))
- Added [#1774](https://github.com/roboflow/supervision/pull/1774): Support for the IOS (Intersection over Smallest) overlap metric that measures how much of the smaller object is covered by the larger one in [`sv.Detections.with_nms`](https://supervision.roboflow.com/0.26.0/detection/core/#supervision.detection.core.Detections.with_nms), [`sv.Detections.with_nmm`](https://supervision.roboflow.com/0.26.0/detection/core/#supervision.detection.core.Detections.with_nmm), [`sv.box_iou_batch`](https://supervision.roboflow.com/0.26.0/detection/utils/iou_and_nms/#supervision.detection.utils.iou_and_nms.box_iou_batch), and [`sv.mask_iou_batch`](https://supervision.roboflow.com/0.26.0/detection/utils/iou_and_nms/#supervision.detection.utils.iou_and_nms.mask_iou_batch).
```python
import numpy as np
import supervision as sv
boxes_true = np.array([
[100, 100, 200, 200],
[300, 300, 400, 400]
])
boxes_detection = np.array([
[150, 150, 250, 250],
[320, 320, 420, 420]
])
sv.box_iou_batch(
boxes_true=boxes_true,
boxes_detection=boxes_detection,
overlap_metric=sv.OverlapMetric.IOU
)
# array([[0.14285714, 0. ],
# [0. , 0.47058824]])
sv.box_iou_batch(
boxes_true=boxes_true,
boxes_detection=boxes_detection,
overlap_metric=sv.OverlapMetric.IOS
)
# array([[0.25, 0. ],
# [0. , 0.64]])
```
- Added [#1874](https://github.com/roboflow/supervision/pull/1874): [`sv.box_iou`](https://supervision.roboflow.com/0.26.0/detection/utils/iou_and_nms/#supervision.detection.utils.iou_and_nms.box_iou) that efficiently computes the Intersection over Union (IoU) between two individual bounding boxes.
- Added [#1816](https://github.com/roboflow/supervision/pull/1816): Support for frame limitations and progress bar in [`sv.process_video`](https://supervision.roboflow.com/0.26.0/utils/video/#supervision.utils.video.process_video).
- Added [#1788](https://github.com/roboflow/supervision/pull/1788): Support for creating [`sv.KeyPoints`](https://supervision.roboflow.com/0.26.0/keypoint/core/#supervision.keypoint.core.KeyPoints) objects from [ViTPose](https://huggingface.co/docs/transformers/en/model_doc/vitpose) and [ViTPose++](https://huggingface.co/docs/transformers/en/model_doc/vitpose#vitpose-models) inference results via [`sv.KeyPoints.from_transformers`](https://supervision.roboflow.com/0.26.0/keypoint/core/#supervision.keypoint.core.KeyPoints.from_transformers).
- Added [#1823](https://github.com/roboflow/supervision/pull/1823): [`sv.xyxy_to_xcycarh`](https://supervision.roboflow.com/0.26.0/detection/utils/converters/#supervision.detection.utils.converters.xyxy_to_xcycarh) function to convert bounding box coordinates from `(x_min, y_min, x_max, y_max)` into measurement space to format `(center x, center y, aspect ratio, height)`, where the aspect ratio is `width / height`.
- Added [#1788](https://github.com/roboflow/supervision/pull/1788): [`sv.xyxy_to_xywh`](https://supervision.roboflow.com/0.26.0/detection/utils/converters/#supervision.detection.utils.converters.xyxy_to_xywh) function to convert bounding box coordinates from `(x_min, y_min, x_max, y_max)` format to `(x, y, width, height)` format.
- Changed [#1820](https://github.com/roboflow/supervision/pull/1820): [`sv.LabelAnnotator`](https://supervision.roboflow.com/0.26.0/detection/annotators/#supervision.annotators.core.LabelAnnotator) now supports the `smart_position` parameter to automatically keep labels within frame boundaries, and the `max_line_length` parameter to control text wrapping for long or multi-line labels.
- Changed [#1825](https://github.com/roboflow/supervision/pull/1825): [`sv.LabelAnnotator`](https://supervision.roboflow.com/0.26.0/detection/annotators/#supervision.annotators.core.LabelAnnotator) now supports non-string labels.
- Changed [#1792](https://github.com/roboflow/supervision/pull/1792): [`sv.Detections.from_vlm`](https://supervision.roboflow.com/0.26.0/detection/core/#supervision.detection.core.Detections.from_vlm) now supports parsing bounding boxes and segmentation masks from responses generated by [Google Gemini models](https://ai.google.dev/gemini-api/docs/vision).
```python
import supervision as sv
gemini_response_text = """```json
[
{"box_2d": [543, 40, 728, 200], "label": "cat", "id": 1},
{"box_2d": [653, 352, 820, 522], "label": "dog", "id": 2}
]
```"""
detections = sv.Detections.from_vlm(
sv.VLM.GOOGLE_GEMINI_2_5,
gemini_response_text,
resolution_wh=(1000, 1000),
classes=['cat', 'dog'],
)
detections.xyxy
# array([[543., 40., 728., 200.], [653., 352., 820., 522.]])
detections.data
# {'class_name': array(['cat', 'dog'], dtype='<U26')}
detections.class_id
# array([0, 1])
```
- Changed [#1878](https://github.com/roboflow/supervision/pull/1878): [`sv.Detections.from_vlm`](https://supervision.roboflow.com/0.26.0/detection/core/#supervision.detection.core.Detections.from_vlm) now supports parsing bounding boxes from responses generated by [Moondream](https://github.com/vikhyat/moondream).
```python
import supervision as sv
moondream_result = {
'objects': [
{
'x_min': 0.5704046934843063,
'y_min': 0.20069346576929092,
'x_max': 0.7049859315156937,
'y_max': 0.3012596592307091
},
{
'x_min': 0.6210969910025597,
'y_min': 0.3300672620534897,
'x_max': 0.8417936339974403,
'y_max': 0.4961046129465103
}
]
}
detections = sv.Detections.from_vlm(
sv.VLM.MOONDREAM,
moondream_result,
resolution_wh=(1000, 1000),
)
detections.xyxy
# array([[1752.28, 818.82, 2165.72, 1229.14],
# [1908.01, 1346.67, 2585.99, 2024.11]])
```
- Changed [#1709](https://github.com/roboflow/supervision/pull/1790): [`sv.Detections.from_vlm`](https://supervision.roboflow.com/0.26.0/detection/core/#supervision.detection.core.Detections.from_vlm) now supports parsing bounding boxes from responses generated by [Qwen-2.5 VL](https://github.com/QwenLM/Qwen2.5-VL).
```python
import supervision as sv
qwen_2_5_vl_result = """```json
[
{"bbox_2d": [139, 768, 315, 954], "label": "cat"},
{"bbox_2d": [366, 679, 536, 849], "label": "dog"}
]
```"""
detections = sv.Detections.from_vlm(
sv.VLM.QWEN_2_5_VL,
qwen_2_5_vl_result,
input_wh=(1000, 1000),
resolution_wh=(1000, 1000),
classes=['cat', 'dog'],
)
detections.xyxy
# array([[139., 768., 315., 954.], [366., 679., 536., 849.]])
detections.class_id
# array([0, 1])
detections.data
# {'class_name': array(['cat', 'dog'], dtype='<U10')}
detections.class_id
# array([0, 1])
```
- Changed [#1786](https://github.com/roboflow/supervision/pull/1786): Significantly improved the speed of HSV color mapping in [`sv.HeatMapAnnotator`](https://supervision.roboflow.com/0.26.0/detection/annotators/#supervision.annotators.core.HeatMapAnnotator), achieving approximately 28x faster performance on 1920x1080 frames.
- Fixed [#1834](https://github.com/roboflow/supervision/pull/1834): Supervision’s [`sv.MeanAveragePrecision`](https://supervision.roboflow.com/0.26.0/metrics/mean_average_precision/#supervision.metrics.mean_average_precision.MeanAveragePrecision) is now fully aligned with [pycocotools](https://github.com/ppwwyyxx/cocoapi), the official COCO evaluation tool, ensuring accurate and standardized metrics. This update enabled us to launch a new version of the [Computer Vision Model Leaderboard](https://leaderboard.roboflow.com/).
```python
import supervision as sv
from supervision.metrics import MeanAveragePrecision
predictions = sv.Detections(...)
targets = sv.Detections(...)
map_metric = MeanAveragePrecision()
map_metric.update(predictions, targets).compute()
# Average Precision (AP) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.464
# Average Precision (AP) @[ IoU=0.50 | area= all | maxDets=100 ] = 0.637
# Average Precision (AP) @[ IoU=0.75 | area= all | maxDets=100 ] = 0.203
# Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.284
# Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.497
# Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.629
```
- Fixed [#1767](https://github.com/roboflow/supervision/pull/1767): Fixed losing `sv.Detections.data` when detections filtering.
### 0.25.0 <small>Nov 12, 2024</small>
- No removals or deprecations in this release!
@@ -382,9 +569,9 @@ detections = sv.Detections.from_sam(sam_result=sam_result)
- Changed [#1434](https://github.com/roboflow/supervision/pull/1434): [`InferenceSlicer`](https://supervision.roboflow.com/0.23.0/detection/tools/inference_slicer/) now features an `overlap_wh` parameter, making it easier to compute slice sizes when handling overlapping slices.
- Fix [#1448](https://github.com/roboflow/supervision/pull/1448): Various annotator type issues have been resolved, supporting expanded error handling.
- Fixed [#1448](https://github.com/roboflow/supervision/pull/1448): Various annotator type issues have been resolved, supporting expanded error handling.
- Fix [#1348](https://github.com/roboflow/supervision/pull/1348): Introduced a new method for [seeking to a specific video frame](https://supervision.roboflow.com/0.23.0/utils/video/#supervision.utils.video.get_video_frames_generator), addressing cases where traditional seek methods were failing. It can be enabled with `iterative_seek=True`.
- Fixed [#1348](https://github.com/roboflow/supervision/pull/1348): Introduced a new method for [seeking to a specific video frame](https://supervision.roboflow.com/0.23.0/utils/video/#supervision.utils.video.get_video_frames_generator), addressing cases where traditional seek methods were failing. It can be enabled with `iterative_seek=True`.
```python
import supervision as sv
@@ -397,7 +584,7 @@ for frame in sv.get_video_frames_generator(
...
```
- Fix [#1424](https://github.com/roboflow/supervision/pull/1424): `plot_image` function now clearly indicates that the size is in inches.
- Fixed [#1424](https://github.com/roboflow/supervision/pull/1424): `plot_image` function now clearly indicates that the size is in inches.
!!! failure "Removed"
@@ -1110,7 +1297,7 @@ array([
### 0.11.1 <small>June 29, 2023</small>
- Fix [#165](https://github.com/roboflow/supervision/pull/165): [`as_folder_structure`](/0.11.1/dataset/core/#supervision.dataset.core.ClassificationDataset.as_folder_structure) fails to save [`sv.ClassificationDataset`](/0.11.1/dataset/core/#classificationdataset) when it is result of inference.
- Fixed [#165](https://github.com/roboflow/supervision/pull/165): [`as_folder_structure`](/0.11.1/dataset/core/#supervision.dataset.core.ClassificationDataset.as_folder_structure) fails to save [`sv.ClassificationDataset`](/0.11.1/dataset/core/#classificationdataset) when it is result of inference.
### 0.11.0 <small>June 28, 2023</small>
@@ -1156,7 +1343,7 @@ array([
- Added [#162](https://github.com/roboflow/supervision/pull/162): additional `start` and `end` arguments to [`sv.get_video_frames_generator`](/0.11.0/utils/video/#get_video_frames_generator) allowing to generate frames only for a selected part of the video.
- Fix [#157](https://github.com/roboflow/supervision/pull/157): incorrect loading of YOLO dataset class names from `data.yaml`.
- Fixed [#157](https://github.com/roboflow/supervision/pull/157): incorrect loading of YOLO dataset class names from `data.yaml`.
### 0.10.0 <small>June 14, 2023</small>
+15 -17
View File
@@ -7,21 +7,19 @@ status: deprecated
These features are phased out due to better alternatives or potential issues in future versions. Deprecated functionalities are supported for **five subsequent releases**, providing time for users to transition to updated methods.
- Constructing [`DetectionDataset`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset) and [`ClassificationDataset`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.ClassificationDataset) with parameter `images` as `Dict[str, np.ndarray]` will be removed in `supervision-0.26.0`. Please pass a list of paths `List[str]` instead.
- The `DetectionDataset.images` property will be removed in `supervision-0.26.0`. Please loop over images with `for path, image, annotation in dataset:`, as that does not require loading all images into memory.
- `BoundingBoxAnnotator` has been renamed to `BoxAnnotator` after the old implementation of [`BoxAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.BoxAnnotator) has been removed. `BoundingBoxAnnotator` will be removed in `supervision-0.26.0`.
- `overlap_filter_strategy` in [`InferenceSlicer.__init__`](https://supervision.roboflow.com/latest/detection/tools/inference_slicer/) is deprecated and will be removed in `supervision-0.27.0`. Use `overlap_strategy` instead.
- `overlap_ratio_wh` in [`InferenceSlicer.__init__`](https://supervision.roboflow.com/latest/detection/tools/inference_slicer/) is deprecated and will be removed in `supervision-0.27.0`. Use `overlap_wh` instead.
- `sv.LMM` enum is deprecated and will be removed in `supervision-0.31.0`. Use `sv.VLM` instead.
- [`sv.Detections.from_lmm`](https://supervision.roboflow.com/0.26.0/detection/core/#supervision.detection.core.Detections.from_lmm) property is deprecated and will be removed in `supervision-0.31.0`. Use [`sv.Detections.from_vlm`](https://supervision.roboflow.com/0.26.0/detection/core/#supervision.detection.core.Detections.from_vlm) instead.
# Removed
### 0.25.0
### 0.26.0
- The `sv.DetectionDataset.images` property has been removed in `supervision-0.26.0`. Please loop over images with `for path, image, annotation in dataset:`, as that does not require loading all images into memory. Also, constructing `sv.DetectionDataset` with parameter `images` as `Dict[str, np.ndarray]` is deprecated and has been removed in `supervision-0.26.0`. Please pass a list of paths `List[str]` instead.
- The name `sv.BoundingBoxAnnotator` is deprecated and has been removed in `supervision-0.26.0`. It has been renamed to [`sv.BoxAnnotator`](https://supervision.roboflow.com/0.22.0/detection/annotators/#supervision.annotators.core.BoxAnnotator).
No removals in this version!
### 0.24.0
@@ -35,12 +33,12 @@ No removals in this version!
### 0.22.0
- `Detections.from_roboflow` is removed as of `supervision-0.22.0`. Use [`Detections.from_inference`](detection/core.md/#supervision.detection.core.Detections.from_inference) instead.
- The method `Color.white()` was removed as of `supervision-0.22.0`. Use the constant `Color.WHITE` instead.
- The method `Color.black()` was removed as of `supervision-0.22.0`. Use the constant `Color.BLACK` instead.
- The method `Color.red()` was removed as of `supervision-0.22.0`. Use the constant `Color.RED` instead.
- The method `Color.green()` was removed as of `supervision-0.22.0`. Use the constant `Color.GREEN` instead.
- The method `Color.blue()` was removed as of `supervision-0.22.0`. Use the constant `Color.BLUE` instead.
- The method `ColorPalette.default()` was removed as of `supervision-0.22.0`. Use the constant [`ColorPalette.DEFAULT`](/utils/draw/#supervision.draw.color.ColorPalette.DEFAULT) instead.
- `BoxAnnotator` was removed as of `supervision-0.22.0`, however `BoundingBoxAnnotator` was immediately renamed to `BoxAnnotator`. Use [`BoxAnnotator`](detection/annotators.md/#supervision.annotators.core.BoxAnnotator) and [`LabelAnnotator`](detection/annotators.md/#supervision.annotators.core.LabelAnnotator) instead of the old `BoxAnnotator`.
- The method `FPSMonitor.__call__` was removed as of `supervision-0.22.0`. Use the attribute [`FPSMonitor.fps`](utils/video.md/#supervision.utils.video.FPSMonitor.fps) instead.
- `sv.Detections.from_roboflow` is removed as of `supervision-0.22.0`. Use [`Detections.from_inference`](detection/core.md/#supervision.detection.core.Detections.from_inference) instead.
- The method `sv.Color.white()` was removed as of `supervision-0.22.0`. Use the constant `sv.Color.WHITE` instead.
- The method `sv.Color.black()` was removed as of `supervision-0.22.0`. Use the constant `sv.Color.BLACK` instead.
- The method `sv.Color.red()` was removed as of `supervision-0.22.0`. Use the constant `sv.Color.RED` instead.
- The method `sv.Color.green()` was removed as of `supervision-0.22.0`. Use the constant `sv.Color.GREEN` instead.
- The method `sv.Color.blue()` was removed as of `supervision-0.22.0`. Use the constant `sv.Color.BLUE` instead.
- The method `sv.ColorPalette.default()` was removed as of `supervision-0.22.0`. Use the constant [`ColorPalette.DEFAULT`](/utils/draw/#supervision.draw.color.ColorPalette.DEFAULT) instead.
- `sv.BoxAnnotator` was removed as of `supervision-0.22.0`, however `sv.BoundingBoxAnnotator` was immediately renamed to `sv.BoxAnnotator`. Use [`BoxAnnotator`](detection/annotators.md/#supervision.annotators.core.BoxAnnotator) and [`LabelAnnotator`](detection/annotators.md/#supervision.annotators.core.LabelAnnotator) instead of the old `sv.BoxAnnotator`.
- The method `sv.FPSMonitor.__call__` was removed as of `supervision-0.22.0`. Use the attribute [`sv.FPSMonitor.fps`](utils/video.md/#supervision.utils.video.FPSMonitor.fps) instead.
+14 -16
View File
@@ -28,28 +28,26 @@ Annotators accept detections and apply box or mask visualizations to the detecti
</div>
<!--
=== "RoundBox"
=== "RoundBox"
```python
import supervision as sv
```python
import supervision as sv
image = ...
detections = sv.Detections(...)
image = ...
detections = sv.Detections(...)
round_box_annotator = sv.RoundBoxAnnotator()
annotated_frame = round_box_annotator.annotate(
scene=image.copy(),
detections=detections
)
```
round_box_annotator = sv.RoundBoxAnnotator()
annotated_frame = round_box_annotator.annotate(
scene=image.copy(),
detections=detections
)
```
<div class="result" markdown>
<div class="result" markdown>
![round-box-annotator-example](https://media.roboflow.com/supervision-annotator-examples/round-box-annotator-example-purple.png){ align=center width="800" }
![round-box-annotator-example](https://media.roboflow.com/supervision-annotator-examples/round-box-annotator-example-purple.png){ align=center width="800" }
</div>
-->
</div>
=== "BoxCorner"
+1
View File
@@ -1,5 +1,6 @@
---
comments: true
status: new
---
# Keypoint Detection
+1 -1
View File
@@ -2,7 +2,7 @@
name = "supervision"
description = "A set of easy-to-use utils that will come in handy in any Computer Vision project"
license = { text = "MIT" }
version = "0.26.0rc8"
version = "0.26.1"
readme = "README.md"
requires-python = ">=3.9"
authors = [
+5 -2
View File
@@ -2239,8 +2239,11 @@ class PercentageBarAnnotator(BaseAnnotator):
self.position: Position = position
self.color_lookup: ColorLookup = color_lookup
if border_thickness is None:
self.border_thickness = int(0.15 * self.height)
self.border_thickness = (
border_thickness
if border_thickness is not None
else int(0.15 * self.height)
)
@ensure_cv2_image_for_annotation
def annotate(
+132 -4
View File
@@ -867,6 +867,53 @@ class Detections:
```
!!! example "Qwen2.5-VL"
??? tip "Prompt engineering"
To get the best results from Qwen2.5-VL, use clear and descriptive prompts
that specify exactly what you want to detect.
**For general object detection, use this comprehensive prompt:**
```
Detect all objects in the image and return their locations and labels.
```
**For specific object detection with detailed descriptions:**
```
Detect the red object that is leading in this image and return its location and label.
```
**For simple, targeted detection:**
```
leading blue truck
```
**Additional effective prompts:**
```
Find all people and vehicles in this scene
```
```
Locate all animals in the image
```
```
Identify traffic signs and their positions
```
**Tips for better results:**
- Use descriptive language that clearly specifies what to look for
- Include color, size, or position descriptors when targeting specific objects
- Be specific about the type of objects you want to detect
- The model responds well to both detailed instructions and concise phrases
- Results are returned in JSON format with `bbox_2d` coordinates and `label` fields
```python
import supervision as sv
@@ -1023,6 +1070,23 @@ class Detections:
```
!!! example "Moondream"
??? tip "Prompt engineering"
To get the best results from Moondream, use optimized prompts that leverage
its object detection capabilities effectively.
**For general object detection, use this simple prompt:**
```
objects
```
This single-word prompt instructs Moondream to detect all visible objects
and return them in the proper JSON format with normalized coordinates.
```python
import supervision as sv
@@ -1137,6 +1201,53 @@ class Detections:
```
!!! example "Qwen2.5-VL"
??? tip "Prompt engineering"
To get the best results from Qwen2.5-VL, use clear and descriptive prompts
that specify exactly what you want to detect.
**For general object detection, use this comprehensive prompt:**
```
Detect all objects in the image and return their locations and labels.
```
**For specific object detection with detailed descriptions:**
```
Detect the red object that is leading in this image and return its location and label.
```
**For simple, targeted detection:**
```
leading blue truck
```
**Additional effective prompts:**
```
Find all people and vehicles in this scene
```
```
Locate all animals in the image
```
```
Identify traffic signs and their positions
```
**Tips for better results:**
- Use descriptive language that clearly specifies what to look for
- Include color, size, or position descriptors when targeting specific objects
- Be specific about the type of objects you want to detect
- The model responds well to both detailed instructions and concise phrases
- Results are returned in JSON format with `bbox_2d` coordinates and `label` fields
```python
import supervision as sv
@@ -1293,6 +1404,23 @@ class Detections:
```
!!! example "Moondream"
??? tip "Prompt engineering"
To get the best results from Moondream, use optimized prompts that leverage
its object detection capabilities effectively.
**For general object detection, use this simple prompt:**
```
objects
```
This single-word prompt instructs Moondream to detect all visible objects
and return them in the proper JSON format with normalized coordinates.
```python
import supervision as sv
@@ -1811,8 +1939,8 @@ class Detections:
class_agnostic (bool): Whether to perform class-agnostic
non-maximum suppression. If True, the class_id of each detection
will be ignored. Defaults to False.
overlap_metric (OverlapMetric): Metric used for measuring overlap between
detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of
overlap between pairs of masks or boxes (e.g., IoU, IoS).
Returns:
Detections: A new Detections object containing the subset of detections
@@ -1875,8 +2003,8 @@ class Detections:
class_agnostic (bool): Whether to perform class-agnostic
non-maximum merging. If True, the class_id of each detection
will be ignored. Defaults to False.
overlap_metric (OverlapMetric): Metric used for measuring overlap between
detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of
overlap between pairs of masks or boxes (e.g., IoU, IoS).
Returns:
Detections: A new Detections object containing the subset of detections
@@ -9,7 +9,7 @@ import numpy as np
from supervision.config import ORIENTED_BOX_COORDINATES
from supervision.detection.core import Detections
from supervision.detection.utils.boxes import move_boxes, move_oriented_boxes
from supervision.detection.utils.iou_and_nms import OverlapFilter
from supervision.detection.utils.iou_and_nms import OverlapFilter, OverlapMetric
from supervision.detection.utils.masks import move_masks
from supervision.utils.image import crop_image
from supervision.utils.internal import (
@@ -75,8 +75,8 @@ class InferenceSlicer:
filtering or merging overlapping detections in slices.
iou_threshold (float): Intersection over Union (IoU) threshold
used when filtering by overlap.
match_metric (str): Metric used for matching detections in slices.
"IOU" or "IOS". Defaults "IOU".
overlap_metric (Union[OverlapMetric, str]): Metric used for matching detections
in slices.
callback (Callable): A function that performs inference on a given image
slice and returns detections.
thread_workers (int): Number of threads for parallel execution.
@@ -96,7 +96,7 @@ class InferenceSlicer:
overlap_wh: tuple[int, int] | None = None,
overlap_filter: OverlapFilter | str = OverlapFilter.NON_MAX_SUPPRESSION,
iou_threshold: float = 0.5,
match_metric: str = "IOU",
overlap_metric: OverlapMetric | str = OverlapMetric.IOU,
thread_workers: int = 1,
):
if overlap_ratio_wh is not None:
@@ -112,7 +112,7 @@ class InferenceSlicer:
self.slice_wh = slice_wh
self.iou_threshold = iou_threshold
self.match_metric = match_metric
self.overlap_metric = OverlapMetric.from_value(overlap_metric)
self.overlap_filter = OverlapFilter.from_value(overlap_filter)
self.callback = callback
self.thread_workers = thread_workers
@@ -173,11 +173,11 @@ class InferenceSlicer:
return merged
elif self.overlap_filter == OverlapFilter.NON_MAX_SUPPRESSION:
return merged.with_nms(
threshold=self.iou_threshold, match_metric=self.match_metric
threshold=self.iou_threshold, overlap_metric=self.overlap_metric
)
elif self.overlap_filter == OverlapFilter.NON_MAX_MERGE:
return merged.with_nmm(
threshold=self.iou_threshold, match_metric=self.match_metric
threshold=self.iou_threshold, overlap_metric=self.overlap_metric
)
else:
warnings.warn(
+18 -9
View File
@@ -164,7 +164,8 @@ def box_iou_batch(
`shape = (N, 4)` where `N` is number of true objects.
boxes_detection (np.ndarray): 2D `np.ndarray` representing detection boxes.
`shape = (M, 4)` where `M` is number of detected objects.
overlap_metric (OverlapMetric): Metric used for matching detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of overlap
between pairs of boxes (e.g., IoU, IoS).
Returns:
np.ndarray: Pairwise IoU of boxes from `boxes_true` and `boxes_detection`.
@@ -381,7 +382,8 @@ def _mask_iou_batch_split(
Args:
masks_true (np.ndarray): 3D `np.ndarray` representing ground-truth masks.
masks_detection (np.ndarray): 3D `np.ndarray` representing detection masks.
overlap_metric (OverlapMetric): Metric used for matching detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of overlap
between pairs of masks (e.g., IoU, IoS).
Returns:
np.ndarray: Pairwise IoU of masks from `masks_true` and `masks_detection`.
@@ -433,7 +435,8 @@ def mask_iou_batch(
Args:
masks_true (np.ndarray): 3D `np.ndarray` representing ground-truth masks.
masks_detection (np.ndarray): 3D `np.ndarray` representing detection masks.
overlap_metric (OverlapMetric): Metric used for matching detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of overlap
between pairs of masks (e.g., IoU, IoS).
memory_limit (int): memory limit in MB, default is 1024 * 5 MB (5GB).
Returns:
@@ -492,7 +495,8 @@ def mask_non_max_suppression(
dimensions of each mask.
iou_threshold (float): The intersection-over-union threshold
to use for non-maximum suppression.
overlap_metric (OverlapMetric): Metric used for matching detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of overlap
between pairs of masks (e.g., IoU, IoS).
mask_dimension (int): The dimension to which the masks should be
resized before computing IOU values. Defaults to 640.
@@ -543,7 +547,8 @@ def box_non_max_suppression(
or `(x_min, y_min, x_max, y_max, score, class)`.
iou_threshold (float): The intersection-over-union threshold
to use for non-maximum suppression.
overlap_metric (OverlapMetric): Metric used for matching detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of overlap
between pairs of boxes (e.g., IoU, IoS).
Returns:
np.ndarray: A boolean array indicating which predictions to keep after n
@@ -603,7 +608,8 @@ def _group_overlapping_masks(
the predictions.
iou_threshold (float): The intersection-over-union threshold
to use for non-maximum suppression. Defaults to 0.5.
overlap_metric (OverlapMetric): Metric used for matching detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of overlap
between pairs of masks (e.g., IoU, IoS).
Returns:
list[list[int]]: Groups of prediction indices be merged.
@@ -664,7 +670,8 @@ def mask_non_max_merge(
to use for non-maximum suppression.
mask_dimension (int): The dimension to which the masks should be
resized before computing IOU values. Defaults to 640.
overlap_metric (OverlapMetric): Metric used for matching detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of overlap
between pairs of masks (e.g., IoU, IoS).
Returns:
np.ndarray: A boolean array indicating which predictions to keep after
@@ -717,7 +724,8 @@ def _group_overlapping_boxes(
and the confidence scores.
iou_threshold (float): The intersection-over-union threshold
to use for non-maximum suppression. Defaults to 0.5.
overlap_metric (OverlapMetric): Metric used for matching detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of overlap
between pairs of boxes (e.g., IoU, IoS).
Returns:
list[list[int]]: Groups of prediction indices be merged.
@@ -765,7 +773,8 @@ def box_non_max_merge(
detections of different classes to be merged.
iou_threshold (float): The intersection-over-union threshold
to use for non-maximum suppression. Defaults to 0.5.
overlap_metric (OverlapMetric): Metric used for matching detections in slices.
overlap_metric (OverlapMetric): Metric used to compute the degree of overlap
between pairs of boxes (e.g., IoU, IoS).
Returns:
list[list[int]]: Groups of prediction indices be merged.
+61 -14
View File
@@ -57,7 +57,11 @@ class MeanAveragePrecisionResult:
@property
def map50_95(self) -> float:
return self.mAP_scores.mean()
valid_scores = self.mAP_scores[self.mAP_scores > -1]
if len(valid_scores) > 0:
return valid_scores.mean()
else:
return -1
@property
def map50(self) -> float:
@@ -421,6 +425,11 @@ class EvaluationDataset:
if not isinstance(predictions, list):
raise ValueError("results must be a list")
# Handle empty predictions
if len(predictions) == 0:
predictions_dataset.dataset["annotations"] = []
return predictions_dataset
ids = [pred["image_id"] for pred in predictions]
# Make sure the image ids from predictions exist in the current dataset
@@ -909,6 +918,35 @@ class COCOEvaluator:
np.array(score_at_recall)
)
self.results = {
"params": self.params,
"counts": [
num_iou_thresholds,
num_recall_thresholds,
num_categories,
num_area_ranges,
num_max_detections,
],
"date": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"precision": precision,
"recall": recall,
"scores": scores,
}
# Helper function to compute average precision while handling -1 sentinel values
def compute_average_precision(precision_slice):
"""Compute average precision while handling -1 sentinel values."""
masked = np.ma.masked_equal(precision_slice, -1)
if masked.count() == 0:
# All values are -1 (no data)
return np.full(num_iou_thresholds, -1), np.full(
(num_categories, num_iou_thresholds), -1
)
else:
mAP_scores = np.ma.filled(masked.mean(axis=(1, 2)), -1)
ap_per_class = np.ma.filled(masked.mean(axis=1), -1).transpose(1, 0)
return mAP_scores, ap_per_class
# Average precision over all sizes, 100 max detections
area_range_idx = list(ObjectSize).index(ObjectSize.ALL)
max_100_dets_idx = self.params.max_dets.index(100)
@@ -917,10 +955,9 @@ class COCOEvaluator:
:, :, :, area_range_idx, max_100_dets_idx
]
# mAP over thresholds (dimension=num_thresholds)
mAP_scores_all_sizes = average_precision_all_sizes.mean(axis=(1, 2))
# AP per class
ap_per_class_all_sizes = average_precision_all_sizes.mean(axis=1).transpose(
1, 0
# Use masked array to exclude -1 values when computing mean
mAP_scores_all_sizes, ap_per_class_all_sizes = compute_average_precision(
average_precision_all_sizes
)
# Average precision for SMALL objects and 100 max detections
@@ -928,24 +965,27 @@ class COCOEvaluator:
average_precision_small = precision[
:, :, :, small_area_range_idx, max_100_dets_idx
]
mAP_scores_small = average_precision_small.mean(axis=(1, 2))
ap_per_class_small = average_precision_small.mean(axis=1).transpose(1, 0)
mAP_scores_small, ap_per_class_small = compute_average_precision(
average_precision_small
)
# Average precision for MEDIUM objects and 100 max detections
medium_area_range_idx = list(ObjectSize).index(ObjectSize.MEDIUM)
average_precision_medium = precision[
:, :, :, medium_area_range_idx, max_100_dets_idx
]
mAP_scores_medium = average_precision_medium.mean(axis=(1, 2))
ap_per_class_medium = average_precision_medium.mean(axis=1).transpose(1, 0)
mAP_scores_medium, ap_per_class_medium = compute_average_precision(
average_precision_medium
)
# Average precision for LARGE objects and 100 max detections
large_area_range_idx = list(ObjectSize).index(ObjectSize.LARGE)
average_precision_large = precision[
:, :, :, large_area_range_idx, max_100_dets_idx
]
mAP_scores_large = average_precision_large.mean(axis=(1, 2))
ap_per_class_large = average_precision_large.mean(axis=1).transpose(1, 0)
mAP_scores_large, ap_per_class_large = compute_average_precision(
average_precision_large
)
self.results = {
"params": self.params,
@@ -1221,7 +1261,7 @@ class MeanAveragePrecision(Metric):
for image_id, image_targets in enumerate(targets):
if self._image_indices is not None:
image_id = self._image_indices[image_id]
for target in image_targets:
for target_idx, target in enumerate(image_targets):
xyxy = target[0] # or xyxy = prediction[0]; xyxy[2:4] -= xyxy[0:2]
xywh = [xyxy[0], xyxy[1], xyxy[2] - xyxy[0], xyxy[3] - xyxy[1]]
# Get "area" and "iscrowd" (default 0) from data
@@ -1231,13 +1271,20 @@ class MeanAveragePrecision(Metric):
category_id = self._class_mapping[target[3].item()]
else:
category_id = target[3].item()
# Use area from data if available (e.g., COCO datasets)
# Otherwise use Detections.area property
area = data.get("area") if data else None
if area is None:
area = image_targets.area[target_idx]
dict_annotation = {
"area": data.get("area", 0),
"area": area,
"iscrowd": data.get("iscrowd", 0),
"image_id": image_id,
"bbox": xywh,
"category_id": category_id,
"id": len(annotations), # incrementally increase the id
"id": len(annotations) + 1, # Start IDs from 1 (0 means no match)
}
annotations.append(dict_annotation)
# Category list
+328
View File
@@ -0,0 +1,328 @@
"""
Tests for Mean Average Precision ID=0 bug fix
"""
import numpy as np
from supervision.detection.core import Detections
from supervision.metrics.mean_average_precision import MeanAveragePrecision
def test_single_perfect_detection():
"""Test that single perfect detection gets 1.0 mAP (not 0.0 due to ID=0 bug)"""
# Perfect detection (identical prediction and target)
detection = Detections(
xyxy=np.array([[10, 10, 50, 50]], dtype=np.float64),
class_id=np.array([0]),
confidence=np.array([0.9]),
)
metric = MeanAveragePrecision()
metric.update([detection], [detection])
result = metric.compute()
# Should be perfect 1.0 mAP, not 0.0 due to ID=0 bug
assert abs(result.map50_95 - 1.0) < 1e-6
def test_multiple_perfect_detections():
"""Test that multiple perfect detections get 1.0 mAP"""
# Multiple perfect detections in one image
detections = Detections(
xyxy=np.array(
[[10, 10, 50, 50], [100, 100, 140, 140], [200, 200, 240, 240]],
dtype=np.float64,
),
class_id=np.array([0, 0, 0]),
confidence=np.array([0.9, 0.9, 0.9]),
)
metric = MeanAveragePrecision()
metric.update([detections], [detections])
result = metric.compute()
# Should be perfect 1.0 mAP
assert abs(result.map50_95 - 1.0) < 1e-6
def test_batch_updates_perfect_detections():
"""Test that batch updates with perfect detections get 1.0 mAP"""
# Single perfect detection for multiple batch updates
detection = Detections(
xyxy=np.array([[10, 10, 50, 50]], dtype=np.float64),
class_id=np.array([0]),
confidence=np.array([0.9]),
)
metric = MeanAveragePrecision()
# Add 3 batch updates
metric.update([detection], [detection])
metric.update([detection], [detection])
metric.update([detection], [detection])
result = metric.compute()
# Should be perfect 1.0 mAP across all batches
assert abs(result.map50_95 - 1.0) < 1e-6
def test_scenario_1_success_case_imperfect_match():
"""Scenario 1: Success Case with imperfect match"""
# Small object (class 0) - area = 30*30 = 900 < 1024
small_perfect = Detections(
xyxy=np.array([[10, 10, 40, 40]], dtype=np.float64),
class_id=np.array([0]),
confidence=np.array([0.95]),
data={"area": np.array([900])},
)
# Medium object (class 1) - area = 50*50 = 2500 (between 1024 and 9216)
medium_target = Detections(
xyxy=np.array([[10, 10, 60, 60]], dtype=np.float64),
class_id=np.array([1]),
data={"area": np.array([2500])},
)
medium_pred = Detections(
xyxy=np.array([[12, 12, 60, 60]], dtype=np.float64), # Slightly off
class_id=np.array([1]),
confidence=np.array([0.9]),
data={"area": np.array([2304])}, # 48*48
)
# Large objects (classes 0, 1, 2) - area = 100*100 = 10000 > 9216
large_targets = Detections(
xyxy=np.array(
[[10, 10, 110, 110], [120, 120, 220, 220], [230, 230, 330, 330]],
dtype=np.float64,
),
class_id=np.array([2, 0, 1]),
data={"area": np.array([10000, 10000, 10000])},
)
large_preds = Detections(
xyxy=np.array(
[[10, 10, 110, 110], [120, 120, 220, 220], [230, 230, 330, 330]],
dtype=np.float64,
),
class_id=np.array([2, 0, 1]),
confidence=np.array([0.9, 0.9, 0.9]),
data={"area": np.array([10000, 10000, 10000])},
)
metric = MeanAveragePrecision()
metric.update([small_perfect], [small_perfect])
metric.update([medium_pred], [medium_target])
metric.update([large_preds], [large_targets])
result = metric.compute()
# Should be close to 0.9 (slightly less than perfect due to medium object)
assert 0.85 < result.map50_95 < 0.98 # Adjusted upper bound
assert result.medium_objects.map50_95 < 1.0 # Medium should be less than perfect
def test_scenario_2_missed_detection():
"""Scenario 2: GT Present, No Prediction (Missed Detection)"""
# Small object - area = 30*30 = 900 < 1024
small_detection = Detections(
xyxy=np.array([[10, 10, 40, 40]], dtype=np.float64),
class_id=np.array([0]),
confidence=np.array([0.95]),
data={"area": np.array([900])},
)
# Medium object - area = 50*50 = 2500 (between 1024 and 9216) - missed
medium_target = Detections(
xyxy=np.array([[10, 10, 60, 60]], dtype=np.float64),
class_id=np.array([1]),
data={"area": np.array([2500])},
)
no_medium_pred = Detections.empty()
# Large objects - area = 100*100 = 10000 > 9216
large_detections = Detections(
xyxy=np.array(
[[10, 10, 110, 110], [120, 120, 220, 220], [230, 230, 330, 330]],
dtype=np.float64,
),
class_id=np.array([2, 0, 1]),
confidence=np.array([0.9, 0.9, 0.9]),
data={"area": np.array([10000, 10000, 10000])},
)
metric = MeanAveragePrecision()
metric.update([small_detection], [small_detection])
metric.update([no_medium_pred], [medium_target])
metric.update([large_detections], [large_detections])
result = metric.compute()
# Medium objects should have 0.0 mAP (missed detection)
assert abs(result.medium_objects.map50_95 - 0.0) < 1e-6
def test_scenario_3_false_positive():
"""Scenario 3: No GT, Prediction Present (False Positive)"""
# Small object - area = 30*30 = 900 < 1024
small_detection = Detections(
xyxy=np.array([[10, 10, 40, 40]], dtype=np.float64),
class_id=np.array([0]),
confidence=np.array([0.95]),
data={"area": np.array([900])},
)
# Medium object - area = 50*50 = 2500 - false positive (no GT)
medium_pred = Detections(
xyxy=np.array([[12, 12, 62, 62]], dtype=np.float64),
class_id=np.array([1]),
confidence=np.array([0.9]),
data={"area": np.array([2500])},
)
no_medium_target = Detections.empty()
# Large objects - area = 100*100 = 10000 > 9216
large_detections = Detections(
xyxy=np.array(
[[10, 10, 110, 110], [120, 120, 220, 220], [230, 230, 330, 330]],
dtype=np.float64,
),
class_id=np.array([2, 0, 1]),
confidence=np.array([0.9, 0.9, 0.9]),
data={"area": np.array([10000, 10000, 10000])},
)
metric = MeanAveragePrecision()
metric.update([small_detection], [small_detection])
metric.update([medium_pred], [no_medium_target])
metric.update([large_detections], [large_detections])
result = metric.compute()
# Medium objects should have -1 mAP (false positive, matching pycocotools)
assert result.medium_objects.map50_95 == -1
def test_scenario_4_no_data():
"""Scenario 4: No GT, No Prediction (Category has no data)"""
# Small object - area = 30*30 = 900 < 1024
small_detection = Detections(
xyxy=np.array([[10, 10, 40, 40]], dtype=np.float64),
class_id=np.array([0]),
confidence=np.array([0.95]),
data={"area": np.array([900])},
)
# Medium object - no data at all
no_medium = Detections.empty()
# Large objects - area = 100*100 = 10000 > 9216 - only classes 0 and 2 (no class 1)
large_targets = Detections(
xyxy=np.array(
[
[10, 10, 110, 110],
[120, 120, 220, 220],
],
dtype=np.float64,
),
class_id=np.array([2, 0]),
data={"area": np.array([10000, 10000])},
)
large_preds = Detections(
xyxy=np.array(
[
[10, 10, 110, 110],
[120, 120, 220, 220],
],
dtype=np.float64,
),
class_id=np.array([2, 0]),
confidence=np.array([0.9, 0.9]),
data={"area": np.array([10000, 10000])},
)
metric = MeanAveragePrecision()
metric.update([small_detection], [small_detection])
metric.update([no_medium], [no_medium])
metric.update([large_preds], [large_targets])
result = metric.compute()
# Should NOT have negative mAP values for overall
assert result.map50_95 >= 0.0
# Medium objects should have -1 mAP (no data, matching pycocotools)
assert result.medium_objects.map50_95 == -1
def test_scenario_5_only_one_class_present():
"""Scenario 5: Only 1 of 3 Classes Present (Perfect Match)"""
# Only class 0 objects with perfect matches
detections_class_0 = [
Detections(
xyxy=np.array([[10, 10, 40, 40]], dtype=np.float64),
class_id=np.array([0]),
confidence=np.array([0.95]),
),
Detections(
xyxy=np.array([[20, 20, 230, 130]], dtype=np.float64),
class_id=np.array([0]),
confidence=np.array([0.9]),
),
]
metric = MeanAveragePrecision()
for det in detections_class_0:
metric.update([det], [det])
result = metric.compute()
# Should be 1.0 mAP (perfect match for the only class present)
assert abs(result.map50_95 - 1.0) < 1e-6
assert abs(result.map50 - 1.0) < 1e-6
assert abs(result.map75 - 1.0) < 1e-6
def test_mixed_classes_with_missing_detections():
"""Test mixed scenario with some classes having no detections"""
# Class 0: Perfect detection
class_0_det = Detections(
xyxy=np.array([[10, 10, 50, 50]], dtype=np.float64),
class_id=np.array([0]),
confidence=np.array([0.9]),
)
# Class 1: GT exists but no prediction
class_1_target = Detections(
xyxy=np.array([[60, 60, 100, 100]], dtype=np.float64),
class_id=np.array([1]),
)
class_1_pred = Detections.empty()
# Class 2: Prediction exists but no GT (false positive)
class_2_pred = Detections(
xyxy=np.array([[110, 110, 150, 150]], dtype=np.float64),
class_id=np.array([2]),
confidence=np.array([0.8]),
)
class_2_target = Detections.empty()
metric = MeanAveragePrecision()
metric.update([class_0_det], [class_0_det])
metric.update([class_1_pred], [class_1_target])
metric.update([class_2_pred], [class_2_target])
result = metric.compute()
# Should not have negative mAP
assert result.map50_95 >= 0.0
# Should be less than 1.0 due to missed detection and false positive
assert result.map50_95 < 1.0
def test_empty_predictions_and_targets():
"""Test completely empty predictions and targets"""
metric = MeanAveragePrecision()
metric.update([Detections.empty()], [Detections.empty()])
result = metric.compute()
# Should return -1 for no data (matching pycocotools behavior)
assert result.map50_95 == -1
assert result.map50 == -1
assert result.map75 == -1
# All object size categories should also be -1
assert result.small_objects.map50_95 == -1
assert result.medium_objects.map50_95 == -1
assert result.large_objects.map50_95 == -1
@@ -0,0 +1,131 @@
from __future__ import annotations
import numpy as np
import pytest
from supervision.detection.core import Detections
from supervision.metrics.mean_average_precision import MeanAveragePrecision
class TestMeanAveragePrecisionArea:
"""Test area calculation in MeanAveragePrecision."""
@pytest.mark.parametrize(
"xyxy, expected_areas, expected_size_maps",
[
(
np.array(
[
[10, 10, 40, 40], # Small: 900
[100, 100, 200, 150], # Medium: 5000
[300, 300, 500, 400], # Large: 20000
],
dtype=np.float32,
),
[900.0, 5000.0, 20000.0],
{"small": True, "medium": True, "large": True},
),
(
np.array([[0, 0, 10, 10]], dtype=np.float32), # Small: 100
[100.0],
{"small": True, "medium": False, "large": False},
),
(
np.array([[0, 0, 50, 50]], dtype=np.float32), # Medium: 2500
[2500.0],
{"small": False, "medium": True, "large": False},
),
(
np.array([[0, 0, 100, 100]], dtype=np.float32), # Large: 10000
[10000.0],
{"small": False, "medium": False, "large": True},
),
],
)
def test_area_calculation_and_size_specific_map(
self, xyxy, expected_areas, expected_size_maps
):
"""Test area calculation and size-specific mAP functionality."""
gt = Detections(
xyxy=xyxy,
class_id=np.arange(len(xyxy)),
)
pred = Detections(
xyxy=gt.xyxy.copy(),
class_id=gt.class_id.copy(),
confidence=np.full(len(xyxy), 0.9),
)
map_metric = MeanAveragePrecision()
map_metric.update([pred], [gt])
# Test area calculation
prepared_targets = map_metric._prepare_targets(map_metric._targets_list)
areas = [ann["area"] for ann in prepared_targets["annotations"]]
assert np.allclose(areas, expected_areas), (
f"Expected {expected_areas}, got {areas}"
)
# Test size-specific mAP
result = map_metric.compute()
if expected_size_maps["small"]:
assert result.small_objects.map50 > 0.9, (
"Small objects should have high mAP"
)
else:
assert result.small_objects.map50 == -1.0, (
"Small objects should have no data"
)
if expected_size_maps["medium"]:
assert result.medium_objects.map50 > 0.9, (
"Medium objects should have high mAP"
)
else:
assert result.medium_objects.map50 == -1.0, (
"Medium objects should have no data"
)
if expected_size_maps["large"]:
assert result.large_objects.map50 > 0.9, (
"Large objects should have high mAP"
)
else:
assert result.large_objects.map50 == -1.0, (
"Large objects should have no data"
)
def test_area_preserved_from_data(self):
"""Test that area from data field is preserved (COCO case)."""
gt = Detections(
xyxy=np.array(
[[100, 100, 200, 150]], dtype=np.float32
), # Would calculate to 5000
class_id=np.array([0]),
)
# Override with custom area
gt.data = {"area": np.array([3000.0])}
pred = Detections(
xyxy=gt.xyxy.copy(),
class_id=gt.class_id.copy(),
confidence=np.array([0.9]),
)
pred.data = {"area": np.array([3000.0])}
map_metric = MeanAveragePrecision()
map_metric.update([pred], [gt])
prepared_targets = map_metric._prepare_targets(map_metric._targets_list)
used_area = prepared_targets["annotations"][0]["area"]
assert np.allclose(used_area, 3000.0), (
f"Should use provided area 3000.0, got {used_area}"
)
# Verify it's different from what would be calculated
calculated_area = (200 - 100) * (150 - 100) # 100 * 50 = 5000
assert not np.allclose(used_area, calculated_area), (
"Should use provided area, not calculated"
)