利用OpenCV实现对车流量的统计
此文在我的個(gè)人博客地址:https://sublimerui.top/archives/48ab4a6e.html
目錄
- 調(diào)試平臺(tái)
- 汽車(chē)識(shí)別原理——背景/前景分割算法
- 整體結(jié)構(gòu)
- 整體流程框架圖
- 主要參數(shù)
- 主要函數(shù)
- 測(cè)試結(jié)果
- 結(jié)論
- 附:程序源代碼
閑話少絮。開(kāi)始正題——OpenCV的車(chē)流量統(tǒng)計(jì)。
調(diào)試平臺(tái)
- OpenCV 4.2
- VS 2019
汽車(chē)識(shí)別原理——背景/前景分割算法
如今,檢測(cè)和提取車(chē)輛時(shí)候,常用的方法有MOG2算法和KNN算法。MOG算法是以高斯混合模型(GMM)為基礎(chǔ)的背景/前景分割算法。它是以2004年和2006年Z.Zivkovic的兩篇文章為基礎(chǔ)的。這個(gè)算法的一個(gè)特點(diǎn)是它為每一個(gè)像素選擇一個(gè)合適數(shù)目的高斯分布。其主要原理為:在一個(gè)固定位置和角度固定的視頻或圖像中,提取分割圖像或視頻中運(yùn)動(dòng)的成分。此算法使用背景建模的方式,將整張圖片或一幀視頻分為前景和后景。此算法運(yùn)行時(shí),會(huì)將動(dòng)態(tài)的前景與靜止的后景相減,得出結(jié)果即為徐提取的運(yùn)動(dòng)物體的圖像。
K最近鄰算法(KNN)是屬于機(jī)器學(xué)習(xí)的一種算法。其主要原理為:給定一個(gè)已訓(xùn)練數(shù)據(jù)集,對(duì)新的輸入實(shí)例,在訓(xùn)練數(shù)據(jù)集中找到與該實(shí)例最鄰近(注:衡量鄰近的標(biāo)準(zhǔn)以具體選取的某個(gè)特征而言,例如下面示意圖中使用的特征為歐式距離)的K個(gè)實(shí)例,這K個(gè)實(shí)例的多數(shù)屬于某個(gè)類(lèi),則判定該輸入實(shí)例同屬此類(lèi)。如下圖所示:訓(xùn)練者取k值,計(jì)算以歐氏距離k為半徑的圓內(nèi)其他類(lèi)別的個(gè)數(shù),圖中中心小紅點(diǎn)以k為半徑的圓內(nèi)三角形個(gè)數(shù)最多,則判定中心小紅點(diǎn)為三角形。
整體結(jié)構(gòu)
整體流程框架圖
本程序的主要運(yùn)行流程為:程序運(yùn)行開(kāi)始,首先從文件中獲取上次保存的光流量檢測(cè)矩形框數(shù)據(jù)(頂點(diǎn)坐標(biāo)和矩形的長(zhǎng)寬);其后,分別初始化背景提取對(duì)象,使用MOG2和KNN兩種算法。與此同時(shí),建立一個(gè)鼠標(biāo)回調(diào)函數(shù),用于捕獲鼠標(biāo)左鍵(繪制矩形框)、中鍵(取消操作)和右鍵(保存矩形框數(shù)據(jù)到文件)的操作。
此后,程序進(jìn)入主循環(huán)狀態(tài)。程序循環(huán)從視頻中獲取一幀的圖像,先進(jìn)行壓縮處理,以提高后續(xù)運(yùn)算速度。之后,將這一幀圖片從RGB轉(zhuǎn)為灰度圖片。為了祛除灰度后可能出現(xiàn)的小毛刺雜點(diǎn),再進(jìn)行平滑濾波處理。此后,分別通過(guò)MOG2和KNN算法提取前景,并將提取后的視頻顯示出來(lái)。與此同時(shí),獲取每個(gè)矩形框中積分后的亮度和。
最后,兩個(gè)算法中,分別將實(shí)時(shí)得到的亮度和與所預(yù)設(shè)的閾值進(jìn)行比較,當(dāng)滿足條件后,便認(rèn)為一輛汽車(chē)通過(guò)矩形框,使得計(jì)數(shù)器加一。如此重復(fù),統(tǒng)計(jì)整個(gè)視頻中的車(chē)流量。
主要參數(shù)
此參數(shù)為預(yù)設(shè)的亮度閾值。確定是否有汽車(chē)經(jīng)過(guò)檢測(cè)框中,其需要聯(lián)合上一幀積分亮度和本次積分亮度后綜合做出決定。
此參數(shù)可以?xún)?chǔ)存一幀視頻縮小后的大小。
此參數(shù)用于儲(chǔ)存最終顯示的圖像矩陣。
主要函數(shù)
此函數(shù)主要用于檢測(cè)鼠標(biāo)左鍵、滑輪(中鍵)和右鍵的一些操作,用于繪制矩形檢測(cè)框。
此函數(shù)主要用于縮小原視頻比例,提高計(jì)算機(jī)運(yùn)算速度。
apply函數(shù)主要用于兩種算法的前景提取。此后,前景提取后的這幀視頻保存于bgMOG2和bgKNN之中。
此函數(shù)主要用于前景提取后在其上面繪制矩形框。
計(jì)算車(chē)道矩形框亮度積分圖。
此函數(shù)可以按照要求,在視頻圖像上顯示文字和統(tǒng)計(jì)數(shù)字。
測(cè)試結(jié)果
進(jìn)行車(chē)流量統(tǒng)計(jì)前,應(yīng)當(dāng)首先根據(jù)矩形框中出現(xiàn)車(chē)輛時(shí),估算其亮度平均值作為亮度閾值,使其設(shè)定為一個(gè)較為合適的值,增強(qiáng)檢測(cè)的靈敏性。由下圖可知,當(dāng)程序運(yùn)行后,通過(guò)手動(dòng)標(biāo)記車(chē)道檢測(cè)區(qū)域,得到2個(gè)矩形檢測(cè)區(qū)。每次獲得矩形中的積分亮度結(jié)果,當(dāng)上從亮度結(jié)果大于閾值,且此次亮度結(jié)果小于閾值時(shí),使車(chē)輛統(tǒng)計(jì)結(jié)果加一。
由測(cè)試結(jié)果可知,相較于MOG2算法,KNN算法在運(yùn)算處理上花費(fèi)更多的時(shí)間,KNN算法時(shí)間約為MOG2時(shí)間的2倍。同時(shí),算法處理總時(shí)間也相對(duì)較長(zhǎng),除了兩個(gè)算法所帶來(lái)的開(kāi)銷(xiāo)外,仍有其他附加代碼所花費(fèi)的時(shí)間。
程序中,可以使用cv::resize();函數(shù)進(jìn)行圖片的壓縮,縮放圖片對(duì)于檢測(cè)速度有較大的影響。不使用縮放時(shí),將視頻中原始一幀的圖像進(jìn)行計(jì)算,經(jīng)測(cè)試發(fā)現(xiàn),處理速度很慢,顯示的圖片有明顯的脫幀和卡頓現(xiàn)象,CPU占用率相當(dāng)高,與壓縮后(上圖)相比,無(wú)論使用MOG2還是KNN算法,其處理時(shí)間均成倍增加。下圖為不使用縮小圖片尺寸條件下的處理時(shí)間。
閾值設(shè)置的合理性也是車(chē)流量檢測(cè)準(zhǔn)確性的一個(gè)重要指標(biāo)。過(guò)高或過(guò)低的閾值均不能很好地反映車(chē)輛的經(jīng)過(guò)和實(shí)現(xiàn)的統(tǒng)計(jì)。閾值過(guò)低,將會(huì)把視頻中環(huán)境干擾噪聲和其他運(yùn)動(dòng)對(duì)象(如三輪車(chē)和行人)當(dāng)做汽車(chē)統(tǒng)計(jì),使得統(tǒng)計(jì)結(jié)果偏大;同理,閾值過(guò)高,將很難檢測(cè)到車(chē)輛的通過(guò),當(dāng)車(chē)輛進(jìn)過(guò)矩形檢測(cè)區(qū)時(shí),無(wú)法實(shí)現(xiàn)車(chē)輛的統(tǒng)計(jì)。本程序中,經(jīng)過(guò)檢測(cè)矩形框內(nèi)平均值的大致估算,將閾值detectTHD設(shè)置為900000。
下圖中顯示了當(dāng)閾值設(shè)定過(guò)小時(shí)的狀況。右端矩形框(右車(chē)道)經(jīng)過(guò)了一輛電動(dòng)車(chē),程序誤認(rèn)為汽車(chē),并錯(cuò)誤地將統(tǒng)計(jì)結(jié)果L1的值從1加為2。
同理,當(dāng)閾值設(shè)置過(guò)大,也會(huì)造成統(tǒng)計(jì)的不準(zhǔn)確。下圖顯示了當(dāng)閾值設(shè)定過(guò)大時(shí)的狀況。可以發(fā)現(xiàn),即使是車(chē)輛經(jīng)過(guò)了矩形框,車(chē)輛統(tǒng)計(jì)變量L0和L1仍為0。
結(jié)論
由以上分析可知,汽車(chē)流量的統(tǒng)計(jì)可以借助設(shè)定矩形區(qū)域內(nèi)的亮度閾值來(lái)確定。為識(shí)別運(yùn)動(dòng)對(duì)象(汽車(chē))的狀態(tài),可使用背景提取算法,如本軟件中使用到的MOG2和KNN算法。通過(guò)比較不同算法間的處理時(shí)間,我們應(yīng)當(dāng)合理選擇一種耗時(shí)短且提取車(chē)輛準(zhǔn)確性高的一種算法。此外,識(shí)別統(tǒng)計(jì)車(chē)流量較為重要的一環(huán)便是設(shè)置合理的亮度閾值,亮度閾值設(shè)置的合理性直接關(guān)系到車(chē)流量統(tǒng)計(jì)準(zhǔn)確性。
附:程序源代碼
此項(xiàng)目Github地址: https://github.com/cwxyr/traffic-detection
#include "stdafx.h" #include <Windows.h> #include <string> #include <opencv2/core.hpp> #include <opencv2/imgproc.hpp> #include <opencv2/highgui.hpp> #include <opencv2/features2d.hpp> #include <opencv2/video.hpp>#ifdef _DEBUG #pragma comment(lib, "opencv_world420d.lib") #else #pragma comment(lib, "opencv_world420.lib") #endif//===【鼠標(biāo)事件回調(diào)函數(shù)】=== int detectTHD = 900000; //亮度閾值:有車(chē)輛經(jīng)過(guò)的; std::vector<int> myLanneLightSum_Last; //車(chē)道亮度和:上一幀的 std::vector<int> myLanneVihicleCnt; //車(chē)道車(chē)輛計(jì)數(shù)器std::vector<cv::Rect> myLanneRect; //車(chē)道矩形框;顯示為紅色; std::vector<cv::Point> myMousePoints; //鼠標(biāo)點(diǎn)向量;顯示為藍(lán)色; int myMouseEventBusy = 0; //鼠標(biāo)回調(diào)事件忙:簡(jiǎn)單的資源鎖 static void onMouse(int event, int x, int y, int flags, void*) {myMouseEventBusy = 1;cv::Point mPoint;cv::Rect mRect;switch (event){case cv::EVENT_LBUTTONDOWN: //左鍵按下:增加myMousePoints中的點(diǎn)數(shù)mPoint = cv::Point(x, y);myMousePoints.push_back(mPoint); //將當(dāng)前鼠標(biāo)點(diǎn)推送到向量中;if (myMousePoints.size() > 4)myMousePoints.erase(myMousePoints.begin()); //保證myMousePoints向量中節(jié)點(diǎn)數(shù)不大于4;break;case cv::EVENT_RBUTTONDOWN: //右鍵鍵按下:將myMousePoints中的4個(gè)點(diǎn)推送到矩形框向量myLanneRectif (myMousePoints.size() == 4){int Xmin = 100000; int Ymin = 100000;int Xmax = 0; int Ymax = 0;for (int k = 0; k < 4; k++){Xmin = std::min(Xmin, myMousePoints.at(k).x);Ymin = std::min(Ymin, myMousePoints.at(k).y);Xmax = std::max(Xmax, myMousePoints.at(k).x);Ymax = std::max(Ymax, myMousePoints.at(k).y);}//for k <<< === 用四個(gè)點(diǎn)構(gòu)成矩形框的參數(shù)mRect = cv::Rect(Xmin, Ymin, Xmax - Xmin, Ymax - Ymin); //構(gòu)成矩形框myLanneRect.push_back(mRect);myLanneLightSum_Last.push_back(0);myLanneVihicleCnt.push_back(0);myMousePoints.clear(); //清除鼠標(biāo)點(diǎn)向量}///ifbreak;case cv::EVENT_MBUTTONDOWN: //中間鍵鍵按下:刪除myMousePoints中的一個(gè)點(diǎn);myMousePoints為空時(shí),刪除myLanneRect中的節(jié)點(diǎn);printf("EVENT_MBUTTONDOWN\n");if (myMousePoints.size() > 0)myMousePoints.pop_back();else{myLanneRect.pop_back();myLanneLightSum_Last.pop_back();myLanneVihicleCnt.pop_back();}break;}/switch myMouseEventBusy = 0; return; }int main(int argc, char* argv[]) {char errorMSG[256];char curPathName[384] = ""; char curModulerPath[384] = "";GetCurrentDirectory(383, curModulerPath); printf("Line39: curModulerPath = %s\n", curModulerPath);//=======讀取標(biāo)記的矩形框文件內(nèi)容到myLanneRect:=======#ifndef READ_RECT_FILEFILE *pFILE = fopen("MarkRect.txt", "r");if (pFILE != NULL){cv::Rect mRect;while (fgets(errorMSG, 255, pFILE) != NULL){int rtn = sscanf(errorMSG, "%d %d %d %d", &mRect.x, &mRect.y, &mRect.width, &mRect.height);if (rtn == 4) {myLanneRect.push_back(mRect);myLanneLightSum_Last.push_back(0);myLanneVihicleCnt.push_back(0);}}fclose(pFILE);}///if#endif // !READ_RECT_FILEstd::string imgName = "video-02.mp4";char FilePath[384];if (strlen(curPathName) > 0)sprintf(FilePath, "%s\\%s", curPathName, imgName.c_str()); //圖片文件路徑elsesprintf(FilePath, "%s", imgName.c_str()); //圖片文件路徑//==【01】== 打開(kāi)視頻文件或攝像頭cv::VideoCapture cap; //VideoCapture類(lèi)實(shí)例化,使用缺省攝像頭if (0 && "UsingCam")cap.open(0);elsecap.open(FilePath);if (!cap.isOpened()) // check if we succeeded{printf("error#73: 打開(kāi)設(shè)備或文件失敗,檢查是否存在!回車(chē)退出!\n路徑=%s\n", FilePath);fgets(FilePath, 127, stdin);return -1;}cv::Mat frame, newframe, greyFrame, floatFrame, lastFrame, frame2, mog2RES, KNN, out_frame, avgFrame;std::vector<cv::Mat> diffIMGvec;//==【02】== 創(chuàng)建運(yùn)動(dòng)視頻背景提取對(duì)象:用于分離背景和運(yùn)動(dòng)對(duì)象cv::Ptr<cv::BackgroundSubtractorMOG2> bgMOG2 = cv::createBackgroundSubtractorMOG2();cv::Ptr<cv::BackgroundSubtractorKNN> bgKNN = cv::createBackgroundSubtractorKNN();bgMOG2->setVarThreshold(30);bool update_bg_model = true;//==【03】== 命名幾個(gè)顯示窗口cv::namedWindow("RawWnd", cv::WINDOW_NORMAL);cv::setMouseCallback("RawWnd", onMouse, &newframe); //設(shè)置鼠標(biāo)事件回調(diào)函數(shù)("RawWnd"窗口的):同時(shí)傳遞彩色圖像指針;cv::namedWindow("Out_KNN", cv::WINDOW_NORMAL);cv::namedWindow("Out_MOG2", cv::WINDOW_NORMAL);int frameNums = 0;for (;;){frame.rows = 0;double t1 = (double)cv::getCPUTickCount(); //開(kāi)始統(tǒng)計(jì)時(shí)間cap.read(frame);if (frame.rows == 0)break;cv::Size newSize(frame.cols / 2, frame.rows / 2); //壓縮圖像,將其尺寸縮小cv::resize(frame, newframe, newSize);cv::cvtColor(newframe, greyFrame, cv::COLOR_RGB2GRAY); //轉(zhuǎn)換為灰度圖cv::blur(greyFrame, greyFrame, cv::Size(3, 3)); //使用平滑運(yùn)算double t2 = (double)cv::getCPUTickCount();bgMOG2->apply(greyFrame, mog2RES, update_bg_model ? -1 : 0); //使用MOG2算法提取前景double t3 = (double)cv::getCPUTickCount(); //獲取處理時(shí)間double t4 = (double)cv::getCPUTickCount();bgKNN->apply(greyFrame, KNN, update_bg_model ? -1 : 0); //使用KNN算法提取前景double t5 = (double)cv::getCPUTickCount(); //獲取處理時(shí)間printf("MOG2 Time = %.3fms\n", 1e0 * (t3 - t2) / (double)cv::getTickFrequency());printf("KNN Time = %.3fms\n", 1e0 * (t5 - t4) / (double)cv::getTickFrequency());printf("Total Time = %.3fms\n", 1e0 * (t5 - t1) / (double)cv::getTickFrequency());printf("--------------------\n");if (!mog2RES.empty()) //計(jì)算MOG2算法下矩形框的積分亮度值{cv::Mat showMat;mog2RES.copyTo(showMat);if (myMouseEventBusy == 0){for (int k = 0; k < myLanneRect.size(); k++){cv::rectangle(showMat, myLanneRect.at(k), cv::Scalar(255, 255, 255), 3);cv::Mat subMat = mog2RES(myLanneRect.at(k)); //再M(fèi)OG2的前景提取結(jié)果中,取車(chē)道標(biāo)記矩形框區(qū)域?yàn)閟ubMat矩陣cv::Mat sumMat; //積分圖 == subMat的積分矩陣cv::integral(subMat, sumMat, CV_32S); //設(shè)置積分矩陣的數(shù)據(jù)類(lèi)型為uint;int sumValue = (int)sumMat.at<int>((int)sumMat.rows - 1, (int)sumMat.cols - 1); //獲取積分圖右下角的值,就是矩形框內(nèi)亮度和;sprintf(errorMSG, "sum = %d;", sumValue);cv::putText(showMat, errorMSG, cv::Point(myLanneRect.at(k).x, myLanneRect.at(k).y + 4), 0.2, 1, cv::Scalar(255, 0, 0), 2);//顯示矩形框內(nèi)的亮度和;}//for k}ifcv::imshow("Out_MOG2", showMat);}if (!KNN.empty()) //計(jì)算KNN算法下矩形框的積分亮度值{cv::Mat showMat;KNN.copyTo(showMat);if (myMouseEventBusy == 0){for (int k = 0; k < myLanneRect.size(); k++){cv::rectangle(showMat, myLanneRect.at(k), cv::Scalar(255, 255, 255), 3);cv::Mat subMat = KNN(myLanneRect.at(k)); //再KNN的前景提取結(jié)果中,取車(chē)道標(biāo)記矩形框區(qū)域?yàn)閟ubMat矩陣cv::Mat sumMat; //積分圖 == subMat的積分矩陣cv::integral(subMat, sumMat, CV_32S); //設(shè)置積分矩陣的數(shù)據(jù)類(lèi)型為uint;int sumValue = (int)sumMat.at<int>((int)sumMat.rows - 1, (int)sumMat.cols - 1); //獲取積分圖右下角的值,就是矩形框內(nèi)亮度和;sprintf(errorMSG, "sum = %d;", sumValue);cv::putText(showMat, errorMSG, cv::Point(myLanneRect.at(k).x, myLanneRect.at(k).y + 4), 0.2, 1, cv::Scalar(255, 0, 0), 2);//顯示矩形框內(nèi)的亮度和;}//for k}ifimshow("Out_KNN", showMat);} //===>>> 顯示原始圖像:顯示車(chē)道標(biāo)記信息 + 矩形框內(nèi)亮度和 + 車(chē)流量統(tǒng)計(jì)#ifndef SHOW_RAW_MATcv::Mat showMat;newframe.copyTo(showMat); //矩陣復(fù)制sprintf(errorMSG, "mL=add Point; mR=add Rect; mM=delete Point;");cv::putText(showMat, errorMSG, cv::Point(8, 32), 0.2, 1, cv::Scalar(255, 0, 0), 2);//顯示提示信息;//==>> 顯示車(chē)道矩形框?yàn)榧t色 + 車(chē)流量統(tǒng)計(jì) + 車(chē)流量顯示if (myMouseEventBusy == 0){for (int k = 0; k < myLanneRect.size(); k++){cv::rectangle(showMat, myLanneRect.at(k), cv::Scalar(0, 0, 255), 3);cv::Mat subMat = mog2RES(myLanneRect.at(k)); //再M(fèi)OG2的前景提取結(jié)果中,取車(chē)道標(biāo)記矩形框區(qū)域?yàn)閟ubMat矩陣cv::Mat sumMat; //積分圖 == subMat的積分矩陣cv::integral(subMat, sumMat, CV_32S); //設(shè)置積分矩陣的數(shù)據(jù)類(lèi)型為int,計(jì)算車(chē)道矩形框內(nèi)亮度積分圖;int sumValue = (int)sumMat.at<int>((int)sumMat.rows - 1, (int)sumMat.cols - 1); //獲取積分圖右下角的值,就是矩形框內(nèi)亮度和;sprintf(errorMSG, "sum = %d;", sumValue);cv::putText(showMat, errorMSG, cv::Point(myLanneRect.at(k).x, myLanneRect.at(k).y + 4), 0.2, 1, cv::Scalar(255, 255, 0), 2);//顯示矩形框內(nèi)的亮度和;//===>>> 車(chē)流量統(tǒng)計(jì):if (myLanneLightSum_Last.at(k) > detectTHD && sumValue <= detectTHD){//:: 車(chē)輛通過(guò)了矩形框:上一幀亮度和大于閾值,本幀亮度和小于閾值;車(chē)輛計(jì)數(shù)器自加;myLanneVihicleCnt.at(k)++;myLanneLightSum_Last.at(k) = sumValue;}else myLanneLightSum_Last.at(k) = sumValue; //存儲(chǔ)當(dāng)前亮度和到myLanneLightSum_Last }//for k//===>> 車(chē)流量統(tǒng)計(jì)結(jié)果顯示cv::Mat topareaMat = showMat(cv::Rect(0, 0, showMat.cols, 75)); //最頂部48行置0;topareaMat *= 255;std::string strVihicleCnt = "VihicleCnt: ";for (int k = 0; k < myLanneRect.size(); k++){sprintf(errorMSG, "L%d = %d;", k, myLanneVihicleCnt.at(k));strVihicleCnt += errorMSG;}cv::putText(showMat, strVihicleCnt.c_str(), cv::Point(8, 64), 0.2, 1, cv::Scalar(0, 0, 255), 2); //流量統(tǒng)計(jì)顯示到彩色圖片上}if//==>> 顯示正在標(biāo)記的坐標(biāo)點(diǎn)為藍(lán)色:if (myMouseEventBusy == 0){for (int k = 1; k < myMousePoints.size(); k++){cv::line(showMat, myMousePoints.at(k - 1), myMousePoints.at(k), cv::Scalar(255, 0, 0), 15);}//for kif(myMousePoints.size() == 4)cv::line(showMat, myMousePoints.at(0), myMousePoints.at(3), cv::Scalar(255, 0, 0), 2);}ifimshow("RawWnd", showMat);#endif // SHOW_RAW_MATint keycode = cv::waitKey(100); //等待100msif (keycode == 'q')break;else if (keycode == ' '){update_bg_model = !update_bg_model;printf("Learn background is in state = %d\n", update_bg_model);}else if (keycode == 'w'){//寫(xiě)文件:記錄標(biāo)記的矩形框到文件中:#ifndef WRITE_RECT_FILEFILE *pFILE = fopen("MarkRect.txt", "w");if (pFILE != NULL){for (int k = 0; k < myLanneRect.size(); k++) {fprintf(pFILE, "%d %d %d %d\n", myLanneRect.at(k).x, myLanneRect.at(k).y, myLanneRect.at(k).width, myLanneRect.at(k).height);}fclose(pFILE);}///if#endif // !WRITE_RECT_FILE}frameNums++;Sleep(50);}//for cap.release();return 0; }總結(jié)
以上是生活随笔為你收集整理的利用OpenCV实现对车流量的统计的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 前端学习(1851)vue之电商管理系统
- 下一篇: excel首行空不能导入access_E