github roboflow/supervision 0.30.5
supervision-0.30.5

5 hours ago

0.30.5: Tracking, metrics, and image-drawing correctness fixes

supervision 0.30.5 is a patch release closing 11 correctness and crash bugs across line-crossing tracking, mAR@K scoring, model connectors, key points, and image drawing/loading — no new public API of note, no breaking changes. The most consequential fixes are silent, not crashes: LineZone.trigger miscounted crossings by one per flicker whenever a tracker briefly touched the far side of the line, and MeanAverageRecall scored mAR@K against the wrong predictions when a lower-ranked one fit a target more tightly than one within the top K. The remaining fixes close a hard crash in InferenceSlicer on conflicting slice metadata (plus two related Detections.__eq__ bugs), bring the OpenCV-free fallback backend to parity with OpenCV for rotated videos, CMYK images, transparent/1-bit PNGs, and default JPEG/WebP write quality, fix two drawing bugs that reproduce with OpenCV installed (16-bit draw_image, grayscale IconAnnotator icons), and fix two isolated bugs in KeyPoints.as_detections and plot_images_grid. Every fix ships a regression test.

✨ Spotlights / highlights

sv.LineZone.trigger no longer counts flicker as a crossing

A crossing was confirmed whenever the oldest entry of a minimum_crossing_threshold + 1 frame history differed from every later entry, which never verified the tracker had actually settled on the side it supposedly came from. With minimum_crossing_threshold=2 the side sequence A,A,A,B,A,A,A counted a crossing into A — the side the object never left — so counts drifted by one per flicker, in the wrong direction; three separated flickers gave in_count=3 instead of 0.

line_zone = sv.LineZone(
    start=sv.Point(0, 0), end=sv.Point(0, 100), minimum_crossing_threshold=2
)
# a tracker that flickers to the far side for one frame and back
# no longer registers a phantom crossing; only a sustained crossing counts

Crossings are now measured against the last side a tracker was confirmed on, not against the oldest history entry. Sustained crossings and minimum_crossing_threshold=1 (the default) are unchanged.

sv.metrics.MeanAverageRecall now scores mAR@K from each image's own top K

The matcher pairs predictions to targets by highest IoU, not confidence, so a prediction ranked below K could take a target away from one ranked within it. Adding a low-confidence duplicate that fit a target more tightly than the top prediction actually lowered mAR@1 — one target with a top prediction at IoU 0.71 scored mAR@1 0.5 alone but 0.0 once a second, unrelated prediction at IoU 1.0 and confidence 0.1 was added. Each detection limit now matches only its own top K predictions.

sv.InferenceSlicer no longer crashes when slices disagree on metadata

RF-DETR and inference-package connectors attach a source_image array per slice; any mismatch across slices previously crashed the merge outright. Any metadata key that isn't identical across every slice is now dropped with a one-time warning naming the dropped keys; source_image is reattached afterward as the full input image.

slicer = sv.InferenceSlicer(callback=callback)
detections = slicer(image)  # metadata.source_image is the full input image again

Fixing this also closed two Detections.__eq__ bugs: NaN in a float-array metadata value now compares equal to itself, and a list-valued value against an ndarray-valued one now returns False instead of raising ValueError.

Rotated videos read upright without OpenCV

OpenCV's FFmpeg backend applies a video's display-rotation matrix to decoded frames and to the reported width/height; the OpenCV-free fallback ignored it, so a portrait phone video came back sideways with its width/height swapped.

sv.KeyPoints.as_detections drops invisible key points from the derived box

as_detections stretched each box over every key point that wasn't [0, 0] or non-finite, ignoring visible — unlike with_nms, which already respected it. A pose whose low-confidence joints were predicted off-frame no longer produces an inflated box.

🔄 Migration guide

No breaking changes in this release.

No deprecations or removals landed in 0.30.5 either. All scheduled remove_in markers in the codebase target 0.31.0 and are untouched by this patch release.

sv.config.SOURCE_IMAGE_METADATA_FIELD is a new public constant (added alongside the InferenceSlicer fix) naming the Detections.metadata key RF-DETR/inference-package connectors use for the source image — additive, no existing code needs to change.

📝 Notable changes

🔧 Fixed

Tracking / metrics

  • sv.LineZone.trigger no longer counts a spurious crossing in the opposite direction when a tracker flickers to the far side of the line for fewer than minimum_crossing_threshold frames. Crossings are now measured against the last side a tracker was confirmed on, and once a tracker has a confirmed side, a new one replaces it only after being held for the full threshold. Sustained crossings, minimum_crossing_threshold=1 (the default), and per-tracker isolation are unchanged. (#2600)
  • sv.metrics.MeanAverageRecall now scores mAR@K from each image's K most confident predictions alone, instead of matching every prediction first and only then keeping the top K by confidence. mAR@1 and mAR@10 now equal the recall of the top 1 and top 10 predictions per image, as documented; mAR@100 changes only for images with more than 100 predictions. (#2604)

Model connectors

  • sv.InferenceSlicer no longer raises when slices disagree on metadata (e.g. a source_image NumPy array attached per-slice by RF-DETR/inference-package connectors), which previously crashed the merge outright. Any metadata key that isn't identical across every slice is now dropped from the merged result, with a SupervisionWarnings warning naming the dropped keys, emitted once per slicer instance. source_image is a special case: removed from each slice before the lenient merge, then reattached afterward as the full input image. Fixing this also closed two related Detections.__eq__ bugs: metadata holding NaN in a float array now compares equal to itself instead of always reading unequal, and comparing a list-valued metadata value against an ndarray-valued one now returns False instead of raising ValueError. (#2596)

OpenCV-free fallback backend

  • sv.VideoInfo.from_video_path, sv.get_video_frames_generator and sv.process_video now turn a rotated video upright when OpenCV is not installed, as they already do with OpenCV. The fallback now applies quarter and half turns, the same angles OpenCV applies, using the container's display-rotation matrix. Videos without a display rotation, and every read with OpenCV installed, are unchanged. (#2601)
  • sv.IconAnnotator and sv.draw_image now draw CMYK JPEG and TIFF images in their colors when OpenCV is not installed, instead of returning the four ink channels as if they were blue/green/red/alpha. Other images, and every read with OpenCV installed, are unchanged. (#2602)
  • sv.ImageSink and the dataset exports that encode in-memory images now write JPEG and WebP files at OpenCV's default quality (JPEG 95, lossless WebP) when OpenCV is not installed, instead of Pillow's defaults (JPEG 75, lossy WebP). The fallback's in-memory encoder also now accepts .jpe, .tif, .jp2, and .pgm, which it previously rejected even though file writes already accepted them. PNG and every write with OpenCV installed are unchanged. (#2592)
  • sv.IconAnnotator and sv.draw_image now keep the transparency of grayscale PNGs with alpha, RGB PNGs with a transparent color, and 1-bit PNGs, when OpenCV is not installed — the fallback previously returned Pillow's pixel layout for IMREAD_UNCHANGED rather than OpenCV's, causing shape-mismatch ValueErrors or black-drawn transparent pixels. Other images, and every read with OpenCV installed, are unchanged. (#2588)

Annotators / drawing

  • sv.draw_image now scales a 16-bit PNG down to 8 bits on load, instead of blending it into an 8-bit scene and clipping channel values above 255 — this reproduces with OpenCV installed, since IMREAD_UNCHANGED keeps a 16-bit PNG at 16 bits. Eight-bit images, and images passed as arrays, are drawn as before. (#2603)
  • sv.IconAnnotator now draws grayscale PNG icons instead of failing with IndexError — the overlay only handled BGR and BGRA arrays, and cv2.imread(..., IMREAD_UNCHANGED) returns a 2-D array for a grayscale PNG without alpha, with or without OpenCV installed. A grayscale icon is now expanded to BGR on load, and a 16-bit icon is scaled to 8 bits, as cv2.imread does by default. Eight-bit color icons are drawn as before. (#2591)

Key points / utils

  • sv.KeyPoints.as_detections now leaves key points marked not visible out of the box it derives for each skeleton, as sv.KeyPoints.with_nms already does. A skeleton with no visible key point is now dropped, as one with only missing key points already was. Key points without visible convert as before. (#2605)
  • sv.plot_images_grid now plots a single image in a grid_size=(1, 1) grid, instead of failing with AttributeErrorplt.subplots returns a lone Axes rather than an array for a 1x1 grid. Grids with more than one cell plot as before. (#2590)

🏆 Contributors

  • kevin (@kevin9327) — the OpenCV-free fallback backend fixes for rotated video, CMYK, transparent/1-bit images and default JPEG/WebP quality, the 16-bit draw_image and grayscale IconAnnotator drawing fixes, plus the MeanAverageRecall/plot_images_grid/KeyPoints.as_detections fixes
  • Souptik Chakraborty (@Souptik96, LinkedIn) — the LineZone flicker-crossing fix
  • RAJVARDHAN (@vardhans07) — the InferenceSlicer conflicting-metadata fix and the two Detections.__eq__ bugs it uncovered

Full changelog: 0.30.4...0.30.5

Don't miss a new supervision release

NewReleases is sending notifications on new releases.