From 62930a207b627106220355ab97f9ca243e906a6b Mon Sep 17 00:00:00 2001 From: sujal <156937690+5usu@users.noreply.github.com> Date: Mon, 25 May 2026 12:18:59 +0530 Subject: [PATCH] Merge pull request #28920 from 5usu:fix/28798-yunet-int8-objbranch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dnn: skip Conv2Int8 fusion for grouped convs with Kg<8 (#28798) #28920 OpenCV Extra: https://github.com/opencv/opencv_extra/pull/1360 - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [x] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [x] The feature is well documented and sample code can be built with the project CMake --- Fixes #28798. The YuNet 2023mar int8 model from opencv_zoo doesn't detect anything on 5.x. With `--score_threshold 0.05` the detector still returns no boxes, so it's not just a confidence drop — the network output is broken. After dumping intermediate tensors and comparing against onnxruntime, the `obj_*` branches collapse to ~0 (saturated int8 `-128`), and the `cls_*` branches saturate the other way. Final score is `cls * obj` so nothing ever crosses threshold. The cause is in `Conv2Int8`'s VNNI kernel. It processes `K0 = 8` output channels per SIMD iteration and writes them as one `K0`-wide block to `out + (n*K1 + k1)*planesize` with `k1 = k_base / K0`. When `ngroups > 1` and `Kg = K/ngroups < K0`, consecutive groups share the same `k1` slot and overwrite each other — only the last group's result survives. YuNet has six `1x1x3x3` depthwise conv heads (`Kg = 1`), which is exactly this case. The unfused `DequantizeLinear → Conv2 → QuantizeLinear` path is fine. The minimal fix here is to skip the rewrite to `Conv2Int8` when `Kg < 8` so we fall back to the float path. A proper depthwise int8 kernel can be added later as a separate optimization. Verified with `samples/dnn/face_detect.py` on Lena: Before: AssertionError: Cannot find a face in samples/data/lena.jpg After: Face 0, top-left coordinates: (204, 187), box width: 149, box height 212, score: 0.89 Cross-check vs onnxruntime: `cls_8` mean `0.673` (was `0.93`, ORT `0.673`), `obj_8` max `0.0039` (was `0`, ORT `0.0039`). The regression test is a single 16-group depthwise `QLinearConv` with non-zero `x_zp`, added to `Quantized_Convolution`. Test data is in opencv_extra on the matching branch (~9 KB total). image --- modules/dnn/src/graph_fusion_qdq.cpp | 11 ++++++++++- modules/dnn/test/test_onnx_importer.cpp | 5 +++++ samples/dnn/face_detect.cpp | 2 +- samples/dnn/face_detect.py | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/modules/dnn/src/graph_fusion_qdq.cpp b/modules/dnn/src/graph_fusion_qdq.cpp index 4ac53fdefa..7ef3da8113 100644 --- a/modules/dnn/src/graph_fusion_qdq.cpp +++ b/modules/dnn/src/graph_fusion_qdq.cpp @@ -486,7 +486,16 @@ struct ModelFusionQDQ symmetric_pads = false; } - if (all_wzp_zero && symmetric_pads) { + // Conv2Int8's VNNI kernel processes K0=8 output channels per + // SIMD iteration. When ngroups>1 and Kgngroups, 1); + const bool kernelSupportsGrouping = + conv->ngroups <= 1 || outChannelsPerGroup >= 8; + + if (all_wzp_zero && symmetric_pads && kernelSupportsGrouping) { Mat bias = Mat::zeros(1, outCn, CV_32S); bool biasOk = true; int dq_bias_idx = -1; diff --git a/modules/dnn/test/test_onnx_importer.cpp b/modules/dnn/test/test_onnx_importer.cpp index a6ee94f5b0..d43fe21717 100644 --- a/modules/dnn/test/test_onnx_importer.cpp +++ b/modules/dnn/test/test_onnx_importer.cpp @@ -2197,6 +2197,11 @@ TEST_P(Test_ONNX_layers, Quantized_Convolution) testONNXModels("quantized_conv_int8_weights", npy, 0.03, 0.5); testONNXModels("quantized_conv_per_channel_weights", npy, 0.06, 0.4); testONNXModels("quantized_conv_asymmetric_pads_int8_weights"); + // Regression test for https://github.com/opencv/opencv/issues/28798: + // depthwise QLinearConv (Kg=1 < kernel SIMD width K0=8) used to be + // miscompiled by Conv2Int8's VNNI kernel because multiple groups + // wrote into the same K0-wide output slot. + testONNXModels("quantized_depthwise_conv_int8_weights", npy, 0.06, 0.5); } { diff --git a/samples/dnn/face_detect.cpp b/samples/dnn/face_detect.cpp index 1f9e6a0ae0..b8c3b622e6 100644 --- a/samples/dnn/face_detect.cpp +++ b/samples/dnn/face_detect.cpp @@ -46,7 +46,7 @@ int main(int argc, char** argv) "{scale sc | 1.0 | Scale factor used to resize input video frames}" "{fd_model fd | face_detection_yunet_2023mar.onnx| Path to the model. Download yunet.onnx in https://github.com/opencv/opencv_zoo/tree/master/models/face_detection_yunet}" "{fr_model fr | face_recognition_sface_2021dec.onnx | Path to the face recognition model. Download the model at https://github.com/opencv/opencv_zoo/tree/master/models/face_recognition_sface}" - "{score_threshold | 0.9 | Filter out faces of score < score_threshold}" + "{score_threshold | 0.85 | Filter out faces of score < score_threshold}" "{nms_threshold | 0.3 | Suppress bounding boxes of iou >= nms_threshold}" "{top_k | 5000 | Keep top_k bounding boxes before NMS}" "{save s | false | Set true to save results. This flag is invalid when using camera}" diff --git a/samples/dnn/face_detect.py b/samples/dnn/face_detect.py index f07c5f3be1..95012fdbba 100644 --- a/samples/dnn/face_detect.py +++ b/samples/dnn/face_detect.py @@ -18,7 +18,7 @@ parser.add_argument('--video', '-v', type=str, help='Path to the input video.') parser.add_argument('--scale', '-sc', type=float, default=1.0, help='Scale factor used to resize input video frames.') parser.add_argument('--face_detection_model', '-fd', type=str, default='face_detection_yunet_2023mar.onnx', help='Path to the face detection model. Download the model at https://github.com/opencv/opencv_zoo/tree/master/models/face_detection_yunet') parser.add_argument('--face_recognition_model', '-fr', type=str, default='face_recognition_sface_2021dec.onnx', help='Path to the face recognition model. Download the model at https://github.com/opencv/opencv_zoo/tree/master/models/face_recognition_sface') -parser.add_argument('--score_threshold', type=float, default=0.9, help='Filtering out faces of score < score_threshold.') +parser.add_argument('--score_threshold', type=float, default=0.85, help='Filtering out faces of score < score_threshold.') parser.add_argument('--nms_threshold', type=float, default=0.3, help='Suppress bounding boxes of iou >= nms_threshold.') parser.add_argument('--top_k', type=int, default=5000, help='Keep top_k bounding boxes before NMS.') parser.add_argument('--save', '-s', type=str2bool, default=False, help='Set true to save results. This flag is invalid when using camera.')