Skip to content

perf(annotators): skip corner circles when drawing square label backgrounds#2346

Open
kounelisagis wants to merge 6 commits into
roboflow:developfrom
kounelisagis:perf/label-square-corners-fast-path
Open

perf(annotators): skip corner circles when drawing square label backgrounds#2346
kounelisagis wants to merge 6 commits into
roboflow:developfrom
kounelisagis:perf/label-square-corners-fast-path

Conversation

@kounelisagis

Copy link
Copy Markdown
Contributor

What

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. Zero is the default for LabelAnnotator and VertexLabelAnnotator, 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. The radius > 0 path is untouched.

Impact

I profiled a typical 1080p frame with 100 labels and _draw_labels was the hottest part of the annotation, with 40k zero-radius circle calls. With the fast path:

  • LabelAnnotator at 1080p, 100 labels: ~2.1 ms to ~1.3 ms per frame (about 1.6x), best of 7 runs
  • the rounded-rectangle call itself at radius 0: ~2.8x faster in isolation
  • VertexLabelAnnotator benefits too, since it uses the shared draw/utils helper

Output is pixel identical, verified against develop for LabelAnnotator, VertexLabelAnnotator, and draw_rounded_rectangle at 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 Rect plus Color), 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_rectangle had no tests before this.

…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).
@kounelisagis kounelisagis requested a review from SkalskiP as a code owner June 20, 2026 20:05
@codecov

codecov Bot commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82%. Comparing base (09b2199) to head (bd2b80f).

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:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_rectangle helpers when border_radius <= 0.
  • Added unit tests covering square-corner equivalence (radius 0 and 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.

Borda and others added 2 commits June 22, 2026 11:01
- 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>
Comment thread src/supervision/annotators/core.py Outdated
kounelisagis and others added 2 commits June 22, 2026 22:57
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 kounelisagis requested a review from Borda June 22, 2026 20:53
@Borda Borda added the enhancement New feature or request label Jun 23, 2026
@Borda

Borda commented Jun 23, 2026

Copy link
Copy Markdown
Member

@kounelisagis, could you pls add visualization before and after? 🦝

@kounelisagis

Copy link
Copy Markdown
Contributor Author

@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:

label_before_after

Top row is border_radius=0 (the default), bottom row is border_radius=25. The diff panel is absdiff x20, so any change would light up; it stays fully black in both cases. Output is pixel identical (max abs diff = 0) for square and rounded alike. The square path now draws one filled rectangle instead of two rectangles plus four zero-radius corner circles, and the rounded path is untouched.

How it was generated
import 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 develop and on this branch, then compare with cv2.absdiff (max abs diff is 0 in both cases).

# Conflicts:
#	tests/draw/test_utils.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants