0.30.6: Pillow-conversion, dataset-export, and line-crossing correctness fixes
supervision 0.30.6 is a patch release closing 12 library correctness bugs and landing 2 behavior refinements across datasets, annotators, key points, line-crossing counting, and the Pillow-based fallback backend, plus a new docs guide and a notebook dead-link fix. No new public API, no breaking changes. The broadest-reach fix rewrites pillow_to_cv2, the entry point every annotator except RichLabelAnnotator and several image helpers use to convert a Pillow image: it used to pass raw mode bytes straight through, so a 1-bit mask, a 16-bit depth map, an LA/PA image, or a CMYK JPEG all drew wrong or crashed. Two fixes close silent-wrong-data bugs rather than crashes: LineZone.trigger let unconfirmed tracks inflate crossing counts, and DetectionDataset.as_yolo/.as_pascal_voc silently dropped box-only objects from a mixed polygon/box COCO dataset. DetectionsSmoother no longer crashes when two tracked objects first seen on different frames carry different metadata (e.g. RF-DETR's source_image); its own docstring pipeline was broken. Every library fix ships a regression test.
✨ Spotlights / highlights
sv.pillow_to_cv2 now converts every Pillow mode the way cv2.imread would
Raw mode bytes used to pass straight through: a 1-bit mask came back as 0/1 instead of 0/255, a 16-bit depth map wrapped modulo 256 and drew as noise, an LA/PA image crashed in cvtColor on its 2-channel array, and a CMYK JPEG drew its cyan/magenta/yellow ink as red/green/blue. This function is the entry point for every annotator except RichLabelAnnotator (which stays on the Pillow path) plus crop_image, resize_image, letterbox_image, scale_image, tint_image, grayscale_image, and plot_image whenever handed a Pillow image, so the bug reached the whole drawing surface, not just one call site. A single-channel grayscale image also drew from a read-only buffer view before this fix. Annotating into it silently failed; it now hands back a writable copy.
from PIL import Image
depth_map = Image.open("depth.tif") # mode "I;16"
scene = sv.pillow_to_cv2(depth_map)
# clipped to the 16-bit range and keeps its high byte, instead of wrapping mod 256RGB, RGBA, grayscale, and palette images are unchanged.
sv.LineZone.trigger no longer lets unconfirmed tracks inflate counts
Detections with a negative tracker_id (how trackers like ByteTrackTracker mark an unconfirmed track) all keyed under one shared id. Two distinct unconfirmed objects crossing on opposite sides in the same frame read as one track oscillating, silently inflating in_count/out_count with crossings no confirmed track ever made. Confirmed tracks (tracker_id >= 0) are counted exactly as before.
sv.DetectionDataset.as_yolo / .as_pascal_voc stop dropping box-only objects
from_coco gives an all-zero mask to any annotation without its own segmentation when a sibling annotation has one, and to every annotation when force_masks=True. The exporters only ever wrote the polygon traced from a mask, so a mixed polygon/box COCO dataset silently lost its box-only objects on export, and a box-only COCO dataset loaded with force_masks=True wrote empty YOLO label files or object-less Pascal VOC files. A detection with an empty or contour-less mask now exports as its bounding box instead.
sv.DetectionsSmoother no longer crashes on a second tracked object
Each smoothed track was built on the oldest frame in its window, so two objects first seen on different frames carried different frames' metadata (exactly what the RF-DETR / inference connectors attach), and Detections.merge rejected the mismatch. class_id and data (e.g. class_name) now follow the current frame instead of lagging up to length - 1 frames behind a class change; xyxy, confidence, and oriented-box corners are still averaged as before.
Dataset splits reject an out-of-range ratio instead of silently mis-splitting
sv.DetectionDataset.split, sv.ClassificationDataset.split (both take split_ratio), and the internal train_test_split helper they call (takes train_ratio) never validated the ratio. A finite out-of-range value looked like a successful split: for 10 images, a ratio of -0.2 returned 8/2 via negative slicing, and 1.2 (or an accidental percentage like 80) returned 10/0 with no held-out data, no error either way.
train, test = dataset.split(split_ratio=80) # meant 80%
# now raises ValueError naming the problem, instead of silently returning
# every image as train and none as test0 and 1 keep their existing meanings. Not an API break: no signature changed, only previously-silent input now raises.
🔄 Migration guide
No breaking changes in this release.
No deprecations or removals landed in 0.30.6 either. All scheduled remove_in markers in the codebase target 0.31.0 or 0.32.0 and are untouched by this patch release.
Two entries change output values rather than only fixing a crash or a silently-wrong count; not API breaks, but worth checking if your code depends on the old values:
sv.KeyPoints.from_ultralytics/.from_inference/.from_detectron2:as_detections().confidencenow carries the model's own detection score, not the mean of the per-keypoint confidences.sv.DetectionsSmoother:class_idanddatafields on a smoothed track now follow the current frame instead of lagging behind a class change.
📝 Notable changes
🔧 Fixed
Annotators / key points
sv.VertexEllipseHaloAnnotatornow draws the whole halo of a key point whose covariance ellipse is not horizontal. A vertical or diagonal ellipse used to be clipped to a thin band because the fade box was sized as if the major axis always ran along the image x axis. Horizontal ellipses are drawn exactly as before. (#2634)sv.KeyPoints.from_ultralytics,.from_inferenceand.from_detectron2now keep each object's detection score asdetection_confidenceinstead of dropping it, which previously madesv.KeyPoints.with_nmsraiseValueErrorandas_detections()report the mean keypoint confidence instead of the model's own score. (#2633)sv.LabelAnnotatornow sizes the label background correctly for a label containing a blank line. Text drew each blank line at full height while the background measured it as zero, pushing the last line outside the box. Labels without blank lines are unchanged. (#2632)sv.IconAnnotatornow draws a palette icon that carries its own alpha channel (PillowPAmode, storable in TIFF) instead of raisingValueError. Palettes without alpha, and every read with OpenCV installed, are unchanged. (#2624)
Datasets
sv.DetectionDataset.as_yolo/.as_pascal_vocnow write a detection whose mask is empty or has no valid contour as its bounding box instead of silently leaving it out of the label file. (#2631)sv.DetectionDataset.from_yolonow loads a label row carrying a trailing confidence or tracker id (written by Ultralyticssave_txtwithsave_conf=Trueor tracking on) instead of raisingValueError, for both box and segmentation rows. The extra token is ignored; box and polygon geometry are unchanged. An odd-length polygon row with no extra field now loses its last value rather than raising, since coordinate parity is the only signal separating the two cases. (#2619, #2626, #2635)
Tracking
sv.DetectionsSmootherno longer raisesValueError: Conflicting metadatawhen a second tracked object, first seen on a different frame, enters the smoothing window. (#2628)sv.LineZone.triggernow ignores detections with a negativetracker_id(the value trackers such asByteTrackTrackerreport for an unconfirmed track) instead of letting them silently inflatein_count/out_count. (#2623)
Detection utils
sv.polygon_to_masknow accepts a list/tuple/array-like of[x, y]vertices (not just a NumPy array) and returns an all-zero mask for an empty or under-3-vertex polygon instead of crashing inside OpenCV/NumPy with an opaque error. A malformed polygon now raisesValueErrornaming the problem. (#2622)sv.Detections.from_sam3now keeps SAM 3 PVS contour fragments with fewer than 3 vertices instead of dropping them. Single points and 2-point edges are rasterized directly into the mask and bounding box. (#2625)
Image / IO
sv.pillow_to_cv2now converts every Pillow mode to the 8-bit arraycv2.imreadwould produce, reaching every annotator pluscrop_image,resize_image,letterbox_image,scale_image,tint_image,grayscale_image, andplot_image. (#2614)sv.CSVSinknow writes UTF-8 on every platform, preserving non-English detection labels and custom fields on Windows. (#2615)
Docs
- Dead documentation links fixed across the published notebooks (
quickstart.ipynb,annotate-video-with-detections.ipynb,underestand-visitors-with-yolo-world.ipynb). (#2630)
🌱 Changed
sv.DetectionDataset.split,sv.ClassificationDataset.split, and the internaltrain_test_splithelper they call now reject a ratio outside[0, 1](NaNand±infincluded) with aValueErrornaming the problem, instead of silently mis-splitting or dropping the held-out set. (#2611)sv.tint_imageandsv.grayscale_imagenow accept a single-channel(H, W)array or grayscaleImage, assv.letterbox_imagealready did. (#2614)- New docs guide: "Evaluate targets from a PyTorch DataLoader" in
docs/metrics/mean_average_precision.md, cross-linked fromdocs/how_to/benchmark_a_model.md.Detections.from_transformersgained a docstring note that it expects post-processed predictions, not raw DataLoader targets. (#2620)
🏆 Contributors
- Mohammad Hijjawi (@MohammadHijjawi97, LinkedIn) — the
DetectionsSmootherconflicting-metadata crash, theKeyPointsdetection-score fix, the YOLO/Pascal VOC empty-mask export fix, theVertexEllipseHaloAnnotatorrotated-ellipse fix, theLabelAnnotatorblank-line background fix, and thepolygon_to_maskcrash fix - Raghav Rathi (@raghav-rathi, LinkedIn) — the
pillow_to_cv2rewrite for every Pillow mode - Zachary Alexander (@zachsplat) — the
LineZoneunconfirmed-track fix - NIKHIL (@Nikhi00718) — the YOLO segmentation-label trailing-value fix
- Manoj Kumar Thapa (@iammanoj807, LinkedIn) — the palette-with-alpha
IconAnnotatorfix - kevin (@kevin9327) — the YOLO box-label trailing-value fix
- Andrew Barnes (@Bortlesboat, LinkedIn) — the CSV Unicode export fix
- 4rch1e (@archie0732) — the dataset split-ratio validation
- Salman Ansari (@salmanwnl44, LinkedIn) — the PyTorch DataLoader-targets docs guide
- Pratik Gandhi (@pratikgx, LinkedIn) — dead-link fixes in the published notebooks
- Jirka Borovec (@Borda, LinkedIn) — the
from_sam3degenerate-fragment fix
Full changelog: 0.30.5...0.30.6