赞
踩
此篇文章接上篇,从上篇中导出的onnx模型的部署,此篇默认已经配置好opencv的环境了,包括如果需要使用cuda加速的环境。opencv的cuda环境需要用cmake编译contrib包(此处也是个大坑),如果cuda没有安装好,opencv会默认切换cpu推理。
2022.09.29更新 c++下面使用opencv部署yolov5和yolov7实例分割模型(六)_爱晚乏客游的博客-CSDN博客
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类。
- //yolo.h
-
- #pragma once
- #include<iostream>
- #include<math.h>
- #include<opencv2/opencv.hpp>
-
- class Yolo {
- public:
- Yolo() {
- }
- ~Yolo() {}
- };
对Yolo类加一点细节,设置一些必要的网络参数
- //参数为私有参数,当然也可以是设置成公开或者保护。
- private:
- //计算归一化函数
- float Sigmoid(float x) {
- return static_cast<float>(1.f / (1.f + exp(-x)));
- }
- //anchors
- const 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 } };
- //stride
- const 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的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;
- }
- //cuda
- if (isCuda) {
- net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
- net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);
- }
- //cpu
- else {
- net.setPreferableBackend(cv::dnn::DNN_BACKEND_DEFAULT);
- net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);
- }
- return true;
- }
推理过程就像煮饭,先准备材料(设置网络输入),然后放入锅中(送入网络中推理)等饭熟。当然煮饭还有一些细节,一起加上去,就不详细说了。
- //yolo.h
- //结果结构体
- struct Output {
- int id;//结果类别id
- float confidence;//结果置信度
- cv::Rect box;//矩形框
- };
- bool Detect(cv::Mat &SrcImg,cv::dnn::Net &net, std::vector<Output> &output);
-
- //yolo.cpp
-
- bool 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());
-
- ...
- ...
- }//这个括号是最末尾的,包括下面添加之后
这一步可以说是整个部署中最难的地方。看过上篇文章的话,就知道网络输出的是一个二维数组【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; //输出的网络宽度是类别数+5
- float* pdata = (float*)netOutputImg[0].data;
- for (int stride = 0; stride < 3; stride++) { //stride
- int grid_x = (int)(netWidth / netStride[stride]);
- int grid_y = (int)(netHeight / netStride[stride]);
- for (int anchor = 0; anchor < 3; anchor++) { //anchors
- const 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]; //x
- float y = (Sigmoid(pdata[1]) * 2.f - 0.5f + i) * netStride[stride]; //y
- float w = powf(Sigmoid(pdata[2]) * 2.f, 2.f) * anchor_w; //w
- float h = powf(Sigmoid(pdata[3]) * 2.f, 2.f) * anchor_h; //h
- int 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;//指针移到下一行
- }
- }
- }
- }
上面网络会有很多的输出框重叠在一起,需要使用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;
- else
- return false;
这里需要注意的是,opencv的nms是普通的nms网络,而yolov5中使用的是nms-giou,这里有点稍微不同,会导致检测结果出现一定的偏差。有强迫症的需要自己实现下nms的功能。也不会很难,可以将yolov5中计算iou的部分改写成c++的代码就可以实现nms-giou。
- //yolo.h
- void 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;
- }
运行看下结果
终于搞定了!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。