利用OpenCV实现对车流量的统计
此文在我的個人博客地址:https://sublimerui.top/archives/48ab4a6e.html
目錄
- 調試平臺
- 汽車識別原理——背景/前景分割算法
- 整體結構
- 整體流程框架圖
- 主要參數
- 主要函數
- 測試結果
- 結論
- 附:程序源代碼
閑話少絮。開始正題——OpenCV的車流量統計。
調試平臺
- OpenCV 4.2
- VS 2019
汽車識別原理——背景/前景分割算法
如今,檢測和提取車輛時候,常用的方法有MOG2算法和KNN算法。MOG算法是以高斯混合模型(GMM)為基礎的背景/前景分割算法。它是以2004年和2006年Z.Zivkovic的兩篇文章為基礎的。這個算法的一個特點是它為每一個像素選擇一個合適數目的高斯分布。其主要原理為:在一個固定位置和角度固定的視頻或圖像中,提取分割圖像或視頻中運動的成分。此算法使用背景建模的方式,將整張圖片或一幀視頻分為前景和后景。此算法運行時,會將動態的前景與靜止的后景相減,得出結果即為徐提取的運動物體的圖像。
K最近鄰算法(KNN)是屬于機器學習的一種算法。其主要原理為:給定一個已訓練數據集,對新的輸入實例,在訓練數據集中找到與該實例最鄰近(注:衡量鄰近的標準以具體選取的某個特征而言,例如下面示意圖中使用的特征為歐式距離)的K個實例,這K個實例的多數屬于某個類,則判定該輸入實例同屬此類。如下圖所示:訓練者取k值,計算以歐氏距離k為半徑的圓內其他類別的個數,圖中中心小紅點以k為半徑的圓內三角形個數最多,則判定中心小紅點為三角形。
整體結構
整體流程框架圖
本程序的主要運行流程為:程序運行開始,首先從文件中獲取上次保存的光流量檢測矩形框數據(頂點坐標和矩形的長寬);其后,分別初始化背景提取對象,使用MOG2和KNN兩種算法。與此同時,建立一個鼠標回調函數,用于捕獲鼠標左鍵(繪制矩形框)、中鍵(取消操作)和右鍵(保存矩形框數據到文件)的操作。
此后,程序進入主循環狀態。程序循環從視頻中獲取一幀的圖像,先進行壓縮處理,以提高后續運算速度。之后,將這一幀圖片從RGB轉為灰度圖片。為了祛除灰度后可能出現的小毛刺雜點,再進行平滑濾波處理。此后,分別通過MOG2和KNN算法提取前景,并將提取后的視頻顯示出來。與此同時,獲取每個矩形框中積分后的亮度和。
最后,兩個算法中,分別將實時得到的亮度和與所預設的閾值進行比較,當滿足條件后,便認為一輛汽車通過矩形框,使得計數器加一。如此重復,統計整個視頻中的車流量。
主要參數
此參數為預設的亮度閾值。確定是否有汽車經過檢測框中,其需要聯合上一幀積分亮度和本次積分亮度后綜合做出決定。
此參數可以儲存一幀視頻縮小后的大小。
此參數用于儲存最終顯示的圖像矩陣。
主要函數
此函數主要用于檢測鼠標左鍵、滑輪(中鍵)和右鍵的一些操作,用于繪制矩形檢測框。
此函數主要用于縮小原視頻比例,提高計算機運算速度。
apply函數主要用于兩種算法的前景提取。此后,前景提取后的這幀視頻保存于bgMOG2和bgKNN之中。
此函數主要用于前景提取后在其上面繪制矩形框。
計算車道矩形框亮度積分圖。
此函數可以按照要求,在視頻圖像上顯示文字和統計數字。
測試結果
進行車流量統計前,應當首先根據矩形框中出現車輛時,估算其亮度平均值作為亮度閾值,使其設定為一個較為合適的值,增強檢測的靈敏性。由下圖可知,當程序運行后,通過手動標記車道檢測區域,得到2個矩形檢測區。每次獲得矩形中的積分亮度結果,當上從亮度結果大于閾值,且此次亮度結果小于閾值時,使車輛統計結果加一。
由測試結果可知,相較于MOG2算法,KNN算法在運算處理上花費更多的時間,KNN算法時間約為MOG2時間的2倍。同時,算法處理總時間也相對較長,除了兩個算法所帶來的開銷外,仍有其他附加代碼所花費的時間。
程序中,可以使用cv::resize();函數進行圖片的壓縮,縮放圖片對于檢測速度有較大的影響。不使用縮放時,將視頻中原始一幀的圖像進行計算,經測試發現,處理速度很慢,顯示的圖片有明顯的脫幀和卡頓現象,CPU占用率相當高,與壓縮后(上圖)相比,無論使用MOG2還是KNN算法,其處理時間均成倍增加。下圖為不使用縮小圖片尺寸條件下的處理時間。
閾值設置的合理性也是車流量檢測準確性的一個重要指標。過高或過低的閾值均不能很好地反映車輛的經過和實現的統計。閾值過低,將會把視頻中環境干擾噪聲和其他運動對象(如三輪車和行人)當做汽車統計,使得統計結果偏大;同理,閾值過高,將很難檢測到車輛的通過,當車輛進過矩形檢測區時,無法實現車輛的統計。本程序中,經過檢測矩形框內平均值的大致估算,將閾值detectTHD設置為900000。
下圖中顯示了當閾值設定過小時的狀況。右端矩形框(右車道)經過了一輛電動車,程序誤認為汽車,并錯誤地將統計結果L1的值從1加為2。
同理,當閾值設置過大,也會造成統計的不準確。下圖顯示了當閾值設定過大時的狀況。可以發現,即使是車輛經過了矩形框,車輛統計變量L0和L1仍為0。
結論
由以上分析可知,汽車流量的統計可以借助設定矩形區域內的亮度閾值來確定。為識別運動對象(汽車)的狀態,可使用背景提取算法,如本軟件中使用到的MOG2和KNN算法。通過比較不同算法間的處理時間,我們應當合理選擇一種耗時短且提取車輛準確性高的一種算法。此外,識別統計車流量較為重要的一環便是設置合理的亮度閾值,亮度閾值設置的合理性直接關系到車流量統計準確性。
附:程序源代碼
此項目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//===【鼠標事件回調函數】=== int detectTHD = 900000; //亮度閾值:有車輛經過的; std::vector<int> myLanneLightSum_Last; //車道亮度和:上一幀的 std::vector<int> myLanneVihicleCnt; //車道車輛計數器std::vector<cv::Rect> myLanneRect; //車道矩形框;顯示為紅色; std::vector<cv::Point> myMousePoints; //鼠標點向量;顯示為藍色; int myMouseEventBusy = 0; //鼠標回調事件忙:簡單的資源鎖 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中的點數mPoint = cv::Point(x, y);myMousePoints.push_back(mPoint); //將當前鼠標點推送到向量中;if (myMousePoints.size() > 4)myMousePoints.erase(myMousePoints.begin()); //保證myMousePoints向量中節點數不大于4;break;case cv::EVENT_RBUTTONDOWN: //右鍵鍵按下:將myMousePoints中的4個點推送到矩形框向量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 <<< === 用四個點構成矩形框的參數mRect = cv::Rect(Xmin, Ymin, Xmax - Xmin, Ymax - Ymin); //構成矩形框myLanneRect.push_back(mRect);myLanneLightSum_Last.push_back(0);myLanneVihicleCnt.push_back(0);myMousePoints.clear(); //清除鼠標點向量}///ifbreak;case cv::EVENT_MBUTTONDOWN: //中間鍵鍵按下:刪除myMousePoints中的一個點;myMousePoints為空時,刪除myLanneRect中的節點;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);//=======讀取標記的矩形框文件內容到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】== 打開視頻文件或攝像頭cv::VideoCapture cap; //VideoCapture類實例化,使用缺省攝像頭if (0 && "UsingCam")cap.open(0);elsecap.open(FilePath);if (!cap.isOpened()) // check if we succeeded{printf("error#73: 打開設備或文件失敗,檢查是否存在!回車退出!\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】== 創建運動視頻背景提取對象:用于分離背景和運動對象cv::Ptr<cv::BackgroundSubtractorMOG2> bgMOG2 = cv::createBackgroundSubtractorMOG2();cv::Ptr<cv::BackgroundSubtractorKNN> bgKNN = cv::createBackgroundSubtractorKNN();bgMOG2->setVarThreshold(30);bool update_bg_model = true;//==【03】== 命名幾個顯示窗口cv::namedWindow("RawWnd", cv::WINDOW_NORMAL);cv::setMouseCallback("RawWnd", onMouse, &newframe); //設置鼠標事件回調函數("RawWnd"窗口的):同時傳遞彩色圖像指針;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(); //開始統計時間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); //轉換為灰度圖cv::blur(greyFrame, greyFrame, cv::Size(3, 3)); //使用平滑運算double t2 = (double)cv::getCPUTickCount();bgMOG2->apply(greyFrame, mog2RES, update_bg_model ? -1 : 0); //使用MOG2算法提取前景double t3 = (double)cv::getCPUTickCount(); //獲取處理時間double t4 = (double)cv::getCPUTickCount();bgKNN->apply(greyFrame, KNN, update_bg_model ? -1 : 0); //使用KNN算法提取前景double t5 = (double)cv::getCPUTickCount(); //獲取處理時間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()) //計算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)); //再MOG2的前景提取結果中,取車道標記矩形框區域為subMat矩陣cv::Mat sumMat; //積分圖 == subMat的積分矩陣cv::integral(subMat, sumMat, CV_32S); //設置積分矩陣的數據類型為uint;int sumValue = (int)sumMat.at<int>((int)sumMat.rows - 1, (int)sumMat.cols - 1); //獲取積分圖右下角的值,就是矩形框內亮度和;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);//顯示矩形框內的亮度和;}//for k}ifcv::imshow("Out_MOG2", showMat);}if (!KNN.empty()) //計算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的前景提取結果中,取車道標記矩形框區域為subMat矩陣cv::Mat sumMat; //積分圖 == subMat的積分矩陣cv::integral(subMat, sumMat, CV_32S); //設置積分矩陣的數據類型為uint;int sumValue = (int)sumMat.at<int>((int)sumMat.rows - 1, (int)sumMat.cols - 1); //獲取積分圖右下角的值,就是矩形框內亮度和;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);//顯示矩形框內的亮度和;}//for k}ifimshow("Out_KNN", showMat);} //===>>> 顯示原始圖像:顯示車道標記信息 + 矩形框內亮度和 + 車流量統計#ifndef SHOW_RAW_MATcv::Mat showMat;newframe.copyTo(showMat); //矩陣復制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);//顯示提示信息;//==>> 顯示車道矩形框為紅色 + 車流量統計 + 車流量顯示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)); //再MOG2的前景提取結果中,取車道標記矩形框區域為subMat矩陣cv::Mat sumMat; //積分圖 == subMat的積分矩陣cv::integral(subMat, sumMat, CV_32S); //設置積分矩陣的數據類型為int,計算車道矩形框內亮度積分圖;int sumValue = (int)sumMat.at<int>((int)sumMat.rows - 1, (int)sumMat.cols - 1); //獲取積分圖右下角的值,就是矩形框內亮度和;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);//顯示矩形框內的亮度和;//===>>> 車流量統計:if (myLanneLightSum_Last.at(k) > detectTHD && sumValue <= detectTHD){//:: 車輛通過了矩形框:上一幀亮度和大于閾值,本幀亮度和小于閾值;車輛計數器自加;myLanneVihicleCnt.at(k)++;myLanneLightSum_Last.at(k) = sumValue;}else myLanneLightSum_Last.at(k) = sumValue; //存儲當前亮度和到myLanneLightSum_Last }//for k//===>> 車流量統計結果顯示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); //流量統計顯示到彩色圖片上}if//==>> 顯示正在標記的坐標點為藍色: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'){//寫文件:記錄標記的矩形框到文件中:#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; }總結
以上是生活随笔為你收集整理的利用OpenCV实现对车流量的统计的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 前端学习(1851)vue之电商管理系统
- 下一篇: excel首行空不能导入access_E