Repository navigation
fix(examples): center the C++ letterbox and undo its padding offset - #320
Merged
Merged
Conversation
Preprocess() in examples/cpp placed the resized image in the top-left corner of
the canvas and padded with black zeros, while
ultralytics.data.augment.LetterBox centers the image and pads with 114 grey. For
a 1080x810 frame the C++ examples therefore fed the model a picture shifted by
80 rows against what model.predict() sends, and every Postprocess* divided by
the resize ratio without removing that offset first, so the boxes came back
displaced by the same amount.
- Preprocess() now reproduces LetterBox(center=True): min-side ratio, centered
padding split with the same round(pad / 2 - 0.1) rule, 114 grey fill, and it
reports the transform in a new yolo::Letterbox {scale, pad, unpad}.
- Every Postprocess* takes that Letterbox and maps model-space coordinates back
with orig = (model - pad) / scale. Masks and semantic maps crop the padding
band before they are resampled to the original frame.
- The six backends (ONNXRuntime, OpenCV-DNN, OpenVINO, MNN, LibTorch, Triton)
thread the struct through instead of a bare `float scale`.
- The contract is documented in examples/cpp/common/README.md.
Verified against Python: for 14 (image size, imgsz in {320, 640}) combinations
the scale, unpadded size and left/top padding match LetterBox.get_params
exactly, and a box round-tripped through the new post-processor returns
100,199,200,200 from an original 100,200,300,400 (1 px of int truncation),
where the old mapping produced y=335.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
Preprocess()inexamples/cpp/common/yolo_postprocess.hppbuilt its canvas as a top-left letterbox with a black fill, while Python usesultralytics.data.augment.LetterBox(center=True), i.e. a centered placement filled with 114 grey. The C++ examples were therefore feeding the model a different picture thanmodel.predict()does, and none of thePostprocess*functions subtracted the padding before dividing by the resize ratio, so boxes came back displaced bypad / scale.For the usual 1080x810 test frame at
imgsz=640that is an 80 row shift in model space, i.e. 135 px of vertical offset in original-image coordinates. Boxes still looked plausible because both sides applied their own inverse consistently, but the tensor the model saw was wrong — which is exactly how a "boxes are right, confidences drift" report looks (see #318).Changes
common/yolo_postprocess.hppPreprocess()now reproducesLetterBox(center=True): min-side ratio, centered padding split with the sameround(pad / 2 - 0.1)/round(pad / 2 + 0.1)rule, 114 grey fill. It reports the transform in a newyolo::Letterbox {scale, pad, unpad}.common/yolo_postprocess.hppPostprocess*take thatLetterboxinstead of a barefloat scaleand map back withorig = (model - pad) / scale— boxes, keypoints (pose, incl. per-keypoint x/y) and OBB centers. Segment/semantic crop the padding band before resampling to the original frame.ONNXRuntime,OpenCV-DNN,OpenVINO,MNN,LibTorch,Tritonthread the struct through.common/README.mdThe
padoffset is carried as a struct rather than an extra default argument on purpose: a caller that forgets an optionalpad = 0would silently reproduce this bug, whereas the signature change is caught at compile time.Verification
No OpenCV in this environment, so I checked the two things that can actually be wrong — geometry and the inverse mapping — against the Python implementation:
Geometry parity. For 14 (image size,
imgsz in {320, 640}) combinations, including portrait frames (333x777,500x700) where the padding is on the x axis,scale,unpadandpadmatch a literal transcription ofLetterBox.get_paramsexactly:Round trip. An original box
100,200 -> 300,400projected into model space and pushed back through the newPostprocessDetectreturns100,199,200,200. The old mapping givesy = 335for the same input.Padding. Corners of the padded bands are
114,114,114after the BGR->RGB conversion, and the first content row lands aty = pad.y.clang++ -std=c++17 -Wall -Wextrais clean on the header. I could not build the backends themselves here (no ORT/OpenVINO/MNN/libtorch installed), so CI or a reviewer with the toolchains should confirm; the changes to them are mechanical (one local variable and the argument in the call sites).Not in this PR
Two smaller mismatches in the same file, so they are visible rather than silently lumped in:
cv::dnn::NMSBoxesis class-agnostic, while Ultralytics validates with per-class NMS (agnostic=False). On frames with several classes this suppresses boxes that Python keeps.cv::Rect(int), and are not clipped to the image bounds the wayscale_boxesclips them.Happy to do either as a follow-up if you want them.