当前位置:   article > 正文

【模型部署】使用opencv C++ 加速YOLO V5_c++下使用opencv部署yolov5模型

c++下使用opencv部署yolov5模型

在ultralytics/YOLO V5中官方给出了利用opencv c++ cuda 进行YOLO V5加速的实例代码,但是代码中并没有给出相关注释,今天花了些时间,把示例源码仔细看了看,并把每一部分都进行了详细注释。内容在下方,欢迎大家交流学习。

官网示例源码参考链接:doleron/yolov5-opencv-cpp-python: Example of using ultralytics YOLO V5 with OpenCV 4.5.4, C++ and Python (github.com)

  1. #include <fstream>
  2. #include <iostream>
  3. #include <opencv2/opencv.hpp>
  4. //将class加载到vector中
  5. std::vector<std::string> load_class_list()
  6. {
  7. std::vector<std::string> class_list;
  8. std::ifstream ifs("config_files/classes.txt");
  9. std::string line;
  10. while (getline(ifs, line))
  11. {
  12. //std::cout << "Class:---------------:::::::" << line << std::endl;
  13. class_list.push_back(line);
  14. }
  15. return class_list;
  16. }
  17. // 加载网络
  18. void load_net(cv::dnn::Net &net, bool is_cuda)
  19. {
  20. //使用readNet()函数加载YOLOV5S.ONNX文件
  21. auto result = cv::dnn::readNet("config_files/yolov5s.onnx");
  22. //依据情况选定是否使用CUDA
  23. if (is_cuda)
  24. {
  25. std::cout << "Attempty to use CUDA\n";
  26. result.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
  27. result.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA_FP16);
  28. }
  29. else
  30. {
  31. std::cout << "Running on CPU\n";
  32. result.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
  33. result.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);
  34. }
  35. net = result;
  36. }
  37. //定义框体颜色
  38. const std::vector<cv::Scalar> colors = {cv::Scalar(255, 255, 0), cv::Scalar(0, 255, 0), cv::Scalar(0, 255, 255), cv::Scalar(255, 0, 0)};
  39. // 定义相关参数值与阈值
  40. const float INPUT_WIDTH = 640.0;
  41. const float INPUT_HEIGHT = 640.0;
  42. const float SCORE_THRESHOLD = 0.2;
  43. const float NMS_THRESHOLD = 0.4;
  44. const float CONFIDENCE_THRESHOLD = 0.4;
  45. // 定义输出结果的结构体类
  46. struct Detection
  47. {
  48. int class_id;
  49. float confidence;
  50. cv::Rect box;
  51. };
  52. //将输入的图像进行预处理,返回一个格式化后的图像。
  53. cv::Mat format_yolov5(const cv::Mat &source) {
  54. int col = source.cols;
  55. int row = source.rows;
  56. int _max = MAX(col, row);
  57. cv::Mat result = cv::Mat::zeros(_max, _max, CV_8UC3);
  58. source.copyTo(result(cv::Rect(0, 0, col, row)));
  59. return result;
  60. }
  61. //YOLOV5网络的数据预处理以及前向推理(包括NMS处理)
  62. void detect(cv::Mat &image, cv::dnn::Net &net, std::vector<Detection> &output, const std::vector<std::string> &className) {
  63. cv::Mat blob;
  64. auto input_image = format_yolov5(image);
  65. cv::dnn::blobFromImage(input_image, blob, 1./255., cv::Size(INPUT_WIDTH, INPUT_HEIGHT), cv::Scalar(), true, false);
  66. net.setInput(blob); // blob-[3,640,640]
  67. std::vector<cv::Mat> outputs;
  68. //网络计算到指定层(第二个参数指定的层),并返回该层的所有输出
  69. net.forward(outputs, net.getUnconnectedOutLayersNames()); // getUnconnectedOutLayersNames()返回具有未连接输出的层的名称,返回最终输出层
  70. //计算x_factor和y_factor,用于后面还原bounding box的位置和大小
  71. float x_factor = input_image.cols / INPUT_WIDTH;
  72. float y_factor = input_image.rows / INPUT_HEIGHT;
  73. /*
  74. 通过outputs[0]可以获得该输出层的结果,其中包含了该层所有的预测框的信息,包括预测框的位置、大小、置信度和类别概率。
  75. 这些信息被保存在一个指向连续内存的地址中,可以通过.data来访问。
  76. outputs[0].data返回一个指向float类型的连续内存的指针,即该指针指向的是一个float类型的数组,其中包含了该层所有预测框的位置、大小、置信度和类别概率。
  77. 因此,将该指针赋值给float* data后,就可以通过data来访问该数组中的每一个元素。
  78. 同时,由于该数组是连续内存,因此可以通过指针的算术运算来访问该数组中的每一个元素,即使用data[i]来访问数组中第i个元素
  79. */
  80. float *data = (float *)outputs[0].data;
  81. //std::cout << "---------------------------------------:" << sizeof(&data) << std::endl;
  82. /*
  83. Yolov5s模型的输出大小为(1, 25200, 85),其中:
  84. 第一维是batch size,为1;
  85. 第二维为每张输入图片生成的预测框数,即anchors数量 x (S1 x S1 + S2 x S2 + S3 x S3),这里的S1, S2, S3分别为输出层的三个特征图的大小,取值为{80, 40, 20},anchors数量为3,因此总的预测框数为25200;
  86. 第三维为每个预测框的信息,包括4个坐标信息、1个置信度信息和80个类别得分信息,共85个信息。
  87. */
  88. const int dimensions = 85;
  89. const int rows = 25200;
  90. std::vector<int> class_ids;
  91. std::vector<float> confidences;
  92. std::vector<cv::Rect> boxes;
  93. for (int i = 0; i < rows; ++i) {
  94. /*
  95. 在C++中,指针使用[]操作符时,其作用与数组类似。
  96. 当指针指向的是连续的内存区域时,可以使用[]操作符来访问该区域中的数据。
  97. 例如,如果有一个指向float类型数据的指针p,我们可以通过p[i]来访问它所指向的内存中的第i个float类型的数据。
  98. 这里的i是一个整数索引,指定了要访问的数据在内存中的偏移量。
  99. data[4]和data + 5可以分别访问指针所指向的内存中的第5个float类型的数据和从第6个float类型的数据开始的一段连续数据。
  100. */
  101. float confidence = data[4];
  102. if (confidence >= CONFIDENCE_THRESHOLD) {
  103. float * classes_scores = data + 5;
  104. /*
  105. cv::Mat的构造函数参数如下:
  106. 第一个参数:矩阵的行数,这里设置为1,表示只有一行;
  107. 第二个参数:矩阵的列数,这里设置为className.size(),表示有className.size()列;
  108. 第三个参数:矩阵的数据类型,这里设置为CV_32FC1,表示元素类型为单通道32位浮点数;
  109. 第四个参数:矩阵的数据指针,这里设置为classes_scores,表示矩阵的数据存储在classes_scores所指向的内存地址处,指向的是内存的首地址。
  110. */
  111. cv::Mat scores(1, className.size(), CV_32FC1, classes_scores);
  112. cv::Point class_id;
  113. double max_class_score;
  114. //获取最大类别分数以及其对应的索引
  115. minMaxLoc(scores, 0, &max_class_score, 0, &class_id);
  116. //通过阈值进行筛选,将符合要求的类别、置信度以及框体进行保存
  117. if (max_class_score > SCORE_THRESHOLD) {
  118. confidences.push_back(confidence);
  119. class_ids.push_back(class_id.x);
  120. float x = data[0];
  121. float y = data[1];
  122. float w = data[2];
  123. float h = data[3];
  124. int left = int((x - 0.5 * w) * x_factor);
  125. int top = int((y - 0.5 * h) * y_factor);
  126. int width = int(w * x_factor);
  127. int height = int(h * y_factor);
  128. boxes.push_back(cv::Rect(left, top, width, height));
  129. }
  130. }
  131. //一个边界框包含85个值————4个坐标信息、1个置信度信息和80个类别得分信息
  132. //data所指内存地址包含输出层所有预测框的位置、大小、置信度和类别概率,在yolov5s中共有25200个边界框,即data所指内存地址包含25200*85个值
  133. //在遍历一个边界框后,data指向需要向后移动85个位置,即 +85
  134. data += 85;
  135. }
  136. std::vector<int> nms_result;
  137. /*
  138. 在目标检测任务中,一个目标可能会被多个边界框检测到,这些边界框可能会有不同的位置和大小,但表示同一个目标。
  139. 非极大值抑制(Non-Maximum Suppression,NMS)是一种常用的方法,用于抑制这些重叠的边界框,只保留置信度最高的那个边界框,从而得到最终的目标检测结果。
  140. NMS的原理如下:
  141. 首先,对所有的边界框按照其置信度进行排序,置信度最高的边界框排在最前面。
  142. 从置信度最高的边界框开始,依次遍历其余边界框。
  143. 对于当前遍历到的边界框,如果它与前面已经保留的边界框的重叠程度(通过计算IOU值)大于一定阈值(比如0.5),那么就将其抑制掉,不保留。
  144. 继续遍历下一个边界框,重复上述过程,直到所有的边界框都被处理完毕。
  145. 通过这样的处理,NMS可以抑制掉大量重叠的边界框,只保留最好的那个边界框,从而得到最终的目标检测结果。这种方法虽然简单,但是在实践中非常有效,已经被广泛应用于各种目标检测任务中。
  146. 关于非极大值抑制,我在新的一篇进行了详细讲解,可在我们博客内容中搜索参考。
  147. */
  148. cv::dnn::NMSBoxes(boxes, confidences, SCORE_THRESHOLD, NMS_THRESHOLD, nms_result);
  149. //将经过NMS处理后的结果加载到const vector<Detection> output中
  150. for (int i = 0; i < nms_result.size(); i++) {
  151. int idx = nms_result[i];
  152. Detection result;
  153. result.class_id = class_ids[idx];
  154. result.confidence = confidences[idx];
  155. result.box = boxes[idx];
  156. output.push_back(result);
  157. }
  158. }
  159. int main(int argc, char **argv)
  160. {
  161. // 加载class列表
  162. std::vector<std::string> class_list = load_class_list();
  163. //读取视频文件并判断是否成功打开
  164. cv::Mat frame;
  165. cv::VideoCapture capture("sample.mp4");
  166. if (!capture.isOpened())
  167. {
  168. std::cerr << "Error opening video file\n";
  169. return -1;
  170. }
  171. //cv::Mat frame = cv::imread("misc/araras.jpg");
  172. //判断是否使用CUDA,在选定使用CUDA前,请确保电脑支持GPU以及安装了CUDA、cudnn。
  173. bool is_cuda = argc > 1 && strcmp(argv[1], "cuda") == 0;
  174. //加载YOLOV5网络
  175. cv::dnn::Net net;
  176. load_net(net, is_cuda);
  177. //创建高精度计时器
  178. auto start = std::chrono::high_resolution_clock::now();
  179. //纪录视频帧数
  180. int frame_count = 0;
  181. //计算FPS(每秒传输帧数)
  182. float fps = -1;
  183. int total_frames = 0;
  184. while (true)
  185. {
  186. //读取视频帧, 关于关于视频处理应用也可参考我其他的opencv讲解内容
  187. capture.read(frame);
  188. if (frame.empty())
  189. {
  190. std::cout << "End of stream\n";
  191. break;
  192. }
  193. std::vector<Detection> output;
  194. //YOLOV5S前向推理
  195. detect(frame, net, output, class_list);
  196. frame_count++;
  197. total_frames++;
  198. //检测的边界框总数
  199. int detections = output.size();
  200. for (int i = 0; i < detections; ++i)
  201. {
  202. auto detection = output[i];
  203. auto box = detection.box;
  204. auto classId = detection.class_id;
  205. //通过取模运算为边界框选定颜色
  206. const auto color = colors[classId % colors.size()];
  207. //绘制边界框
  208. cv::rectangle(frame, box, color, 3);
  209. //绘制用于写类别的边框范围,一般就在边框的上面
  210. cv::rectangle(frame, cv::Point(box.x, box.y - 20), cv::Point(box.x + box.width, box.y), color, cv::FILLED);
  211. //在上面绘制的框界内写出类别
  212. cv::putText(frame, class_list[classId].c_str(), cv::Point(box.x, box.y - 5), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0));
  213. }
  214. //根据帧数以及计时器结果计算FPS
  215. if (frame_count >= 30)
  216. {
  217. auto end = std::chrono::high_resolution_clock::now();
  218. fps = frame_count * 1000.0 / std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
  219. frame_count = 0;
  220. start = std::chrono::high_resolution_clock::now();
  221. }
  222. //如果FPS大于0,就在视频左上角写出来
  223. if (fps > 0)
  224. {
  225. std::ostringstream fps_label;
  226. fps_label << std::fixed << std::setprecision(2);
  227. fps_label << "FPS: " << fps;
  228. std::string fps_label_str = fps_label.str();
  229. cv::putText(frame, fps_label_str.c_str(), cv::Point(10, 25), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 0, 255), 2);
  230. }
  231. cv::imshow("output", frame);
  232. //如果用户按下了键,那么capture.release()函数会释放视频捕获对象,跳出循环,关闭视频流。同时,程序输出一条消息提示用户程序已经结束。
  233. if (cv::waitKey(1) != -1)
  234. {
  235. capture.release();
  236. std::cout << "finished by user\n";
  237. break;
  238. }
  239. }
  240. //输出视频检测的总帧数
  241. std::cout << "Total frames: " << total_frames << "\n";
  242. return 0;
  243. }

e6fc437eb06843a59baab3b3ce728931.png

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/300332
推荐阅读
相关标签
  

闽ICP备14008679号