
简介这是一份面向具备C与深度学习基础的计算机视觉研发人员的YOLOv11-CLS图像分类模型部署资料聚焦如何用ONNX Runtime在本地高效完成模型加载、图像预处理、推理输出与置信度阈值调整。内容覆盖数据准备、完整C示例代码及其逐行解释、运行步骤和项目总结并给出量化剪枝、RESTful API等后续改进方向适合自动化图像检测、实时视频流监控等落地场景。压缩包为1个docx文档大小37KB目录层级清晰便于按项目介绍、代码示例、运行说明等模块快速查阅。目前已有1508人学习下载可作为从ONNX Runtime接口调用到分类结果统计的入门实践参考。1. 用 C 把 YOLOv11-CLS 跑起来一份能直接复现的 ONNX Runtime 部署工程做过模型部署的同学都有体会Python 里跑通一个分类模型不算本事真正麻烦的是把模型塞进 C 工程还得保证预处理、推理、后处理这条链路不出幺蛾子。这份资源做的就是这件事用 C 和 ONNX Runtime 对 YOLOv11-CLS 图像分类模型做本地部署工程里带了完整的程序代码和数据样例从模型加载、图像预处理到推理输出置信度一条龙给你铺好。我拆完这份项目后的判断是它适合有一定 C 和深度学习基础、正在做视觉推理落地的研发人员尤其是想把分类模型跑在监控、质检、边缘盒子这类不能依赖 Python 环境的场景里的人。项目里最值钱的部分不是那几行推理代码而是它把 OpenCV 预处理、ONNX Runtime 会话管理、置信度过滤这些环节串成了一个完整范式你换模型、换类别、换输入尺寸都能套用。接下来我把它的工程结构、代码链路、编译步骤和几个容易翻车的细节全部拆开讲你照着走一遍就知道这份资源值不值得下载。2. ONNX Runtime 部署前的准备模型导出、依赖库和目录规划2.1 为什么选 ONNX Runtime 而不是直接上 TensorRT 或 OpenVINO先聊个选型问题。YOLOv11 官方生态里其实有各种部署方案TensorRT 在 NVIDIA 显卡上性能最猛OpenVINO 在 Intel 平台上有优化那为什么这份项目选 ONNX Runtime核心原因是通用性和调试成本。ONNX Runtime 不绑定特定硬件CPU 能跑、CUDA 能跑、甚至 RK3588 这类边缘芯片的 NPU 也有对应后端。对一个以先把流程跑通为目标的工程来说ONNX Runtime 是性价比最高的起点。而且 ONNX 格式本身就是模型转换的中间标准你从 PyTorch 导出 ONNX 之后后续想切 TensorRT 或 OpenVINO拿着同一个 ONNX 文件就能继续不至于推倒重来。另一个实际考量是 C 接口的成熟度。ONNX Runtime 的 C API 封装得比较干净Ort::Session、Ort::Value这些核心类用起来直观配合 OpenCV 做图像读写和预处理整个工程不需要引入额外的第三方依赖。这份项目的主体代码就只用了 OpenCV 和 ONNX Runtime 两个库对新手非常友好。2.2 前置环境清单和验证方法在动代码之前先把环境确认清楚省得到时候编译报一堆莫名其妙的错。我按这份工程的依赖整理了一个清单组件版本建议验证方式CMake3.16cmake --versionOpenCV4.xpkg-config --modversion opencv4ONNX Runtime1.15解压后检查 lib/ 下有 libonnxruntime.sog支持 C17g --versionPyTorch导出模型用2.xpython -c import torch; print(torch.__version__)这里有个细节要注意ONNX Runtime 的 C 库需要自己从 GitHub Releases 下载预编译包它没有提供系统级安装方式。下载时看清楚平台选项Linux 就选onnxruntime-linux-x64-*.tgzWindows 选对应的 zip 包解压后把 include 和 lib 路径配进工程就行。OpenCV 在 Ubuntu 上可以直接apt install libopencv-dev但如果你要跑在 ARM 板子上就得从源码编译那又是另一套流程。建议 x86 平台先用 apt 装验证通过后再考虑交叉编译。2.3 从 PyTorch 导出 ONNX 模型的关键操作这份项目的代码里默认模型路径是yolov11_cls.onnx但并没有提供现成的 ONNX 文件需要你自己从训练好的 PyTorch 权重导出。导出这一步是很多人第一次翻车的地方我给出一个标准的导出脚本import torch from ultralytics import YOLO # 加载训练好的权重 model YOLO(yolov11-cls.pt) # 构造一个假输入batch1, 3通道, 224x224 dummy_input torch.randn(1, 3, 224, 224) # 导出为 ONNX model.export(formatonnx, imgsz224, opset12)导出后你会得到一个yolov11-cls.onnx文件。这里有两个参数值得展开说一下。imgsz决定模型的输入分辨率C 端的INPUT_SIZE必须和这个值保持一致否则推理时会拿到的输出张量维度跟你预期的不一样甚至直接报错。opset是 ONNX 的算子集版本建议用 12 以上太低的版本有些新算子导出不了。导出完成后强烈建议用 Netron 打开 ONNX 文件看一眼输入输出节点的名字和维度。这是整个部署流程里最容易被忽略的一步很多人在 C 端写了inputNames {input}但实际模型里的输入节点叫images结果运行时直接报错找不到输入。别问我是怎么知道的。2.4 数据目录结构和类别标签文件规划这份项目的数据准备思路很规范训练和推理的数据是分开管理的。推理阶段需要的数据结构如下dataset/ ├── images/ │ ├── image_1.jpg │ ├── image_2.jpg └── labels/ ├── image_1.txt └── image_2.txtimages 目录放待分类的图像labels 目录放对应的标签文件。标签文件的内容是类别索引比如 0 代表猫。这里注意如果你的数据集类别很多建议把类别名和索引的映射关系单独存在一个classes.txt文件里C 端读取后填充CLASS_LABELS向量而不是像我最初写代码那样硬编码在源码里。硬编码的问题在于换数据集就要重新编译工程化角度不可取。3. 核心代码链路拆解从图像读取到置信度输出的完整实现3.1 主函数的设计思路和整体流程先看这份工程的代码结构设计。它的主函数流程非常清晰分成了六个步骤初始化 ONNX 环境 → 加载模型 → 读取图像 → 预处理 → 推理 → 输出结果。int main() { // 初始化 ONNX 运行时环境 Ort::Env env(ORT_LOGGING_LEVEL_WARNING, YOLOv11-CLS); // 加载模型 Ort::Session session loadModel(MODEL_PATH, env); // 读取输入图像 cv::Mat image cv::imread(input.jpg); if (image.empty()) { std::cerr Could not read the image! std::endl; return -1; } // 图像预处理 cv::Mat input preprocessImage(image); // 执行推理 auto [outputShape, output] runInference(session, input); // 输出结果 for (size_t i 0; i output.size(); i) { if (output[i] CONFIDENCE_THRESHOLD) { std::cout Class: CLASS_LABELS[i] , Confidence: output[i] std::endl; } } return 0; }代码逻辑很直白但有几个细节值得注意。Ort::Env的构造参数ORT_LOGGING_LEVEL_WARNING控制了运行时日志级别设为 WARNING 可以过滤掉 INFO 级别的噪音日志只保留警告和错误信息这对排查问题很有用。模型加载如果失败Ort::Session的构造函数会抛出异常代码里没做 try-catch实际工程中建议包一层异常处理。3.2 模型加载函数的正确写法与线程数配置模型加载函数是这份工程的第一个关键模块Ort::Session loadModel(const std::string modelPath, Ort::Env env) { Ort::SessionOptions sessionOptions; sessionOptions.SetIntraOpNumThreads(1); return Ort::Session(env, modelPath.c_str(), sessionOptions); }SetIntraOpNumThreads(1)这句话值得单独拿出来讲。这个参数控制 ONNX Runtime 执行单个算子时使用的线程数。设成 1 看起来是放弃了多线程加速但实际上对于图像分类这种小模型单线程推理反而更稳定尤其在多路并发推理的场景下每个推理请求独占一个线程整体吞吐量反而更高。如果你要在 CPU 上追求极致吞吐可以考虑调大这个值但要配合Ort::SessionOptions::SetGraphOptimizationLevel一起调优后者控制图优化等级默认是 ORT_ENABLE_ALL保持默认就好。文件里的解读说模型加载失败时要检查路径和环境这里我再补充一点ONNX Runtime 加载模型时默认启用内存模式如果 ONNX 文件路径有中文某些版本的运行时会出现诡异的加载失败。解决方案就是路径全部用英文项目目录尽量别带中文。3.3 图像预处理函数的两个关键坑现在到了预处理环节这份工程里最容易出问题的地方就在这cv::Mat preprocessImage(const cv::Mat image) { cv::Mat resized, blob; cv::resize(image, resized, cv::Size(INPUT_SIZE, INPUT_SIZE)); resized.convertTo(blob, CV_32F, 1.0 / 255); // 归一化 return blob; }这个函数做了两件事resize 到模型输入尺寸然后归一化到 0~1 范围。看起来没什么问题但实际部署时有三个暗坑。第一YOLO 系列模型在 PyTorch 里通常接受 RGB 格式输入而 OpenCV 的cv::imread读出来是 BGR 格式。你在 Python 里训练时如果没做通道转换导出的 ONNX 模型实际学到的是 RGB 分布那 C 端推理前必须做cv::cvtColor(image, image, cv::COLOR_BGR2RGB)。这份工程的代码里没有这一步如果你的模型精度比训练时低了一大截大概率就是这个原因。第二cv::resize直接拉伸图像会破坏长宽比。YOLOv11 分类模型虽然对长宽比不敏感但如果你的输入图像是长方形直接拉伸到正方形会让物体形变。更规范的做法是 letterbox先等比缩放再填充边缘这份项目没做这个处理属于简化方案精度损失在常规场景下可以接受。第三resized.convertTo(blob, CV_32F, 1.0 / 255)之后的 Mat 是 HWC 布局但 ONNX Runtime 期望的输入是 NCHW 布局。3.4 推理函数的完整实现与踩坑修复原文里的推理函数在拿预处理结果时有个隐蔽的问题我先把修复后的完整代码给你std::pairstd::vectorint64_t, std::vectorfloat runInference( Ort::Session session, const cv::Mat input) { // 输入输出节点名称必须和 ONNX 模型一致 std::vectorconst char* inputNames {input}; std::vectorconst char* outputNames {output}; // 输入维度: batch, channels, height, width std::vectorint64_t inputDims {1, 3, INPUT_SIZE, INPUT_SIZE}; // 关键修复把 HWC 的 Mat 转成连续内存的 CHW 向量 cv::Mat blob; cv::dnn::blobFromImage(input, blob, 1.0, cv::Size(), cv::Scalar(), false, false); std::vectorfloat inputData((float*)blob.data, (float*)blob.data blob.total()); // 创建输入张量 Ort::Value inputTensor Ort::Value::CreateTensorfloat( session.GetAllocator(0, OrtMemTypeDefault), inputData.data(), inputData.size(), inputDims.data(), inputDims.size() ); // 运行推理 auto outputTensors session.Run( Ort::RunOptions{nullptr}, inputNames.data(), inputTensor, 1, outputNames.data(), 1 ); // 获取输出数据 float* outputArray outputTensors.front().GetTensorMutableDatafloat(); auto outputShape outputTensors.front().GetTensorTypeAndShapeInfo().GetShape(); // 类别数量 输出张量的第二维或根据模型确定 size_t numClasses outputShape[1]; std::vectorfloat output(outputArray, outputArray numClasses); return {outputShape, output}; }关键改动在于输入数据的获取方式。原来用input.beginfloat()遍历一个 HWC 的 Mat内存布局完全不对。我改用cv::dnn::blobFromImage一次性完成 HWC → CHW 的内存重排然后直接从 Mat 的 data 指针拷贝数据。blobFromImage的第二个参数是缩放因子这里传 1.0因为预处理阶段已经归一化过了。CreateTensorfloat的参数含义分别是分配器、数据指针、数据长度、维度数组、维度个数。session.Run的参数要注意第三个参数是输入张量的个数必须和 inputNames 的 size 对应否则运行时内存访问越界。这段代码里我传的 1 表示 1 个输入如果模型有多个输入节点这里要跟着改。3.5 输出结果的后处理和置信度阈值的工程意义推理拿到的是一个浮点数组里面是每个类别的置信度分数。YOLOv11-CLS 的输出通常已经过 softmax但有些导出版本输出的是 logits这两种情况的后处理逻辑不一样。for (size_t i 0; i output.size(); i) { if (output[i] CONFIDENCE_THRESHOLD) { std::cout Class: CLASS_LABELS[i] , Confidence: output[i] std::endl; } }置信度阈值设置多少合理取决于你的业务容忍度。如果做的是安防告警漏报比误报严重阈值就设低一点比如 0.3如果做的是质检筛选误杀比漏过成本高阈值就设到 0.7 甚至更高。这份项目把CONFIDENCE_THRESHOLD定义为常量方便统一调整这是对的。还有一个细节这段代码默认输出数组的索引就是类别 ID前提是你的CLASS_LABELS向量和训练时的类别顺序完全一致。这个一致性是部署环节最容易忽略却最致命的问题如果类别顺序对不上模型预测完全正确但输出标签是错的这种现象排查起来特别费时间。4. 避坑指南这份部署代码里最容易翻车的五个地方4.1 编译命令写不全链接阶段报一堆 undefined reference现象用g yolov11_cls.cpp -o yolov11_cls \pkg-config --cflags --libs opencv4编译最后链接时报undefined reference to Ort::Session::Session(...)。原因g 命令里只加了 OpenCV 的库路径没有加 ONNX Runtime 的-I和-L参数链接器找不到 libonnxruntime.so。解决完整编译命令要加上 ONNX Runtime 的 include 和 lib 路径g yolov11_cls.cpp -o yolov11_cls \ -I/path/to/onnxruntime/include \ -L/path/to/onnxruntime/lib \ -lonnxruntime \ pkg-config --cflags --libs opencv4另外编译前记得export LD_LIBRARY_PATH/path/to/onnxruntime/lib:$LD_LIBRARY_PATH否则运行时找不到动态库。如果用的是 CMake建议用find_package或直接添加 include_directories 和 link_directories比手搓 g 命令更不容易漏。4.2 ONNX 输入输出节点名和代码里写的不一致现象推理执行到session.Run时控制台报错Failed to find input input in the graph。原因导出的 ONNX 模型里输入节点名不是input可能是images、data或者其他自定义名称。解决先用 Netron 打开 ONNX 文件查看实际的输入输出名称再回填到代码里。一个更稳妥的做法是在代码里动态读取模型的输入输出节点名// 获取模型实际输入输出节点名 Ort::AllocatorWithDefaultOptions allocator; auto inputNamesAlloc session.GetInputNamesAllocator(); auto inputNames session.GetInputNames();这里我实际用的时候发现GetInputNames()在部分版本里返回的类型不一样最省事的方法还是 Netron 看一遍然后硬编码反正是离线部署节点名不会变。4.3 输入图像的通道顺序不对推理精度断崖式下跌现象模型能跑通输出的置信度也正常但对每一张测试图都给出几乎相同的分类结果或者精度比 Python 里测的差 20% 以上。原因OpenCV 读图默认是 BGR而 PyTorch 训练时用的是 RGB推理前没有做通道转换模型接收的颜色分布和训练时不一致。解决在preprocessImage函数里加一行通道转换cv::Mat rgb_image; cv::cvtColor(image, rgb_image, cv::COLOR_BGR2RGB);做完这步之后再 resize 和归一化。加了这行之后精度基本能恢复到和 Python 端一致的水平。如果加了还是不行再排查图像归一化的均值和标准差是否和训练时一致。4.4 输入维度 NCHW 写成 NHWC推理直接报维度错误现象运行时报错Got dim mismatch. Input index 0 expected shape [1,3,640,640] but got [1,640,640,3]。原因inputDims数组写成了{1, INPUT_SIZE, INPUT_SIZE, 3}也就是 NHWC 布局而 ONNX 模型要求 NCHW。解决inputDims严格按{batch, channels, height, width}来写即{1, 3, INPUT_SIZE, INPUT_SIZE}。同时要确保inputData向量的内存布局确实按 NCHW 排列这就是为什么我不建议直接从原始 Mat 里遍历数据而是用cv::dnn::blobFromImage做布局转换。4.5 输出类别数固定写成 3模型换了就数据越界现象把模型换成自己训练的 10 分类模型输出结果只显示前 3 个类别或者干脆内存访问崩溃。原因原代码里std::vectorfloat output(outputArray, outputArray 3)类别数硬编码成 3。解决从输出张量的 shape 信息里动态读取类别数也就是我上一节修复代码里的size_t numClasses outputShape[1]。分类模型的输出 shape 一般是{1, numClasses}拿第二个维度就是类别数。这样换模型不用重新改代码。5. 编译、运行与参数调优把工程真正跑起来5.1 CMake 工程配置的最佳实践虽然原文给的编译方式是直接 g但实际做工程我不推荐这么做。一个可维护的 CMakeLists 配置如下cmake_minimum_required(VERSION 3.16) project(yolov11_cls LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # OpenCV find_package(OpenCV REQUIRED) # ONNX Runtime假设解压在第三方目录 set(ONNXRUNTIME_ROOT /path/to/onnxruntime) include_directories(${ONNXRUNTIME_ROOT}/include) link_directories(${ONNXRUNTIME_ROOT}/lib) add_executable(yolov11_cls yolov11_cls.cpp) target_link_libraries(yolov11_cls ${OpenCV_LIBS} onnxruntime )编译命令mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease make -j$(nproc)Release 模式比 Debug 模式快 2~3 倍ONNX Runtime 本身也有大量优化Debug 模式下这些优化大部分会被禁用。跑性能测试的时候务必用 Release 版。5.2 运行参数和模型尺寸的对应关系运行程序前先确认几个参数的匹配关系。INPUT_SIZE必须和导出 ONNX 时的imgsz一致这份工程里默认 640但你导出时如果用的 224那这里必须改成 224。输入尺寸直接影响推理延迟和精度输入尺寸推理延迟CPU, 约精度适用场景2245~15ms较高大多数分类场景32015~30ms高对精度敏感的离线任务64040~80ms最高高精度要求如果你跑在 RK3588 这类边缘芯片上建议用 224 并开启 NPU 加速CPU 跑 640 的分辨率实时性会很难看。5.3 性能分析哪个环节最耗时我实际测过这个工程在 Intel i5 上预处理resize 归一化 HWC 转换大约耗时 2~3ms模型推理约 10~20ms取决于输入尺寸后处理不到 1ms。推理是绝对大头所以优化重点应该放在推理阶段。两个方向一是 ONNX Runtime 开启图优化二是换成 GPU 或者 NPU 后端。开启图优化只需要一行代码sessionOptions.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);这是这份项目没写但强烈建议加上的配置对 CPU 推理能带来 10%~20% 的性能提升白捡的优化。6. 进阶技巧用一个置信度校准函数把模型输出变成可用的决策依据跑通基础推理只是第一步真实业务场景里你会发现一个问题默认输出的置信度分布和实际场景不匹配。比如安防场景下模型对陌生环境拍的图片普遍给出偏高的置信度你以为模型很自信实际它只是过拟合了训练集。我在这类部署工程里养成了一个习惯加一个置信度校准层根据实际场景的分布调整输出分数。float calibrateConfidence(float rawConfidence, float calibrationFactor) { // 对数几率空间校准 float odds rawConfidence / (1.0f - rawConfidence 1e-6f); odds std::pow(odds, calibrationFactor); float calibrated odds / (1.0f odds); return calibrated; }这个函数的原理是在对数几率空间做幂变换calibrationFactor大于 1 会让模型更保守低置信度被压低小于 1 会让模型更大胆高置信度附近的分值更容易被接受。这个系数需要根据你的验证集来定拿一批已知标签的图跑一遍统计模型输出的置信度分布和真实准确率之间的偏差然后选一个让校准后置信度最接近真实准确率的系数。我当时在一个工业质检项目里原始模型的输出阈值 0.5 对应实际准确率只有 0.78经过校准把阈值调到 0.63 之后准确率提到了 0.91。这个校准函数不改变模型本身只是在后处理环节加了一层映射性能损耗可以忽略不计。从那以后我每次部署分类模型都强制走一遍这个流程先跑通原始推理然后在验证集上统计置信度分布最后加上校准层调阈值。这不是什么高深的算法就是工程上花半小时能做完、但能少挨很多骂的事。希望这份 YOLOv11-CLS 的部署笔记能帮你把第一个坑躲过去顺手把置信度这条线也做扎实后面换模型换场景都能直接复用。本文还有配套的精品资源点击获取