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:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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.')
|
||||
|
||||
Reference in New Issue
Block a user