
简介这份资源面向希望在Windows CPU环境下部署图像分类模型的C开发者与边缘计算实践者提供基于YOLOv11的ONNX推理完整工程无需GPU即可运行。包内共365个文件以197个hpp与74个h头文件构成核心代码骨架辅以cmake构建脚本、dll与lib运行库、exe可执行程序及onnx模型文件压缩包约363MB目录按src源码与models模型分层组织。已有104人学习下载。工程覆盖预处理、模型加载、推理与后处理全流程支持直接替换自定义ONNX模型兼容YOLOv8/v11等架构并针对CPU做多线程加速实测i5-12400F单帧约120ms。随包附带1000类ImageNet标签与预训练模型配合环境配置指南、API说明与常见问题排查文档可帮助读者快速验证CPU端推理性能或将深度学习模型集成进工业摄像头、树莓派等边缘设备项目。1. cppYolo11OnnxPredict 到底解决什么问题Windows 纯 CPU 跑 YOLO11 分类的落地路径很多做工业视觉的团队手里只有一台 Windows 工控机没有独显也不方便装 CUDA却要在产线上跑一个图像分类模型。这时候 cppYolo11OnnxPredict 这类方案的价值就出来了它把 YOLO11 训练出来的分类权重导出成 ONNX再用 C 在 Windows 上通过 ONNX Runtime 的 CPU 后端推理整个链路不依赖 Python 运行时也不依赖显卡。标题里说的「可直接替换模型」指的就是换掉那个 .onnx 文件、同步改一下类别标签代码不用重编。这篇文章面向的是需要在 Windows CPU 环境里把 YOLO11 分类模型真正跑起来的人从环境搭建、模型导出、C 推理代码到参数调优和踩坑一步步讲清楚。如果你正在纠结要不要走这条路先记住一个结论CPU 上跑 YOLO11 分类单张 224 输入在普通桌面 CPU 上做到几十毫秒是现实的但前提是导出和预处理别翻车。2. 从 PyTorch 权重到 ONNXYOLO11 分类模型导出的关键参数2.1 为什么分类任务也要走 ONNX 而不是直接上 LibTorchYOLO11 在 Ultralytics 的体系里同时支持检测、分割、分类等任务分类模型通常是 yolo11n-cls.pt 这类权重。要在 C 里推理常见做法有两条一是用 LibTorch 直接加载 TorchScript二是导出 ONNX 再用 ONNX Runtime。我一般会选 ONNX原因有三个。第一ONNX Runtime 在 Windows 上的 CPU 推理做了大量图优化和算子融合尤其是对卷积和矩阵乘的优化比裸 LibTorch 更省心。第二ONNX 是跨框架的中间格式模型文件小、加载快部署时不用带一整套 PyTorch 的动态库。第三标题强调「可直接替换模型」ONNX 文件替换后只要输入输出形状一致C 侧几乎不用动而 TorchScript 换模型往往要重新确认序列化版本。需要说清楚的是YOLO11 分类模型的输出不是检测框而是一个类别概率向量。导出时输入通常是 1x3x224x224输出是 1xN 的 logits 或 softmax 后的概率。这一点决定了后面 C 预处理和后处理的写法和检测模型完全不同别把检测那套 NMS 逻辑搬过来。2.2 导出 ONNX 的完整命令与参数说明导出这一步在 Python 环境里做装好 ultralytics 之后用下面这段脚本。注意 opset 版本和动态轴的处理这是后面 C 能不能顺利加载的关键。from ultralytics import YOLO # 加载官方分类权重也可以换成自己训练出来的 best.pt model YOLO(yolo11n-cls.pt) # 导出 ONNX指定输入尺寸和 opset model.export( formatonnx, imgsz224, # 分类模型常用 224和训练时保持一致 opset12, # 12 兼容性好Windows 上 ORT 支持稳定 simplifyTrue, # 简化计算图去掉冗余算子 dynamicFalse, # 固定 batch 和尺寸CPU 推理更快 halfFalse # CPU 不支持 fp16必须关掉 )这段代码执行完会在权重同目录生成 yolo11n-cls.onnx。几个参数值得展开说。imgsz 必须和训练时的输入一致YOLO11 分类默认是 224如果你训练时改了尺寸这里要同步改否则精度会掉。opset 选 12 是比较稳的选择太新的 opset 在部分 ONNX Runtime 版本上会遇到算子不支持。simplifyTrue 会调用 onnxsim 做图简化能去掉一些恒等算子和多余的 transpose对 CPU 推理速度有实际帮助。dynamicFalse 表示固定输入形状CPU 上固定形状能让 ORT 做更激进的优化如果你确实需要变尺寸输入再开动态轴但分类任务一般不需要。halfFalse 一定要确认CPU 后端不支持 fp16开了导出会失败或者推理报错。导出完成后建议用 Python 侧的 onnxruntime 先验证一遍确认模型能加载、输出形状符合预期再去写 C。这一步能省掉大量在 C 里排查模型问题的时间。import onnxruntime as ort import numpy as np sess ort.InferenceSession(yolo11n-cls.onnx, providers[CPUExecutionProvider]) # 打印输入输出信息确认形状 for i in sess.get_inputs(): print(input:, i.name, i.shape, i.type) for o in sess.get_outputs(): print(output:, o.name, o.shape, o.type) # 造一个假输入跑一遍 dummy np.random.randn(1, 3, 224, 224).astype(np.float32) out sess.run(None, {sess.get_inputs()[0].name: dummy}) print(output shape:, out[0].shape)正常情况下你会看到输入是 [1, 3, 224, 224]输出是 [1, 1000]ImageNet 类别数或者你自己训练的类别数。如果输出形状不对先回去检查导出参数别急着写 C。2.3 自训练模型的替换要点标题里「可直接替换模型」是很多人的核心诉求。替换时要注意三件事。第一类别数变了C 里读取输出的维度要跟着变不能写死 1000。第二类别标签文件要同步替换通常是一个 classes.txt每行一个类别名顺序必须和训练时一致。第三如果自训练模型改了输入尺寸导出时的 imgsz 和 C 预处理里的 resize 尺寸要一起改。这三点任何一处没对齐结果就是分类全错但程序不报错属于最隐蔽的坑。3. Windows 上搭 C 推理工程ONNX Runtime 配置与最小可跑代码3.1 ONNX Runtime 的获取与工程配置Windows 上用 C 调 ONNX Runtime最省事的方式是下载官方预编译包里面有头文件、lib 和 dll。把 include 目录加到工程的头文件搜索路径把 lib 目录加到库搜索路径链接 onnxruntime.lib运行时把 onnxruntime.dll 放到 exe 同目录。如果你用 Visual Studio直接在项目属性里配如果用 CMake写一个 find 逻辑或者手动指定路径都行。一个容易忽略的点是架构匹配。ONNX Runtime 的预编译包分 x64 和 x86你的工程也要对应。现在基本都是 x64但老工控机上偶尔遇到 32 位系统那就得找对应版本。另外Debug 和 Release 用的 lib 可能不同混用会链接失败这个坑我踩过报错信息还很不直观。3.2 图像预处理CPU 推理里最容易被低估的一环YOLO11 分类模型的预处理包括 resize 到 224x224、归一化到 0-1、按 ImageNet 均值方差标准化、HWC 转 CHW。这几步在 Python 里一行 transforms 就搞定在 C 里要自己写而且写错了不会报错只会让精度悄悄下降。// 假设已用 OpenCV 读入图像 cv::Mat img cv::Mat resized; cv::resize(img, resized, cv::Size(224, 224)); // 双线性插值和训练时一致 // 转 float 并归一化 resized.convertTo(resized, CV_32F, 1.0 / 255.0); // 按 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(resized, channels); for (int c 0; c 3; c) { channels[c] (channels[c] - mean[c]) / std[c]; } cv::merge(channels, resized); // HWC 转 CHW构造 1x3x224x224 的输入 std::vectorfloat inputTensorValues(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) { inputTensorValues[c * 224 * 224 h * 224 w] channels[c].atfloat(h, w); } } }这段代码里resize 的插值方式要和训练时一致Ultralytics 默认用的是双线性。均值方差是 ImageNet 的标准值如果你训练时用了别的归一化参数这里必须同步改。HWC 转 CHW 的顺序不能错错了模型输出会完全乱掉。很多人在这一步图省事结果精度对不上还找不到原因属于典型的血泪经验。3.3 用 ONNX Runtime C API 跑通一次推理下面是最小的推理代码骨架包含创建会话、构造输入张量、运行、取输出。#include onnxruntime_cxx_api.h #include vector #include iostream int main() { // 创建环境日志级别设为警告避免刷屏 Ort::Env env(ORT_LOGGING_LEVEL_WARNING, cls); Ort::SessionOptions sessionOptions; sessionOptions.SetIntraOpNumThreads(4); // CPU 线程数按核数调 sessionOptions.SetGraphOptimizationLevel( GraphOptimizationLevel::ORT_ENABLE_ALL); // 加载模型宽字符路径在 Windows 上更稳 Ort::Session session(env, Lyolo11n-cls.onnx, sessionOptions); // 获取输入输出名称 Ort::AllocatorWithDefaultOptions allocator; auto inputName session.GetInputNameAllocated(0, allocator); auto outputName session.GetOutputNameAllocated(0, allocator); // 输入形状 1x3x224x224 std::vectorint64_t inputShape {1, 3, 224, 224}; std::vectorfloat inputData(1 * 3 * 224 * 224, 0.0f); // 这里填入上一节的预处理结果 inputTensorValues auto memoryInfo Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, OrtMemTypeDefault); Ort::Value inputTensor Ort::Value::CreateTensorfloat( memoryInfo, inputData.data(), inputData.size(), inputShape.data(), inputShape.size()); // 运行推理 const char* inputNames[] {inputName.get()}; const char* outputNames[] {outputName.get()}; auto outputs session.Run(Ort::RunOptions{nullptr}, inputNames, inputTensor, 1, outputNames, 1); // 取输出并找最大类别 float* outData outputs[0].GetTensorMutableDatafloat(); auto outShape outputs[0].GetTensorTypeAndShapeInfo().GetShape(); int numClasses static_castint(outShape[1]); int bestIdx 0; float bestScore outData[0]; for (int i 1; i numClasses; i) { if (outData[i] bestScore) { bestScore outData[i]; bestIdx i; } } std::cout class index: bestIdx , score: bestScore std::endl; return 0; }几个关键点。SetIntraOpNumThreads 控制算子内部并行线程数CPU 上设成物理核数比较合适设太大反而因为线程切换变慢。SetGraphOptimizationLevel 用 ORT_ENABLE_ALL 开启全部图优化。模型路径用宽字符Windows 上中文路径不会出问题。输出如果是 logits找最大值就是预测类别如果导出时带了 softmax最大值就是置信度。判断一下你的模型输出是哪种可以在 Python 侧验证时看数值范围logits 会有负数概率都在 0 到 1 之间。3.4 线程数与批处理的取舍CPU 推理的吞吐优化主要靠两点线程数和批处理。线程数方面单张推理时 IntraOpNumThreads 设成物理核数通常最优但如果你要同时跑多个模型实例就要把线程数降下来避免争抢。批处理方面分类模型支持 batch 输入把多张图拼成一个 batch 能提高吞吐但单张延迟会上升。产线上如果是一张一张来batch 设 1如果是离线批量处理batch 设 8 或 16 能明显提升总吞吐。这个取舍没有标准答案要按你的实际场景测。4. 避坑与排查CPU 部署 YOLO11 分类最常见的五类问题4.1 推理结果全是同一类或置信度异常现象是无论输入什么图输出都是同一个类别或者置信度分布很平。原因通常是预处理和训练时不一致最常见的是归一化参数写错、通道顺序搞反、resize 插值方式不同。解决方法是拿一张训练集里的图分别在 Python 和 C 里跑对比预处理后的张量数值逐元素比对差异大的地方就是问题所在。这个对比方法很笨但最有效。4.2 加载模型时报算子不支持现象是创建 Session 时抛异常提示某个算子不支持。原因一般是导出时 opset 版本太高或者用了 ONNX Runtime 还没实现的算子。解决办法是把 opset 降到 12 重新导出如果还不行就检查是不是用了自定义算子。另外确认一下 ONNX Runtime 的版本太老的版本对新算子支持差换个较新的稳定版通常能解决。4.3 程序运行时报找不到 dll现象是 exe 双击没反应或者命令行报缺少 onnxruntime.dll。原因是 dll 没放到 exe 同目录或者放的是错误架构的版本。解决方法是把对应架构的 onnxruntime.dll 复制到 exe 旁边用 Dependency Walker 之类的工具确认依赖是否齐全。Debug 和 Release 用的 dll 也可能不同别混用。4.4 推理速度远低于预期现象是单张推理要几百毫秒甚至更久。原因可能是线程数没设、图优化没开、输入尺寸太大、或者模型本身太大。解决方法是先确认 SetGraphOptimizationLevel 开了 ORT_ENABLE_ALL再调 IntraOpNumThreads然后考虑换更小的模型比如 yolo11n 而不是 yolo11l。如果还慢用性能分析工具看时间花在哪有时候瓶颈在预处理而不是推理本身。4.5 替换模型后类别对不上现象是换了自训练的 onnx 和标签文件结果类别名和预测对不上。原因是标签文件顺序和训练时不一致或者类别数变了但代码里写死了。解决方法是确认 classes.txt 的顺序和训练时的 data.yaml 里 names 顺序完全一致代码里读取类别数用输出维度动态获取不要写死。这个坑很隐蔽因为程序不报错只是结果错。5. 进阶技巧用动态批处理和量化把 CPU 推理再压一压5.1 动态批处理在分类任务里的实际收益分类任务和检测不同输入尺寸固定天然适合批处理。如果你的场景是离线批量处理图片把 batch 开到 8 或 16吞吐能提升两到三倍。做法是在导出 ONNX 时把 batch 轴设为动态C 侧按实际 batch 构造输入张量。注意动态轴会让 ORT 少一些优化所以如果 batch 固定直接导出固定 batch 的模型更快。我一般会导出两个版本一个 batch1 用于实时一个 batch16 用于离线按场景切换。5.2 ONNX Runtime 的量化能带来多少提速ONNX Runtime 支持动态量化把 fp32 权重转成 int8模型体积减半CPU 推理速度通常能提升 1.5 到 2 倍精度损失在分类任务上一般小于 1 个百分点。量化用 Python 侧的 onnxruntime.quantization 工具做命令很简单。from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( model_inputyolo11n-cls.onnx, model_outputyolo11n-cls-int8.onnx, weight_typeQuantType.QInt8 )量化后的模型在 C 侧加载方式和原来完全一样不用改代码。但要注意量化对某些层敏感如果精度掉得厉害可以用 QuantType.QUInt8 或者只量化部分层。量化不是万能的建议量化后在验证集上跑一遍确认精度可接受再上线。5.3 一个我常用的验证习惯每次换模型或者改预处理我都会准备一组固定的测试图大概二十张覆盖各个类别然后在 Python 和 C 两侧分别跑对比 top-1 结果是否一致。只要有一张不一致就说明预处理或后处理有问题必须查清楚再继续。这个习惯帮我省了很多次上线后才发现精度不对的麻烦。CPU 部署 YOLO11 分类这件事难点从来不在推理本身而在预处理对齐和模型替换的细节上。把这两块做扎实剩下的就是调参和优化。希望帮到你。本文还有配套的精品资源点击获取