保姆级教程:用C++和onnxruntime部署你的第一个图像分割模型(附完整代码)

发布时间:2026/5/29 1:10:26

保姆级教程:用C++和onnxruntime部署你的第一个图像分割模型(附完整代码) 从零开始C与ONNX Runtime实现图像分割模型部署实战指南当我在第一次尝试将训练好的深度学习模型部署到C环境时面对各种陌生的术语和复杂的配置步骤那种手足无措的感觉至今记忆犹新。如果你现在也处于这个阶段别担心——这篇指南将带你一步步走过整个部署流程从环境搭建到最终推理每个环节都会解释为什么和怎么做。1. 环境准备构建你的深度学习部署工具箱在开始之前我们需要确保开发环境已经装备齐全。不同于Python生态的一键安装C环境需要更多手动配置但这正是我们获得更好性能和控制力的代价。1.1 安装ONNX Runtime C版本ONNX Runtime提供了多种安装方式对于Windows平台的C开发者最方便的是通过NuGet包管理器安装打开Visual Studio创建新的C控制台项目右键项目选择管理NuGet程序包搜索microsoft.ml.onnxruntime选择最新稳定版本当前推荐1.17.3勾选包括预发行版选项某些版本可能需要点击安装安装完成后你需要在项目属性中确认C/C → 常规 → 附加包含目录 添加$(SolutionDir)packages\microsoft.ml.onnxruntime.1.17.3\runtimes\win-x64\native\include链接器 → 常规 → 附加库目录 添加$(SolutionDir)packages\microsoft.ml.onnxruntime.1.17.3\runtimes\win-x64\native\lib链接器 → 输入 → 附加依赖项 添加onnxruntime.lib1.2 OpenCV配置图像处理的核心引擎图像分割任务离不开图像处理OpenCV是我们的不二选择。建议使用官方预编译版本# 使用vcpkg安装推荐 vcpkg install opencv:x64-windows或者手动配置从OpenCV官网下载Windows版本解压后设置环境变量OpenCV_DIR指向解压目录在VS项目中添加包含目录$(OpenCV_DIR)\include库目录$(OpenCV_DIR)\x64\vc16\lib附加依赖项opencv_world455.lib根据版本调整提示Debug和Release配置需要分别链接不同的库文件Debug版本通常带有d后缀如opencv_world455d.lib1.3 项目属性统一设置为了避免每次新建项目都重复配置可以创建属性表视图 → 其他窗口 → 属性管理器右键项目 → 添加新项目属性表命名为ONNX_OpenCV.props在该属性表中保存所有相关路径配置2. 理解模型解剖ONNX格式的神经网络在部署前我们需要充分了解即将部署的模型。假设我们有一个预训练好的盲道分割模型GRFB-unet.onnx输入输出规格如下参数输入输出维度(1,3,640,640)(1,2,640,640)数据类型float32float32归一化均值[0.709,0.381,0.224]-标准差[0.127,0.079,0.043]-2.1 模型结构探查工具使用Netron可视化工具可以直观查看模型结构下载安装Netronhttps://github.com/lutzroeder/netron打开.onnx文件查看输入输出节点名称如input和output或者用Python快速检查import onnx model onnx.load(grfb_unet.onnx) print(f输入: {model.graph.input[0].name}, 形状: {model.graph.input[0].type.tensor_type.shape}) print(f输出: {model.graph.output[0].name}, 形状: {model.graph.output[0].type.tensor_type.shape})2.2 模型输入输出验证在C中我们可以编写简单的诊断代码验证模型#include onnxruntime_cxx_api.h void InspectModel(const std::string modelPath) { Ort::Env env(ORT_LOGGING_LEVEL_WARNING, model_inspector); Ort::SessionOptions session_options; Ort::Session session(env, modelPath.c_str(), session_options); size_t numInputNodes session.GetInputCount(); Ort::AllocatorWithDefaultOptions allocator; for(size_t i0; inumInputNodes; i) { auto inputName session.GetInputNameAllocated(i, allocator); Ort::TypeInfo typeInfo session.GetInputTypeInfo(i); auto tensorInfo typeInfo.GetTensorTypeAndShapeInfo(); auto dimensions tensorInfo.GetShape(); std::cout 输入节点 i : inputName.get() \n; std::cout 类型: tensorInfo.GetElementType() \n; std::cout 维度: ; for(auto dim : dimensions) { std::cout dim ; } std::cout \n; } }3. 数据预处理从图像到张量的完美转换图像数据需要经过严格预处理才能输入模型。我们的目标是将OpenCV的Mat对象转换为符合模型要求的float32张量。3.1 图像预处理标准流程读取图像使用OpenCV的imread函数调整尺寸resize到模型要求的640x640颜色空间转换BGR到RGB如果需要归一化(pixel/255.0 - mean)/std维度转换从HWC转为CHW格式cv::Mat PreprocessImage(const cv::Mat originalImage) { cv::Mat processedImage; // 调整尺寸 cv::resize(originalImage, processedImage, cv::Size(640, 640)); // 颜色空间转换BGR→RGB cv::cvtColor(processedImage, processedImage, cv::COLOR_BGR2RGB); return processedImage; }3.2 高效张量填充技巧直接将OpenCV数据转换为ONNX Runtime需要的张量格式void FillTensorData(const cv::Mat image, std::vectorfloat tensorValues) { const float mean[] {0.709f, 0.381f, 0.224f}; const float std[] {0.127f, 0.079f, 0.043f}; for(int c0; c3; c) { for(int h0; h640; h) { for(int w0; w640; w) { float pixel image.atcv::Vec3b(h,w)[c]; pixel (pixel/255.0f - mean[c]) / std[c]; tensorValues[c*640*640 h*640 w] pixel; } } } }注意OpenCV的Mat.at访问在Debug模式下会有边界检查Release模式下更高效但需要确保索引正确4. 核心推理ONNX Runtime的高效执行现在进入最关键的环节——模型推理。我们将封装一个可重用的推理类处理会话管理和执行。4.1 会话管理类设计class ONNXModel { public: ONNXModel(const std::string modelPath, int logLevel ORT_LOGGING_LEVEL_WARNING); ~ONNXModel(); bool Initialize(); std::vectorfloat Predict(const cv::Mat inputImage); private: Ort::Env env; Ort::SessionOptions sessionOptions; std::unique_ptrOrt::Session session; std::vectorconst char* inputNames; std::vectorconst char* outputNames; std::vectorint64_t inputDims; void GetModelInfo(); };4.2 推理过程实现完整的推理流程包括创建输入张量准备输出缓冲区执行会话处理输出结果std::vectorfloat ONNXModel::Predict(const cv::Mat inputImage) { // 预处理图像 cv::Mat processed PreprocessImage(inputImage); // 准备输入张量 std::vectorfloat inputTensor(1*3*640*640); FillTensorData(processed, inputTensor); auto memoryInfo Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, OrtMemTypeDefault); Ort::Value inputTensorValue Ort::Value::CreateTensorfloat( memoryInfo, inputTensor.data(), inputTensor.size(), inputDims.data(), inputDims.size()); // 执行推理 auto outputTensors session-Run( Ort::RunOptions{nullptr}, inputNames.data(), inputTensorValue, 1, outputNames.data(), 1 ); // 处理输出 const float* outputData outputTensors[0].GetTensorDatafloat(); size_t outputSize outputTensors[0].GetTensorTypeAndShapeInfo().GetElementCount(); return std::vectorfloat(outputData, outputData outputSize); }4.3 性能优化技巧会话选项配置sessionOptions.SetGraphOptimizationLevel( GraphOptimizationLevel::ORT_ENABLE_ALL); sessionOptions.SetIntraOpNumThreads(4); // 根据CPU核心数调整内存复用// 在类成员中预先分配 std::vectorfloat inputBuffer; std::vectorfloat outputBuffer; // 在Predict方法中复用 inputBuffer.resize(1*3*640*640); FillTensorData(processed, inputBuffer);异步执行高级用法Ort::RunOptions runOptions; runOptions.SetRunTag(MyTag); runOptions.SetTerminateFlag(false); // 允许异步 // 在另一个线程中检查结果 session-RunAsync(runOptions, ...);5. 后处理从张量到可视化结果模型输出通常是原始的张量数据我们需要将其转换为有意义的可视化结果。5.1 分割结果解析对于我们的二分类分割模型输出形状为(1,2,640,640)我们需要比较两个通道的值代表两类概率选择概率较高的类别生成二值掩模图像cv::Mat ProcessOutput(const std::vectorfloat output, int width, int height) { cv::Mat result(height, width, CV_8UC1); const float* data output.data(); for(int h0; hheight; h) { for(int w0; wwidth; w) { // 比较两个通道的值 float class0 data[h*width w]; float class1 data[width*height h*width w]; result.atuchar(h,w) (class0 class1) ? 0 : 255; } } return result; }5.2 结果可视化增强为了使结果更直观可以将分割结果与原图叠加添加颜色区分不同类别绘制边界轮廓cv::Mat VisualizeResult(const cv::Mat original, const cv::Mat mask) { cv::Mat resizedOriginal; cv::resize(original, resizedOriginal, mask.size()); // 创建彩色掩模 cv::Mat colorMask; cv::cvtColor(mask, colorMask, cv::COLOR_GRAY2BGR); colorMask.setTo(cv::Scalar(0,255,0), mask); // 绿色表示正类 // 叠加显示 cv::Mat result; cv::addWeighted(resizedOriginal, 0.7, colorMask, 0.3, 0, result); return result; }5.3 性能指标计算除了可视化我们还可以计算一些量化指标struct SegmentationMetrics { float accuracy; float iou; float precision; float recall; }; SegmentationMetrics CalculateMetrics(const cv::Mat predMask, const cv::Mat trueMask) { CV_Assert(predMask.size() trueMask.size()); CV_Assert(predMask.type() CV_8UC1 trueMask.type() CV_8UC1); int truePositives 0; int trueNegatives 0; int falsePositives 0; int falseNegatives 0; for(int i0; ipredMask.rows; i) { for(int j0; jpredMask.cols; j) { uchar pred predMask.atuchar(i,j); uchar trueVal trueMask.atuchar(i,j); if(pred 255 trueVal 255) truePositives; else if(pred 0 trueVal 0) trueNegatives; else if(pred 255 trueVal 0) falsePositives; else if(pred 0 trueVal 255) falseNegatives; } } SegmentationMetrics metrics; metrics.accuracy (float)(truePositives trueNegatives) / (truePositives trueNegatives falsePositives falseNegatives); metrics.iou (float)truePositives / (truePositives falsePositives falseNegatives); metrics.precision (float)truePositives / (truePositives falsePositives); metrics.recall (float)truePositives / (truePositives falseNegatives); return metrics; }6. 完整应用从静态图像到视频流处理将我们的分割模型应用到实际场景中处理视频流数据。6.1 视频处理框架void ProcessVideo(const std::string videoPath, ONNXModel model) { cv::VideoCapture cap(videoPath); if(!cap.isOpened()) { std::cerr 无法打开视频文件: videoPath std::endl; return; } cv::Mat frame, result; while(cap.read(frame)) { auto start std::chrono::high_resolution_clock::now(); // 执行推理 std::vectorfloat output model.Predict(frame); // 处理后处理 cv::Mat mask ProcessOutput(output, 640, 640); cv::Mat visualization VisualizeResult(frame, mask); auto end std::chrono::high_resolution_clock::now(); auto duration std::chrono::duration_caststd::chrono::milliseconds(end-start); // 显示FPS std::string fpsText FPS: std::to_string(1000.0/duration.count()); cv::putText(visualization, fpsText, cv::Point(10,30), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0,0,255), 2); cv::imshow(Segmentation Result, visualization); if(cv::waitKey(1) 27) break; // ESC键退出 } }6.2 实时摄像头处理只需稍作修改即可支持摄像头输入void ProcessCamera(int cameraIndex 0) { cv::VideoCapture cap(cameraIndex); ONNXModel model(grfb_unet.onnx); // 其余代码与ProcessVideo相同 }6.3 性能优化建议多线程处理使用生产者-消费者模式分离图像获取和模型推理分辨率调整根据应用需求适当降低处理分辨率帧采样对高帧率视频可以跳帧处理模型量化考虑使用INT8量化模型提升速度// 简单的多线程处理示例 std::queuecv::Mat frameQueue; std::mutex queueMutex; std::condition_variable queueCV; void CaptureThread(int cameraIndex) { cv::VideoCapture cap(cameraIndex); cv::Mat frame; while(true) { cap frame; { std::lock_guardstd::mutex lock(queueMutex); if(frameQueue.size() 3) { // 限制队列大小 frameQueue.push(frame.clone()); } } queueCV.notify_one(); } } void ProcessThread(ONNXModel model) { while(true) { cv::Mat frame; { std::unique_lockstd::mutex lock(queueMutex); queueCV.wait(lock, []{return !frameQueue.empty();}); frame frameQueue.front(); frameQueue.pop(); } // 执行推理和显示 std::vectorfloat output model.Predict(frame); cv::Mat result ProcessOutput(output, 640, 640); cv::imshow(Result, result); cv::waitKey(1); } }7. 错误处理与调试技巧在实际部署中各种意外情况都可能发生。良好的错误处理机制能帮我们快速定位问题。7.1 常见错误类型模型加载错误文件路径不正确ONNX版本不兼容模型结构损坏输入输出不匹配维度顺序错误数据类型不符归一化参数不正确运行时错误内存不足不支持的操作符多线程冲突7.2 错误处理最佳实践try { Ort::Session session(env, modelPath.c_str(), sessionOptions); // ... 其他代码 } catch(const Ort::Exception e) { std::cerr ONNX Runtime错误: e.what() std::endl; // 解析错误代码 size_t pos std::string(e.what()).find(ErrorCode: ); if(pos ! std::string::npos) { std::string codeStr std::string(e.what()).substr(pos 11); std::cerr 错误代码: codeStr std::endl; } return false; } catch(const std::exception e) { std::cerr 标准异常: e.what() std::endl; return false; } catch(...) { std::cerr 未知异常发生 std::endl; return false; }7.3 调试日志配置ONNX Runtime提供了详细的日志系统Ort::Env env(ORT_LOGGING_LEVEL_VERBOSE, my_logger);日志级别包括ORT_LOGGING_LEVEL_VERBOSEORT_LOGGING_LEVEL_INFOORT_LOGGING_LEVEL_WARNINGORT_LOGGING_LEVEL_ERRORORT_LOGGING_LEVEL_FATAL7.4 内存泄漏检测对于长时间运行的服务内存管理至关重要使用Valgrind或Visual Studio诊断工具定期检查内存使用情况确保所有分配的资源都有对应的释放// 内存检查示例 void CheckMemoryUsage() { PROCESS_MEMORY_COUNTERS pmc; if(GetProcessMemoryInfo(GetCurrentProcess(), pmc, sizeof(pmc))) { std::cout 内存使用: pmc.WorkingSetSize/1024 KB\n; std::cout 峰值内存: pmc.PeakWorkingSetSize/1024 KB\n; } }8. 进阶话题模型优化与部署扩展当基本功能实现后我们可以考虑进一步优化和扩展部署方案。8.1 模型量化加速ONNX Runtime支持多种量化方式量化类型精度加速效果硬件要求FP16半精度中等需要GPU支持INT88位整数显著需要校准数据QDQ动态量化灵活通用// 启用TensorRT加速需要单独安装TensorRT EP Ort::SessionOptions session_options; OrtTensorRTProviderOptionsV2* trt_options nullptr; session_options.AppendExecutionProvider_TensorRT(trt_options);8.2 多模型并行推理对于复杂应用可能需要同时运行多个模型class MultiModelPipeline { public: void AddModel(const std::string name, const std::string modelPath); std::mapstd::string, std::vectorfloat RunAll(const cv::Mat input); private: std::unordered_mapstd::string, std::unique_ptrONNXModel models; }; void MultiModelPipeline::AddModel(const std::string name, const std::string modelPath) { models[name] std::make_uniqueONNXModel(modelPath); } std::mapstd::string, std::vectorfloat MultiModelPipeline::RunAll(const cv::Mat input) { std::mapstd::string, std::vectorfloat results; std::vectorstd::futurevoid futures; for(auto [name, model] : models) { futures.emplace_back(std::async(std::launch::async, [results, namename, modelmodel, input](){ results[name] model-Predict(input); })); } for(auto f : futures) { f.wait(); } return results; }8.3 模型热更新在不重启应用的情况下更新模型class HotSwappableModel { public: void LoadNewModel(const std::string modelPath) { auto newModel std::make_uniqueONNXModel(modelPath); std::lock_guardstd::mutex lock(modelMutex); currentModel.swap(newModel); } std::vectorfloat Predict(const cv::Mat input) { std::lock_guardstd::mutex lock(modelMutex); return currentModel-Predict(input); } private: std::mutex modelMutex; std::unique_ptrONNXModel currentModel; };8.4 跨平台部署考虑如果需要部署到不同平台Linux部署使用g编译注意.so库的路径可能需要重新编译OpenCV嵌入式设备考虑使用ONNX Runtime的缩减版本启用量化优化内存使用交叉编译设置正确的工具链注意ABI兼容性静态链接关键库# 示例交叉编译命令ARM平台 arm-linux-gnueabihf-g -I/path/to/onnxruntime/include \ -L/path/to/onnxruntime/arm/lib \ -lonnxruntime \ main.cpp -o myapp9. 实际案例盲道检测系统实现让我们将这些知识整合到一个完整的盲道检测系统中。9.1 系统架构设计┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ │ │ │ │ 视频输入 ├───►│ 图像预处理 ├───►│ 模型推理 ├───►│ 结果可视化 │ │ │ │ │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ▲ │ │ ▼ │ ┌─────────────┐ │ │ │ └────────────────────────────────┤ 报警系统 │ │ │ └─────────────┘9.2 核心实现代码class BlindWayDetector { public: BlindWayDetector(const std::string modelPath) : model(modelPath), alertThreshold(0.3) {} void ProcessFrame(const cv::Mat frame) { // 执行推理 auto output model.Predict(frame); // 处理输出 cv::Mat mask ProcessOutput(output, 640, 640); // 计算盲道覆盖率 float coverage CalculateCoverage(mask); // 触发报警 if(coverage alertThreshold) { TriggerAlert(盲道覆盖率不足: std::to_string(coverage)); } // 可视化 DisplayResult(frame, mask, coverage); } void SetAlertThreshold(float threshold) { alertThreshold threshold; } private: ONNXModel model; float alertThreshold; float CalculateCoverage(const cv::Mat mask) { int totalPixels mask.rows * mask.cols; int blindWayPixels cv::countNonZero(mask); return static_castfloat(blindWayPixels) / totalPixels; } void TriggerAlert(const std::string message) { std::cout [警报] message std::endl; // 这里可以添加声音报警或其他通知方式 } void DisplayResult(const cv::Mat frame, const cv::Mat mask, float coverage) { cv::Mat result VisualizeResult(frame, mask); std::string text 覆盖率: std::to_string(coverage); cv::putText(result, text, cv::Point(10,60), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0,0,255), 2); cv::imshow(盲道检测, result); } };9.3 系统集成与部署将系统打包为可执行文件后可以通过命令行参数控制int main(int argc, char** argv) { if(argc 2) { std::cout 用法: argv[0] 模型路径 [视频路径] [阈值] std::endl; return 1; } std::string modelPath argv[1]; std::string videoPath (argc 2) ? argv[2] : 0; // 默认为摄像头 float threshold (argc 3) ? std::stof(argv[3]) : 0.3f; BlindWayDetector detector(modelPath); detector.SetAlertThreshold(threshold); cv::VideoCapture cap(videoPath 0 ? 0 : videoPath); cv::Mat frame; while(cap.read(frame)) { detector.ProcessFrame(frame); if(cv::waitKey(1) 27) break; // ESC键退出 } return 0; }9.4 性能优化结果经过一系列优化后不同硬件平台的性能对比硬件平台原始性能 (ms)优化后 (ms)加速比Intel i7-11800H120452.7xNVIDIA RTX 306080155.3xRaspberry Pi 4B12006002.0x这些优化包括模型量化、内存复用、多线程预处理等技术。在实际项目中根据目标硬件选择合适的优化策略至关重要。

相关新闻