本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.
树莓派摄像头 C++ OpenCV YoloV3 实现实时目标检测
本文将实现树莓派摄像头 C++ OpenCV YoloV3 实现实时目标检测,我们会先实现树莓派对视频文件的逐帧检测来验证算法流程,成功后,再接入摄像头进行实时目标检测。
先声明一下笔者的主要软硬件配置:
树莓派4B 4GB内存
CSI 摄像头
Ubuntu 20.04
OpenCV 的安装
不多讲,参考Ubuntu 18.04 安装OpenCV C++
。
准备YoloV3模型权重文件和视频文件
模型配置文件和权重、COCO数据集名称文件
我们先下载作者官方发布的 YoloV3 模型配置文件、权重文件:
wget https://pjreddie.com/media/files/yolov3.weights wget https://github.com/pjreddie/darknet/blob/master/cfg/yolov3.cfg?raw=true -O ./yolov3.cfg
上面是比较大的网络,由于我们的树莓派算力比较一遍,所以建议使用轻量型的网络 yolov3-tiny:
wget https://pjreddie.com/media/files/yolov3-tiny.weights wget https://github.com/pjreddie/darknet/blob/master/cfg/yolov3-tiny.cfg?raw=true -O ./yolov3-tiny.cfg
另外,由于模型权重是在 COCO 数据集上进行预训练的,所以我们还要准备 COCO 的类别名称文件,方便在模型输出检测结果后进行后处理,将类别显示在检测结果框上。
wget https://github.com/pjreddie/darknet/blob/master/data/coco.names?raw=true -O ./coco.names
注:如果上面的 github 中的配置文件在命令行下载的比较慢的话,可以直接去 github 网页复制粘贴下来。
准备视频文件
我们会先对一个视频文件进行逐帧检测来验证算法的流程,在之后再使用摄像头进行实时检测。
我们直接通过 you-get 工具去B站下载视频文件并改个名:
pip install you-get you-get https://www.bilibili.com/video/av32184680 rm fileName.cmt.xml mv fileName.mp4 demo.mp4
如果是 flv 文件,可以用 ffmpeg 转为 mp4 文件:
ffmpeg -i input.flv output.mp4
视频文件的检测
一切准备就绪我们开始先测试一下视频文件的检测,我们先讲解一遍代码,在最后会给出整个源码。
1 初始化参数
YOLOv3算法的预测结果就是边界框。每一个边界框都旁随着一个置信值。第一阶段中,全部低于置信度阀值的都会排除掉。 对剩余的边界框执行非最大抑制算法,以去除重叠的边界框。非最大抑制由一个参数nmsThrehold
控制。读者可以尝试改变这个数值,观察输出的边界框的改变。 接下来,设置输入图片的宽度inpWidth
和高度inpHeight
。这里设置为416。如果想要更快的速度,可以把宽度和高度设置为320。如果想要更准确的结果,可改变为608。
#include <iostream> #include <fstream> #include <vector> #include <string> #include <opencv.hpp> using namespace std; float confThreshold = 0.5;//置信度阈值 float nmsThreshold = 0.4;//非最大抑制阈值 int inpWidth = 416;//网络输入图片宽度 int inpHeight = 416;//网络输入图片高度
2 读取模型和COCO类别名
接下来我们读入COCO 类别名并存入classes
容器。并加载模型与权重文件yolov3-tiny.cfg
和yolov3-tiny.weights
。这里用到的几个文件就是我们刚才下载好的,读者需要改为自己的路径。最后把DNN的后端设置为OpenCV,目标设置CPU。这里我们树莓派没有GPU等加速推理硬件,就用CPU,如果有GPU,可改为OpenCL、CUDA等
//将类名存进容器 vector<string> classes;//储存名字的容器 string classesFile = "./coco.names";//coco.names包含80种不同的类名 ifstream ifs(classesFile.c_str()); string line; while(getline(ifs,line))classes.push_back(line); //取得模型的配置和权重文件 cv::String modelConfiguration = "./yolov3-tiny.cfg"; cv::String modelWeights = "./yolov3-tiny.weights"; //加载网络 cv::dnn::Net net = cv::dnn::readNetFromDarknet(modelConfiguration,modelWeights); net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV); net.setPreferableTarget(cv::dnn::DNN_TARGET_OPENCL);// 这里我们树莓派没有GPU等加速推理硬件,就用CPU,如果有GPU,可改为OpenCL、CUDA等
3 读取输入
这里我们先读入下载好的视频文件,一会儿再使用本地摄像头测试。这里如果是树莓派外接显示器的话可以用创建GUI窗口来查看,但是我们通常是命令行SSH连接树莓派,这时我们就直接将每一帧的检测结果保存为图像文件查看:
// 打开视频文件或者本地摄像头来读取输入 string str, outputFile; cv::VideoCapture cap("./demo.mp4"); cv::VideoWriter video; cv::Mat frame,blob; // 开启摄像头 // cv::VideoCapture capture(0); // 创建窗口 // static const string kWinName = "YoloV3 OpenCV"; // cv::namedWindow(kWinName,cv::WINDOW_AUTOSIZE); // 非GUI界面不需要创建窗口
4 循环处理每一帧
OpenCV中,输入到神经网络的图像需要以一种叫 bolb 的格式保存。 读取了输入图片或者视频流的一帧图像后,这帧图像需要经过bolbFromImage()
函数处理为神经网络的输入类型 bolb。在这个过程中,图像像素以一个 1/255 的比例因子,被缩放到0到1之间。同时,图像在不裁剪的情况下,大小调整到 416×416。注意我们没有降低图像平均值,因此传递 [0,0,0] 到函数的平均值输入,保持swapRB 参数到默认值1。 输出的bolb传递到网络,经过网络正向处理,网络输出了所预测到的一个边界框清单。这些边界框通过后处理,滤除了低置信值的。我们随后再详细的说明后处理的步骤。我们在每一帧的左上方打印出了推断时间。伴随着最后的边界框的完成,图像保存到硬盘中,之后可以作为图像输入或者通过 VideoWriter 作为视频流输入。
while(cv::waitKey(1)<0){ // 取每帧图像 cap>>frame; // 如果视频播放完则停止程序 if(frame.empty()){ break; } // 在dnn中从磁盘加载图片 cv::dnn::blobFromImage(frame,blob,1/255.0,cv::Size(inpWidth,inpHeight)); // 设置输入 net.setInput(blob); // 设置输出层 vector<cv::Mat> outs;//储存识别结果 net.forward(outs,getOutputNames(net)); // 移除低置信度边界框 postprocess(frame,outs); // 显示s延时信息并绘制 vector<double> layersTimes; double freq = cv::getTickFrequency()/1000; double t=net.getPerfProfile(layersTimes)/freq; string label = cv::format("Infercence time for a frame:%.2f ms",t); cv::putText(frame,label,cv::Point(0,15),cv::FONT_HERSHEY_SIMPLEX,0.5,cv::Scalar(0,255,255)); cout << "Frame: " << frame_cnt++ << ", time: " << t << "ms" << endl; // 绘制识别框,在这里如果我们用的是GUI界面,并且刚才创建了窗口的话,可以imshow,否则是命令行SSH连接树莓派的话就imwrite保存图像 // cv::imshow(kWinName,frame); cv::imwrite("output.jpg",frame); }
5-1 得到输出层的名字
第五步我们给出几个用到的函数的实现。
OpenCV 的网络类中的前向功能需要结束层,直到它在网络中运行。因为我们需要运行整个网络,所以我们需要识别网络中的最后一层。我们通过使用getUnconnectedOutLayers()
获得未连接的输出层的名字,该层基本就是网络的最后层。然后我们运行前向网络,得到输出,如前面的代码片段net.forward(getOutputsNames(net))
。
//从输出层得到名字 vector<cv::String> getOutputNames(const cv::dnn::Net& net){ static vector<cv::String> names; if(names.empty()){ //取得输出层指标 vector<int> outLayers = net.getUnconnectedOutLayers(); vector<cv::String> layersNames = net.getLayerNames(); //取得输出层名字 names.resize(outLayers.size()); for(size_t i =0;i<outLayers.size();i++){ names[i] = layersNames[outLayers[i]-1]; } } return names; }
5-2 后处理
网络输出的每个边界框都分别由一个包含着类别名字和5个元素的向量表示。 头四个元素代表center_x, center_y, width, height
。第五个元素表示包含着目标的边界框的置信度。 其余的元素是和每个类别(如目标种类)有关的置信度。边界框分配给最高分数对应的那一种类。 一个边界框的最高分数也叫做它的置信度confidence
。如果边界框的置信度低于规定的阀值,算法上不再处理这个边界框。 置信度大于或等于置信度阀值的边界框,将进行非最大抑制。这会减少重叠的边界框数目。
// 移除低置信度边界框 void postprocess(cv::Mat& frame,const vector<cv::Mat>& outs){ vector<int> classIds;// 储存识别类的索引 vector<float> confidences;// 储存置信度 vector<cv::Rect> boxes;// 储存边框 for(size_t i=0;i<outs.size();i++){ //从网络输出中扫描所有边界框 //保留高置信度选框 //目标数据data:x,y,w,h为百分比,x,y为目标中心点坐标 float* data = (float*)outs[i].data; for(int j=0;j<outs[i].rows;j++,data+=outs[i].cols){ cv::Mat scores = outs[i].row(j).colRange(5,outs[i].cols); cv::Point classIdPoint; double confidence;//置信度 //取得最大分数值与索引 cv::minMaxLoc(scores,0,&confidence,0,&classIdPoint); if(confidence>confThreshold){ int centerX = (int)(data[0]*frame.cols); int centerY = (int)(data[1]*frame.rows); int width = (int)(data[2]*frame.cols); int height = (int)(data[3]*frame.rows); int left = centerX-width/2; int top = centerY-height/2; classIds.push_back(classIdPoint.x); confidences.push_back((float)confidence); boxes.push_back(cv::Rect(left, top, width, height)); } } } //低置信度 vector<int> indices;//保存没有重叠边框的索引 //该函数用于抑制重叠边框 cv::dnn::NMSBoxes(boxes,confidences,confThreshold,nmsThreshold,indices); for(size_t i=0;i<indices.size();i++){ int idx = indices[i]; cv::Rect box = boxes[idx]; drawPred(classIds[idx],confidences[idx],box.x,box.y, box.x+box.width,box.y+box.height,frame); } }
5-3 画出边界框
最后,经过非最大抑制后,得到了边界框。我们把边界框在输入帧上画出,并标出种类名和置信值。
//绘制预测边界框 void drawPred(int classId,float conf,int left,int top,int right,int bottom,cv::Mat& frame){ //绘制边界框 cv::rectangle(frame,cv::Point(left,top),cv::Point(right,bottom),cv::Scalar(255,178,50),3); string label = cv::format("%.2f",conf); if(!classes.empty()){ CV_Assert(classId < (int)classes.size()); label = classes[classId]+":"+label;//边框上的类别标签与置信度 } //绘制边界框上的标签 int baseLine; cv::Size labelSize = cv::getTextSize(label,cv::FONT_HERSHEY_SIMPLEX,0.5,1,&baseLine); top = max(top,labelSize.height); cv::rectangle(frame,cv::Point(left,top-round(1.5*labelSize.height)),cv::Point(left+round(1.5*labelSize.width),top+baseLine),cv::Scalar(255,255,255),cv::FILLED); cv::putText(frame, label,cv::Point(left, top), cv::FONT_HERSHEY_SIMPLEX, 0.75,cv::Scalar(0, 0, 0), 1); }
文件全部源码在文末。
写好之后我们编译执行即可,关于 OpenCV 众多头文件包含、链接库链接时、运行时的链接,对初学者来说可能会遇到一些问题,可以参考:
Linux下编译、链接、加载运行C++ OpenCV的两种方式及常见问题的解决
Linux下C/C++程序编译链接加载过程中的常见问题及解决方法
可以从左上角和标准输出看到,每帧的检测时间大概在 280ms,速度还可以,精度大体也不错。但是由于模型较小,性能受限,对于一些边缘小物体会有误差,如上图中右侧的小车。
树莓派摄像头实时检测
树莓派摄像头调试参考:树莓派摄像头基础配置及测试
。
在视频文件的检测顺利完成后,实时树莓派的检测就很简单了,只需要将读取输入部分从视频文件改为本地摄像头即可。
主要就是这一行修改:
// cv::VideoCapture cap("./video/demo.mp4"); // cv::VideoWriter video; // 改为 cv::VideoCapture cap(0);
另外,我们需要设置一些 OpenCV 读取摄像头输入的尺寸大小,否则笔者亲测是有一些小bug:
cap.set(cv::CAP_PROP_FRAME_WIDTH, 640); cap.set(cv::CAP_PROP_FRAME_HEIGHT, 480);
在笔者实验室中实测,速度和精度也都还可以。
全部代码
这里给出摄像头实时测试的全部代码
#include <iostream> #include <fstream> #include <vector> #include <string> #include <opencv.hpp> using namespace std; vector<string> classes;//储存名字的容器 float confThreshold = 0.5;//置信度阈值 float nmsThreshold = 0.4;//非最大抑制阈值 // int inpWidth = 416;//网络输入图片宽度 // int inpHeight = 416;//网络输入图片高度 int inpWidth = 320; int inpHeight = 320; //移除低置信度边界框 void postprocess(cv::Mat& frame,const vector<cv::Mat>& out); //画出预测边界框 void drawPred(int classId,float conf,int left,int top,int right,int bottom,cv::Mat& frame); //取得输出层的名字 vector<cv::String> getOutputNames(const cv::dnn::Net& net); int main(int argc, char const *argv[]) { //将类名存进容器 string classesFile = "./coco.names";//coco.names包含80种不同的类名 ifstream ifs(classesFile.c_str()); string line; while(getline(ifs,line))classes.push_back(line); //取得模型的配置和权重文件 cv::String modelConfiguration = "./model_file/yolov3-tiny.cfg"; cv::String modelWeights = "./model_file/yolov3-tiny.weights"; //加载网络 cv::dnn::Net net = cv::dnn::readNetFromDarknet(modelConfiguration,modelWeights); net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV); net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU); // net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA); // net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA); //打开视频文件或者图形文件或者相机数据流 string str, outputFile; // cv::VideoCapture cap("demo.mp4"); // cv::VideoWriter video; cv::Mat frame, blob; //开启摄像头 cv::VideoCapture cap(0); cap.set(cv::CAP_PROP_FRAME_WIDTH, 640); cap.set(cv::CAP_PROP_FRAME_HEIGHT, 480); // 创建窗口 // static const string kWinName = "Deep learning object detection in OpenCV"; // cv::namedWindow(kWinName,cv::WINDOW_AUTOSIZE); //处理每帧 int frame_cnt = 0; while(cv::waitKey(1)<0){ //取每帧图像 cap >> frame; //如果视频播放完则停止程序 if(frame.empty()){ break; } //在dnn中从磁盘加载图片 cv::dnn::blobFromImage(frame,blob,1/255.0,cv::Size(inpWidth,inpHeight)); //设置输入网络 net.setInput(blob); //设置输出层 vector<cv::Mat> outs;//储存识别结果 net.forward(outs,getOutputNames(net)); //移除低置信度边界框 postprocess(frame,outs); //显示s延时信息并绘制 vector<double> layersTimes; double freq = cv::getTickFrequency()/1000; double t=net.getPerfProfile(layersTimes)/freq; string label = cv::format("Infercence time for a frame:%.2f ms",t); cv::putText(frame,label,cv::Point(0,15),cv::FONT_HERSHEY_SIMPLEX,0.5,cv::Scalar(0,255,255)); //绘制识别框 cv::Mat detecteFrame; frame.convertTo(detecteFrame,CV_8U); // cv::imshow(kWinName,frame); cout << "Frame: " << frame_cnt++ << ", time: " << t << "ms" << endl; cv::imwrite("output.jpg",frame); } cap.release(); return 0; } //移除低置信度边界框 void postprocess(cv::Mat& frame,const vector<cv::Mat>& outs){ vector<int> classIds;//储存识别类的索引 vector<float> confidences;//储存置信度 vector<cv::Rect> boxes;//储存边框 for(size_t i=0;i<outs.size();i++){ //从网络输出中扫描所有边界框 //保留高置信度选框 //目标数据data:x,y,w,h为百分比,x,y为目标中心点坐标 float* data = (float*)outs[i].data; for(int j=0;j<outs[i].rows;j++,data+=outs[i].cols){ cv::Mat scores = outs[i].row(j).colRange(5,outs[i].cols); cv::Point classIdPoint; double confidence;//置信度 //取得最大分数值与索引 cv::minMaxLoc(scores,0,&confidence,0,&classIdPoint); if(confidence>confThreshold){ int centerX = (int)(data[0]*frame.cols); int centerY = (int)(data[1]*frame.rows); int width = (int)(data[2]*frame.cols); int height = (int)(data[3]*frame.rows); int left = centerX-width/2; int top = centerY-height/2; classIds.push_back(classIdPoint.x); confidences.push_back((float)confidence); boxes.push_back(cv::Rect(left, top, width, height)); } } } //低置信度 vector<int> indices;//保存没有重叠边框的索引 //该函数用于抑制重叠边框 cv::dnn::NMSBoxes(boxes,confidences,confThreshold,nmsThreshold,indices); for(size_t i=0;i<indices.size();i++){ int idx = indices[i]; cv::Rect box = boxes[idx]; drawPred(classIds[idx],confidences[idx],box.x,box.y, box.x+box.width,box.y+box.height,frame); } } //绘制预测边界框 void drawPred(int classId,float conf,int left,int top,int right,int bottom,cv::Mat& frame){ //绘制边界框 cv::rectangle(frame,cv::Point(left,top),cv::Point(right,bottom),cv::Scalar(255,178,50),3); string label = cv::format("%.2f",conf); if(!classes.empty()){ CV_Assert(classId < (int)classes.size()); label = classes[classId]+":"+label;//边框上的类别标签与置信度 } //绘制边界框上的标签 int baseLine; cv::Size labelSize = cv::getTextSize(label,cv::FONT_HERSHEY_SIMPLEX,0.5,1,&baseLine); top = max(top,labelSize.height); cv::rectangle(frame,cv::Point(left,top-round(1.5*labelSize.height)),cv::Point(left+round(1.5*labelSize.width),top+baseLine),cv::Scalar(255,255,255),cv::FILLED); cv::putText(frame, label,cv::Point(left, top), cv::FONT_HERSHEY_SIMPLEX, 0.75,cv::Scalar(0, 0, 0), 1); } //从输出层得到名字 vector<cv::String> getOutputNames(const cv::dnn::Net& net){ static vector<cv::String> names; if(names.empty()){ //取得输出层指标 vector<int> outLayers = net.getUnconnectedOutLayers(); vector<cv::String> layersNames = net.getLayerNames(); //取得输出层名字 names.resize(outLayers.size()); for(size_t i =0;i<outLayers.size();i++){ names[i] = layersNames[outLayers[i]-1]; } } return names; }
如有错误或遗漏,欢迎留言指正。
Ref:
https://blog.csdn.net/cuma2369/article/details/107666559
https://ryanadex.github.io/2019/01/15/opencv-yolov3/
Be First to Comment