简介:这份源码资源面向计算机视觉方向的学生与开发者,聚焦于用C++结合ONNXRuntime推理引擎部署YOLOv8的ONNX模型,可用于毕业设计、期末大作业或课程设计场景,帮助解决模型从训练到C++工程化落地的实际问题。压缩包共28个文件,约5.03MB,以cpp与h源码为主体,涵盖目标检测、实例分割、姿态估计、旋转框检测及RT-DETR等多个任务模块,另附CMakeLists构建脚本、模型放置目录、示例图片与说明文档,便于快速编译运行。目前已有427人学习下载,代码注释较为完整,新手也能理解整体推理流程。读者可获得一套结构清晰的C++推理工程,掌握预处理、会话推理与后处理等关键环节,并借助示例图片验证检测效果,为后续二次开发或项目答辩提供可参考的实现基础。
1. 从 PyTorch 到 C++ 推理:这套源码到底解决了什么问题
你训练完一个 YOLOv8 模型,best.pt在 Python 里跑得挺欢,但一到交付环节就卡住了——客户现场只有一台工控机,装不了完整的 Python 环境,或者要求推理延迟必须压到 10ms 以内,Python 解释器那点开销就成了瓶颈。这时候把模型导出成 ONNX,再用 C++ 配合 onnxruntime 做推理,就是一条很自然的落地路径。这套源码干的事情很明确:给你一个已经搭好架子的 C++ 工程,里面包含了 ONNX 模型加载、预处理、推理、后处理(NMS)的完整链路,你拿到手改改路径、调调参数就能跑起来。它适合两类人:一类是做毕业设计或期末大作业的学生,需要一个能跑通、结构清晰、方便写论文的完整项目;另一类是做边缘部署的工程师,想找一个不依赖 OpenCV DNN 这种“黑匣子”的纯 onnxruntime 推理参考。源码本身不包含训练部分,它只负责推理这一环,所以你得先有一个训练好的 YOLOv8 模型,并且已经导出成了 ONNX 格式。
2. 环境搭建与 ONNX 模型导出:别在第一步就翻车
2.1 为什么选 onnxruntime 而不是 OpenCV DNN
很多人第一次做 C++ 部署会直接用 OpenCV 的dnn::readNetFromONNX,因为 OpenCV 本来就要用,看起来省事。但实际用下来,OpenCV DNN 对 ONNX 算子集的支持是滞后的,YOLOv8 里的一些算子(比如Split、Resize的某些模式)在旧版本 OpenCV 上直接报错,而且它内部做了一层图优化,你很难控制推理时的具体行为。onnxruntime 是微软官方维护的推理引擎,对 ONNX 标准的跟进最及时,CPU 和 GPU 的 Execution Provider 切换也干净。这套源码选 onnxruntime 作为推理后端,意味着你不需要链接 OpenCV 的 dnn 模块,只需要 core 和 imgproc 做图像读写和预处理就行,依赖更轻,行为也更可预测。
2.2 导出 ONNX 模型时的三个关键参数
训练完的best.pt不能直接给 C++ 用,得先导出。Ultralytics 的 YOLOv8 提供了export命令,但有几个参数直接决定后面 C++ 端好不好写:
yolo export model=best.pt format=onnx imgsz=640 opset=12 simplify=True dynamic=Falseimgsz=640:导出时固定输入尺寸。如果你设成dynamic=True,ONNX 模型会接受动态尺寸输入,但 C++ 端预处理就得做更复杂的 letterbox 计算,而且 onnxruntime 对动态维度的处理会稍微慢一点。新手建议先固定 640×640,跑通再说。opset=12:算子集版本。onnxruntime 1.16 以上对 opset 12 支持很稳,再高也不是不行,但有些旧版 onnxruntime 会不认。如果你用的 onnxruntime 是 1.14 以下,opset 别超过 13。simplify=True:这个很重要。YOLOv8 导出的原始 ONNX 图里有一堆冗余的Identity、Constant节点,simplify 会调用 onnx-simplifier 把图压干净,推理时能少走不少弯路。但注意,simplify 有时候会引入一些奇怪的算子融合,如果你后面发现输出对不上,可以试试关掉它对比一下。
导出完成后你会得到一个best.onnx,用 Netron 打开看一眼输入输出。YOLOv8 的 ONNX 输出通常是output0,形状是[1, 84, 8400]——84 是 4 个框坐标加 80 个类别分数,8400 是三个尺度特征图展平后的锚点数。记住这个形状,后面写后处理全靠它。
2.3 C++ 工程的依赖配置
这套源码的 CMakeLists.txt 里主要找三个东西:onnxruntime、OpenCV、和线程库。onnxruntime 的 C++ API 需要你下载预编译包,Windows 下是onnxruntime-win-x64-1.16.3.zip,Linux 下是onnxruntime-linux-x64-1.16.3.tgz。解压后里面include和lib两个目录,CMake 里这样写:
set(ONNXRUNTIME_ROOT "/path/to/onnxruntime") find_package(OpenCV REQUIRED) include_directories(${ONNXRUNTIME_ROOT}/include ${OpenCV_INCLUDE_DIRS}) target_link_libraries(your_target ${ONNXRUNTIME_ROOT}/lib/onnxruntime.lib ${OpenCV_LIBS})Linux 下把.lib换成.so,并且注意运行时LD_LIBRARY_PATH要包含 onnxruntime 的 lib 目录,否则编译过了运行时报找不到动态库。Windows 下把onnxruntime.dll拷到 exe 同目录,或者加进 PATH。这个坑几乎每个人都会踩一次,编译链接都过了,一运行就弹窗说缺 dll。
3. 预处理、推理与后处理:C++ 端的三段式拆解
3.1 图像预处理:letterbox 的 C++ 实现细节
YOLOv8 训练时用的是 letterbox 预处理——保持宽高比缩放,短边补灰边到 640×640。C++ 端必须做一模一样的操作,否则精度掉得你怀疑人生。源码里通常有一个letterbox函数,核心逻辑是:
cv::Mat letterbox(cv::Mat& img, int new_shape, cv::Scalar color) { int w = img.cols, h = img.rows; float r = std::min(new_shape / (float)w, new_shape / (float)h); int unpad_w = round(w * r), unpad_h = round(h * r); cv::Mat resized; cv::resize(img, resized, cv::Size(unpad_w, unpad_h)); int dw = new_shape - unpad_w, dh = new_shape - unpad_h; int top = dh / 2, bottom = dh - top; int left = dw / 2, right = dw - left; cv::Mat padded; cv::copyMakeBorder(resized, padded, top, bottom, left, right, cv::BORDER_CONSTANT, color); return padded; }这里有几个参数要盯住:color一般用(114, 114, 114),这是 YOLO 系列的惯例;r是缩放比例,后面还原框坐标时要用到,所以这个值得存下来。补边的分配是dh/2给上、dh-dh/2给下,左右同理,这样能保证和 Python 端letterbox的像素级对齐。如果你发现 C++ 推理结果和 Python 差那么一两个像素的框偏移,八成是这里取整方式不一致。
预处理还有一步:BGR 转 RGB、归一化到 0~1、HWC 转 CHW。源码里一般用cv::dnn::blobFromImage一把梭,但注意blobFromImage默认会做均值减法,你得把mean设成Scalar(0,0,0),scalefactor设成1/255.0,swapRB设成true。这样出来的 blob 就是[1,3,640,640]的 float 数组,直接喂给 onnxruntime。
3.2 onnxruntime 会话创建与推理调用
C++ 端用 onnxruntime 的 API 分四步:创建环境、创建会话、准备输入输出张量、跑。源码里通常封装了一个OrtSession类,核心代码长这样:
Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "yolov8"); Ort::SessionOptions session_options; session_options.SetIntraOpNumThreads(4); session_options.SetGraphOptimizationLevel( GraphOptimizationLevel::ORT_ENABLE_ALL); Ort::Session session(env, "best.onnx", session_options); // 获取输入输出名称 Ort::AllocatorWithDefaultOptions allocator; auto input_name = session.GetInputNameAllocated(0, allocator); auto output_name = session.GetOutputNameAllocated(0, allocator); // 准备输入张量 std::vector<int64_t> input_shape = {1, 3, 640, 640}; auto memory_info = Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor = Ort::Value::CreateTensor<float>( memory_info, blob_data, blob_size, input_shape.data(), 4); // 推理 auto outputs = session.Run(Ort::RunOptions{nullptr}, &input_name.get(), &input_tensor, 1, &output_name.get(), 1);SetIntraOpNumThreads(4)控制算子内并行线程数,设成 CPU 核心数就行,设太大反而会因为线程切换开销掉性能。ORT_ENABLE_ALL会开启所有图优化,包括常量折叠和算子融合,一般建议开着。GetInputNameAllocated返回的是AllocatedStringPtr,用.get()拿const char*。输入张量的内存必须连续,blobFromImage出来的cv::Mat如果是连续的,直接拿data指针就行,不连续的话得先clone()。
推理完的输出是一个Ort::Value,用GetTensorMutableData<float>()拿到 float 指针,形状是[1, 84, 8400]。注意 onnxruntime 的输出内存归它自己管,你不需要释放,但也不能在 session 销毁后还拿着指针用。
3.3 后处理:从 84×8400 到最终框
后处理是整个链路里最容易写错的地方。YOLOv8 的输出[1, 84, 8400]里,前 4 行是cx, cy, w, h,后 80 行是类别分数。注意 YOLOv8 没有单独的 objectness 分数,类别分数直接就是置信度。处理流程:
// 遍历 8400 个锚点 for (int i = 0; i < 8400; i++) { float* ptr = output_data + i; // 找最大类别分数 float max_score = 0; int max_idx = 0; for (int c = 0; c < 80; c++) { float score = ptr[(4 + c) * 8400]; if (score > max_score) { max_score = score; max_idx = c; } } if (max_score < conf_threshold) continue; // 解析框坐标 float cx = ptr[0 * 8400], cy = ptr[1 * 8400]; float w = ptr[2 * 8400], h = ptr[3 * 8400]; // 还原到原图坐标 float x1 = (cx - w / 2 - left) / r; float y1 = (cy - h / 2 - top) / r; float x2 = (cx + w / 2 - left) / r; float y2 = (cy + h / 2 - top) / r; // 存入候选框列表 } // NMS std::vector<int> keep = nms(boxes, scores, nms_threshold);这里left、top、r就是 letterbox 时存下来的参数。conf_threshold一般设 0.25,nms_threshold设 0.45。NMS 的 C++ 实现网上很多,源码里一般用cv::dnn::NMSBoxes,但注意那个函数接受的框格式是[x, y, w, h],你得把x1y1x2y2转一下。还有一个细节:YOLOv8 的框坐标是相对于 640×640 输入图的,不是归一化的,所以还原时直接减 padding 再除以r就行,不需要乘 640。
4. 避坑与排查:那些让你怀疑人生的报错
4.1 推理结果全错或框位置偏移
现象:C++ 跑出来的框和 Python 端差很多,或者类别全乱。原因通常是预处理没对齐。检查三件事:letterbox 的补边颜色是不是(114,114,114),swapRB有没有设成true,归一化是不是除以了 255。还有一个隐蔽的坑:OpenCV 读图默认是 BGR,如果你在 Python 端训练时用的是 RGB,C++ 端忘了swapRB,颜色通道就反了,模型会把红色当成蓝色,检测结果自然一塌糊涂。解决方法是拿同一张图,Python 和 C++ 各跑一遍,把预处理后的 blob 前几个像素值打印出来对比,能快速定位。
4.2 onnxruntime 报 “Invalid Feed Input Name”
现象:session.Run时抛异常,说输入名称不对。原因是GetInputNameAllocated返回的字符串指针在AllocatedStringPtr析构后就失效了,如果你把它存成const char*而没保留AllocatedStringPtr对象,后面用的时候就是野指针。正确做法是存std::string,或者把AllocatedStringPtr的生命周期管好。另外,有些 ONNX 模型的输入名不是images,可能是input或input.1,别硬编码,用 API 查。
4.3 编译链接报 “undefined reference to Ort::...”
现象:Linux 下 CMake 配置看着没问题,一编译就报一堆 onnxruntime 的未定义符号。原因是链接顺序不对,或者target_link_libraries里 onnxruntime 的路径写错了。onnxruntime 的.so必须放在依赖它的目标后面,CMake 里就是target_link_libraries(your_target ${OpenCV_LIBS} ${ONNXRUNTIME_LIB}),顺序反了就可能找不到。Windows 下如果用的是.lib导入库,确保onnxruntime.dll在运行时能找到,否则编译过了运行直接崩。
4.4 内存泄漏与 Ort::Value 的生命周期
现象:程序跑一段时间后内存涨到几个 G。原因通常是Ort::Value和Ort::Session的析构顺序有问题,或者你在循环里反复创建Ort::Env。Ort::Env全局一个就够了,不要每次推理都新建。Ort::Value是 RAII 管理的,出了作用域自动释放,但如果你用CreateTensor时传了自己的内存指针,那块内存得自己管。源码里一般把 session 和 env 做成单例或全局,推理循环里只创建输入张量,这样内存曲线是平的。
4.5 多线程推理时的线程安全问题
现象:单线程跑得好好的,一开多线程就偶尔出框错乱或崩溃。原因是Ort::Session的Run方法本身是线程安全的,但如果你多个线程共用一个输入 blob 缓冲区,就会互相踩内存。解决办法是每个线程自己分配输入输出缓冲区,或者加锁。onnxruntime 的 session 可以并发调用Run,内部会做调度,但你的数据得各自独立。如果追求吞吐,可以创建多个 session 实例,每个线程一个,但内存占用会翻倍。
5. 进阶技巧:让这套源码跑得更快更稳
5.1 用 FP16 或 INT8 量化压榨 CPU 性能
如果你在 CPU 上跑,FP32 的 ONNX 模型推理 640×640 大概要 80~150ms,取决于 CPU 型号。想再快,可以试试量化。ONNX Runtime 提供了动态量化工具,把权重从 FP32 转成 INT8,模型体积缩小 4 倍,推理速度能提升 1.5~2 倍,精度掉 1~2 个点。命令很简单:
from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic("best.onnx", "best_int8.onnx", weight_type=QuantType.QUInt8)注意动态量化只量化权重,激活值还是动态算的,所以不需要校准数据集。但 YOLOv8 的检测头部分对量化比较敏感,如果发现小目标漏检严重,可以只量化 backbone 部分,检测头保持 FP32。这个需要你手动改 ONNX 图,用 onnx 的 Python API 把检测头节点排除掉。
5.2 切换 Execution Provider 到 CUDA 或 DirectML
如果你的机器有 NVIDIA 显卡,把 onnxruntime 换成 GPU 版本,然后在 C++ 里加一行:
OrtCUDAProviderOptions cuda_options; cuda_options.device_id = 0; session_options.AppendExecutionProvider_CUDA(cuda_options);这样推理会走 CUDA,速度直接降到 10ms 以内。但注意 GPU 版的 onnxruntime 包不一样,得下onnxruntime-win-x64-gpu-1.16.3.zip。Windows 上如果没有 N 卡,可以用 DirectML provider,A 卡和核显都能加速,但延迟比 CUDA 高一些。切换 EP 后记得重新测精度,GPU 的浮点运算顺序和 CPU 有细微差别,极端情况下框坐标会差零点几个像素。
5.3 验证推理一致性的一个笨办法但有效
每次改完预处理或后处理代码,别急着看检测效果图,先做数值对比。用同一张图,Python 端用 ultralytics 的model.predict跑一遍,把原始输出[1,84,8400]存成二进制文件;C++ 端也把output_data存成同样的文件,然后用 Python 读进来算最大绝对误差。如果误差在 1e-4 以内,说明预处理和推理链路对齐了;如果误差很大,那就是预处理某一步不对。这个办法比肉眼看框准得多,而且能定位到具体是哪个阶段出的问题。我一般会在工程里留一个debug_dump开关,编译时打开就 dump 数据,平时关掉不影响性能。
5.4 一个容易忽略的细节:输入图像的连续内存
cv::Mat经过cv::resize或copyMakeBorder后,内存不一定是连续的,尤其是当 ROI 或步长不为宽度×通道数时。blobFromImage内部会处理,但如果你手动做归一化和 HWC 转 CHW,一定要先img = img.clone()或者检查img.isContinuous()。不连续的内存传给 onnxruntime,轻则结果错乱,重则段错误。这个坑我在两个项目里各踩过一次,后来养成了习惯:只要是自己拼的 blob,先clone()再说,多一次拷贝换安心。
从那以后我每次部署新模型,都强制走一遍“Python 存输出 → C++ 存输出 → 数值对比”的流程,不管多急都不跳过。希望帮到你。
本文还有配套的精品资源,点击获取