perf(annotators): skip corner circles when drawing square label backgrounds#2346
perf(annotators): skip corner circles when drawing square label backgrounds#2346kounelisagis wants to merge 6 commits into
Conversation
…rounds Both rounded-rectangle helpers (LabelAnnotator.draw_rounded_rectangle and the public draw_rounded_rectangle in draw/utils) always drew two rectangles plus four corner circles, even when border_radius is 0, which is the default for LabelAnnotator and VertexLabelAnnotator. With a zero radius that is six cv2 calls per label per frame (the four circles are zero-radius no-ops) where one fill rectangle does the same thing. Add a square-corner fast path to both helpers. Output is pixel identical; only the redundant calls go away. On a 1080p frame with 100 labels LabelAnnotator drops from ~2.1 ms to ~1.3 ms (about 1.6x), and the rounded-rectangle call itself is ~2.8x faster at radius 0. The radius > 0 path is unchanged. Adds tests pinning square output to a plain rectangle for both helpers (the public draw/utils function had no tests before).
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #2346 +/- ##
=======================================
- Coverage 82% 82% -0%
=======================================
Files 68 68
Lines 9560 9566 +6
=======================================
- Hits 7881 7880 -1
- Misses 1679 1686 +7 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR optimizes rounded-rectangle drawing in annotators and draw utilities by adding a fast path for the common border_radius <= 0 case, reducing unnecessary OpenCV calls while keeping output identical.
Changes:
- Added a square-corner fast path in both
draw_rounded_rectanglehelpers whenborder_radius <= 0. - Added unit tests covering square-corner equivalence (radius
0and negative) for both helpers. - Added a test asserting that positive radius actually rounds corners.
Review Scores (n/5):
- Code quality: 5/5
- Testing: 5/5
- Documentation: 4/5
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/supervision/draw/utils.py |
Adds early-return square-corner rectangle fill when border_radius <= 0 to avoid extra cv2 calls. |
src/supervision/annotators/core.py |
Adds the same fast path to LabelAnnotator.draw_rounded_rectangle for the hot per-label background draw. |
tests/draw/test_utils.py |
New tests for draw/utils.draw_rounded_rectangle square behavior and a positive-radius rounding assertion. |
tests/annotators/test_core.py |
Adds a regression test ensuring LabelAnnotator.draw_rounded_rectangle matches a plain filled rectangle for non-positive radius. |
- Rename LabelAnnotator.draw_rounded_rectangle to _draw_rounded_rectangle (accidentally public static method — now signals internal) - Expand draw/utils.py border_radius docstring: document <= 0 and clamp-to-zero fast-path behaviour - Add crash-era comment to both test files: border_radius < 0 previously raised cv2.error; fast path silently draws square corners instead - Add clamped-to-zero test in both test files: positive radius on a 1px-wide box clamps to 0 and triggers the fast path - Strengthen positive-radius assertion: full center-row check + all four corners unpainted (replaces two-pixel spot check) - Add pytest.param(id=) slugs to all parametrize decorators per CONTRIBUTING.md convention --- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Reverts the public-to-private rename of LabelAnnotator.draw_rounded_rectangle from this branch: renaming a public method is a breaking change and out of scope for a perf fix. The method stays public; only the border_radius <= 0 fast path remains (per review).
|
@kounelisagis, could you pls add visualization before and after? 🦝 |
Sure! Here is before (develop) vs after (this PR), for both the square default and a rounded radius, with an amplified diff on the right:
Top row is How it was generatedimport numpy as np, supervision as sv
scene = np.zeros((360, 640, 3), np.uint8)
scene[:] = np.linspace(40, 120, 640)[None, :, None].astype(np.uint8)
det = sv.Detections(
xyxy=np.array([[40, 40, 200, 150], [260, 60, 460, 180],
[120, 210, 360, 330], [420, 210, 600, 320]], float),
confidence=np.array([0.91, 0.86, 0.78, 0.69]),
class_id=np.array([0, 1, 2, 1]),
data={"class_name": np.array(["person", "car", "dog", "car"])},
)
box = sv.BoxAnnotator(thickness=2)
for radius in (0, 25):
lab = sv.LabelAnnotator(border_radius=radius, text_scale=0.7, text_padding=8)
img = lab.annotate(box.annotate(scene.copy(), det), det)Run on |
# Conflicts: # tests/draw/test_utils.py

What
Both rounded-rectangle helpers (
LabelAnnotator.draw_rounded_rectangleand the publicdraw_rounded_rectangleindraw/utils) always drew two rectangles plus four corner circles, even whenborder_radiusis 0. Zero is the default forLabelAnnotatorandVertexLabelAnnotator, so the common case paid for six cv2 calls per label per frame where one fill rectangle does the same thing (the four circles are zero-radius no-ops, and one of the two rectangles already covers the whole box).This adds a square-corner fast path to both helpers: when
border_radius <= 0, draw a single fill rectangle and return. Theradius > 0path is untouched.Impact
I profiled a typical 1080p frame with 100 labels and
_draw_labelswas the hottest part of the annotation, with 40k zero-radiuscirclecalls. With the fast path:LabelAnnotatorat 1080p, 100 labels: ~2.1 ms to ~1.3 ms per frame (about 1.6x), best of 7 runsVertexLabelAnnotatorbenefits too, since it uses the shareddraw/utilshelperOutput is pixel identical, verified against develop for
LabelAnnotator,VertexLabelAnnotator, anddraw_rounded_rectangleat both radius 0 and radius > 0.I kept the two helpers separate rather than merging them: they take different inputs (xyxy tuple plus BGR vs
RectplusColor), and routing one through the other would add per-label object allocation in exactly the hot path this speeds up.Tests
Added tests pinning the square output to a plain rectangle for both helpers, plus a rounded case that checks corners are actually cut. The public
draw_rounded_rectanglehad no tests before this.