2021.09.02更新说明 c++下使用opencv部署yolov5模型 (三)

2024-02-20 16:50

本文主要是介绍2021.09.02更新说明 c++下使用opencv部署yolov5模型 (三),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

此篇文章接上篇,从上篇中导出的onnx模型的部署,此篇默认已经配置好opencv的环境了,包括如果需要使用cuda加速的环境opencv的cuda环境需要用cmake编译contrib包(此处也是个大坑),如果cuda没有安装好,opencv会默认切换cpu推理。

 -----2022.10.10 更新了下yolov5-seg实例分割的部署:

2022.09.29更新 c++下面使用opencv部署yolov5和yolov7实例分割模型(六)_爱晚乏客游的博客-CSDN博客

 -----2022.07.25 更新了下yolov7的部署,有需要的自取

2022.07.25 C++下使用opencv部署yolov7模型(五)_爱晚乏客游的博客-CSDN博客

 ------2021.11.01更新说明

由于yolov5在6.0版本增加了对opencv的支持,所以模型部署1-3适用于4.0和5.0版本的修改,6.0版本的可以看这里:

2021.11.01 c++下 opencv部署yolov5-6.0版本 (四)_爱晚乏客游的博客-CSDN博客

建议本篇文章加上第四篇的修改来达成最优的部署。

修改了置信度算法,原本使用最大类别,现在使用最大类别置信度乘以box的置信度,结果与python下更为一致。

confidences.push_back(max_class_socre*box_score);

-----2021.09.02更新

        目前YOLO版本已经迭代到第五个版本了,我之前部署的时候用的是第4个版本。目前来说大致粗略的测试一下第五版本,还是一样的修改方法,但是在dnnet=readFromONNX(f)这里,第五版会报错,但是按照前面的修改方法,c++下面的模型是可以正常读取的,如果你对这个有强迫症,那么建议你使用第4个版本的yolov5,这个版本可以在Releases · ultralytics/yolov5 · GitHub这里面找到并下载。

        而对于yolov5的P6模型,则需要根据使用的P6模型修改成对应的anchors数据,具体的数据可以在data/hub/下面找到对应模型的yaml里面找到(现在最新版变成了models/hub)。stride需要在后面添加上64步长的数据,然后将代码for里面的stride变成小于4(之前的模型是3),就可以通用其余剩下的代码。

        对于长宽比过大的图片,由于opencv的blobFromImage()函数在缩放的时候不是无损缩放,会导致图像变形严重导致结果错误或者漏检。虽然blobFromImage里面有个参数可以保持比例缩放,但是只会保留中间的部分,两边信息全部丢掉,所以如果你的目标全部在中间就可以无所谓,如果不是,那么需要简单的自己做个无损缩放,制作一张全黑的3通道正方形图片,边长为原图的长边,最后将原图放在(0,0)的位置上面,这样就可以直接输入blobFromImage里面就可以实现无损缩放了,而且不用对检测结果进行二次修正位置了。

-----2021.5.12更新:
需要注意的是:
我在写这篇文章的时候,opencv的版本是4.5.0+contrib,如果使用其他版本,不能保证不会出问题。
目前测试了3.4.x版本,需要使用3.4.13及其以上的版本,低于此版本会出现报错,报错信息指dnn模块支持CV_32S,而不支持模型中的CV_32F

-----2021.05.06 原始更新:

目录

零、新建一个头文件yolo.h和yolo.cpp,在头文件中定义一个Yolo类。

一、设置yolov5网络的一些参数。

二、opencv加载onnx模型。

三、获取网络输出结果

1、设置网络输入

2.遍历网络输出获取结果。。

3.非极大值抑制(NMS)

四、对结果进行画框显示输出

五、写个简单的调用代码测试下结果。


零、新建一个头文件yolo.h和yolo.cpp,在头文件中定义一个Yolo类。

//yolo.h#pragma once
#include<iostream>
#include<math.h>
#include<opencv2/opencv.hpp>class Yolo {
public:Yolo() {}~Yolo() {}
};

一、设置yolov5网络的一些参数。

对Yolo类加一点细节,设置一些必要的网络参数

//参数为私有参数,当然也可以是设置成公开或者保护。
private://计算归一化函数float Sigmoid(float x) {return static_cast<float>(1.f / (1.f + exp(-x)));}//anchorsconst float netAnchors[3][6] = { { 10.0, 13.0, 16.0, 30.0, 33.0, 23.0 },{ 30.0, 61.0, 62.0, 45.0, 59.0, 119.0 },{ 116.0, 90.0, 156.0, 198.0, 373.0, 326.0 } };//strideconst float netStride[3] = { 8.0, 16.0, 32.0 };const int netWidth = 640; //网络模型输入大小const int netHeight = 640;float nmsThreshold = 0.45; float boxThreshold = 0.35;float classThreshold = 0.35;//类名std::vector<std::string> className = { "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light","fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow","elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee","skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard","tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple","sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch","potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", "cell phone","microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear","hair drier", "toothbrush" };

二、opencv加载onnx模型。

opencv的dnn模块提供了读取神经网络模型的函数接口readNetFromONNX(),该函数很简单。所以就可以用一句简单的语句就可以得到模型了。net = readNetFromONNX(netPath).为了整个工程,加一点点小细节,并定义一个标注位设置推理引擎用cpu或者gpu。

//在yolo.h中的 Yolo类中添加成员函数readModel:
bool readModel(cv::dnn::Net &net, std::string &netPath,bool isCuda)//yolo.cpp中实现readModel函数
//在yolo.cpp中使用命名空间
#include "yolo.h"
using namespace std;
using namespace cv;
using namespace dnn;bool Yolo::readModel(Net &net, string &netPath,bool isCuda = false) {try {net = readNetFromONNX(netPath);}catch (const std::exception&) {return false;}//cudaif (isCuda) {net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);}//cpuelse {net.setPreferableBackend(cv::dnn::DNN_BACKEND_DEFAULT);net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);}return true;
}

三、获取网络输出结果

1、设置网络输入

推理过程就像煮饭,先准备材料(设置网络输入),然后放入锅中(送入网络中推理)等饭熟。当然煮饭还有一些细节,一起加上去,就不详细说了。

//yolo.h
//结果结构体
struct Output {int id;//结果类别idfloat confidence;//结果置信度cv::Rect box;//矩形框
};
bool Detect(cv::Mat &SrcImg,cv::dnn::Net &net, std::vector<Output> &output);//yolo.cppbool Yolo::Detect(Mat &SrcImg,Net &net,vector<Output> &output) {
Mat blob;int col = SrcImg.cols;int row = SrcImg.rows;int maxLen = MAX(col, row);Mat netInputImg = SrcImg.clone();if (maxLen > 1.2*col || maxLen > 1.2*row) {Mat resizeImg = Mat::zeros(maxLen, maxLen, CV_8UC3);SrcImg.copyTo(resizeImg(Rect(0, 0, col, row)));netInputImg = resizeImg;}blobFromImage(netInputImg, blob, 1 / 255.0, cv::Size(netWidth, netHeight), cv::Scalar(104, 117,123), true, false);//blobFromImage(netInputImg, blob, 1 / 255.0, cv::Size(netWidth, netHeight), cv::Scalar(0, 0,0), true, false);//如果训练集未对图片进行减去均值操作,则需要设置为这句//blobFromImage(netInputImg, blob, 1 / 255.0, cv::Size(netWidth, netHeight), cv::Scalar(114, 114,114), true, false);net.setInput(blob);std::vector<cv::Mat> netOutputImg;//vector<string> outputLayerName{"345","403", "461","output" };//net.forward(netOutputImg, outputLayerName[3]); //获取output的输出net.forward(netOutputImg, net.getUnconnectedOutLayersNames());......
}//这个括号是最末尾的,包括下面添加之后

2.遍历网络输出获取结果。。

这一步可以说是整个部署中最难的地方。看过上篇文章的话,就知道网络输出的是一个二维数组【25200*85】。这一步需要做的就是遍历每一行的长度为85的一维数组,并且获取符合条件的结果,先上代码,有些地方有注释,可以看看。

//接上面std::vector<int> classIds;//结果id数组std::vector<float> confidences;//结果每个id对应置信度数组std::vector<cv::Rect> boxes;//每个id矩形框float ratio_h = (float)netInputImg.rows / netHeight;float ratio_w = (float)netInputImg.cols / netWidth;int net_width = className.size() + 5;  //输出的网络宽度是类别数+5float* pdata = (float*)netOutputImg[0].data;for (int stride = 0; stride < 3; stride++) {    //strideint grid_x = (int)(netWidth / netStride[stride]);int grid_y = (int)(netHeight / netStride[stride]);for (int anchor = 0; anchor < 3; anchor++) { //anchorsconst float anchor_w = netAnchors[stride][anchor * 2];const float anchor_h = netAnchors[stride][anchor * 2 + 1];for (int i = 0; i < grid_y; i++) {for (int j = 0; j < grid_y; j++) {float box_score = Sigmoid(pdata[4]);//获取每一行的box框中含有某个物体的概率if (box_score > boxThreshold) {//为了使用minMaxLoc(),将85长度数组变成Mat对象cv::Mat scores(1,className.size(), CV_32FC1, pdata+5);Point classIdPoint;double max_class_socre;minMaxLoc(scores, 0, &max_class_socre, 0, &classIdPoint);max_class_socre = Sigmoid((float)max_class_socre);if (max_class_socre > classThreshold) {//rect [x,y,w,h]float x = (Sigmoid(pdata[0]) * 2.f - 0.5f + j) * netStride[stride];  //xfloat y = (Sigmoid(pdata[1]) * 2.f - 0.5f + i) * netStride[stride];   //yfloat w = powf(Sigmoid(pdata[2]) * 2.f, 2.f) * anchor_w;   //wfloat h = powf(Sigmoid(pdata[3]) * 2.f, 2.f) * anchor_h;  //hint left = (x - 0.5*w)*ratio_w;int top = (y - 0.5*h)*ratio_h;classIds.push_back(classIdPoint.x);confidences.push_back(max_class_socre*box_score);boxes.push_back(Rect(left, top, int(w*ratio_w), int(h*ratio_h)));}}pdata += net_width;//指针移到下一行}}}}

3.非极大值抑制(NMS)

上面网络会有很多的输出框重叠在一起,需要使用nms进行过滤重叠框。

//接上面执行非最大抑制以消除具有较低置信度的冗余重叠框(NMS)vector<int> nms_result;NMSBoxes(boxes, confidences, classThreshold, nmsThreshold, nms_result);for (int i = 0; i < nms_result.size(); i++) {int idx = nms_result[i];Output result;result.id = classIds[idx];result.confidence = confidences[idx];result.box = boxes[idx];output.push_back(result);}if (output.size())return true;elsereturn false;
未经过NMS(左图)和经过NMS结果对比(右图)

这里需要注意的是,opencv的nms是普通的nms网络,而yolov5中使用的是nms-giou,这里有点稍微不同,会导致检测结果出现一定的偏差。有强迫症的需要自己实现下nms的功能。也不会很难,可以将yolov5中计算iou的部分改写成c++的代码就可以实现nms-giou。

四、对结果进行画框显示输出

//yolo.hvoid drawPred(cv::Mat &img, std::vector<Output> result, std::vector<cv::Scalar> color);//yolo.cpp
//这里的color是颜色数组,对没一个id随机分配一种颜色
void Yolo::drawPred(Mat &img, vector<Output> result, vector<Scalar> color) {for (int i = 0; i < result.size(); i++) {int left, top;left = result[i].box.x;top = result[i].box.y;int color_num = i;rectangle(img, result[i].box, color[result[i].id], 2, 8);string label = className[result[i].id] +":" + to_string(result[i].confidence);int baseLine;Size labelSize = getTextSize(label, FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);top = max(top, labelSize.height);//rectangle(frame, Point(left, top - int(1.5 * labelSize.height)), Point(left + int(1.5 * labelSize.width), top + baseLine), Scalar(0, 255, 0), FILLED);putText(img, label, Point(left, top), FONT_HERSHEY_SIMPLEX, 1, color[result[i].id], 2);}imshow("res", img);//imwrite("./result.jpg", img);waitKey();//destroyAllWindows();
}

五、写个简单的调用代码测试下结果。

高端的食材往往只需要简单的烹饪方式,忙活了一天的林师傅,开始准备写调用代码的正餐了,只见他先焚香沐浴了半小时,终于成功的配置好了opencv的环境,然后的处理好的yolo.h头文件和yolo.cpp文件include进代码中,并且郑重的写下了第一行代码:

#include "yolo.h"
#include <iostream>
#include<opencv2//opencv.hpp>
#include<math.h>using namespace std;
using namespace cv;
using namespace dnn;int main()
{cout << "Hello World" << endl;return 0;
}

 林师傅尝了一口感觉不得劲,又加了点细节,

int main()
{cout << "Hello World" << endl;string img_path = "./test.jpg";string model_path = "./yolov5s.onnx";Yolo test;Net net;if (test.readModel(net, model_path, true)) {cout << "read net ok!" << endl;}else {return -1;}//生成随机颜色vector<Scalar> color;srand(time(0));for (int i = 0; i < 80; i++) {int b = rand() % 256;int g = rand() % 256;int r = rand() % 256;color.push_back(Scalar(b, g, r));}vector<Output> result;Mat img = imread(img_path);if (test.Detect(img, net, result)) {test.drawPred(img, result, color);}else {cout << "Detect Failed!"<<endl;}system("pause");return 0;
}

运行看下结果

エロ卡门

终于搞定了!

这篇关于2021.09.02更新说明 c++下使用opencv部署yolov5模型 (三)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/728884

相关文章

Git可视化管理工具(SourceTree)使用操作大全经典

《Git可视化管理工具(SourceTree)使用操作大全经典》本文详细介绍了SourceTree作为Git可视化管理工具的常用操作,包括连接远程仓库、添加SSH密钥、克隆仓库、设置默认项目目录、代码... 目录前言:连接Gitee or github,获取代码:在SourceTree中添加SSH密钥:Cl

Python中模块graphviz使用入门

《Python中模块graphviz使用入门》graphviz是一个用于创建和操作图形的Python库,本文主要介绍了Python中模块graphviz使用入门,具有一定的参考价值,感兴趣的可以了解一... 目录1.安装2. 基本用法2.1 输出图像格式2.2 图像style设置2.3 属性2.4 子图和聚

windows和Linux使用命令行计算文件的MD5值

《windows和Linux使用命令行计算文件的MD5值》在Windows和Linux系统中,您可以使用命令行(终端或命令提示符)来计算文件的MD5值,文章介绍了在Windows和Linux/macO... 目录在Windows上:在linux或MACOS上:总结在Windows上:可以使用certuti

CentOS和Ubuntu系统使用shell脚本创建用户和设置密码

《CentOS和Ubuntu系统使用shell脚本创建用户和设置密码》在Linux系统中,你可以使用useradd命令来创建新用户,使用echo和chpasswd命令来设置密码,本文写了一个shell... 在linux系统中,你可以使用useradd命令来创建新用户,使用echo和chpasswd命令来设

Python使用Matplotlib绘制3D曲面图详解

《Python使用Matplotlib绘制3D曲面图详解》:本文主要介绍Python使用Matplotlib绘制3D曲面图,在Python中,使用Matplotlib库绘制3D曲面图可以通过mpl... 目录准备工作绘制简单的 3D 曲面图绘制 3D 曲面图添加线框和透明度控制图形视角Matplotlib

Pandas利用主表更新子表指定列小技巧

《Pandas利用主表更新子表指定列小技巧》本文主要介绍了Pandas利用主表更新子表指定列小技巧,通过创建主表和子表的DataFrame对象,并使用映射字典进行数据关联和更新,实现了从主表到子表的同... 目录一、前言二、基本案例1. 创建主表数据2. 创建映射字典3. 创建子表数据4. 更新子表的 zb

Pandas中统计汇总可视化函数plot()的使用

《Pandas中统计汇总可视化函数plot()的使用》Pandas提供了许多强大的数据处理和分析功能,其中plot()函数就是其可视化功能的一个重要组成部分,本文主要介绍了Pandas中统计汇总可视化... 目录一、plot()函数简介二、plot()函数的基本用法三、plot()函数的参数详解四、使用pl

使用Python实现IP地址和端口状态检测与监控

《使用Python实现IP地址和端口状态检测与监控》在网络运维和服务器管理中,IP地址和端口的可用性监控是保障业务连续性的基础需求,本文将带你用Python从零打造一个高可用IP监控系统,感兴趣的小伙... 目录概述:为什么需要IP监控系统使用步骤说明1. 环境准备2. 系统部署3. 核心功能配置系统效果展

使用Java将各种数据写入Excel表格的操作示例

《使用Java将各种数据写入Excel表格的操作示例》在数据处理与管理领域,Excel凭借其强大的功能和广泛的应用,成为了数据存储与展示的重要工具,在Java开发过程中,常常需要将不同类型的数据,本文... 目录前言安装免费Java库1. 写入文本、或数值到 Excel单元格2. 写入数组到 Excel表格

redis中使用lua脚本的原理与基本使用详解

《redis中使用lua脚本的原理与基本使用详解》在Redis中使用Lua脚本可以实现原子性操作、减少网络开销以及提高执行效率,下面小编就来和大家详细介绍一下在redis中使用lua脚本的原理... 目录Redis 执行 Lua 脚本的原理基本使用方法使用EVAL命令执行 Lua 脚本使用EVALSHA命令