github roboflow/supervision 0.31.5
supervision-0.31.5

4 hours ago

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 rtl extra.
  • 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: LineZone keeps a tracker's side for max(30, minimum_crossing_threshold + 1) absent frames instead of 2, so in_count and out_count can rise on the same input. Pass max_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_coco writes 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 cvtColor fallback reads float32 COLOR_HSV2BGR input as OpenCV does (hue in degrees, other values in [0, 1]) and raises ValueError for depths OpenCV rejects. Convert float32 HSV to that scale, or cast to uint8, first. (#2885)
  • Video reads: get_video_frames_generator stops at the requested end instead of reading excluded frames. (#2874)

📝 Notable changes

🚀 Added

  • rtl extra. pip install "supervision[rtl]" installs khatt, and RichLabelAnnotator then shapes Arabic and Hebrew lines in the label font, with fallback fonts from khatt for 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 raise ValueError. (#2873)

🌱 Changed

  • LineZone default retention goes from max(2, minimum_crossing_threshold + 1) to max(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.Image change only the pixels they draw: RGBA and LA transparency stays, and I;16 and I images keep the depth of untouched pixels. (#2883)
  • HeatMapAnnotator and VertexEllipseHaloAnnotator annotate grayscale scenes, shaped (H, W), (H, W, 1) or a Pillow "L" image, instead of raising ValueError. (#2872, #2878)

Detections, zones and metrics

  • oriented_box_iou_batch on 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.sum works on a CompactMask, with or without axis, and matches the dense mask for dtype, out, keepdims, initial and where; it raised TypeError before. (#2880)
  • Detections.get_anchors_coordinates(Position.CENTER_OF_MASS) returns an empty (0, 2) array for an empty batch without masks instead of raising ValueError; a populated batch without masks still raises. (#2877)
  • CSVSink keeps an exception raised inside a with block when cleanup also raises; the cleanup exception is logged. (#2869)

Datasets and exports

  • as_coco keeps the holes of masks written as polygons with iscrowd=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_voc and as_labelme retries top-down, so valid masks with an enclosed hole listed before its outer hole no longer raise ValueError. (#2879)
  • COCO export computes box width, height and area with Python scalars: without a mask or stored area, a 300×300 float16 or int16 box exports an area of 90000 instead of infinity or 24464. (#2867)

Key points, video and windows

  • Skeleton.GHUM (in supervision.key_points.skeletons) connects the left wrist to the left index finger; EdgeAnnotator had drawn a bone from the right elbow to the right pinky on 33-keypoint poses. (#2876)
  • get_video_frames_generator stops reading and skipping at the requested end. (#2874)
  • ImageWindow.show draws 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

  • cvtColor accepts BGRA input for COLOR_BGR2GRAY and (H, W, 1) input for COLOR_GRAY2BGR, so grayscale_image no longer fails on BGRA frames. (#2866)
  • blur on uint16 and int16 images uses exact integer window sums and OpenCV's scaling precision. (#2875)
  • cvtColor reads float32 COLOR_HSV2BGR input 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.sum on CompactMask, empty-batch center-of-mass anchors, window fixes, the GHUM wrist edge and the oriented-box overlap clipper.
  • kevin (@kevin9327) – made the cvtColor and blur fallbacks follow OpenCV for BGRA, gray, 16-bit and float32 input.
  • Mohammad Essam (@DrEssam97, LinkedIn) – shaped Arabic and Hebrew labels in RichLabelAnnotator with the rtl extra.
  • NIKHIL (@Nikhi00718, LinkedIn) – made LineZone count 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 with block exceptions in CSVSink.
  • 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.

Don't miss a new supervision release

NewReleases is sending notifications on new releases.