github roboflow/supervision 0.31.4
supervision-0.31.4

4 hours ago

v0.31.4 — Stricter dataset export, COCO-exact recall buckets, and a closer NumPy fallback

Fourteen bug fixes: stricter dataset export, COCO-exact recall buckets, sturdier VLM parsing, and a NumPy fallback closer to OpenCV.

  • CreateML and Pascal VOC export raise a clear ValueError for a class_id outside classes.
  • DetectionDataset.merge keeps annotations that have no class IDs instead of raising.
  • MeanAverageRecall counts boundary-area objects in both COCO size buckets, like MeanAveragePrecision.
  • Qwen and PaliGemma parsing keep valid boxes and class labels beside malformed ones.
  • Without OpenCV, image reads, gray conversion, drawing colors and keyword parameters match OpenCV.

Drop-in upgrade; see the Migration guide if you export out-of-range class IDs or compare mAR values or fallback pixels against stored references.

✨ Spotlights / highlights

Dataset export rejects out-of-range class IDs

detections_to_createml_annotations and detections_to_pascal_voc now raise ValueError for a class_id outside classes, as detections_to_labelme_shapes already does. A negative id was exported under the last class name, and an id past the end raised IndexError. (#2859)

import numpy as np
import supervision as sv
from supervision.dataset.formats.createml import detections_to_createml_annotations

detections = sv.Detections(
    xyxy=np.array([[10.0, 10.0, 50.0, 50.0]]), class_id=np.array([-1])
)
detections_to_createml_annotations(detections, classes=["cat", "dog"])
# ValueError: class_id -1 at detection index 0 is out of range for classes list of length 2.
# 0.31.3: exported with the label "dog"

DetectionDataset.merge keeps annotations without class IDs

Merging datasets used to raise ValueError when some annotations had no class_id. They are now kept as they are while labeled annotations are remapped. (#2858)

import numpy as np
import supervision as sv

labeled = sv.DetectionDataset(
    classes=["a"],
    images=["x.jpg"],
    annotations={
        "x.jpg": sv.Detections(xyxy=np.array([[0.0, 0, 5, 5]]), class_id=np.array([0]))
    },
)
unlabeled = sv.DetectionDataset(
    classes=["b"],
    images=["y.jpg"],
    annotations={"y.jpg": sv.Detections(xyxy=np.array([[0.0, 0, 5, 5]]))},
)
merged = sv.DetectionDataset.merge([labeled, unlabeled])
merged.classes
# ['a', 'b']  (0.31.3: ValueError)

MeanAverageRecall size buckets match COCO

An object whose area is exactly 32² or 96² now counts in both neighbouring size buckets, the inclusive ranges pycocotools and MeanAveragePrecision use. It used to land in only one, so mAR and mAP disagreed on the small, medium and large results. (#2848)

import numpy as np
import supervision as sv
from supervision.metrics import MeanAverageRecall

box = np.array([[0.0, 0.0, 32.0, 32.0]])  # area is exactly 32²
target = sv.Detections(xyxy=box, class_id=np.array([0]))
prediction = sv.Detections(xyxy=box, class_id=np.array([0]), confidence=np.array([0.9]))

result = MeanAverageRecall().update([prediction], [target]).compute()
result.small_objects.mAR_at_100, result.medium_objects.mAR_at_100
# (1.0, 1.0)  (0.31.3: (0.0, 1.0))

🔄 Migration guide

No public symbol was removed or renamed, and nothing is deprecated. A few fixes change what existing code returns or raises:

  • New error: CreateML and Pascal VOC export raise ValueError for a class_id outside classes. Make sure every exported id indexes into your classes list; filter or remap ids such as -1 first. (#2859)
  • Different numbers: MeanAverageRecall small, medium and large results can change for datasets with objects of area exactly 32² or 96². (#2848)
  • Different pixels without OpenCV: drawing colors saturate instead of wrapping, uint16 gray conversion and 16-bit grayscale reads follow OpenCV, and IMREAD_GRAYSCALE returns a two-dimensional uint8 image. Regenerate stored reference images from 0.31.4. (#2854, #2855, #2840, #2841)

📝 Notable changes

🔧 Fixed

Datasets

  • detections_to_createml_annotations and detections_to_pascal_voc raise ValueError for a class_id outside classes; a negative id was exported under the last class name and a too-large id raised IndexError. (#2859)
  • DetectionDataset.merge raised ValueError when some annotations had no class IDs; it now keeps them and remaps the labeled ones. (#2858)

Metrics

  • MeanAverageRecall counts objects of area exactly 32² or 96² in both neighbouring COCO size buckets, as MeanAveragePrecision does. (#2848)

Detections, VLM and slicing

  • Detections.from_ultralytics passed an empty crop to image resizing when restoring masks from thin letterboxed images; it now keeps at least one content pixel per axis. (#2857)
  • scale_boxes keeps representable corners when opposite-sign float coordinates overflow the intermediate box width, including float16 boxes from -40000 to 40000 scaled by 0.5. (#2853)
  • Qwen parsing skips malformed or non-finite boxes and keeps the valid detections from the same response. (#2851)
  • PaliGemma parsing keeps punctuation in class labels such as person (left) and cat/dog, so class filtering no longer drops them. (#2852)
  • Detections.from_vlm returns an integer class_id when a Gemini classes filter removes every detection and when a DeepSeek-VL2 response has none; it was a float64 array. (#2839)
  • InferenceSlicer(compact_masks=True) keeps the dense masks on callback-owned detections, so callbacks that reuse their output buffers no longer raise AttributeError after the first tile. (#2847)

NumPy fallback without OpenCV

  • Image reads honor IMREAD_GRAYSCALE for files and bytes, including sv.load_image_from_url(..., cv_imread_flags=0), and keep the high byte of 16-bit grayscale PNG and TIFF images. (#2841, #2840)
  • cvtColor(..., COLOR_BGR2GRAY) converts uint16 images with OpenCV's fixed-point weights. (#2855)
  • Drawing operations and copyMakeBorder saturate colors and round floating-point inputs for integer images; out-of-range border constants no longer raise OverflowError under NumPy 2.x. (#2854)
  • blur, approxPolyDP and intersectConvexConvex take OpenCV's parameter names, so keyword calls work, and intersectConvexConvex(handleNested=False) returns no intersection for strictly nested polygons. (#2856)

🏆 Contributors

  • kevin (@kevin9327) — made MeanAverageRecall size buckets match COCO, converted uint16 images to gray like OpenCV, named the blur, approxPolyDP and intersectConvexConvex fallback parameters, and rejected out-of-range class IDs in dataset export.
  • NIKHIL (@Nikhi00718, LinkedIn) — kept valid Qwen boxes beside malformed ones, preserved punctuation in PaliGemma labels, and fixed scale_boxes corner overflow.
  • A Aswanth Raj (@aswanth-07, LinkedIn) — restored masks from thin Ultralytics images and kept slicer callback masks reusable.
  • LEE, WEI-HONG (@AuricTW) — honored grayscale read flags and kept the high byte of 16-bit image reads.
  • Vuln3rat0r (@Vuln3rat0r) — made fallback drawing colors saturate and round like OpenCV.
  • Devulapalli Naga Sri Vaishnavi (@Vaishnavi220506, LinkedIn) — made DetectionDataset.merge keep annotations without class IDs.
  • Mohammad Hijjawi (@MohammadHijjawi97, LinkedIn) — kept class_id integral when Gemini or DeepSeek-VL2 return nothing.
  • Zhenhuan Wang (@NaCr05) — enabled strict typing for the video tests.
  • Oke Iyanuoluwa Enoch (@Iyanuoluwa007, LinkedIn) — type checked the metrics tests.
  • Jirka Borovec (@Borda, LinkedIn) — prepared the release.

Automated contributions: Dependabot, Codex, pre-commit.ci


Full changelog: 0.31.3...0.31.4
Co-authored with /oss:release.

Don't miss a new supervision release

NewReleases is sending notifications on new releases.