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

相关文章

使用Python删除Excel中的行列和单元格示例详解

《使用Python删除Excel中的行列和单元格示例详解》在处理Excel数据时,删除不需要的行、列或单元格是一项常见且必要的操作,本文将使用Python脚本实现对Excel表格的高效自动化处理,感兴... 目录开发环境准备使用 python 删除 Excphpel 表格中的行删除特定行删除空白行删除含指定

golang程序打包成脚本部署到Linux系统方式

《golang程序打包成脚本部署到Linux系统方式》Golang程序通过本地编译(设置GOOS为linux生成无后缀二进制文件),上传至Linux服务器后赋权执行,使用nohup命令实现后台运行,完... 目录本地编译golang程序上传Golang二进制文件到linux服务器总结本地编译Golang程序

深入理解Go语言中二维切片的使用

《深入理解Go语言中二维切片的使用》本文深入讲解了Go语言中二维切片的概念与应用,用于表示矩阵、表格等二维数据结构,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧... 目录引言二维切片的基本概念定义创建二维切片二维切片的操作访问元素修改元素遍历二维切片二维切片的动态调整追加行动态

prometheus如何使用pushgateway监控网路丢包

《prometheus如何使用pushgateway监控网路丢包》:本文主要介绍prometheus如何使用pushgateway监控网路丢包问题,具有很好的参考价值,希望对大家有所帮助,如有错误... 目录监控网路丢包脚本数据图表总结监控网路丢包脚本[root@gtcq-gt-monitor-prome

Python通用唯一标识符模块uuid使用案例详解

《Python通用唯一标识符模块uuid使用案例详解》Pythonuuid模块用于生成128位全局唯一标识符,支持UUID1-5版本,适用于分布式系统、数据库主键等场景,需注意隐私、碰撞概率及存储优... 目录简介核心功能1. UUID版本2. UUID属性3. 命名空间使用场景1. 生成唯一标识符2. 数

SpringBoot中如何使用Assert进行断言校验

《SpringBoot中如何使用Assert进行断言校验》Java提供了内置的assert机制,而Spring框架也提供了更强大的Assert工具类来帮助开发者进行参数校验和状态检查,下... 目录前言一、Java 原生assert简介1.1 使用方式1.2 示例代码1.3 优缺点分析二、Spring Fr

zookeeper端口说明及介绍

《zookeeper端口说明及介绍》:本文主要介绍zookeeper端口说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、zookeeper有三个端口(可以修改)aVNMqvZ二、3个端口的作用三、部署时注意总China编程结一、zookeeper有三个端口(可以

Android kotlin中 Channel 和 Flow 的区别和选择使用场景分析

《Androidkotlin中Channel和Flow的区别和选择使用场景分析》Kotlin协程中,Flow是冷数据流,按需触发,适合响应式数据处理;Channel是热数据流,持续发送,支持... 目录一、基本概念界定FlowChannel二、核心特性对比数据生产触发条件生产与消费的关系背压处理机制生命周期

java使用protobuf-maven-plugin的插件编译proto文件详解

《java使用protobuf-maven-plugin的插件编译proto文件详解》:本文主要介绍java使用protobuf-maven-plugin的插件编译proto文件,具有很好的参考价... 目录protobuf文件作为数据传输和存储的协议主要介绍在Java使用maven编译proto文件的插件

c++ 类成员变量默认初始值的实现

《c++类成员变量默认初始值的实现》本文主要介绍了c++类成员变量默认初始值,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录C++类成员变量初始化c++类的变量的初始化在C++中,如果使用类成员变量时未给定其初始值,那么它将被