基于 ONNX Runtime 的 YOLOv8 高性能 C++ 推理实现

基于 ONNX Runtime 的 YOLOv8 高性能 C++ 推理实现

目录

一、项目背景

二、代码讲解

1. inference.cpp注释版代码:

2. inference.cpp代码框架讲解:

(1)整体思路

(2)文件头与杂项

(3)BlobFromImage(Mat → NCHW 浮点数组)

(4)PreProcess(根据模型类型做图像预处理)

(5)CreateSession(会话创建与参数)

(6)RunSession(一次完整推理)

(7)TensorProcess(核心:Run + 解码输出)

(8)WarmUpSession(预热)

(9)关键参数/结构

3. inference.h代码:

4. main.cpp注释版代码:

三、环境配置

1. CPU 推理环境

2. GPU 推理环境(CUDA 加速)


一、项目背景

本文介绍的项目基于 ONNX RuntimeOpenCV,实现了一个轻量、高效、可扩展的 YOLOv8 C++ 推理模块。它不依赖 PyTorch,可直接加载 .onnx 模型进行推理,适用于 Windows/Linux 平台,支持 CPU 与 CUDA 加速。

项目有三个文件:inference.h,inference.cpp和main.cpp,核心文件inference.cpp。

二、代码讲解

1. inference.cpp注释版代码:

// Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license #define _CRT_SECURE_NO_WARNINGS 1 // 关闭 MSVC 下部分 C 运行库的“安全”警告(如 strcpy 等函数) #include "inference.h" #include <regex> #define benchmark // 打开后会进行简单的时间统计(前处理/推理/后处理耗时打印) #define min(a,b) (((a) < (b)) ? (a) : (b)) // 自定义 min 宏(注意:可能与 std::min 冲突,项目里保持原样) YOLO_V8::YOLO_V8() { } YOLO_V8::~YOLO_V8() { delete session; // 析构时释放 ONNX Runtime 的 Session(注意:input/output 节点名里 new 的 char* 未释放,存在内存泄露风险) } #ifdef USE_CUDA namespace Ort { // 当使用 CUDA 且输入为 half(fp16)时,告知 ORT 该模板类型映射为 ONNX 的 FLOAT16 template<> struct TypeToTensorType<half> { static constexpr ONNXTensorElementDataType type = ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT16; }; } #endif // 将 OpenCV 的 Mat 数据打包为连续的 NCHW blob 并归一化到 [0,1] // T 是指针类型(float* 或 half*),通过 remove_pointer 获得元素类型 template<typename T> char* BlobFromImage(cv::Mat& iImg, T& iBlob) { int channels = iImg.channels(); int imgHeight = iImg.rows; int imgWidth = iImg.cols; // NCHW 排列:先通道 c,再行 h,再列 w for (int c = 0; c < channels; c++) { for (int h = 0; h < imgHeight; h++) { for (int w = 0; w < imgWidth; w++) { // 从 8bit 图像读取像素,转换到 [0,1] 浮点(无减均值/除方差,符合 Ultralytics 默认 img/=255) iBlob[c * imgWidth * imgHeight + h * imgWidth + w] = typename std::remove_pointer<T>::type( (iImg.at<cv::Vec3b>(h, w)[c]) / 255.0f); } } } return RET_OK; // RET_OK 定义为 nullptr,表示成功 } // 预处理:统一三通道 RGB,按模型类型做 Letterbox(检测/姿态)或 CenterCrop(分类) // iImgSize 是模型期望的输入尺寸,通常为 {H, W}(本项目里常用 {640, 640}) // oImg 输出预处理后的图(尺寸为 iImgSize) char* YOLO_V8::PreProcess(cv::Mat& iImg, std::vector<int> iImgSize, cv::Mat& oImg) { // 将输入统一转换为 RGB 三通道 if (iImg.channels() == 3) { oImg = iImg.clone(); cv::cvtColor(oImg, oImg, cv::COLOR_BGR2RGB); // OpenCV 读图默认 BGR,这里转为 RGB } else { cv::cvtColor(iImg, oImg, cv::COLOR_GRAY2RGB); // 灰度图补成 3 通道 } switch (modelType) { // 检测/姿态(及其 FP16 版本)走 Letterbox:等比例缩放到长边=目标边,并把图贴到左上角,其余区域用黑色填充 case YOLO_DETECT_V8: case YOLO_POSE: case YOLO_DETECT_V8_HALF: case YOLO_POSE_V8_HALF://LetterBox { if (iImg.cols >= iImg.rows) { // 原图更宽:以宽为基准缩放到目标宽 iImgSize[0] // resizeScales 记录“原图 -> 模型输入”的缩放比例(长边/目标边),后处理时用于还原坐标 resizeScales = iImg.cols / (float)iImgSize.at(0); cv::resize(oImg, oImg, cv::Size(iImgSize.at(0), int(iImg.rows / resizeScales))); } else { // 原图更高:以高为基准缩放到目标高 iImgSize[0](注意:此处假定 iImgSize[0] 为目标的“对齐边”) resizeScales = iImg.rows / (float)iImgSize.at(0); cv::resize(oImg, oImg, cv::Size(int(iImg.cols / resizeScales), iImgSize.at(1))); } // 构造一个目标大小的黑底图(注意:Mat::zeros 的参数是 rows, cols = 高, 宽, // 这里传入的是 iImgSize.at(0), iImgSize.at(1),当二者相等时无影响;不等时需确保含义一致) cv::Mat tempImg = cv::Mat::zeros(iImgSize.at(0), iImgSize.at(1), CV_8UC3); // 将缩放后的图贴到黑底的左上角(0,0)。=> 后处理时不需要扣 padding 偏移 oImg.copyTo(tempImg(cv::Rect(0, 0, oImg.cols, oImg.rows))); oImg = tempImg; break; } case YOLO_CLS://CenterCrop { // 分类模型:中心裁出正方形(边长 m=min(h,w)),再缩放到目标尺寸 int h = iImg.rows; int w = iImg.cols; int m = min(h, w); // 使用了上面定义的宏 min(h,w) int top = (h - m) / 2; int left = (w - m) / 2; cv::resize(oImg(cv::Rect(left, top, m, m)), oImg, cv::Size(iImgSize.at(0), iImgSize.at(1))); break; } } return RET_OK; } // 创建 ONNX Runtime Session,并预热;同时做路径合法性检查和运行选项配置 char* YOLO_V8::CreateSession(DL_INIT_PARAM& iParams) { char* Ret = RET_OK; std::regex pattern("[\u4e00-\u9fa5]"); // 匹配中文字符的正则 bool result = std::regex_search(iParams.modelPath, pattern); if (result) { // 如果模型路径含中文,提前提示更换路径(避免某些平台/库对非 ASCII 路径处理不一致) Ret = (char*)"[YOLO_V8]:Your model path is error.Change your model path without chinese characters."; std::cout << Ret << std::endl; return Ret; } try { // 记录初始化参数 rectConfidenceThreshold = iParams.rectConfidenceThreshold; iouThreshold = iParams.iouThreshold; imgSize = iParams.imgSize; modelType = iParams.modelType; cudaEnable = iParams.cudaEnable; // 创建 ORT 环境(设置日志等级标签) env = Ort::Env(ORT_LOGGING_LEVEL_WARNING, "Yolo"); // 配置会话选项 Ort::SessionOptions sessionOption; if (iParams.cudaEnable) { // 若启用 CUDA,则追加 CUDA 执行提供器(默认 device_id=0) OrtCUDAProviderOptions cudaOption; cudaOption.device_id = 0; sessionOption.AppendExecutionProvider_CUDA(cudaOption); } // 图优化级别:开启全部优化 sessionOption.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); // 线程数 sessionOption.SetIntraOpNumThreads(iParams.intraOpNumThreads); // 日志等级 sessionOption.SetLogSeverityLevel(iParams.logSeverityLevel); #ifdef _WIN32 // Windows 下:将 UTF-8 的路径转换为宽字符路径(满足 ORT 接口) int ModelPathSize = MultiByteToWideChar(CP_UTF8, 0, iParams.modelPath.c_str(), static_cast<int>(iParams.modelPath.length()), nullptr, 0); wchar_t* wide_cstr = new wchar_t[ModelPathSize + 1]; MultiByteToWideChar(CP_UTF8, 0, iParams.modelPath.c_str(), static_cast<int>(iParams.modelPath.length()), wide_cstr, ModelPathSize); wide_cstr[ModelPathSize] = L'\0'; const wchar_t* modelPath = wide_cstr; #else // 其他平台:直接使用 UTF-8 窄字节路径 const char* modelPath = iParams.modelPath.c_str(); #endif // _WIN32 // 创建 ORT Session(加载模型) session = new Ort::Session(env, modelPath, sessionOption); // 读取输入/输出节点名(用于 Run 时指定) Ort::AllocatorWithDefaultOptions allocator; size_t inputNodesNum = session->GetInputCount(); for (size_t i = 0; i < inputNodesNum; i++) { Ort::AllocatedStringPtr input_node_name = session->GetInputNameAllocated(i, allocator); char* temp_buf = new char[50]; // 注意:这里分配的内存未在析构中释放,存在泄露 strcpy(temp_buf, input_node_name.get()); inputNodeNames.push_back(temp_buf); } size_t OutputNodesNum = session->GetOutputCount(); for (size_t i = 0; i < OutputNodesNum; i++) { Ort::AllocatedStringPtr output_node_name = session->GetOutputNameAllocated(i, allocator); char* temp_buf = new char[10]; // 同上:未释放 strcpy(temp_buf, output_node_name.get()); outputNodeNames.push_back(temp_buf); } // Run 选项(默认空) options = Ort::RunOptions{ nullptr }; // 预热一次,降低首帧延迟 WarmUpSession(); return RET_OK; } catch (const std::exception& e) { // 捕获异常并打印(这里额外拼接了一段字符串,随后释放临时分配的 merged) const char* str1 = "[YOLO_V8]:"; const char* str2 = e.what(); std::string result = std::string(str1) + std::string(str2); char* merged = new char[result.length() + 1]; strcpy(merged, result.c_str()); std::cout << merged << std::endl; delete[] merged; return (char*)"[YOLO_V8]:Create session failed."; } } // 运行一次推理:包含预处理、打包 blob、调用 TensorProcess(内部完成 session->Run 与后处理) // 输出结果存入 oResult char* YOLO_V8::RunSession(cv::Mat& iImg, std::vector<DL_RESULT>& oResult) { #ifdef benchmark clock_t starttime_1 = clock(); // 开始计时:前处理起点 #endif // benchmark char* Ret = RET_OK; cv::Mat processedImg; PreProcess(iImg, imgSize, processedImg); // 先做预处理(RGB、Letterbox/CenterCrop、缩放) if (modelType < 4) { // FP32 路径:为输入创建 float* blob(大小 = H*W*3) float* blob = new float[processedImg.total() * 3]; BlobFromImage(processedImg, blob); // Mat -> NCHW blob,归一化到 [0,1] // 注意:这里把输入维度设为 {1, 3, imgSize[0], imgSize[1]},即 NCHW std::vector<int64_t> inputNodeDims = { 1, 3, imgSize.at(0), imgSize.at(1) }; TensorProcess(starttime_1, iImg, blob, inputNodeDims, oResult); // 进入核心推理与后处理 } else { #ifdef USE_CUDA // FP16 路径(仅在定义了 USE_CUDA 时有效) half* blob = new half[processedImg.total() * 3]; BlobFromImage(processedImg, blob); std::vector<int64_t> inputNodeDims = { 1,3,imgSize.at(0),imgSize.at(1) }; TensorProcess(starttime_1, iImg, blob, inputNodeDims, oResult); #endif } return Ret; } // 核心流程:把 blob 封装成 ORT Tensor -> session->Run -> 读取输出 -> 按模型类型做后处理(检测/NMS 或 分类) // 模板类型 N 为 float* 或 half*,通过 remove_pointer 决定张量元素类型 template<typename N> char* YOLO_V8::TensorProcess(clock_t& starttime_1, cv::Mat& iImg, N& blob, std::vector<int64_t>& inputNodeDims, std::vector<DL_RESULT>& oResult) { // 创建输入张量(使用 CPU 内存) Ort::Value inputTensor = Ort::Value::CreateTensor<typename std::remove_pointer<N>::type>( Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU), blob, 3 * imgSize.at(0) * imgSize.at(1), inputNodeDims.data(), inputNodeDims.size()); #ifdef benchmark clock_t starttime_2 = clock(); // 记录推理开始时间 #endif // benchmark // 进行推理:传入输入名数组、输入张量指针、输出名数组,得到输出张量列表 auto outputTensor = session->Run(options, inputNodeNames.data(), &inputTensor, 1, outputNodeNames.data(), outputNodeNames.size()); #ifdef benchmark clock_t starttime_3 = clock(); // 记录推理结束时间 #endif // benchmark // 读取输出张量的形状信息(本实现只读取第一个输出) Ort::TypeInfo typeInfo = outputTensor.front().GetTypeInfo(); auto tensor_info = typeInfo.GetTensorTypeAndShapeInfo(); std::vector<int64_t> outputNodeDims = tensor_info.GetShape(); // 获取可写数据指针(float* 或 half*,之后按需转成 CV_32F) auto output = outputTensor.front().GetTensorMutableData<typename std::remove_pointer<N>::type>(); delete[] blob; // 释放输入 blob switch (modelType) { case YOLO_DETECT_V8: case YOLO_DETECT_V8_HALF: { // 典型的 YOLOv8 检测输出形状示例:[1, 84, 8400] // 其中 84 = 4(x,y,w,h) + num_classes;8400 = 三个特征层展平后的总候选数 int signalResultNum = outputNodeDims[1];//84 int strideNum = outputNodeDims[2];//8400 std::vector<int> class_ids; std::vector<float> confidences; std::vector<cv::Rect> boxes; cv::Mat rawData; if (modelType == YOLO_DETECT_V8) { // FP32:直接视作 CV_32F rawData = cv::Mat(signalResultNum, strideNum, CV_32F, output); } else { // FP16:先视作 CV_16F,再转成 CV_32F 便于处理 rawData = cv::Mat(signalResultNum, strideNum, CV_16F, output); rawData.convertTo(rawData, CV_32F); } // 重要说明: // Ultralytics 在导出 yolov8 ONNX 时,对输出做了 transpose,使得 v5/v7/v8 的输出形状在解码阶段一致 // 参考:https://github.com/ultralytics/assets/releases/download/v8.3.0/yolov8n.pt rawData = rawData.t(); // 现在 rawData 形状为 (strideNum, signalResultNum) = (8400, 84) float* data = (float*)rawData.data; // 遍历每个候选(anchor/位置) for (int i = 0; i < strideNum; ++i) { // data[0..3] 为 (cx, cy, w, h),data[4..] 为各类分数 float* classesScores = data + 4; // 将类别分数封装成一行 Mat,便于用 minMaxLoc 找最大类和分数 cv::Mat scores(1, this->classes.size(), CV_32FC1, classesScores); cv::Point class_id; double maxClassScore; cv::minMaxLoc(scores, 0, &maxClassScore, 0, &class_id); // 置信度阈值过滤 if (maxClassScore > rectConfidenceThreshold) { confidences.push_back(maxClassScore); class_ids.push_back(class_id.x); // 从输入尺度(Letterbox 后的 640×640 坐标系)读取中心点与宽高 float x = data[0]; float y = data[1]; float w = data[2]; float h = data[3]; // 将 (cx,cy,w,h) 转成左上角 + 宽高,并用 resizeScales 映射回原图坐标系 // 注意:预处理时把缩放后的图贴到了左上角,所以这里不需要额外减去 padding 偏移 int left = int((x - 0.5 * w) * resizeScales); int top = int((y - 0.5 * h) * resizeScales); int width = int(w * resizeScales); int height = int(h * resizeScales); boxes.push_back(cv::Rect(left, top, width, height)); } // 跳到下一行(下一个候选),每行有 signalResultNum (=84) 个 float data += signalResultNum; } // 非极大值抑制(NMS):根据 iouThreshold 去掉与高分框重叠过大的框 std::vector<int> nmsResult; cv::dnn::NMSBoxes(boxes, confidences, rectConfidenceThreshold, iouThreshold, nmsResult); // 根据 NMS 保留的索引组装最终结果 for (int i = 0; i < nmsResult.size(); ++i) { int idx = nmsResult[i]; DL_RESULT result; result.classId = class_ids[idx]; result.confidence = confidences[idx]; result.box = boxes[idx]; oResult.push_back(result); } #ifdef benchmark // 统计耗时:前处理(starttime_1->2), 推理(2->3), 后处理(3->4) clock_t starttime_4 = clock(); double pre_process_time = (double)(starttime_2 - starttime_1) / CLOCKS_PER_SEC * 1000; double process_time = (double)(starttime_3 - starttime_2) / CLOCKS_PER_SEC * 1000; double post_process_time = (double)(starttime_4 - starttime_3) / CLOCKS_PER_SEC * 1000; if (cudaEnable) { std::cout << "[YOLO_V8(CUDA)]: " << pre_process_time << "ms pre-process, " << process_time << "ms inference, " << post_process_time << "ms post-process." << std::endl; } else { std::cout << "[YOLO_V8(CPU)]: " << pre_process_time << "ms pre-process, " << process_time << "ms inference, " << post_process_time << "ms post-process." << std::endl; } #endif // benchmark break; } case YOLO_CLS: case YOLO_CLS_HALF: { // 分类模型:输出为一行 num_classes 的分数 cv::Mat rawData; if (modelType == YOLO_CLS) { // FP32 rawData = cv::Mat(1, this->classes.size(), CV_32F, output); } else { // FP16:转为 CV_32F rawData = cv::Mat(1, this->classes.size(), CV_16F, output); rawData.convertTo(rawData, CV_32F); } float* data = (float*)rawData.data; // 将每个类别的分数封装到 DL_RESULT(这里没有做 softmax/topk,原样输出) DL_RESULT result; for (int i = 0; i < this->classes.size(); i++) { result.classId = i; result.confidence = data[i]; oResult.push_back(result); } break; } default: // 其它模型类型(例如姿态)在当前实现中未覆盖 std::cout << "[YOLO_V8]: " << "Not support model type." << std::endl; } return RET_OK; } // 预热:构造一张目标尺寸的空图,走一遍预处理+推理,触发内存分配、内核加载等,避免首帧抖动 char* YOLO_V8::WarmUpSession() { clock_t starttime_1 = clock(); // 注意:cv::Size(width, height),这里传的是 (imgSize.at(0), imgSize.at(1)) // 若 imgSize 表示 {H, W},此处相当于 {W, H},在方形输入下无影响;非方形时要确保与实际约定一致 cv::Mat iImg = cv::Mat(cv::Size(imgSize.at(0), imgSize.at(1)), CV_8UC3); cv::Mat processedImg; PreProcess(iImg, imgSize, processedImg); if (modelType < 4) { // FP32 预热 float* blob = new float[iImg.total() * 3]; BlobFromImage(processedImg, blob); std::vector<int64_t> YOLO_input_node_dims = { 1, 3, imgSize.at(0), imgSize.at(1) }; Ort::Value input_tensor = Ort::Value::CreateTensor<float>( Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU), blob, 3 * imgSize.at(0) * imgSize.at(1), YOLO_input_node_dims.data(), YOLO_input_node_dims.size()); auto output_tensors = session->Run(options, inputNodeNames.data(), &input_tensor, 1, outputNodeNames.data(), outputNodeNames.size()); delete[] blob; clock_t starttime_4 = clock(); double post_process_time = (double)(starttime_4 - starttime_1) / CLOCKS_PER_SEC * 1000; if (cudaEnable) { std::cout << "[YOLO_V8(CUDA)]: " << "Cuda warm-up cost " << post_process_time << " ms. " << std::endl; } } else { #ifdef USE_CUDA // FP16 预热(仅在定义了 USE_CUDA 时) half* blob = new half[iImg.total() * 3]; BlobFromImage(processedImg, blob); std::vector<int64_t> YOLO_input_node_dims = { 1,3,imgSize.at(0),imgSize.at(1) }; Ort::Value input_tensor = Ort::Value::CreateTensor<half>(Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU), blob, 3 * imgSize.at(0) * imgSize.at(1), YOLO_input_node_dims.data(), YOLO_input_node_dims.size()); auto output_tensors = session->Run(options, inputNodeNames.data(), &input_tensor, 1, outputNodeNames.data(), outputNodeNames.size()); delete[] blob; clock_t starttime_4 = clock(); double post_process_time = (double)(starttime_4 - starttime_1) / CLOCKS_PER_SEC * 1000; if (cudaEnable) { std::cout << "[YOLO_V8(CUDA)]: " << "Cuda warm-up cost " << post_process_time << " ms. " << std::endl; } #endif } return RET_OK; } 

2. inference.cpp代码框架讲解:

(1)整体思路

  1. CreateSession:加载 ONNX 模型 → 创建 Ort::Session(可选 CUDA)→ 记录输入/输出节点名 → 预热一次。
  2. RunSession:对输入图像做预处理(按模型种类)→ 组装 NCHW 的输入张量 → 推理 → 按模型类型做后处理(目标框/NMS 或 分类)。
  3. WarmUpSession:构造一张空图,走一遍预处理+推理,避免首次调用慢
  4. 其余是工具函数/模板BlobFromImagecv::Mat 转为连续内存(按通道排布),TensorProcess 模板负责真正的 session->Run 和解析输出

(2)文件头与杂项

  • #define benchmark:打开后会打印前处理、推理、后处理耗时。
  • #define min(a,b):自定义了一个 min 宏(不建议,容易和 std::min 冲突)。
  • 构/析:构造函数空;析构里只 delete session;注意:没释放输入/输出节点名的 new char[],有内存泄露,下文详述)。

FP16 支持(仅在 USE_CUDA 下)

template<> struct TypeToTensorType<half> { ... }; 

让 ORT 能识别 half 类型建张量。


(3)BlobFromImage(Mat → NCHW 浮点数组)

template<typename T> char* BlobFromImage(cv::Mat& iImg, T& iBlob) 
  • 输入:BGR/RGB 的 cv::Mat(8UC3)。
  • 输出:把像素按 通道优先 NCHW 排列到一段连续内存 iBlob(在外面 new 出来的)。
  • 逐像素取值 /255.0f 归一化;不做减均值/除方差。
  • 注意:使用 iImg.at<cv::Vec3b>(h,w)[c],默认假定图像是 8bit 3 通道;理论上传灰度图会越界,不过在 PreProcess 里已经统一成 3 通道 RGB

(4)PreProcess(根据模型类型做图像预处理)

char* YOLO_V8::PreProcess(cv::Mat& iImg, std::vector<int> iImgSize, cv::Mat& oImg) 
  • 先保证输入为 RGB:BGR→RGB;灰度→RGB。
  • 分两类:
    1. 检测/姿态(YOLO_DETECT_V8YOLO_POSE,以及它们的 FP16 版本)
      • 计算 resizeScales = 原边/目标边,按长边对齐缩放:
        • 若宽≥高:目标宽=iImgSize[0],高按比例;反之相同。
      • 然后把缩放后的图复制到一个 全 0 的大图iImgSize[0]×iImgSize[1])的左上角(0,0)。
        • 这相当于 Letterbox,但只在右边或下边补零不是居中)。后处理时按同一 resizeScales 去恢复坐标。
    2. 分类(YOLO_CLS
      • CenterCrop:从中间裁一个正方形(边长=min(h,w)),再缩放到 iImgSize
很多实现会把 Letterbox 居中,这里是 左上角对齐对应地后处理没有加偏移,只乘了 resizeScales,保持了一致

(5)CreateSession(会话创建与参数)

char* YOLO_V8::CreateSession(DL_INIT_PARAM& iParams) 
  • 校验 模型路径不能含中文(用正则查 [\u4e00-\u9fa5])。
  • 记录阈值、输入尺寸、模型类型、是否启用 CUDA、线程数、日志等级等。
  • Ort::Env + Ort::SessionOptions
    • cudaEnable==true:设置 OrtCUDAProviderOptions
    • SetGraphOptimizationLevel(ORT_ENABLE_ALL)
    • SetIntraOpNumThreads(...)
  • Windows 下把 utf-8 路径转宽字符;非 Windows 直接用 char*
  • session = new Ort::Session(env, modelPath, sessionOption);
  • 获取 I/O 节点名:调用 GetInputNameAllocated GetOutputNameAllocated,拷贝到 new char[]push_back 保存。
  • WarmUpSession() 预热。
  • 发生异常会返回统一的错误文本。

(6)RunSession(一次完整推理)

char* YOLO_V8::RunSession(cv::Mat& iImg, std::vector<DL_RESULT>& oResult) 
  • 计时(可选)。
  • PreProcess 得到 processedImg
  • 根据 modelType 选择 FP32 或(在 USE_CUDA 编译时)FP16blob
    • inputNodeDims = {1, 3, imgH, imgW}NCHW)。
  • TensorProcess(...) 执行推理与后处理。
如果把 modelType 设到 FP16 路径,但工程没有定义 USE_CUDA,这段代码不会给出替代逻辑(编译能过,但不会走 FP16 分支),要保证两者匹配。

(7)TensorProcess(核心:Run + 解码输出)

template<typename N> char* YOLO_V8::TensorProcess(clock_t& starttime_1, cv::Mat& iImg, N& blob, std::vector<int64_t>& inputNodeDims, std::vector<DL_RESULT>& oResult) 
  • session->Run(...) 得到 outputTensor(只取第一个输出)。
  • 取形状 outputNodeDims,拿到数据指针 output
  • 模型类型分支
    1. 检测(YOLO v8 Detect)
      • 读取输出形状:[batch, 84, 8400](示例),其中 84=4(bbox)+num_classes。
      • 遍历每一行:
        • data[0..3]cx, cy, w, hdata[4..] 是各类分数。
        • 取最大类分数,若 > rectConfidenceThreshold
          • 计算左上角与宽高,仅用 resizeScales 等比放大回原图坐标(因为前处理是左上填充,不需要加偏移)。
          • 存入 boxes/confidences/class_ids
      • cv::dnn::NMSBoxes(..., iouThreshold) 做 NMS,组装 DL_RESULT 返回。
      • benchmark:打印三段耗时(pre/infer/post)。
    2. 分类(YOLO v8 Cls)
      • 输出 shape 就是一行 num_classes,FP16 时先 convertTo(CV_32F)
      • 逐类把 (id, score) 推到 oResult(是否取 top-k 交由调用方自己处理)。
    3. 其它类型:打印不支持。
  • 释放delete[] blob;(输入缓存释放及时)。

代码把原始数据构成 cv::Mat(signalResultNum, strideNum, CV_32F/16F, output),再 转置(8400, 84)

将输出 84x8400 的矩阵转置为 8400x84,便于按行处理每个检测框。

创建输入张量

Ort::Value::CreateTensor<typename std::remove_pointer<N>::type>(..., blob, 3*H*W, dims...) 

Nfloat*half*,通过 remove_pointer 得到元素类型。


(8)WarmUpSession(预热)

  • 构造一张 imgSize 大小的三通道黑图;跑一次 PreProcess +(按模型类型选择 FP32/FP16)+ session->Run
  • 打印 CUDA 预热耗时(只有 cudaEnable=true 时打印)。
  • 作用:避免首次真实推理时的内存分配、内核 JIT 等导致的抖动

(9)关键参数/结构

  • DL_INIT_PARAM(在头文件里定义):包含
    • rectConfidenceThreshold(置信度阈值)
    • iouThreshold
    • imgSize(例如 {640,640}
    • modelType(枚举:Detect/Cls/Pose 及 FP16 版本)
    • cudaEnable
    • 线程数/日志等级/modelPath
  • DL_RESULT:后处理输出
    • 检测:classId, confidence, box
    • 分类:classId, confidence(每类一条)

3. inference.h代码:

// Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license #pragma once #define RET_OK nullptr #ifdef _WIN32 #include <Windows.h> #include <direct.h> #include <io.h> #endif #include <string> #include <vector> #include <cstdio> #include <opencv2/opencv.hpp> #include "onnxruntime_cxx_api.h" #ifdef USE_CUDA #include <cuda_fp16.h> #endif enum MODEL_TYPE { //FLOAT32 MODEL YOLO_DETECT_V8 = 1, YOLO_POSE = 2, YOLO_CLS = 3, //FLOAT16 MODEL YOLO_DETECT_V8_HALF = 4, YOLO_POSE_V8_HALF = 5, YOLO_CLS_HALF = 6 }; typedef struct _DL_INIT_PARAM { std::string modelPath; MODEL_TYPE modelType = YOLO_DETECT_V8; std::vector<int> imgSize = { 640, 640 }; float rectConfidenceThreshold = 0.6; float iouThreshold = 0.5; int keyPointsNum = 2;//Note:kpt number for pose bool cudaEnable = false; int logSeverityLevel = 3; int intraOpNumThreads = 1; } DL_INIT_PARAM; typedef struct _DL_RESULT { int classId; float confidence; cv::Rect box; std::vector<cv::Point2f> keyPoints; } DL_RESULT; class YOLO_V8 { public: YOLO_V8(); ~YOLO_V8(); public: char* CreateSession(DL_INIT_PARAM& iParams); char* RunSession(cv::Mat& iImg, std::vector<DL_RESULT>& oResult); char* WarmUpSession(); template<typename N> char* TensorProcess(clock_t& starttime_1, cv::Mat& iImg, N& blob, std::vector<int64_t>& inputNodeDims, std::vector<DL_RESULT>& oResult); char* PreProcess(cv::Mat& iImg, std::vector<int> iImgSize, cv::Mat& oImg); std::vector<std::string> classes{}; private: Ort::Env env; Ort::Session* session; bool cudaEnable; Ort::RunOptions options; std::vector<const char*> inputNodeNames; std::vector<const char*> outputNodeNames; MODEL_TYPE modelType; std::vector<int> imgSize; float rectConfidenceThreshold; float iouThreshold; float resizeScales;//letterbox scale }; 

4. main.cpp注释版代码:

// Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license #include <iostream> // 标准输入输出,用于打印日志/提示 #include <iomanip> // 控制浮点输出格式(setprecision等) #include "inference.h"// 本项目的推理类 YOLO_V8 的声明 #include <filesystem> // C++17 文件系统库,遍历目录读取图片 #include <fstream> // 读写文件(用于读取 coco.yaml) #include <random> // 随机数(生成随机颜色等) // ------------------------------- // Detector:目标检测的演示函数 // 参数 p 为 YOLO_V8* 的引用(YOLO_V8*&),保留“能在函数内修改指针本身”的能力 // 功能:遍历工作目录下的 ./images/ 文件夹,逐张图片执行 RunSession,绘制检测框与标签并显示 // ------------------------------- void Detector(YOLO_V8*& p) { std::filesystem::path current_path = std::filesystem::current_path(); // 当前工作目录 std::filesystem::path imgs_path = current_path / "images"; // 约定图片放在 ./images/ 目录 for (auto& i : std::filesystem::directory_iterator(imgs_path)) // 遍历目录下所有文件 { // 仅处理常见的位图格式 if (i.path().extension() == ".jpg" || i.path().extension() == ".png" || i.path().extension() == ".jpeg") { std::string img_path = i.path().string(); // 完整路径字符串 cv::Mat img = cv::imread(img_path); // OpenCV 读图(BGR) std::vector<DL_RESULT> res; // 存放推理结果(多个目标) p->RunSession(img, res); // 核心推理(前处理→推理→后处理) // 遍历本张图片的所有检测结果,绘制可视化 for (auto& re : res) { // 生成随机颜色:不同目标用不同颜色,便于区分 cv::RNG rng(cv::getTickCount()); cv::Scalar color(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256)); // 在原图上画出目标框(左上角、右下角由 re.box 决定;线宽=3) cv::rectangle(img, re.box, color, 3); // 置信度格式化:保留两位小数 // floor(100*x)/100 是一种“截断到两位”的方式,后续 substr 仅为美观去掉多余字符 float confidence = floor(100 * re.confidence) / 100; std::cout << std::fixed << std::setprecision(2); // 控制 cout 的浮点显示为两位小数 std::string label = p->classes[re.classId] + " " + std::to_string(confidence).substr(0, std::to_string(confidence).size() - 4); // 上面 substr(... size()-4) 的小技巧:去掉 to_string 默认多余的位数(如 "0.50xxxx") // 在框上方绘制一块实心矩形作为文字背景,避免文本与图像混淆 cv::rectangle( img, cv::Point(re.box.x, re.box.y - 25), cv::Point(re.box.x + label.length() * 15, re.box.y), color, cv::FILLED ); // 在背景矩形上绘制类别+置信度文本(黑字) cv::putText( img, label, cv::Point(re.box.x, re.box.y - 5), cv::FONT_HERSHEY_SIMPLEX, 0.75, cv::Scalar(0, 0, 0), 2 ); } // 显示当前图片的检测结果;等待任意键继续到下一张 std::cout << "Press any key to exit" << std::endl; cv::imshow("Result of Detection", img); cv::waitKey(0); cv::destroyAllWindows(); } } } // ------------------------------- // Classifier:分类任务的演示函数 // 功能:遍历当前目录下的图片,调用分类模型,直接把每个类别的分数写到图像上显示 // 说明:分类输出是“对每个类别的置信度”,此处简单地按序写出;可自行改为只显示Top-K // ------------------------------- void Classifier(YOLO_V8*& p) { std::filesystem::path current_path = std::filesystem::current_path(); // 当前工作目录 std::filesystem::path imgs_path = current_path;// / "images" // 示例使用当前目录;也可改为 ./images // 为了使每一行分数显示不同颜色,准备一个[0,255]的均匀分布随机数生成器 std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<int> dis(0, 255); for (auto& i : std::filesystem::directory_iterator(imgs_path)) { if (i.path().extension() == ".jpg" || i.path().extension() == ".png") { std::string img_path = i.path().string(); //std::cout << img_path << std::endl; cv::Mat img = cv::imread(img_path); std::vector<DL_RESULT> res; // 分类结果:每个类别一条记录(classId, confidence) char* ret = p->RunSession(img, res); // 运行分类推理(FP32/FP16 由模型类型决定) // 逐行把每个类别的分数打印到图像上(从 y=50 开始,每行间距 50 像素) float positionY = 50; for (int i = 0; i < res.size(); i++) { int r = dis(gen); int g = dis(gen); int b = dis(gen); cv::putText(img, std::to_string(i) + ":", cv::Point(10, positionY), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(b, g, r), 2); cv::putText(img, std::to_string(res.at(i).confidence), cv::Point(70, positionY), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(b, g, r), 2); positionY += 50; } // 显示分类结果;按键关闭窗口 cv::imshow("TEST_CLS", img); cv::waitKey(0); cv::destroyAllWindows(); //cv::imwrite("E:\\output\\" + std::to_string(k) + ".png", img); // 可选:把结果保存到硬盘 } } } // ------------------------------- // ReadCocoYaml:从 coco.yaml 读取类别名到 p->classes // 假定 coco.yaml 中存在形如: // names: // 0: person // 1: bicycle // ... // 这种简单键值对列表。这里用最朴素的行扫描+字符串分割来解析。 // ------------------------------- int ReadCocoYaml(YOLO_V8*& p) { // Open the YAML file std::ifstream file("coco.yaml"); // 从当前工作目录读取 coco.yaml if (!file.is_open()) { std::cerr << "Failed to open file" << std::endl; return 1; } // Read the file line by line std::string line; std::vector<std::string> lines; while (std::getline(file, line)) { lines.push_back(line); // 全部行读入内存,后续扫描 } // Find the start and end of the names section // 思路:找到包含 "names:" 的行作为起点,再找到“下一段的起始”作为终点(简单根据冒号是否出现判断) std::size_t start = 0; std::size_t end = 0; for (std::size_t i = 0; i < lines.size(); i++) { if (lines[i].find("names:") != std::string::npos) { start = i + 1; // names: 的下一行起为数据起点 } else if (start > 0 && lines[i].find(':') == std::string::npos) { end = i; // 碰到不含冒号的行,认为 names 段结束(简化处理) break; } } // Extract the names // 将每行按冒号分割,取冒号后的字符串作为类别名(不去空白,按原样) std::vector<std::string> names; for (std::size_t i = start; i < end; i++) { std::stringstream ss(lines[i]); std::string name; std::getline(ss, name, ':'); // Extract the number before the delimiter // 左侧序号(丢弃) std::getline(ss, name); // Extract the string after the delimiter // 右侧名称(保留) names.push_back(name); } p->classes = names; // 写回 YOLO_V8 实例,供可视化使用(label 文本) return 0; } // ------------------------------- // DetectTest:检测 Demo 的入口 // 负责:创建 YOLO_V8 实例 → 设定类别名/参数 → CreateSession → 调用 Detector → 释放实例 // ------------------------------- void DetectTest() { YOLO_V8* yoloDetector = new YOLO_V8; // 动态创建(也可用智能指针,这里保持示例风格) //ReadCocoYaml(yoloDetector); // 可选:从 coco.yaml 读取 80 类 yoloDetector->classes = { "face" }; // 示例:仅一类“face”,便于测试人脸模型 DL_INIT_PARAM params; // 初始化推理参数(见 inference.h) params.rectConfidenceThreshold = 0.1; // 置信度阈值(较低,便于观察效果) params.iouThreshold = 0.5; // NMS 的 IOU 阈值 params.modelPath = "best.onnx"; // ONNX 模型路径(与可执行文件相对路径) params.imgSize = { 640, 640 }; // 模型输入分辨率(与导出模型一致) #ifdef USE_CUDA params.cudaEnable = true; // 启用 CUDA EP(前提:ORT 构建包含 CUDA) // GPU FP32 inference params.modelType = YOLO_DETECT_V8; // 使用 FP32 检测模型 // GPU FP16 inference //Note: change fp16 onnx model //params.modelType = YOLO_DETECT_V8_HALF; // 使用 FP16(需换成对应的 FP16 ONNX) #else // CPU inference params.modelType = YOLO_DETECT_V8; // CPU 版仍使用 FP32 模型 params.cudaEnable = false; // 关闭 CUDA #endif yoloDetector->CreateSession(params); // 创建 ORT 会话并预热,准备推理 Detector(yoloDetector); // 运行检测 Demo:遍历 ./images/ 并可视化 delete yoloDetector; // 释放实例(注意:当前实现中 I/O 节点名有内存泄露,示例不处理) } // ------------------------------- // ClsTest:分类 Demo 的入口 // 负责:创建实例 → 读取类别名 → 设定分类模型参数 → CreateSession → 调用 Classifier // ------------------------------- void ClsTest() { YOLO_V8* yoloDetector = new YOLO_V8; std::string model_path = "cls.onnx"; // 分类模型的 ONNX 路径 ReadCocoYaml(yoloDetector); // 从 coco.yaml 读取类别名(也可改成自定义) DL_INIT_PARAM params{ model_path, YOLO_CLS, {224, 224} }; // 简写的聚合初始化:路径、模型类型、输入尺寸 yoloDetector->CreateSession(params); // 创建会话(分类分支) Classifier(yoloDetector); // 遍历目录图片,叠加每类分数并显示 } // ------------------------------- // main:程序入口 // 默认跑检测 Demo;若需要跑分类,注释 DetectTest 并打开 ClsTest 即可 // ------------------------------- int main() { DetectTest(); //ClsTest(); return 0; } 

 推理结果如图所示

三、环境配置

在本项目中,既可以选择 CPU 推理,也可以选择 GPU(CUDA)推理
两种方式的配置方法略有不同,下面分别说明。

1. CPU 推理环境

如果只打算在 CPU 上运行,那么只需要在 Visual Studio 中正确配置好 OpenCV + ONNXRuntime 的依赖环境即可。

另外,还需要将 D:\onnxruntime\lib 下的部分 DLL 文件复制到 ...\x64\Debug 文件夹中。如下图:

这是因为:

  • VS 在调试运行时,会默认到 可执行文件所在目录(如 x64\Debugx64\Release)寻找依赖库;
  • 如果 DLL 只存在于 D:\onnxruntime\lib 而没有被拷贝过来,程序运行时就会提示 缺少某某 DLL,无法启动
  • 把 DLL 复制到输出目录,就能确保可执行文件能在运行时直接加载到所需的动态链接库。

完成配置后,程序会自动调用 CPU 进行推理,控制台会显示类似下面的信息:

此时无需额外操作,就能正常跑通推理与检测,适合在没有 NVIDIA GPU 的环境中使用。

2. GPU 推理环境(CUDA 加速)

如果希望利用 GPU 进行加速,则需要在CPU的环境配置基础上进行以下额外步骤

  1. 启用 CUDA 宏
    打开 inference.h 头文件,在文件开头加入:

此时直接运行会报错,报错内容是找不到#include <cuda_fp16.h>,此时需要在 Visual Studio 项目属性中,正确添加 CUDA Include 目录Lib 目录

此时生成依旧会报错,显示Could not locate zlibwapi.dll. Please make sure it is in your library path! 
这是因为 OpenCV 在运行时依赖 zlibwapi.dll,但系统中找不到。

解决方法:需要到zlibwapi.dll下载-zlibwapi.dll官方版下载[程序文本]-下载之家这个链接下载zlibwapi.dll,放在放到c:\windows\system32下面。

重新运行程序,皆可以正常运行,命令行也显示启用GPU了。

Read more

优选算法——位运算

👇作者其它专栏 《数据结构与算法》《算法》《C++起始之路》 1.前要知识 《位操作符的妙用》 2.相关题解 2.1判定字符是否唯一 算法思路: 利用【位图】的思想,每一个【比特位】代表一个【字符】,一个int类型的变量的32位足够表示所有的小写字母。比特位里若为0,表示这个字符没有出现过;若为1,表示该字符出现过。 可以用一个【整数】来充当【哈希表】。 class Solution { public: bool isUnique(string astr) { //利用鸽巢原理优化 if(astr.size()>26) return false; int bitmap=0; for(auto i:

By Ne0inhk
动态规划 线性 DP 经典四题一遍吃透

动态规划 线性 DP 经典四题一遍吃透

文章目录 * 台阶问题 * 最大子段和 * 传球游戏 * 乌龟棋 线性dp 是动态规划问题中最基础、最常⻅的⼀类问题。它的特点是状态转移只依赖于前⼀个或前⼏个状态,状态之间的关系是线性的,通常可以⽤⼀维或者⼆维数组来存储状态。 我们在⼊⻔阶段解决的《下楼梯》以及《数字三⻆形》其实都是线性dp,⼀个是⼀维的,另⼀个是⼆ 维的。 台阶问题 题目描述 题目解析 本题就是上一节下楼梯的问题的加强版,总体思路不变,下面我们还是按照动规5板斧来分析一下这道题。 1、状态表示 dp[i]表示走到第i个台阶的所有方案数 2、状态转移方程 第i个台阶的方案数等于从i-1阶到i-k阶的所有方案数之和,因为本题数据比较大,用long long都无法保证数据不越界,所以题目规定方案数还需要模100003,第i个台阶的方案数等于从i-1阶到i-k阶的所有方案数之和再模上100003,所以但是注意是可能越界访问的,比如i为3,

By Ne0inhk
Flutter 三方库 sm_crypto 的鸿蒙化适配指南 - 实现国产密码算法 SM2/SM3/SM4 的端侧加解密、支持数字签名与国密 SSL 安全通信实战

Flutter 三方库 sm_crypto 的鸿蒙化适配指南 - 实现国产密码算法 SM2/SM3/SM4 的端侧加解密、支持数字签名与国密 SSL 安全通信实战

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 sm_crypto 的鸿蒙化适配指南 - 实现国产密码算法 SM2/SM3/SM4 的端侧加解密、支持数字签名与国密 SSL 安全通信实战 前言 在进行针对中国市场的 Flutter for OpenHarmony 企业级或政务级应用开发时,支持国产密码算法(国密)是硬性的合规要求。sm_crypto 是一个功能完备的国密算法 Dart 实现库。它涵盖了非对称加密 SM2、哈希摘要 SM3 以及对称加密 SM4。本文将探讨如何在鸿蒙端利用该库构建符合国家标准的安全加密体系。 一、原原理性解析 / 概念介绍 1.1 基础原理 sm_crypto 严格遵循国家密码管理局发布的 GM/

By Ne0inhk
优选算法——前缀和(5):和为 K 的子数组

优选算法——前缀和(5):和为 K 的子数组

🔥近津薪荼: [个人主页]🎬个人专栏: 《近津薪荼的算法日迹》《Linux操作系统及网络基础知识分享》《c++基础知识详解》《c语言基础知识详解》✨不要物化,矮化,弱化,钝化自己,保持锋芒,不要停止学习这个世界上只有两个人真正在注意着你八岁的你,和八十岁的你,他们此刻正在注视着你,一个希望你 勇敢开始,一个希望你 不留遗憾 1.上期参考代码 classSolution{public: vector<int>productExceptSelf(vector<int>& nums){int n=nums.size(); vector<int>front(n,1);for(int i=1;

By Ne0inhk