1. 程式人生 > >C++基於OpenCV實現實時監控和運動檢測記錄

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的基本功能後,梳理程式執行的步驟:

  1. 程式開始執行,啟動攝像頭並獲得資料流
  2. 進入迴圈:
    1. 捕獲一幀
    2. 是否為第一幀?是則記錄該幀作為監控區域的背景
    3. 將該幀做適當的變換,輸出到監視器中
    4. 分析該幀和背景幀的相似程度
    5. 相似程度是否低於閾值且當前沒有在記錄視訊?低於閾值開始記錄。
    6. 相似程度是否低於閾值且當前已經開始記錄視訊?低於閾值繼續記錄,否則停止記錄。
  3. 迴圈中程式的停止,通過接受外部中斷相應。

3.程式碼實現 processCamera

  1. 啟動攝像頭並獲得資料流,呼叫上面提到的cvCaptureFromCAM函式,預設攝像頭的 device 為 0。

    void processCamera() {CvCapture *capture = cvCaptureFromCAM(0);if (!capture){    cerr << "Unable to open camera " << endl;    exit(EXIT_FAILURE);}// TODO...}    // end processCamera
  2. 進入迴圈,迴圈條件中使用到一個 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
  3. 判斷當前幀是否為第一幀,通過一個bool型變數backGroundFlag來標識。若backGroundFlagtrue表示當前幀為第一幀,則記錄該幀並將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
  4. 對當前幀做適當變換並輸出到監視器。此時程式碼如下。我們要實現的程式可以對原始影象做兩種背景減除處理,因此需要使用者指定使用哪種方式,這裡通過引數傳給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
  5. 比較當前幀和背景幀的相似度,當出現異常時開始記錄視訊。這裡直接呼叫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等,不需要理解,下面在標頭檔案中宣告。pMOGpMOG2對應了對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

下面編寫程式入口。

  1. 首先需要告知使用者程式的使用方式,編寫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";}
  1. 編寫main函式。在main函式中我們需要用到外部宣告的 pMOGpMOG2以及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