Repository navigation
fix: accept single-class end2end YOLOv10 exports in NMS-free postprocessing - #8
Conversation
…rocessing classes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…l-ops Document the single-class face_detection postprocessing contract and the face-model input geometry taken from mblt-model-ops pipeline.yaml, including the YOLOv8m-face/YOLOv8l-face 960x960 exception, across AGENTS.md, CLAUDE.md, both SKILL.md copies, and mblt_vision/README.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
model-ops carries 24 face models; only 15 were listed here. Add the nine
missing anchor-based configs from ../mblt-model-ops/models: YOLOv5{n,n-0.5,s,m}-face
(deepcam-cn) and YOLOv7{,s,-tiny,-lite-s,-lite-t}-face (derronqi), taking
pre_cfg/post_cfg verbatim from each model's pipeline.yaml.
These are the first shipped YAMLs that reach YOLOAnchorFaceDetectionPost. Their
published ONNX exports emit three raw (batch, 3, H, W, 6) heads with the
upstream landmark pairs stripped, so they decode through the shared anchor path
with nc = 1 and iou_thres 0.5.
Verified end to end against both the model-ops model.onnx files and the
Hub-resolved exports: all nine decode, agree with the shipped YOLOv8n-face
reference to within ~2px on the same face, restore into original image
coordinates in bounds, and label every row "face".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
At nc == 1 a five-column tensor is ambiguous: it is both the end2end export layout (xyxy + confidence, class column stripped) and the width of a converted xywh candidate tensor (4 + nc), which belongs to non_e2e. Separate them by row count. An end2end export is top-k selected and never exceeds max_det rows, while a converted tensor carries one row per anchor (8400 at 640x640), so only the bounded form is treated as already-decoded detections.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7ddcd64064
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Three issues from the Codex review of 7ddcd64: - Row count is not provenance. A small single-class configuration produces fewer candidates than max_det (a 64x64 three-head model yields 84), so a converted (B, 84, 5) tensor passed the bound and skipped filter_conversion and dual_topk. Separate the two by the one exact property available: a converted tensor carries precisely one row per anchor, and the configuration knows its own anchor count. Ties are read as converted, so a genuine export of that shape fails loudly instead of silently skipping candidate selection. - Squeezing whichever leading axis happened to be 1 dropped the batch for a (B, 1, N, 5) compiler output with B > 1, which then fell back to raw-head decoding and raised the very error this restore exists to prevent. Squeeze the compiler's axis 1 specifically. - With e2e disabled the restored tensor returned a per-image list, bypassing the non_e2e export contract the CLI promises. Stack it to (B, max_det, 6), matching the decoded-triplet branch above.
|
@codex review All three P2 findings from the What changed
Deliberate decisions — please do not re-raise these as defects
Validation already performed, if useful as context
The most valuable thing to scrutinize is point 1: whether anchor count is a |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 452d5099fd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
`tensor.shape[1] == self._anchor_grid_size()` is false for every integer row count when the grid size is None, so a configuration with unresolved anchors lost the guard entirely and had every ambiguous five-column tensor restored as a final export -- the exact silent bypass of conversion and candidate selection the guard exists to prevent. Resolve the grid size first and skip restoration when it is unavailable. Without an anchor count there is nothing to separate a stripped export from a converted candidate tensor, so an unrecognized tensor now fails loudly on raw-head decoding instead. This does not affect the shipped path: anchors resolve at construction, before any inference.
|
@codex review Fixed in You were right, and the finding also caught me asserting something false: my
Verified behavior after the fix
The shipped path is unaffected: anchors resolve at construction, before any Still deliberate, please do not re-raise as defectsRestating these because they are settled decisions, not oversights, and the
Re-validated at 449cd7b
If a further finding is a style or structural preference rather than a |
|
Codex Review: Didn't find any major issues. You're on a roll. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Problem
YOLOv10*-faceONNX artifacts could not be postprocessed at all. Every one ofthe four models raised:
build_postprocessdispatches these correctly toYOLONMSFreeFaceDetectionPost, andextract_final_outputsalready handlesqbcompiler's decode-enabled output triplet. But a single already-decoded
end2end tensor was only recognised at six columns, because
base._normalize_final_detection_tensoris called with a hardcodedfinal_det_dim = 6 + n_extra(xyxy, confidence, class id).A model with
nc == 1has a class column that is constant0, andmblt-model-ops strips it at export, so the shipped
*-faceONNX emits(1, N, 5). That never matched, fell through to raw split-head decoding, andwas rejected. The six-column object-detection form (
YOLOv10m,YOLOv10s)was unaffected and worked throughout.
Fix
YOLONMSFreeDetectionPost.extract_final_outputsnow restores the implicitclass column when a single-class model presents the five-column form, then
delegates to the existing six-column path rather than duplicating any decode
logic.
At
nc == 1five columns are also the width of a converted xywh candidatetensor (
4 + nc), which is a different thing and belongs tonon_e2e. The twoare separated by row count: an end2end export is top-k selected and never
exceeds
max_det, while a converted tensor carries one row per anchor (8400 at640x640). Only the bounded form is treated as decoded detections.
Validation
*-facemodels in mblt-model-ops now agree with their model-localcommon/local_inference.pyon identical inputs: 24/24 on ONNX and 24/24 onMXQ across all three decode families (9 anchor, 11 anchorless, 4 nmsfree),
scores within 0.005.
anchorless were already 20/20, so the change is scoped to the broken path.
YOLOv10mstill returns 160 multi-classdetections,
YOLOv10s183.(
test_mxq_inference.py::test_mxq_classification_runs_with_shared_npu_optionsand two
test_wrapper.pydflfree-pose cases) fail identically with thischange reverted.
mblt-model-opscan now dropface_detectionfrom its parity harnessEXCLUDED_TASKSonce a release carrying this fix is pinned; with it applied,parity covers 350 models including all 24 face models.
One open question, not addressed here
While validating, a second divergence surfaced that is a product decision
rather than a bug fix, so it is deliberately left out of this PR.
mblt-vision caps NMS output at
max_det=300for every task. Every face model'scommon/local_inference.pyin mblt-model-ops is uncapped, as isdatasets/widerface/evaluator.py. On WiderFace this is reachable and material:at the committed
conf_thres: 0.001, 5 of 12 sampled0--Paradeimages exceed300 detections and one reaches 1277. Because the Hard subset supplying the
primary metric is exactly the dense, tiny-face regime, a 300-detection cap
discards real recall and will report a lower Hard AP than the repository does on
the same artifact.
The NMS-free path is unaffected — its selection is architecturally top-300, so
both sides already agree there.
Worth deciding whether
YOLOFaceDetectionMixinshould raise or remove the capfor WiderFace, or whether the repository should adopt the 300 cap instead. The
parity adapters currently pin the difference to the cap alone: both sides must
agree on every detection mblt-vision returns, and the repository's surplus rows
must be strictly the lowest-scoring ones, so a genuine decode disagreement still
fails.