diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 7504d094..9397fc27 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -252,6 +252,18 @@ Type hints are required on all new code. mypy is enforced by the pre-commit hook
- Prefer vectorized operations over Python loops in hot paths.
- Lazy-import heavy framework dependencies (`torch`, `transformers`, `ultralytics`) inside the function that needs them — never at module top level.
+### Deprecation policy
+
+**Minimum window**: deprecated APIs must remain for at least **3 minor releases** before removal. Example: deprecated in `0.29.0` → removed in `0.32.0`.
+
+Use the appropriate mechanism depending on what is being deprecated:
+
+- **Module-level alias**: `supervision.utils.internal.warn_deprecated` in the deprecated module's `__init__.py`
+- **Renamed parameter**: `supervision.utils.internal.deprecated_parameter` decorator
+- **Public function, method, or class**: `@deprecated` from `pydeprecate`
+
+Always specify both the deprecation version and the planned removal version in the message or decorator arguments.
+
### Deprecated module aliases
`supervision.keypoint` is deprecated since `0.27.0` and will be removed in `0.30.0`. Always import from `supervision.key_points`:
diff --git a/AGENTS.md b/AGENTS.md
index a666fcb7..7ce1a7d2 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -75,6 +75,8 @@ For branching, commit, code style, and API design conventions see [CONTRIBUTING.
## 5. Deprecating APIs
+**Minimum window**: deprecated APIs must remain for at least **3 minor releases** before removal. Example: deprecated in `0.29.0` → removed in `0.32.0`.
+
- Module-level: `supervision.utils.internal.warn_deprecated` in the deprecated module's own `__init__.py`
- Parameter renamed (old→new): `supervision.utils.internal.deprecated_parameter` decorator
- Public function, method, or class: `@deprecated` from `pydeprecate`
@@ -82,7 +84,7 @@ For branching, commit, code style, and API design conventions see [CONTRIBUTING.
Always name the version introduced and the removal version:
```python
-warn_deprecated("'foo' deprecated in `0.27.0`, removed in `0.30.0`. Use 'bar'.")
+warn_deprecated("'foo' deprecated in `0.29.0`, removed in `0.32.0`. Use 'bar'.")
```
---
diff --git a/docs/changelog.md b/docs/changelog.md
index d2aa0779..428204c6 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,27 +1,61 @@
---
description: "Full version history of the supervision Python library — release notes, breaking changes, new features, and deprecations for every version."
-date_modified: 2026-06-09
+date_modified: 2026-06-15
---
# Changelog
### UnReleased
-- Fixed [#2306](https://github.com/roboflow/supervision/pull/2306): [`sv.Detections.area`](https://supervision.roboflow.com/latest/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/latest/detection/annotators/#supervision.annotators.core.MaskAnnotator) and [`HaloAnnotator`](https://supervision.roboflow.com/latest/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.
+### 0.29.0 Jun 15, 2026
-- Fixed [#2289](https://github.com/roboflow/supervision/pull/2289): [`DetectionDataset.as_yolo`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.as_yolo) now accepts `is_obb=True` to round-trip oriented bounding box datasets without losing the rotation. Previously the save path had no OBB option and silently wrote 5-token axis-aligned bbox lines for datasets loaded with `from_yolo(..., is_obb=True)`, dropping the four corners stored in `detections.data["xyxyxyxy"]`. Re-loading those saved files with `is_obb=True` then crashed the validator. Mirrors the existing `from_yolo(..., is_obb=True)` load path.
+- 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).
-- Deprecated: public `validate_*` helper functions now delegate to private `_validate_*` implementations via `pydeprecate` shims. Existing imports continue to work with a `FutureWarning`; internal Supervision code uses the private helpers directly.
+- 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.
-- Fixed [#2282](https://github.com/roboflow/supervision/pull/2282): [`oriented_box_iou_batch`](https://supervision.roboflow.com/latest/detection/utils/iou_and_nms/) now allocates the rasterization canvas with correct dimensions. Previously x-extents were mapped to canvas height and y-extents to width, causing IoU to be computed on a transposed canvas for non-square oriented bounding boxes. Metrics using OBB IoU (Precision, Recall, F1Score, MeanAverageRecall with `metric_target=MetricTarget.ORIENTED_BOUNDING_BOXES`) now return correct scores.
+- 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.
-- Fixed [#2276](https://github.com/roboflow/supervision/pull/2276): [`DetectionDataset.as_coco`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.as_coco) now emits 1-indexed `category_id` and `categories[].id` values as required by the COCO spec, fixing CVAT import failures reported in [#1181](https://github.com/roboflow/supervision/issues/1181). Files generated by earlier supervision versions used 0-indexed category ids; they can still be loaded correctly by `from_coco` because the read path maps categories by name, not by id value.
+- 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 [#2267](https://github.com/roboflow/supervision/pull/2267): [`DetectionDataset.as_coco`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.as_coco) and `save_coco_annotations` now accept `starting_image_id` and `starting_annotation_id` parameters (both default to `1`, preserving existing behavior) and return a `(next_image_id, next_annotation_id)` tuple. Feed the returned values into the next split's call to produce globally unique COCO ids across train/valid/test exports. Fixes id collisions reported in [#768](https://github.com/roboflow/supervision/issues/768). **Note**: the return type changes from `None` to `tuple[int, int]` — callers that assert `result is None` must be updated.
+- 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).
-- Changed [#2312](https://github.com/roboflow/supervision/pull/2312): [`sv.Detections.with_nmm`](https://supervision.roboflow.com/latest/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, rather than keeping the highest-confidence detection's original OBB geometry. The winner is the highest-confidence detection in the group; its principal axis direction is used so the result is never wider than the union. The axis-aligned `xyxy` field is updated to the bounding box of the merged OBB. For zero-rotation OBBs this equals the axis-aligned union (consistent with AABB NMM); for rotated OBBs the merged geometry inherits the winner's rotation angle. Detections without `data["xyxyxyxy"]` and single-detection groups are unaffected.
+- 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.
-- Changed [#2303](https://github.com/roboflow/supervision/pull/2303): [`sv.Detections.with_nms`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.with_nms) and [`sv.Detections.with_nmm`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.with_nmm) now use oriented-box IoU when `data["xyxyxyxy"]` coordinates are present, fixing silent suppression of crossed/rotated OBB detections (reported in [#1679](https://github.com/roboflow/supervision/issues/1679)). Previously, both methods always used axis-aligned bounding-box IoU even for OBB inputs, causing the X-pattern AABB IoU ≈ 1.0 to suppress valid detections at any practical threshold. Callers relying on the old AABB behaviour (e.g. to force AABB suppression on OBB data) should remove `data["xyxyxyxy"]` before calling these methods. Threshold values calibrated against AABB IoU may need adjustment when OBB IoU is substantially different.
+- 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.VertexAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.VertexAnnotator) now respect the `visible` mask. Invisible keypoints and their edges are skipped during rendering.
+
+- 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 [#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 [#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.
+
+- 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 [#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 [#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 [#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.
+
+- Fixed [#2297](https://github.com/roboflow/supervision/pull/2297): Letterbox utility now supports grayscale images.
+
+- Fixed [#2298](https://github.com/roboflow/supervision/pull/2298): File extension filters now normalize casing (e.g. `.JPG` matches `.jpg`).
+
+- 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 Apr 30, 2026
diff --git a/pyproject.toml b/pyproject.toml
index f4563279..b942c21a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ requires = [ "setuptools>=61" ]
[project]
name = "supervision"
-version = "0.29.0.rc0"
+version = "0.29.0"
description = "A set of easy-to-use utils that will come in handy in any Computer Vision project"
readme = "README.md"
keywords = [
diff --git a/src/supervision/annotators/core.py b/src/supervision/annotators/core.py
index d63e4501..559528cc 100644
--- a/src/supervision/annotators/core.py
+++ b/src/supervision/annotators/core.py
@@ -2746,7 +2746,7 @@ class PercentageBarAnnotator(BaseAnnotator):
@deprecated( # type: ignore[untyped-decorator]
target=_validate_custom_values.__func__, # type: ignore[attr-defined]
deprecated_in="0.29.0",
- remove_in="0.31.0",
+ remove_in="0.32.0",
)
def validate_custom_values(
custom_values: npt.NDArray[np.float64] | list[float] | None,
diff --git a/src/supervision/annotators/utils.py b/src/supervision/annotators/utils.py
index a4eef0b4..caa53757 100644
--- a/src/supervision/annotators/utils.py
+++ b/src/supervision/annotators/utils.py
@@ -226,7 +226,7 @@ def _validate_labels(labels: list[str] | None, detections: Detections) -> None:
@deprecated( # type: ignore[untyped-decorator]
target=_validate_labels,
deprecated_in="0.29.0",
- remove_in="0.31.0",
+ remove_in="0.32.0",
)
def validate_labels(labels: list[str] | None, detections: Detections) -> None:
void(labels, detections)
diff --git a/src/supervision/detection/core.py b/src/supervision/detection/core.py
index 4f742ab6..4f38f1e5 100644
--- a/src/supervision/detection/core.py
+++ b/src/supervision/detection/core.py
@@ -2761,7 +2761,7 @@ def _merge_detection_group(detections: list[Detections]) -> Detections:
)
-@deprecated(deprecated_in="0.29.0", remove_in="0.34.0") # type: ignore[untyped-decorator]
+@deprecated(deprecated_in="0.29.0", remove_in="0.32.0") # type: ignore[untyped-decorator]
def merge_inner_detection_object_pair(
detections_1: Detections, detections_2: Detections
) -> Detections:
@@ -2854,7 +2854,7 @@ def merge_inner_detection_object_pair(
)
-@deprecated(deprecated_in="0.29.0", remove_in="0.34.0") # type: ignore[untyped-decorator]
+@deprecated(deprecated_in="0.29.0", remove_in="0.32.0") # type: ignore[untyped-decorator]
def merge_inner_detections_objects(
detections: list[Detections],
threshold: float = 0.5,
@@ -2882,7 +2882,7 @@ def merge_inner_detections_objects(
return detections_1
-@deprecated(deprecated_in="0.29.0", remove_in="0.34.0") # type: ignore[untyped-decorator]
+@deprecated(deprecated_in="0.29.0", remove_in="0.32.0") # type: ignore[untyped-decorator]
def merge_inner_detections_objects_without_iou(
detections: list[Detections],
) -> Detections:
@@ -2924,7 +2924,7 @@ def _validate_fields_both_defined_or_none(
@deprecated( # type: ignore[untyped-decorator]
target=_validate_fields_both_defined_or_none,
deprecated_in="0.29.0",
- remove_in="0.31.0",
+ remove_in="0.32.0",
)
def validate_fields_both_defined_or_none(
detections_1: Detections, detections_2: Detections
diff --git a/src/supervision/detection/vlm.py b/src/supervision/detection/vlm.py
index 2b6b7bb2..0b0bc830 100644
--- a/src/supervision/detection/vlm.py
+++ b/src/supervision/detection/vlm.py
@@ -206,7 +206,7 @@ def _validate_vlm_parameters(
@deprecated( # type: ignore[untyped-decorator]
target=_validate_vlm_parameters,
deprecated_in="0.29.0",
- remove_in="0.31.0",
+ remove_in="0.32.0",
)
def validate_vlm_parameters(vlm: VLM | str, result: Any, kwargs: dict[str, Any]) -> VLM:
return void(vlm, result, kwargs) # type: ignore[no-any-return]
diff --git a/src/supervision/metrics/detection.py b/src/supervision/metrics/detection.py
index d20210c7..409ac34a 100644
--- a/src/supervision/metrics/detection.py
+++ b/src/supervision/metrics/detection.py
@@ -187,7 +187,7 @@ def _validate_input_tensors(
@deprecated( # type: ignore[untyped-decorator]
target=_validate_input_tensors,
deprecated_in="0.29.0",
- remove_in="0.31.0",
+ remove_in="0.32.0",
)
def validate_input_tensors(
predictions: list[npt.NDArray[np.float32]],
diff --git a/src/supervision/validators/__init__.py b/src/supervision/validators/__init__.py
index be8c5870..41420cb0 100644
--- a/src/supervision/validators/__init__.py
+++ b/src/supervision/validators/__init__.py
@@ -182,7 +182,7 @@ def _validate_tracker_id(tracker_id: Any, n: int) -> None:
@deprecated( # type: ignore[untyped-decorator]
target=_validate_tracker_id,
deprecated_in="0.29.0",
- remove_in="0.31.0",
+ remove_in="0.32.0",
)
def validate_tracker_id(tracker_id: Any, n: int) -> None:
void(tracker_id, n)
@@ -207,7 +207,7 @@ def _validate_data(data: dict[str, Any], n: int) -> None:
@deprecated( # type: ignore[untyped-decorator]
target=_validate_data,
deprecated_in="0.29.0",
- remove_in="0.31.0",
+ remove_in="0.32.0",
)
def validate_data(data: dict[str, Any], n: int) -> None:
void(data, n)
@@ -227,7 +227,7 @@ def _validate_xy(xy: Any, n: int, m: int) -> None:
@deprecated( # type: ignore[untyped-decorator]
target=_validate_xy,
deprecated_in="0.29.0",
- remove_in="0.31.0",
+ remove_in="0.32.0",
)
def validate_xy(xy: Any, n: int, m: int) -> None:
void(xy, n, m)
@@ -276,7 +276,7 @@ def _validate_detections_fields(
@deprecated( # type: ignore[untyped-decorator]
target=_validate_detections_fields,
deprecated_in="0.29.0",
- remove_in="0.31.0",
+ remove_in="0.32.0",
)
def validate_detections_fields(
xyxy: Any,
@@ -312,7 +312,7 @@ def _validate_keypoints_fields(
@deprecated( # type: ignore[untyped-decorator]
target=_validate_keypoints_fields,
deprecated_in="0.29.0",
- remove_in="0.31.0",
+ remove_in="0.32.0",
)
def validate_key_points_fields(
xy: Any, class_id: Any, confidence: Any, data: dict[str, Any]
@@ -357,7 +357,7 @@ def _validate_resolution(resolution: Any) -> tuple[int, int]:
@deprecated( # type: ignore[untyped-decorator]
target=_validate_resolution,
deprecated_in="0.29.0",
- remove_in="0.31.0",
+ remove_in="0.32.0",
)
def validate_resolution(resolution: Any) -> tuple[int, int]:
return void(resolution) # type: ignore[no-any-return]