YOLOv8 C++部署实战:基于ONNX Runtime的检测、分割与OBB推理指南

发布时间:2026/10/1 13:00:44
YOLOv8 C++部署实战:基于ONNX Runtime的检测、分割与OBB推理指南 简介这一份以YOLOv8为核心、通过ONNXRuntime和OpenCV完成目标检测与实例分割的工程实践资源面向具备基础视觉开发经验、希望快速将模型部署到实际应用中的开发者。压缩包共52个文件以C源文件和头文件为主辅以示例图像、模型放置说明、CMake构建配置及README整体仅7.46MB结构清晰便于直接查阅和二次开发。项目不仅覆盖常规目标检测还提供实例分割与OBB旋转框检测等扩展实现利用ONNXRuntime提升跨平台推理效率OpenCV负责图像读取与结果绘制可帮助理解从模型加载到结果输出的完整链路。目前已有508人学习下载适合需要参考完整工程结构、调试模型推理或构建轻量级视觉识别方案的读者内含可直接运行的示例与目录索引能够有效降低上手成本。1. 从ONNX导出到C推理这份YOLOv8检测/分割资源包能直接落地的地方做过YOLOv8部署的人大概都有这种体验Python里跑得好好的模型,一旦要交到C工程、嵌入式板子或者Windows服务里就变成半个黑匣子——权重有了、torch也有但目标机器没有Python环境只能把模型转成ONNX再拿ONNXRuntime硬啃。这份项目资源把 YOLOv8 目标检测、实例分割两条链路直接做成了 C 源码包附带 ONNX Runtime OpenCV 的完整 CMake 工程打开就能对着跑。项目里你能直接看到yolov8_onnx.cpp、yolov8_seg_onnx.cpp、yolov8_obb_onnx.cpp三套推理实现还有rtdetr_onnx.cpp做对照images 目录备好了 bus.jpg、DOTA_0032.png 这类验证图。适合人群很明确手里有 YOLOv8 的 PyTorch 权重想在 C 环境里快速验证效果或者准备往 RK3588、Jetson 这类边缘设备移植的开发者。它解决的不是训练问题而是「训练完之后怎么高效落地」这一段最容易被忽略的路。2. 先理清三种任务检测/分割/OBB的ONNX输出结构与时序2.1 从 ultralytics 导出 ONNX一条命令和三个必须盯的输出节点动手前得先把模型导出这一步理顺。这个项目包里没有放 PyTorch 权重models目录是个空壳叫put_model_here意思是模型要自己导。常见做法是用 ultralytics 官方包导出命令很简单pip install ultralytics yolo export modelyolov8n.pt formatonnx opset12 imgsz640如果是 Python 接口也可以写from ultralytics import YOLO model YOLO(yolov8n.pt) model.export(formatonnx, opset12, imgsz640, simplifyFalse)导出时三个参数要特别盯紧。第一个是opsetONNXRuntime 对低版本 opset 支持比较保守opset 12 是保险线低于 10 的话某些算子会导出失败第二个是imgsz它决定了模型的输入分辨率训练时用 640 就导出 640导出尺寸跟训练尺寸不一致往往会掉点第三个是simplify默认不开开了会去冗余算子但偶尔会把一些动态 shape 的算子折叠掉导致分割模型的 mask 输出没了所以一开始我建议先不 simplify跑通了再优化。导完以后拿 Netron 打开 ONNX 文件看输出节点。这是整个项目里最重要的一步——因为 C 代码里怎么解析张量完全取决于导出时输出节点的 shape。检测模型一般只有一个输出节点shape 是[1, 84, 8400]分割模型会有两个输出节点除了[1, 116, 8400]还有一个原型 mask 的[1, 32, 160, 160]。如果是 OBB 旋转框模型输出的维度会多几个角度相关参数。这些数字直接决定后处理代码怎么写。2.2 检测、分割、OBB的输出张量shape 决定了解析代码怎么写很多人在 C 推理时翻车不是模型问题是根本没弄懂输出张量的布局。YOLOv8 检测模型的输出是[1, 84, 8400]其中 8400 是三个尺度加起来的目标候选数——80x80 的 6400 个、40x40 的 1600 个、20x20 的 400 个。84 的意思是 4 个边界框坐标加 80 个 COCO 类别得分。这个布局是「通道在前」的也就是要按列来遍历 8400 个候选。模型类型输出节点 shape解析要点检测[1, 84, 8400]第 0-3 行是 cx, cy, w, h第 4-83 行是各类别得分分割[1, 116, 8400][1, 32, 160, 160]前 84 行同检测后 32 行是 mask 系数OBB[1, 88, 8400]常见坐标 类别 角度相关参数具体布局以导出版本为准分割模型的 116 是怎么来的84 加 32这 32 是 mask 系数。最终的分割掩码不是直接输出的得拿 32 个系数去跟原型 mask就是那个[1, 32, 160, 160]做矩阵乘法再经过 sigmoid 才能得到 160x160 的每个类别的掩码。这一步在 C 里就是嵌套循环做乘加代码量不大但维度顺序一旦搞反出来的 mask 就是花的。OBB 模型的输出在不同版本里布局有差异常见的是 88 维多了角度相关编码。项目里的yolov8_obb_onnx.cpp是专门处理这个的DOTA_0032.png 就是遥感旋转框的验证图。2.3 模型选型什么时候用 YOLOv8-ONNX什么时候换 RT-DETR项目里还带了rtdetr_onnx.h和rtdetr_onnx.cpp这提醒了一件很重要的事不是所有场景都该死磕 YOLOv8。RT-DETR 是百度提出的实时端到端检测器最大的特点是不需要 NMS——它的输出直接就是检测结果省掉了后处理里最让人头疼的重复框抑制逻辑。选型逻辑我一般这样把握如果设备是 x86 工控机CPU 推理追求稳定和生态成熟用 YOLOv8-ONNX 加 ONNXRuntime 是稳妥路线如果精度要求高、且设备是新出的边缘盒子RT-DETR 的 transformer 结构在 ONNXRuntime 上跑起来效率未必比 YOLOv8 差尤其它能省掉 NMS 这一步的调参时间。项目同时给了两套实现其实就是让开发者在同一个 CMake 工程里做对比不用来回切代码。当然RT-DETR 的模型导出方式跟 YOLOv8 不太一样得用官方仓库或 ultralytics 对 RT-DETR 的支持接口来导。导出来的 ONNX 输入输出节点命名也跟 YOLOv8 不同项目里rtdetr_onnx.h的接口是单独封了一层的说明作者早就踩过这个坑。3. 搭一套能跑的 C 工程CMake集成OpenCV与ONNXRuntime的落地边界3.1 依赖准备ONNXRuntime 库版本与 OpenCV 的匹配问题这个项目是纯 C 的核心依赖就两个ONNXRuntime 和 OpenCV。ONNXRuntime 推荐直接下 release 包Windows 选onnxruntime-win-x64Linux 选onnxruntime-linux-x64注意别下错了带 GPU 的版本——CPU 版和 GPU 版的头文件虽然一样但链接的库文件名字不同GPU 版还需要 CUDA 和 cuDNN 一堆依赖。OpenCV 这边有个隐形要求项目代码用到了cv::dnn模块来做一些基础的前处理或辅助操作同时在 Windows 上如果用了cv::imshow还需要 OpenCV 的 GUI 模块。用 vcpkg 安装的话一条命令搞定vcpkg install opencv:x64-windows如果是在 Ubuntu 20.04 上搭环境apt 安装的 OpenCV 版本往往偏旧可能不支持项目里用到的某些 API。这个时候我会自己编译 OpenCV注意开启WITH_CUDA和WITH_CUDNN的选项否则就算 ONNXRuntime 换成 GPU 版图像预处理还是在 CPU 上跑整体延迟下不来。版本匹配上见过最典型的坑是ONNXRuntime 1.15 之后Ort::Session的构造方式有小变化如果项目源码是按旧版 API 写的编译会有no matching function的报错。解决办法是优先选 ONNXRuntime 1.14 或 1.13或者反过来改代码适配新版 API。项目里yolov8_onnx.h的封装方式基本是按 1.13-1.15 这个区间写的用这个范围内的版本最省事。3.2 CMakeLists.txt最小可用的链接配置与三个常见报错项目里给了 CMakeLists.txt路径配置好就能用。一个能跑起来的 CMake 配置大致长这样cmake_minimum_required(VERSION 3.12) project(yolov8_onnx) set(CMAKE_CXX_STANDARD 17) find_package(OpenCV REQUIRED) find_package(onnxruntime REQUIRED) add_executable(yolov8_seg_onnx yolov8_seg_onnx.cpp yolov8_seg_onnx.h yolov8_utils.cpp yolov8_utils.h ) target_link_libraries(yolov8_seg_onnx ${OpenCV_LIBS} onnxruntime::onnxruntime )要解释一下onnxruntime::onnxruntime这个 target如果你是通过find_package(onnxruntime)找到的它一般会暴露这个 target如果是手动把 ONNXRuntime 的头文件和 lib 路径加进来的就得自己写include_directories和link_directories。新手最容易在这里挂三个报错非常典型。第一个是fatal error: onnxruntime_cxx_api.h: No such file or directory这就是 include 路径没指对确认 ONNXRuntime 解压目录里的 include 路径是否真的被加进来了。第二个是undefined reference to Ort::Session::Session(...)这是链接阶段没找到 lib 文件Windows 下要同时链接onnxruntime.libLinux 下要链接libonnxruntime.so而且注意 release 和 debug 版本别混。第三个是运行时报DLL load failed或libonnxruntime.so: cannot open shared object file这是运行时动态库没在搜索路径里Windows 把 DLL 拷到 exe 旁边Linux 设LD_LIBRARY_PATH。3.3 目录约定模型放哪、图片放哪、输出写哪项目里models目录写明put_model_here图片都在images下输出结果直接写到当前目录或者指定路径。这个看似不起眼的约定其实决定了你拿到资源包之后能不能第一时间跑通。yolov8_seg_onnx.cpp里通常会有类似这样的路径常量const std::string modelPath models/yolov8n-seg.onnx; const std::string imagePath images/bus.jpg;第一次运行之前先确认工作目录是工程根目录不然相对路径会找不到文件。我的习惯是永远不依赖相对路径直接用std::filesystem::absolute()打印一下或者干脆在 CMake 里把工程目录定义成编译宏传进去。这也是排查「明明文件就在那儿程序就是说读不到」最快的手段。另外输出文件这块项目里 bus.jpg 对应会生成 bus_out.bmp可以看出作者刻意用了 BMP 格式而不是 JPG。原因很现实BMP 不会引入额外的压缩损失在验证分割 mask 是否逐像素正确时BMP 更适合做像素级对比。4. 代码走读YOLOv8 检测与实例分割的推理实现要点4.1 检测链路letterbox 预处理、session 推理、NMS 后处理C 检测代码的骨架在yolov8_onnx.cpp和yolov8.cpp里基本就三件事预处理、推理、后处理。先看预处理这段代码是最容易写错却最影响精度的。cv::Mat letterbox(const cv::Mat src, int targetSize, float ratio, int padX, int padY) { int w src.cols, h src.rows; ratio std::min(static_castfloat(targetSize) / w, static_castfloat(targetSize) / h); int newW std::round(w * ratio); int newH std::round(h * ratio); padX (targetSize - newW) / 2; padY (targetSize - newH) / 2; cv::Mat resized; cv::resize(src, resized, cv::Size(newW, newH), 0, 0, cv::INTER_LINEAR); cv::Mat canvas(targetSize, targetSize, CV_8UC3, cv::Scalar(114, 114, 114)); resized.copyTo(canvas(cv::Rect(padX, padY, newW, newH))); return canvas; }这段代码的逻辑是先把原图按比例缩放到目标尺寸内然后放到一个 640x640 的灰色画布中央而不是直接把图拉伸到 640x640。为什么要这样因为直接拉伸会改变目标的宽高比导致检测框偏移和分类置信度下降。ratio和padX/padY必须保存下来后处理还原坐标时要用。推理部分用 ONNXRuntime 的 C API核心逻辑是这样的auto memoryInfo Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); std::vectorfloat inputTensorValues(1 * 3 * 640 * 640); // ... 将 letterbox 后的图像数据按 HWC 转 CHW并归一化到 [0,1] ... Ort::Value inputTensor Ort::Value::CreateTensorfloat( memoryInfo, inputTensorValues.data(), inputTensorValues.size(), inputShape.data(), inputShape.size()); auto outputTensors session.Run(Ort::RunOptions{nullptr}, inputNames.data(), inputTensor, 1, outputNames.data(), outputNames.size());这里最关键的参数是outputNames。在你导出的 ONNX 模型里输出节点叫什么名字这里就得写什么名字。YOLOv8 官方导出时默认输出名是output0检测和output1分割的 mask但如果你用simplifyTrue重新导出过名字可能变成一串数字或 conv 层的名字。拿 Netron 看一眼再填进代码比瞎猜快得多。推理之后是 NMS 后处理。YOLOv8 输出 8400 个候选框绝大多数置信度极低。处理流程是先按置信度阈值过滤比如 0.25然后把通过过滤的框送入cv::dnn::NMSBoxesstd::vectorcv::Rect boxes; std::vectorfloat confidences; // 遍历 8400 个候选筛选得分 0.25 的框存入 boxes/confidences std::vectorint indices; cv::dnn::NMSBoxes(boxes, confidences, 0.25, 0.45, indices); for (int idx : indices) { // 还原坐标x (cx - padX) / ratio ... }NMSBoxes的两个阈值要理解透第一个是score_threshold过滤低分框第二个是nms_threshold控制重叠框的抑制力度。0.45 是相对宽松的值物体密集的场景可以收到 0.4如果场景里物体少、检测框本来就稀疏0.5 也能用。这个参数没有绝对标准看实际效果调。4.2 分割链路mask 系数与原型 mask 的矩阵运算分割模型的后处理比检测复杂得多核心在yolov8_seg_onnx.cpp里。拿到两个输出张量后要做一次矩阵乘才能得到每个候选框的掩码。// output0: [1, 116, 8400] - 前 84 行是检测结果后 32 行是 mask 系数 // output1: [1, 32, 160, 160] - 原型 mask const float* protoData outputTensors[1].GetTensorDatafloat(); const float* data0 outputTensors[0].GetTensorDatafloat(); for (int i 0; i numBoxes; i) { int clsId ...; float* maskCoeff const_castfloat*(data0 i * 116 84); cv::Mat mask(160, 160, CV_32FC1, cv::Scalar(0.f)); for (int c 0; c 32; c) { float coeff maskCoeff[c]; const float* proto protoData c * 160 * 160; for (int p 0; p 160 * 160; p) { mask.atfloat(p) coeff * proto[p]; } } }这段代码的逻辑不要看晕maskCoeff是当前检测框特有的 32 个系数proto是模型学出来的共享原型 mask。每个检测框的掩码就是这 32 个原型 mask 按系数加权求和。求完和还要过 sigmoid 变成 0-1 的概率值然后阈值化成二值掩码。这段代码有个明显的性能优化空间160x160 的嵌套循环是纯 CPU 浮点运算在 x86 上跑 30 个框大概是几十毫秒的量级但如果目标平台是 ARM 板子这个循环可能成了瓶颈。常见的优化方式是用 OpenCV 的矩阵运算把乘加改写成cv::Mat的加权和或者干脆把这段逻辑下沉到 ONNX 模型里让 ONNXRuntime 一次性算完——导出时把 mask 系数和原型 mask 的乘加做成一个自定义算子推理输出直接就是 160x160 的掩码。分割结果的可视化逻辑项目里用cv::rectangle画检测框、用cv::fillPoly填充分割区域最终输出bus_out.bmp。到这里整个分割链路的 C 实现就闭环了。5. 避坑从 shape 对不上到 mask 全黑的五个真实翻车现场5.1 shape 对不上输出节点比预期少了一个现象代码运行时提示输出的张量数量不对或者output0读出来的数据长度远小于预期。原因大部分情况是导出时simplifyTrue把 mask 的输出分支当成冗余算子折叠了。分割模型的两个输出节点在简化模型时如果某些算子被合并原型 mask 那个[1, 32, 160, 160]节点会消失或改名。解决重新用simplifyFalse导出导出后立刻在 Netron 里数一下输出节点数量。检测模型 1 个、分割模型 2 个、OBB 模型 1 个少于这个数就说明导出阶段出了问题。跑工程之前花两分钟确认这一步能省下后面一个小时的排查时间。5.2 mask 全黑原型 mask 被当成普通张量丢弃现象检测框画出来了但分割区域是全黑的或者整张图只有零星几个白点。原因读取 mask 系数时下标错了。output0的 116 维里后 32 位才是 mask 系数很多新手会下意识认为输出前 84 位是坐标和类别、直接把整个 116 位当成纯检测输出去解析还有一种情况是读取原型 mask 时把维度当成[32, 160, 160]但实际内存布局是[1, 32, 160, 160]偏移量正好差了 160x160 个 float。解决先把输出张量的 shape 打印出来逐维核对。取 mask 系数时确认偏移是84不是4——检测模型是 4分割模型是 84这个坑在两种模型之间来回切的时候特别容易踩。5.3 letterbox 没还原坐标检测框整体偏移现象框能画出来但框的位置整体往右下角偏尤其在图片不是正方形时特别明显。原因推理得到的坐标是 640x640 画布坐标系下的值直接画到原图上没做(x - pad) / ratio的反变换。letterbox 时图被缩放并居中放置推理结果必须逆变换回原图坐标。解决在 NMS 之后、画框之前每个坐标都做一次逆变换。注意padX和padY是 letterbox 函数返回的原始值ratio也一样必须保留。我在代码里见过把 ratio 和 pad 算完却忘了传出去的版本问题就出在作用域上。5.4 中文路径与 cv::imread 静默失败现象程序跑完没报错但所有图片都是黑的或者检测结果为空。原因OpenCV 在 Windows 上对中文路径的支持是出名的烂。cv::imread(images/测试图.jpg)很可能返回空 Mat但代码里没判空继续往下走就全错了。项目里用的是bus.jpg这种 ASCII 路径所以没事一旦你自己换成中文路径就碰上这个经典坑。解决项目内用英文路径。如果必须读中文路径用cv::imdecode配合std::ifstream把文件按二进制读进来再解码这是 Windows 上最稳妥的方案。5.5 CPU/GPU 推理库不通用换台机器就崩现象本机跑得好好的部署到另一台机器直接启动崩溃报错信息指向 ONNXRuntime 的 DLL 或 so 文件。原因你用的是 GPU 版 ONNXRuntime目标机器没有 CUDA 环境或者反过来用了 CPU 版但链接时混入了 GPU 版的头文件。解决工程里声明清楚依赖的 ONNXRuntime 版本和类型。CPU 版就全程 CPU 版GPU 版部署时打包上 CUDA、cuDNN 的 DLL。另外留意 ONNXRuntime 1.16 之后的版本对 CUDA 版本有硬性要求版本对不上会直接报No such operator之类的错。这个坑最折磨人因为报错信息往往指向的是算子丢失而不是库不匹配。6. 进阶验证OBB 检测与 RT-DETR 的结果核对6.1 用 DOTA 样例图验证 OBB角度还原与可视化项目里DOTA_0032.png是遥感图像适合验证 OBB 旋转框检测。OBB 的后处理比普通检测多一步角度还原一个最小实现是这样的// OBB 输出中角度相关参数因导出版本而异这里按 4 位角度编码来还原 float angle std::atan2(angleParams[2], angleParams[3]) * 180.0f / CV_PI; cv::RotatedRect rrect(cv::Point2f(cx, cy), cv::Size2f(w, h), angle); cv::ellipse(canvas, rrect, cv::Scalar(0, 0, 255), 2);这段代码的关键是角度编码方式。不同导出版本可能用单角度值、cos/sin 或者 4 位连续编码解析方式不同。跑 DOTA_0032.png 时如果旋转框角度明显不对优先检查导出的 OBB 模型输出布局而不是怀疑代码逻辑。6.2 RT-DETR 的额外小步无 NMS 后处理与置信度选择RT-DETR 没有 NMS 这一步它的输出经过nn.PostProcess后直接是框和类别。在 C 实现的差异点在于检测结果不再有 8400 个候选只有模型设置的 query 数量默认是 300 个后处理只需要按置信度阈值过滤然后排序取前 N 个。用项目里的rtdetr_onnx.cpp跑同一张 bus.jpg跟 YOLOv8 的检测结果对比一下框的数量和置信度分布是验证 RT-DETR 是否正常最直接的方法。跑通了这一步你就在同一个工程里掌握了两种检测方案的切换能力后续接到实际项目里选型就灵活多了。这个资源包最值得借鉴的地方在于它把三种任务的 C 推理都放到了同一个框架下目录清晰、接口统一。从模型导出到避坑回头再看我在 RK3588 上部署 YOLOv8 时踩过的那些坑——中文路径读图失败、简化模型丢了 mask 分支、NMS 阈值设太高漏检——现在都有了解法。从那以后我每次跑新模型都强制走一遍「Netron 看输出节点 → 确认 shape → 小图跑通 → 全数据集验证」这条流程基本没有再被这类问题绊住过。希望帮到你。本文还有配套的精品资源点击获取