ONNXRuntime是微软推出的一款推理框架,用户可以非常便利的用其运行一个onnx模型。ONNXRuntime支持多种运行后端包括CPU,GPU,TensorRT,DML等。可以说ONNXRuntime是对ONNX模型最原生的支持。
当我们需要在windows的CPU上运行我们的YOLO模型的时候。我们可以采用ONNXRuntime进行部署推理。
下载ORT
先进入到ORT官网https://onnxruntime.ai/
由于我们目的是采用的是C++进行推理,按照如下方式进行下载
选择合适的选项后进行动态库和静态库的下载。
提供多种方式选其一即可
下载完成后进行解压缩
这个目录下存放着所有架构的动态库和静态库
这里我们选择win--x64
lib路径:\you\path\microsoft.ml.onnxruntime.1.20.1\runtimes\win-x64\native
include存放在build里面
Include路径:\you\path\microsoft.ml.onnxruntime.1.20.1\build\native\include
下载Yolov5模型
Ultralytics YOLOv5 是一款尖端的、最先进的 (SOTA) 模型,它建立在以前 YOLO 版本的成功基础之上,并引入了新的功能和改进,以进一步提高性能和灵活性。YOLOv5 的设计目标是快速、准确且易于使用,使其成为各种目标检测、实例分割和图像分类任务的绝佳选择。
先拉取整个yolov5工程
git clone https://github.com/ultralytics/yolov5.git
对整个工程环境进行配置
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
对于yolov5使用细节这里不进行展开
直接运行
python export.py --include onnx
将默认自动下载yolov5s.pt后转为yolov5s.onnx
然后将我们的onnx模型文件拿出来放置在工程目录中
下载Opencv
OpenCV(Open Source Computer Vision Library)是一个开源的计算机视觉和机器学习软件库。
OpenCV 由一系列 C 函数和少量 C++ 类构成,同时提供了 Python、Java、MATLAB 等语言的接口。OpenCV 提供了大量的计算机视觉算法和图像处理工具,广泛应用于图像和视频的处理、分析以及机器学习领域。OpenCV 的设计目标是提供一套简单易用的计算机视觉基础库,帮助开发人员快速构建复杂的视觉应用。用opencv来读取图片是个不错的选择
下载链接:https://opencv.org/releases/
下载方法不限制
工程配置测试
打开你所拥有的Visual Studio2019建立工程
名称自定义,我是叫yolov5_ort_demo
右击工程名称
配置对应的头文件路径和库的路径也就是opencv和onnxruntime对应的那些头文件和库
将几个动态库复制到可执行文件存在的目录
接着尝试写入测试代码,看看是否接入成功
#include <iostream>
#include <onnxruntime_cxx_api.h>
int main(int argc, char* argv[]) {
Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "test");
return 0;
}
点击运行
能成功运行就说明配置成功了
基于ORT构造Yolov5业务逻辑
- 先配置一个会话(Session)来加载和运行我们的yolov5s.onnx模型
#include "onnxruntime_cxx_api.h"
#include <iostream>
int main() {
Ort::Env env;
Ort::SessionOptions session_options;
std::wstring model_path = L"D:\\you\\path\\yolov5s.onnx";//这里选择你自己的模型路径
session_options.SetIntraOpNumThreads(4);
session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);
Ort::Session session(env, model_path.c_str(), session_options);
return 0;
}
Ort::SessionOptions session_options;
这行代码创建了一个 SessionOptions 对象,这个对象用于配置会话的各种选项。
session_options.SetIntraOpNumThreads(4);
这行代码设置了会话在执行单个操作时使用的线程数。这里设置为4,意味着在执行单个操作时,ORT 将尝试使用4个线程来并行化工作,以加快执行速度。
session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); 这行代码设置了图优化级别。ORT_ENABLE_ALL 表示启用所有可用的图优化,内部是个枚举
Ort::Session session(env, model_path.c_str(), session_options);这行代码创建一个会话Session对象。这是用于执行模型推理的主要接口。
分别都有提供这几种选型
ORT_DISABLE_ALL = 0:禁用所有图优化。
ORT_ENABLE_BASIC = 1:启用基本图优化。这通常包括一些简单的优化,如常量折叠。
ORT_ENABLE_EXTENDED = 2:启用扩展图优化。这包括比基本优化更复杂的优化,可能涉及算子融合之类的。
ORT_ENABLE_ALL = 99:启用所有可用的图优化。可以最大限度地提高模型的执行效率。
- 加载图片
这里采用的是opencv来读取图片
#include "onnxruntime_cxx_api.h"
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
Ort::Env env;
Ort::SessionOptions session_options;
std::wstring model_path = L"D:\\you\\path\\yolov5s.onnx";
session_options.SetIntraOpNumThreads(4);
session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);
cv::Mat frame = cv::imread("D:\\you\\path\\bus.jpg");
return 0;
}
cv::imread 函数用于从指定文件路径读取图像,并将其存储在一个 cv::Mat 对象中。cv::Mat 是 OpenCV 中用于存储图像、矩阵等数据的核心类。
- 获取标签数组
对于yolov5官方提供的基座模型其拥有80个类的标签,将80个类的标记数据按行放入一个文本文件中(路径没有特定要求,自己找得到即可)
std::vector<std::string> readClassNames(std::string labels_txt_file);
std::vector<std::string> readClassNames(std::string labels_txt_file = "./classes.txt"){
std::vector<std::string> classNames;
std::ifstream fp(labels_txt_file);
if (!fp.is_open()){
printf("could not open file...\n");
exit(-1);
}
std::string name;
while (!fp.eof()){
std::getline(fp, name);
if (name.length())
classNames.push_back(name);
}
fp.close();
return classNames;
}
实现一个readClassNames函数用来获取classes.txt中的标签数据并存放在一个vector中
int main() {
Ort::Env env;
Ort::SessionOptions session_options;
std::wstring model_path = L"D:\\you\\path\\yolov5s.onnx";
session_options.SetIntraOpNumThreads(4);
session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);
cv::Mat frame = cv::imread("D:\\you\\path\\bus.jpg");
std::vector<std::string> labels = readClassNames("D:\\you\\path\\classes.txt");
return 0;
}
- 预处理
要做这个步骤,首先就要学会看onnx的拓扑图。
推荐使用这个网站,将你的onnx中间件模型直接拖入即可看到模型的拓扑图。
- 输入结点:
红色框内对应着模型的输入结点,是一个四维张量,可以理解成是一个1*3*640*640的数组
第一个维度(1)代表批次大小(batch size),即一次处理多少张图片。
第二个维度(3)代表颜色通道数,对于彩色RGB图像来说是3。
第三个和第四个维度(640*640)代表图像的高度和宽度,即图像的分辨率
蓝色框内对应着模型的输出结点,是一个三维张量,同样可以理解成是一个1*25200*85的数组
- 输出结点
第一个维度(1)代表批次大小,意味着模型一次只处理并输出一张图片的结果。
第二个维度(25200)代表这个数字表示模型在每个输出位置上预测的检测框数量,25200是由三个颜色通道的特征图上的检测框数量相加得到的。这些特征图是通过下采样得到的,大小分别为80x80、40x40和20x20,每个特征图上的每个位置都会预测三个不同大小和比例的锚框。25200 = (80x80 + 40x40 + 20x20) * 3
第三个维度(85)代表每个检测框的输出维度。前5个元素分别是检测框的中心点坐标(x, y)、宽度(w)和高度(h),以及一个对象置信度,表示检测框中是否包含对象的概率。
剩下的80个元素是类别得分,每个元素对应一个类别的得分,表示检测框中的对象属于该类别的概率,比如第6个元素是0.86,那就代表检测框中属于第一个标签的概率为0.86。
有了这些先前知识之后,你就应该明白要跑通我们的模型检测,我们就需要把我们输入的图像数据resize(也可以等比缩小后进行补全)成一个640*640的分辨率再送进模型,最后会得到一个三维张量,然后把这个三位张量进行解析,转换最后映射到源图像中。
换而言之呢,整个模型的推理过程我们可以理解为三个步骤。
预处理、模型推理、后处理。
预处理就是把我们的图像内的数据转换为可以输入进模型的图像格式。
模型推理这个交给我们的推理框架,输出结点的数据。
后处理就是把结点输出的数据映射回源图像的过程。
所以这里我们先要去实现预处理的操作,从session中获取输入节点的信息,根据这些信息调整输入图像的大小,其转换为ORT推理所需的格式,再去输入进ORT::Value
std::vector<std::string> input_node_names;
size_t numInputNodes = session.GetInputCount();
Ort::AllocatorWithDefaultOptions allocator;
int input_w = 0;
int input_h = 0;
int batch_size = 0;
int channel = 0;
int w = frame.cols;
int h = frame.rows;
int _max = std::max(h, w);
for (int i = 0; i < numInputNodes; i++) {
auto input_name = session.GetInputNameAllocated(i, allocator);
input_node_names.push_back(input_name.get());
Ort::TypeInfo input_type_info = session.GetInputTypeInfo(i);
auto input_tensor_info = input_type_info.GetTensorTypeAndShapeInfo();
auto input_dims = input_tensor_info.GetShape();
input_w = input_dims[3];
input_h = input_dims[2];
batch_size = input_dims[1];
channel = input_dims[0];
std::cout << "input format: NxCxHxW = " << input_dims[0] << "x" << input_dims[1] << "x" << input_dims[2] << "x" << input_dims[3] << std::endl;
}
cv::Mat image = cv::Mat::zeros(cv::Size(_max, _max), CV_8UC3);
cv::Rect roi(0, 0, w, h);
frame.copyTo(image(roi));
cv::Mat blob = cv::dnn::blobFromImage(image, 1 / 255.0, cv::Size(input_w, input_h), cv::Scalar(0, 0, 0), true, false);
size_t tpixels = input_h * input_w * channel;
std::array<int64_t, 4> input_shape_info{ batch_size, channel, input_h, input_w };
auto allocator_info = Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU);
Ort::Value input_tensor_ = Ort::Value::CreateTensor<float>(allocator_info, blob.ptr<float>(), tpixels, input_shape_info.data(), input_shape_info.size());
size_t numInputNodes = session.GetInputCount(); 用于从session中获取输入节点数据。
Ort::AllocatorWithDefaultOptions allocator; 用于分配内存。
cv::Mat image = cv::Mat::zeros(cv::Size(_max, _max), CV_8UC3);
cv::Rect roi(0, 0, w, h);
frame.copyTo(image(roi));
这三段代码创建了一个大小为_max, x _max的空白图像image,使用cv::Rect roi(0, 0, w, h);定义一个矩形区域roi,其大小为原始图像frame的大小,后将frame复制到image的roi区域中,实际上这一步如果_max等于w或h,则相当于没有改变frame的大小,如果_max大于w或h,则会在图像周围补全黑色像素点。
cv::dnn::blobFromImage是将函数将图像image转换为onnx模型的BGR格式。
std::array<int64_t, 4> input_shape_info{ batch_size, channel, input_h, input_w }; 设置一个包含输入形状信息的数组,格式为NxCxHxW。
由于session.RUN(推理模型的接口)还需要用到输出结点,我们同理对输出结点进行获取这里就不多做解释
size_t numOutputNodes = session.GetOutputCount();
std::vector<std::string> output_node_names;
int output_h = 0;
int output_w = 0;
for (int i = 0; i < numOutputNodes; i++) {
Ort::TypeInfo output_type_info = session.GetOutputTypeInfo(i);
auto output_tensor_info = output_type_info.GetTensorTypeAndShapeInfo();
auto output_dims = output_tensor_info.GetShape();
output_h = output_dims[1];
output_w = output_dims[2];
auto out_name = session.GetOutputNameAllocated(i, allocator);
output_node_names.push_back(out_name.get());
std::cout << "output format : HxW = " << output_dims[1] << "x" << output_dims[2] << std::endl;
}
- 模型推理
const std::array<const char*, 1> inputNames = { input_node_names[0].c_str() };
const std::array<const char*, 1> outNames = { output_node_names[0].c_str() };
std::vector<Ort::Value> ort_outputs;
try {
ort_outputs = session.Run(Ort::RunOptions{ nullptr }, inputNames.data(), &input_tensor_, numInputNodes, outNames.data(), outNames.size());
}
catch (std::exception e) {
std::cout << e.what() << std::endl;
}
Ort::RunOptions{ nullptr } 创建了一个默认的运行选项对象。
inputNames.data() 和 outNames.data() 提供了输入和输出节点名称的指针。
numInputNodes是选用之前输入张量的数量
- 后处理+数据映射回源文件
float x_factor = image.cols / static_cast<float>(input_w);
float y_factor = image.rows / static_cast<float>(input_h);
const float* pdata = ort_outputs[0].GetTensorMutableData<float>();
cv::Mat dout(output_h, output_w, CV_32F, (float*)pdata);
std::vector<cv::Rect> boxes;
std::vector<int> classIds;
std::vector<float> confidences;
int nLabelSize = labels.size();
for (int i = 0; i < dout.rows; i++) {
cv::Mat classes_scores = dout.row(i).colRange(5, nLabelSize + 5);
cv::Point classIdPoint;
double score;
minMaxLoc(classes_scores, 0, &score, 0, &classIdPoint);
float conf = dout.at<float>(i, 4);
//预测框置信度
if (conf < 0.45){
continue;
}
//类别置信度判断
if (score > 0.70){
float cx = dout.at<float>(i, 0);
float cy = dout.at<float>(i, 1);
float ow = dout.at<float>(i, 2);
float oh = dout.at<float>(i, 3);
int x = static_cast<int>((cx - 0.5 * ow) * x_factor);
int y = static_cast<int>((cy - 0.5 * oh) * y_factor);
int width = static_cast<int>(ow * x_factor);
int height = static_cast<int>(oh * y_factor);
cv::Rect box;
box.x = x;
box.y = y;
box.width = width;
box.height = height;
boxes.push_back(box);
classIds.push_back(classIdPoint.x);
confidences.push_back(score);
}
}
std::vector<int> indexes;
cv::dnn::NMSBoxes(boxes, confidences, 0.70, 0.45, indexes);
for (size_t i = 0; i < indexes.size(); i++) {
int index = indexes[i];
int idx = classIds[index];
cv::rectangle(frame, boxes[index], cv::Scalar(0, 255, 0), 2, 8);
putText(frame, labels[idx] + std::to_string(confidences[index]),
cv::Point(boxes[index].tl().x, boxes[index].tl().y), cv::FONT_HERSHEY_PLAIN, 2.0, cv::Scalar(0, 0, 255), 2, 8);
}
cv::imshow("YOLOv5+ONNXRUNTIME", frame);
cv::waitKey(0);
通过这两句代码获取了指向浮点数据的指针,并使用它创建了一个opencv的Mat(dout)。尺寸与 output_h 和 output_w 相匹配。
std::vector<int> indexes;
cv::dnn::NMSBoxes(boxes, confidences, 0.70, 0.45, indexes);
这两句话效果是利用opencv的 NMSBoxes 函数来执行非极大值抑制,以减少重叠的边界框数量。这里,0.70 是NMS的阈值,0.45 是IoU(交并比)阈值。
检测结果如下:
整个工程源码如下:
#include "onnxruntime_cxx_api.h"
#include <opencv2/opencv.hpp>
#include <iostream>
std::vector<std::string> readClassNames(std::string labels_txt_file);
std::vector<std::string> readClassNames(std::string labels_txt_file = "./classes.txt")
{
std::vector<std::string> classNames;
std::ifstream fp(labels_txt_file);
if (!fp.is_open())
{
printf("could not open file...\n");
exit(-1);
}
std::string name;
while (!fp.eof())
{
std::getline(fp, name);
if (name.length())
classNames.push_back(name);
}
fp.close();
return classNames;
}
int main() {
Ort::Env env;
Ort::SessionOptions session_options;
std::wstring model_path = L"\\you\\path\\yolov5s.onnx";
session_options.SetIntraOpNumThreads(4);
session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);
Ort::Session session(env, model_path.c_str(), session_options);
cv::Mat frame = cv::imread("\\you\\path\\bus.jpg");
std::vector<std::string> labels = readClassNames("\\you\\path\\classes.txt");
std::vector<std::string> input_node_names;
size_t numInputNodes = session.GetInputCount();
Ort::AllocatorWithDefaultOptions allocator;
int input_w = 0;
int input_h = 0;
int batch_size = 0;
int channel = 0;
int w = frame.cols;
int h = frame.rows;
int _max = std::max(h, w);
for (int i = 0; i < numInputNodes; i++) {
auto input_name = session.GetInputNameAllocated(i, allocator);
input_node_names.push_back(input_name.get());
Ort::TypeInfo input_type_info = session.GetInputTypeInfo(i);
auto input_tensor_info = input_type_info.GetTensorTypeAndShapeInfo();
auto input_dims = input_tensor_info.GetShape();
input_w = input_dims[3];
input_h = input_dims[2];
channel = input_dims[1];
batch_size = input_dims[0];
std::cout << "input format: NxCxHxW = " << input_dims[0] << "x" << input_dims[1] << "x" << input_dims[2] << "x" << input_dims[3] << std::endl;
}
cv::Mat image = cv::Mat::zeros(cv::Size(_max, _max), CV_8UC3);
cv::Rect roi(0, 0, w, h);
frame.copyTo(image(roi));
cv::Mat blob = cv::dnn::blobFromImage(image, 1 / 255.0, cv::Size(input_w, input_h), cv::Scalar(0, 0, 0), true, false);
size_t tpixels = input_h * input_w * channel;
std::array<int64_t, 4> input_shape_info{ batch_size, channel, input_h, input_w };
auto allocator_info = Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU);
Ort::Value input_tensor_ = Ort::Value::CreateTensor<float>(allocator_info, blob.ptr<float>(), tpixels, input_shape_info.data(), input_shape_info.size());
size_t numOutputNodes = session.GetOutputCount();
std::vector<std::string> output_node_names;
int output_h = 0;
int output_w = 0;
for (int i = 0; i < numOutputNodes; i++) {
Ort::TypeInfo output_type_info = session.GetOutputTypeInfo(i);
auto output_tensor_info = output_type_info.GetTensorTypeAndShapeInfo();
auto output_dims = output_tensor_info.GetShape();
output_h = output_dims[1];
output_w = output_dims[2];
auto out_name = session.GetOutputNameAllocated(i, allocator);
output_node_names.push_back(out_name.get());
std::cout << "output format : HxW = " << output_dims[1] << "x" << output_dims[2] << std::endl;
}
const std::array<const char*, 1> inputNames = { input_node_names[0].c_str() };
const std::array<const char*, 1> outNames = { output_node_names[0].c_str() };
std::vector<Ort::Value> ort_outputs;
try {
ort_outputs = session.Run(Ort::RunOptions{ nullptr }, inputNames.data(), &input_tensor_, numInputNodes, outNames.data(), outNames.size());
}
catch (std::exception e) {
std::cout << e.what() << std::endl;
}
float x_factor = image.cols / static_cast<float>(input_w);
float y_factor = image.rows / static_cast<float>(input_h);
const float* pdata = ort_outputs[0].GetTensorMutableData<float>();
cv::Mat dout(output_h, output_w, CV_32F, (float*)pdata);
std::vector<cv::Rect> boxes;
std::vector<int> classIds;
std::vector<float> confidences;
int nLabelSize = labels.size();
for (int i = 0; i < dout.rows; i++) {
cv::Mat classes_scores = dout.row(i).colRange(5, nLabelSize + 5);
cv::Point classIdPoint;
double score;
minMaxLoc(classes_scores, 0, &score, 0, &classIdPoint);
float conf = dout.at<float>(i, 4);
//预测框置信度
if (conf < 0.45){
continue;
}
//类别置信度判断
if (score > 0.70){
float cx = dout.at<float>(i, 0);
float cy = dout.at<float>(i, 1);
float ow = dout.at<float>(i, 2);
float oh = dout.at<float>(i, 3);
int x = static_cast<int>((cx - 0.5 * ow) * x_factor);
int y = static_cast<int>((cy - 0.5 * oh) * y_factor);
int width = static_cast<int>(ow * x_factor);
int height = static_cast<int>(oh * y_factor);
cv::Rect box;
box.x = x;
box.y = y;
box.width = width;
box.height = height;
boxes.push_back(box);
classIds.push_back(classIdPoint.x);
confidences.push_back(score);
}
}
std::vector<int> indexes;
cv::dnn::NMSBoxes(boxes, confidences, 0.70, 0.45, indexes);
for (size_t i = 0; i < indexes.size(); i++) {
int index = indexes[i];
int idx = classIds[index];
cv::rectangle(frame, boxes[index], cv::Scalar(0, 255, 0), 2, 8);
putText(frame, labels[idx] + std::to_string(confidences[index]), cv::Point(boxes[index].tl().x, boxes[index].tl().y), cv::FONT_HERSHEY_PLAIN, 2.0, cv::Scalar(0, 0, 255), 2, 8);
}
cv::imshow("YOLOv5+ONNXRUNTIME", frame);
cv::waitKey(0);
session_options.release();
session.release();
return 0;
}