树莓派摄像头 C++ OpenCV YoloV3 实现实时目标检测
樹(shù)莓派攝像頭 C++ OpenCV YoloV3 實(shí)現(xiàn)實(shí)時(shí)目標(biāo)檢測(cè)
本文將實(shí)現(xiàn)樹(shù)莓派攝像頭 C++ OpenCV YoloV3 實(shí)現(xiàn)實(shí)時(shí)目標(biāo)檢測(cè),我們會(huì)先實(shí)現(xiàn)樹(shù)莓派對(duì)視頻文件的逐幀檢測(cè)來(lái)驗(yàn)證算法流程,成功后,再接入攝像頭進(jìn)行實(shí)時(shí)目標(biāo)檢測(cè)。
先聲明一下筆者的主要軟硬件配置:
樹(shù)莓派4B 4GB內(nèi)存
CSI 攝像頭
Ubuntu 20.04
OpenCV 的安裝
不多講,參考 Ubuntu 18.04 安裝OpenCV C++ 。
準(zhǔn)備YoloV3模型權(quán)重文件和視頻文件
模型配置文件和權(quán)重、COCO數(shù)據(jù)集名稱(chēng)文件
我們先下載作者官方發(fā)布的 YoloV3 模型配置文件、權(quán)重文件:
wget https://pjreddie.com/media/files/yolov3.weights wget https://github.com/pjreddie/darknet/blob/master/cfg/yolov3.cfg?raw=true -O ./yolov3.cfg上面是比較大的網(wǎng)絡(luò),由于我們的樹(shù)莓派算力比較一遍,所以建議使用輕量型的網(wǎng)絡(luò) 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另外,由于模型權(quán)重是在 COCO 數(shù)據(jù)集上進(jìn)行預(yù)訓(xùn)練的,所以我們還要準(zhǔn)備 COCO 的類(lèi)別名稱(chēng)文件,方便在模型輸出檢測(cè)結(jié)果后進(jìn)行后處理,將類(lèi)別顯示在檢測(cè)結(jié)果框上。
wget https://github.com/pjreddie/darknet/blob/master/data/coco.names?raw=true -O ./coco.names注:如果上面的 github 中的配置文件在命令行下載的比較慢的話,可以直接去 github 網(wǎng)頁(yè)復(fù)制粘貼下來(lái)。
準(zhǔn)備視頻文件
我們會(huì)先對(duì)一個(gè)視頻文件進(jìn)行逐幀檢測(cè)來(lái)驗(yàn)證算法的流程,在之后再使用攝像頭進(jìn)行實(shí)時(shí)檢測(cè)。
我們直接通過(guò) you-get 工具去B站下載視頻文件并改個(gè)名:
pip install you-get you-get https://www.bilibili.com/video/av32184680 rm fileName.cmt.xml mv fileName.mp4 demo.mp4如果是 flv 文件,可以用 ffmpeg 轉(zhuǎn)為 mp4 文件:
ffmpeg -i input.flv output.mp4視頻文件的檢測(cè)
一切準(zhǔn)備就緒我們開(kāi)始先測(cè)試一下視頻文件的檢測(cè),我們先講解一遍代碼,在最后會(huì)給出整個(gè)源碼。
1 初始化參數(shù)
YOLOv3算法的預(yù)測(cè)結(jié)果就是邊界框。每一個(gè)邊界框都旁隨著一個(gè)置信值。第一階段中,全部低于置信度閥值的都會(huì)排除掉。 對(duì)剩余的邊界框執(zhí)行非最大抑制算法,以去除重疊的邊界框。非最大抑制由一個(gè)參數(shù) nmsThrehold 控制。讀者可以嘗試改變這個(gè)數(shù)值,觀察輸出的邊界框的改變。 接下來(lái),設(shè)置輸入圖片的寬度inpWidth和高度 inpHeight。這里設(shè)置為416。如果想要更快的速度,可以把寬度和高度設(shè)置為320。如果想要更準(zhǔn)確的結(jié)果,可改變?yōu)?08。
#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; //網(wǎng)絡(luò)輸入圖片寬度 int inpHeight = 416; //網(wǎng)絡(luò)輸入圖片高度2 讀取模型和COCO類(lèi)別名
接下來(lái)我們讀入COCO 類(lèi)別名并存入 classes 容器。并加載模型與權(quán)重文件 yolov3-tiny.cfg 和 yolov3-tiny.weights。這里用到的幾個(gè)文件就是我們剛才下載好的,讀者需要改為自己的路徑。最后把DNN的后端設(shè)置為OpenCV,目標(biāo)設(shè)置CPU。這里我們樹(shù)莓派沒(méi)有GPU等加速推理硬件,就用CPU,如果有GPU,可改為OpenCL、CUDA等
//將類(lèi)名存進(jìn)容器 vector<string> classes; //儲(chǔ)存名字的容器 string classesFile = "./coco.names"; //coco.names包含80種不同的類(lèi)名 ifstream ifs(classesFile.c_str()); string line; while(getline(ifs,line))classes.push_back(line);//取得模型的配置和權(quán)重文件 cv::String modelConfiguration = "./yolov3-tiny.cfg"; cv::String modelWeights = "./yolov3-tiny.weights";//加載網(wǎng)絡(luò) cv::dnn::Net net = cv::dnn::readNetFromDarknet(modelConfiguration,modelWeights); net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV); net.setPreferableTarget(cv::dnn::DNN_TARGET_OPENCL); // 這里我們樹(shù)莓派沒(méi)有GPU等加速推理硬件,就用CPU,如果有GPU,可改為OpenCL、CUDA等3 讀取輸入
這里我們先讀入下載好的視頻文件,一會(huì)兒再使用本地?cái)z像頭測(cè)試。這里如果是樹(shù)莓派外接顯示器的話可以用創(chuàng)建GUI窗口來(lái)查看,但是我們通常是命令行SSH連接樹(shù)莓派,這時(shí)我們就直接將每一幀的檢測(cè)結(jié)果保存為圖像文件查看:
// 打開(kāi)視頻文件或者本地?cái)z像頭來(lái)讀取輸入 string str, outputFile; cv::VideoCapture cap("./demo.mp4"); cv::VideoWriter video; cv::Mat frame,blob; // 開(kāi)啟攝像頭 // cv::VideoCapture capture(0); // 創(chuàng)建窗口 // static const string kWinName = "YoloV3 OpenCV"; // cv::namedWindow(kWinName,cv::WINDOW_AUTOSIZE); // 非GUI界面不需要?jiǎng)?chuàng)建窗口4 循環(huán)處理每一幀
OpenCV中,輸入到神經(jīng)網(wǎng)絡(luò)的圖像需要以一種叫 bolb 的格式保存。 讀取了輸入圖片或者視頻流的一幀圖像后,這幀圖像需要經(jīng)過(guò)bolbFromImage() 函數(shù)處理為神經(jīng)網(wǎng)絡(luò)的輸入類(lèi)型 bolb。在這個(gè)過(guò)程中,圖像像素以一個(gè) 1/255 的比例因子,被縮放到0到1之間。同時(shí),圖像在不裁剪的情況下,大小調(diào)整到 416x416。注意我們沒(méi)有降低圖像平均值,因此傳遞 [0,0,0] 到函數(shù)的平均值輸入,保持swapRB 參數(shù)到默認(rèn)值1。 輸出的bolb傳遞到網(wǎng)絡(luò),經(jīng)過(guò)網(wǎng)絡(luò)正向處理,網(wǎng)絡(luò)輸出了所預(yù)測(cè)到的一個(gè)邊界框清單。這些邊界框通過(guò)后處理,濾除了低置信值的。我們隨后再詳細(xì)的說(shuō)明后處理的步驟。我們?cè)诿恳粠淖笊戏酱蛴〕隽送茢鄷r(shí)間。伴隨著最后的邊界框的完成,圖像保存到硬盤(pán)中,之后可以作為圖像輸入或者通過(guò) VideoWriter 作為視頻流輸入。
while(cv::waitKey(1)<0){// 取每幀圖像cap>>frame;// 如果視頻播放完則停止程序if(frame.empty()){break;}// 在dnn中從磁盤(pán)加載圖片cv::dnn::blobFromImage(frame,blob,1/255.0,cv::Size(inpWidth,inpHeight));// 設(shè)置輸入net.setInput(blob);// 設(shè)置輸出層vector<cv::Mat> outs; //儲(chǔ)存識(shí)別結(jié)果net.forward(outs,getOutputNames(net));// 移除低置信度邊界框postprocess(frame,outs);// 顯示s延時(shí)信息并繪制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;// 繪制識(shí)別框,在這里如果我們用的是GUI界面,并且剛才創(chuàng)建了窗口的話,可以imshow,否則是命令行SSH連接樹(shù)莓派的話就imwrite保存圖像// cv::imshow(kWinName,frame);cv::imwrite("output.jpg",frame); }5-1 得到輸出層的名字
第五步我們給出幾個(gè)用到的函數(shù)的實(shí)現(xiàn)。
OpenCV 的網(wǎng)絡(luò)類(lèi)中的前向功能需要結(jié)束層,直到它在網(wǎng)絡(luò)中運(yùn)行。因?yàn)槲覀冃枰\(yùn)行整個(gè)網(wǎng)絡(luò),所以我們需要識(shí)別網(wǎng)絡(luò)中的最后一層。我們通過(guò)使用 getUnconnectedOutLayers() 獲得未連接的輸出層的名字,該層基本就是網(wǎng)絡(luò)的最后層。然后我們運(yùn)行前向網(wǎng)絡(luò),得到輸出,如前面的代碼片段 net.forward(getOutputsNames(net))。
//從輸出層得到名字 vector<cv::String> getOutputNames(const cv::dnn::Net& net){static vector<cv::String> names;if(names.empty()){//取得輸出層指標(biāo)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 后處理
網(wǎng)絡(luò)輸出的每個(gè)邊界框都分別由一個(gè)包含著類(lèi)別名字和5個(gè)元素的向量表示。 頭四個(gè)元素代表center_x, center_y, width, height。第五個(gè)元素表示包含著目標(biāo)的邊界框的置信度。 其余的元素是和每個(gè)類(lèi)別(如目標(biāo)種類(lèi))有關(guān)的置信度。邊界框分配給最高分?jǐn)?shù)對(duì)應(yīng)的那一種類(lèi)。 一個(gè)邊界框的最高分?jǐn)?shù)也叫做它的置信度 confidence。如果邊界框的置信度低于規(guī)定的閥值,算法上不再處理這個(gè)邊界框。 置信度大于或等于置信度閥值的邊界框,將進(jìn)行非最大抑制。這會(huì)減少重疊的邊界框數(shù)目。
// 移除低置信度邊界框 void postprocess(cv::Mat& frame,const vector<cv::Mat>& outs){vector<int> classIds; // 儲(chǔ)存識(shí)別類(lèi)的索引vector<float> confidences;// 儲(chǔ)存置信度vector<cv::Rect> boxes; // 儲(chǔ)存邊框for(size_t i=0;i<outs.size();i++){//從網(wǎng)絡(luò)輸出中掃描所有邊界框//保留高置信度選框//目標(biāo)數(shù)據(jù)data:x,y,w,h為百分比,x,y為目標(biāo)中心點(diǎn)坐標(biāo)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;//置信度//取得最大分?jǐn)?shù)值與索引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;//保存沒(méi)有重疊邊框的索引//該函數(shù)用于抑制重疊邊框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 畫(huà)出邊界框
最后,經(jīng)過(guò)非最大抑制后,得到了邊界框。我們把邊界框在輸入幀上畫(huà)出,并標(biāo)出種類(lèi)名和置信值。
//繪制預(yù)測(cè)邊界框 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;//邊框上的類(lèi)別標(biāo)簽與置信度}//繪制邊界框上的標(biāo)簽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); }文件全部源碼在文末。
寫(xiě)好之后我們編譯執(zhí)行即可,關(guān)于 OpenCV 眾多頭文件包含、鏈接庫(kù)鏈接時(shí)、運(yùn)行時(shí)的鏈接,對(duì)初學(xué)者來(lái)說(shuō)可能會(huì)遇到一些問(wèn)題,可以參考:
Linux下編譯、鏈接、加載運(yùn)行C++ OpenCV的兩種方式及常見(jiàn)問(wèn)題的解決
Linux下C/C++程序編譯鏈接加載過(guò)程中的常見(jiàn)問(wèn)題及解決方法
可以從左上角和標(biāo)準(zhǔn)輸出看到,每幀的檢測(cè)時(shí)間大概在 280ms,速度還可以,精度大體也不錯(cuò)。但是由于模型較小,性能受限,對(duì)于一些邊緣小物體會(huì)有誤差,如上圖中右側(cè)的小車(chē)。
樹(shù)莓派攝像頭實(shí)時(shí)檢測(cè)
樹(shù)莓派攝像頭調(diào)試參考:樹(shù)莓派攝像頭基礎(chǔ)配置及測(cè)試 。
在視頻文件的檢測(cè)順利完成后,實(shí)時(shí)樹(shù)莓派的檢測(cè)就很簡(jiǎn)單了,只需要將讀取輸入部分從視頻文件改為本地?cái)z像頭即可。
主要就是這一行修改:
// cv::VideoCapture cap("./video/demo.mp4"); // cv::VideoWriter video; // 改為 cv::VideoCapture cap(0);另外,我們需要設(shè)置一些 OpenCV 讀取攝像頭輸入的尺寸大小,否則筆者親測(cè)是有一些小bug:
cap.set(cv::CAP_PROP_FRAME_WIDTH, 640); cap.set(cv::CAP_PROP_FRAME_HEIGHT, 480);在筆者實(shí)驗(yàn)室中實(shí)測(cè),速度和精度也都還可以。
全部代碼
全部代碼可參考:https://github.com/Adenialzz/Hello-AIDeployment
如有錯(cuò)誤或遺漏,歡迎留言指正。
Ref:
https://blog.csdn.net/cuma2369/article/details/107666559
https://ryanadex.github.io/2019/01/15/opencv-yolov3/
總結(jié)
以上是生活随笔為你收集整理的树莓派摄像头 C++ OpenCV YoloV3 实现实时目标检测的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 实施“最严国六”标准对汽车行业的影响有哪
- 下一篇: 依维柯柴油3.0发动机水温接近100度正