ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

RetinaFace C++ ONNX推理实战:PyTorch转ONNX与部署全链路

RetinaFace C++ ONNX推理实战:PyTorch转ONNX与部署全链路 简介这是一份面向计算机视觉学习者与开发者的RetinaFace算法C工程实现将人脸检测模型转换为ONNX格式后完成跨平台推理可用于人像摄影、智能监控、安全验证等场景也适合作为毕业设计或技术研究的实践基础。压缩包共12个文件约892KB以cpp与h源码为主配合png示例图、txt与md说明文档涵盖模型加载、图像预处理、前向推理与内存管理等核心环节并附有CMake构建配置与第三方依赖目录便于直接编译运行。已有62人学习下载。读者可借此理解深度学习模型转换与C推理部署的完整链路掌握OpenCV图像处理与ONNX引擎调用的配合方式并通过输入输出示例图直观验证检测效果为后续调整算法参数、优化推理速度或迁移到嵌入式设备提供可复用的代码骨架。1. RetinaFaceCONNX推理实现从 PyTorch 权重到 C 可执行文件的那条链路拿到RetinaFaceCONNX推理实现.zip这个标题很多人第一反应是「人脸检测我已经会了不就是调个库吗」。真到工程里你会发现Python 侧insightface一行FaceAnalysis就能出框可一旦要嵌进 C 桌面端、游戏客户端或者边缘盒子里Python 那套依赖立刻变成负担。RetinaFace 本身是个很扎实的单阶段人脸检测器五个关键点加置信度一起回归小脸和侧脸都比传统 Haar、MTCNN 稳。问题在于训练侧是 PyTorch部署侧是 C中间必须有一座桥这座桥通常就是 ONNX。这个标题讲的就是这条链路把 RetinaFace 的 PyTorch 权重导出成 ONNX用 ONNX Runtime 的 C API 加载自己写预处理、后处理、NMS最后编出一个不依赖 Python 的可执行程序。适合两类人一类是做 C 客户端、需要本地人脸检测但不想拖 Python 运行时的工程师另一类是想搞懂「模型部署到底在部署什么」的算法同学。热搜里pytorch转onnx、推理引擎、onnx怎么运行这几个词基本就是这条链路的三个卡点。下面按我实际做过的顺序拆开讲参数和坑都给到能直接抄的程度。2. 先把 RetinaFace 的输入输出钉死ONNX 导出前的三个决定导出之前如果不把输入输出形状、动态轴、后处理归属想清楚后面 C 侧会反复返工。这一步不是走流程是决定整个工程结构的地方。2.1 为什么输入固定成 1x3x640x640 而不是动态尺寸RetinaFace 原始实现里输入尺寸是可变的det_size传多少就 resize 到多少。但导出 ONNX 时如果保留动态 H/WONNX Runtime 在 C 侧每次都要重新推断 shape而且很多算子在不同尺寸下走的分支不一样容易出玄学问题。我一般直接固定成1x3x640x640理由有三条一是 640 对绝大多数监控、门禁、桌面场景够用小脸召回也还行二是固定 shape 后 ORT 能做更彻底的内存规划和算子融合推理更稳三是 C 侧预处理只需要写一套 resize 逻辑不用处理任意比例。代价是极端宽高比的图会被拉伸变形。解决办法是在预处理里做 letterbox保持比例补灰边后处理再把坐标映射回原图。这个后面 3.1 会写。2.2 导出时把 NMS 留在模型外别塞进 ONNX很多教程图省事用torch.onnx.export时把 NMS 也 trace 进去。我不建议这么干。原因很直接ONNX 里的 NMS 算子NonMaxSuppression在不同推理引擎上实现差异大阈值是常量还是输入、IOU 计算细节都可能不一样一旦换引擎结果就对不上。更稳的做法是模型只输出原始预测分类置信度、框回归、关键点回归NMS 在 C 里用几十行代码自己写。这样阈值可调、可调试、可打日志出问题能定位。2.3 导出脚本与关键参数下面是我常用的导出脚本基于 RetinaFace 的mobilenet0.25或resnet50骨干都适用核心是opset和dynamic_axes两个参数。import torch import torch.onnx # model 为已加载权重的 RetinaFace 实例eval 模式必须开 model.eval() dummy torch.randn(1, 3, 640, 640) torch.onnx.export( model, dummy, retinaface.onnx, opset_version11, # 11 对 NMS/Resize 支持成熟别盲目上 17 input_names[input], output_names[cls, bbox, kps], # 三个头分开命名C 侧好取 dynamic_axesNone, # 固定 shape不设动态轴 do_constant_foldingTrue, # 常量折叠减小图体积 )逻辑说明eval()关掉 BN 和 dropout 的训练行为否则导出结果和推理不一致这是最常见的翻车点。opset_version11是个平衡点再低一些算子不支持再高一些部分推理引擎兼容性反而下降。output_names一定要显式命名C 侧拿输出靠名字而不是索引可读性和可维护性差很多。dynamic_axesNone配合固定 dummy导出的就是静态图。参数说明do_constant_foldingTrue会把能提前算的常量算掉图更小如果导出后 ORT 报某个算子不支持先降 opset 再考虑改模型结构不要一上来就换引擎。导出完用onnx.checker.check_model过一遍再用onnxruntimePython 版跑一张图和 PyTorch 输出对比误差在 1e-3 量级才算过。3. C 侧预处理与后处理真正决定精度的地方模型导出只是开始C 里预处理和后处理写错精度能掉一大截而且这种错不会报异常只会让你觉得「模型怎么这么差」。3.1 letterbox 预处理保持比例补边// 输入 src 为 cv::Mat BGR输出 blob 为 1x3x640x640 的 float 向量 cv::Mat letterbox(const cv::Mat src, int dst_w, int dst_h, float scale) { int w src.cols, h src.rows; scale std::min(dst_w * 1.0f / w, dst_h * 1.0f / h); int nw int(w * scale), nh int(h * scale); cv::Mat resized; cv::resize(src, resized, cv::Size(nw, nh)); cv::Mat canvas(dst_h, dst_w, CV_8UC3, cv::Scalar(114, 114, 114)); int dx (dst_w - nw) / 2, dy (dst_h - nh) / 2; resized.copyTo(canvas(cv::Rect(dx, dy, nw, nh))); return canvas; }逻辑说明scale取宽高缩放比的较小值保证整图能塞进目标尺寸。补边颜色用 114 灰和很多检测框架默认一致减少分布偏移。dx/dy是补边偏移后处理映射坐标时必须减掉否则框会整体偏移。参数说明dst_w/dst_h固定 640补边值 114 可改成 0但要和训练时一致。归一化在转 blob 时做(pixel - 127.5) / 128.0这是 RetinaFace 训练时的标准归一化别用 ImageNet 的 mean/std会掉点。3.2 解码三个输出头与手写 NMSRetinaFace 输出三个张量cls形状1xNx2bbox形状1xNx4kps形状1xNx10N 是所有特征层 anchor 总数。解码时先按置信度阈值筛再做 NMS。struct Face { float score; cv::Rect2f box; float kps[10]; }; std::vectorFace decode(float* cls, float* bbox, float* kps, int num, float conf_thres, float nms_thres, float scale, int dx, int dy) { std::vectorFace cand; for (int i 0; i num; i) { float s cls[i * 2 1]; // 前景置信度 if (s conf_thres) continue; Face f; f.score s; // bbox 为相对 anchor 的偏移需按 anchor 中心与步长还原 // 此处省略 anchor 生成按实际特征图 stride 计算 cand.push_back(f); } // 按 score 降序逐框抑制 IOU 大于阈值的 std::sort(cand.begin(), cand.end(), [](const Face a, const Face b){ return a.score b.score; }); std::vectorFace keep; std::vectorbool removed(cand.size(), false); for (size_t i 0; i cand.size(); i) { if (removed[i]) continue; keep.push_back(cand[i]); for (size_t j i 1; j cand.size(); j) { if (removed[j]) continue; if (iou(cand[i].box, cand[j].box) nms_thres) removed[j] true; } } return keep; }逻辑说明cls[i*21]取的是前景通道背景通道是i*2别取反。NMS 用经典的贪心抑制先按分数排序保留高分框并抑制与它 IOU 超阈的框。坐标还原时先减补边偏移dx/dy再除以scale映射回原图。参数说明conf_thres一般 0.5 起步召回不够降到 0.3nms_thres0.4 是常用值人脸密集场景可提到 0.5。anchor 生成必须和训练配置完全一致stride 通常是 8/16/32每个位置 2 个 anchor这些数字对不上框会全乱。3.3 ONNX Runtime C 会话初始化Ort::Env env(ORT_LOGGING_LEVEL_WARNING, retinaface); Ort::SessionOptions opts; opts.SetIntraOpNumThreads(4); opts.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); Ort::Session session(env, retinaface.onnx, opts); // 取输入输出名 Ort::AllocatorWithDefaultOptions alloc; auto in_name session.GetInputNameAllocated(0, alloc); auto out0 session.GetOutputNameAllocated(0, alloc);逻辑说明SetIntraOpNumThreads控制单算子并行线程数桌面端 4 线程通常够ORT_ENABLE_ALL开启图优化能融合的算子会融合。输入输出名用GetInputNameAllocated拿别硬编码字符串模型一改就崩。参数说明线程数不是越大越好超过物理核数会因调度开销变慢实测 4 到 8 之间比较稳。如果部署在核显或边缘设备考虑用OpenVINO或NCNN后端ONNX 转过去再跑纯 CPU 上 ORT 已经够用。4. 避坑与排查那些不报错但结果不对的坑这一章是我踩过的血泪经验每条都按现象、原因、解决写照着排查能省很多时间。4.1 检测框整体偏移或缩放不对现象框能出来但位置整体偏右下或大小差一截。原因letterbox 的补边偏移dx/dy没在后处理里减掉或者scale用反了乘除搞混。解决在解码后统一做x (x - dx) / scaley (y - dy) / scale四个坐标都要处理关键点十个值同理。4.2 置信度普遍偏低、漏检多现象明明是人脸分数只有 0.2 到 0.3。原因归一化用错常见是用了 ImageNet 的 mean/std 而不是(x-127.5)/128或者输入通道顺序搞成 RGB而模型训练用的是 BGR。解决确认归一化公式和通道顺序OpenCV 读进来默认 BGR直接转 blob 即可别多此一举cvtColor。4.3 ONNX Runtime 加载报算子不支持现象Session构造抛异常提示某算子不支持。原因导出 opset 太高或者用了 ORT 当前版本没实现的算子。解决先把 opset 降到 11 重新导出仍不行就查 ORT 版本对应的算子支持列表必要时把该算子替换成等价组合。别急着换引擎先确认是不是导出参数问题。4.4 多线程下结果串图现象并发调用时偶尔框跑到别的图上。原因Ort::Session本身线程安全但如果你复用了同一个输入Ort::Value或输出缓冲就会串。解决每次推理新建输入 tensor输出用GetOutputNameAllocated后立即拷贝到自己的结构体不要跨线程共享可变缓冲。4.5 小脸召回明显低于 Python 版现象同一张图 Python 出 10 张脸C 只出 6 张。原因预处理 resize 插值方式不同Python 侧可能用cv2.INTER_LINEARC 用了INTER_NEAREST或者置信度阈值、NMS 阈值不一致。解决对齐插值方式为INTER_LINEAR把两侧阈值打印出来比对通常对齐后差距在 1 到 2 张以内。5. 进阶把推理耗时压下来和验证结果一致性工程落地到最后拼的是速度和可验证性。速度上我一般先测单张耗时再决定优化方向。用std::chrono在推理前后打点跑 100 次取平均排除首次加载的冷启动。640x640 的 mobilenet0.25 骨干桌面 CPU 单张通常在 15 到 40 毫秒resnet50 会到 80 毫秒以上。如果不够快优先换轻量骨干其次考虑量化。ONNX 量化 int8 是热搜里常出现的词但我要泼盆冷水RetinaFace 这种带回归头的检测模型int8 量化后框回归精度容易掉尤其是关键点。真要做用 ORT 的quantize_static配合校准集校准集要覆盖你的实际场景量化后必须重新跑一遍精度对比别只看速度。我一般只在纯分类模型上放心用 int8检测模型更倾向 fp16 或者干脆换更小的骨干。验证一致性是另一个容易被忽略的环节。我的习惯是固定一批测试图Python 侧和 C 侧各跑一遍把框坐标、分数、关键点导出成文本写个脚本算最大绝对误差。框坐标误差在 1 到 2 像素、分数误差在 1e-3 以内就算对齐了。超过这个范围一定是预处理或后处理某一步不一致回去按第 4 章逐条查。还有个具体技巧把中间结果 dump 出来。预处理后的 blob 存成二进制C 和 Python 各存一份用numpy.fromfile读进来比 diff能直接定位是预处理问题还是模型问题。这个习惯帮我省过好几次通宵。做部署这行后悔药就是日志和中间结果提前留好出问题才不慌。希望帮到你。本文还有配套的精品资源点击获取
返回列表