C++基於OpenCV實現實時監控和運動檢測記錄
基於OpenCV實現實時監控並通過運動檢測記錄視訊
一、課程介紹
1. 課程來源
課程使用的作業系統為 Ubuntu 14.04
,OpenCV 版本為OpenCV 2.4.13.1
,你可以在這裡檢視該版本 OpenCV 的文件。官方文件中有兩個例子可以幫助你理解此課程,分別是
你可以在我的 Github倉庫 上找到 Windows 系統對應的 Visual Studio 工程。全部程式碼檔案也可以在我的倉庫中找到。
這裡提供了完整的程式碼 http://labfile.oss.aliyuncs.com/courses/671/monitor-recorder.zip 。
2. 內容簡介
- 課程實驗使用PC機自帶的攝像頭作為監視器進行實時監控。
- 對原始影象做一定處理,使監控人員或監控軟體更易發現監控中存在的問題。
- 當攝像頭捕捉到運動產生時自動記錄視訊。
3. 課程知識點
本課程專案完成過程中將學習:
- 對攝像頭資料的捕獲
- 對捕獲到的監控幀作背景處理
- 對監控視訊做運動檢測並記錄視訊
二、實驗環境
本實驗需要先在實驗平臺安裝 OpenCV ,需下載依賴的庫、原始碼並編譯安裝。安裝過程建議按照教程給出的步驟,或者你可以參考官方文件中 Linux 環境下的安裝步驟,但 有些選項需要變更。安裝過程所需時間會比較長,這期間你可以先閱讀接下來的教程,在大致瞭解程式碼原理後再親自編寫嘗試。
我提供了一個編譯好的
2.4.13-binary.tar.gz
包,你可以通過下面的命令下載並安裝,節省了編譯的時間,通過這個包安裝大概需要20~30分鐘,視實驗樓當前環境運轉速度而定。$ sudo apt-get update$ sudo apt-get install build-essential libgtk2.0-dev libjpeg-dev libtiff5-dev libjasper-dev libopenexr-dev cmake python-dev python-numpy python-tk libtbb-dev libeigen2-dev yasm libfaac-dev libopencore-amrnb-dev libopencore-amrwb-dev libtheora-dev libvorbis-dev libxvidcore-dev libx264-dev libqt4-dev libqt4-opengl-dev sphinx-common texlive-latex-extra libv4l-dev libdc1394-22-dev libavcodec-dev libavformat-dev libswscale-dev$ cd ~$ mkdir OpenCV && cd OpenCV$ wget http://labfile.oss.aliyuncs.com/courses/671/2.4.13-binary.tar.gz$ tar -zxvf 2.4.13-binary.tar.gz$ cd opencv-2.4.13$ cd build$ sudo make install
如果你想體驗編譯的整個過程,我也提供了一個一鍵安裝的指令碼檔案,你可以通過下面的命令嘗試。這個過程會非常漫長,約2小時,期間可能還需要你做一定的互動確認工作。
$ cd ~$ sudo apt-get update$ wget http://labfile.oss.aliyuncs.com/courses/671/opencv.sh$ sudo chmod 777 opencv.sh$ ./opencv.sh
如果你覺得有必要親自嘗試一下安裝的每一步,可以按照下面的命令逐條輸入執行,在實驗樓的環境中大概需要兩個小時。
$ sudo apt-get update$ sudo apt-get install build-essential libgtk2.0-dev libjpeg-dev libtiff5-dev libjasper-dev libopenexr-dev cmake python-dev python-numpy python-tk libtbb-dev libeigen2-dev yasm libfaac-dev libopencore-amrnb-dev libopencore-amrwb-dev libtheora-dev libvorbis-dev libxvidcore-dev libx264-dev libqt4-dev libqt4-opengl-dev sphinx-common texlive-latex-extra libv4l-dev libdc1394-22-dev libavcodec-dev libavformat-dev libswscale-dev$ wget https://github.com/Itseez/opencv/archive/2.4.13.zip$ unzip 2.4.13.zip$ cd 2.4.13$ mkdir release && cd release$ cmake -D WITH_TBB=ON -D BUILD_NEW_PYTHON_SUPPORT=ON -D WITH_V4L=ON -D INSTALL_C_EXAMPLES=ON -D INSTALL_PYTHON_EXAMPLES=ON -D BUILD_EXAMPLES=ON -D WITH_QT=ON -D WITH_GTK=ON -D WITH_OPENGL=ON ..$ sudo make$ sudo make install$ sudo gedit /etc/ld.so.conf.d/opencv.conf $ 輸入 /usr/local/lib,按 Ctrl + X 退出,退出時詢問是否儲存,按 Y 確認。$ sudo ldconfig -v$ sudo gedit /etc/bash.bashrc$ 在檔案末尾加入$ PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfigexport PKG_CONFIG_PATH按 Ctrl + X 退出,按 Y 確認儲存。
檢驗配置是否成功。將 OpenCV 自帶的例子(在目錄
PATH_TO_OPENCV/samples/C
下)執行檢測。如果成功,將顯示 lena 的臉部照片,同時圈出其面部。$ cd samples/C$ ./build_all.sh$ ./facedetect --cascade="/usr/local/share/OpenCV/haarcascades/haarcascade_frontalface_alt.xml" --scale=1.5 lena.jpg
三、實驗原理
實驗通過 OpenCV 提供的 API 完成大部分任務,首先捕獲攝像頭資料,之後對捕獲到的每一幀作背景減除處理,得出易於識別的影象,最後利用直方圖做實時影象和背景影象的對比,實現運動檢測並寫入視訊檔案。
四、實驗步驟
通過以下命令可下載專案原始碼,作為參照對比完成下面詳細步驟的學習。
wget http://labfile.oss.aliyuncs.com/courses/671/monitor-recorder.zipunzip monitor-recorder.zip
1.定義標頭檔案
工程檔案由一個頭檔案 monitor.hpp
和一個入口檔案 main.cpp
構成。首先在標頭檔案中定義將使用的庫和相關變數。
程式碼中使用到的 OpenCV 標頭檔案和 C++ 標頭檔案在標頭檔案 monitor.hpp
中宣告如下,其中 unistd.h
包含了 Linux 下的 sleep
函式,引數為睡眠的秒數。
#ifndef __MONITOR_H_#define __MONITOR_H_//opencv#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>#include <opencv2/video/background_segm.hpp>#include <opencv2/objdetect/objdetect.hpp>#include <opencv2/imgproc/imgproc.hpp>//C++#include <ctime>#include <iostream>#include <string>#include <cstdio>#include <unistd.h> #include <sstream>using namespace cv;using namespace std;// ...#endif __MONITOR_H_
2.設計 processCamera
函式
processCamera
負責完成主要功能,包括監控資料的獲取和處理。首先要了解 OpenCV 中提供的幾個 API。
CvCapture* cvCaptureFromCAM(int device)
: 此函式捕獲指定裝置的資料並返回一個cvCapture
型別指標,捕獲失敗時返回NULL
。cvCreateFileCapture(char * filepath)
: 從本地視訊讀入。CvVideoWriter * cvCreateVideoWriter(char * filepath, , fps, size, is_color)
: 新建一個視訊寫入物件,返回其指標,filepath
指定寫入視訊的路徑,fps
指定寫入視訊的幀速,size
指定寫入視訊的畫素大小,is_color
僅在windows下有效,指定寫入是否為彩色。double cvGetCaptureProperty(CvCapture* capture, int property_id)
: 獲取一個視訊流的某個特性,property_id
指定要獲取的特性名稱。IplImage* cvQueryFrame(CvCapture* capture)
: 從視訊流獲取幀。void cvCvtColor(const CvArr* src, CvArr* dst, int code)
: 按code
指定的模式將src
指向的幀轉換後寫入dst
指向的地址。void calcHist(const Mat* images, int nimages, const int* channels, InputArray mask, SparseMat& hist, int dims, const int* histSize, const float** ranges, bool uniform, bool accumulate)
: 為image
指向的幀計算直方圖,儲存在hist
中。void normalize(const SparseMat& src, SparseMat& dst, double alpha, int normType)
: 按指定模式將src
正常化並寫入dst
中。double compareHist(const SparseMat& H1, const SparseMat& H2, int method)
:按method
指定的方式比較兩個直方圖的相似程度。int cvWriteFrame(CvVideoWriter* writer, const IplImage* image)
: 向視訊寫入流寫入一幀。成功返回 1,否則返回 0。
瞭解這些API的基本功能後,梳理程式執行的步驟:
- 程式開始執行,啟動攝像頭並獲得資料流
- 進入迴圈:
- 捕獲一幀
- 是否為第一幀?是則記錄該幀作為監控區域的背景
- 將該幀做適當的變換,輸出到監視器中
- 分析該幀和背景幀的相似程度
- 相似程度是否低於閾值且當前沒有在記錄視訊?低於閾值開始記錄。
- 相似程度是否低於閾值且當前已經開始記錄視訊?低於閾值繼續記錄,否則停止記錄。
- 迴圈中程式的停止,通過接受外部中斷相應。
3.程式碼實現 processCamera
啟動攝像頭並獲得資料流,呼叫上面提到的
cvCaptureFromCAM
函式,預設攝像頭的device
為 0。void processCamera() {CvCapture *capture = cvCaptureFromCAM(0);if (!capture){ cerr << "Unable to open camera " << endl; exit(EXIT_FAILURE);}// TODO...} // end processCamera
進入迴圈,迴圈條件中使用到一個
keyboard
變數用於接收外部中斷,如果Esc
或者q
鍵被按下則退出迴圈。keyboard
通過 OpenCV 提供的waitKey()
函式獲得外部按鍵情況。在下面的迴圈中,每次先檢查視訊輸入流capture
是否為空,防止訪問違例記憶體。在capture
不為NULL
的情況下,從capture
讀取一幀。在退出 while迴圈後,要通過cvReleaseCapture
釋放此前申請的capture
。void processCamera() {// ... Mat frame; // current framewhile ((char)keyboard != 'q' && (char)keyboard != 27){ if (!capture) { cerr << "Unable to read camera" << endl; cerr << "Exiting..." << endl; exit(EXIT_FAILURE); } frame = cvQueryFrame(capture); // TODO... keyboard = waitKey(30);} // end WhilecvReleaseCapture(&capture);} // end processCamera
判斷當前幀是否為第一幀,通過一個bool型變數
backGroundFlag
來標識。若backGroundFlag
為true
表示當前幀為第一幀,則記錄該幀並將backGroundFlag
置為False
。此時程式碼如下。在當前幀為第一幀的情況下,我們不需要記錄該幀的真實資料,只需要記錄該幀對應的直方圖,這裡首先將RGB型別的影象轉為HSV格式,之後計算該幀的直方圖,儲存在base
中。void processCamera() {// ... bool backGroundFlag = true;Mat frame; // current frameMat HSV; // HSV formatMatND base; // histogramwhile ((char)keyboard != 'q' && (char)keyboard != 27){ if (!capture) { cerr << "Unable to read camera" << endl; cerr << "Exiting..." << endl; exit(EXIT_FAILURE); } frame = cvQueryFrame(capture); // set background if (backGroundFlag){ cvtColor(frame, HSV, CV_BGR2HSV); calcHist(&HSV, 1, channels, Mat(), base, 2, histSize, ranges, true, false); normalize(base, base, 0, 1, NORM_MINMAX, -1, Mat()); backGroundFlag = false; } // TODO... keyboard = waitKey(30);} // end WhilecvReleaseCapture(&capture);} // end processCamera
對當前幀做適當變換並輸出到監視器。此時程式碼如下。我們要實現的程式可以對原始影象做兩種背景減除處理,因此需要使用者指定使用哪種方式,這裡通過引數傳給
processCamera
,method 為 0 代表使用 MOG2 方式減除, method為 1代表使用 MOG1 方式減除, method為0 代表不作任何變換。兩種方式均可以突出背景外的變化情況,實際效果將在最終程式執行時展示。在對原始frame
做處理並寫入fgMask
後,通過imshow
函式輸出到監視器中。imshow
函式的第一個引數為輸出的視窗名,這裡先假設已經有一個名為Monitor
的視窗等待接收輸出,這個視窗將在最終的main
函式中建立。使用者應當可以指定這個監控程式是否將處理後的影象輸出,因此我們傳入一個showWindow
引數表明是否顯示實時監控視窗。對每一幀的處理方式和上面對背景幀的處理方式相同。void processCamera(bool showWindow, unsigned int method) {// ... bool backGroundFlag = true;Mat frame; // current frameMat HSV; // HSV formatMatND base; // histogramwhile ((char)keyboard != 'q' && (char)keyboard != 27){ if (!capture) { cerr << "Unable to read camera" << endl; cerr << "Exiting..." << endl; exit(EXIT_FAILURE); } frame = cvQueryFrame(capture); if (method == 0) pMOG2->operator()(frame, fgMask); else if (method == 1) pMOG->operator()(frame, fgMask); else if (method == 2) fgMask = frame; // set background if (backGroundFlag){ cvtColor(frame, HSV, CV_BGR2HSV); calcHist(&HSV, 1, channels, Mat(), base, 2, histSize, ranges, true, false); normalize(base, base, 0, 1, NORM_MINMAX, -1, Mat()); backGroundFlag = false; } cvtColor(frame, HSV, CV_BGR2HSV); calcHist(&HSV, 1, channels, Mat(), cur, 2, histSize, ranges, true, false); normalize(cur, cur, 0, 1, NORM_MINMAX, -1, Mat()); // TODO ... if (showWindow && !fgMask.empty()){ imshow("Monitor", fgMask); } keyboard = waitKey(30);} // end WhilecvReleaseCapture(&capture);} // end processCamera
比較當前幀和背景幀的相似度,當出現異常時開始記錄視訊。這裡直接呼叫
compareHist
函式,輸出一個 0 - 1 範圍內的指標,越接近1 表示兩個直方圖代表的影象越相似。這裡我設定的閾值為 0.65,這個閾值應當根據實際監控區域的光線、色彩等因素修正。我們建立了一個recorder
指標用於寫入視訊,需要指定寫入視訊的幀速和大小,這裡大小通過cvGetCaptureProperty
自動獲取,幀速fps
由使用者傳入引數指定。在這裡為了避免監控過於敏感的情況出現,設定了一個UnnormalFrames
引數,該引數記錄當前已經持續出現了多少幀與背景不同的畫面,也就是運動狀態出現了多久。當UnnormalFrames
達到使用者指定的閾值unnormal
時,我們認為監控中確實出現了異常,因此開始記錄。為了更完整的提供監控資訊,一旦確認監控中有運動狀態發生,在運動結束後,也就是檢測到當前幀和背景重新一致後,程式將繼續記錄視訊資訊,繼續記錄的時長和之前運動狀態持續的時長相同。程式碼中通過recordFlag
標識當前是否應該記錄視訊,在UnnormalFrames > unnormal
時,recordFlag
被置位,同時UnnormalFrames
隨著運動幀被檢測持續增加,當運動結束後,UnnormalFrames
將遞減,至 0 時停止記錄視訊。這裡unnormal
通過引數傳入processCamera
。至此,processCamera
函式編寫完成。void processCamera(bool showWindow, unsigned int method, unsigned int unnormal = 10, unsigned int fps = 24) {CvCapture *capture = cvCaptureFromCAM(0);if (!capture){ cerr << "Unable to open camera " << endl; exit(EXIT_FAILURE);}bool backGroundFlag = true, recordFlag = false;Mat frame, fgMask; // current frame, fg maskMat HSV; // HSV formatMatND base, cur; // histogramunsigned int UnnormalFrames = 0;int channels[] = { 0, 1 };CvSize size = cvSize( (int)cvGetCaptureProperty(capture, CV_CAP_PROP_FRAME_WIDTH), (int)cvGetCaptureProperty(capture, CV_CAP_PROP_FRAME_HEIGHT) );CvVideoWriter * recorder = cvCreateVideoWriter(recordName, CV_FOURCC('D', 'I', 'V', 'X'), 32, size, 1);// ESC or 'q' for quittingwhile ((char)keyboard != 'q' && (char)keyboard != 27){ if (!capture) { cerr << "Unable to read camera" << endl; cerr << "Exiting..." << endl; exit(EXIT_FAILURE); } frame = cvQueryFrame(capture); if (method == 0) pMOG2->operator()(frame, fgMask); else if (method == 1) pMOG->operator()(frame, fgMask); else if (method == 2) fgMask = frame; // set background if (backGroundFlag){ cvtColor(frame, HSV, CV_BGR2HSV); calcHist(&HSV, 1, channels, Mat(), base, 2, histSize, ranges, true, false); normalize(base, base, 0, 1, NORM_MINMAX, -1, Mat()); backGroundFlag = false; } cvtColor(frame, HSV, CV_BGR2HSV); calcHist(&HSV, 1, channels, Mat(), cur, 2, histSize, ranges, true, false); normalize(cur, cur, 0, 1, NORM_MINMAX, -1, Mat()); double comp = compareHist(base, cur, 0); if (comp < 0.65) UnnormalFrames += 1; else if (UnnormalFrames > 0) UnnormalFrames--; if (UnnormalFrames > unnormal) recordFlag = true; else if (UnnormalFrames <= 0){ UnnormalFrames = 0; recordFlag = false; } // DO SOMETHING WARNING // Here We Starting Recoding if (recordFlag){ cvWriteFrame(recorder, &(IplImage(frame))); } if (showWindow && !fgMask.empty()){ imshow("Monitor", fgMask); } keyboard = waitKey(30);} // end WhilecvReleaseVideoWriter(&recorder);cvReleaseCapture(&capture);} // end processCamera
4.定義外部或全域性變數
在processCamera
函式中使用到了一些函式中沒有宣告過的變數,這些變數有的是配置使用的常量如ranges
等,不需要理解,下面在標頭檔案中宣告。pMOG
和pMOG2
對應了對frame做變換的兩個方式,他們將在main
函式中被定義。keyboard
用於接收外部鍵盤輸入。其餘的均為常量,用於配置 OpenCV 提供的函式。
// ...extern Ptr<BackgroundSubtractor> pMOG; //MOG Background subtractorextern Ptr<BackgroundSubtractor> pMOG2; //MOG2 Background subtractorextern int keyboard;const float h_ranges[] = { 0, 256 };const float s_ranges[] = { 0, 180 };const float* ranges[] = { h_ranges, s_ranges };const int h_bins = 50, s_bins = 60;const int histSize[] = { h_bins, s_bins };extern char recordName[128];// ...
5.編寫 main.cpp
下面編寫程式入口。
- 首先需要告知使用者程式的使用方式,編寫
help
函式輸出幫助資訊。-vis
選項用於指定程式顯示實時監控,[MODE]
引數指定使用何種方式顯示監控,[FPS]
指定幀速,[THRESHOLD]
指定經過多少異常幀後開始記錄,[OUTPUTFILE]
指定輸出視訊記錄位置。
void help(){ cout << "----------------------------------------------------------------------------\n" << "Usage: \n" << " ./MonitorRecorder.exe [VIS] [MODE] [FPS] [THRESHOLD] [OUTPUTFILE] \n" << " [VIS] : use -vis to show the monitor window, or it will run background. \n" << " [MODE] : -src shows the original frame; \n" << " -mog1 shows the MOG frame; \n" << " -mog2 shows the MOG2 frame. \n" << " [FPS] : set the fps of record file, default is 24. \n" << " [THRESHOLD] \n" << " : set the number x that the monitor will start recording after \n" << " x unnormal frames passed. \n" << " [OUTPUTFILE] \n" << " : assign the output recording file. It must be .avi format. \n" << " designed by Forec \n"; << "----------------------------------------------------------------------------\n";}
- 編寫
main
函式。在main
函式中我們需要用到外部宣告的pMOG
、pMOG2
以及recordName
。這裡需要在函式外宣告。主函式中主要部分為解析使用者的命令列引數,其中stoi
函式需要使用 C++ 11 標準編譯。我們使用sleep(2)
將processCamera
延時 2 秒執行,這個時間你可以離開電腦,讓程式捕獲你的背景,之後可以回到電腦前,觀察監控程式的顯示和記錄情況。如果使用者指定了-vis
引數,則產生一個Monitor
視窗顯示實時監控,這個視窗就是此前processCamera
函式中輸出影象的視窗。在main
函式最後,用destroyAllWindows
銷燬namedWindow
產生的視窗。
#include "monitor.hpp"Ptr<BackgroundSubtractor> pMOG; //MOG Background subtractorPtr<BackgroundSubtractor> pMOG2; //MOG2 Background subtractorint keyboard;char recordName[128];void help();int main(int argc, char* argv[]){ bool showWindow = false; unsigned int method = 0, unnormal = 10, fps = 24; if (argc > 6){ cerr << "Invalid Parameters, Exiting..." << endl; exit(EXIT_FAILURE); } if (argc >= 2){ if (strcmp(argv[1], "-vis") == 0) showWindow = true; if (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0){ help(); exit(EXIT_SUCCESS); } } if (argc >= 3){ if (strcmp(argv[2], "-mog2") == 0) method = 0; if (strcmp(argv[2], "-mog1") == 0) method = 1; if (strcmp(argv[2], "-src") == 0) method = 2; } if (argc >= 4){ int param = stoi(argv[3], nullptr, 10