1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-30 07:43:03 +04:00

Merge pull request #28920 from 5usu:fix/28798-yunet-int8-objbranch

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).

  
<img width="1864" height="1060" alt="image" src="https://github.com/user-attachments/assets/a1b15c64-6b84-42b1-86a4-1d55474cb38c" />
This commit is contained in:
sujal
2026-05-25 12:18:59 +05:30
committed by GitHub
parent f539c0907e
commit 62930a207b
4 changed files with 17 additions and 3 deletions
+10 -1
View File
@@ -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 Kg<K0, multiple groups map
// to the same K0-wide output slot and clobber one another, so
// skip the fusion and fall back to the unfused float path.
// See https://github.com/opencv/opencv/issues/28798
const int outChannelsPerGroup = outCn / std::max(conv->ngroups, 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;
+5
View File
@@ -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);
}
{
+1 -1
View File
@@ -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}"
+1 -1
View File
@@ -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.')