OpenCV + YOLOv5 最稳C++实时检测链路是cv::VideoCapture读帧 + TorchScript模型加载,需导出为TorchScript、手动transpose维度、复用tensor内存、多线程安全传帧、正确解析输出并还原坐标。

OpenCV + YOLOv5 的最小可行检测链路
直接用 cv::VideoCapture 读帧 + torch::jit::script::Module 加载 TorchScript 模型,是当前最稳的 C++ 实时检测路径。别碰 OpenCV 的 DNN 模块跑 YOLO —— 它对 Upsample 和 Concat 层支持不全,容易在 net.forward() 时抛出 cv::error: OpenCV(4.10.0) ... Unsupported layer type。
- 模型必须导出为 TorchScript(
torch.jit.trace或torch.jit.script),不能用 ONNX 中间格式 -
cv::dnn::blobFromImage()输出默认是 NHWC,但 LibTorch 期望 NCHW,得手动 transpose:用cv::transpose()+cv::dnn::blobFromImage(..., false)关闭自动通道交换 - 推理前务必调用
module.to(torch::kCUDA)和module.eval(),否则 CPU 模式下 batch=1 的 forward 可能卡住数秒
帧率瓶颈常卡在 cv::Mat → torch::Tensor 转换
每次 cv::Mat 转 torch::Tensor 若走 torch::from_blob() + clone(),会触发内存拷贝和 layout 重排,实测 1080p 帧下吞吐掉到 8 FPS。真正低开销的做法是复用 tensor 内存并绕过 clone:
- 预分配固定尺寸的
torch::Tensor input = torch::empty({1, 3, 640, 640}, torch::kFloat32).to(device) - 用
cv::resize(frame, resized, {640, 640})后,直接resized.convertScaleAbs(...)归一化到 [0,1],再用torch::from_blob(resized.data, {1, 3, 640, 640}, torch::kFloat32)绑定内存 - 关键:绑定后立即
.permute({0, 3, 1, 2}).contiguous()调整维度顺序,且不加.clone()
多线程下 cv::VideoCapture 丢帧与同步陷阱
cv::VideoCapture::read() 是阻塞调用,单线程串行读帧+推理必然拖垮 FPS。但盲目开两个线程(读帧线程 + 推理线程)会因 cv::Mat 共享数据导致段错误 —— cv::Mat 的引用计数在跨线程传递时不可靠。
- 必须用
cv::Mat frame.clone()复制数据后再传给推理线程,不能传原始frame引用 - 推荐用
std::queue<:shared_ptr>></:shared_ptr>+std::mutex管理帧队列,容量设为 3,满则cap.grab()跳过当前帧(避免缓冲区堆积) - 别用
cv::CAP_PROP_BUFFERSIZE调大缓存——USB 摄像头固件通常忽略该参数,反而增加首帧延迟
YOLOv5 输出解析时的 shape 误判
TorchScript 模型输出通常是 torch::Tensor output,shape 为 [1, 25200, 85](YOLOv5s),但新手常误以为第 2 维是“框数量”,实际它是 anchor * grid 的固定展平长度。真正有效检测需按置信度阈值过滤:
立即学习“C++免费学习笔记(深入)”;
- 先取
output.squeeze(0)得到[25200, 85],再用output.index({torch::indexing::Slice(), 4})提取第 4 列(objectness) - 用
torch::nonzero(conf_scores > 0.5f)获取有效索引,再切片output.index({valid_idx, torch::indexing::Slice()}) - 坐标解码必须还原归一化:x,y,w,h 需乘以原图宽高(不是 640),且 x,y 是中心点,要算
x - w/2才是左上角
漏掉最后一步,画出来的框永远偏右下 —— 这个细节在 C++ 里没有 Python 的 auto-broadcasting 救你。


















