Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49ecac0376 | ||
|
|
9251893bd9 | ||
|
|
ccd7098f2f | ||
|
|
07d182be56 | ||
|
|
cca3c14911 | ||
|
|
44546a13f2 | ||
|
|
4b60bbc9cc | ||
|
|
6918d44190 | ||
|
|
a48532f6f5 | ||
|
|
11a586c133 | ||
|
|
31e84f7909 | ||
|
|
7e765eb054 | ||
|
|
393ff52954 | ||
|
|
c9962c9262 | ||
|
|
04b6d45ea2 | ||
|
|
2854932965 | ||
|
|
bcc785fbb5 | ||
|
|
c0df72b84a | ||
|
|
15cf80abac | ||
|
|
b9c24ddafd | ||
|
|
c2490e6015 | ||
|
|
a914a00ab8 | ||
|
|
d3a0a84dd5 |
@@ -19,6 +19,7 @@ exclude = [
|
||||
"http://127.0.0.1:8000", # hint for local docs server
|
||||
"https://sam2.metademolab.com/", # returns 403 Forbidden
|
||||
"https://snyk.io/advisor/python/supervision/badge.svg", # badge URL
|
||||
"https://trendshift.io", # badge API times out in CI
|
||||
"https://universe.roboflow.com/",
|
||||
"https://universe.roboflow.com/model-examples/segmented-animals-basic",
|
||||
# fixme: this page returns 401 Unauthorized when accessed and 404 Not Found when accessed with browser,
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: 🐍 Install uv and set Python version ${{ inputs.python-version }}
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: 🐍 Install uv and set Python
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: 🔗 Link Checker
|
||||
uses: lycheeverse/lychee-action@v2
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -62,6 +62,7 @@ jobs:
|
||||
env:
|
||||
MKDOCS_GIT_COMMITTERS_APIKEY: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if mike list | grep -Eq '^latest(\s|$)'; then mike delete latest; fi
|
||||
mike deploy --push latest
|
||||
|
||||
- name: 🏷️ Determine release deployment metadata
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
<p>
|
||||
<a align="center" href="" target="https://supervision.roboflow.com">
|
||||
<a align="center" href="https://supervision.roboflow.com" target="_blank">
|
||||
<img
|
||||
width="100%"
|
||||
src="https://media.roboflow.com/open-source/supervision/rf-supervision-banner.png?updatedAt=1678995927529"
|
||||
@@ -24,11 +24,27 @@
|
||||
|
||||
</div>
|
||||
|
||||
## 👋 hello
|
||||
<details>
|
||||
<summary><strong>📑 Table of Contents</strong></summary>
|
||||
|
||||
- [👋 Hello](#-hello)
|
||||
- [💻 Install](#-install)
|
||||
- [🔥 Quickstart](#-quickstart)
|
||||
- [Models](#models)
|
||||
- [Annotators](#annotators)
|
||||
- [Datasets](#datasets)
|
||||
- [🎬 Tutorials](#-tutorials)
|
||||
- [💜 Built with Supervision](#-built-with-supervision)
|
||||
- [📚 Documentation](#-documentation)
|
||||
- [🏆 Contribution](#-contribution)
|
||||
|
||||
</details>
|
||||
|
||||
## 👋 Hello
|
||||
|
||||
**We are your essential toolkit for computer vision.** From data loading to real-time zone counting, we provide the building blocks so you can focus on building applications around your models. 🤝
|
||||
|
||||
## 💻 install
|
||||
## 💻 Install
|
||||
|
||||
Pip install the supervision package in a [**Python>=3.9**](https://www.python.org/) environment.
|
||||
|
||||
@@ -38,9 +54,9 @@ pip install supervision
|
||||
|
||||
Read more about conda, mamba, and installing from source in our [guide](https://roboflow.github.io/supervision/).
|
||||
|
||||
## 🔥 quickstart
|
||||
## 🔥 Quickstart
|
||||
|
||||
### models
|
||||
### Models
|
||||
|
||||
Supervision was designed to be model agnostic. Just plug in any classification, detection, or segmentation model. For your convenience, we have created [connectors](https://supervision.roboflow.com/latest/detection/core/#detections) for the most popular libraries like Ultralytics, Transformers, MMDetection, or Inference. Other integrations, like `rfdetr`, already return `sv.Detections` directly.
|
||||
|
||||
@@ -51,7 +67,7 @@ import supervision as sv
|
||||
from PIL import Image
|
||||
from rfdetr import RFDETRSmall
|
||||
|
||||
image = Image.open(...)
|
||||
image = Image.open("path/to/image.jpg")
|
||||
model = RFDETRSmall()
|
||||
detections = model.predict(image, threshold=0.5)
|
||||
|
||||
@@ -71,7 +87,7 @@ len(detections)
|
||||
from PIL import Image
|
||||
from inference import get_model
|
||||
|
||||
image = Image.open(...)
|
||||
image = Image.open("path/to/image.jpg")
|
||||
model = get_model(model_id="rfdetr-small", api_key="ROBOFLOW_API_KEY")
|
||||
result = model.infer(image)[0]
|
||||
detections = sv.Detections.from_inference(result)
|
||||
@@ -82,7 +98,7 @@ len(detections)
|
||||
|
||||
</details>
|
||||
|
||||
### annotators
|
||||
### Annotators
|
||||
|
||||
Supervision offers a wide range of highly customizable [annotators](https://supervision.roboflow.com/latest/detection/annotators/), allowing you to compose the perfect visualization for your use case.
|
||||
|
||||
@@ -90,7 +106,8 @@ Supervision offers a wide range of highly customizable [annotators](https://supe
|
||||
import cv2
|
||||
import supervision as sv
|
||||
|
||||
image = cv2.imread(...)
|
||||
image = cv2.imread("path/to/image.jpg")
|
||||
# Assuming detections are obtained from a model
|
||||
detections = sv.Detections(...)
|
||||
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
@@ -99,7 +116,7 @@ annotated_frame = box_annotator.annotate(scene=image.copy(), detections=detectio
|
||||
|
||||
https://github.com/roboflow/supervision/assets/26109316/691e219c-0565-4403-9218-ab5644f39bce
|
||||
|
||||
### datasets
|
||||
### Datasets
|
||||
|
||||
Supervision provides a set of [utils](https://supervision.roboflow.com/latest/datasets/core/) that allow you to load, split, merge, and save datasets in one of the supported formats.
|
||||
|
||||
@@ -123,7 +140,7 @@ for path, image, annotation in ds:
|
||||
pass
|
||||
```
|
||||
|
||||
<details close>
|
||||
<details>
|
||||
<summary>👉 more dataset utils</summary>
|
||||
|
||||
- load
|
||||
@@ -213,7 +230,7 @@ for path, image, annotation in ds:
|
||||
|
||||
</details>
|
||||
|
||||
## 🎬 tutorials
|
||||
## 🎬 Tutorials
|
||||
|
||||
Want to learn how to use Supervision? Explore our [how-to guides](https://supervision.roboflow.com/develop/how_to/detect_and_annotate/), [end-to-end examples](./examples), [cheatsheet](https://roboflow.github.io/cheatsheet-supervision/), and [cookbooks](https://supervision.roboflow.com/develop/cookbooks/)!
|
||||
|
||||
@@ -233,7 +250,7 @@ Want to learn how to use Supervision? Explore our [how-to guides](https://superv
|
||||
<div><strong>Created: 11 Jan 2024</strong></div>
|
||||
<br/>Learn how to track and estimate the speed of vehicles using YOLO, ByteTrack, and Roboflow Inference. This comprehensive tutorial covers object detection, multi-object tracking, filtering detections, perspective transformation, speed estimation, visualization improvements, and more.</p>
|
||||
|
||||
## 💜 built with supervision
|
||||
## 💜 Built with Supervision
|
||||
|
||||
Did you build something cool using supervision? [Let us know!](https://github.com/roboflow/supervision/discussions/categories/built-with-supervision)
|
||||
|
||||
@@ -243,11 +260,11 @@ https://github.com/roboflow/supervision/assets/26109316/c9436828-9fbf-4c25-ae8c-
|
||||
|
||||
https://github.com/roboflow/supervision/assets/26109316/3ac6982f-4943-4108-9b7f-51787ef1a69f
|
||||
|
||||
## 📚 documentation
|
||||
## 📚 Documentation
|
||||
|
||||
Visit our [documentation](https://roboflow.github.io/supervision) page to learn how supervision can help you build computer vision applications faster and more reliably.
|
||||
|
||||
## 🏆 contribution
|
||||
## 🏆 Contribution
|
||||
|
||||
We love your input! Please see our [contributing guide](.github/CONTRIBUTING.md) to get started. Thank you 🙏 to all our contributors!
|
||||
|
||||
@@ -259,8 +276,6 @@ We love your input! Please see our [contributing guide](.github/CONTRIBUTING.md)
|
||||
|
||||
<br>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<div align="center">
|
||||
<a href="https://youtube.com/roboflow">
|
||||
<img
|
||||
@@ -295,6 +310,7 @@ We love your input! Please see our [contributing guide](.github/CONTRIBUTING.md)
|
||||
src="https://media.roboflow.com/notebooks/template/icons/purple/forum.png?ik-sdk-version=javascript-1.4.3&updatedAt=1672949633584"
|
||||
width="3%"
|
||||
/>
|
||||
</a>
|
||||
<img src="https://raw.githubusercontent.com/ultralytics/assets/main/social/logo-transparent.png" width="3%"/>
|
||||
<a href="https://blog.roboflow.com">
|
||||
<img
|
||||
@@ -302,6 +318,4 @@ We love your input! Please see our [contributing guide](.github/CONTRIBUTING.md)
|
||||
width="3%"
|
||||
/>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+45
-11
@@ -1,23 +1,55 @@
|
||||
---
|
||||
description: "Full version history of the supervision Python library — release notes, breaking changes, new features, and deprecations for every version."
|
||||
date_modified: 2026-06-15
|
||||
date_modified: 2026-06-16
|
||||
---
|
||||
|
||||
# Changelog
|
||||
|
||||
### UnReleased
|
||||
### 0.29.1 <small>Jun 23, 2026</small>
|
||||
|
||||
- Added [#2338](https://github.com/roboflow/supervision/pull/2338): [`sv.KeyPoints.with_nms`](https://supervision.roboflow.com/latest/keypoint/core/#supervision.key_points.core.KeyPoints.with_nms) — non-maximum suppression for keypoint detections. Derives axis-aligned bounding boxes from valid (non-zero and visible) keypoints and applies `box_non_max_suppression`. Requires `detection_confidence`; supports class-aware and class-agnostic modes via `threshold`, `class_agnostic`, and `overlap_metric`.
|
||||
|
||||
- Fixed [#2342](https://github.com/roboflow/supervision/pull/2342): `sv.Detections.from_vlm` with `sv.VLM.GOOGLE_GEMINI_2_0`, `sv.VLM.GOOGLE_GEMINI_2_5`, and `sv.VLM.QWEN_2_5_VL` no longer raises when the model returns valid JSON of the wrong shape (non-list top-level or non-dict elements). A non-string or malformed `"mask"` value in Gemini 2.5 output no longer triggers `AttributeError`; invalid base64 or non-PNG mask data falls back to an empty mask, keeping `xyxy`, `confidence`, and `masks` arrays aligned.
|
||||
|
||||
- Fixed [#2341](https://github.com/roboflow/supervision/pull/2341): `sv.DetectionDataset.as_pascal_voc` no longer mutates the source `Detections.xyxy` by the 1-index offset on every call. Previously, repeated exports accumulated a `+1` shift in the caller's bounding boxes.
|
||||
|
||||
- Fixed [#2334](https://github.com/roboflow/supervision/pull/2334): `sv.JSONSink` now serializes NumPy scalars (e.g. `np.int64` frame indices) in `custom_data` as JSON numbers instead of raising `TypeError` at close time. File handle is now guaranteed to close even when serialization fails.
|
||||
|
||||
- Fixed [#2333](https://github.com/roboflow/supervision/pull/2333): [`sv.DetectionsSmoother`](https://supervision.roboflow.com/latest/detection/tools/smoother/#supervision.detection.tools.smoother.DetectionsSmoother) no longer raises when smoothing detections without `confidence`. Confidence is now averaged over the frames that carry it; when tracks in the same frame disagree on confidence presence, `confidence` is set to `None` for all smoothed detections.
|
||||
|
||||
- Fixed [#2332](https://github.com/roboflow/supervision/pull/2332): `sv.approximate_polygon` now returns a polygon within the requested point-count budget (at most `floor(N * (1 - percentage))` points, minimum 3). The function now also validates that `epsilon_step > 0`.
|
||||
|
||||
- Fixed [#2331](https://github.com/roboflow/supervision/pull/2331): `sv.Precision` and `sv.F1Score` now count predictions on background images (empty target set) as false positives, and count predictions of classes absent from ground truth as false positives under `MICRO` and `MACRO` averaging. Previously both edge cases were silently ignored, inflating scores. `WEIGHTED` averaging is unchanged — absent classes retain weight 0, consistent with scikit-learn. Users relying on previous scores should re-evaluate after upgrading; no API change is required.
|
||||
|
||||
- Fixed [#2322](https://github.com/roboflow/supervision/pull/2322): COCO export now preserves all polygon parts for multi-component masks. Previously, only the first polygon was written when a non-crowd mask had disjoint segments; all parts are now included.
|
||||
|
||||
- Performance [#2339](https://github.com/roboflow/supervision/pull/2339): `sv.HaloAnnotator` now uses the same CompactMask painting path as `sv.MaskAnnotator` via a shared `_paint_masks_by_area` helper. On a 1080p frame with 30 CompactMask detections, `HaloAnnotator` runs approximately 4× faster; annotated output is unchanged.
|
||||
|
||||
- Performance [#2330](https://github.com/roboflow/supervision/pull/2330): `sv.mask_to_xyxy` and `sv.KeyPoints.as_detections` are now vectorized. `mask_to_xyxy` uses batched occupancy-profile reductions instead of per-mask pixel scans; `KeyPoints.as_detections` computes all bounding boxes in a single batch operation. Both produce bit-identical results.
|
||||
|
||||
- Performance [#2323](https://github.com/roboflow/supervision/pull/2323): Mask IoU computation now uses matrix multiplication on flattened masks instead of an explicit `(N, M, H, W)` intersection tensor, reducing peak memory for large mask sets. For masks larger than 4096×4096 pixels, computation automatically promotes to float64 to preserve exact pixel counts. Results are numerically identical.
|
||||
|
||||
### 0.29.0.post0 <small>Jun 17, 2026</small>
|
||||
|
||||
- Fixed [#2335](https://github.com/roboflow/supervision/pull/2335): `sv.KeyPoints(confidence=...)` now works again. The `0.29.0` refactor accidentally dropped the deprecated `confidence` constructor kwarg; it is now accepted and mapped to `keypoint_confidence` with a deprecation warning.
|
||||
|
||||
### 0.29.0 <small>Jun 15, 2026</small>
|
||||
|
||||
- Added [#2314](https://github.com/roboflow/supervision/pull/2314): new cookbook **Oriented Bounding Boxes** showing how an oriented box differs from an axis-aligned one on a marina of boats: DOTA-pretrained detection, the effect on [`with_nms`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.with_nms) and [`Detections.area`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.area), and YOLO OBB dataset export.
|
||||
|
||||
- Fixed [#2306](https://github.com/roboflow/supervision/pull/2306): [`sv.Detections.area`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.area) now returns the rotated body's area for detections carrying `data["xyxyxyxy"]` (oriented box corners) instead of the area of the derived axis-aligned bounding box, which overestimates by up to ~2x at 45° rotation. Affects annotator z-ordering inside [`MaskAnnotator`](https://supervision.roboflow.com/0.29.0/detection/annotators/#supervision.annotators.core.MaskAnnotator) and [`HaloAnnotator`](https://supervision.roboflow.com/0.29.0/detection/annotators/#supervision.annotators.core.HaloAnnotator), and any user code that filters or sorts OBB detections by area. The mask path and the non-OBB AABB fallback are unchanged.
|
||||
|
||||
- Added [#2277](https://github.com/roboflow/supervision/pull/2277), [#2286](https://github.com/roboflow/supervision/pull/2286): [`sv.VertexEllipseAreaAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.VertexEllipseAreaAnnotator), [`sv.VertexEllipseOutlineAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.VertexEllipseOutlineAnnotator), and [`sv.VertexEllipseHaloAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.VertexEllipseHaloAnnotator) for visualizing keypoint uncertainty as covariance ellipses. Requires models that output keypoint uncertainty (e.g. RF-DETR keypoint models).
|
||||
|
||||
- Added [#2303](https://github.com/roboflow/supervision/pull/2303): [`sv.oriented_box_non_max_suppression`](https://supervision.roboflow.com/0.29.0/detection/utils/iou_and_nms/#supervision.detection.utils.iou_and_nms.oriented_box_non_max_suppression) and [`sv.oriented_box_non_max_merge`](https://supervision.roboflow.com/0.29.0/detection/utils/iou_and_nms/#supervision.detection.utils.iou_and_nms.oriented_box_non_max_merge) for performing NMS and NMM directly on oriented bounding boxes using oriented-box IoU instead of axis-aligned IoU.
|
||||
|
||||
- Added [#2247](https://github.com/roboflow/supervision/pull/2247): [`sv.ConfusionMatrix`](https://supervision.roboflow.com/0.29.0/metrics/detection/#supervision.metrics.detection.ConfusionMatrix) now supports `MetricTarget.ORIENTED_BOUNDING_BOXES`, computing IoU via `oriented_box_iou_batch` on `xyxyxyxy` corners.
|
||||
- Added [#2247](https://github.com/roboflow/supervision/pull/2247): [`sv.ConfusionMatrix`](https://supervision.roboflow.com/0.29.0/detection/metrics/#supervision.metrics.detection.ConfusionMatrix) now supports `MetricTarget.ORIENTED_BOUNDING_BOXES`, computing IoU via `oriented_box_iou_batch` on `xyxyxyxy` corners. Previously, OBB inputs silently fell back to axis-aligned bounding-box IoU, producing incorrect match scores for rotated detections.
|
||||
|
||||
- Added [#2252](https://github.com/roboflow/supervision/pull/2252): [`sv.process_video`](https://supervision.roboflow.com/0.29.0/utils/video/#supervision.utils.video.process_video) gains a `preserve_audio` parameter. When enabled, the audio stream from the source video is muxed into the output using ffmpeg.
|
||||
|
||||
- Added [#2302](https://github.com/roboflow/supervision/pull/2302), [#2289](https://github.com/roboflow/supervision/pull/2289): [`sv.DetectionDataset.as_yolo`](https://supervision.roboflow.com/0.29.0/datasets/#supervision.dataset.core.DetectionDataset.as_yolo) gains an `is_obb` parameter for exporting oriented bounding box annotations in the YOLO OBB format (9-token lines with 4 corner coordinates).
|
||||
- Added [#2302](https://github.com/roboflow/supervision/pull/2302), [#2289](https://github.com/roboflow/supervision/pull/2289): [`sv.DetectionDataset.as_yolo`](https://supervision.roboflow.com/0.29.0/datasets/core/#supervision.dataset.core.DetectionDataset.as_yolo) gains an `is_obb` parameter for exporting oriented bounding box annotations in the YOLO OBB format (9-token lines with 4 corner coordinates).
|
||||
|
||||
- Added [#2312](https://github.com/roboflow/supervision/pull/2312): [`sv.xyxyxyxy_to_xyxy`](https://supervision.roboflow.com/0.29.0/detection/utils/boxes/#supervision.detection.utils.boxes.xyxyxyxy_to_xyxy) — vectorised utility that converts oriented bounding box corners `(N, 4, 2)` to axis-aligned bounding boxes `(N, 4)`.
|
||||
|
||||
- Changed [#2286](https://github.com/roboflow/supervision/pull/2286): [`sv.KeyPoints`](https://supervision.roboflow.com/0.29.0/keypoint/core/#supervision.key_points.core.KeyPoints) now separates keypoint-level and detection-level confidence into distinct fields: `keypoint_confidence` (shape `(n, m)`) and `detection_confidence` (shape `(n,)`). A new `visible` mask (shape `(n, m)`) controls per-keypoint visibility. The legacy `KeyPoints.confidence` property still works but is deprecated.
|
||||
|
||||
@@ -25,29 +57,31 @@ date_modified: 2026-06-15
|
||||
|
||||
- Changed [#2286](https://github.com/roboflow/supervision/pull/2286): [`sv.EdgeAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.EdgeAnnotator) and [`sv.VertexLabelAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.VertexLabelAnnotator) now support per-class skeleton definitions, enabling correct rendering when multiple skeleton topologies (e.g. person + animal) coexist in one frame.
|
||||
|
||||
- Changed [#2303](https://github.com/roboflow/supervision/pull/2303): [`sv.Detections.with_nms`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.with_nms) and [`sv.Detections.with_nmm`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.with_nmm) now use oriented-box IoU when `data["xyxyxyxy"]` coordinates are present, instead of axis-aligned box IoU.
|
||||
- Changed [#2303](https://github.com/roboflow/supervision/pull/2303): [`sv.Detections.with_nms`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.with_nms) and [`sv.Detections.with_nmm`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.with_nmm) now use oriented-box IoU when `data["xyxyxyxy"]` coordinates are present, instead of axis-aligned box IoU. Callers relying on the previous axis-aligned behaviour should remove `data["xyxyxyxy"]` before calling, or recalibrate any IoU thresholds.
|
||||
|
||||
- Changed [#2312](https://github.com/roboflow/supervision/pull/2312): [`sv.Detections.with_nmm`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.with_nmm) now computes the merged oriented bounding box as the tightest rectangle at the winner's orientation enclosing all corners from every detection in a merge group.
|
||||
|
||||
- Changed [#2306](https://github.com/roboflow/supervision/pull/2306): [`sv.Detections.area`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.area) now returns the polygon area of the rotated bounding box (via the shoelace formula) when oriented box coordinates are present, instead of the axis-aligned box area.
|
||||
- Changed [#2325](https://github.com/roboflow/supervision/pull/2325): [`sv.VertexEllipseAreaAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.VertexEllipseAreaAnnotator), [`sv.VertexEllipseOutlineAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.VertexEllipseOutlineAnnotator), and [`sv.VertexEllipseHaloAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.VertexEllipseHaloAnnotator) now draw sigma levels level-by-level (outermost first) across all points, ensuring correct visual layering when ellipses overlap.
|
||||
|
||||
- Changed [#2256](https://github.com/roboflow/supervision/pull/2256): [`sv.InferenceSlicer`](https://supervision.roboflow.com/0.29.0/detection/tools/inference_slicer/#supervision.detection.tools.inference_slicer.InferenceSlicer) now detects OBB outputs from callbacks and automatically falls back to sequential processing to avoid thread-safety issues when `thread_workers > 1`.
|
||||
|
||||
- Fixed [#2282](https://github.com/roboflow/supervision/pull/2282): [`sv.oriented_box_iou_batch`](https://supervision.roboflow.com/0.29.0/detection/utils/iou_and_nms/#supervision.detection.utils.iou_and_nms.oriented_box_iou_batch) now correctly handles non-square canvases. Previously, rasterization assumed square dimensions, leading to incorrect IoU values for tall or wide images.
|
||||
- Changed [#2324](https://github.com/roboflow/supervision/pull/2324): Project-wide deprecation policy unified to a minimum 3-minor-release window. All current deprecations (including `KeyPoints.confidence` and `validate_*` helpers) are scheduled for removal in `0.32.0`.
|
||||
|
||||
- Fixed [#2252](https://github.com/roboflow/supervision/pull/2252): [`sv.process_video`](https://supervision.roboflow.com/0.29.0/utils/video/#supervision.utils.video.process_video) audio muxing path now correctly creates temp files on the same filesystem, decodes ffmpeg errors, and avoids muxing incomplete output.
|
||||
|
||||
- Fixed [#2282](https://github.com/roboflow/supervision/pull/2282), [#2317](https://github.com/roboflow/supervision/pull/2317): [`sv.oriented_box_iou_batch`](https://supervision.roboflow.com/0.29.0/detection/utils/iou_and_nms/#supervision.detection.utils.iou_and_nms.oriented_box_iou_batch) now computes exact IoU via convex polygon intersection (`cv2.intersectConvexConvex`) and uses an axis-aligned bounding box envelope gate to skip pairs that cannot overlap, improving both accuracy and performance. Previously, rasterization on a discrete grid was used, which assumed square dimensions and introduced quantisation noise.
|
||||
|
||||
- Fixed [#2239](https://github.com/roboflow/supervision/pull/2239): [`sv.Detections.from_vlm`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.from_vlm) no longer returns `None` for `class_id` on empty VLM parses; now returns an empty int ndarray.
|
||||
|
||||
- Fixed [#2270](https://github.com/roboflow/supervision/pull/2270): [`sv.Detections.from_inference`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.from_inference) now preserves `class_name` as a string-dtype array when predictions are empty.
|
||||
|
||||
- Fixed [#2269](https://github.com/roboflow/supervision/pull/2269): [`sv.HeatMapAnnotator`](https://supervision.roboflow.com/0.29.0/annotators/#supervision.annotators.core.HeatMapAnnotator) no longer crashes with a divide-by-zero when called with empty detections.
|
||||
- Fixed [#2269](https://github.com/roboflow/supervision/pull/2269): [`sv.HeatMapAnnotator`](https://supervision.roboflow.com/0.29.0/detection/annotators/#supervision.annotators.core.HeatMapAnnotator) no longer crashes with a divide-by-zero when called with empty detections.
|
||||
|
||||
- Fixed [#2276](https://github.com/roboflow/supervision/pull/2276): COCO export now emits 1-indexed `category_id` values as required by the COCO specification.
|
||||
|
||||
- Fixed [#2267](https://github.com/roboflow/supervision/pull/2267): COCO annotation and image IDs are now sequential across train/val/test splits via `starting_image_id` and `starting_annotation_id` parameters.
|
||||
|
||||
- Fixed [#2289](https://github.com/roboflow/supervision/pull/2289): [`sv.DetectionDataset.as_yolo`](https://supervision.roboflow.com/0.29.0/datasets/#supervision.dataset.core.DetectionDataset.as_yolo) no longer loses OBB rotation when exporting oriented bounding boxes.
|
||||
- Fixed [#2289](https://github.com/roboflow/supervision/pull/2289): [`sv.DetectionDataset.as_yolo`](https://supervision.roboflow.com/0.29.0/datasets/core/#supervision.dataset.core.DetectionDataset.as_yolo) no longer loses OBB rotation when exporting oriented bounding boxes.
|
||||
|
||||
- Fixed [#2296](https://github.com/roboflow/supervision/pull/2296): YOLO dataset loading now sorts class names by numeric keys when `data.yaml` uses integer class IDs.
|
||||
|
||||
@@ -55,6 +89,8 @@ date_modified: 2026-06-15
|
||||
|
||||
- Fixed [#2298](https://github.com/roboflow/supervision/pull/2298): File extension filters now normalize casing (e.g. `.JPG` matches `.jpg`).
|
||||
|
||||
- Fixed [#2321](https://github.com/roboflow/supervision/pull/2321): [`sv.DetectionDataset.as_coco()`](https://supervision.roboflow.com/0.29.0/datasets/core/#supervision.dataset.core.DetectionDataset.as_coco) now round-trips polygon and RLE segmentation data. Segmentations loaded from COCO annotations are preserved in `detections.data["coco_raw_segmentation"]` and written back on export, preventing data loss in train/val/test split workflows.
|
||||
|
||||
- Deprecated: `KeyPoints.confidence` (use `KeyPoints.keypoint_confidence`), `merge_inner_detection_object_pair`, `merge_inner_detections_objects`, `merge_inner_detections_objects_without_iou`, `validate_detections_fields`, `validate_vlm_parameters`, `validate_fields_both_defined_or_none`, `validate_xyxy`, `validate_mask`, `validate_class_id`, `validate_confidence`, `validate_tracker_id`, `validate_data`, `validate_xy`, `validate_key_point_confidence`, `validate_key_points_fields`, `validate_resolution`, `validate_custom_values`, `validate_input_tensors`, and `validate_labels` are deprecated in `0.29.0` and will be removed in `0.32.0`.
|
||||
|
||||
### 0.28.0 <small>Apr 30, 2026</small>
|
||||
@@ -77,8 +113,6 @@ date_modified: 2026-06-15
|
||||
|
||||
- Added [#932](https://github.com/roboflow/supervision/pull/932): [`sv.ImageAssets`](https://supervision.roboflow.com/latest/assets/) for downloading sample images alongside existing video assets, useful for examples and tutorials.
|
||||
|
||||
- Added [#2247](https://github.com/roboflow/supervision/pull/2247): [`sv.ConfusionMatrix`](https://supervision.roboflow.com/metrics/detection/#confusionmatrix) now accepts `metric_target=MetricTarget.ORIENTED_BOUNDING_BOXES`, computing IoU via `oriented_box_iou_batch` over `xyxyxyxy` coordinates. Previously, OBB inputs silently fell back to axis-aligned bounding-box IoU, producing incorrect match scores for rotated detections.
|
||||
|
||||
- Changed [#2169](https://github.com/roboflow/supervision/pull/2169): [`sv.MeanAveragePrecisionResult`](https://supervision.roboflow.com/latest/metrics/mean_average_precision/) and related metric arrays (`mAP_scores`, `ap_per_class`, `iou_thresholds`, precision/recall) are now `float32` instead of `float64`. Reduces memory and speeds up computation; numerical results may differ in the last few digits.
|
||||
|
||||
- Changed [#2178](https://github.com/roboflow/supervision/pull/2178): [`sv.rle_to_mask`](https://supervision.roboflow.com/latest/detection/utils/converters/#supervision.detection.utils.converters.rle_to_mask) and [`sv.mask_to_rle`](https://supervision.roboflow.com/latest/detection/utils/converters/#supervision.detection.utils.converters.mask_to_rle) moved to `supervision.detection.utils.converters`. The old import path `supervision.dataset.utils` continues to work but is deprecated.
|
||||
|
||||
+311
File diff suppressed because one or more lines are too long
Vendored
+4
@@ -62,6 +62,10 @@
|
||||
<p class="card repo-card" data-name="Memory-Efficient Instance Segmentation"
|
||||
data-labels="COMPACT MASK,SAM3,SEGMENTATION" data-version="v0.28.0" data-author="Borda"></p>
|
||||
</a>
|
||||
<a href="../notebooks/oriented-bounding-boxes/">
|
||||
<p class="card repo-card" data-name="Oriented Bounding Boxes for Densely Packed Objects"
|
||||
data-labels="OBB,DETECTIONS,NMS,DATASET" data-version="v0.29.0" data-author="kounelisagis"></p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ requires = [ "setuptools>=61" ]
|
||||
|
||||
[project]
|
||||
name = "supervision"
|
||||
version = "0.29.0"
|
||||
version = "0.29.1"
|
||||
description = "A set of easy-to-use utils that will come in handy in any Computer Vision project"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
|
||||
@@ -361,6 +361,65 @@ class OrientedBoxAnnotator(BaseAnnotator):
|
||||
return scene
|
||||
|
||||
|
||||
# --- Shared mask-painting utilities ---
|
||||
def _paint_masks_by_area(
|
||||
canvas: npt.NDArray[np.uint8],
|
||||
detections: Detections,
|
||||
color: Color | ColorPalette,
|
||||
color_lookup: ColorLookup | npt.NDArray[np.int_],
|
||||
collect_union: bool = False,
|
||||
) -> npt.NDArray[np.bool_] | None:
|
||||
"""Paint each detection's mask into `canvas` in descending-area order.
|
||||
|
||||
Smaller masks are drawn on top of larger ones. `CompactMask` detections
|
||||
are painted into their bounding-box crop only, avoiding a full `(H, W)`
|
||||
allocation per mask; dense masks fall back to full-frame boolean indexing.
|
||||
|
||||
Args:
|
||||
canvas: BGR image array painted in place. Shape ``(H, W, 3)``.
|
||||
detections: Detections whose masks to paint. Returns immediately
|
||||
without modifying `canvas` when ``detections.mask`` is ``None``.
|
||||
color: Single color or palette used to resolve each detection's color.
|
||||
color_lookup: Strategy for mapping colors to detection indices.
|
||||
collect_union: When ``True``, allocate and return a ``(H, W)``
|
||||
boolean array that accumulates the union of all painted masks
|
||||
(useful for callers like `HaloAnnotator` that need the combined
|
||||
mask footprint). When ``False`` (default), returns ``None``.
|
||||
|
||||
Returns:
|
||||
A ``(H, W)`` boolean union array when ``collect_union=True``,
|
||||
otherwise ``None``.
|
||||
"""
|
||||
masks = detections.mask
|
||||
if masks is None:
|
||||
return None
|
||||
union: npt.NDArray[np.bool_] | None = (
|
||||
np.zeros(canvas.shape[:2], dtype=bool) if collect_union else None
|
||||
)
|
||||
compact_mask = masks if isinstance(masks, CompactMask) else None
|
||||
for detection_idx in np.flip(np.argsort(detections.area)):
|
||||
color_bgr = resolve_color(
|
||||
color=color,
|
||||
detections=detections,
|
||||
detection_idx=detection_idx,
|
||||
color_lookup=color_lookup,
|
||||
).as_bgr()
|
||||
if compact_mask is not None:
|
||||
x1 = int(compact_mask.offsets[detection_idx, 0])
|
||||
y1 = int(compact_mask.offsets[detection_idx, 1])
|
||||
crop_m = compact_mask.crop(detection_idx)
|
||||
crop_h, crop_w = crop_m.shape
|
||||
canvas[y1 : y1 + crop_h, x1 : x1 + crop_w][crop_m] = color_bgr
|
||||
if union is not None:
|
||||
union[y1 : y1 + crop_h, x1 : x1 + crop_w] |= crop_m
|
||||
else:
|
||||
mask = np.asarray(masks[detection_idx], dtype=bool)
|
||||
canvas[mask] = color_bgr
|
||||
if union is not None:
|
||||
union |= mask
|
||||
return union
|
||||
|
||||
|
||||
class MaskAnnotator(BaseAnnotator):
|
||||
"""
|
||||
A class for drawing masks on an image using provided detections.
|
||||
@@ -437,35 +496,12 @@ class MaskAnnotator(BaseAnnotator):
|
||||
return scene
|
||||
|
||||
colored_mask = np.array(scene, copy=True, dtype=np.uint8)
|
||||
|
||||
compact_mask = (
|
||||
detections.mask if isinstance(detections.mask, CompactMask) else None
|
||||
_paint_masks_by_area(
|
||||
colored_mask,
|
||||
detections,
|
||||
self.color,
|
||||
self.color_lookup if custom_color_lookup is None else custom_color_lookup,
|
||||
)
|
||||
for detection_idx in np.flip(np.argsort(detections.area)):
|
||||
color = resolve_color(
|
||||
color=self.color,
|
||||
detections=detections,
|
||||
detection_idx=detection_idx,
|
||||
color_lookup=self.color_lookup
|
||||
if custom_color_lookup is None
|
||||
else custom_color_lookup,
|
||||
)
|
||||
if compact_mask is not None:
|
||||
# Paint only the bounding-box crop — avoids a full (H, W) alloc.
|
||||
x1 = int(compact_mask.offsets[detection_idx, 0])
|
||||
y1 = int(compact_mask.offsets[detection_idx, 1])
|
||||
crop_m = compact_mask.crop(detection_idx)
|
||||
crop_h, crop_w = crop_m.shape
|
||||
colored_mask[y1 : y1 + crop_h, x1 : x1 + crop_w][crop_m] = (
|
||||
color.as_bgr()
|
||||
)
|
||||
else:
|
||||
mask = np.asarray(
|
||||
detections.mask[detection_idx],
|
||||
dtype=bool,
|
||||
)
|
||||
colored_mask[mask] = color.as_bgr()
|
||||
|
||||
cv2.addWeighted(
|
||||
colored_mask, self.opacity, scene, 1 - self.opacity, 0, dst=scene
|
||||
)
|
||||
@@ -701,7 +737,7 @@ class HaloAnnotator(BaseAnnotator):
|
||||
Annotates the given scene with halos based on the provided detections.
|
||||
|
||||
Args:
|
||||
scene: The image where masks will be drawn.
|
||||
scene: The image where the halo effect will be applied.
|
||||
`ImageType` is a flexible type, accepting either `numpy.ndarray`
|
||||
or `PIL.Image.Image`.
|
||||
detections: Object detections to annotate.
|
||||
@@ -719,6 +755,7 @@ class HaloAnnotator(BaseAnnotator):
|
||||
>>> image = np.zeros((100, 100, 3), dtype=np.uint8)
|
||||
>>> detections = sv.Detections(
|
||||
... xyxy=np.array([[20, 20, 80, 80]]),
|
||||
... mask=np.zeros((1, 100, 100), dtype=bool),
|
||||
... class_id=np.array([0])
|
||||
... )
|
||||
>>> halo_annotator = sv.HaloAnnotator()
|
||||
@@ -737,28 +774,23 @@ class HaloAnnotator(BaseAnnotator):
|
||||
if detections.mask is None:
|
||||
return scene
|
||||
colored_mask = np.zeros_like(scene, dtype=np.uint8)
|
||||
fmask = np.array([False] * scene.shape[0] * scene.shape[1]).reshape(
|
||||
scene.shape[0], scene.shape[1]
|
||||
fmask = _paint_masks_by_area(
|
||||
colored_mask,
|
||||
detections,
|
||||
self.color,
|
||||
self.color_lookup if custom_color_lookup is None else custom_color_lookup,
|
||||
collect_union=True,
|
||||
)
|
||||
|
||||
for detection_idx in np.flip(np.argsort(detections.area)):
|
||||
color = resolve_color(
|
||||
color=self.color,
|
||||
detections=detections,
|
||||
detection_idx=detection_idx,
|
||||
color_lookup=self.color_lookup
|
||||
if custom_color_lookup is None
|
||||
else custom_color_lookup,
|
||||
)
|
||||
mask = np.asarray(detections.mask[detection_idx], dtype=bool)
|
||||
fmask = np.logical_or(fmask, mask)
|
||||
color_bgr = color.as_bgr()
|
||||
colored_mask[mask] = color_bgr
|
||||
assert fmask is not None # collect_union=True always returns an array
|
||||
|
||||
colored_mask = cv2.blur(colored_mask, (self.kernel_size, self.kernel_size))
|
||||
colored_mask[fmask] = [0, 0, 0]
|
||||
gray = cv2.cvtColor(colored_mask, cv2.COLOR_BGR2GRAY)
|
||||
alpha = self.opacity * gray / gray.max()
|
||||
gray_max = gray.max()
|
||||
if gray_max == 0:
|
||||
# no halo to draw (e.g. empty masks); leave the scene untouched
|
||||
return scene
|
||||
alpha = self.opacity * gray / gray_max
|
||||
alpha_mask = alpha[:, :, np.newaxis]
|
||||
blended_scene = np.uint8(scene * (1 - alpha_mask) + colored_mask * self.opacity)
|
||||
np.copyto(scene, blended_scene)
|
||||
|
||||
@@ -247,6 +247,18 @@ def detections_to_coco_annotations(
|
||||
Raises:
|
||||
ValueError: If any detection has ``class_id`` equal to ``None``.
|
||||
|
||||
Note:
|
||||
For ``iscrowd=0`` annotations, ``segmentation`` is a
|
||||
``list[list[float]]`` where each inner list encodes one polygon
|
||||
part as flat ``[x1, y1, x2, y2, ...]`` coordinates. A single
|
||||
object with *N* disjoint parts produces *N* inner lists.
|
||||
|
||||
When ``iscrowd`` is not in ``detections.data``, masks with holes
|
||||
or multiple disjoint segments are auto-encoded as RLE
|
||||
(``iscrowd=1``); simple single-region masks use polygon format
|
||||
(``iscrowd=0``). Supply ``data={"iscrowd": np.array([0])}`` to
|
||||
force polygon output regardless of mask topology.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
import numpy as np
|
||||
|
||||
@@ -21,13 +21,41 @@ def object_to_pascal_voc(
|
||||
name: str,
|
||||
polygon: npt.NDArray[np.number] | None = None,
|
||||
) -> Element:
|
||||
"""Build a Pascal VOC ``<object>`` XML element for one detection.
|
||||
|
||||
Coordinates are converted to 1-indexed Pascal VOC convention before writing.
|
||||
The input arrays are never mutated; new arrays are allocated for the offset.
|
||||
|
||||
Args:
|
||||
xyxy: Bounding box in zero-indexed pixel coordinates ``[x1, y1, x2, y2]``.
|
||||
Shape ``(4,)``.
|
||||
name: Class label string written to the ``<name>`` child element.
|
||||
polygon: Optional segmentation polygon in zero-indexed pixel coordinates.
|
||||
Shape ``(N, 2)``.
|
||||
|
||||
Returns:
|
||||
An XML ``Element`` rooted at ``<object>`` containing ``<name>``,
|
||||
``<bndbox>``, and optionally ``<polygon>`` children.
|
||||
|
||||
Examples:
|
||||
>>> import numpy as np
|
||||
>>> from supervision.dataset.formats.pascal_voc import object_to_pascal_voc
|
||||
>>> elem = object_to_pascal_voc(np.array([0, 0, 9, 9]), name="cat")
|
||||
>>> elem.find("bndbox/xmin").text
|
||||
'1'
|
||||
>>> elem.find("bndbox/xmax").text
|
||||
'10'
|
||||
"""
|
||||
root = Element("object")
|
||||
|
||||
object_name = SubElement(root, "name")
|
||||
object_name.text = name
|
||||
|
||||
# https://github.com/roboflow/supervision/issues/144
|
||||
xyxy += 1
|
||||
# Pascal VOC coordinates are 1-indexed (https://github.com/roboflow/supervision/issues/144).
|
||||
# Rebind to a new array instead of `+= 1`: `xyxy` is a view into the source
|
||||
# `Detections.xyxy` (yielded by `Detections.__iter__`), so an in-place add
|
||||
# would corrupt the caller's detections by +1 on every export.
|
||||
xyxy = xyxy + 1
|
||||
|
||||
bndbox = SubElement(root, "bndbox")
|
||||
xmin = SubElement(bndbox, "xmin")
|
||||
@@ -40,8 +68,8 @@ def object_to_pascal_voc(
|
||||
ymax.text = str(int(xyxy[3]))
|
||||
|
||||
if polygon is not None:
|
||||
# https://github.com/roboflow/supervision/issues/144
|
||||
polygon += 1
|
||||
# 1-indexed, rebound to avoid mutating the caller's array (see above).
|
||||
polygon = polygon + 1
|
||||
object_polygon = SubElement(root, "polygon")
|
||||
for index, point in enumerate(polygon, start=1):
|
||||
x_coordinate, y_coordinate = point
|
||||
@@ -81,6 +109,11 @@ def detections_to_pascal_voc(
|
||||
polygon points to be removed from the input polygon, in the range [0, 1).
|
||||
Returns:
|
||||
An XML string in Pascal VOC format representing the detections.
|
||||
|
||||
Note:
|
||||
``detections`` is never mutated by this function; the source ``xyxy``
|
||||
array is unchanged after the call. The function is therefore safe to
|
||||
call multiple times on the same ``Detections`` object.
|
||||
"""
|
||||
height, width, depth = image_shape
|
||||
|
||||
|
||||
@@ -2632,7 +2632,9 @@ class Detections:
|
||||
return Detections.merge(result)
|
||||
|
||||
|
||||
def _merge_obb_corners(corners_list: list[np.ndarray]) -> npt.NDArray[np.floating]:
|
||||
def _merge_obb_corners(
|
||||
corners_list: list[npt.NDArray[np.floating]],
|
||||
) -> npt.NDArray[np.floating]:
|
||||
"""Merge multiple OBB corner arrays using winner-angle projection.
|
||||
|
||||
The first entry in *corners_list* is the winner. Its orientation angle
|
||||
|
||||
@@ -24,6 +24,8 @@ class JSONSink:
|
||||
When a list or tuple value in custom_data (or detections.data) has the
|
||||
same length as the detection count, each element is written to the
|
||||
corresponding detection row; any other value is broadcast to all rows.
|
||||
NumPy scalars (e.g. ``np.int64``, ``np.float32``) are serialized as
|
||||
JSON numbers; NumPy arrays are serialized as JSON arrays.
|
||||
|
||||
Args:
|
||||
file_name: The name of the JSON file where the detections will be stored.
|
||||
@@ -79,13 +81,42 @@ class JSONSink:
|
||||
|
||||
self.file = open(self.file_name, "w")
|
||||
|
||||
@staticmethod
|
||||
def _json_default(value: Any) -> Any:
|
||||
"""Return a JSON-serializable equivalent of a NumPy scalar or array.
|
||||
|
||||
Called as the ``default`` hook by :func:`json.dump`. Converts
|
||||
:class:`numpy.generic` scalars via ``.item()`` and
|
||||
:class:`numpy.ndarray` instances via ``.tolist()``.
|
||||
|
||||
Args:
|
||||
value: Object the standard JSON encoder could not serialize.
|
||||
|
||||
Returns:
|
||||
A Python scalar or nested list equivalent of ``value``.
|
||||
|
||||
Raises:
|
||||
TypeError: If ``value`` is neither a NumPy scalar nor an ndarray.
|
||||
"""
|
||||
if isinstance(value, np.generic):
|
||||
return value.item()
|
||||
if isinstance(value, np.ndarray):
|
||||
return value.tolist()
|
||||
raise TypeError(
|
||||
f"Object of type {type(value).__name__} is not JSON serializable"
|
||||
)
|
||||
|
||||
def write_and_close(self) -> None:
|
||||
"""
|
||||
Write and close the JSON file.
|
||||
"""
|
||||
if self.file:
|
||||
json.dump(self.data, self.file, indent=4)
|
||||
self.file.close()
|
||||
try:
|
||||
json.dump(
|
||||
self.data, self.file, indent=4, default=JSONSink._json_default
|
||||
)
|
||||
finally:
|
||||
self.file.close()
|
||||
|
||||
@staticmethod
|
||||
def _slice_value(value: Any, i: int, n: int) -> Any:
|
||||
|
||||
@@ -29,6 +29,9 @@ class DetectionsSmoother:
|
||||
[Roboflow Trackers](/latest/trackers/) for
|
||||
information on integrating tracking into your inference pipeline.
|
||||
- This class is not compatible with segmentation models.
|
||||
- When detections in a frame disagree on confidence presence — some tracks
|
||||
carry confidence scores and others do not — `confidence` is set to `None`
|
||||
for all smoothed detections in that frame.
|
||||
|
||||
Example:
|
||||
```pycon
|
||||
@@ -128,6 +131,20 @@ class DetectionsSmoother:
|
||||
return self.get_smoothed_detections()
|
||||
|
||||
def get_track(self, track_id: int) -> Detections | None:
|
||||
"""Return the smoothed `Detections` for a single track.
|
||||
|
||||
Averages `xyxy` over all valid (non-`None`) frames in the track window.
|
||||
`confidence` is averaged only over frames that carry it; frames with
|
||||
`confidence=None` are excluded. Returns `None` when the track is unknown
|
||||
or its entire window is empty.
|
||||
|
||||
Args:
|
||||
track_id: The tracker ID whose smoothed detection to retrieve.
|
||||
|
||||
Returns:
|
||||
Smoothed `Detections` for the track, or `None` if the track is
|
||||
unknown or all frames in its window are empty.
|
||||
"""
|
||||
track = self.tracks.get(track_id, None)
|
||||
if track is None:
|
||||
return None
|
||||
@@ -138,7 +155,11 @@ class DetectionsSmoother:
|
||||
|
||||
ret = deepcopy(valid[0])
|
||||
ret.xyxy = np.mean([d.xyxy for d in valid], axis=0)
|
||||
ret.confidence = np.mean([d.confidence for d in valid], axis=0)
|
||||
# Average confidence only over frames that carry it; frames with
|
||||
# confidence=None contribute nothing to the mean. Retain None when
|
||||
# no frame in the window carries confidence.
|
||||
confidences = [d.confidence for d in valid if d.confidence is not None]
|
||||
ret.confidence = np.mean(np.array(confidences), axis=0) if confidences else None
|
||||
|
||||
return ret
|
||||
|
||||
@@ -149,6 +170,13 @@ class DetectionsSmoother:
|
||||
if track is not None:
|
||||
tracked_detections.append(track)
|
||||
|
||||
# Detections.merge requires all-or-none for optional fields.
|
||||
# When tracks disagree on confidence presence, drop it from all to
|
||||
# prevent ValueError inside Detections.merge (stack_or_none invariant).
|
||||
if tracked_detections and any(d.confidence is None for d in tracked_detections):
|
||||
for d in tracked_detections:
|
||||
d.confidence = None
|
||||
|
||||
detections = Detections.merge(tracked_detections)
|
||||
if len(detections) == 0:
|
||||
detections.tracker_id = np.array([], dtype=int)
|
||||
|
||||
@@ -204,23 +204,44 @@ def mask_to_xyxy(masks: npt.NDArray[np.bool_]) -> npt.NDArray[np.int_]:
|
||||
Converts a 3D `np.array` of 2D bool masks into a 2D `np.array` of bounding boxes.
|
||||
|
||||
Args:
|
||||
masks: A 3D `np.array` of shape `(N, W, H)` containing 2D bool masks.
|
||||
masks: A 3D `np.array` of shape `(N, H, W)` containing 2D bool masks.
|
||||
|
||||
Returns:
|
||||
A 2D `np.array` of shape `(N, 4)` containing the bounding boxes
|
||||
`(x_min, y_min, x_max, y_max)` for each mask.
|
||||
|
||||
Examples:
|
||||
```pycon
|
||||
>>> import numpy as np
|
||||
>>> import supervision as sv
|
||||
>>> masks = np.array([
|
||||
... [[False, False, False],
|
||||
... [False, True, True],
|
||||
... [False, True, True]],
|
||||
... ])
|
||||
>>> sv.mask_to_xyxy(masks)
|
||||
array([[1, 1, 2, 2]])
|
||||
|
||||
```
|
||||
"""
|
||||
n = masks.shape[0]
|
||||
xyxy = np.zeros((n, 4), dtype=int)
|
||||
n, height, width = masks.shape
|
||||
if masks.size == 0:
|
||||
return np.zeros((n, 4), dtype=int)
|
||||
|
||||
for i, mask in enumerate(masks):
|
||||
rows, cols = np.where(mask)
|
||||
# Reduce the mask stack to per-row / per-column occupancy, then read the
|
||||
# tight bounds straight off those 1D profiles instead of scanning every
|
||||
# pixel of every mask with `np.where`.
|
||||
rows_any = cast(npt.NDArray[np.bool_], masks.any(axis=2)) # (N, H)
|
||||
cols_any = cast(npt.NDArray[np.bool_], masks.any(axis=1)) # (N, W)
|
||||
|
||||
if len(rows) > 0 and len(cols) > 0:
|
||||
x_min, x_max = int(np.min(cols)), int(np.max(cols))
|
||||
y_min, y_max = int(np.min(rows)), int(np.max(rows))
|
||||
xyxy[i, :] = [x_min, y_min, x_max, y_max]
|
||||
x_min = cols_any.argmax(axis=1)
|
||||
x_max = width - 1 - cols_any[:, ::-1].argmax(axis=1)
|
||||
y_min = rows_any.argmax(axis=1)
|
||||
y_max = height - 1 - rows_any[:, ::-1].argmax(axis=1)
|
||||
|
||||
xyxy = np.stack((x_min, y_min, x_max, y_max), axis=1).astype(int)
|
||||
# Empty masks have no bounds; keep the original all-zeros box for them.
|
||||
xyxy[~rows_any.any(axis=1)] = 0
|
||||
return xyxy
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from enum import Enum
|
||||
from typing import Any, cast
|
||||
@@ -687,12 +688,23 @@ def _mask_iou_batch_split(
|
||||
Returns:
|
||||
Pairwise IoU of masks from `masks_true` and `masks_detection`.
|
||||
"""
|
||||
intersection_area = np.logical_and(masks_true[:, None], masks_detection).sum(
|
||||
axis=(2, 3)
|
||||
# The overlap of two binary masks is the dot product of their flattened
|
||||
# pixels, so the whole (N, M) intersection matrix is a single matmul.
|
||||
# float32 counts pixels exactly up to 2**24; for larger masks (beyond
|
||||
# ~4096x4096) we promote to float64 so the counts stay exact.
|
||||
pixels = int(np.prod(masks_true.shape[1:]))
|
||||
count_dtype = np.float32 if pixels <= 2**24 else np.float64
|
||||
true_flat = masks_true.reshape(masks_true.shape[0], pixels).astype(
|
||||
count_dtype, copy=False
|
||||
)
|
||||
detection_flat = masks_detection.reshape(masks_detection.shape[0], pixels).astype(
|
||||
count_dtype, copy=False
|
||||
)
|
||||
with np.errstate(divide="ignore", over="ignore", invalid="ignore"):
|
||||
intersection_area: npt.NDArray[np.floating[Any]] = true_flat @ detection_flat.T
|
||||
|
||||
masks_true_area = masks_true.sum(axis=(1, 2)) # (area1, area2, ...)
|
||||
masks_detection_area = masks_detection.sum(axis=(1, 2)) # (area1)
|
||||
masks_true_area = true_flat.sum(axis=1)
|
||||
masks_detection_area = detection_flat.sum(axis=1)
|
||||
|
||||
if overlap_metric == OverlapMetric.IOU:
|
||||
union_area = masks_true_area[:, None] + masks_detection_area - intersection_area
|
||||
@@ -743,10 +755,20 @@ def mask_iou_batch(
|
||||
overlap_metric: Metric used to compute the degree of overlap
|
||||
between pairs of masks (e.g., IoU, IoS).
|
||||
memory_limit: Memory limit in MB, default is 1024 * 5 MB (5GB).
|
||||
Ignored when both inputs are CompactMask.
|
||||
Controls chunking of ``masks_true`` so that flattened detection
|
||||
masks plus each chunk's buffers stay within this limit. A
|
||||
``UserWarning`` is raised when ``masks_detection`` alone
|
||||
exceeds the limit, as chunking cannot reduce peak memory
|
||||
below that floor. Ignored when both inputs are
|
||||
:class:`~supervision.detection.compact_mask.CompactMask`.
|
||||
|
||||
Returns:
|
||||
Pairwise IoU of masks from `masks_true` and `masks_detection`.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``masks_true`` or ``masks_detection`` are not 3D
|
||||
``(N, H, W)`` arrays, or if they do not share the same
|
||||
spatial dimensions ``(H, W)``.
|
||||
"""
|
||||
|
||||
if isinstance(masks_true, CompactMask) and isinstance(masks_detection, CompactMask):
|
||||
@@ -758,29 +780,43 @@ def mask_iou_batch(
|
||||
if isinstance(masks_detection, CompactMask):
|
||||
masks_detection = np.asarray(masks_detection)
|
||||
|
||||
memory = (
|
||||
masks_true.shape[0]
|
||||
* masks_true.shape[1]
|
||||
* masks_true.shape[2]
|
||||
* masks_detection.shape[0]
|
||||
/ 1024
|
||||
/ 1024
|
||||
)
|
||||
if memory <= memory_limit:
|
||||
if masks_true.ndim != 3 or masks_detection.ndim != 3:
|
||||
raise ValueError(
|
||||
"masks_true and masks_detection must be 3D (N, H, W); got "
|
||||
f"ndim={masks_true.ndim} and ndim={masks_detection.ndim}."
|
||||
)
|
||||
if masks_true.shape[1:] != masks_detection.shape[1:]:
|
||||
raise ValueError(
|
||||
"masks_true and masks_detection must share the same (H, W); got "
|
||||
f"{masks_true.shape[1:]} and {masks_detection.shape[1:]}."
|
||||
)
|
||||
# A single pass already handles empty inputs and avoids np.vstack([]) below.
|
||||
if masks_true.shape[0] == 0 or masks_detection.shape[0] == 0:
|
||||
return _mask_iou_batch_split(masks_true, masks_detection, overlap_metric)
|
||||
|
||||
# Peak memory of a single matmul pass: the flattened detection masks (shared
|
||||
# across chunks) plus, per true-mask row, its flattened pixels and the three
|
||||
# (N, M) matrices it touches (intersection, denominator and output). The
|
||||
# previous (N, M, H, W) estimate overcounted by a factor of M and forced
|
||||
# needless chunking now that the intersection is a matmul.
|
||||
pixels = masks_true.shape[1] * masks_true.shape[2]
|
||||
itemsize = 4 if pixels <= 2**24 else 8
|
||||
limit_bytes = memory_limit * 1024 * 1024
|
||||
detection_bytes = masks_detection.shape[0] * pixels * itemsize
|
||||
per_true_row = pixels * itemsize + 3 * masks_detection.shape[0] * 8
|
||||
if detection_bytes > limit_bytes > 0:
|
||||
warnings.warn(
|
||||
f"detection masks ({detection_bytes // 1024 // 1024} MB) exceed "
|
||||
f"memory_limit ({memory_limit} MB); chunking cannot reduce peak "
|
||||
"memory below this floor.",
|
||||
UserWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if detection_bytes + masks_true.shape[0] * per_true_row <= limit_bytes:
|
||||
return _mask_iou_batch_split(masks_true, masks_detection, overlap_metric)
|
||||
|
||||
ious = []
|
||||
step = max(
|
||||
memory_limit
|
||||
* 1024
|
||||
* 1024
|
||||
// (
|
||||
masks_detection.shape[0]
|
||||
* masks_detection.shape[1]
|
||||
* masks_detection.shape[2]
|
||||
),
|
||||
1,
|
||||
)
|
||||
step = max((limit_bytes - detection_bytes) // per_true_row, 1)
|
||||
for chunk_start in range(0, masks_true.shape[0], step):
|
||||
ious.append(
|
||||
_mask_iou_batch_split(
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
@@ -57,19 +55,47 @@ def approximate_polygon(
|
||||
the `x`, `y` coordinates of the input polygon's points.
|
||||
percentage: The percentage of points to be removed from the
|
||||
input polygon, in the range `[0, 1)`.
|
||||
epsilon_step: Approximation accuracy step.
|
||||
epsilon_step: Approximation accuracy step, must be positive.
|
||||
Epsilon is the maximum distance between the original curve
|
||||
and its approximation.
|
||||
|
||||
Returns:
|
||||
A new 2D NumPy array of shape `(M, 2)`,
|
||||
where `M <= N * (1 - percentage)`, containing
|
||||
the `x`, `y` coordinates of the
|
||||
approximated polygon's points.
|
||||
A new 2D NumPy array of shape `(M, 2)` containing the `x`, `y`
|
||||
coordinates of the approximated polygon's points. `M` is at most
|
||||
`floor(N * (1 - percentage))` (minimum 3). Because epsilon
|
||||
advances in discrete `epsilon_step` increments, `M` may be
|
||||
noticeably smaller than the budget when a single step crosses
|
||||
the target band. At least 3 points are always kept; when further
|
||||
simplification would collapse the polygon below 3 points the last
|
||||
valid approximation is returned and `M` may exceed the budget.
|
||||
|
||||
Raises:
|
||||
ValueError: If `percentage` is outside the range `[0, 1)`.
|
||||
ValueError: If `epsilon_step` is not positive.
|
||||
|
||||
Examples:
|
||||
Reduce a polygon to at most half its original point count:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> polygon = np.array([[0, 0], [10, 0], [10, 10], [0, 10],
|
||||
... [5, 10], [5, 5], [3, 7], [1, 9]])
|
||||
>>> result = approximate_polygon(polygon, percentage=0.5)
|
||||
>>> result.shape[1]
|
||||
2
|
||||
>>> len(result) <= max(int(len(polygon) * 0.5), 3)
|
||||
True
|
||||
|
||||
Polygon already at or below target — returned unchanged:
|
||||
|
||||
>>> tiny = np.array([[0, 0], [5, 0], [2, 4]])
|
||||
>>> approximate_polygon(tiny, percentage=0.5) is tiny
|
||||
True
|
||||
"""
|
||||
|
||||
if percentage < 0 or percentage >= 1:
|
||||
raise ValueError("Percentage must be in the range [0, 1).")
|
||||
if epsilon_step <= 0:
|
||||
raise ValueError("epsilon_step must be positive.")
|
||||
|
||||
target_points = max(int(len(polygon) * (1 - percentage)), 3)
|
||||
|
||||
@@ -78,12 +104,13 @@ def approximate_polygon(
|
||||
|
||||
epsilon: float = 0
|
||||
approximated_points = polygon
|
||||
while True:
|
||||
while len(approximated_points) > target_points:
|
||||
epsilon += epsilon_step
|
||||
new_approximated_points = cv2.approxPolyDP(polygon, epsilon, closed=True)
|
||||
if len(new_approximated_points) > target_points:
|
||||
approximated_points = new_approximated_points
|
||||
else:
|
||||
candidate = np.squeeze(cv2.approxPolyDP(polygon, epsilon, closed=True), axis=1)
|
||||
# Stop before the approximation collapses below a valid polygon; keep the
|
||||
# last result with at least three points.
|
||||
if len(candidate) < 3:
|
||||
break
|
||||
approximated_points = candidate
|
||||
|
||||
return cast(npt.NDArray[np.number], np.squeeze(approximated_points, axis=1))
|
||||
return approximated_points
|
||||
|
||||
@@ -214,7 +214,7 @@ def validate_vlm_parameters(vlm: VLM | str, result: Any, kwargs: dict[str, Any])
|
||||
|
||||
def from_paligemma(
|
||||
result: str, resolution_wh: tuple[int, int], classes: list[str] | None = None
|
||||
) -> tuple[npt.NDArray[Any], npt.NDArray[Any], npt.NDArray[Any]]:
|
||||
) -> tuple[npt.NDArray[Any], npt.NDArray[Any] | None, npt.NDArray[Any]]:
|
||||
"""
|
||||
Parse bounding boxes from paligemma-formatted text, scale them to the specified
|
||||
resolution, and optionally filter by classes.
|
||||
@@ -238,23 +238,23 @@ def from_paligemma(
|
||||
r"(?<!<loc\d{4}>)<loc(\d{4})><loc(\d{4})><loc(\d{4})><loc(\d{4})> ([\w\s\-]+)"
|
||||
)
|
||||
matches = pattern.findall(result)
|
||||
matches = np.array(matches) if matches else np.empty((0, 5))
|
||||
matches_arr: npt.NDArray[Any] = np.array(matches) if matches else np.empty((0, 5))
|
||||
|
||||
if matches.shape[0] == 0:
|
||||
if matches_arr.shape[0] == 0:
|
||||
return np.empty((0, 4)), np.empty((0,), dtype=int), np.empty(0, dtype=str)
|
||||
|
||||
xyxy, class_name = matches[:, [1, 0, 3, 2]], matches[:, 4]
|
||||
xyxy = xyxy.astype(int) / 1024 * np.array([w, h, w, h])
|
||||
class_name = np.char.strip(class_name.astype(str))
|
||||
class_id = None
|
||||
xyxy_arr = np.array(matches_arr[:, [1, 0, 3, 2]], dtype=float)
|
||||
xyxy_arr = xyxy_arr.astype(int) / 1024 * np.array([w, h, w, h])
|
||||
class_name = np.char.strip(matches_arr[:, 4].astype(str))
|
||||
class_id: npt.NDArray[Any] | None = None
|
||||
|
||||
if classes is not None:
|
||||
mask = np.array([name in classes for name in class_name], dtype=bool)
|
||||
xyxy = xyxy[mask]
|
||||
xyxy_arr = xyxy_arr[mask]
|
||||
class_name = class_name[mask]
|
||||
class_id = np.array([classes.index(name) for name in class_name])
|
||||
|
||||
return xyxy, class_id, class_name
|
||||
return xyxy_arr, class_id, class_name
|
||||
|
||||
|
||||
def recover_truncated_qwen_2_5_vl_response(text: str) -> Any | None:
|
||||
@@ -369,7 +369,7 @@ def from_qwen_2_5_vl(
|
||||
labels_list = []
|
||||
|
||||
for item in data:
|
||||
if "bbox_2d" not in item or "label" not in item:
|
||||
if not isinstance(item, dict) or "bbox_2d" not in item or "label" not in item:
|
||||
continue
|
||||
boxes_list.append(item["bbox_2d"])
|
||||
labels_list.append(item["label"])
|
||||
@@ -460,12 +460,13 @@ def from_deepseek_vl_2(
|
||||
f"and det tags ({len(detection_segments)}) in the result must be equal."
|
||||
)
|
||||
|
||||
xyxy, class_name_list = [], []
|
||||
xyxy_list: list[list[float]] = []
|
||||
class_name_list: list[str] = []
|
||||
for label, detection_blob in zip(label_segments, detection_segments):
|
||||
current_class_name = label.strip()
|
||||
for box in re.findall(r"\[(.*?)\]", detection_blob):
|
||||
x1, y1, x2, y2 = map(float, box.strip("[]").split(","))
|
||||
xyxy.append(
|
||||
xyxy_list.append(
|
||||
[
|
||||
(x1 / 999 * width),
|
||||
(y1 / 999 * height),
|
||||
@@ -475,7 +476,7 @@ def from_deepseek_vl_2(
|
||||
)
|
||||
class_name_list.append(current_class_name)
|
||||
|
||||
xyxy = np.array(xyxy, dtype=np.float32)
|
||||
xyxy = np.array(xyxy_list, dtype=np.float32)
|
||||
class_name = np.array(class_name_list)
|
||||
|
||||
if classes is not None:
|
||||
@@ -541,15 +542,15 @@ def from_florence_2(
|
||||
return xyxy, labels, None, xyxyxyxy
|
||||
|
||||
if task in ["<REFERRING_EXPRESSION_SEGMENTATION>", "<REGION_TO_SEGMENTATION>"]:
|
||||
xyxy_list = []
|
||||
masks_list = []
|
||||
xyxy_list: list[npt.NDArray[Any]] = []
|
||||
masks_list: list[npt.NDArray[Any]] = []
|
||||
for polygons_of_same_class in result["polygons"]:
|
||||
for polygon in polygons_of_same_class:
|
||||
polygon = np.reshape(polygon, (-1, 2)).astype(np.int32)
|
||||
mask = polygon_to_mask(polygon, resolution_wh).astype(bool)
|
||||
masks_list.append(mask)
|
||||
xyxy = polygon_to_xyxy(polygon)
|
||||
xyxy_list.append(xyxy)
|
||||
xyxy_box = polygon_to_xyxy(polygon)
|
||||
xyxy_list.append(xyxy_box)
|
||||
# per-class labels also provided, but they are ["", "", "", ...]
|
||||
# when we figure out how to set class names, we can do
|
||||
# zip(result["labels"], result["polygons"])
|
||||
@@ -640,11 +641,14 @@ def from_google_gemini_2_0(
|
||||
except json.JSONDecodeError:
|
||||
return np.empty((0, 4)), np.empty((0,), dtype=int), np.empty((0,), dtype=str)
|
||||
|
||||
if not isinstance(data, list):
|
||||
return np.empty((0, 4)), np.empty((0,), dtype=int), np.empty((0,), dtype=str)
|
||||
|
||||
labels = []
|
||||
xyxy = []
|
||||
|
||||
for item in data:
|
||||
if "box_2d" not in item or "label" not in item:
|
||||
if not isinstance(item, dict) or "box_2d" not in item or "label" not in item:
|
||||
continue
|
||||
labels.append(item["label"])
|
||||
box = item["box_2d"]
|
||||
@@ -734,13 +738,22 @@ def from_google_gemini_2_5(
|
||||
None,
|
||||
)
|
||||
|
||||
if not isinstance(data, list):
|
||||
return (
|
||||
np.empty((0, 4)),
|
||||
np.array([], dtype=int),
|
||||
np.array([], dtype=str),
|
||||
np.array([], dtype=float),
|
||||
None,
|
||||
)
|
||||
|
||||
boxes_list: list[Any] = []
|
||||
labels_list: list[str] = []
|
||||
confidence_list: list[float] | None = []
|
||||
masks_list: list[npt.NDArray[Any]] | None = []
|
||||
|
||||
for item in data:
|
||||
if "box_2d" not in item or "label" not in item:
|
||||
if not isinstance(item, dict) or "box_2d" not in item or "label" not in item:
|
||||
continue
|
||||
labels_list.append(item["label"])
|
||||
box = item["box_2d"]
|
||||
@@ -755,29 +768,39 @@ def from_google_gemini_2_5(
|
||||
if "mask" in item:
|
||||
if masks_list is not None:
|
||||
png_str = item["mask"]
|
||||
if not png_str.startswith("data:image/png;base64,"):
|
||||
if not isinstance(png_str, str) or not png_str.startswith(
|
||||
"data:image/png;base64,"
|
||||
):
|
||||
# Malformed mask: keep an empty mask but still fall through to
|
||||
# the confidence handling below, so the per-item arrays stay
|
||||
# aligned (a `continue` here desynced confidence vs boxes).
|
||||
masks_list.append(np.zeros((h, w), dtype=bool))
|
||||
continue
|
||||
|
||||
png_str = png_str.removeprefix("data:image/png;base64,")
|
||||
png_str = base64.b64decode(png_str)
|
||||
mask_img = Image.open(io.BytesIO(png_str))
|
||||
|
||||
y_min, y_max = int(absolute_bbox[1]), int(absolute_bbox[3])
|
||||
x_min, x_max = int(absolute_bbox[0]), int(absolute_bbox[2])
|
||||
|
||||
bbox_height = y_max - y_min
|
||||
bbox_width = x_max - x_min
|
||||
|
||||
if bbox_height > 0 and bbox_width > 0:
|
||||
mask_img = mask_img.resize(
|
||||
(bbox_width, bbox_height), resample=Image.Resampling.BILINEAR
|
||||
)
|
||||
np_mask: npt.NDArray[np.bool_] = np.zeros((h, w), dtype=bool)
|
||||
np_mask[y_min:y_max, x_min:x_max] = np.array(mask_img) > 0
|
||||
masks_list.append(np_mask)
|
||||
else:
|
||||
masks_list.append(np.zeros((h, w), dtype=bool))
|
||||
png_str = png_str.removeprefix("data:image/png;base64,")
|
||||
try:
|
||||
png_bytes = base64.b64decode(png_str)
|
||||
mask_img = Image.open(io.BytesIO(png_bytes)).convert("L")
|
||||
except Exception:
|
||||
masks_list.append(np.zeros((h, w), dtype=bool))
|
||||
else:
|
||||
y_min, y_max = int(absolute_bbox[1]), int(absolute_bbox[3])
|
||||
x_min, x_max = int(absolute_bbox[0]), int(absolute_bbox[2])
|
||||
|
||||
bbox_height = y_max - y_min
|
||||
bbox_width = x_max - x_min
|
||||
|
||||
if bbox_height > 0 and bbox_width > 0:
|
||||
mask_img = mask_img.resize(
|
||||
(bbox_width, bbox_height),
|
||||
resample=Image.Resampling.BILINEAR,
|
||||
)
|
||||
np_mask: npt.NDArray[np.bool_] = np.zeros(
|
||||
(h, w), dtype=bool
|
||||
)
|
||||
np_mask[y_min:y_max, x_min:x_max] = np.array(mask_img) > 0
|
||||
masks_list.append(np_mask)
|
||||
else:
|
||||
masks_list.append(np.zeros((h, w), dtype=bool))
|
||||
else:
|
||||
masks_list = None
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ import numpy.typing as npt
|
||||
from supervision.config import CLASS_NAME_DATA_FIELD
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.detection.utils.internal import get_data_item, is_data_equal
|
||||
from supervision.detection.utils.iou_and_nms import (
|
||||
OverlapMetric,
|
||||
box_non_max_suppression,
|
||||
)
|
||||
from supervision.utils.internal import warn_deprecated
|
||||
from supervision.validators import _validate_keypoints_fields
|
||||
|
||||
@@ -67,7 +71,7 @@ def _normalize_row_index(
|
||||
return i
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(init=False)
|
||||
class KeyPoints:
|
||||
"""
|
||||
The `sv.KeyPoints` class in the Supervision library standardizes results from
|
||||
@@ -238,6 +242,59 @@ class KeyPoints:
|
||||
visible: npt.NDArray[np.bool_] | None = None
|
||||
data: dict[str, npt.NDArray[np.generic] | list[Any]] = field(default_factory=dict)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
xy: npt.NDArray[np.float32],
|
||||
class_id: npt.NDArray[np.int_] | None = None,
|
||||
keypoint_confidence: npt.NDArray[np.float32] | None = None,
|
||||
detection_confidence: npt.NDArray[np.float32] | None = None,
|
||||
visible: npt.NDArray[np.bool_] | None = None,
|
||||
data: dict[str, npt.NDArray[np.generic] | list[Any]] | None = None,
|
||||
*,
|
||||
confidence: npt.NDArray[np.float32] | None = None,
|
||||
) -> None:
|
||||
"""Initialize KeyPoints.
|
||||
|
||||
Args:
|
||||
xy: Array of shape `(n, m, 2)` with keypoint coordinates.
|
||||
class_id: Array of shape `(n,)` with class IDs. Defaults to None.
|
||||
keypoint_confidence: Array of shape `(n, m)` with per-keypoint
|
||||
confidence scores. Defaults to None.
|
||||
detection_confidence: Array of shape `(n,)` with detection-level
|
||||
confidence scores. Defaults to None.
|
||||
visible: Boolean array of shape `(n, m)` indicating visible
|
||||
keypoints. Defaults to None.
|
||||
data: Dictionary of additional per-detection data arrays.
|
||||
Defaults to an empty dict.
|
||||
confidence: Deprecated since `0.29.0`, removed in `0.32.0`.
|
||||
Use ``keypoint_confidence`` instead. Raises ``ValueError``
|
||||
if passed together with ``keypoint_confidence``.
|
||||
|
||||
Raises:
|
||||
ValueError: If both ``confidence`` and ``keypoint_confidence``
|
||||
are provided.
|
||||
"""
|
||||
if confidence is not None:
|
||||
if keypoint_confidence is not None:
|
||||
raise ValueError(
|
||||
"Cannot pass both 'confidence' and 'keypoint_confidence'. "
|
||||
"'confidence' is deprecated — use 'keypoint_confidence' only."
|
||||
)
|
||||
warn_deprecated(
|
||||
"'confidence' parameter in `KeyPoints()` is deprecated since "
|
||||
"`0.29.0` and will be removed in `0.32.0`. Use "
|
||||
"'keypoint_confidence' instead."
|
||||
)
|
||||
keypoint_confidence = confidence
|
||||
|
||||
self.xy = xy
|
||||
self.class_id = class_id
|
||||
self.keypoint_confidence = keypoint_confidence
|
||||
self.detection_confidence = detection_confidence
|
||||
self.visible = visible
|
||||
self.data = data if data is not None else {}
|
||||
self.__post_init__()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_validate_keypoints_fields(
|
||||
xy=self.xy,
|
||||
@@ -1076,6 +1133,94 @@ class KeyPoints:
|
||||
empty_key_points.data = self.data
|
||||
return self == empty_key_points
|
||||
|
||||
def with_nms(
|
||||
self,
|
||||
threshold: float = 0.5,
|
||||
class_agnostic: bool = False,
|
||||
overlap_metric: OverlapMetric = OverlapMetric.IOU,
|
||||
) -> KeyPoints:
|
||||
"""
|
||||
Performs non-max suppression on the keypoint detections. Bounding boxes
|
||||
are derived from valid keypoints of each skeleton, and standard box NMS
|
||||
is applied. A keypoint is considered valid when its coordinates are not
|
||||
all-zero and its `visible` flag is `True` (if `visible` is set).
|
||||
|
||||
Args:
|
||||
threshold: The intersection-over-union threshold to use for
|
||||
non-maximum suppression. Must be in [0, 1]. Defaults to 0.5.
|
||||
class_agnostic: Whether to perform class-agnostic non-maximum
|
||||
suppression. If True, the class_id of each detection will be
|
||||
ignored. Defaults to False.
|
||||
overlap_metric: Metric used to compute the degree of overlap
|
||||
between pairs of bounding boxes. Defaults to
|
||||
`OverlapMetric.IOU`.
|
||||
|
||||
Returns:
|
||||
A new `sv.KeyPoints` object after non-maximum suppression.
|
||||
|
||||
Raises:
|
||||
ValueError: If `detection_confidence` is None.
|
||||
ValueError: If `class_agnostic` is False and `class_id`
|
||||
is None.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
import cv2
|
||||
import supervision as sv
|
||||
from rfdetr import RFDETRKeypointPreview
|
||||
|
||||
image = cv2.imread("<SOURCE_IMAGE_PATH>")
|
||||
model = RFDETRKeypointPreview()
|
||||
|
||||
key_points = model.predict(image)
|
||||
key_points = key_points.with_nms(threshold=0.5)
|
||||
```
|
||||
"""
|
||||
if len(self) == 0:
|
||||
return self
|
||||
|
||||
if self.detection_confidence is None:
|
||||
raise ValueError(
|
||||
"KeyPoints detection_confidence must be given for NMS to be executed."
|
||||
)
|
||||
|
||||
if not class_agnostic and self.class_id is None:
|
||||
raise ValueError(
|
||||
"KeyPoints class_id must be given for NMS to be executed. If "
|
||||
"you intended to perform class agnostic NMS set "
|
||||
"class_agnostic=True."
|
||||
)
|
||||
|
||||
xy = self.xy
|
||||
valid = ~np.all(xy == 0, axis=-1)
|
||||
if self.visible is not None:
|
||||
valid = valid & self.visible
|
||||
x_min = np.min(np.where(valid, xy[..., 0], np.inf), axis=1)
|
||||
y_min = np.min(np.where(valid, xy[..., 1], np.inf), axis=1)
|
||||
x_max = np.max(np.where(valid, xy[..., 0], -np.inf), axis=1)
|
||||
y_max = np.max(np.where(valid, xy[..., 1], -np.inf), axis=1)
|
||||
xyxy = np.stack([x_min, y_min, x_max, y_max], axis=1).astype(np.float32)
|
||||
|
||||
if class_agnostic:
|
||||
predictions = np.hstack([xyxy, self.detection_confidence.reshape(-1, 1)])
|
||||
else:
|
||||
class_id = cast(npt.NDArray[np.int_], self.class_id)
|
||||
predictions = np.hstack(
|
||||
[
|
||||
xyxy,
|
||||
self.detection_confidence.reshape(-1, 1),
|
||||
class_id.reshape(-1, 1),
|
||||
]
|
||||
)
|
||||
|
||||
keep = box_non_max_suppression(
|
||||
predictions=predictions,
|
||||
iou_threshold=threshold,
|
||||
overlap_metric=overlap_metric,
|
||||
)
|
||||
|
||||
return cast(KeyPoints, self[keep])
|
||||
|
||||
def as_detections(
|
||||
self, selected_keypoint_indices: Iterable[int] | None = None
|
||||
) -> Detections:
|
||||
@@ -1088,7 +1233,9 @@ class KeyPoints:
|
||||
selected_keypoint_indices: The
|
||||
indices of the key points to include in the bounding box
|
||||
calculation. This helps focus on a subset of key points,
|
||||
e.g. when some are occluded. Captures all key points by default.
|
||||
e.g. when some are occluded. Captures all key points by
|
||||
default. An empty sequence (`[]`) is treated the same as
|
||||
`None` and selects all key points.
|
||||
|
||||
Returns:
|
||||
detections: The converted detections object.
|
||||
@@ -1109,40 +1256,37 @@ class KeyPoints:
|
||||
if self.is_empty():
|
||||
return Detections.empty()
|
||||
|
||||
detections_list = []
|
||||
for i, xy in enumerate(self.xy):
|
||||
xy = self.xy
|
||||
if selected_keypoint_indices:
|
||||
indices = np.asarray(list(selected_keypoint_indices), dtype=np.intp)
|
||||
xy = xy[:, indices, :]
|
||||
|
||||
# [0, 0] is used by some frameworks to indicate a missing keypoint; those
|
||||
# points are excluded from each skeleton's bounding box.
|
||||
valid = ~np.all(xy == 0, axis=2) # (N, M)
|
||||
has_valid = valid.any(axis=1) # (N,)
|
||||
|
||||
x, y = xy[:, :, 0], xy[:, :, 1]
|
||||
x_min = np.where(valid, x, np.inf).min(axis=1)
|
||||
y_min = np.where(valid, y, np.inf).min(axis=1)
|
||||
x_max = np.where(valid, x, -np.inf).max(axis=1)
|
||||
y_max = np.where(valid, y, -np.inf).max(axis=1)
|
||||
|
||||
xyxy = np.stack((x_min, y_min, x_max, y_max), axis=1).astype(np.float32)
|
||||
# Skeletons with no valid keypoints keep the original empty [0, 0, 0, 0] box.
|
||||
xyxy[~has_valid] = 0.0
|
||||
|
||||
if self.detection_confidence is not None:
|
||||
confidence = self.detection_confidence.astype(np.float32)
|
||||
elif self.keypoint_confidence is not None:
|
||||
keypoint_confidence = self.keypoint_confidence
|
||||
if selected_keypoint_indices:
|
||||
xy = xy[selected_keypoint_indices]
|
||||
keypoint_confidence = keypoint_confidence[:, indices]
|
||||
confidence = keypoint_confidence.mean(axis=1).astype(np.float32)
|
||||
else:
|
||||
confidence = None
|
||||
|
||||
# [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.detection_confidence is not None:
|
||||
confidence = np.array([self.detection_confidence[i]], dtype=np.float32)
|
||||
elif self.keypoint_confidence is not None:
|
||||
confidence = self.keypoint_confidence[i]
|
||||
if selected_keypoint_indices:
|
||||
confidence = confidence[selected_keypoint_indices]
|
||||
confidence = np.array([confidence.mean()], dtype=np.float32)
|
||||
else:
|
||||
confidence = None
|
||||
|
||||
detections_list.append(
|
||||
Detections(
|
||||
xyxy=xyxy,
|
||||
confidence=confidence,
|
||||
)
|
||||
)
|
||||
|
||||
detections = Detections.merge(detections_list)
|
||||
detections = Detections(xyxy=xyxy, confidence=confidence)
|
||||
detections.class_id = self.class_id
|
||||
detections.data = self.data
|
||||
detections = cast(Detections, detections[cast(Any, detections.area) > 0])
|
||||
|
||||
@@ -152,6 +152,14 @@ class F1Score(Metric):
|
||||
def _compute(
|
||||
self, predictions_list: list[Detections], targets_list: list[Detections]
|
||||
) -> F1ScoreResult:
|
||||
"""Build per-image stats tuples and delegate to class-level computation.
|
||||
|
||||
Each stats tuple is ``(matches, confidence, class_ids, true_class_ids)``:
|
||||
- Both empty: skip (no information).
|
||||
- Targets empty, predictions present: all predictions are FPs; true_class_ids
|
||||
is ``zeros((0,))``.
|
||||
- Targets present: IoU matching produces ``matches`` array.
|
||||
"""
|
||||
iou_thresholds = np.linspace(0.5, 0.95, 10)
|
||||
stats: list[Any] = []
|
||||
|
||||
@@ -159,7 +167,22 @@ class F1Score(Metric):
|
||||
prediction_contents = self._detections_content(predictions)
|
||||
target_contents = self._detections_content(targets)
|
||||
|
||||
if len(targets) > 0:
|
||||
if len(targets) == 0 and len(predictions) > 0:
|
||||
# Only predictions are present (e.g. a background image); every
|
||||
# prediction is a false positive.
|
||||
if predictions.class_id is None or predictions.confidence is None:
|
||||
continue
|
||||
stats.append(
|
||||
(
|
||||
np.zeros(
|
||||
(len(predictions), iou_thresholds.size), dtype=np.bool_
|
||||
),
|
||||
predictions.confidence,
|
||||
predictions.class_id,
|
||||
np.zeros((0,), dtype=np.int32),
|
||||
)
|
||||
)
|
||||
elif len(targets) > 0:
|
||||
if len(predictions) == 0:
|
||||
stats.append(
|
||||
(
|
||||
@@ -244,10 +267,23 @@ class F1Score(Metric):
|
||||
npt.NDArray[np.float64],
|
||||
npt.NDArray[np.int32],
|
||||
]:
|
||||
"""Compute F1 scores from concatenated stats across all images.
|
||||
|
||||
``unique_classes`` is the union of GT and predicted classes so that
|
||||
predictions of classes absent from GT still count as false positives.
|
||||
"""
|
||||
sorted_indices = np.argsort(-prediction_confidence)
|
||||
matches = matches[sorted_indices]
|
||||
prediction_class_ids = prediction_class_ids[sorted_indices]
|
||||
unique_classes, class_counts = np.unique(true_class_ids, return_counts=True)
|
||||
# Predictions whose class never appears in the ground truth are still
|
||||
# false positives, so include those classes in the confusion matrix
|
||||
# (their true-instance count is zero).
|
||||
unique_classes = np.unique(
|
||||
np.concatenate((true_class_ids, prediction_class_ids))
|
||||
)
|
||||
true_classes, true_counts = np.unique(true_class_ids, return_counts=True)
|
||||
class_counts = np.zeros(unique_classes.shape[0], dtype=int)
|
||||
class_counts[np.searchsorted(unique_classes, true_classes)] = true_counts
|
||||
|
||||
# Shape: PxTh,P,C,C -> CxThx3
|
||||
confusion_matrix = self._compute_confusion_matrix(
|
||||
@@ -265,7 +301,13 @@ class F1Score(Metric):
|
||||
f1_scores = self._compute_f1(confusion_matrix_merged)
|
||||
elif self.averaging_method == AveragingMethod.WEIGHTED:
|
||||
class_counts = class_counts.astype(np.float32)
|
||||
f1_scores = np.average(f1_per_class, axis=0, weights=class_counts)
|
||||
if class_counts.sum() == 0:
|
||||
# No ground-truth support (e.g. only false-positive classes, or a
|
||||
# size bucket with predictions but no targets): weighting is
|
||||
# undefined, so report 0 as the empty case did before.
|
||||
f1_scores = np.zeros(f1_per_class.shape[1])
|
||||
else:
|
||||
f1_scores = np.average(f1_per_class, axis=0, weights=class_counts)
|
||||
|
||||
return f1_scores, f1_per_class, unique_classes
|
||||
|
||||
@@ -483,10 +525,12 @@ class F1ScoreResult:
|
||||
f1_scores: the F1 scores at each IoU threshold.
|
||||
Shape: `(num_iou_thresholds,)`
|
||||
f1_per_class: the F1 scores per class and IoU threshold.
|
||||
Shape: `(num_target_classes, num_iou_thresholds)`
|
||||
Shape: `(num_classes, num_iou_thresholds)`
|
||||
iou_thresholds: the IoU thresholds used in the calculations.
|
||||
matched_classes: the class IDs of all matched classes.
|
||||
Corresponds to the rows of `f1_per_class`.
|
||||
matched_classes: the class IDs present in either predictions or ground
|
||||
truth. Corresponds to the rows of `f1_per_class`. Classes that
|
||||
appear only in predictions (no ground-truth instances) are
|
||||
included; their per-threshold F1 values will be `0.0`.
|
||||
small_objects: the F1 metric results
|
||||
for small objects (area < 32²).
|
||||
medium_objects: the F1 metric results
|
||||
|
||||
@@ -155,6 +155,14 @@ class Precision(Metric):
|
||||
def _compute(
|
||||
self, predictions_list: list[Detections], targets_list: list[Detections]
|
||||
) -> PrecisionResult:
|
||||
"""Build per-image stats tuples and delegate to class-level computation.
|
||||
|
||||
Each stats tuple is ``(matches, confidence, class_ids, true_class_ids)``:
|
||||
- Both empty: skip (no information).
|
||||
- Targets empty, predictions present: all predictions are FPs; true_class_ids
|
||||
is ``zeros((0,))``.
|
||||
- Targets present: IoU matching produces ``matches`` array.
|
||||
"""
|
||||
iou_thresholds = np.linspace(0.5, 0.95, 10)
|
||||
stats: list[Any] = []
|
||||
|
||||
@@ -162,7 +170,22 @@ class Precision(Metric):
|
||||
prediction_contents = self._detections_content(predictions)
|
||||
target_contents = self._detections_content(targets)
|
||||
|
||||
if len(targets) > 0:
|
||||
if len(targets) == 0 and len(predictions) > 0:
|
||||
# Only predictions are present (e.g. a background image); every
|
||||
# prediction is a false positive.
|
||||
if predictions.class_id is None or predictions.confidence is None:
|
||||
continue
|
||||
stats.append(
|
||||
(
|
||||
np.zeros(
|
||||
(len(predictions), iou_thresholds.size), dtype=np.bool_
|
||||
),
|
||||
predictions.confidence,
|
||||
predictions.class_id,
|
||||
np.zeros((0,), dtype=np.int32),
|
||||
)
|
||||
)
|
||||
elif len(targets) > 0:
|
||||
if len(predictions) == 0:
|
||||
stats.append(
|
||||
(
|
||||
@@ -247,10 +270,23 @@ class Precision(Metric):
|
||||
npt.NDArray[np.float64],
|
||||
npt.NDArray[np.int32],
|
||||
]:
|
||||
"""Compute precision scores from concatenated stats across all images.
|
||||
|
||||
``unique_classes`` is the union of GT and predicted classes so that
|
||||
predictions of classes absent from GT still count as false positives.
|
||||
"""
|
||||
sorted_indices = np.argsort(-prediction_confidence)
|
||||
matches = matches[sorted_indices]
|
||||
prediction_class_ids = prediction_class_ids[sorted_indices]
|
||||
unique_classes, class_counts = np.unique(true_class_ids, return_counts=True)
|
||||
# Predictions whose class never appears in the ground truth are still
|
||||
# false positives, so include those classes in the confusion matrix
|
||||
# (their true-instance count is zero).
|
||||
unique_classes = np.unique(
|
||||
np.concatenate((true_class_ids, prediction_class_ids))
|
||||
)
|
||||
true_classes, true_counts = np.unique(true_class_ids, return_counts=True)
|
||||
class_counts = np.zeros(unique_classes.shape[0], dtype=int)
|
||||
class_counts[np.searchsorted(unique_classes, true_classes)] = true_counts
|
||||
|
||||
# Shape: PxTh,P,C,C -> CxThx3
|
||||
confusion_matrix = self._compute_confusion_matrix(
|
||||
@@ -268,9 +304,15 @@ class Precision(Metric):
|
||||
precision_scores = self._compute_precision(confusion_matrix_merged)
|
||||
elif self.averaging_method == AveragingMethod.WEIGHTED:
|
||||
class_counts = class_counts.astype(np.float32)
|
||||
precision_scores = np.average(
|
||||
precision_per_class, axis=0, weights=class_counts
|
||||
)
|
||||
if class_counts.sum() == 0:
|
||||
# No ground-truth support (e.g. only false-positive classes, or a
|
||||
# size bucket with predictions but no targets): weighting is
|
||||
# undefined, so report 0 as the empty case did before.
|
||||
precision_scores = np.zeros(precision_per_class.shape[1])
|
||||
else:
|
||||
precision_scores = np.average(
|
||||
precision_per_class, axis=0, weights=class_counts
|
||||
)
|
||||
|
||||
return precision_scores, precision_per_class, unique_classes
|
||||
|
||||
@@ -428,8 +470,6 @@ class Precision(Metric):
|
||||
|
||||
raise ValueError(f"Invalid metric target: {self._metric_target}")
|
||||
|
||||
raise ValueError(f"Invalid metric target: {self._metric_target}")
|
||||
|
||||
def _filter_detections_by_size(
|
||||
self, detections: Detections, size_category: ObjectSizeCategory
|
||||
) -> Detections:
|
||||
@@ -494,10 +534,12 @@ class PrecisionResult:
|
||||
precision_scores: the precision scores at each IoU threshold.
|
||||
Shape: `(num_iou_thresholds,)`
|
||||
precision_per_class: the precision scores per class and
|
||||
IoU threshold. Shape: `(num_target_classes, num_iou_thresholds)`
|
||||
IoU threshold. Shape: `(num_classes, num_iou_thresholds)`
|
||||
iou_thresholds: the IoU thresholds used in the calculations.
|
||||
matched_classes: the class IDs of all matched classes.
|
||||
Corresponds to the rows of `precision_per_class`.
|
||||
matched_classes: the class IDs present in either predictions or ground
|
||||
truth. Corresponds to the rows of `precision_per_class`. Classes
|
||||
that appear only in predictions (no ground-truth instances) are
|
||||
included; their per-threshold precision values will be `0.0`.
|
||||
small_objects: the Precision metric results
|
||||
for small objects (area < 32²).
|
||||
medium_objects: the Precision metric results
|
||||
|
||||
@@ -158,7 +158,9 @@ def pillow_to_cv2(image: Image.Image) -> npt.NDArray[np.uint8]:
|
||||
"""
|
||||
scene = np.array(image)
|
||||
scene = cv2.cvtColor(scene, cv2.COLOR_RGB2BGR)
|
||||
return scene.astype(np.uint8)
|
||||
# cvtColor already returns uint8 here, so astype is a no-op other than the
|
||||
# full-image copy it forces; copy=False keeps the dtype guard without it.
|
||||
return scene.astype(np.uint8, copy=False)
|
||||
|
||||
|
||||
def cv2_to_pillow(image: npt.NDArray[np.uint8]) -> Image.Image:
|
||||
|
||||
@@ -30,8 +30,10 @@ from supervision.annotators.core import (
|
||||
RoundBoxAnnotator,
|
||||
TraceAnnotator,
|
||||
TriangleAnnotator,
|
||||
_paint_masks_by_area,
|
||||
)
|
||||
from supervision.annotators.utils import ColorLookup
|
||||
from supervision.detection.compact_mask import CompactMask
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.draw.color import Color
|
||||
from supervision.geometry.core import Position
|
||||
@@ -363,6 +365,173 @@ class TestHaloAnnotator:
|
||||
)
|
||||
assert np.array_equal(result_bool, result_uint8)
|
||||
|
||||
def test_annotate_with_all_false_mask_preserves_scene(self):
|
||||
"""Test that an all-False mask leaves the scene unchanged, not corrupted."""
|
||||
scene = np.full((100, 100, 3), 127, dtype=np.uint8)
|
||||
masks = [np.zeros((100, 100), dtype=bool)]
|
||||
detections = _create_detections(
|
||||
xyxy=[[10, 10, 90, 90]], mask=masks, class_id=[0]
|
||||
)
|
||||
result = HaloAnnotator().annotate(scene=scene.copy(), detections=detections)
|
||||
assert np.array_equal(result, scene)
|
||||
|
||||
|
||||
class TestPaintMasksByArea:
|
||||
"""Tests for the _paint_masks_by_area helper function."""
|
||||
|
||||
def test_paint_masks_by_area_is_noop_without_masks(self):
|
||||
"""_paint_masks_by_area is a no-op when detections carry no mask."""
|
||||
canvas = np.full((10, 10, 3), 7, dtype=np.uint8)
|
||||
detections = _create_detections(xyxy=[[1, 1, 8, 8]], class_id=[0])
|
||||
_paint_masks_by_area(canvas, detections, Color.RED, ColorLookup.INDEX)
|
||||
assert np.array_equal(canvas, np.full((10, 10, 3), 7, dtype=np.uint8))
|
||||
|
||||
def test_union_accumulation_dense(self):
|
||||
"""Dense path: collect_union=True returns array covering all painted pixels."""
|
||||
height, width = 50, 60
|
||||
canvas = np.zeros((height, width, 3), dtype=np.uint8)
|
||||
masks = [np.zeros((height, width), dtype=bool)]
|
||||
masks[0][5:20, 10:40] = True
|
||||
detections = _create_detections(
|
||||
xyxy=[[10.0, 5.0, 40.0, 20.0]], mask=masks, class_id=[0]
|
||||
)
|
||||
result_union = _paint_masks_by_area(
|
||||
canvas, detections, Color.RED, ColorLookup.INDEX, collect_union=True
|
||||
)
|
||||
assert result_union is not None
|
||||
# every painted pixel must be in the union (RED is BGR (0, 0, 255),
|
||||
# so detect painted pixels via any non-zero channel)
|
||||
painted = canvas.any(axis=-1)
|
||||
assert np.array_equal(painted, result_union)
|
||||
|
||||
def test_union_accumulation_compact(self):
|
||||
"""CompactMask path: collect_union=True returns array matching dense."""
|
||||
height, width = 50, 60
|
||||
mask = np.zeros((height, width), dtype=bool)
|
||||
mask[5:20, 10:40] = True
|
||||
xyxy = np.array([[10.0, 5.0, 40.0, 20.0]])
|
||||
|
||||
canvas_dense = np.zeros((height, width, 3), dtype=np.uint8)
|
||||
dense = _create_detections(xyxy=xyxy.tolist(), mask=[mask], class_id=[0])
|
||||
union_dense = _paint_masks_by_area(
|
||||
canvas_dense, dense, Color.RED, ColorLookup.INDEX, collect_union=True
|
||||
)
|
||||
|
||||
canvas_compact = np.zeros((height, width, 3), dtype=np.uint8)
|
||||
compact = _create_detections(xyxy=xyxy.tolist(), mask=[mask], class_id=[0])
|
||||
compact.mask = CompactMask.from_dense(
|
||||
np.array([mask]), compact.xyxy, (height, width)
|
||||
)
|
||||
union_compact = _paint_masks_by_area(
|
||||
canvas_compact, compact, Color.RED, ColorLookup.INDEX, collect_union=True
|
||||
)
|
||||
|
||||
assert union_dense is not None
|
||||
assert union_compact is not None
|
||||
# compact union must cover exactly the same pixels as dense union
|
||||
assert np.array_equal(union_dense, union_compact)
|
||||
|
||||
def test_compact_mask_drops_pixels_outside_bbox(self):
|
||||
"""CompactMask is lossy: True pixels outside xyxy bbox are silently dropped.
|
||||
|
||||
This test documents that compact and dense paths diverge when a mask has
|
||||
True pixels outside its bounding box — the 'bit-identical' claim holds
|
||||
only for bbox-contained masks.
|
||||
"""
|
||||
height, width = 50, 60
|
||||
mask = np.zeros((height, width), dtype=bool)
|
||||
mask[5:25, 10:40] = True # mask extends 5 rows beyond bbox bottom
|
||||
|
||||
bbox = [[10.0, 5.0, 40.0, 20.0]] # y2=20 clips the mask at row 20
|
||||
|
||||
canvas_dense = np.zeros((height, width, 3), dtype=np.uint8)
|
||||
dense = _create_detections(xyxy=bbox, mask=[mask], class_id=[0])
|
||||
_paint_masks_by_area(canvas_dense, dense, Color.RED, ColorLookup.INDEX)
|
||||
|
||||
canvas_compact = np.zeros((height, width, 3), dtype=np.uint8)
|
||||
compact = _create_detections(xyxy=bbox, mask=[mask], class_id=[0])
|
||||
compact.mask = CompactMask.from_dense(
|
||||
np.array([mask]), compact.xyxy, (height, width)
|
||||
)
|
||||
_paint_masks_by_area(canvas_compact, compact, Color.RED, ColorLookup.INDEX)
|
||||
|
||||
# Dense paints all True pixels incl. rows 21-24; compact only within bbox.
|
||||
assert not np.array_equal(canvas_dense, canvas_compact), (
|
||||
"Expected divergence: compact mask drops True pixels outside bbox"
|
||||
)
|
||||
# Compact subset: every pixel painted by compact is also painted by dense.
|
||||
compact_painted = canvas_compact.any(axis=-1)
|
||||
dense_painted = canvas_dense.any(axis=-1)
|
||||
assert np.all(dense_painted[compact_painted])
|
||||
|
||||
|
||||
class TestCompactMaskParity:
|
||||
"""Tests that CompactMask and dense mask produce identical annotator output."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"annotator_factory",
|
||||
[
|
||||
pytest.param(
|
||||
lambda: MaskAnnotator(opacity=1.0, color_lookup=ColorLookup.INDEX),
|
||||
id="mask",
|
||||
),
|
||||
pytest.param(
|
||||
lambda: HaloAnnotator(kernel_size=15, color_lookup=ColorLookup.INDEX),
|
||||
id="halo",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_annotator_compact_mask_matches_dense_mask(self, annotator_factory):
|
||||
"""CompactMask detections annotate identically to dense bool masks."""
|
||||
height, width = 120, 160
|
||||
rng = np.random.default_rng(0)
|
||||
scene = rng.integers(0, 256, (height, width, 3), dtype=np.uint8)
|
||||
boxes = [[10, 10, 70, 60], [40, 30, 150, 110], [90, 70, 140, 115]]
|
||||
masks = []
|
||||
for x1, y1, x2, y2 in boxes:
|
||||
mask = np.zeros((height, width), dtype=bool)
|
||||
mask[y1 : y2 + 1, x1 : x2 + 1] = True
|
||||
masks.append(mask)
|
||||
class_id = [0, 1, 2]
|
||||
xyxy = [[float(value) for value in box] for box in boxes]
|
||||
|
||||
dense = _create_detections(xyxy=xyxy, mask=masks, class_id=class_id)
|
||||
compact = _create_detections(xyxy=xyxy, mask=masks, class_id=class_id)
|
||||
compact.mask = CompactMask.from_dense(
|
||||
np.array(masks), compact.xyxy, (height, width)
|
||||
)
|
||||
|
||||
result_dense = annotator_factory().annotate(
|
||||
scene=scene.copy(), detections=dense
|
||||
)
|
||||
result_compact = annotator_factory().annotate(
|
||||
scene=scene.copy(), detections=compact
|
||||
)
|
||||
|
||||
assert not np.array_equal(result_dense, scene), "annotator painted nothing"
|
||||
assert np.array_equal(result_dense, result_compact)
|
||||
|
||||
def test_annotator_compact_mask_handles_edge_clipping(self):
|
||||
"""CompactMask detection straddling image edge paints via NumPy clip."""
|
||||
height, width = 50, 60
|
||||
rng = np.random.default_rng(42)
|
||||
scene = rng.integers(0, 256, (height, width, 3), dtype=np.uint8)
|
||||
|
||||
# Box extends 10 pixels beyond right/bottom edges
|
||||
mask = np.zeros((height, width), dtype=bool)
|
||||
mask[40:height, 50:width] = True
|
||||
bbox = [[50.0, 40.0, width + 10.0, height + 10.0]]
|
||||
|
||||
detections = _create_detections(xyxy=bbox, mask=[mask], class_id=[0])
|
||||
detections.mask = CompactMask.from_dense(
|
||||
np.array([mask]), detections.xyxy, (height, width)
|
||||
)
|
||||
|
||||
annotator = MaskAnnotator(opacity=1.0, color_lookup=ColorLookup.INDEX)
|
||||
result = annotator.annotate(scene=scene.copy(), detections=detections)
|
||||
# Result must differ from scene (something was painted) and must not raise
|
||||
assert not np.array_equal(result, scene), "Expected pixels to be painted"
|
||||
|
||||
|
||||
class TestHeatMapAnnotator:
|
||||
"""Tests for HeatMapAnnotator class"""
|
||||
|
||||
@@ -13,6 +13,7 @@ from supervision.dataset.formats.coco import (
|
||||
build_coco_class_index_mapping,
|
||||
classes_to_coco_categories,
|
||||
coco_annotations_to_detections,
|
||||
coco_annotations_to_masks,
|
||||
coco_categories_to_classes,
|
||||
detections_to_coco_annotations,
|
||||
group_coco_annotations_by_image_id,
|
||||
@@ -995,6 +996,80 @@ def test_detections_to_coco_annotations_handles_empty_approximated_polygons() ->
|
||||
assert annotations[0]["iscrowd"] == 0
|
||||
|
||||
|
||||
_DISJOINT_2X2_MASK = np.array(
|
||||
[
|
||||
[
|
||||
[1, 1, 0, 0, 0],
|
||||
[1, 1, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 1, 1],
|
||||
[0, 0, 0, 1, 1],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
)
|
||||
|
||||
_SINGLE_COMPONENT_MASK = np.array(
|
||||
[
|
||||
[
|
||||
[1, 1, 1, 0, 0],
|
||||
[1, 1, 1, 0, 0],
|
||||
[1, 1, 1, 0, 0],
|
||||
[0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
)
|
||||
|
||||
|
||||
def _make_iscrowd0_detections(mask: np.ndarray) -> Detections:
|
||||
"""Build a single-detection Detections with iscrowd=0 from a (1, H, W) mask."""
|
||||
_, h, w = mask.shape
|
||||
return Detections(
|
||||
xyxy=np.array([[0, 0, w, h]], dtype=np.float32),
|
||||
class_id=np.array([0], dtype=int),
|
||||
mask=mask,
|
||||
data={"iscrowd": np.array([0])},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mask", "expected_segment_count"),
|
||||
[
|
||||
pytest.param(_DISJOINT_2X2_MASK, 2, id="disjoint-two-parts"),
|
||||
pytest.param(_SINGLE_COMPONENT_MASK, 1, id="single-component"),
|
||||
],
|
||||
)
|
||||
def test_detections_to_coco_annotations_segmentation_count(
|
||||
mask: np.ndarray, expected_segment_count: int
|
||||
) -> None:
|
||||
"""Non-crowd mask export produces one polygon list entry per connected component."""
|
||||
annotations, _ = detections_to_coco_annotations(
|
||||
detections=_make_iscrowd0_detections(mask), image_id=0, annotation_id=0
|
||||
)
|
||||
|
||||
segmentation = annotations[0]["segmentation"]
|
||||
assert annotations[0]["iscrowd"] == 0
|
||||
assert isinstance(segmentation, list)
|
||||
assert len(segmentation) == expected_segment_count
|
||||
assert all(len(part) >= 6 for part in segmentation)
|
||||
assert all(np.isfinite(c) for part in segmentation for c in part)
|
||||
|
||||
|
||||
def test_detections_to_coco_annotations_round_trip_disjoint_mask() -> None:
|
||||
"""Two-part disjoint mask round-trips through COCO export and import unchanged."""
|
||||
W, H = 5, 5
|
||||
annotations, _ = detections_to_coco_annotations(
|
||||
detections=_make_iscrowd0_detections(_DISJOINT_2X2_MASK),
|
||||
image_id=0,
|
||||
annotation_id=0,
|
||||
)
|
||||
reloaded = coco_annotations_to_masks([annotations[0]], resolution_wh=(W, H))
|
||||
|
||||
assert np.array_equal(reloaded[0], _DISJOINT_2X2_MASK[0])
|
||||
|
||||
|
||||
def test_detections_to_coco_annotations_preserves_area_from_data() -> None:
|
||||
"""area stored in detections.data should be used instead of bbox area."""
|
||||
detections = Detections(
|
||||
|
||||
@@ -8,6 +8,7 @@ from defusedxml import ElementTree
|
||||
|
||||
from supervision.dataset.formats.pascal_voc import (
|
||||
detections_from_xml_obj,
|
||||
detections_to_pascal_voc,
|
||||
object_to_pascal_voc,
|
||||
parse_polygon_points,
|
||||
)
|
||||
@@ -72,6 +73,47 @@ def test_object_to_pascal_voc(
|
||||
assert are_xml_elements_equal(result, expected_result)
|
||||
|
||||
|
||||
def test_object_to_pascal_voc_does_not_mutate_inputs():
|
||||
"""Serializing an object must not write the 1-index offset back into the inputs."""
|
||||
xyxy = np.array([10, 20, 30, 40], dtype=np.float32)
|
||||
polygon = np.array([[0, 0], [10, 0], [10, 10], [0, 10]], dtype=np.float32)
|
||||
|
||||
object_to_pascal_voc(xyxy=xyxy, name="test", polygon=polygon)
|
||||
|
||||
assert np.array_equal(xyxy, np.array([10, 20, 30, 40], dtype=np.float32))
|
||||
assert np.array_equal(
|
||||
polygon, np.array([[0, 0], [10, 0], [10, 10], [0, 10]], dtype=np.float32)
|
||||
)
|
||||
|
||||
|
||||
def test_object_to_pascal_voc_does_not_mutate_view_input():
|
||||
"""Mutation guard holds when xyxy is a NumPy row-view (the actual bug scenario)."""
|
||||
base = np.array([[10, 20, 30, 40]], dtype=np.float32)
|
||||
xyxy_view = base[0] # row-view, shares memory with base
|
||||
|
||||
object_to_pascal_voc(xyxy=xyxy_view, name="test", polygon=None)
|
||||
|
||||
assert np.array_equal(base[0], np.array([10, 20, 30, 40], dtype=np.float32)), (
|
||||
"object_to_pascal_voc mutated the source array via a view"
|
||||
)
|
||||
|
||||
|
||||
def test_detections_to_pascal_voc_does_not_mutate_detections():
|
||||
"""Exporting detections must not shift the source xyxy, and must be repeatable."""
|
||||
detections = _create_detections(xyxy=[[10, 20, 30, 40]], class_id=[0])
|
||||
expected_xyxy = detections.xyxy.copy()
|
||||
|
||||
first = detections_to_pascal_voc(
|
||||
detections, classes=["test"], filename="image.jpg", image_shape=(100, 100, 3)
|
||||
)
|
||||
second = detections_to_pascal_voc(
|
||||
detections, classes=["test"], filename="image.jpg", image_shape=(100, 100, 3)
|
||||
)
|
||||
|
||||
assert np.array_equal(detections.xyxy, expected_xyxy)
|
||||
assert first == second
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("polygon_element", "expected_result", "exception"),
|
||||
[
|
||||
|
||||
@@ -395,6 +395,53 @@ def test_json_sink(
|
||||
assert_json_equal(file_name, expected_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("scalar", "expected"),
|
||||
[
|
||||
pytest.param(np.int64(7), 7, id="np_int64"),
|
||||
pytest.param(np.float32(0.5), pytest.approx(0.5), id="np_float32"),
|
||||
pytest.param(np.float64(1.5), pytest.approx(1.5), id="np_float64"),
|
||||
pytest.param(np.int32(3), 3, id="np_int32"),
|
||||
pytest.param(np.bool_(True), True, id="np_bool"),
|
||||
],
|
||||
)
|
||||
def test_json_sink_serializes_numpy_scalar_custom_data(
|
||||
tmp_path: Any, scalar: Any, expected: Any
|
||||
) -> None:
|
||||
"""NumPy scalar in custom_data serializes as a JSON number or boolean."""
|
||||
file_name = str(tmp_path / "test_numpy_scalar.json")
|
||||
detections = sv.Detections(
|
||||
xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32),
|
||||
class_id=np.array([0]),
|
||||
confidence=np.array([0.9]),
|
||||
)
|
||||
with sv.JSONSink(file_name) as sink:
|
||||
sink.append(detections, custom_data={"value": scalar})
|
||||
with open(file_name) as f:
|
||||
data = json.load(f)
|
||||
assert data[0]["value"] == expected
|
||||
|
||||
|
||||
def test_json_sink_serializes_nested_numpy_array_custom_data(tmp_path: Any) -> None:
|
||||
"""NumPy array nested inside a custom_data dict value serializes as a JSON array."""
|
||||
file_name = str(tmp_path / "test_nested_array.json")
|
||||
detections = sv.Detections(
|
||||
xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32),
|
||||
class_id=np.array([0]),
|
||||
)
|
||||
with sv.JSONSink(file_name) as sink:
|
||||
sink.append(detections, custom_data={"meta": {"arr": np.array([1, 2, 3])}})
|
||||
with open(file_name) as f:
|
||||
data = json.load(f)
|
||||
assert data[0]["meta"]["arr"] == [1, 2, 3]
|
||||
|
||||
|
||||
def test_json_default_raises_for_unserializable_type() -> None:
|
||||
"""_json_default raises TypeError for non-numpy objects."""
|
||||
with pytest.raises(TypeError, match="is not JSON serializable"):
|
||||
sv.JSONSink._json_default(object())
|
||||
|
||||
|
||||
def assert_json_equal(file_name, expected_rows):
|
||||
with open(file_name) as file:
|
||||
data = json.load(file)
|
||||
|
||||
@@ -1297,3 +1297,49 @@ def test_from_deepseek_vl_2(
|
||||
detections.data[CLASS_NAME_DATA_FIELD],
|
||||
expected_detections.data[CLASS_NAME_DATA_FIELD],
|
||||
)
|
||||
|
||||
|
||||
def test_from_google_gemini_2_5_malformed_mask_keeps_confidence_aligned():
|
||||
"""A non-data-URI mask must not skip the item's confidence and desync arrays."""
|
||||
result = (
|
||||
'[{"box_2d": [10, 10, 100, 100], "label": "cat", "mask": "bad", '
|
||||
'"confidence": 0.8}, {"box_2d": [20, 20, 120, 120], "label": "dog", '
|
||||
'"mask": "bad", "confidence": 0.9}]'
|
||||
)
|
||||
|
||||
xyxy, _, _, confidence, masks = from_google_gemini_2_5(
|
||||
result=result, resolution_wh=(640, 480)
|
||||
)
|
||||
|
||||
assert xyxy.shape == (2, 4)
|
||||
assert confidence is not None
|
||||
assert confidence.shape == (2,)
|
||||
assert np.allclose(confidence, [0.8, 0.9])
|
||||
assert masks is not None
|
||||
assert masks.shape == (2, 480, 640)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("parser", "result"),
|
||||
[
|
||||
pytest.param(
|
||||
from_google_gemini_2_0, "[1, 2, 3]", id="gemini_2_0_non_dict_items"
|
||||
),
|
||||
pytest.param(from_google_gemini_2_0, "42", id="gemini_2_0_non_list"),
|
||||
pytest.param(
|
||||
from_google_gemini_2_5, "[1, 2, 3]", id="gemini_2_5_non_dict_items"
|
||||
),
|
||||
pytest.param(from_google_gemini_2_5, "42", id="gemini_2_5_non_list"),
|
||||
pytest.param(from_qwen_2_5_vl, "[1, 2, 3]", id="qwen_2_5_non_dict_items"),
|
||||
pytest.param(from_qwen_2_5_vl, "42", id="qwen_2_5_non_list"),
|
||||
],
|
||||
)
|
||||
def test_vlm_parsers_degrade_on_malformed_json(parser, result):
|
||||
"""Valid JSON of the wrong shape should yield empty results, not raise."""
|
||||
kwargs: dict = {"resolution_wh": (640, 480)}
|
||||
if parser is from_qwen_2_5_vl:
|
||||
kwargs["input_wh"] = (640, 480)
|
||||
|
||||
xyxy = parser(result=result, **kwargs)[0]
|
||||
|
||||
assert xyxy.shape == (0, 4)
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
"""Tests for DetectionsSmoother bounding-box and confidence smoothing."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from numpy.testing import assert_allclose
|
||||
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.detection.tools.smoother import DetectionsSmoother
|
||||
from supervision.utils.internal import SupervisionWarnings
|
||||
|
||||
|
||||
class TestDetectionsSmoother:
|
||||
@pytest.mark.parametrize(
|
||||
("conf1", "conf2", "expected_confidence"),
|
||||
[
|
||||
pytest.param(
|
||||
np.array([0.5]),
|
||||
np.array([0.7]),
|
||||
np.array([0.6]),
|
||||
id="with_confidence",
|
||||
),
|
||||
pytest.param(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
id="no_confidence",
|
||||
),
|
||||
pytest.param(
|
||||
np.array([0.5]),
|
||||
None,
|
||||
np.array([0.5]),
|
||||
id="mixed_window_averages_present",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_smoother_confidence_scenarios(
|
||||
self,
|
||||
conf1: np.ndarray | None,
|
||||
conf2: np.ndarray | None,
|
||||
expected_confidence: np.ndarray | None,
|
||||
) -> None:
|
||||
"""Boxes average over window; confidence averages present values or None."""
|
||||
smoother = DetectionsSmoother(length=3)
|
||||
smoother.update_with_detections(
|
||||
Detections(
|
||||
xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32),
|
||||
confidence=conf1,
|
||||
tracker_id=np.array([1]),
|
||||
)
|
||||
)
|
||||
smoothed = smoother.update_with_detections(
|
||||
Detections(
|
||||
xyxy=np.array([[2, 2, 12, 12]], dtype=np.float32),
|
||||
confidence=conf2,
|
||||
tracker_id=np.array([1]),
|
||||
)
|
||||
)
|
||||
|
||||
assert_allclose(smoothed.xyxy, np.array([[1, 1, 11, 11]]), atol=1e-5)
|
||||
if expected_confidence is None:
|
||||
assert smoothed.confidence is None
|
||||
else:
|
||||
assert smoothed.confidence is not None
|
||||
assert_allclose(smoothed.confidence, expected_confidence, atol=1e-5)
|
||||
|
||||
def test_smoother_multi_track_mixed_confidence_does_not_crash(self) -> None:
|
||||
"""Two tracks with different confidence presence must not raise on merge."""
|
||||
smoother = DetectionsSmoother(length=3)
|
||||
smoother.update_with_detections(
|
||||
Detections(
|
||||
xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32),
|
||||
confidence=np.array([0.5]),
|
||||
tracker_id=np.array([1]),
|
||||
)
|
||||
)
|
||||
smoothed = smoother.update_with_detections(
|
||||
Detections(
|
||||
xyxy=np.array([[20, 20, 30, 30]], dtype=np.float32),
|
||||
tracker_id=np.array([2]),
|
||||
)
|
||||
)
|
||||
|
||||
assert len(smoothed) == 2
|
||||
assert smoothed.confidence is None
|
||||
|
||||
def test_smoother_tracker_id_none_warns_and_returns_unchanged(self) -> None:
|
||||
"""update_with_detections warns and returns input when tracker_id is None."""
|
||||
smoother = DetectionsSmoother(length=3)
|
||||
detections = Detections(
|
||||
xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32),
|
||||
tracker_id=None,
|
||||
)
|
||||
|
||||
with pytest.warns(SupervisionWarnings):
|
||||
result = smoother.update_with_detections(detections)
|
||||
|
||||
assert result is detections
|
||||
|
||||
def test_smoother_window_full_averages_all_frames(self) -> None:
|
||||
"""Full window (length=3) averages all 3 frames, not just the last two."""
|
||||
smoother = DetectionsSmoother(length=3)
|
||||
smoother.update_with_detections(
|
||||
Detections(
|
||||
xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32),
|
||||
confidence=np.array([0.3]),
|
||||
tracker_id=np.array([1]),
|
||||
)
|
||||
)
|
||||
smoother.update_with_detections(
|
||||
Detections(
|
||||
xyxy=np.array([[3, 3, 13, 13]], dtype=np.float32),
|
||||
confidence=np.array([0.6]),
|
||||
tracker_id=np.array([1]),
|
||||
)
|
||||
)
|
||||
smoothed = smoother.update_with_detections(
|
||||
Detections(
|
||||
xyxy=np.array([[6, 6, 16, 16]], dtype=np.float32),
|
||||
confidence=np.array([0.9]),
|
||||
tracker_id=np.array([1]),
|
||||
)
|
||||
)
|
||||
|
||||
assert_allclose(smoothed.xyxy, np.array([[3, 3, 13, 13]]), atol=1e-5)
|
||||
assert smoothed.confidence is not None
|
||||
assert_allclose(smoothed.confidence, np.array([0.6]), atol=1e-5)
|
||||
@@ -15,6 +15,7 @@ from supervision.detection.utils.converters import (
|
||||
_rle_counts_to_mask,
|
||||
is_compressed_rle,
|
||||
mask_to_rle,
|
||||
mask_to_xyxy,
|
||||
rle_to_mask,
|
||||
xcycwh_to_xyxy,
|
||||
xywh_to_xyxy,
|
||||
@@ -315,6 +316,86 @@ def test_xyxy_to_mask(boxes: np.ndarray, resolution_wh, expected: np.ndarray) ->
|
||||
np.testing.assert_array_equal(result, expected)
|
||||
|
||||
|
||||
def _mask_to_xyxy_reference(masks: np.ndarray) -> np.ndarray:
|
||||
"""Per-mask `np.where` loop used as a ground-truth oracle."""
|
||||
xyxy = np.zeros((masks.shape[0], 4), dtype=int)
|
||||
for i, mask in enumerate(masks):
|
||||
rows, cols = np.where(mask)
|
||||
if len(rows) > 0 and len(cols) > 0:
|
||||
xyxy[i, :] = [
|
||||
int(cols.min()),
|
||||
int(rows.min()),
|
||||
int(cols.max()),
|
||||
int(rows.max()),
|
||||
]
|
||||
return xyxy
|
||||
|
||||
|
||||
class TestMaskToXyxy:
|
||||
@pytest.mark.parametrize(
|
||||
("masks", "expected"),
|
||||
[
|
||||
pytest.param(
|
||||
np.zeros((0, 5, 5), dtype=bool),
|
||||
np.zeros((0, 4), dtype=int),
|
||||
id="no-masks",
|
||||
),
|
||||
pytest.param(
|
||||
np.zeros((2, 5, 5), dtype=bool),
|
||||
np.zeros((2, 4), dtype=int),
|
||||
id="all-empty-masks",
|
||||
),
|
||||
pytest.param(
|
||||
np.array([[[False, False], [False, True]]], dtype=bool),
|
||||
np.array([[1, 1, 1, 1]], dtype=int),
|
||||
id="single-pixel-bottom-right",
|
||||
),
|
||||
pytest.param(
|
||||
np.array([[[True, False], [False, False]]], dtype=bool),
|
||||
np.array([[0, 0, 0, 0]], dtype=int),
|
||||
id="single-pixel-top-left",
|
||||
),
|
||||
pytest.param(
|
||||
np.array([[[False, True], [False, False]]], dtype=bool),
|
||||
np.array([[1, 0, 1, 0]], dtype=int),
|
||||
id="single-pixel-top-right",
|
||||
),
|
||||
pytest.param(
|
||||
np.array([[[False, False], [True, False]]], dtype=bool),
|
||||
np.array([[0, 1, 0, 1]], dtype=int),
|
||||
id="single-pixel-bottom-left",
|
||||
),
|
||||
pytest.param(
|
||||
np.ones((1, 4, 6), dtype=bool),
|
||||
np.array([[0, 0, 5, 3]], dtype=int),
|
||||
id="full-mask",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_mask_to_xyxy_known_values(
|
||||
self, masks: np.ndarray, expected: np.ndarray
|
||||
) -> None:
|
||||
"""Known boxes for empty, single-pixel, and full masks."""
|
||||
result = mask_to_xyxy(masks)
|
||||
|
||||
assert result.dtype == expected.dtype
|
||||
assert result.shape == expected.shape
|
||||
np.testing.assert_array_equal(result, expected)
|
||||
|
||||
@pytest.mark.parametrize("seed", [0, 1, 2, 3, 4])
|
||||
def test_mask_to_xyxy_matches_reference(self, seed: int) -> None:
|
||||
"""Vectorized output is bit-identical to the per-mask scalar loop."""
|
||||
rng = np.random.default_rng(seed)
|
||||
masks = rng.random((6, 30, 40)) < 0.1
|
||||
masks[0] = False # include an empty mask in the batch
|
||||
|
||||
result = mask_to_xyxy(masks)
|
||||
reference = _mask_to_xyxy_reference(masks)
|
||||
|
||||
assert result.dtype == reference.dtype
|
||||
np.testing.assert_array_equal(result, reference)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mask", "compressed", "expected_rle", "exception"),
|
||||
[
|
||||
|
||||
@@ -11,6 +11,7 @@ from supervision.detection.utils.iou_and_nms import (
|
||||
box_iou,
|
||||
box_iou_batch,
|
||||
box_non_max_suppression,
|
||||
mask_iou_batch,
|
||||
mask_non_max_merge,
|
||||
mask_non_max_suppression,
|
||||
oriented_box_iou_batch,
|
||||
@@ -1580,3 +1581,161 @@ class TestOrientedBoxNonMaxMerge:
|
||||
|
||||
sorted_groups = sorted(sorted(g) for g in groups)
|
||||
assert sorted_groups == [[0, 1], [2]]
|
||||
|
||||
|
||||
def _naive_mask_iou(
|
||||
masks_true: np.ndarray,
|
||||
masks_detection: np.ndarray,
|
||||
overlap_metric: OverlapMetric = OverlapMetric.IOU,
|
||||
) -> np.ndarray:
|
||||
"""Reference IoU that materialises the (N, M, H, W) overlap explicitly."""
|
||||
intersection = np.logical_and(masks_true[:, None], masks_detection).sum(axis=(2, 3))
|
||||
area_true = masks_true.sum(axis=(1, 2))
|
||||
area_detection = masks_detection.sum(axis=(1, 2))
|
||||
if overlap_metric == OverlapMetric.IOU:
|
||||
denominator = area_true[:, None] + area_detection - intersection
|
||||
else:
|
||||
denominator = np.minimum(area_true[:, None], area_detection)
|
||||
return np.divide(
|
||||
intersection,
|
||||
denominator,
|
||||
out=np.zeros((len(masks_true), len(masks_detection)), dtype=float),
|
||||
where=denominator != 0,
|
||||
)
|
||||
|
||||
|
||||
class TestMaskIouBatch:
|
||||
"""`mask_iou_batch` (matmul intersection) matches the naive boolean reference."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"overlap_metric",
|
||||
[
|
||||
pytest.param(OverlapMetric.IOU, id="iou"),
|
||||
pytest.param(OverlapMetric.IOS, id="ios"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("n_true", "n_detection", "height", "width"),
|
||||
[
|
||||
pytest.param(1, 1, 20, 24, id="single"),
|
||||
pytest.param(7, 7, 32, 40, id="several-square"),
|
||||
pytest.param(5, 9, 28, 36, id="rectangular"),
|
||||
],
|
||||
)
|
||||
def test_matches_naive_reference_on_random_masks(
|
||||
self,
|
||||
n_true: int,
|
||||
n_detection: int,
|
||||
height: int,
|
||||
width: int,
|
||||
overlap_metric: OverlapMetric,
|
||||
) -> None:
|
||||
"""Random masks give the same matrix as the explicit (N, M, H, W) reference."""
|
||||
rng = np.random.default_rng(0)
|
||||
masks_true = rng.random((n_true, height, width)) > 0.5
|
||||
masks_detection = rng.random((n_detection, height, width)) > 0.5
|
||||
|
||||
result = mask_iou_batch(masks_true, masks_detection, overlap_metric)
|
||||
|
||||
expected = _naive_mask_iou(masks_true, masks_detection, overlap_metric)
|
||||
np.testing.assert_allclose(result, expected)
|
||||
|
||||
def test_chunking_matches_single_pass(self) -> None:
|
||||
"""A tiny memory_limit forces chunking yet returns the same matrix."""
|
||||
rng = np.random.default_rng(1)
|
||||
masks_true = rng.random((6, 16, 16)) > 0.5
|
||||
masks_detection = rng.random((4, 16, 16)) > 0.5
|
||||
|
||||
single_pass = mask_iou_batch(masks_true, masks_detection)
|
||||
chunked = mask_iou_batch(masks_true, masks_detection, memory_limit=0)
|
||||
|
||||
np.testing.assert_allclose(single_pass, chunked, rtol=1e-6)
|
||||
|
||||
# empty masks_true under a tiny limit must not hit np.vstack([])
|
||||
empty = mask_iou_batch(
|
||||
np.zeros((0, 16, 16), dtype=bool), masks_detection, memory_limit=0
|
||||
)
|
||||
assert empty.shape == (0, 4)
|
||||
|
||||
def test_mismatched_spatial_shape_raises(self) -> None:
|
||||
"""Equal pixel counts but different (H, W) must raise, not silently mismatch."""
|
||||
masks_true = np.zeros((1, 4, 9), dtype=bool)
|
||||
masks_detection = np.zeros((1, 6, 6), dtype=bool) # 36 px, different (H, W)
|
||||
with pytest.raises(ValueError, match="must share the same"):
|
||||
mask_iou_batch(masks_true, masks_detection)
|
||||
|
||||
def test_invalid_ndim_raises(self) -> None:
|
||||
"""2D and 4D inputs must raise ValueError before reaching shape[1:] check."""
|
||||
mask_3d = np.zeros((1, 10, 10), dtype=bool)
|
||||
with pytest.raises(ValueError, match="3D"):
|
||||
mask_iou_batch(np.zeros((10, 10), dtype=bool), mask_3d)
|
||||
with pytest.raises(ValueError, match="3D"):
|
||||
mask_iou_batch(mask_3d, np.zeros((10, 10), dtype=bool))
|
||||
with pytest.raises(ValueError, match="3D"):
|
||||
mask_iou_batch(np.zeros((1, 10, 10, 1), dtype=bool), mask_3d)
|
||||
|
||||
def test_identical_disjoint_and_empty(self) -> None:
|
||||
"""Identical/all-True → 1.0; all-False/disjoint → 0.0; empty keeps shape."""
|
||||
mask = np.zeros((1, 10, 10), dtype=bool)
|
||||
mask[0, 2:6, 2:6] = True
|
||||
far = np.zeros((1, 10, 10), dtype=bool)
|
||||
far[0, 7:9, 7:9] = True
|
||||
|
||||
assert mask_iou_batch(mask, mask)[0, 0] == pytest.approx(1.0)
|
||||
assert mask_iou_batch(mask, far)[0, 0] == 0.0
|
||||
assert mask_iou_batch(np.zeros((0, 10, 10), dtype=bool), mask).shape == (0, 1)
|
||||
|
||||
# all-True: denominator is non-zero, result must be exactly 1.0
|
||||
all_true = np.ones((2, 5, 5), dtype=bool)
|
||||
np.testing.assert_allclose(mask_iou_batch(all_true, all_true), np.ones((2, 2)))
|
||||
|
||||
# all-False: denominator is zero; result must be 0.0 (not NaN)
|
||||
all_false = np.zeros((2, 5, 5), dtype=bool)
|
||||
np.testing.assert_array_equal(
|
||||
mask_iou_batch(all_false, all_false), np.zeros((2, 2))
|
||||
)
|
||||
|
||||
def test_matmul_emits_no_runtime_warnings(
|
||||
self, recwarn: pytest.WarningsChecker
|
||||
) -> None:
|
||||
"""mask_iou_batch on bool input must not emit any RuntimeWarning."""
|
||||
rng = np.random.default_rng(2)
|
||||
masks_true = rng.random((7, 32, 40)) > 0.5
|
||||
masks_detection = rng.random((5, 32, 40)) > 0.5
|
||||
mask_iou_batch(masks_true, masks_detection)
|
||||
runtime = [w for w in recwarn.list if issubclass(w.category, RuntimeWarning)]
|
||||
assert not runtime, (
|
||||
f"Unexpected RuntimeWarnings: {[str(w.message) for w in runtime]}"
|
||||
)
|
||||
|
||||
def test_float64_promotion_for_large_float32_masks(self) -> None:
|
||||
"""float32 masks with pixel count > 2**24 must give exact IoU via float64."""
|
||||
# 1x(2**24+1) pixels: float32 area sum rounds, float64 area sum is exact.
|
||||
# Without CRIT-2 fix, IoU would exceed 1.0 due to dtype mismatch.
|
||||
mask = np.ones((1, 1, 2**24 + 1), dtype=np.float32)
|
||||
result = mask_iou_batch(mask, mask)
|
||||
assert result[0, 0] == pytest.approx(1.0)
|
||||
|
||||
def test_compact_mask_mixed_input_gives_same_result(self) -> None:
|
||||
"""CompactMask + dense mixed dispatch must match dense + dense."""
|
||||
from supervision.detection.compact_mask import CompactMask
|
||||
from supervision.detection.utils.converters import mask_to_xyxy
|
||||
|
||||
rng = np.random.default_rng(3)
|
||||
masks = (rng.random((4, 20, 24)) > 0.3).astype(bool)
|
||||
xyxy = mask_to_xyxy(masks)
|
||||
cm = CompactMask.from_dense(masks, xyxy, image_shape=(20, 24))
|
||||
|
||||
dense_vs_dense = mask_iou_batch(masks, masks)
|
||||
np.testing.assert_allclose(mask_iou_batch(cm, masks), dense_vs_dense, rtol=1e-5)
|
||||
np.testing.assert_allclose(mask_iou_batch(masks, cm), dense_vs_dense, rtol=1e-5)
|
||||
|
||||
def test_detection_exceeds_limit_warns_and_completes(self) -> None:
|
||||
"""detection > memory_limit emits UserWarning; result shape is still correct."""
|
||||
rng = np.random.default_rng(4)
|
||||
masks_true = rng.random((3, 32, 32)) > 0.5
|
||||
# 500 x 1024 px x 4 bytes ~2 MB > memory_limit=1 MB triggers OOM warning.
|
||||
masks_detection = rng.random((500, 32, 32)) > 0.5
|
||||
with pytest.warns(UserWarning, match="exceed"):
|
||||
result = mask_iou_batch(masks_true, masks_detection, memory_limit=1)
|
||||
assert result.shape == (3, 500)
|
||||
|
||||
@@ -5,7 +5,10 @@ from contextlib import ExitStack as DoesNotRaise
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from supervision.detection.utils.polygons import filter_polygons_by_area
|
||||
from supervision.detection.utils.polygons import (
|
||||
approximate_polygon,
|
||||
filter_polygons_by_area,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -100,3 +103,69 @@ def test_filter_polygons_by_area(
|
||||
assert len(result) == len(expected_result)
|
||||
for result_polygon, expected_result_polygon in zip(result, expected_result):
|
||||
assert np.array_equal(result_polygon, expected_result_polygon)
|
||||
|
||||
|
||||
def _regular_polygon(num_points: int, radius: float = 40.0) -> np.ndarray:
|
||||
angles = np.linspace(0, 2 * np.pi, num_points, endpoint=False)
|
||||
return np.stack(
|
||||
[50 + radius * np.cos(angles), 50 + radius * np.sin(angles)], axis=1
|
||||
).astype(np.float32)
|
||||
|
||||
|
||||
class TestApproximatePolygon:
|
||||
@pytest.mark.parametrize("num_points", [20, 50, 100, 200])
|
||||
@pytest.mark.parametrize("percentage", [0.1, 0.5, 0.75, 0.9])
|
||||
def test_within_budget_and_valid(self, num_points: int, percentage: float) -> None:
|
||||
"""The result stays a valid polygon and respects the point budget.
|
||||
|
||||
The exception is the 3-point floor: keeping a valid polygon (at least 3
|
||||
points) can leave more points than the budget by an arbitrary margin,
|
||||
since a single epsilon step may jump straight from above budget to below
|
||||
3 points.
|
||||
"""
|
||||
polygon = _regular_polygon(num_points)
|
||||
target_points = max(int(num_points * (1 - percentage)), 3)
|
||||
|
||||
result = approximate_polygon(polygon, percentage=percentage)
|
||||
|
||||
assert result.ndim == 2
|
||||
assert result.shape[1] == 2
|
||||
assert 3 <= len(result) <= num_points
|
||||
if target_points > 3:
|
||||
assert len(result) <= target_points
|
||||
|
||||
def test_zero_percentage_keeps_polygon(self) -> None:
|
||||
"""A percentage of 0 removes no points."""
|
||||
polygon = _regular_polygon(40)
|
||||
|
||||
result = approximate_polygon(polygon, percentage=0.0)
|
||||
|
||||
assert len(result) == len(polygon)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"percentage",
|
||||
[
|
||||
pytest.param(-0.001, id="just-below-zero"),
|
||||
pytest.param(-1.0, id="negative-one"),
|
||||
pytest.param(1.0, id="exactly-one"),
|
||||
pytest.param(1.5, id="above-one"),
|
||||
],
|
||||
)
|
||||
def test_raises_on_out_of_range_percentage(self, percentage: float) -> None:
|
||||
"""Percentage outside [0, 1) must raise ValueError."""
|
||||
polygon = _regular_polygon(20)
|
||||
with pytest.raises(ValueError, match="Percentage must be in the range"):
|
||||
approximate_polygon(polygon, percentage=percentage)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"epsilon_step",
|
||||
[
|
||||
pytest.param(0.0, id="zero"),
|
||||
pytest.param(-0.05, id="negative"),
|
||||
],
|
||||
)
|
||||
def test_raises_on_non_positive_epsilon_step(self, epsilon_step: float) -> None:
|
||||
"""Non-positive epsilon_step must raise ValueError."""
|
||||
polygon = _regular_polygon(20)
|
||||
with pytest.raises(ValueError, match="epsilon_step must be positive"):
|
||||
approximate_polygon(polygon, percentage=0.5, epsilon_step=epsilon_step)
|
||||
|
||||
@@ -706,6 +706,75 @@ def test_key_points_as_detections_empty():
|
||||
assert empty_detections.is_empty()
|
||||
|
||||
|
||||
def test_key_points_as_detections_ignores_missing_keypoints():
|
||||
"""A [0, 0] keypoint is treated as missing and excluded from the box."""
|
||||
key_points = _create_key_points(
|
||||
xy=[[[0, 0], [10, 20], [30, 40]]],
|
||||
confidence=[[0.0, 0.8, 0.6]],
|
||||
class_id=[0],
|
||||
)
|
||||
|
||||
detections = key_points.as_detections()
|
||||
|
||||
assert np.array_equal(detections.xyxy, np.array([[10, 20, 30, 40]]))
|
||||
|
||||
|
||||
def test_key_points_as_detections_uses_detection_confidence():
|
||||
"""detection_confidence is preferred over the keypoint-confidence mean."""
|
||||
key_points = _create_key_points(
|
||||
xy=[[[10, 20], [30, 40]]],
|
||||
confidence=[[0.1, 0.2]],
|
||||
class_id=[0],
|
||||
detection_confidence=[0.95],
|
||||
)
|
||||
|
||||
detections = key_points.as_detections()
|
||||
|
||||
assert np.allclose(detections.confidence, np.array([0.95], dtype=np.float32))
|
||||
|
||||
|
||||
def test_key_points_as_detections_selected_keypoint_indices():
|
||||
"""Only the selected keypoints contribute to the bounding box."""
|
||||
key_points = _create_key_points(
|
||||
xy=[[[0, 0], [10, 20], [30, 40], [100, 100]]],
|
||||
confidence=[[0.5, 0.8, 0.6, 0.9]],
|
||||
class_id=[0],
|
||||
)
|
||||
|
||||
detections = key_points.as_detections(selected_keypoint_indices=[1, 2])
|
||||
|
||||
assert np.array_equal(detections.xyxy, np.array([[10, 20, 30, 40]]))
|
||||
|
||||
|
||||
def test_key_points_as_detections_confidence_over_selected_indices():
|
||||
"""Confidence mean uses only the selected keypoint columns, not all."""
|
||||
key_points = _create_key_points(
|
||||
xy=[[[0, 0], [10, 20], [30, 40], [100, 100]]],
|
||||
confidence=[[0.5, 0.8, 0.6, 0.9]],
|
||||
class_id=[0],
|
||||
)
|
||||
|
||||
detections = key_points.as_detections(selected_keypoint_indices=[1, 2])
|
||||
|
||||
expected_confidence = np.mean([0.8, 0.6], dtype=np.float32)
|
||||
assert np.isclose(detections.confidence[0], expected_confidence)
|
||||
|
||||
|
||||
def test_key_points_as_detections_mixed_valid_invalid_batch():
|
||||
"""Batch with one all-zero skeleton: invalid skeleton gets box zeroed."""
|
||||
key_points = _create_key_points(
|
||||
xy=[[[0, 0], [0, 0]], [[10, 20], [30, 40]]],
|
||||
confidence=[[0.0, 0.0], [0.8, 0.6]],
|
||||
class_id=[0, 1],
|
||||
)
|
||||
|
||||
detections = key_points.as_detections()
|
||||
|
||||
# Only the valid skeleton survives the area>0 filter
|
||||
assert len(detections) == 1
|
||||
assert np.array_equal(detections.xyxy, np.array([[10, 20, 30, 40]]))
|
||||
|
||||
|
||||
def test_key_points_as_detections_with_data():
|
||||
"""Test the as_detections method preserves data."""
|
||||
key_points = _create_key_points(
|
||||
@@ -925,3 +994,447 @@ def test_from_mediapipe_input(mediapipe_results, resolution_wh, expected_key_poi
|
||||
mediapipe_results, resolution_wh=resolution_wh
|
||||
)
|
||||
assert key_points == expected_key_points
|
||||
|
||||
|
||||
class TestDeprecatedConfidenceConstructor:
|
||||
"""Tests for backward-compatible `confidence=` kwarg in KeyPoints()."""
|
||||
|
||||
def test_constructor_accepts_and_warns_on_deprecated_confidence_kwarg(self):
|
||||
"""Deprecated confidence= warns and maps value to keypoint_confidence."""
|
||||
from supervision.utils.internal import SupervisionWarnings
|
||||
|
||||
xy = np.array([[[1.0, 2.0], [3.0, 4.0]]], dtype=np.float32)
|
||||
confidence = np.array([[0.9, 0.8]], dtype=np.float32)
|
||||
|
||||
with pytest.warns(SupervisionWarnings, match="deprecated since"):
|
||||
key_points = KeyPoints(xy=xy, confidence=confidence)
|
||||
|
||||
np.testing.assert_array_equal(key_points.keypoint_confidence, confidence)
|
||||
assert key_points.xy is xy
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"kwargs",
|
||||
[
|
||||
pytest.param(
|
||||
{"confidence": None, "keypoint_confidence": None},
|
||||
id="confidence-first",
|
||||
),
|
||||
pytest.param(
|
||||
{"keypoint_confidence": None, "confidence": None},
|
||||
id="keypoint-confidence-first",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_constructor_rejects_both_confidence_and_keypoint_confidence(
|
||||
self, kwargs: dict
|
||||
):
|
||||
"""ValueError raised regardless of kwarg order when both are passed."""
|
||||
xy = np.array([[[1.0, 2.0], [3.0, 4.0]]], dtype=np.float32)
|
||||
confidence_arr = np.array([[0.9, 0.8]], dtype=np.float32)
|
||||
actual_kwargs = {k: confidence_arr for k in kwargs}
|
||||
|
||||
with pytest.raises(ValueError, match="Cannot pass both"):
|
||||
KeyPoints(xy=xy, **actual_kwargs)
|
||||
|
||||
def test_constructor_normal_keypoint_confidence_path(self):
|
||||
"""Normal keypoint_confidence= path works and emits no deprecation warning."""
|
||||
import warnings
|
||||
|
||||
xy = np.array([[[1.0, 2.0], [3.0, 4.0]]], dtype=np.float32)
|
||||
kp_conf = np.array([[0.9, 0.8]], dtype=np.float32)
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
kp = KeyPoints(xy=xy, keypoint_confidence=kp_conf)
|
||||
|
||||
np.testing.assert_array_equal(kp.keypoint_confidence, kp_conf)
|
||||
assert kp.data == {}
|
||||
|
||||
def test_constructor_confidence_none_does_not_warn(self):
|
||||
"""Explicit confidence=None is silently ignored — no warning emitted."""
|
||||
import warnings
|
||||
|
||||
xy = np.array([[[1.0, 2.0], [3.0, 4.0]]], dtype=np.float32)
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
kp = KeyPoints(xy=xy, confidence=None)
|
||||
|
||||
assert kp.keypoint_confidence is None
|
||||
|
||||
def test_constructor_data_none_defaults_to_empty_dict(self):
|
||||
"""Explicit data=None normalizes to empty dict, not None."""
|
||||
xy = np.array([[[1.0, 2.0]]], dtype=np.float32)
|
||||
|
||||
assert KeyPoints(xy=xy).data == {}
|
||||
assert KeyPoints(xy=xy, data=None).data == {}
|
||||
|
||||
def test_keypoints_init_covers_all_dataclass_fields(self):
|
||||
"""Custom __init__ must assign every dataclass field — guards against drift."""
|
||||
import dataclasses
|
||||
import inspect
|
||||
|
||||
field_names = {f.name for f in dataclasses.fields(KeyPoints)}
|
||||
init_params = set(inspect.signature(KeyPoints.__init__).parameters) - {
|
||||
"self",
|
||||
"confidence",
|
||||
}
|
||||
assert field_names == init_params, (
|
||||
f"Field/init drift: {field_names.symmetric_difference(init_params)}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("key_points", "threshold", "class_agnostic", "expected_result"),
|
||||
[
|
||||
pytest.param(
|
||||
KeyPoints.empty(),
|
||||
0.5,
|
||||
True,
|
||||
KeyPoints.empty(),
|
||||
id="empty",
|
||||
),
|
||||
pytest.param(
|
||||
_create_key_points(
|
||||
xy=[[[10, 20], [30, 40]]],
|
||||
detection_confidence=[0.9],
|
||||
class_id=[0],
|
||||
),
|
||||
0.5,
|
||||
False,
|
||||
_create_key_points(
|
||||
xy=[[[10, 20], [30, 40]]],
|
||||
detection_confidence=[0.9],
|
||||
class_id=[0],
|
||||
),
|
||||
id="single-skeleton",
|
||||
),
|
||||
pytest.param(
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[10, 10], [20, 20]],
|
||||
[[500, 500], [600, 600]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.8],
|
||||
class_id=[0, 0],
|
||||
),
|
||||
0.5,
|
||||
False,
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[10, 10], [20, 20]],
|
||||
[[500, 500], [600, 600]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.8],
|
||||
class_id=[0, 0],
|
||||
),
|
||||
id="two-non-overlapping",
|
||||
),
|
||||
pytest.param(
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[100, 100], [200, 200]],
|
||||
[[150, 150], [250, 250]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.7],
|
||||
class_id=[0, 0],
|
||||
),
|
||||
0.9,
|
||||
False,
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[100, 100], [200, 200]],
|
||||
[[150, 150], [250, 250]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.7],
|
||||
class_id=[0, 0],
|
||||
),
|
||||
id="two-overlapping-below-threshold",
|
||||
),
|
||||
pytest.param(
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[100, 100], [200, 200]],
|
||||
[[110, 110], [210, 210]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.7],
|
||||
class_id=[0, 0],
|
||||
),
|
||||
0.3,
|
||||
False,
|
||||
_create_key_points(
|
||||
xy=[[[100, 100], [200, 200]]],
|
||||
detection_confidence=[0.9],
|
||||
class_id=[0],
|
||||
),
|
||||
id="two-overlapping-above-threshold",
|
||||
),
|
||||
pytest.param(
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[100, 100], [200, 200]],
|
||||
[[110, 110], [210, 210]],
|
||||
[[500, 500], [600, 600]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.7, 0.8],
|
||||
class_id=[0, 0, 0],
|
||||
),
|
||||
0.3,
|
||||
False,
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[100, 100], [200, 200]],
|
||||
[[500, 500], [600, 600]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.8],
|
||||
class_id=[0, 0],
|
||||
),
|
||||
id="three-skeletons-two-overlap",
|
||||
),
|
||||
pytest.param(
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[100, 100], [200, 200]],
|
||||
[[110, 110], [210, 210]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.7],
|
||||
class_id=[0, 1],
|
||||
),
|
||||
0.3,
|
||||
False,
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[100, 100], [200, 200]],
|
||||
[[110, 110], [210, 210]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.7],
|
||||
class_id=[0, 1],
|
||||
),
|
||||
id="class-aware-different-classes-kept",
|
||||
),
|
||||
pytest.param(
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[100, 100], [200, 200]],
|
||||
[[110, 110], [210, 210]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.7],
|
||||
class_id=[0, 1],
|
||||
),
|
||||
0.3,
|
||||
True,
|
||||
_create_key_points(
|
||||
xy=[[[100, 100], [200, 200]]],
|
||||
detection_confidence=[0.9],
|
||||
class_id=[0],
|
||||
),
|
||||
id="class-agnostic-suppresses-across-classes",
|
||||
),
|
||||
pytest.param(
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[0, 0], [100, 100], [200, 200]],
|
||||
[[0, 0], [110, 110], [210, 210]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.7],
|
||||
class_id=[0, 0],
|
||||
),
|
||||
0.3,
|
||||
False,
|
||||
_create_key_points(
|
||||
xy=[[[0, 0], [100, 100], [200, 200]]],
|
||||
detection_confidence=[0.9],
|
||||
class_id=[0],
|
||||
),
|
||||
id="zero-keypoints-excluded-from-bbox",
|
||||
),
|
||||
pytest.param(
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[100, 100], [200, 200], [0, 0], [0, 0]],
|
||||
[[0, 0], [0, 0], [110, 110], [210, 210]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.7],
|
||||
class_id=[0, 1],
|
||||
),
|
||||
0.3,
|
||||
False,
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[100, 100], [200, 200], [0, 0], [0, 0]],
|
||||
[[0, 0], [0, 0], [110, 110], [210, 210]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.7],
|
||||
class_id=[0, 1],
|
||||
),
|
||||
id="multi-skeleton-schema-class-aware",
|
||||
),
|
||||
pytest.param(
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[100, 100], [200, 200], [0, 0], [0, 0]],
|
||||
[[0, 0], [0, 0], [110, 110], [210, 210]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.7],
|
||||
class_id=[0, 1],
|
||||
),
|
||||
0.3,
|
||||
True,
|
||||
_create_key_points(
|
||||
xy=[[[100, 100], [200, 200], [0, 0], [0, 0]]],
|
||||
detection_confidence=[0.9],
|
||||
class_id=[0],
|
||||
),
|
||||
id="multi-skeleton-schema-class-agnostic",
|
||||
),
|
||||
pytest.param(
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[0, 0], [0, 0]],
|
||||
[[100, 100], [200, 200]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.8],
|
||||
class_id=[0, 0],
|
||||
),
|
||||
0.5,
|
||||
False,
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[0, 0], [0, 0]],
|
||||
[[100, 100], [200, 200]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.8],
|
||||
class_id=[0, 0],
|
||||
),
|
||||
id="all-zero-skeleton-passes-through",
|
||||
),
|
||||
pytest.param(
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[100, 100], [200, 200]],
|
||||
[[110, 110], [210, 210]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.7],
|
||||
class_id=[0, 0],
|
||||
visible=[[True, False], [False, True]],
|
||||
),
|
||||
0.3,
|
||||
False,
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[100, 100], [200, 200]],
|
||||
[[110, 110], [210, 210]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.7],
|
||||
class_id=[0, 0],
|
||||
visible=[[True, False], [False, True]],
|
||||
),
|
||||
id="visible-mask-excludes-keypoints-from-bbox",
|
||||
),
|
||||
pytest.param(
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[100, 100], [0, 0]],
|
||||
[[300, 300], [0, 0]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.8],
|
||||
class_id=[0, 0],
|
||||
),
|
||||
0.3,
|
||||
False,
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[100, 100], [0, 0]],
|
||||
[[300, 300], [0, 0]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.8],
|
||||
class_id=[0, 0],
|
||||
),
|
||||
id="single-valid-keypoint-zero-area-bbox",
|
||||
),
|
||||
pytest.param(
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[100, 100], [200, 200]],
|
||||
[[110, 110], [210, 210]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.7],
|
||||
class_id=[0, 0],
|
||||
),
|
||||
0.0,
|
||||
False,
|
||||
_create_key_points(
|
||||
xy=[[[100, 100], [200, 200]]],
|
||||
detection_confidence=[0.9],
|
||||
class_id=[0],
|
||||
),
|
||||
id="threshold-zero-suppresses-any-overlap",
|
||||
),
|
||||
pytest.param(
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[100, 100], [200, 200]],
|
||||
[[110, 110], [210, 210]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.7],
|
||||
class_id=[0, 0],
|
||||
),
|
||||
1.0,
|
||||
False,
|
||||
_create_key_points(
|
||||
xy=[
|
||||
[[100, 100], [200, 200]],
|
||||
[[110, 110], [210, 210]],
|
||||
],
|
||||
detection_confidence=[0.9, 0.7],
|
||||
class_id=[0, 0],
|
||||
),
|
||||
id="threshold-one-keeps-all",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_with_nms(key_points, threshold, class_agnostic, expected_result):
|
||||
"""NMS filters overlapping keypoint skeletons."""
|
||||
result = key_points.with_nms(threshold=threshold, class_agnostic=class_agnostic)
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("key_points", "threshold", "class_agnostic", "match"),
|
||||
[
|
||||
pytest.param(
|
||||
_create_key_points(
|
||||
xy=[[[10, 20], [30, 40]]],
|
||||
class_id=[0],
|
||||
),
|
||||
0.5,
|
||||
False,
|
||||
"detection_confidence",
|
||||
id="no-detection-confidence",
|
||||
),
|
||||
pytest.param(
|
||||
_create_key_points(
|
||||
xy=[[[10, 20], [30, 40]]],
|
||||
detection_confidence=[0.9],
|
||||
),
|
||||
0.5,
|
||||
False,
|
||||
"class_id",
|
||||
id="no-class-id-class-aware",
|
||||
),
|
||||
pytest.param(
|
||||
_create_key_points(
|
||||
xy=[[[10, 20], [30, 40]]],
|
||||
class_id=[0],
|
||||
),
|
||||
0.5,
|
||||
True,
|
||||
"detection_confidence",
|
||||
id="no-detection-confidence-class-agnostic",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_with_nms_raises(key_points, threshold, class_agnostic, match):
|
||||
"""NMS raises when required fields are missing."""
|
||||
with pytest.raises(ValueError, match=match):
|
||||
key_points.with_nms(threshold=threshold, class_agnostic=class_agnostic)
|
||||
|
||||
@@ -124,6 +124,86 @@ class TestF1Score:
|
||||
assert result.f1_50 == 0.0
|
||||
assert result.f1_75 == 0.0
|
||||
|
||||
def test_false_positives_on_background_image_counted(self):
|
||||
"""Predictions on an image with no targets must count as false positives."""
|
||||
predictions_with_gt = Detections(
|
||||
xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32),
|
||||
class_id=np.array([0]),
|
||||
confidence=np.array([0.9]),
|
||||
)
|
||||
targets_with_gt = Detections(
|
||||
xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32),
|
||||
class_id=np.array([0]),
|
||||
)
|
||||
background_predictions = Detections(
|
||||
xyxy=np.array([[20, 0, 25, 5], [40, 0, 45, 5], [60, 0, 65, 5]], np.float32),
|
||||
class_id=np.array([0, 0, 0]),
|
||||
confidence=np.array([0.9, 0.9, 0.9]),
|
||||
)
|
||||
|
||||
metric = F1Score(averaging_method=AveragingMethod.MICRO)
|
||||
result = metric.update(
|
||||
[predictions_with_gt, background_predictions],
|
||||
[targets_with_gt, Detections.empty()],
|
||||
).compute()
|
||||
|
||||
# TP=1, FP=3, FN=0 -> F1 = 2*1 / (2*1 + 3 + 0) = 0.4
|
||||
assert result.f1_50 == pytest.approx(0.4)
|
||||
assert 0 in result.matched_classes
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("method", "expected"),
|
||||
[
|
||||
pytest.param(
|
||||
AveragingMethod.MICRO, 0.5, id="micro-counts-absent-class-fps"
|
||||
),
|
||||
pytest.param(
|
||||
AveragingMethod.MACRO, 0.5, id="macro-counts-absent-class-fps"
|
||||
),
|
||||
pytest.param(
|
||||
AveragingMethod.WEIGHTED,
|
||||
1.0,
|
||||
id="weighted-absent-class-fps-not-counted-by-design",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_false_positives_of_absent_class_counted(self, method, expected):
|
||||
"""Predictions of absent class count as FPs under MICRO/MACRO; WEIGHTED
|
||||
excludes them by design (GT support=0 → weight=0, consistent with sklearn)."""
|
||||
predictions = Detections(
|
||||
xyxy=np.array(
|
||||
[[0, 0, 10, 10], [100, 0, 110, 10], [120, 0, 130, 10]], np.float32
|
||||
),
|
||||
class_id=np.array([0, 1, 1]), # class 1 never appears in the targets
|
||||
confidence=np.array([0.9, 0.8, 0.7]),
|
||||
)
|
||||
targets = Detections(
|
||||
xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32),
|
||||
class_id=np.array([0]),
|
||||
)
|
||||
|
||||
metric = F1Score(averaging_method=method)
|
||||
result = metric.update(predictions, targets).compute()
|
||||
|
||||
# MICRO: TP=1, FP=2, FN=0 -> F1 = 2/(2+2) = 0.5
|
||||
# MACRO: mean([F1_class0=1.0, F1_class1=0.0]) = 0.5
|
||||
# WEIGHTED: class_1 weight=0 -> only class 0 contributes -> 1.0
|
||||
assert result.f1_50 == pytest.approx(expected)
|
||||
|
||||
def test_false_positives_on_background_image_weighted_returns_zero(self):
|
||||
"""WEIGHTED F1 is 0.0 when all images are background (no GT anywhere)."""
|
||||
background_predictions = Detections(
|
||||
xyxy=np.array([[20, 0, 25, 5], [40, 0, 45, 5]], np.float32),
|
||||
class_id=np.array([0, 0]),
|
||||
confidence=np.array([0.9, 0.8]),
|
||||
)
|
||||
|
||||
metric = F1Score(averaging_method=AveragingMethod.WEIGHTED)
|
||||
result = metric.update(background_predictions, Detections.empty()).compute()
|
||||
|
||||
# No GT support anywhere -> class_counts.sum() == 0 -> returns 0.0
|
||||
assert result.f1_50 == 0.0
|
||||
|
||||
def test_single_class_mixed_results(
|
||||
self, predictions_confidence_ranking, targets_50_50
|
||||
):
|
||||
|
||||
@@ -115,6 +115,86 @@ class TestPrecision:
|
||||
assert result.precision_at_50 == 0.0
|
||||
assert result.precision_at_75 == 0.0
|
||||
|
||||
def test_false_positives_on_background_image_counted(self):
|
||||
"""Predictions on an image with no targets must count as false positives."""
|
||||
predictions_with_gt = Detections(
|
||||
xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32),
|
||||
class_id=np.array([0]),
|
||||
confidence=np.array([0.9]),
|
||||
)
|
||||
targets_with_gt = Detections(
|
||||
xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32),
|
||||
class_id=np.array([0]),
|
||||
)
|
||||
background_predictions = Detections(
|
||||
xyxy=np.array([[20, 0, 25, 5], [40, 0, 45, 5], [60, 0, 65, 5]], np.float32),
|
||||
class_id=np.array([0, 0, 0]),
|
||||
confidence=np.array([0.9, 0.9, 0.9]),
|
||||
)
|
||||
|
||||
metric = Precision(averaging_method=AveragingMethod.MICRO)
|
||||
result = metric.update(
|
||||
[predictions_with_gt, background_predictions],
|
||||
[targets_with_gt, Detections.empty()],
|
||||
).compute()
|
||||
|
||||
# 1 TP, 3 background FPs -> precision = 1 / (1 + 3) = 0.25
|
||||
assert result.precision_at_50 == 0.25
|
||||
assert 0 in result.matched_classes
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("method", "expected"),
|
||||
[
|
||||
pytest.param(
|
||||
AveragingMethod.MICRO, 1 / 3, id="micro-counts-absent-class-fps"
|
||||
),
|
||||
pytest.param(
|
||||
AveragingMethod.MACRO, 0.5, id="macro-counts-absent-class-fps"
|
||||
),
|
||||
pytest.param(
|
||||
AveragingMethod.WEIGHTED,
|
||||
1.0,
|
||||
id="weighted-absent-class-fps-not-counted-by-design",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_false_positives_of_absent_class_counted(self, method, expected):
|
||||
"""Predictions of absent class count as FPs under MICRO/MACRO; WEIGHTED
|
||||
excludes them by design (GT support=0 → weight=0, consistent with sklearn)."""
|
||||
predictions = Detections(
|
||||
xyxy=np.array(
|
||||
[[0, 0, 10, 10], [100, 0, 110, 10], [120, 0, 130, 10]], np.float32
|
||||
),
|
||||
class_id=np.array([0, 1, 1]), # class 1 never appears in the targets
|
||||
confidence=np.array([0.9, 0.8, 0.7]),
|
||||
)
|
||||
targets = Detections(
|
||||
xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32),
|
||||
class_id=np.array([0]),
|
||||
)
|
||||
|
||||
metric = Precision(averaging_method=method)
|
||||
result = metric.update(predictions, targets).compute()
|
||||
|
||||
# MICRO: 1 TP (class 0), 2 FPs (class 1) -> 1/3
|
||||
# MACRO: mean([precision_class0=1.0, precision_class1=0.0]) = 0.5
|
||||
# WEIGHTED: class_1 weight=0 -> only class 0 contributes -> 1.0
|
||||
assert result.precision_at_50 == pytest.approx(expected)
|
||||
|
||||
def test_false_positives_on_background_image_weighted_returns_zero(self):
|
||||
"""WEIGHTED precision is 0.0 when all images are background (no GT anywhere)."""
|
||||
background_predictions = Detections(
|
||||
xyxy=np.array([[20, 0, 25, 5], [40, 0, 45, 5]], np.float32),
|
||||
class_id=np.array([0, 0]),
|
||||
confidence=np.array([0.9, 0.8]),
|
||||
)
|
||||
|
||||
metric = Precision(averaging_method=AveragingMethod.WEIGHTED)
|
||||
result = metric.update(background_predictions, Detections.empty()).compute()
|
||||
|
||||
# No GT support anywhere -> class_counts.sum() == 0 -> returns 0.0
|
||||
assert result.precision_at_50 == 0.0
|
||||
|
||||
def test_single_class(self, predictions_confidence_ranking, targets_50_50):
|
||||
"""Test precision calculation for single class with mixed results"""
|
||||
metric = Precision()
|
||||
|
||||
@@ -12,8 +12,10 @@ resolution-markers = [
|
||||
"python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version > '3.9' and python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"python_full_version <= '3.9' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version > '3.9' and python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"(python_full_version <= '3.9' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version <= '3.9' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version <= '3.9' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -280,8 +282,10 @@ resolution-markers = [
|
||||
"(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version > '3.9' and python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"python_full_version <= '3.9' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version > '3.9' and python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"(python_full_version <= '3.9' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version <= '3.9' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version <= '3.9' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "pycparser", marker = "(python_full_version < '3.10' and implementation_name != 'PyPy' and platform_machine != 'arm64') or (implementation_name != 'PyPy' and sys_platform != 'darwin')" },
|
||||
@@ -379,8 +383,10 @@ version = "3.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version > '3.9' and python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"python_full_version <= '3.9' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version > '3.9' and python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"(python_full_version <= '3.9' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version <= '3.9' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version <= '3.9' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" }
|
||||
wheels = [
|
||||
@@ -496,8 +502,10 @@ version = "8.1.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version > '3.9' and python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"python_full_version <= '3.9' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version > '3.9' and python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"(python_full_version <= '3.9' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version <= '3.9' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version <= '3.9' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
|
||||
@@ -557,8 +565,10 @@ version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version > '3.9' and python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"python_full_version <= '3.9' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version > '3.9' and python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"(python_full_version <= '3.9' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version <= '3.9' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version <= '3.9' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
@@ -716,8 +726,10 @@ version = "7.10.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version > '3.9' and python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"python_full_version <= '3.9' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version > '3.9' and python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"(python_full_version <= '3.9' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version <= '3.9' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version <= '3.9' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" }
|
||||
wheels = [
|
||||
@@ -948,45 +960,45 @@ toml = [
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "46.0.7"
|
||||
version = "48.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and platform_python_implementation != 'PyPy') or (platform_python_implementation != 'PyPy' and sys_platform != 'darwin')" },
|
||||
{ name = "typing-extensions", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform != 'darwin')" },
|
||||
{ name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64' and platform_python_implementation != 'PyPy') or (python_full_version > '3.9' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin')" },
|
||||
{ name = "typing-extensions", marker = "(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version > '3.9' and python_full_version < '3.11' and sys_platform != 'darwin')" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/85/6379d42181bfc713094f081360fc5784d6c816b599d45e7f082502d173ce/cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225", size = 4696243, upload-time = "2026-06-09T22:32:33.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/87/c85d147b53323c7eb4d850920c8901377323c2a0ff8d79c262d4fee89aa2/cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691", size = 4713235, upload-time = "2026-06-09T22:31:40.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/58/67cbf8cf1ee7c54b439ca07bbecf8362c07afc11a3724fea70f745784add/cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242", size = 4702323, upload-time = "2026-06-09T22:31:42.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/bb/cc4b78784f97efc8c5874c2a9743708d172be6663024b34a0467885ae0c8/cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6", size = 4746137, upload-time = "2026-06-09T22:31:31.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/52/0c44de3f5267f8fbe8e835138017522a333436166e406f0db9b9e6e3033f/cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8", size = 4333867, upload-time = "2026-06-09T22:32:28.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/2e/772d7adbfa931537bc401640b7cac9976bff689bda187833e5d63b428e49/cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429", size = 4701805, upload-time = "2026-06-09T22:31:38.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/13/8b765e2e12b07c74941caadb9d1c8fdc006c4dfbf2b8f2d610519758954d/cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f", size = 4745488, upload-time = "2026-06-09T22:32:30.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/aa/48972bce55049b32a94f4907eda4d75fa385aad8a39506cc2fc72196ecf0/cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41", size = 4830256, upload-time = "2026-06-09T22:31:43.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117, upload-time = "2026-06-09T22:31:05.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/13/6476736484b94041110c8340a3eb63962fea4975baea8cb4a512adb44d4d/cryptography-48.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4", size = 4689201, upload-time = "2026-06-09T22:31:09.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/62/65a87f34d2a431546e2509b85d55e8c90df86d668f6731da64d538512ac2/cryptography-48.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991", size = 4702822, upload-time = "2026-06-09T22:32:24.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/59/810b5204b0a9b10f4b6bc06bd551a8b609803cd931806bc3b71884b225e5/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265", size = 4694875, upload-time = "2026-06-09T22:32:08.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/8c/3be6cb4da181f5bb6c19cf560c2359d60644a6b5fc5b57854e528f47b296/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411", size = 4737082, upload-time = "2026-06-09T22:32:22.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/f6/d5f60a5a1434dbfd949e227fd0065d194c7e6b6ac526b17f5c06152b8231/cryptography-48.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02", size = 4325328, upload-time = "2026-06-09T22:32:10.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/b7/ba75dd947a14b6ad907b01ae8f6b5b348cdd1b48142f0063dee9e20c1d9d/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa", size = 4694530, upload-time = "2026-06-09T22:31:53.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/04/618f4115cfc0add0838c82507aa18a346089428da8653ad38b3ff36f5cb3/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c", size = 4736660, upload-time = "2026-06-09T22:32:12.676Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/9c/06e062462a0de28a3b3911322eded4c16deb9f441b1b7575d3dc59488ab5/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72", size = 4822229, upload-time = "2026-06-09T22:31:17.062Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/be/0561971eaaee4b8a0e7d5113c536921063ab91aaf23278ac374eaf881e11/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9", size = 4966364, upload-time = "2026-06-09T22:31:32.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196, upload-time = "2026-06-09T22:31:54.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782, upload-time = "2026-06-09T22:31:45.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/c2/81a4fb4e4373c500bb526bc337ac5719dd31dd15b970b84a238168c6aa08/cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577", size = 4696618, upload-time = "2026-06-09T22:31:11.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/13/fba657f958d2af66ea959a4ba01212632089249d34af1ae48054136344d7/cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d", size = 4731873, upload-time = "2026-06-09T22:31:22.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/4c/9a964756d24a26b3e34dfcb16f961b89838786e6700b635b0d1e3adff4b6/cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6", size = 4330804, upload-time = "2026-06-09T22:31:36.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/0f/a10f3a6eb12950a10e3a874070283aa2dd5875b2bfd15fad8a3e17b3f13e/cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46", size = 4696217, upload-time = "2026-06-09T22:31:13.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/ab/8aaa12e4516ec4464033ab79b6f3b592bd5a92102467c4ace8a0d970203f/cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b", size = 4731388, upload-time = "2026-06-09T22:32:04.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/24/50027ea4dca85ec1f40688f3c24fb32ccacd520583c9592c3cc95628e6fb/cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1", size = 4824186, upload-time = "2026-06-09T22:32:18.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/41/04cb5eb17085ade6f50cc611fb657df6a0f5885350de8764ece89c050197/cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475", size = 4964539, upload-time = "2026-06-09T22:31:18.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/4a/3f43451b4f858bfceaaaffc649e6e787e8d4fb332a1d443af39ab02cc8f1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd", size = 4641226, upload-time = "2026-06-09T22:31:02.532Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/4e/855584c2c23b09e4ce2d3b9c30e983e679cd60b068c513c6bbdb91e11782/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c", size = 4668958, upload-time = "2026-06-09T22:32:06.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/3b/d35750e41d803d1e516fd6d6011f065424924da7af1748cef4cc9cb3ede1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9", size = 4640793, upload-time = "2026-06-09T22:32:26.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/aa/cdb7181fe865285e87e96825aaab239400f1de0c3bfba9bd9769b79f1a92/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92", size = 4668505, upload-time = "2026-06-09T22:31:27.534Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1112,8 +1124,10 @@ version = "3.18.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version > '3.9' and python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"python_full_version <= '3.9' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version > '3.9' and python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"(python_full_version <= '3.9' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version <= '3.9' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version <= '3.9' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" }
|
||||
wheels = [
|
||||
@@ -1317,8 +1331,10 @@ version = "2.6.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version > '3.9' and python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"python_full_version <= '3.9' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version > '3.9' and python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"(python_full_version <= '3.9' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version <= '3.9' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version <= '3.9' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" }
|
||||
wheels = [
|
||||
@@ -1419,8 +1435,10 @@ version = "8.18.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version > '3.9' and python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"python_full_version <= '3.9' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version > '3.9' and python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"(python_full_version <= '3.9' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version <= '3.9' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version <= '3.9' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
|
||||
@@ -1544,7 +1562,7 @@ name = "jaraco-classes"
|
||||
version = "3.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "more-itertools" },
|
||||
{ name = "more-itertools", marker = "(python_full_version > '3.9' and platform_machine != 'arm64') or (python_full_version > '3.9' and sys_platform != 'darwin') or (platform_machine == 'arm64' and sys_platform == 'darwin')" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" }
|
||||
wheels = [
|
||||
@@ -1556,7 +1574,7 @@ name = "jaraco-context"
|
||||
version = "6.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "backports-tarfile", marker = "python_full_version < '3.12'" },
|
||||
{ name = "backports-tarfile", marker = "(python_full_version > '3.9' and python_full_version < '3.12' and platform_machine != 'arm64') or (python_full_version > '3.9' and python_full_version < '3.12' and sys_platform != 'darwin') or (python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin')" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" }
|
||||
wheels = [
|
||||
@@ -1568,7 +1586,7 @@ name = "jaraco-functools"
|
||||
version = "4.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "more-itertools" },
|
||||
{ name = "more-itertools", marker = "(python_full_version > '3.9' and platform_machine != 'arm64') or (python_full_version > '3.9' and sys_platform != 'darwin') or (platform_machine == 'arm64' and sys_platform == 'darwin')" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/49/1c/831faaaa0f090b711c355c6d8b2abf277c72133aab472b6932b03322294c/jaraco_functools-4.2.1.tar.gz", hash = "sha256:be634abfccabce56fa3053f8c7ebe37b682683a4ee7793670ced17bab0087353", size = 19661, upload-time = "2025-06-21T19:22:03.201Z" }
|
||||
wheels = [
|
||||
@@ -1773,7 +1791,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "jupyterlab"
|
||||
version = "4.5.7"
|
||||
version = "4.5.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "async-lru" },
|
||||
@@ -1792,9 +1810,9 @@ dependencies = [
|
||||
{ name = "tornado" },
|
||||
{ name = "traitlets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2b/22/8440ec827762146e7cdecf04335bd348795899d29dc6ae82238707353a2c/jupyterlab-4.5.7.tar.gz", hash = "sha256:55a9822c4754da305f41e113452c68383e214dcf96de760146af89ce5d5117b0", size = 23992763, upload-time = "2026-04-29T16:43:51.328Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/52/a8d4895bef501ffeb6af448e8bf7079541c7772978211963aa653518c2d9/jupyterlab-4.5.9.tar.gz", hash = "sha256:dd79a073fecae7a39066ea99e4627ed6c76269ac926e95a810e1e1df6358d865", size = 23994445, upload-time = "2026-06-17T15:42:16.406Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/aa/537b8f7d80e799af19af35fb3ddfc970b951088a13c57dd9387dcfbb7f61/jupyterlab-4.5.7-py3-none-any.whl", hash = "sha256:fba4cb0e2c44a52859669d8c98b45de029d5e515f8407bf8534d2a8fc5f0964d", size = 12450123, upload-time = "2026-04-29T16:43:46.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/bb/2f9b425062416fba58f580c9b89c3b07277ccdf0a292501fedbca8ea00ea/jupyterlab-4.5.9-py3-none-any.whl", hash = "sha256:5ff0f908e8ac0afbed32b106fdef360f101c0a6654d1bf4a81e98a293ae1b336", size = 12449803, upload-time = "2026-06-17T15:42:12.18Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1851,18 +1869,49 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/f1/82ea8e783433707cafd9790099a2d19f113c22f32a31c8bb5abdc7a61dbb/jupytext-1.17.2-py3-none-any.whl", hash = "sha256:4f85dc43bb6a24b75491c5c434001ad5ef563932f68f15dd3e1c8ce12a4a426b", size = 164401, upload-time = "2025-06-01T21:31:46.319Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyring"
|
||||
version = "21.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version <= '3.9' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version <= '3.9' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version <= '3.9' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version <= '3.9' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "pywin32-ctypes", marker = "python_full_version <= '3.9' and sys_platform == 'win32'" },
|
||||
{ name = "secretstorage", version = "2.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version <= '3.9' and sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/93/b2/66cab0195a41415567ccfbf0e02c3ecc7f423729ca0c385819afbcd8e256/keyring-21.0.0.tar.gz", hash = "sha256:5f5f92327b6c7432bebc18a1b60cb3797d99b08db1f5b919b8187c37a01f1ccc", size = 53474, upload-time = "2019-12-24T20:01:38.041Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/03/4f57f7b28bfd21a0dc05aec33e012fbaa06cff56d8af63bf2f1451a12ead/keyring-21.0.0-py2.py3-none-any.whl", hash = "sha256:ad84f7fe26ab51731f089eaf1c44ebf4c5fae323653c908888a3a6212ad0bbe7", size = 31095, upload-time = "2019-12-24T20:01:35.715Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyring"
|
||||
version = "25.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12' and sys_platform == 'darwin'",
|
||||
"python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
|
||||
"python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version == '3.10.*' and sys_platform == 'darwin'",
|
||||
"python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'",
|
||||
"python_full_version > '3.9' and python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version > '3.9' and python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "importlib-metadata", marker = "python_full_version < '3.12'" },
|
||||
{ name = "jaraco-classes" },
|
||||
{ name = "jaraco-context" },
|
||||
{ name = "jaraco-functools" },
|
||||
{ name = "jeepney", marker = "sys_platform == 'linux'" },
|
||||
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
|
||||
{ name = "secretstorage", marker = "sys_platform == 'linux'" },
|
||||
{ name = "importlib-metadata", marker = "(python_full_version > '3.9' and python_full_version < '3.12' and platform_machine != 'arm64') or (python_full_version > '3.9' and python_full_version < '3.12' and sys_platform != 'darwin') or (python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin')" },
|
||||
{ name = "jaraco-classes", marker = "(python_full_version > '3.9' and platform_machine != 'arm64') or (python_full_version > '3.9' and sys_platform != 'darwin') or (platform_machine == 'arm64' and sys_platform == 'darwin')" },
|
||||
{ name = "jaraco-context", marker = "(python_full_version > '3.9' and platform_machine != 'arm64') or (python_full_version > '3.9' and sys_platform != 'darwin') or (platform_machine == 'arm64' and sys_platform == 'darwin')" },
|
||||
{ name = "jaraco-functools", marker = "(python_full_version > '3.9' and platform_machine != 'arm64') or (python_full_version > '3.9' and sys_platform != 'darwin') or (platform_machine == 'arm64' and sys_platform == 'darwin')" },
|
||||
{ name = "jeepney", marker = "python_full_version > '3.9' and sys_platform == 'linux'" },
|
||||
{ name = "pywin32-ctypes", marker = "python_full_version > '3.9' and sys_platform == 'win32'" },
|
||||
{ name = "secretstorage", version = "3.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version > '3.9' and sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" }
|
||||
wheels = [
|
||||
@@ -1875,8 +1924,10 @@ version = "1.4.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version > '3.9' and python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"python_full_version <= '3.9' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version > '3.9' and python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"(python_full_version <= '3.9' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version <= '3.9' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version <= '3.9' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286, upload-time = "2024-09-04T09:39:44.302Z" }
|
||||
wheels = [
|
||||
@@ -2170,8 +2221,10 @@ version = "3.9.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version > '3.9' and python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"python_full_version <= '3.9' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version > '3.9' and python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"(python_full_version <= '3.9' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version <= '3.9' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version <= '3.9' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "contourpy", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
@@ -2685,8 +2738,10 @@ version = "2.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version > '3.9' and python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"python_full_version <= '3.9' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version > '3.9' and python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"(python_full_version <= '3.9' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version <= '3.9' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version <= '3.9' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" }
|
||||
wheels = [
|
||||
@@ -3148,8 +3203,10 @@ version = "4.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version > '3.9' and python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"python_full_version <= '3.9' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version > '3.9' and python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"(python_full_version <= '3.9' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version <= '3.9' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version <= '3.9' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "cfgv", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
@@ -3253,6 +3310,12 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycrypto"
|
||||
version = "2.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/db/645aa9af249f059cc3a368b118de33889219e0362141e75d4eaf6f80f163/pycrypto-2.6.1.tar.gz", hash = "sha256:f2ce1e989b272cfcb677616763e0a2e7ec659effa67a88aa92b3a65528f60a3c", size = 446240, upload-time = "2014-06-20T08:10:20.813Z" }
|
||||
|
||||
[[package]]
|
||||
name = "pydeprecate"
|
||||
version = "0.9.0"
|
||||
@@ -3822,8 +3885,10 @@ version = "1.13.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version > '3.9' and python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"python_full_version <= '3.9' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version > '3.9' and python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"(python_full_version <= '3.9' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version <= '3.9' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version <= '3.9' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
@@ -3972,13 +4037,36 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/c4/231cac7a8385394ebbbb4f1ca662203e9d8c332825ab4f36ffc3ead09a42/scipy-1.16.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f56296fefca67ba605fd74d12f7bd23636267731a72cb3947963e76b8c0a25db", size = 38515076, upload-time = "2025-06-22T16:21:45.694Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secretstorage"
|
||||
version = "2.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version <= '3.9' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version <= '3.9' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version <= '3.9' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version <= '3.9' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "pycrypto", marker = "(python_full_version <= '3.9' and platform_machine != 'arm64') or (python_full_version <= '3.9' and sys_platform != 'darwin')" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/af/50f2324784ee65e2b752624144249c7710e761be437dd82815ff86d3021f/SecretStorage-2.2.1.tar.gz", hash = "sha256:d771e99ada09e39aceb75c4bb2d460895627b2260ab24c9cb790fb8aad1ecab6", size = 15972, upload-time = "2016-06-27T19:37:45.887Z" }
|
||||
|
||||
[[package]]
|
||||
name = "secretstorage"
|
||||
version = "3.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version > '3.9' and python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version > '3.9' and python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "cryptography", marker = "(python_full_version < '3.10' and platform_machine != 'arm64') or sys_platform != 'darwin'" },
|
||||
{ name = "jeepney", marker = "(python_full_version < '3.10' and platform_machine != 'arm64') or sys_platform != 'darwin'" },
|
||||
{ name = "cryptography", marker = "(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64') or (python_full_version > '3.9' and sys_platform != 'darwin')" },
|
||||
{ name = "jeepney", marker = "(python_full_version > '3.9' and python_full_version < '3.10' and platform_machine != 'arm64') or (python_full_version > '3.9' and sys_platform != 'darwin')" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739, upload-time = "2022-08-13T16:22:46.976Z" }
|
||||
wheels = [
|
||||
@@ -4055,7 +4143,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "supervision"
|
||||
version = "0.29.0rc0"
|
||||
version = "0.29.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "defusedxml" },
|
||||
@@ -4218,19 +4306,19 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "tornado"
|
||||
version = "6.5.6"
|
||||
version = "6.5.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/57/6d7303a77ae439d9189108f76c0c4fd89ee5e2cc8387bffb55232565c4ed/tornado-6.5.6.tar.gz", hash = "sha256:9a365179fe8ff6b8766f602c0f67c185d778193e9bdd828b19f0b6ed7764177d", size = 518139, upload-time = "2026-05-27T15:35:54.646Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/64/24/95ec527ad67b76d59299e5465b3935d05e4294b7e0290a3924b7487df30b/tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2", size = 519252, upload-time = "2026-06-08T17:34:51.232Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/0d/b4f481e18c5a51864e6d12b9a05ecf72919696680b747c958c3fc1f4fbae/tornado-6.5.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:65fcfaafb079435c2c19dc9e07c0f1cf0fa9051759ed0a7d0a3ba7ea7f64919c", size = 447737, upload-time = "2026-05-27T15:35:38.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/9c/5430c39fcab1144d35860f457b15e9c08b4bc7ac86764354204e983d6183/tornado-6.5.6-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:38bc01b4acacded2de63ae78023548e41ebe6fbed3ec05a796d7ae3ad893887e", size = 445899, upload-time = "2026-05-27T15:35:40.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/79/fa7e14a2f939c807a8d30619b4eb604eab219601b78792516ebe22d40cf9/tornado-6.5.6-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b942e6a137fda31ff54bf8e6e2c8d1c37f1f50583f3ed53fb840b53b9601d104", size = 448964, upload-time = "2026-05-27T15:35:42.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/71/bd67d5f5199f937dafe03a49a37989f60f600ff6fef34c79412a829d97bd/tornado-6.5.6-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8666946e70171b8c3f1fc9b7876fac492e84822c4c7f3746f4e8f8bc9ac92a79", size = 449935, upload-time = "2026-05-27T15:35:43.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/a4/c24388c9cf5b3c3a513b56a158af9f23092c9a2810d789e294310797df21/tornado-6.5.6-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c34cfab7ad6d104f052f55de06d39bbafc5885cfeb4da688803308dbcfa90b7", size = 449767, upload-time = "2026-05-27T15:35:45.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/eb/6a07ad550c3f7b37244bd0becdf293ec3d3e961783d8b720a97df50de1b2/tornado-6.5.6-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:385f35e4e22fb52551dfcda4cdc8c30c61c2c001aef5ddad99cdfe116952efd3", size = 449174, upload-time = "2026-05-27T15:35:47.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/84/3469e098dccdb6763130e06aacd786bb4363fca7b590a55c101ddf34ed30/tornado-6.5.6-cp39-abi3-win32.whl", hash = "sha256:db475f1b67b2809b10bb16264829087724ca8d24fe4ed47f7b8675cae453ef86", size = 450230, upload-time = "2026-05-27T15:35:49.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/3c/273a04e0b9dd9016f1685cca0c1c8795a71ac88a34a8c889a0b443483226/tornado-6.5.6-cp39-abi3-win_amd64.whl", hash = "sha256:6739bf1e8eb09230f1280ddbd3236f0309db70f2c551a8dbc40f62babdf82f79", size = 450667, upload-time = "2026-05-27T15:35:51.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/98/0cffe22a224f60c5fb1e3aa0b76f9da2e1ca78b0e9545e3d077c68ce60a7/tornado-6.5.6-cp39-abi3-win_arm64.whl", hash = "sha256:2543597b24a695d72338a9a77818362d72387c03ae173f1f169eadc5c91466ac", size = 449690, upload-time = "2026-05-27T15:35:52.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163", size = 448543, upload-time = "2026-06-08T17:34:38.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/4f/090b1431e5a43df696feceffc268c5383cc079ecb5f08ce58f917109aafe/tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100", size = 446707, upload-time = "2026-06-08T17:34:39.594Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972", size = 449774, upload-time = "2026-06-08T17:34:41.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/37/d434c73f4c6e014b745b9b37085f34f40c022f007efff3d7fe65991899f3/tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b", size = 450745, upload-time = "2026-06-08T17:34:42.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/2b/56b9aff361d7f1ab728a805ec7d7ea835f8807afa9f5cc690ea0e630efb9/tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92", size = 450578, upload-time = "2026-06-08T17:34:43.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/30/a7444fb23aa76860a14198fab96ac79f1866b0a6e19e26c4381b0938e50f/tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5", size = 449985, upload-time = "2026-06-08T17:34:45.326Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/42/5f0e56c01e8d9d36f4e23f367b85ae6cae0c1ecddd5e6977d8388ad27488/tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4", size = 451047, upload-time = "2026-06-08T17:34:46.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4", size = 451485, upload-time = "2026-06-08T17:34:48.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/2e/7b1c769803121b809112cf9a00681c472eae1d80e32d7ec0e0bd61d0d0e1/tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796", size = 450506, upload-time = "2026-06-08T17:34:49.702Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4284,7 +4372,8 @@ source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "id" },
|
||||
{ name = "importlib-metadata", marker = "python_full_version < '3.10'" },
|
||||
{ name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" },
|
||||
{ name = "keyring", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version <= '3.9' and platform_machine != 'arm64' and platform_machine != 'ppc64le' and platform_machine != 's390x') or (python_full_version <= '3.9' and platform_machine != 'ppc64le' and platform_machine != 's390x' and sys_platform != 'darwin')" },
|
||||
{ name = "keyring", version = "25.6.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version > '3.9' and platform_machine != 'arm64' and platform_machine != 'ppc64le' and platform_machine != 's390x') or (python_full_version > '3.9' and platform_machine != 'ppc64le' and platform_machine != 's390x' and sys_platform != 'darwin') or (platform_machine == 'arm64' and sys_platform == 'darwin')" },
|
||||
{ name = "packaging" },
|
||||
{ name = "readme-renderer" },
|
||||
{ name = "requests" },
|
||||
|
||||
Reference in New Issue
Block a user