YOLO11 模型 C++ 部署:ONNX 导出、NMS 判别与推理实战
YOLO11 模型 C++ 部署流程涵盖 ONNX 导出配置、NMS 节点判别及推理环境搭建。核心在于使用 nms=True 参数导出 end2end 模型,并通过输出张量形状验证是否包含 NonMaxSuppression 算子。针对无 NMS 的原始头模型,需在 C++ 端手动实现后处理逻辑,同时解决 CMake 配置及依赖库兼容性问题以确保稳定运行。

YOLO11 模型 C++ 部署流程涵盖 ONNX 导出配置、NMS 节点判别及推理环境搭建。核心在于使用 nms=True 参数导出 end2end 模型,并通过输出张量形状验证是否包含 NonMaxSuppression 算子。针对无 NMS 的原始头模型,需在 C++ 端手动实现后处理逻辑,同时解决 CMake 配置及依赖库兼容性问题以确保稳定运行。

现象:C++ 推理出现'满屏框'或'0 框'。
根因:导出的 ONNX 多为不带 NMS 的原始头,输出形状常见 [1, 84, 8400](84=4+80),模型不会帮你做阈值与 NMS;C++ 若不做后处理,就会'满屏框'。

相对地,带 NMS 的 ONNX(end2end) 会直接输出最终框,形状常见 [N,6] 或 [1,N,6](x1 y1 x2 y2 score class),C++ 只需画框。
YOLO 在多尺度(如 80×80、40×40、20×20)上为每个网格点预测很多候选框。同一目标附近会出现大量位置相近、得分也不低的候选。NMS(Non-Maximum Suppression,非极大值抑制)的任务就是:在重叠的候选中,只保留那个最靠谱的(分数最高),把与之高度重叠的重复框删掉。
# boxes: [N, 4] in x1y1x2y2
# scores: [N]
# thr_conf: 置信度阈值(如 0.25)
# thr_iou: NMS IoU 阈值(如 0.45)
keep = []
idxs = argsort(scores, descending=True)
while idxs not empty:
i = idxs[0]
keep.append(i)
ious = IoU(boxes[i], boxes[idxs[1:]])
idxs = idxs[1:][ious <= thr_iou]
return keep
常见做法是按类别分别做 NMS(class-wise NMS)。Ultralytics 默认如此。
| idx | score | box(x1,y1,x2,y2) |
|---|---|---|
| 0 | 0.90 | (10,10,50,50) |
| 1 | 0.85 | (12,12,49,49) |
| 2 | 0.60 | (200,200,260,260) |
| 3 | 0.40 | (205,205,258,258) |
必须从
.pt权重导出;不能在现有.onnx上'打开 NMS'。
from ultralytics import YOLO
m = YOLO(r"path/to/best.pt") # 训练得到的 .pt 或 yolov8n.pt
m.export(format="onnx", imgsz=640, # 与训练一致 dynamic=False, simplify=True, # 安装了 onnxsim 就 True,没有也行 nms=True, # ★★★ 关键:将 NonMaxSuppression 写进图里 conf=0.25, # 内置置信阈 iou=0.45, # 内置 NMS IoU opset=12 # 12/13/16 均可,保持一致即可)
导出成功后,继续下一节检查是否真的带 NMS。
import onnxruntime as ort
sess = ort.InferenceSession("best.onnx", providers=["CPUExecutionProvider"])
print([(o.name, o.shape) for o in sess.get_outputs()]) # 看到 [..., 6] / [..., 7] => 带 NMS
# 看到 84/85 出现在任一维 => 原始头(不带 NMS)

#include <onnxruntime_cxx_api.h>
#include <iostream>
#include <vector>
#include <filesystem>
static std::string Shape2Str(const std::vector<int64_t>& v){
std::string s = "[";
for(size_t i = 0; i < v.size(); ++i){
s += std::to_string(v[i]);
if(i + 1 < v.size()) s += ',';
}
return s += ']';
}
static bool LikeNMS(const std::vector<int64_t>& shp){
return shp.size() >= 2 && (shp.back() == 6 || shp.back() == 7);
}
static bool LikeRaw(const std::vector<int64_t>& shp){
for(auto d : shp)
if(d == 84 || d == 85) return true;
return false;
}
static void InspectOutputs(const Ort::Session& sess){
const size_t n = sess.GetOutputCount();
bool bAnyN = false, bAnyR = false;
for(size_t i = 0; i < n; ++i){
Ort::AllocatorWithDefaultOptions alloc;
auto name = sess.GetOutputNameAllocated(i, alloc);
auto shp = sess.GetOutputTypeInfo(i).GetTensorTypeAndShapeInfo().GetShape();
std::cout << "out[" << i << "] " << (name ? name.get() : "(null)") << " shape=" << Shape2Str(shp) << "\n";
bAnyN |= LikeNMS(shp);
bAnyR |= LikeRaw(shp);
}
if(bAnyN) std::cout << "[detect] ✅ 带 NMS 的 end2end 输出\n";
else if(bAnyR) std::cout << "[detect] ⚠️ 原始头(不带 NMS)\n";
else std::cout << "[detect] ℹ️ 非常见形状,建议用 Netron 检查是否含 NonMaxSuppression\n";
}
int main(int argc, char** argv){
if(argc < 2){
std::cout << "Usage:\n " << argv[0] << " <onnx_model>\n";
return 0;
}
std::filesystem::path strOnnxPath = argv[1];
if(!std::filesystem::exists(strOnnxPath)){
std::cerr << "[error] model not found: " << strOnnxPath;
return 1;
}
// ... truncated
}
(此处省略后续内容,基于原文逻辑推断)
(此处省略后续内容)
(此处省略后续内容)
(此处省略后续内容)
(此处省略后续内容)
(此处省略后续内容)
(此处省略后续内容)

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online