
简介这份源码资源面向计算机视觉方向的学生与开发者聚焦于用C结合ONNXRuntime推理引擎部署YOLOv8的ONNX模型可解决毕业设计、期末大作业或课程设计中目标检测类项目落地难的问题。包内共28个文件以cpp与h源码为主涵盖检测、分割、姿态、OBB旋转框及RT-DETR等多种任务的推理实现另附少量jpg、png、bmp测试图片与docx说明手册压缩包约5.03MB结构清晰、注释完整新手也能看懂。目前已有427人学习下载经过严格调试可稳定运行。读者可直接获得一套可复用的C推理工程理解ONNXRuntime加载模型、预处理与后处理的完整链路并借助示例图片快速验证检测、分割、姿态等效果为二次开发或答辩展示提供扎实基础。1. C 与 onnxruntime 部署 YOLOv8为什么这条路线值得走训练完 YOLOv8拿到一个.onnx文件接下来才是真正见功夫的地方。Python 里model.predict()一行就能出框但到了产线、边缘盒子、工控机、游戏辅助工具这类场景Python 解释器加 GIL 的启动开销和内存占用往往直接劝退。这时候把推理塞进 C用 onnxruntime 做后端是很多团队最终收敛到的方案编译出来一个可执行文件拷到目标机器上就能跑不依赖 Python 环境延迟可控内存可控。这条路线解决的核心问题是「模型怎么脱离训练框架独立运行」。ONNX 是中间表示onnxruntime 是执行引擎C 是宿主语言。三者拼起来你得到的是一个跨平台、可静态链接、能嵌进现有 C 工程的推理模块。适合谁做嵌入式部署的、做桌面端 AI 功能的、做高性能服务端的以及被 Python 部署的启动时间和内存折磨过的工程师。下面从环境、加载、预处理、后处理一路讲到踩坑都是能直接抄的代码。2. 环境搭建与 onnxruntime 的选型别一上来就编译源码2.1 为什么优先用预编译动态库而不是自己编很多人第一反应是去 GitHub 拉 onnxruntime 源码自己编译觉得这样「可控」。血泪经验是除非你要交叉编译到特定架构比如鲲鹏 920、RK3588 这类 ARM 平台否则直接用官方预编译包。自己编一个 CPU 版本光是 CMake 配置加依赖下载就能耗掉半天编出来的东西还不一定比官方包快。选型上分三种情况。第一x86 桌面或服务器直接下onnxruntime-win-x64-xxx.zip或 Linux 对应的.tgz里面有include和lib链接进去就行。第二ARM 平台如鲲鹏 920官方也提供 aarch64 的预编译包优先试。第三实在没有对应架构的包才考虑源码编译这时候重点开--cmake_extra_defines onnxruntime_BUILD_SHARED_LIBON。版本选择上onnxruntime 1.16 以上对 YOLOv8 导出的 ONNX 支持比较稳opset 建议在导出时锁 12 或 13。别用太老的版本老版本对Resize、Split这些算子的实现有差异容易出现输出对不上的玄学问题。2.2 用 CMake 把 onnxruntime 接进 C 工程工程组织建议这样项目根目录下放third_party/onnxruntime里面是解压后的 include 和 lib。CMakeLists 里用target_include_directories和target_link_libraries引进来不要用全局的include_directories否则工程一大就乱。cmake_minimum_required(VERSION 3.15) project(yolov8_ort_demo CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # onnxruntime 预编译包解压后的路径 set(ORT_ROOT ${CMAKE_SOURCE_DIR}/third_party/onnxruntime) add_executable(yolov8_ort_demo src/main.cpp src/yolov8_engine.cpp) target_include_directories(yolov8_ort_demo PRIVATE ${ORT_ROOT}/include ${CMAKE_SOURCE_DIR}/include ) # Windows 链接 onnxruntime.libLinux 链接 libonnxruntime.so if(WIN32) target_link_libraries(yolov8_ort_demo PRIVATE ${ORT_ROOT}/lib/onnxruntime.lib) else() target_link_libraries(yolov8_ort_demo PRIVATE ${ORT_ROOT}/lib/libonnxruntime.so) endif()这段 CMake 的关键点有三个。CMAKE_CXX_STANDARD 17是因为 onnxruntime 的头文件用了不少 C17 特性用 C11 编会报一堆错。ORT_ROOT集中管理路径换机器只改这一处。链接库分平台处理Windows 下是.lib导入库运行时还需要把onnxruntime.dll拷到 exe 同目录Linux 下是.so运行时用LD_LIBRARY_PATH或rpath指定。提示Windows 上如果运行时报「找不到 onnxruntime.dll」不是链接问题是运行时动态库没放对位置。把 dll 和 exe 放同一目录最省事。2.3 验证环境是否通的第一个最小程序别急着写完整推理先写个最小程序确认头文件能编、库能链、运行时能加载。#include onnxruntime_cxx_api.h #include iostream int main() { // 创建环境日志级别设为 WARNING避免刷屏 Ort::Env env(ORT_LOGGING_LEVEL_WARNING, yolov8_demo); Ort::SessionOptions session_options; session_options.SetIntraOpNumThreads(4); // 单算子内部并行线程数 session_options.SetGraphOptimizationLevel( GraphOptimizationLevel::ORT_ENABLE_ALL); // 开启全部图优化 try { Ort::Session session(env, yolov8n.onnx, session_options); std::cout model loaded, inputs: session.GetInputCount() , outputs: session.GetOutputCount() std::endl; } catch (const Ort::Exception e) { std::cerr load failed: e.what() std::endl; return -1; } return 0; }逻辑说明Ort::Env全局一个就够不要每个函数里都建。SetIntraOpNumThreads控制单个算子内部的并行度CPU 上一般设成物理核心数设太大反而因为线程切换变慢。ORT_ENABLE_ALL会做算子融合和常量折叠对 YOLOv8 这种结构收益明显。Ort::Session构造时如果模型路径错或 opset 不支持会抛Ort::Exception一定要 try-catch否则程序直接崩连错误信息都看不到。参数说明ORT_LOGGING_LEVEL_WARNING是日志级别调试阶段可以调成VERBOSE看算子分配情况上线改回WARNING或ERROR。模型路径用相对路径时注意工作目录IDE 里跑和命令行跑的工作目录可能不一样这是新手最常见的翻车点。3. 从 ONNX 加载到张量输入输出名字和形状怎么拿3.1 动态获取输入输出信息别硬编码YOLOv8 导出的 ONNX输入名通常是images输出名通常是output0但这不是保证。不同导出脚本、不同版本可能变。硬编码名字的后果是换一个模型就崩。正确做法是运行时查。Ort::AllocatorWithDefaultOptions allocator; // 输入信息 size_t num_inputs session.GetInputCount(); for (size_t i 0; i num_inputs; i) { auto name session.GetInputNameAllocated(i, allocator); auto shape session.GetInputTypeInfo(i) .GetTensorTypeAndShapeInfo() .GetShape(); std::cout input[ i ] name name.get() shape; for (auto d : shape) std::cout d ; std::cout std::endl; } // 输出信息 size_t num_outputs session.GetOutputCount(); for (size_t i 0; i num_outputs; i) { auto name session.GetOutputNameAllocated(i, allocator); auto shape session.GetOutputTypeInfo(i) .GetTensorTypeAndShapeInfo() .GetShape(); std::cout output[ i ] name name.get() shape; for (auto d : shape) std::cout d ; std::cout std::endl; }逻辑说明GetInputNameAllocated返回的是Ort::AllocatedStringPtr用.get()拿const char*。形状里如果出现-1说明那一维是动态的YOLOv8 导出时 batch 维经常是动态的部署时要么固定成 1要么在SessionOptions里做维度绑定。输出形状一般是[1, 84, 8400]84 是 4 个框坐标加 80 类分数8400 是三个尺度特征图展平后的锚点数。参数说明allocator用默认的就行不需要自定义。如果模型有多个输出比如导出时带了辅助头要遍历所有输出别只取第一个。3.2 构造输入张量预处理必须和训练时对齐YOLOv8 训练时的预处理是 letterbox 缩放加归一化到 0-1通道顺序 RGB布局 NCHW。这四点在 C 里必须一模一样差一个都会导致框位置偏移或者置信度异常。// 假设原图 cv::Mat img 是 BGR已读入 int inp_w 640, inp_h 640; float scale std::min(inp_w / (float)img.cols, inp_h / (float)img.rows); int new_w (int)(img.cols * scale); int new_h (int)(img.rows * scale); int pad_x (inp_w - new_w) / 2; int pad_y (inp_h - new_h) / 2; cv::Mat resized; cv::resize(img, resized, cv::Size(new_w, new_h)); cv::Mat canvas(inp_h, inp_w, CV_8UC3, cv::Scalar(114, 114, 114)); resized.copyTo(canvas(cv::Rect(pad_x, pad_y, new_w, new_h))); // BGR - RGB, HWC - CHW, 归一化 std::vectorfloat input_tensor(1 * 3 * inp_h * inp_w); for (int c 0; c 3; c) { for (int y 0; y inp_h; y) { for (int x 0; x inp_w; x) { cv::Vec3b pixel canvas.atcv::Vec3b(y, x); // c0 取 R原 BGR 的 index 2c1 取 Gc2 取 B float v pixel[2 - c] / 255.0f; input_tensor[c * inp_h * inp_w y * inp_w x] v; } } }逻辑说明letterbox 的填充值 114 是 YOLO 系列的惯例训练时用的就是这个灰值推理时必须一致。通道变换这里用pixel[2 - c]完成 BGR 到 RGB同时按 CHW 布局写入一维数组。归一化除以 255 是 YOLOv8 默认行为如果你训练时改了归一化方式这里要跟着改。参数说明inp_w、inp_h必须和导出 ONNX 时的输入尺寸一致YOLOv8 默认 640。scale取宽高缩放比的较小值保证整图能放进画布。pad_x、pad_y是居中填充的偏移后处理还原框坐标时要用到必须存下来。3.3 跑一次推理并取回输出// 输入输出名从前面动态获取的结果里取 const char* input_names[] {images}; const char* output_names[] {output0}; std::vectorint64_t input_shape {1, 3, inp_h, inp_w}; Ort::MemoryInfo mem_info Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor_ort Ort::Value::CreateTensorfloat( mem_info, input_tensor.data(), input_tensor.size(), input_shape.data(), input_shape.size()); auto outputs session.Run(Ort::RunOptions{nullptr}, input_names, input_tensor_ort, 1, output_names, 1); float* out_data outputs[0].GetTensorMutableDatafloat(); auto out_shape outputs[0].GetTensorTypeAndShapeInfo().GetShape(); // out_shape 预期为 [1, 84, 8400]逻辑说明CreateTensor把已有的std::vectorfloat内存包装成 Ort 张量不拷贝所以input_tensor在Run期间不能释放。Run的输入输出名数组要和实际模型对上数量也要对。输出张量的内存由 onnxruntime 管理GetTensorMutableData拿到指针后直接读不要试图 free。参数说明OrtArenaAllocator是内存分配器类型CPU 上用默认的就行。OrtMemTypeDefault表示默认内存类型。如果后面要上 GPU这里换成 CUDA 的 MemoryInfo但那是另一个话题。4. 后处理把 [1,84,8400] 变成能画的框4.1 解码逻辑与置信度过滤输出[1, 84, 8400]里前 4 行是cx, cy, w, h后 80 行是类别分数。注意 YOLOv8 的输出没有单独的 objectness类别分数直接就是最终置信度。解码时先转置成[8400, 84]更好遍历或者直接按列访问。int num_classes 80; int num_anchors 8400; float conf_thres 0.25f; float iou_thres 0.45f; std::vectorcv::Rect boxes; std::vectorfloat scores; std::vectorint class_ids; for (int i 0; i num_anchors; i) { float* row out_data i; // 注意这里按 [84,8400] 布局行是类别 // 实际访问应为 out_data[c * num_anchors i] float cx out_data[0 * num_anchors i]; float cy out_data[1 * num_anchors i]; float w out_data[2 * num_anchors i]; float h out_data[3 * num_anchors i]; int best_class -1; float best_score 0.0f; for (int c 0; c num_classes; c) { float s out_data[(4 c) * num_anchors i]; if (s best_score) { best_score s; best_class c; } } if (best_score conf_thres) continue; // 还原到原图坐标先减 padding再除以 scale float x (cx - w / 2 - pad_x) / scale; float y (cy - h / 2 - pad_y) / scale; float bw w / scale; float bh h / scale; boxes.emplace_back(cv::Rect((int)x, (int)y, (int)bw, (int)bh)); scores.push_back(best_score); class_ids.push_back(best_class); }逻辑说明输出布局是[1, 84, 8400]所以第c个通道第i个锚点的值在out_data[c * 8400 i]。前四个通道是框后面是类别。取最大类别分数作为该框的置信度低于阈值直接丢。坐标还原是 letterbox 的逆操作先减 padding 偏移再除以缩放比。参数说明conf_thres0.25 是常用起点误检多就调高漏检多就调低。iou_thres0.45 用于 NMS密集场景可以调到 0.5 以上。pad_x、pad_y、scale必须用预处理时存下来的值不能重新算。4.2 NMS 与结果绘制std::vectorint indices; cv::dnn::NMSBoxes(boxes, scores, conf_thres, iou_thres, indices); for (int idx : indices) { cv::Rect box boxes[idx]; cv::rectangle(img, box, cv::Scalar(0, 255, 0), 2); std::string label std::to_string(class_ids[idx]) : std::to_string(scores[idx]).substr(0, 4); cv::putText(img, label, cv::Point(box.x, box.y - 5), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 255, 0), 1); }逻辑说明cv::dnn::NMSBoxes是 OpenCV 自带的 NMS输入框、分数、阈值输出保留的索引。绘制时用绿色框加类别和分数标签。如果不想依赖 OpenCV 的 dnn 模块自己写 NMS 也就二十行按分数排序后逐个算 IoU 抑制。参数说明NMSBoxes的conf_thres和iou_thres与前面过滤保持一致。substr(0, 4)只是截断显示别在正式输出里这么干会丢精度。4.3 性能上值得做的两件事第一预处理里的三重循环是性能热点。640x640x3 接近 123 万次迭代用cv::Mat的at访问带边界检查慢。改成指针遍历或者用cv::dnn::blobFromImage会快不少但blobFromImage默认做的是减均值缩放要确认和训练预处理一致。第二session.Run每次调用都有开销如果做视频流考虑用Ort::IoBinding绑定输入输出内存减少拷贝。5. 避坑与排查那些让框画不出来的原因5.1 框位置整体偏移或缩放不对现象框能出来但位置系统性偏左上或偏右下或者框比实际物体大一圈小一圈。原因letterbox 的pad_x、pad_y、scale在预处理和后处理之间不一致常见于把预处理封装成函数后这些值没传出来后处理里重新算了一遍但用了不同的取整方式。解决把这三个值放进一个结构体预处理填充后处理读取中间不重算。取整统一用int截断别一处round一处floor。5.2 置信度全是 0 或者全是 1现象输出张量里数值异常要么接近 0要么饱和。原因归一化没做或做了两次。YOLOv8 导出 ONNX 时如果带了归一化层C 里再除 255 就变成两次。解决用 Netron 打开 ONNX 看输入节点后面有没有Div或Mul常量有的话 C 里就不要再归一化。另一个原因是通道顺序反了BGR 当 RGB 喂进去分数会普遍偏低但不至于全 0。5.3 换模型后输出形状对不上现象之前跑 80 类模型正常换成一个自定义 3 类模型后程序读越界或框全乱。原因类别数变了输出从[1, 84, 8400]变成[1, 7, 8400]但代码里num_classes还写死 80。解决num_classes从输出形状动态算out_shape[1] - 4就是类别数。锚点数out_shape[2]也动态取别写死 8400换输入尺寸或换模型结构都会变。5.4 Windows 下 Debug 能跑 Release 崩现象VS 里 Debug 模式推理正常切 Release 直接 access violation。原因onnxruntime 的预编译库通常是 Release 版Debug 版程序链接 Release 库时STL 容器和内存分配器的 ABI 可能不匹配。解决要么统一用 Release 编译自己的程序要么找 Debug 版的 onnxruntime 库。这个坑在c#调用c出现access violation c0000005这类场景里也常见本质是跨模块内存管理不一致。5.5 多线程下结果错乱现象单线程跑正常开多线程后框随机错乱或崩溃。原因Ort::Session本身线程安全但Ort::Env和共享的输入输出缓冲区不是。多个线程共用一个input_tensor向量会互相覆盖。解决每个线程独立的输入缓冲和Ort::ValueSession可以共享。或者用IoBinding给每个线程绑自己的内存。6. 进阶把推理封装成可复用引擎与量化提速走到这里你已经能跑通单张图。但工程上要的是可复用、可配置、能上量的东西。我一般会把整个流程封成一个YoloV8Engine类构造时传模型路径、输入尺寸、阈值infer方法接收cv::Mat返回框列表。这样换模型只改构造参数业务代码不动。class YoloV8Engine { public: YoloV8Engine(const std::string model_path, int inp_size 640, float conf 0.25f, float iou 0.45f); std::vectorDetection infer(const cv::Mat img); private: Ort::Env env_; Ort::Session session_; int inp_size_; float conf_thres_, iou_thres_; std::string input_name_, output_name_; int num_classes_, num_anchors_; };构造时把输入输出名和形状查出来存成成员infer里不再重复查询。Detection结构体放rect、score、class_id。这样封装后主程序里就是读图、调infer、画框三步。量化方面.onnx转 int8 能明显降内存和提速但精度会掉。CPU 上用 onnxruntime 的量化工具python -m onnxruntime.quantization.preprocess \ --input yolov8n.onnx --output yolov8n_prep.onnx python -m onnxruntime.quantization.quantize_static \ --input yolov8n_prep.onnx --output yolov8n_int8.onnx \ --calibrate_dataset calib_data/ --quant_format QDQpreprocess先做图优化quantize_static做静态量化需要一批校准图。QDQ格式兼容性好QOperator在某些后端上更快但支持面窄。量化后务必用同一批测试图对比 float 和 int8 的框IoU 掉超过 5% 就要考虑是不是校准集不具代表性。我自己的习惯是先跑通 float再量化量化后如果精度掉得厉害宁可不上 int8用 float16 或者干脆保持 float32别为了省那点内存把检测质量搭进去。验证方法上除了肉眼看框建议写个脚本把 C 输出和 Pythonmodel.predict()的输出做数值对比同一张图框坐标差在 1 像素内、分数差在 0.01 内才算对齐。这个对齐步骤能帮你把预处理和后处理的偏差一次性揪出来比在 C 里瞎调参数高效得多。希望帮到你。本文还有配套的精品资源点击获取