十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

YOLOv7模型部署:OpenCV DNN与ONNXRuntime双路径解析

YOLOv7模型部署:OpenCV DNN与ONNXRuntime双路径解析 简介基于OpenCV DNN和ONNXRuntime部署YOLOv7的完整资源包面向计算机、电子信息工程、数学等专业学生适用于课程设计、期末大作业或毕业设计阶段的算法部署参考。资源内含Python与C两套部署源码分别对应ONNXRuntime和OpenCV DNN两种推理引擎并附有模型获取方式与说明文档方便学习者快速复现目标检测流程。包体共12个文件涵盖py脚本、cpp源文件、测试图片、txt说明、markdown文档及类别映射文件压缩包仅913KB轻量实用。目前已有1952人学习下载。通过阅读文档与源码使用者可掌握模型加载、图像预处理、推理输出解析等关键环节借助不同测试图验证检测效果为后续二次开发或迁移到实际项目奠定基础。1. OpenCV DNN和ONNXRuntime部署YOLOv7同一个包里的两条推理路径拿到一个YOLOv7部署包里面同时有OpenCV DNN和ONNXRuntime两套推理入口这对课程设计和实际项目都很有价值。OpenCV DNN胜在API简单、依赖少装个opencv-python就能跑ONNXRuntime则能利用图优化和多线程加速C工程里更稳。很多初学者只会在PyTorch里调用权重一碰到部署就懵就是缺这份“切换推理框架”的经验。这个包把模型、Python脚本、C工程和说明文档都备齐了适合用来理解从ONNX导出到不同后端落地的完整链路。2. YOLOv7模型导出从PyTorch权重到ONNX格式2.1 部署场景下为什么不用.pt文件YOLOv7训练完给的是PyTorch的.pt权重里面包含网络结构、优化器状态和ema权重。直接拿去推理不是不行但部署机器上得装完整的PyTorch还要匹配训练时的CUDA版本环境容易炸。OpenCV DNN和ONNXRuntime都读不了.pt只能读ONNX或者OpenVINO等中间格式。ONNX把计算图固化成节点序列权重也打包进文件里部署时只需要推理引擎按图执行不需要反向传播相关的元信息。另一方面ONNX是开放标准转一份模型就能在多种框架间切换这也是资源包给你保留yolov7.onnx而不是让你现场去转的原因。需要提醒的是并不是随便用torch.onnx.export就能转成功。YOLOv7的检测头里有网格生成、anchor拼接等动态操作官方仓库专门写了export.py来处理。它会把一些常量固化到图中让推理时不用重复生成网格。2.2export.py导出命令与参数资源包里的模型应该已经转好但如果你想用自己训练后的权重重新导出命令如下python export.py --weights yolov7.pt --grid --end2end --simplify \ --topk-all 100 --iou-thres 0.45 --conf-thres 0.25 \ --img-size 640 640 --max-wh 640我一般会保留--grid而不是去掉它因为不带--grid时模型会把三个尺度的输出concat成一个Tensor虽然看起来简单但后续在OpenCV DNN里解码反而要多一步拆分。--end2end是另一个方向把NMS也放进模型输出直接是框。但这个选项会让模型包含一个动态的NMS节点OpenCV DNN目前对这类动态输出的支持不稳定所以如果你准备用OpenCV DNN就不要加--end2end。ONNXRuntime倒是两边都能跑。下面把几个容易调错的参数整理成表格参数作用说明--img-size推理输入尺寸一般保持训练尺寸如640x640--grid保留网格输出输出三张特征图解码灵活--end2end在模型内做NMS输出框但OpenCV DNN支持差--simplify调用onnx-simplifier减少冗余节点提升推理速度--iou-thresNMS的IoU阈值端到端模型预设值--conf-thres置信度阈值会被固化为常量后续不能动态调--max-wh框坐标最大值配合--topk-all限制检测数量注意--conf-thres和--iou-thres只有在--end2end时才影响模型内的NMS如果你走--grid三输出模式这两个参数在后处理阶段自己指定。2.3 导出后检查输出张量形状导出完成后先用ONNXRuntime快速验证一下模型能否加载以及输出形状是否符合预期。下面这段脚本可以打印每个输出的shapeimport onnxruntime as ort import numpy as np sess ort.InferenceSession(yolov7.onnx, providers[CPUExecutionProvider]) x np.random.rand(1, 3, 640, 640).astype(np.float32) out sess.run(None, {images: x}) for i, o in enumerate(out): print(i, o.shape)如果是三输出模式你会看到(1, 255, 80, 80)、(1, 255, 40, 40)、(1, 255, 20, 20)这样的形状。255是单个anchor的通道数x、y、w、h、objectness再加上80类共58085而每个尺度有3个anchor所以是85*3255。如果是端到端模式输出可能是(1, 100, 6)或者(1, 6, 100)100对应--topk-all。测试时如果打印出nan或者形状不对先检查opset版本一般ONNXRuntime需要opset 11。2.4 动态轴与opset的坑导出ONNX时默认输入形状是固定的[1, 3, 640, 640]。如果你后续想用ONNXRuntime处理动态尺寸需要给输入加上动态轴dynamic_axes { images: {0: batch, 2: height, 3: width}, output: {0: batch} } torch.onnx.export(model, dummy_input, yolov7_dynamic.onnx, dynamic_axesdynamic_axes, opset_version12)但加了动态轴后OpenCV DNN基本没法加载因为它的shape推断是静态的。所以资源包里通常会放两个版本固定640x640的给OpenCV DNN用动态版本的给ONNXRuntime用。还有一个常见问题是模型里有些算子导出后是com.microsoft域下的比如GridSampleONNXRuntime需要torch.onnx.export时设置opset_version12以上如果导出后报Unsupported Operator先看算子版本再考虑升级onnxruntime到最新。3. Python端OpenCV DNN推理预处理、解码与坐标映射3.1 加载ONNX模型与选择推理后端OpenCV DNN读取ONNX模型非常直接readNetFromONNX一行搞定。但加载后要显式指定后端和目标设备否则OpenCV默认使用DNN_BACKEND_OPENCV加DNN_TARGET_CPU速度可能不是最优。如果你编译了OpenCV的CUDA支持可以换成CUDA后端YOLOv7的卷积在CUDA上能快不少。需要留意的是并非所有自定义算子都会被CUDA后端支持所以最好先用CPU跑通再切CUDA验证一次。import cv2 net cv2.dnn.readNetFromONNX(yolov7.onnx) net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)代码逻辑setPreferableBackend控制运行时优化和算子实现setPreferableTarget决定在CPU还是GPU上执行。对于资源包里的main.py这两个函数已经默认设置好你换模型时不需要动它们。3.2 letterbox预处理保持长宽比的resizeYOLOv7训练时使用640x640输入但直接cv2.resize会把图像拉伸导致目标变形检测框偏差。所以部署代码里都是先按比例缩放再在两边补灰边这个操作常被称为letterbox。下面是一个标准实现def letterbox(img, new_shape(640, 640), color(114, 114, 114)): h, w img.shape[:2] r min(new_shape[0] / h, new_shape[1] / w) new_h, new_w int(round(h * r)), int(round(w * r)) dw, dh (new_shape[1] - new_w) / 2, (new_shape[0] - new_h) / 2 if (new_h, new_w) ! (h, w): img cv2.resize(img, (new_w, new_h), interpolationcv2.INTER_LINEAR) top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) left, right int(round(dw - 0.1)), int(round(dw 0.1)) img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, valuecolor) return img, r, (dw, dh)这里返回r是缩放比例(dw, dh)是灰边偏移。后处理把模型输出的坐标转换成原图坐标时必须用这两个值做逆运算。有个细节copyMakeBorder的边界尺寸用round(dh-0.1)这种写法是为了避免偶数像素时上下边差1个像素实际OpenCV的blobFromImage内部对边缘处理并不敏感但坐标映射时如果dw、dh是小数就要小心。3.3 blobFromImage参数设置OpenCV DNN通过blobFromImage把图像转成网络输入的4维blob。很多人在这一步调不通多半是参数没对齐blob cv2.dnn.blobFromImage(img_letter, 1/255.0, (640, 640), (0, 0, 0), swapRBTrue, cropFalse) net.setInput(blob) outs net.forward() # list, 每个元素是 (1, 255, h, w)参数说明参数值含义imageletterbox后的图像已经是640x640scalefactor1/255.0像素值归一化到[0,1]size(640,640)网络要求的输入尺寸mean(0,0,0)数据集的均值YOLO系列一般不用减均值swapRBTrue原图是BGR模型训练用RGB所以交换通道cropFalse不裁剪直接用letterbox结果swapRBTrue是个容易漏的坑。OpenCV读图默认是BGR而YOLOv7训练时用RGB。如果你不交换通道检测框位置会对但类别会错乱因为颜色通道影响特征提取。另外scalefactor必须在mean之前执行OpenCV内部会先减mean再乘scale所以这里mean是0就不会出问题。提示如果你的模型输入名不是images需要用net.getUnconnectedOutLayersNames()确认。3.4 解码三个特征图anchor、置信度与坐标映射拿到outs后需要解码。以非end2end的三输出模型为例每个out的形状是(1, 255, h, w)先reshape成(1, 3, h, w, 85)然后做sigmoid再按YOLOv7的公式换算成框坐标。下面是一个numpy版解码函数可以直接替换资源里的main.py测试import numpy as np def decode(outs, anchors, strides, num_classes80, conf_thres0.25): dets [] for out, anchor, stride in zip(outs, anchors, strides): # out: (1, 255, h, w) b, c, h, w out.shape num_anchors len(anchor) // 2 out out.reshape(b, num_anchors, -1, h, w) out out.transpose(0, 1, 3, 4, 2) # (b, num_anchors, h, w, 85) out 1 / (1 np.exp(-out)) # sigmoid # 生成网格坐标 grid_x, grid_y np.meshgrid(np.arange(w), np.arange(h)) grid np.stack((grid_x, grid_y), axis-1) # (h, w, 2) # anchor列表转为 (3, 2) anchors np.array(anchor).reshape(-1, 2) xy (out[..., 0:2] * 2 - 0.5 grid) * stride wh (out[..., 2:4] * 2) ** 2 * anchors[:, None, None, :] conf out[..., 4:5] cls out[..., 5:] scores conf * cls # (b, num_anchors, h, w, 80) boxes_xy np.concatenate((xy, wh), axis-1) conf_m np.max(scores, axis-1) class_ids np.argmax(scores, axis-1) # 过滤低置信度 mask conf_m conf_thres for b_i in range(b): mask_b mask[b_i] if np.any(mask_b): box boxes_xy[b_i][mask_b] cls_id class_ids[b_i][mask_b] score conf_m[b_i][mask_b] dets.append((box, cls_id, score)) return dets这段代码的逻辑是先对网络输出做sigmoid再把位置偏移xy通过*2 - 0.5 grid还原到特征图坐标乘以stride得到640坐标系下的中心点宽高wh则通过(*2)^2 * anchor来还原。conf * cls是把objectness与类别概率相乘得到每个类别的最终得分最后用conf_thres过滤。注意这里没有做NMS资源里的main.py在拿到dets后还会调用cv2.dnn.NMSBoxes或者自己实现一个非极大值抑制NMS的IoU阈值一般设在0.45到0.5之间。坐标映射部分因为之前做了letterbox现在得到的xywh是letterbox图像上的坐标。要变回原图坐标x1 (x_center - w / 2 - dw) / r y1 (y_center - h / 2 - dh) / r x2 (x_center w / 2 - dw) / r y2 (y_center h / 2 - dh) / r其中dw、dh、r就是letterbox函数返回的灰边偏移和缩放比例。四舍五入后再用cv2.rectangle画框就行。4. C端ONNXRuntime部署会话配置与内存布局4.1 初始化Session设置线程数与图优化ONNXRuntime的C接口和Python差别不大核心是Ort::Session。资源包里的main.cpp使用的是旧版API还是新版API不一定但原理一致。以一个可编译的示例说明#include onnxruntime_cxx_api.h #include opencv2/opencv.hpp #include vector #include iostream int main() { Ort::Env env(ORT_LOGGING_LEVEL_WARNING, yolov7_ort); Ort::SessionOptions opt; opt.SetIntraOpNumThreads(4); opt.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); opt.SetSessionLogSeverityLevel(3); Ort::Session session(env, yolov7.onnx, opt); Ort::AllocatorWithDefaultOptions allocator; auto input_name session.GetInputNameAllocated(0, allocator); auto input_shape session.GetInputTypeInfo(0).GetTensorTypeAndShapeInfo().GetShape(); std::cout input name: input_name.get() std::endl; return 0; }这里SetIntraOpNumThreads控制线程数对于多核CPU设置成物理核数比较合理ORT_ENABLE_ALL会做算子融合和常量折叠一般能带来15%-30%的加速。如果你的模型是动态形状GetInputTypeInfo().GetShape()可能返回-1这在后面创建输入张量时需要注意。4.2 输入张量的构造与内存布局ONNXRuntime要求输入是连续内存的std::vectorfloat不能直接塞cv::Mat。所以我们需要先把图像转成blob再拷贝到vector里。注意布局是NCHW即[1, 3, 640, 640]通道维在最前面。下面是把cv::Mat填充到输入buffer的常见写法cv::Mat img cv::imread(images/bus.jpg); cv::Mat letter; // 这里letterbox函数与Python端保持一致可从资源包main.cpp中获得 std::tie(letter, std::ignore, std::ignore) letterbox(img, 640, 640); cv::Mat blob cv::dnn::blobFromImage(letter, 1/255.0, cv::Size(640, 640), cv::Scalar(0,0,0), true, false); // blob是NCHW布局且内存连续 std::vectorfloat input(blob.beginfloat(), blob.endfloat());片段里用cv::dnn::blobFromImage直接生成NCHW的浮点数据true对应Python里的swapRBTruefalse对应cropFalse。这样省去了手动split通道的麻烦。需要留意的是blob.beginfloat()依赖Mat连续性这里生成的blob是连续的所以可以安全使用。如果你在别处看到用memcpy(input.data(), blob.data, ...)的写法效果一样但要注意blob.size()必须等于1*3*640*640。4.3 创建Ort::Value并执行RunONNXRuntime的Run函数接受输入输出名列表和Ort::Value。下面是把std::vectorfloat包装成Ort::Value并运行std::vectorint64_t input_shape {1, 3, 640, 640}; Ort::MemoryInfo mem_info Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor Ort::Value::CreateTensorfloat( mem_info, input.data(), input.size(), input_shape.data(), input_shape.size()); const char* input_names[] {images}; const char* output_names[] {output}; // 以实际模型名为准 auto output_tensors session.Run(Ort::RunOptions{nullptr}, input_names, input_tensor, 1, output_names, 1); auto output_tensor output_tensors.front(); auto output_info output_tensor.GetTensorTypeAndShapeInfo(); std::vectorint64_t output_shape output_info.GetShape(); size_t total output_info.GetElementCount(); float* output_data output_tensor.GetTensorMutableDatafloat();输出名很重要。导出ONNX时输入名通常叫images但输出名可能是output、345或模型节点名字。如果你不确定可以用session.GetOutputNameAllocated(i, allocator)打印出来。上述代码没有处理多输出的情况如果三输出模型output_names要对应三个名字分别取数据。C端后处理逻辑和Python一样只是把numpy换成纯for循环。提示Ort::Value::CreateTensor要求数据指针在内存中必须连续不要用std::vectorstd::vectorfloat这种嵌套结构。4.4 动态形状与NMS的取舍端到端ONNX模型的输出是动态的比如(1, num_dets, 6)其中num_dets在运行前未知。C端读取output_shape[1]后再用这个值去遍历框。而三输出模型的形状是固定的CPU上更友好。如果你要在C工程里集成建议先用三输出模式把NMS放在自己的C代码里这样调试起来更容易。这里给出一个两套方案的对比帮你决定用哪种对比项OpenCV DNNONNXRuntime依赖opencv-python即可需要onnxruntime库动态形状支持较弱部分算子报错支持较好图优化有限ORT_ENABLE_ALL较强的融合优化CPU多线程内部自动可手动设置线程数CUDA支持需要编译OpenCV CUDA安装onnxruntime-gpu5. 部署中的阈值与后处理细节按场景调优阈值不是调一次就完事的。图像场景变化后同样的置信度阈值可能让检测框要么多到重叠要么漏检。常见做法是把conf_thres改成从命令行传入或者做一个滑动条方便实时调试。我一般会保留一份调试模式当--debug开启时把所有未过滤的框连同分数画出来用不同透明度叠加快速判断是阈值问题还是模型问题。另一个容易踩的坑是NMS的IoU阈值。YOLOv7的默认NMS是class-agnostic还是class-wise决定了重叠目标会不会被误删。OpenCV DNN的cv2.dnn.NMSBoxes只能做class-wise的NMS它接受scores列表底层会按类别分别抑制。如果你希望做class-agnostic就得自己写循环把不同类别的框放在一起做IoU过滤。在拥挤场景下比如公交车上的密集行人class-agnostic通常漏检更少但可能把两个不同类别的重叠框合并成一个需要根据业务权衡。还有一个值得说的技巧是letterbox后处理的一致性。C端和Python端必须用同一套round规则否则同一个模型在两个端上框的位置会偏移一两个像素。资源包里的两个main文件虽然语言不同但letterbox和dw/dh的计算公式一致这能在跨语言联调时省下很多排查时间。如果结果有差异优先检查copyMakeBorder的上下、左右边是不是偶数分配以及r是否取了min而不是max。最后把--end2end模型和普通三输出模型的输出shape混用是部署时常犯的错。如果你只有一个ONNX文件启动时先用Python打印一次输出shape写进运行日志后续切换推理框架时对照shape就能立刻判断是解码问题还是模型问题。比如端到端模型输出是(1, 100, 6)而三输出模型是三个(1, 255, 20, 20)这两者在代码里完全是两种解析方式。本文还有配套的精品资源点击获取
返回列表