ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Windows CPU 上 C++ 部署 YOLO11 分类 ONNX 推理实战

Windows CPU 上 C++ 部署 YOLO11 分类 ONNX 推理实战 简介本资源面向希望在Windows CPU环境下部署图像分类模型的C开发者与边缘计算实践者提供基于YOLOv11的轻量级ONNX推理完整工程。核心代码采用纯C编写覆盖预处理、模型加载、推理与后处理全流程并针对CPU进行多线程加速实测Intel i5-12400F单帧推理约120ms效率优于Python版本。工程支持直接替换自定义ONNX模型兼容YOLOv8/v11等架构可一键切换为检测或分割任务适用于工业摄像头、树莓派等边缘设备及C项目集成深度学习模型。压缩包共365个文件约363.02MB以hpp与h头文件为主辅以cmake构建脚本、dll动态库、exe可执行文件及onnx模型与标签文件目录结构清晰。资源内含环境配置指南、API接口说明与常见问题排查文档新手可快速上手。目前已有104人学习下载适合需要验证CPU端推理性能或集成模型的中高级开发者参考。1. cppYolo11OnnxPredict在 Windows CPU 上跑通 YOLO11 分类推理很多做工业质检、边缘设备或者桌面工具的朋友手里只有一台普通 Windows 办公机没有独立显卡却想用 YOLO11 做图像分类推理。这时候第一反应往往是装 PyTorch、配 CUDA结果被环境折腾一整天最后发现 CPU 版本跑得慢、依赖还冲突。cppYolo11OnnxPredict 这个方向解决的正是这件事把 YOLO11 分类模型导出成 ONNX用 C 配合 ONNX Runtime 在 Windows CPU 上做推理并且代码结构支持直接替换模型文件。它适合两类人一类是想把 Python 训练好的模型落地成独立 exe 的工程师另一类是需要在无 GPU 的 Windows 机器上做批量图片分类的开发者。整条链路的核心是「导出 ONNX → C 加载 → 预处理 → 推理 → 后处理」每一步都有明确的参数和坑点下面按落地顺序拆开讲。2. 为什么选 ONNX Runtime C 而不是 Python 部署2.1 CPU 推理场景下 ONNX Runtime 的实际优势在 Windows CPU 上做推理可选方案有 PyTorch CLibTorch、OpenCV DNN、ONNX Runtime 几种。LibTorch 的包体积大一个 release 版本解压后动辄 1GB 以上而且和 PyTorch 版本强绑定升级模型时容易连带升级整个运行时。OpenCV DNN 对 YOLO 系列的支持依赖版本YOLO11 的新算子不一定能直接吃进去。ONNX Runtime 的优势在于运行时体积小CPU 版核心 dll 约十几 MB算子覆盖跟得上 ONNX opset 更新而且 C API 稳定模型换版本时只要重新导出 ONNX 即可不用动 C 代码。另一个实际考量是部署环境。很多工厂现场的 Windows 机器是 Win10 LTSC 或者 Win7 升级上来的装 Python 环境本身就有风险更别说 pip 装 torch。用 C 编译出一个 exe配合几个 dll拷贝过去就能跑这是最省心的交付方式。ONNX Runtime 官方提供预编译的 Windows 包直接下载解压就能链接不需要自己编译。从性能上看YOLO11 分类模型比如 yolo11n-cls输入 224x224在普通 i5 上单张推理大约 20-40ms用 ONNX Runtime 的 CPU EP 加上合理的线程数设置能跑到接近理论值。如果换成 PyTorch CPU 版本同样的模型往往要多花 30% 以上的时间因为 PyTorch 的 CPU 推理路径没有 ONNX Runtime 那么针对推理做过图优化。2.2 从 PyTorch 导出 YOLO11 分类 ONNX 的完整命令导出这一步决定了后面 C 能不能顺利加载。YOLO11 分类模型用 ultralytics 库导出时要注意 opset 版本和动态轴设置。下面是我常用的导出脚本from ultralytics import YOLO # 加载训练好的分类模型这里以 yolo11n-cls 为例 model YOLO(yolo11n-cls.pt) # 导出 ONNX指定输入尺寸和 opset model.export( formatonnx, imgsz224, # 分类模型常用 224也可用 640 opset12, # opset 12 兼容性好ONNX Runtime 1.10 都支持 simplifyTrue, # 用 onnx-simplifier 简化图结构 dynamicFalse, # CPU 部署固定 batch 更稳 halfFalse # CPU 不支持 fp16必须关掉 )导出后会在同目录生成yolo11n-cls.onnx。这里几个参数值得展开说opset12是保守选择如果你用的 ONNX Runtime 版本较新可以用 17但 12 能覆盖绝大多数 Windows 部署环境。simplifyTrue会调用 onnx-simplifier 做常量折叠和冗余节点消除对推理速度有 5%-10% 的提升但偶尔会引入兼容问题如果加载报错可以先关掉试试。dynamicFalse表示固定输入尺寸CPU 推理时固定 shape 能让 ONNX Runtime 做更充分的内存规划。halfFalse必须强调CPU 上 fp16 要么不支持要么被转成 fp32开了反而多一层转换。导出完成后建议用 Python 的 onnxruntime 先验证一遍确认模型能加载且输出 shape 符合预期import onnxruntime as ort import numpy as np sess ort.InferenceSession(yolo11n-cls.onnx, providers[CPUExecutionProvider]) input_name sess.get_inputs()[0].name # 构造一个假输入NCHW 格式 dummy np.random.randn(1, 3, 224, 224).astype(np.float32) outputs sess.run(None, {input_name: dummy}) print(输出 shape:, outputs[0].shape) # 应该是 (1, 类别数)这一步能跑通说明 ONNX 文件本身没问题后面 C 加载失败就大概率是环境或代码问题而不是模型问题。2.3 C 工程里 ONNX Runtime 的引入方式Windows 上用 C 调 ONNX Runtime有两种引入方式一种是下载官方预编译包手动配置 include 和 lib 路径另一种是用 vcpkg 安装。我一般推荐第一种因为可控性强不依赖包管理器。从 ONNX Runtime 官方 release 页面下载onnxruntime-win-x64-1.x.x.zip解压后目录结构是onnxruntime-win-x64-1.17.0/ ├── include/ # 头文件 ├── lib/ # onnxruntime.lib 等 └── bin/ # onnxruntime.dll 等运行时在 Visual Studio 工程里需要做三件事在「附加包含目录」加上include路径在「附加库目录」加上lib路径在「附加依赖项」里加上onnxruntime.lib。编译完成后把bin目录下的 dll 拷贝到 exe 同目录否则运行时会报找不到 dll。如果用 CMake可以这样写cmake_minimum_required(VERSION 3.15) project(Yolo11ClsCpp) set(ONNXRUNTIME_DIR C:/onnxruntime-win-x64-1.17.0) include_directories(${ONNXRUNTIME_DIR}/include) link_directories(${ONNXRUNTIME_DIR}/lib) add_executable(yolo11_cls main.cpp) target_link_libraries(yolo11_cls onnxruntime)这里ONNXRUNTIME_DIR换成你实际解压的路径。注意路径里不要有中文和空格否则 CMake 解析可能出问题这是 Windows 上很常见的翻车点。3. C 推理代码的四个核心环节3.1 图像预处理从 cv::Mat 到模型输入张量YOLO11 分类模型的预处理流程是resize 到 224x224、BGR 转 RGB、归一化到 [0,1]、按 ImageNet 均值方差标准化、HWC 转 CHW、加 batch 维度。用 OpenCV 读图后代码大致如下#include opencv2/opencv.hpp #include vector // 输入BGR 的 cv::Mat输出float 数组形状 1x3x224x224 std::vectorfloat preprocess(const cv::Mat img) { cv::Mat resized, rgb, float_img; // 1. resize 到 224x224 cv::resize(img, resized, cv::Size(224, 224)); // 2. BGR - RGB cv::cvtColor(resized, rgb, cv::COLOR_BGR2RGB); // 3. 转 float 并归一化到 [0,1] rgb.convertTo(float_img, CV_32FC3, 1.0 / 255.0); // 4. 按 ImageNet 均值方差标准化 cv::Scalar mean(0.485, 0.456, 0.406); cv::Scalar std(0.229, 0.224, 0.225); std::vectorcv::Mat channels(3); cv::split(float_img, channels); for (int i 0; i 3; i) { channels[i] (channels[i] - mean[i]) / std[i]; } cv::merge(channels, float_img); // 5. HWC - CHW并展平 std::vectorfloat input_tensor(1 * 3 * 224 * 224); for (int c 0; c 3; c) { for (int h 0; h 224; h) { for (int w 0; w 224; w) { input_tensor[c * 224 * 224 h * 224 w] float_img.atcv::Vec3f(h, w)[c]; } } } return input_tensor; }这段代码里最容易出错的是均值方差和通道顺序。YOLO11 分类模型训练时用的是 ImageNet 的 mean/std如果你导出时用了halfTrue或者自定义了归一化这里必须对应改。另外cv::cvtColor之后float_img的通道顺序已经是 RGB后面 split 出来的 channels[0] 就是 R 通道顺序不能乱。如果预处理错了模型输出会完全乱掉但不会报错这是最隐蔽的坑。3.2 创建会话与运行推理Ort::Session 的正确用法ONNX Runtime 的 C API 用起来比 Python 啰嗦但结构清晰。核心对象是Ort::Env、Ort::Session、Ort::SessionOptions。下面是一个完整的推理函数#include onnxruntime_cxx_api.h // 全局或类成员 Ort::Env env(ORT_LOGGING_LEVEL_WARNING, yolo11_cls); Ort::SessionOptions session_options; session_options.SetIntraOpNumThreads(4); // 设置线程数 session_options.SetGraphOptimizationLevel( GraphOptimizationLevel::ORT_ENABLE_ALL); // 加载模型 Ort::Session session(env, Lyolo11n-cls.onnx, session_options); // 获取输入输出信息 Ort::AllocatorWithDefaultOptions allocator; auto input_name session.GetInputNameAllocated(0, allocator); auto output_name session.GetOutputNameAllocated(0, allocator); // 构造输入张量 std::vectorint64_t input_shape {1, 3, 224, 224}; std::vectorfloat input_data preprocess(img); auto memory_info Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor Ort::Value::CreateTensorfloat( memory_info, input_data.data(), input_data.size(), input_shape.data(), input_shape.size()); // 运行推理 const char* input_names[] {input_name.get()}; const char* output_names[] {output_name.get()}; auto outputs session.Run(Ort::RunOptions{nullptr}, input_names, input_tensor, 1, output_names, 1); // 取输出 float* output_data outputs[0].GetTensorMutableDatafloat(); auto output_shape outputs[0].GetTensorTypeAndShapeInfo().GetShape(); int num_classes output_shape[1];几个关键点SetIntraOpNumThreads控制单次推理内部的并行线程数CPU 上一般设成物理核心数设太大反而因为线程切换变慢。ORT_ENABLE_ALL会启用所有图优化包括算子融合对 CPU 推理有实际收益。Ort::Session的模型路径在 Windows 上要用宽字符L...这是 ONNX Runtime C API 在 Windows 上的一个约定用窄字符会编译报错或者运行时找不到文件。3.3 后处理从输出向量到分类结果YOLO11 分类模型的输出是未经 softmax 的 logits形状[1, num_classes]。要得到最终类别需要做 softmax 然后取 argmax。代码#include cmath #include algorithm // 对 logits 做 softmax std::vectorfloat softmax(const float* logits, int n) { std::vectorfloat probs(n); float max_val *std::max_element(logits, logits n); float sum 0.0f; for (int i 0; i n; i) { probs[i] std::exp(logits[i] - max_val); sum probs[i]; } for (int i 0; i n; i) { probs[i] / sum; } return probs; } // 取 top-1 auto probs softmax(output_data, num_classes); int best_idx std::distance(probs.begin(), std::max_element(probs.begin(), probs.end())); float confidence probs[best_idx];这里减去max_val是为了数值稳定性防止 exp 溢出。分类模型不需要 NMS所以后处理比检测模型简单很多。如果你要 top-5可以用 partial_sort 或者维护一个大小为 5 的小顶堆。实际部署时类别名称通常存在一个 txt 文件里按行读取索引对应 best_idx 即可。3.4 模型替换只改一个路径就能换模型这个方案支持直接替换模型关键在于 C 代码里不要硬编码类别数。类别数从输出 shape 动态获取类别名称从外部 txt 读取。替换模型时只需要把新的 ONNX 文件放到指定目录修改代码里的模型路径如果类别数变了更新类别名称文件。不需要重新编译代码。我一般会把模型路径和类别文件路径做成命令行参数或者配置文件// 从命令行读取模型路径 int main(int argc, char** argv) { std::string model_path yolo11n-cls.onnx; std::string label_path labels.txt; if (argc 3) { model_path argv[1]; label_path argv[2]; } // ... 加载模型和标签 }这样交付时用户拿到 exe 和 dll自己换 onnx 和 labels.txt 就行不用碰代码。注意 ONNX Runtime 的Ort::Session构造函数在 Windows 上需要宽字符路径如果从std::string转要用std::wstring转换函数比如std::wstring(model_path.begin(), model_path.end())但这对中文路径不生效所以模型路径最好全英文。4. 避坑与排查Windows CPU 部署的五个血泪教训4.1 加载模型报「找不到指定模块」现象编译通过运行 exe 时弹窗或控制台报「找不到 onnxruntime.dll」或者「无法加载 onnxruntime.dll」。原因ONNX Runtime 的 dll 没有放到 exe 同目录或者放错了版本x64 的 exe 配了 x86 的 dll。解决把onnxruntime-win-x64-1.x.x/bin目录下的所有 dll 拷贝到 exe 所在目录。如果还报错用 Dependency Walker 或者dumpbin /dependents检查 exe 依赖了哪些 dll缺哪个补哪个。注意 Visual Studio 调试时工作目录可能是工程目录而不是 exe 目录要在项目属性里把「调试 → 工作目录」设成$(OutDir)。4.2 推理结果全是同一个类别现象不管输入什么图片输出都是第 0 类或者某个固定类别置信度还很高。原因预处理和训练时不一致。最常见的是归一化参数错了或者 BGR/RGB 没转或者 resize 的插值方式不同。解决先用 Python 的 onnxruntime 跑同一张图确认 Python 端结果正常。然后把 C 预处理后的张量前几个值打印出来和 Python 端对比。如果对不上逐项检查 resize 尺寸、颜色通道、归一化系数。YOLO11 分类默认用 ImageNet 的 mean/std但如果你训练时改了导出 ONNX 后 C 也要跟着改。4.3 多线程推理时结果错乱现象单张推理正常多线程并发调用同一个Ort::Session时结果随机错乱或者崩溃。原因Ort::Session本身是线程安全的但如果你把输入输出张量做成全局变量多个线程会互相覆盖。解决每个线程创建自己的输入张量和输出缓冲区Ort::Session可以共享。或者用线程池每个线程独立调用session.Run。ONNX Runtime 的Run方法是线程安全的但前提是输入输出内存不共享。4.4 模型文件路径含中文导致加载失败现象模型放在中文目录下Ort::Session构造时抛异常提示找不到文件。原因ONNX Runtime 在 Windows 上使用宽字符 API但如果你用std::string转std::wstring的简单方式中文会乱码。解决模型路径和类别文件路径全部用英文不要放在中文目录下。如果必须支持中文路径用MultiByteToWideChar做正确的编码转换代码会多几行但能避免玄学问题。4.5 Debug 模式推理速度极慢现象Release 模式单张 30msDebug 模式要 300ms 甚至更久。原因Debug 模式下编译器不优化ONNX Runtime 的图优化也可能被禁用而且 Debug 版 dll 本身性能就差。解决部署和性能测试一律用 Release 模式。如果需要在 Debug 下调试可以接受速度慢但不要用 Debug 的耗时去评估方案可行性。另外Release 模式下要确保链接的是 Release 版的 onnxruntime.lib混用 Debug/Release 会导致运行时崩溃。5. 进阶技巧用 OpenMP 和批处理把 CPU 吃满单张推理在 CPU 上很难吃满所有核心因为预处理和后处理是串行的。如果要做批量图片分类可以把 batch size 设成 4 或 8一次推理多张图这样 ONNX Runtime 能更好地利用多核。导出 ONNX 时把dynamic设为 True输入 shape 写成[batch, 3, 224, 224]C 端构造对应大小的输入张量即可。另一个技巧是用 OpenMP 并行预处理。读图和 resize 是 CPU 密集型操作用#pragma omp parallel for把预处理并行化能显著缩短批量处理的总时间。下面是一个批量推理的骨架#include omp.h std::vectorstd::string image_paths /* ... */; int batch_size 8; int num_batches (image_paths.size() batch_size - 1) / batch_size; for (int b 0; b num_batches; b) { std::vectorcv::Mat batch_imgs(batch_size); #pragma omp parallel for for (int i 0; i batch_size; i) { int idx b * batch_size i; if (idx image_paths.size()) { batch_imgs[i] cv::imread(image_paths[idx]); } } // 构造 batch 输入张量形状 [batch_size, 3, 224, 224] // ... 调用 session.Run }这里batch_size要根据内存和 CPU 核心数调一般设成核心数的 1-2 倍。设太大内存占用高设太小并行度不够。我一般会在 i5 上设 8i7 上设 16实测吞吐量能比单张循环提升 2-3 倍。验证方法很简单准备 100 张测试图分别用单张循环和批处理跑一遍用std::chrono计时对比总耗时和 CPU 占用率。如果批处理没有明显提升检查 ONNX 导出时dynamic是否开启以及SetIntraOpNumThreads是否设成了物理核心数。最后说一个我自己的习惯每次换模型或者换机器先跑一个 10 张图的小测试集把每张图的 top-1 类别和置信度打印出来和 Python 端的结果逐张对比。只要有一张对不上就停下来查预处理不要急着上批量。这个习惯帮我省了很多次返工。希望帮到你。本文还有配套的精品资源点击获取
返回列表