[Bug]: NumPy fallback draws thick rectangle borders inward instead of centering them on the edge
Maintainers usually reply within 1 day
@px12811 is already working on this.
Since Oct 16, 2026.
- #2688 by @dajiaohuang — closed without merging
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 52/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- numpy, python
- Domain
- computer-graphics, computer-vision
Research direction
Start in src/supervision/_cv2/_drawing.py at _rectangle, which uses Pillow ImageDraw.rectangle with width= that only grows inward. Compare against OpenCV stroke centering using the MRE in the issue (BoxAnnotator default thickness 2, with and without opencv-python). Tighten or add tests beyond test_fallback_line_draws_and_matches_opencv_extent so a 1px-per-side error at thickness 2 fails. Done when fallback painted bbox and pixel count match OpenCV for stroked rectangles; do not expand to _line/_circle/_polylines/_ellipse unless maintainers confirm scope.
Written by the indexing model from the issue text.
Description
Bug
What's the bug?
The NumPy fallback draws a stroked rectangle entirely inside the rectangle, while OpenCV centers the stroke on the rectangle's edges. Any box drawn with thickness >= 2 therefore comes out thinner and smaller when opencv-python is not installed.
Since OpenCV is no longer a dependency, the fallback is what a plain pip install supervision uses, and BoxAnnotator's default thickness is 2, so this is the default path. CropAnnotator (default border_thickness=2), PercentageBarAnnotator's border and sv.draw_rectangle (default thickness=2) reach the same call. The filled calls in ColorAnnotator and LabelAnnotator are unaffected.
Expected behavior: the fallback matches OpenCV, per the intent stated in #2433 and #2439.
Actual behavior: the border is drawn inward only. thickness=1 and filled (thickness=-1) rectangles are unaffected.
| backend | painted px | painted bbox |
|---|---|---|
opencv |
596 | x 19-81, y 19-61 |
fallback |
392 | x 20-80, y 20-60 |
For a (3,3) to (11,8) rectangle at thickness=2, OpenCV paints rows 2 to 9 and columns 2 to 12, its left band covering columns 2 to 4. The fallback paints rows 3 to 8 and columns 3 to 11, its left band covering only columns 3 and 4. The stroke is one pixel narrower and sits one pixel inside OpenCV's on every side.
Cause: _rectangle in src/supervision/_cv2/_drawing.py uses Pillow's ImageDraw.rectangle(outline=1, width=thickness), and Pillow's width= only grows inward. OpenCV strokes each edge as a band centered on it and rounds the four joints with a filled disc, so the border spreads outside the rectangle as well as inside.
This was not caught because test_fallback_line_draws_and_matches_opencv_extent compares only the raster bounding box with atol=1, and the error at thickness=2 is exactly 1 pixel per side.
Environment
- Supervision: 0.31.0.dev0 (
develop, 09e0b53) - Python: 3.12
- OS: Windows 11
Reproduced identically against opencv-python 4.10.0 and 5.0.0.
Minimal Reproducible Example
Run once with opencv-python installed and once without.
import numpy as np
import supervision as sv
from supervision._cv2 import BACKEND_NAME
detections = sv.Detections(
xyxy=np.array([[20.0, 20.0, 80.0, 60.0]]),
class_id=np.array([0]),
confidence=np.array([0.9]),
)
scene = np.zeros((80, 100, 3), dtype=np.uint8)
painted = sv.BoxAnnotator().annotate(scene, detections).any(axis=2)
ys, xs = np.where(painted)
print(f"backend : {BACKEND_NAME}")
print(f"painted pixels : {int(painted.sum())}")
print(f"painted bbox : x {xs.min()}-{xs.max()}, y {ys.min()}-{ys.max()}")
Are you willing to submit a PR?
Yes. I have a fix for rectangle that matches OpenCV exactly on 1310 randomized and edge cases (degenerate, reversed, negative and out-of-bounds corners, thickness 1 to 25) on both OpenCV 4.10 and 5.0, with a test that fails on develop and passes with it.
Two scoping questions before I open it:
_line,_circle,_polylinesand_ellipsehave the same inward-stroke cause. Do you want one PR for all five, orrectanglefirst and the rest separately?_circleand_ellipsealso differ slightly when filled or atthickness=1, because Pillow's ellipse rasterizer is not OpenCV's. OpenCV's filled circle is exactly the integer discx^2 + y^2 <= r^2, so that is cheap to match. Worth doing in the same pass, or is a separate issue better?
- Dominant language
- Python
- Stars
- 51.6k
- Forks
- 5k
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 191
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from roboflow/supervision
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
roboflow/supervision#2971 ·
Maintainers usually reply within 1 day
-
[Bug]: DetectionDataset.split loses a train item when len(images) * split_ratio is a whole number (90 images at 0.7 give 62/28, not 63/27)Possibly taken @avanish-garg claimed this today. Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 30/100
roboflow/supervision#2968 ·
Maintainers usually reply within 1 day
-
[Bug]: MeanAverageRecall returns 0.0 instead of -1 when there are no targets (whole run or empty size bucket)Possibly taken @MaiAditya claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 20/100
roboflow/supervision#2964 ·
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 38/100
roboflow/supervision#2950 ·
Maintainers usually reply within 1 day
-
bug
Difficulty 3/5 1-2 days Newbie friendliness 56/100
roboflow/supervision#2938 · 2 comments ·
Maintainers usually reply within 1 day
All issues in roboflow/supervision
Similar issues
-
area:core-platform effort:S priority:P3 risk:night-safe status:ready type:test
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
randomparity/kdive#3620 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
FinanceFlash/unvibecode#760 ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 88/100
google-research/google-research#3470 ·
Maintainers usually reply within 21 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 88/100
mrvautin/ha-workouts#11 ·