OpenCV進階之路:一個簡化的視頻摘要程序
原文出處: Ronny 的博客(@RonnyYoung)
一、前言
視頻摘要又稱視頻濃縮,是對視頻內容的一個簡單概括,先通過運動目標分析,提取運動目標,然后對各個目標的運動軌跡進行分析,將不同的目標拼接到一個共同的背景場景中,并將它們以某種方式進行組合。視頻摘要在視頻分析和基于內容的視頻檢索中扮演著重要角色。
視頻摘要主要運用在對長時間的監控視頻的壓縮上,它可以將不同時刻場景內目標的運動顯示在同一時刻,這樣大量減少了整個場景事件的時間跨度。一般的視頻摘要的步驟可以總結為:
視頻讀取→背景建模 → 前景提取→ 目標軌跡跟蹤→ 目標的時序與空間規劃 → 生成濃縮視頻
</blockquote>但是本文并不討論上面的這些主題,這里我只想通過一個簡單的去除視頻里非運動幀來實現一個簡單的視頻壓縮的功能。視頻摘要不是本文的主題,文章想通過做一個簡單的視頻摘要程序對OpenCV下面幾個功能進行介紹:
1)OpenCV與XML數據通信
2)視頻的讀取與寫入
3)如何在沒有OpenCV的環境中運行編譯好的程序
</blockquote>二、與XML的交互
很多程序都需要有一個配置文件,可以手動的去調整一些運行中的參數,xml文件格式就是我們常用到的一種配置文件格式。opencv中提供了一個處理xml的類用來與xml文件進行簡單的數據存儲與讀取通信。但這個類的功能有限,如果需要更多的功能可以利用第三方的庫,比如libxml等。
我們所設計的視頻摘要程序,跟常規的視頻摘要不同,這里只是通過刪除一些無運動目標的幀來達到視頻壓縮的目的,所以我們的算法可以設計如下:
1,定義一個目標運動的興趣區域,作為檢測區域。 2,遍歷指定目錄下的所有視頻文件,并逐一的進行視頻處理。 3,針對視頻的每一幀,在檢測區域內運用幀差法檢測前景移動。 4,如果檢測區域內前景的面積超過區域面積的10%,則說明有運動物體,則此幀進行保留,寫入壓縮視頻。否則,該幀直接舍棄。 5,所有視頻處理結束,則程序終止。
![]()
么,我們需要一個配置文件,這個文件里需要保存下面幾個內容:
1,檢測區域的參數
2,視頻文件的目錄
3,視頻文件的后綴格式
4,生存視頻的保存目錄
<?xml version="1.0"?> <opencv_storage> <roi> 3 460 1250 480</roi> <videoReadPath>D:\ExtractKeyImages\video\</videoReadPath> <videoSuffix>*.mp4</videoSuffix> <videoSavePath>../result.avi</videoSavePath> </opencv_storage>注意所有的節點都保存在opencv_storage節點下。
在OpenCV中定義了一個叫FileStorage的類,提供了一些簡單的打開與讀取xml文件內容的操作。
我們先來看xml文件數據的讀取:
1,用FileStorage的構造函數可以打開一個xml或yml文件,也可以用FileStorage::open()來打開一個數據文件。
FileStorage::FileStorage(); // 默認構造函數 FileStorage::FileStorage(const string& source, int flags, const string& encoding = string());上面第二個構造函數中有三個參數。
第一個參數source指定讀取文件的路徑。
第二個參數flag指定操作的模式,可以設置為READ說明以只讀的方式打開一個文件,或者設置為WRITE,這種情況下,如果文件不存在,則創建一個文件,如果文件已經存在,則會清空當前文件里的內容。還可以設置為APPEND用來打開一個存在的文件,并且可以在原來基礎上寫入。
第三個參數用來指定文件的編碼格式,一般都為UTF-8。
而open成員函數的接口與第二個構造函數接口一致。
bool FileStorage::open(const string& filename, int flags, const string& encoding=string())2,讀取文件內的數據,FileStorage重載的操作符[],用來獲得指定的節點內容。
FileNode FileStorage::operator[](const string& nodename) const FileNode FileNode::operator[](const string& nodename) const上面兩個操作符都返回FileNode類型,它是一個子節點類型。
比如:我們想讀取<book>結點下的<name>結點,則可以:
FileStorage fs("../config.xml", FileStorage::READ); string book_name; fs["book"] ["name"]>> book_name;如果要取出A節點下的B結點下的C結點則為fs["A"]["B"]["C"]>>content;要記住所有節點都是在根結點opencv_storage下的,但是訪問時忽略它。
而如果需要將數據寫入,則簡單的寫入可以直接用<<運算符,比如增加一個節點為book,內容為theOpenCV:
string book_name=”theOpenCV”; fs<<”book”<<book_name;最后給出我們程序中讀取配置參數的代碼,我們需要4項配置項,上面已經介紹過了:
FileStorage fs("../config.xml", FileStorage::WRITE); string videoPath; string videoSuffix; Rect roiRect; string imgSavePath; fs["videoReadPath"] >> videoPath; fs["videoSuffix"] >> videoSuffix; fs["imgSavePath"] >> imgSavePath; fs["roi"] >> roiRect;二、檢測區域的運動檢測
這里我們要進行簡單的視頻壓縮就是想把完全靜止不動的視頻幀從原視頻里刪除,我們的興趣目標一般是在移動的視頻里。所以我們可以用幀差法來檢測移動物體,它的原理是利用視頻中物體的移動將引起相鄰視頻幀內容的不同,從而顯示出移動的前景。
兩幀之間的幀差圖像可以這樣定義:
![]()
其中imgCur代表當前幀的圖像,imgPre代表前一幀圖像
![]()
在得到幀差圖像后,我們并不能得到很明顯的判斷條件,所以我們需要對幀差圖像進行二值化,我們設置一個閾值T
![]()
然后我們只需遍歷圖像求出圖像中所有白點的個數,即是運動前景的面積,計算一下面積比例即可以確定當前幀是否有物體移動。
當然我們得到的前景目標并不移動的物體的輪廓,而是與前一幀相比目標移動的部分。
![]()
下面為這一部分的OpenCV實現,相關的視頻讀取和寫入的操作可以參考OpenCV成長之路中的相關文章。
// 查找文件目錄下的所有視頻文件 vector<string> videoPathStr = FindAllFile((videoPath + videoSuffix).c_str(), true); // 先讀取一個視頻文件,用于獲取相關的參數 VideoCapture capture(videoPathStr[0]); // 視頻大小 Size videoSize(capture.get(CV_CAP_PROP_FRAME_WIDTH), capture.get(CV_CAP_PROP_FRAME_HEIGHT)); // 創建一個視頻寫入對象 VideoWriter writer("../result.avi", CV_FOURCC('M', 'J', 'P', 'G'), 25.0, videoSize); for (auto videoName : videoPathStr) { capture.open(videoName); // 讀入路徑下的視頻 Mat preFrame; bool stop(false); double totleFrameNum = capture.get(CV_CAP_PROP_FRAME_COUNT); // 獲取視頻總幀數 for (int frameNum = 0; frameNum < totleFrameNum; frameNum++) { Mat imgSrc; capture >> imgSrc; // 讀一視頻的一幀 if (!imgSrc.data) break; Mat frame; cvtColor(imgSrc, frame, CV_BGR2GRAY); ++frameNum; if (frameNum == 1) { preFrame = frame; } Mat frameDif; absdiff(frame, preFrame, frameDif); // 幀差法 preFrame = frame; threshold(frameDif, frameDif, 30, 255, THRESH_BINARY); // 二值化 Mat imgRoi = frameDif(roiRect); double matArea = computeMatArea(imgRoi); // 計算區域面積 if (matArea / (imgRoi.rows*imgRoi.cols) > 0.1) // 面積比例大于10% { writer << frameDif;// 寫入視頻 } } } capture.release(); writer.release();三、在沒有OpenCV的環境下運行程序
這里是指基于windows系統下VS平臺的程序,很多時候我們編譯好的程序需要在別人的電腦上運行,而別人電腦上是沒有OpenCV的基本庫的,而我們的編譯的opencv程序一般是動態鏈接一些dll的。
有兩種方法:一種是拷貝用到的dll到release目錄下,另一種是把相關的源文件加入工程中一起編譯。
下面主要介紹第一種方法,因為看起來簡單,很多人還是運行不了。
我們從openCV的環境配置開始說起:
首先,我們先找到我們下載并解壓后的OpenCV目錄下的這幾個目錄:
頭文件目錄:F:\EvProjects\OpenCV\OpenCV248\build\include
運行庫目錄:F:\EvProjects\OpenCV\OpenCV248\build\x86\vc12\lib
上面的vc12指定你的vs的版本,這里是vs2013
然后我們在我們新建的工程中找到屬性管理器:
![]()
然后分別在DeBug和Release下配置屬性表:
![]()
我們可以新建一個名字為opencv248_debug.props的屬性表,以后新建的工程,直接拷貝添加即可。
然后右鍵配置opencv248_debug.props的屬性,在VC目錄下配置兩項:
一項是包含目錄,加入:F:\EvProjects\OpenCV\OpenCV248\build\include
第二項是在庫目錄下加入:F:\EvProjects\OpenCV\OpenCV248\build\x86\vc12\lib
![]()
最后我們需要在鏈接器->輸入->附加依賴項中加入一些常用到的庫文件
opencv_core248d.lib opencv_imgproc248d.lib opencv_highgui248d.lib opencv_ml248d.lib opencv_video248d.lib opencv_features2d248d.lib opencv_calib3d248d.lib opencv_objdetect248d.lib opencv_contrib248d.lib opencv_legacy248d.lib opencv_flann248d.lib注意上面的248說明了我的opencv版本,你的可能是246或247。
也可以把F:\EvProjects\OpenCV\OpenCV248\build\x86\vc12\lib目錄里的lib文件都加入,注意只加入帶d的表示debug庫。
![]()
這樣的話debug下就配置完了,我們按相同方法,在release下配置一個屬性表opencv248_release.props,與debug不同的是,在鏈接器的配置里加入的庫名,都是不包含d的。
![]()
OK,屬性表都配置好后,我們把當前的編譯環境改為Release:
在解決方案里,右鍵項目名->屬性->配置管理器
![]()
然后把活動解決方案配置改為release即可。
![]()
所有的環境配置好后,只需要編譯好程序,然后在release下找到exe文件,這個就是我們的可執行文件,但是它不能單獨運行,我們需要把它需要依賴的一些dll拷貝過來,dll在opencv的F:\EvProjects\OpenCV\OpenCV248\build\x86\vc12\bin目錄下,如果你不確定你的程序里需要哪些庫,你就把全部都拷貝過來。或者可以用一個依賴庫查看軟件查看你的程序所依賴的庫,把對應的dll拷貝過來即可。
另外值得注意,如果是VS的較高版本,如VS2012,VS2013你還安裝對應的運行庫。