前言本文会介绍 YOLO ONNX 模型在 C ONNX Runtime 环境下的目标检测流程并附上完整的代码解析。准备工作安装 onnxruntime安装 OpenCV准备 .onnx 模型和测试图片一、整体流程概览在 C 部署 YOLO ONNX 模型时整个推理流程如下原始图像 → 图像预处理(Resize Padding Normalize) → 构造 Tensor(NHWC → NCHW) → ONNX Runtime 推理 → 获取模型输出 → YOLO 输出解码 → 坐标映射回原图 → NMS 去重 → 最终检测结果1.1 完整代码修改下面几处后进行推理// 模型路径 const wchar_t* onnx_model_path Lcap_label_roi.onnx; //图片路径 std::string img_path IMG20260521090357.jpg; //将图像转换成模型输入的形式 // 第一个640是输入图像宽度第二个640是输入图像高度 //如果训练时是默认imgsz640不用修改 PreprocessResult prep preprocessImage1(img, 640, 640); // 输入数据维度 // 第一个640是输入图像高度注意这里先是高度第二个640是输入图像宽度 // 如果训练时是默认imgsz640不用修改 std::vectorint64_t inputShape { 1,3, 640, 640 };完整代码如下#include filesystem #include iostream #include onnxruntime_cxx_api.h #include opencv2/opencv.hpp struct PreprocessResult { std::vectorfloat tensorValues; float scale; int padLeft; int padTop; }; struct Detection { cv::Rect box; float score; int classId; }; PreprocessResult preprocessImage1(const cv::Mat inputImage, int inputWidth, int inputHeight) { cv::Mat image; //inputImage类型为CV_8UC3 cv::cvtColor(inputImage, image, cv::COLOR_BGR2RGB); //将图像转换成模型输入的形式 float padScale; int padLeft, padTop; //缩放比例 float scale_x static_castfloat(inputWidth) / image.cols; float scale_y static_castfloat(inputHeight) / image.rows; float scale std::min(scale_x, scale_y); int new_w static_castint(image.cols * scale); int new_h static_castint(image.rows * scale); //缩放 cv::Mat resized; cv::resize(image, resized, cv::Size(new_w, new_h)); //填充 int dh inputHeight - new_h; int dw inputWidth - new_w; int top dh / 2, bottom dh - top; int left dw / 2, right dw - left; cv::Mat padded(inputHeight, inputWidth, CV_8UC3, cv::Scalar(114, 114, 114)); resized.copyTo(padded(cv::Rect(left, top, new_w, new_h))); cv::Mat new_image; padded.convertTo(new_image, CV_32F, 1.0 / 255.0); padScale scale; padLeft left; padTop top; //模型tensor输入数据 std::vectorcv::Mat channels(3); cv::split(new_image, channels); std::vectorfloat tensorValues(inputHeight * inputWidth * 3); for (int c 0; c 3; c) { std::memcpy( tensorValues.data() c * inputWidth * inputHeight, channels[c].data, inputHeight * inputWidth * sizeof(float) ); } return { tensorValues, scale, padLeft, padTop }; } std::vectorDetection inferDetections1(Ort::Session session, const cv::Mat image, const PreprocessResult prep, const std::vectorint64_t inputShape, const char* inputName, const char* outputName, float confThreshold, float nmsThreshold) { // 1. 构建输入 Tensor Ort::MemoryInfo memoryInfo Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, OrtMemTypeDefault ); Ort::Value inputTensor Ort::Value::CreateTensorfloat( memoryInfo, const_castfloat*(prep.tensorValues.data()), prep.tensorValues.size(), inputShape.data(), inputShape.size() ); const char* inputNames[] { inputName }; const char* outputNames[] { outputName }; // 2. 推理 auto outputs session.Run( Ort::RunOptions{ nullptr }, inputNames, inputTensor, 1, outputNames, 1 ); // 3. 解析输出 float* rawData outputs[0].GetTensorMutableDatafloat(); auto outputShape outputs[0].GetTensorTypeAndShapeInfo().GetShape(); int totalSize 1; for (auto s : outputShape) totalSize * s; std::vectorfloat rawOutput(rawData, rawData totalSize); int num_values outputShape[2]; std::vectorcv::Rect boxes; std::vectorfloat confidences; std::vectorint classIds; float scale prep.scale; int padLeft prep.padLeft; int padTop prep.padTop; // 4. 解码 int channels static_castint(outputShape[1]); int numClasses channels - 4; for (int i 0; i num_values; i) { float x rawOutput[0 * num_values i]; float y rawOutput[1 * num_values i]; float w rawOutput[2 * num_values i]; float h rawOutput[3 * num_values i]; float score 0.0f; int classId -1; for (int c 0; c numClasses; c) { float clsScore rawOutput[(4 c) * num_values i]; if (clsScore score) { score clsScore; classId c; } } if (score confThreshold) continue; // 映射回原图 float cx (x - padLeft) / scale; float cy (y - padTop) / scale; float bw w / scale; float bh h / scale; int x1 static_castint(cx - bw * 0.5f); int y1 static_castint(cy - bh * 0.5f); int x2 static_castint(cx bw * 0.5f); int y2 static_castint(cy bh * 0.5f); x1 std::max(0, x1); y1 std::max(0, y1); x2 std::min(image.cols - 1, x2); y2 std::min(image.rows - 1, y2); if (x2 x1 || y2 y1) continue; boxes.emplace_back(x1, y1, x2 - x1, y2 - y1); confidences.push_back(score); classIds.push_back(classId); } // 5. NMS std::vectorint indices; if (!boxes.empty()) { cv::dnn::NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices); } // 6. 输出 std::vectorDetection detections; for (int idx : indices) { detections.push_back({ boxes[idx], confidences[idx], classIds[idx] }); } return detections; } int main() { const wchar_t* onnx_model_path Lcap_label_roi.onnx; std::string img_path IMG20260521090357.jpg; cv::Mat img cv::imread(img_path); auto providers Ort::GetAvailableProviders(); for (auto p : providers) { std::cout p std::endl; } Ort::Env env(ORT_LOGGING_LEVEL_WARNING, YOLO); Ort::SessionOptions sessionOptions; sessionOptions.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); try { OrtCUDAProviderOptions cuda_options; sessionOptions.AppendExecutionProvider_CUDA(cuda_options); } catch (const std::exception e) { std::cout e.what() std::endl; } Ort::Session session(env, onnx_model_path, sessionOptions); auto inputInfo session.GetInputTypeInfo(0).GetTensorTypeAndShapeInfo(); auto shape inputInfo.GetShape(); for (auto s : shape) std::cout s ; if (img.empty()) { std::cout image load failed std::endl; return -1; } PreprocessResult prep preprocessImage1(img, 640, 640); std::vectorint64_t inputShape { 1,3, 640, 640 }; auto detections inferDetections1(session, img, prep, inputShape, images, output0, 0.5f, 0.45f); std::cout detections count detections.size() std::endl; for (const auto det : detections) { cv::rectangle(img, det.box, cv::Scalar(0, 255, 0), 2); std::cout class det.classId score det.score std::endl; } //cv::imshow(result, img); //cv::waitKey(0); //cv::imwrite(result.png, img); return 0; }推理结果二、代码说明2.1 数据结构定义PreprocessResulttensorValues - 模型输入数据scale - 记录缩放比例例如原图 1920 × 1080输入 640 × 640则scale min(640/1920,640/1080)后续恢复坐标时需要使用。padLeft、padTop - 左侧填充像素数顶部填充像素数。用于反向映射坐标。Detection保存最终检测结果包括目标框、置信度、类别ID。struct PreprocessResult { std::vectorfloat tensorValues; // 模型输入张量数据 float scale; // 缩放比例 int padLeft; // 左填充像素数 int padTop; // 上填充像素数 }; struct Detection { cv::Rect box; // 检测框 float score; // 置信度 int classId; // 类别ID };2.2 图像预处理函数preprocessImage1()流程1.读取图片2.BGR转RGBOpenCV读取图片位BGR而YOLO训练时通常采用RGB3.计算缩放比例目的保持宽高比例如原图 1920 × 1080输入 640 × 640scale 0.3334.缩放图像Resize5.LetterBox填充使用114灰度值填充即YOLO默认的填充色6.归一化归一化到[0,1]范围并转换为浮点数7.HWC转NCHWOpenCV格式为HWC即height、width、channelYOLO输入为NCHW即batch、channel、height、width。最终形成下面的布局。RRRRRR...GGGGGG...BBBBBB...PreprocessResult preprocessImage1(const cv::Mat inputImage, int inputWidth, int inputHeight) { cv::Mat image; cv::cvtColor(inputImage, image, cv::COLOR_BGR2RGB); // BGR转RGB // 计算缩放比例保持宽高比 float scale_x static_castfloat(inputWidth) / image.cols; float scale_y static_castfloat(inputHeight) / image.rows; float scale std::min(scale_x, scale_y); // 缩放图像 int new_w static_castint(image.cols * scale); int new_h static_castint(image.rows * scale); cv::Mat resized; cv::resize(image, resized, cv::Size(new_w, new_h)); // 填充至目标尺寸使用114灰度值即YOLO默认的填充色 int dh inputHeight - new_h; int dw inputWidth - new_w; int top dh / 2, bottom dh - top; int left dw / 2, right dw - left; cv::Mat padded(inputHeight, inputWidth, CV_8UC3, cv::Scalar(114, 114, 114)); resized.copyTo(padded(cv::Rect(left, top, new_w, new_h))); // 归一化到[0,1]范围并转换为浮点数 cv::Mat new_image; padded.convertTo(new_image, CV_32F, 1.0 / 255.0); // 将HWC格式转换为CHW格式通道分离 std::vectorcv::Mat channels(3); cv::split(new_image, channels); std::vectorfloat tensorValues(inputHeight * inputWidth * 3); for (int c 0; c 3; c) { std::memcpy( tensorValues.data() c * inputWidth * inputHeight, channels[c].data, inputHeight * inputWidth * sizeof(float) ); } return { tensorValues, scale, left, top }; }2.3 推理与结果解码函数inferDetections1()1.创建 CPU 内存描述Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);表示 Tensor 位于 CPU 内存2.创建输入TensorOrt::Value::CreateTensorfloat(memoryInfo, prep.tensorValues.data(), prep.tensorValues.size(), inputShape.data(), inputShape.size());对应 [1,3,640,640]3.执行推理auto outputs session.Run(...);最终返回outputs[0]4.YOLO输出a. 获取原始输出数据float* rawData outputs[0].GetTensorMutableDatafloat();b. Shapeauto outputShape outputs[0].GetTensorTypeAndShapeInfo().GetShape();例如yolo会输出 [1,84,8400]batch 1、84 4 80类别、8400 候选框数量5.YOLO输出解码在结果解码过程中rawData 前4个值表示边界框坐标cx, cy, w, h(中心点x, 中心点y, 宽, 高)剩余值对应各类别的置信度分数。后续会详细说明。6.坐标映射回原图先去掉Padding、恢复宽高、然后中心点转左上角坐标最终得到原图坐标。7.NMS去重8.构建最终结果结构体 Detection。std::vectorDetection inferDetections1( Ort::Session session, const cv::Mat image, const PreprocessResult prep, const std::vectorint64_t inputShape, const char* inputName, const char* outputName, float confThreshold, float nmsThreshold ) { // 1. 构建输入Tensor Ort::MemoryInfo memoryInfo Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, OrtMemTypeDefault ); Ort::Value inputTensor Ort::Value::CreateTensorfloat( memoryInfo, const_castfloat*(prep.tensorValues.data()), prep.tensorValues.size(), inputShape.data(), inputShape.size() ); // 2. 执行推理 const char* inputNames[] { inputName }; const char* outputNames[] { outputName }; auto outputs session.Run( Ort::RunOptions{ nullptr }, inputNames, inputTensor, 1, outputNames, 1 ); // 3. 获取原始输出数据 float* rawData outputs[0].GetTensorMutableDatafloat(); auto outputShape outputs[0].GetTensorTypeAndShapeInfo().GetShape(); int num_values outputShape[2]; // 检测框数量 int channels static_castint(outputShape[1]); int numClasses channels - 4; // 类别数 // 4. 解码每个检测框 for (int i 0; i num_values; i) { float x rawOutput[0 * num_values i]; float y rawOutput[1 * num_values i]; float w rawOutput[2 * num_values i]; float h rawOutput[3 * num_values i]; // 寻找最大类别得分 float score 0.0f; int classId -1; for (int c 0; c numClasses; c) { float clsScore rawOutput[(4 c) * num_values i]; if (clsScore score) { score clsScore; classId c; } } // 置信度阈值过滤 if (score confThreshold) continue; // 映射回原始图像坐标 float cx (x - padLeft) / scale; float cy (y - padTop) / scale; float bw w / scale; float bh h / scale; // 转换为左上角坐标 int x1 static_castint(cx - bw * 0.5f); int y1 static_castint(cy - bh * 0.5f); int x2 static_castint(cx bw * 0.5f); int y2 static_castint(cy bh * 0.5f); // 边界裁剪 x1 std::max(0, x1); y1 std::max(0, y1); x2 std::min(image.cols - 1, x2); y2 std::min(image.rows - 1, y2); if (x2 x1 || y2 y1) continue; boxes.emplace_back(x1, y1, x2 - x1, y2 - y1); confidences.push_back(score); classIds.push_back(classId); } // 5. NMS后处理 std::vectorint indices; if (!boxes.empty()) { cv::dnn::NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices); } // 6. 构建最终结果 std::vectorDetection detections; for (int idx : indices) { detections.push_back({boxes[idx], confidences[idx], classIds[idx]}); } return detections; }2.4 主程序与模型加载1.创建环境2.Session配置3.CUDA加速4.加载模型5.读取图片、预处理、推理、可视化和保存结果int main() { // 1. 初始化ONNXRuntime环境 Ort::Env env(ORT_LOGGING_LEVEL_WARNING, YOLO); // 2. 配置会话选项并尝试启用CUDA加速 Ort::SessionOptions sessionOptions; sessionOptions.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); try { OrtCUDAProviderOptions cuda_options; sessionOptions.AppendExecutionProvider_CUDA(cuda_options); } catch (const std::exception e) { std::cout CUDA not available, using CPU std::endl; } // 3. 加载模型 Ort::Session session(env, onnx_model_path, sessionOptions); // 4. 获取模型输入信息 auto inputInfo session.GetInputTypeInfo(0).GetTensorTypeAndShapeInfo(); auto shape inputInfo.GetShape(); // 5. 读取并预处理图像 cv::Mat img cv::imread(img_path); PreprocessResult prep preprocessImage1(img, 640, 640); // 6. 执行推理 std::vectorint64_t inputShape {1, 3, 640, 640}; auto detections inferDetections1( session, img, prep, inputShape, images, output0, 0.5f, 0.45f ); // 7. 可视化结果 for (const auto det : detections) { cv::rectangle(img, det.box, cv::Scalar(0, 255, 0), 2); std::cout class det.classId score det.score std::endl; } return 0; }三、其他3.1 计算推理时间在函数inferDetections1()中的auto outputs session.Run( ... );后面加入下面的代码然后运行。第一次 auto outputs session.Run( ... ) 会加载 CUDA 等不计算后续再循环100次auto outputs session.Run( ... )计算推理时间。for (int i 0; i 100; i) { auto start std::chrono::high_resolution_clock::now(); auto outputs session.Run( Ort::RunOptions{ nullptr }, inputNames, inputTensor, 1, outputNames, 1 ); auto end std::chrono::high_resolution_clock::now(); double cost std::chrono::durationdouble, std::milli(end - start).count(); std::cout Run i 1 : cost ms std::endl; }输出结果如下3.2 用CPU推理注释掉main()函数中的下面的代码// main()函数中 try { OrtCUDAProviderOptions cuda_options; sessionOptions.AppendExecutionProvider_CUDA(cuda_options); } catch (const std::exception e) { std::cout e.what() std::endl; } // 直接注释掉 //try //{ // OrtCUDAProviderOptions cuda_options; // sessionOptions.AppendExecutionProvider_CUDA(cuda_options); //} //catch (const std::exception e) //{ // std::cout e.what() std::endl; //}输出结果如下推理时间变长。3.3 Ort::Session.Run()返回值Ort::Session session(env, model_path, session_options); auto output_tensors session.Run(Ort::RunOptions{nullptr}, input_names.data(), input_tensor, 1, output_names.data(), 1);output_tensors是一个std::vectorOrt::Value通常包含1个元素(因为output_names大小为1)。在ONNXRuntime中使用YOLO模型时output_tensors 的内容取决于YOLO的版本和导出格式。格式特征Shape: [batch, features, anchors]例如: [1, 84, 8400]维度说明:- 维度0: batch size (通常为1)- 维度1: 84 80个类别 4个边界框坐标(x,y,w,h)- 维度2: 8400 检测框数量(取决于输入图像大小和模型结构)内存布局[所有框的x坐标, 所有框的y坐标, 所有框的w坐标, 所有框的h坐标, 所有框的class0分数, 所有框的class1分数, ..., 所有框的class79分数]内存中数据排列顺序(以3个检测框简化为例即维度2 3):┌───────────────────────────────────────────┐│ 所有框的x坐标 │ 所有框的y坐标 │ 所有框的w坐标 │ 所有框的h坐标 ...├───────────────────────────────────────────┤│ [x0,x1,x2] │ [y0,y1,y2] │ [w0,w1,w2] │ [h0,h1,h2] ...└───────────────────────────────────────────┘┌──────────────────────────────────────────────┐... 所有框的class0分数 │ 所有框的class1分数 │ ... │ 所有框的class79分数 │├──────────────────────────────────────────────┤... [c0_0,c0_1,c0_2] │ [c1_0,c1_1,c1_2] │ ... │ [c79_0,c79_1,c79_2] │└──────────────────────────────────────────────┘下面代码中以维度num_values为3举例rawOutput内存分布看上图当 i 0 时x rawOutput[0 * num_values i] rawOutput[0] x0当 i 0 时y rawOutput[1 * num_values i] rawOutput[3] y0当 i 1 时x rawOutput[0 * num_values i] rawOutput[1] x1for (int i 0; i num_values; i) { float x rawOutput[0 * num_values i]; float y rawOutput[1 * num_values i]; float w rawOutput[2 * num_values i]; float h rawOutput[3 * num_values i]; float score 0.0f; int classId -1; for (int c 0; c numClasses; c) { float clsScore rawOutput[(4 c) * num_values i]; if (clsScore score) { score clsScore; classId c; } } }3.4 可视化和保存在代码最后增加一下代码int main() { cv::imshow(result, img); cv::waitKey(0); cv::imwrite(result.png, img); return 0; }四、总结YOLOv8、YOLOv11YOLO26 其 C 部署框架基本都是一致的。