v0.31.5: Line counts through occlusion, the exact clipper for oriented boxes, and right-to-left labels
Right-to-left labels, line counting that survives occlusion, and a batch of edge-case fixes across annotators, exports and the NumPy fallback.
- LineZone counts objects that are hidden for a few frames while they cross the line.
- Arabic and Hebrew labels render correctly with the new
rtlextra. - Oriented-box IoU, NMS and metrics now use the exact clipper on OpenCV installs.
- Annotating a Pillow image keeps its transparency and untouched pixels.
- Heat maps and ellipse halos now work on grayscale scenes.
It's a drop-in upgrade, but check the Migration guide if you count line crossings, score-oriented boxes on OpenCV, or compare stored outputs.
✨ Spotlights / highlights
Right-to-left labels with the new rtl extra
sv.RichLabelAnnotator now draws Arabic and Hebrew labels correctly on RGB and RGBA images when the new rtl extra is installed. Pillow only joins Arabic letters and orders right-to-left text when it's built with Raqm, and most Windows installs and many containers lack it. There, "سيارة 0.92" came out as isolated letters in reverse order. Lines with Arabic or Hebrew are now shaped in the label's font, using fallback Arabic and Hebrew fonts from khatt for missing glyphs, and right-aligned in the background box. Other image modes fall back to Pillow, with a warning that suggests RGB or RGBA when Raqm is missing. Without the extra, a one-time warning names it. (#2864)
# pip install "supervision[rtl]"
import supervision as sv
label_annotator = sv.RichLabelAnnotator(font_path="NotoNaskhArabic-Regular.ttf")
annotated = label_annotator.annotate(
scene=image, detections=detections, labels=["سيارة 0.92"]
)LineZone counts objects occluded while crossing
If a tracker disappears for a few frames while crossing the line, it's still counted. By default, sv.LineZone drops a tracker's stored side after max(30, minimum_crossing_threshold + 1) consecutive absent frames. It used to drop it after max(2, minimum_crossing_threshold + 1), which is 2 at the default threshold. The new max_absent_frames parameter sets this. Counts can rise against 0.31.4 on the same input. (#2873)
import numpy as np
import supervision as sv
line = sv.LineZone(start=sv.Point(0, 50), end=sv.Point(100, 50))
def detection(y):
return sv.Detections(
xyxy=np.array([[40.0, y - 10, 60.0, y]]), tracker_id=np.array([1])
)
line.trigger(detection(40)) # above the line
for _ in range(5): # occluded for five frames
line.trigger(sv.Detections.empty())
line.trigger(detection(80)) # reappears below the line
line.in_count, line.out_count
# (0, 1) (0.31.4: (0, 0))
# Restore the old retention:
sv.LineZone(start=sv.Point(0, 50), end=sv.Point(100, 50), max_absent_frames=2)The exact clipper for oriented boxes on OpenCV installs
sv.oriented_box_iou_batch, OBB NMS and OBB metrics now use the same convex clipper on OpenCV installs that the NumPy fallback uses. With OpenCV, rotated boxes with nearly collinear overlapping edges, and boxes with both sides far below one coordinate unit, could get an IoU of 0 or 1. The clipper is roughly 10x slower per candidate pair than the OpenCV call, so crowded scenes can take seconds. Oriented-box precision, recall, F1, and mean average recall also keep corner precision at large coordinate origins. (#2882, #2887)
import numpy as np
import supervision as sv
def rotated(width, height, degrees, center=(200.0, 200.0)):
t = np.deg2rad(degrees)
u = np.array([np.cos(t), np.sin(t)]) * width / 2
v = np.array([-np.sin(t), np.cos(t)]) * height / 2
c = np.array(center)
return np.array([[c - u - v, c + u - v, c + u + v, c - u + v]])
t = np.deg2rad(10)
a = rotated(100, 40, 10)
b = a + np.array([np.cos(t), np.sin(t)]) * 50 # true IoU is 0.3333
sv.oriented_box_iou_batch(a, b)[0, 0]
# 0.3333 (0.31.4 with OpenCV: 1.0)Pillow scenes keep the pixels annotators do not draw
Annotators handed a PIL.Image now change only the pixels they draw. A transparent RGBA or LA background stays transparent, and a 16-bit (I;16) or 32-bit (I) image keeps the depth of its untouched pixels, with drawn pixels scaled back to the 16-bit range. Before, the whole image went through an 8-bit round trip. (#2883)
import numpy as np
import supervision as sv
from PIL import Image
scene = Image.new("RGBA", (100, 100), (0, 0, 0, 0))
detections = sv.Detections(
xyxy=np.array([[20.0, 20.0, 60.0, 60.0]]), class_id=np.array([0])
)
annotated = sv.BoxAnnotator().annotate(scene, detections)
annotated.getpixel((0, 0))
# (0, 0, 0, 0) (0.31.4: (0, 0, 0, 255))🔄 Migration guide
No public symbol was removed or renamed, and nothing is deprecated. A few fixes change what existing code returns or draws:
- Different counts:
LineZonekeeps a tracker's side formax(30, minimum_crossing_threshold + 1)absent frames instead of 2, soin_countandout_countcan rise on the same input. Passmax_absent_frames=max(2, minimum_crossing_threshold + 1)to restore the old retention. (#2873) - Different scores and speed on OpenCV installs: oriented-box IoU, NMS and metrics use the exact clipper, roughly 10x slower per candidate pair. Regenerate any stored IoU, NMS and mAP references. (#2882, #2887)
- Different pixels and files: Pillow scenes keep unannotated pixels (#2883),
as_cocowrites mask holes joined to their outline (#2879), and the GHUM skeleton gains its left wrist-to-left-index edge (#2876). - New error without OpenCV: the
cvtColorfallback reads float32COLOR_HSV2BGRinput as OpenCV does (hue in degrees, other values in[0, 1]) and raisesValueErrorfor depths OpenCV rejects. Convert float32 HSV to that scale, or cast touint8, first. (#2885) - Video reads:
get_video_frames_generatorstops at the requested end instead of reading excluded frames. (#2874)
📝 Notable changes
🚀 Added
rtlextra.pip install "supervision[rtl]"installskhatt, andRichLabelAnnotatorthen shapes Arabic and Hebrew lines in the label font, with fallback fonts fromkhattfor missing glyphs. Without the extra and without Raqm, a one-time warning names it. (#2864)LineZone(max_absent_frames=...). Sets how many consecutive missing frames pass before a tracker's crossing state is dropped; values below 1 raiseValueError. (#2873)
🌱 Changed
LineZonedefault retention goes frommax(2, minimum_crossing_threshold + 1)tomax(30, minimum_crossing_threshold + 1)absent frames, so occluded objects are counted. (#2873)oriented_box_iou_batch, OBB NMS and OBB metrics use the convex clipper of the NumPy fallback on OpenCV installs, so IoU can differ for affected pairs; expect roughly 10x the cost per candidate pair. (#2882)
🔧 Fixed
Annotators
- Annotators given a
PIL.Imagechange only the pixels they draw: RGBA and LA transparency stays, andI;16andIimages keep the depth of untouched pixels. (#2883) HeatMapAnnotatorandVertexEllipseHaloAnnotatorannotate grayscale scenes, shaped(H, W),(H, W, 1)or a Pillow"L"image, instead of raisingValueError. (#2872, #2878)
Detections, zones and metrics
oriented_box_iou_batchon OpenCV installs no longer reports an IoU of 0 or 1 for rotated boxes with nearly collinear overlapping edges, or for boxes with both sides far below one coordinate unit. (#2882)- Oriented-box precision, recall, F1 and mean average recall keep corner precision at large coordinate origins, so perfect predictions there are no longer misses or filed under the wrong object-size category. (#2887)
np.sumworks on aCompactMask, with or withoutaxis, and matches the dense mask fordtype,out,keepdims,initialandwhere; it raisedTypeErrorbefore. (#2880)Detections.get_anchors_coordinates(Position.CENTER_OF_MASS)returns an empty(0, 2)array for an empty batch without masks instead of raisingValueError; a populated batch without masks still raises. (#2877)CSVSinkkeeps an exception raised inside awithblock when cleanup also raises; the cleanup exception is logged. (#2869)
Datasets and exports
as_cocokeeps the holes of masks written as polygons withiscrowd=0, joining them to the outline instead of writing extra polygon parts that readers fill in; a hole that cannot be joined warns and keeps the old parts. (#2879)- Hole bridging in
as_yolo,as_pascal_vocandas_labelmeretries top-down, so valid masks with an enclosed hole listed before its outer hole no longer raiseValueError. (#2879) - COCO export computes box width, height and area with Python scalars: without a mask or stored area, a
300×300float16orint16box exports an area of90000instead of infinity or24464. (#2867)
Key points, video and windows
Skeleton.GHUM(insupervision.key_points.skeletons) connects the left wrist to the left index finger;EdgeAnnotatorhad drawn a bone from the right elbow to the right pinky on 33-keypoint poses. (#2876)get_video_frames_generatorstops reading and skipping at the requested end. (#2874)ImageWindow.showdraws the frame on the call that opens the window, opens at the frame's size up to the screen size without growing on each redraw, and fits frames with a one-pixel dimension. (#2881)
NumPy fallback without OpenCV
cvtColoraccepts BGRA input forCOLOR_BGR2GRAYand(H, W, 1)input forCOLOR_GRAY2BGR, sograyscale_imageno longer fails on BGRA frames. (#2866)bluronuint16andint16images uses exact integer window sums and OpenCV's scaling precision. (#2875)cvtColorreads float32COLOR_HSV2BGRinput as OpenCV does. (#2885)
🏆 Contributors
Welcome to our first-time contributors, Mohammad Essam, Barsam Ershadi, and oha!
- Barsam Ershadi (@BarsamErshadi, LinkedIn) – grayscale heat maps and halos, COCO mask holes,
np.sumonCompactMask, empty-batch center-of-mass anchors, window fixes, the GHUM wrist edge and the oriented-box overlap clipper. - kevin (@kevin9327) – made the
cvtColorandblurfallbacks follow OpenCV for BGRA, gray, 16-bit and float32 input. - Mohammad Essam (@DrEssam97, LinkedIn) – shaped Arabic and Hebrew labels in
RichLabelAnnotatorwith thertlextra. - NIKHIL (@Nikhi00718, LinkedIn) – made
LineZonecount objects occluded while they cross. - A Aswanth Raj (@aswanth-07, LinkedIn) – kept oriented-box metric precision at large coordinate origins.
- LEE, WEI-HONG (@AuricTW) – fixed COCO area overflow and kept
withblock exceptions inCSVSink. - Mohammad Hijjawi (@MohammadHijjawi97, LinkedIn) – kept unannotated pixels of Pillow scenes unchanged.
- Devulapalli Naga Sri Vaishnavi (@Vaishnavi220506, LinkedIn) – stopped the video frame generator at the requested end.
- oha (@duriantaco) – removed unused code.
- Jirka Borovec (@Borda, LinkedIn) – prepared the release.
Automated contributions: pre-commit.ci
Full changelog: 0.31.4...0.31.5
Co-authored with /oss:release.