基於opencv3實現運動物體識別
發表於2017/9/22 22:26:30 927人閱讀
分類: Opencv_Learning-Diary
一:背景減法
對於一個穩定的監控場景而言,在沒有運動目標,光照沒有變化的情況下,視頻圖像中各個像素點的灰度值是符合隨機概率分布的。由於攝像機在采集圖像的過程中,會不可避免地引入噪聲,這些灰度值以某一個均值為基准線,在附近做一定范圍內的隨機振盪,這種場景就是所謂的“背景”。
背景減法(Background subtraction)是當前運動目標檢測技術中應用較為廣泛的一類方法,它的基本思想和幀間差分法相類似,都是利用不同圖像的差分運算提取目標區域。不過與幀間差分法不同的是,背景減法不是將當前幀圖像與相鄰幀圖像相減,而是將當前幀圖像與一個不斷更新的背景模型相減,在差分圖像中提取運動目標。
背景減法的運算過程如圖2-6 所示。首先利用數學建模的方法建立一幅背景圖像幀B ,記當前圖像幀為fn,背景幀和當前幀對應像素點的灰度值分別記為B(x, y )和fn(x , y ) ,按照式2.17 將兩幀圖像對應像素點的灰度值進行相減,並取其絕對值,得到差分圖像D n:
設定閾值 T ,按照式2.18 逐個對像素點進行二值化處理,得到二值化圖像 Rn' 。其中,灰度值為255 的點即為前景(運動目標)點,灰度值為0 的點即為背景點;對圖像 Rn'進行連通性分析,最終可得到含有完整運動目標的圖像Rn 。
背景減法計算較為簡單,由於背景圖像中沒有運動目標,當前圖像中有運動目標,將兩幅圖像相減,顯然可以提取出完整的運動目標,解決了幀間差分法提取的目標內部含有“空洞”的問題。
利用背景減法實現目標檢測主要包括四個環節:背景建模,背景更新,目標檢測,后期處理。其中,背景建模和背景更新是背景減法中的核心問題。背景模型建立的好壞直接影響到目標檢測的效果。所謂背景建模,就是通過數學方法,構建出一種可以表征“背景”的模型。獲取背景的最理想方法是在沒有運動目標的情況下獲取一幀“純凈”的圖像作為背景,但是,在實際情況中,由於光照變化、雨雪天氣、目標運動等諸多因素的影響,這種情況是很難實現。
代碼實現:
// Vedio_detect_human.cpp : 定義控制台應用程序的入口點。 // #include "stdafx.h" // 運動物體檢測——背景減法 #include "opencv2/opencv.hpp" using namespace cv; #include <iostream> using namespace std; // 運動物體檢測函數聲明 Mat MoveDetect(Mat background, Mat frame); int main() { VideoCapture video("D:\\opencv\\soft\\3.3.0\\win\\opencv\\sources\\samples\\data\\vtest.avi");//定義VideoCapture類video if (!video.isOpened()) //對video進行異常檢測 { cout << "video open error!" << endl; return 0; } // 獲取幀數 int frameCount = video.get(CV_CAP_PROP_FRAME_COUNT); // 獲取FPS double FPS = video.get(CV_CAP_PROP_FPS); // 存儲幀 Mat frame; // 存儲背景圖像 Mat background; // 存儲結果圖像 Mat result; for (int i = 0; i < frameCount; i++) { // 讀幀進frame video >> frame; imshow("frame", frame); // 對幀進行異常檢測 if (frame.empty()) { cout << "frame is empty!" << endl; break; } // 獲取幀位置(第幾幀) int framePosition = video.get(CV_CAP_PROP_POS_FRAMES); cout << "framePosition: " << framePosition << endl; // 將第一幀作為背景圖像 if (framePosition == 1) background = frame.clone(); // 調用MoveDetect()進行運動物體檢測,返回值存入result result = MoveDetect(background, frame); imshow("result", result); // 按原FPS顯示 if (waitKey(1000.0 / FPS) == 27) { cout << "ESC退出!" << endl; break; } } return 0; } Mat MoveDetect(Mat background, Mat frame) { Mat result = frame.clone(); // 1.將background和frame轉為灰度圖 Mat gray1, gray2; cvtColor(background, gray1, CV_BGR2GRAY); cvtColor(frame, gray2, CV_BGR2GRAY); // 2.將background和frame做差 Mat diff; absdiff(gray1, gray2, diff); imshow("diff", diff); // 3.對差值圖diff_thresh進行閾值化處理 Mat diff_thresh; threshold(diff, diff_thresh, 50, 255, CV_THRESH_BINARY); imshow("diff_thresh", diff_thresh); // 4.腐蝕 Mat kernel_erode = getStructuringElement(MORPH_RECT, Size(3, 3)); Mat kernel_dilate = getStructuringElement(MORPH_RECT, Size(15, 15)); erode(diff_thresh, diff_thresh, kernel_erode); imshow("erode", diff_thresh); // 5.膨脹 dilate(diff_thresh, diff_thresh, kernel_dilate); imshow("dilate", diff_thresh); // 6.查找輪廓並繪制輪廓 vector<vector<Point>> contours; findContours(diff_thresh, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); // 在result上繪制輪廓 drawContours(result, contours, -1, Scalar(0, 0, 255), 2); // 7.查找正外接矩形 vector<Rect> boundRect(contours.size()); for (int i = 0; i < contours.size(); i++) { boundRect[i] = boundingRect(contours[i]); // 在result上繪制正外接矩形 rectangle(result, boundRect[i], Scalar(0, 255, 0), 2); } // 返回result return result; }

二:幀差法
幀間差分方法利用圖像序列中相鄰兩幀或者三幀圖像對應像素值相減,然后取差值圖像進行閾值化處理提取出圖像中的運動區域:
代碼:
// Vedio_detect_human.cpp : 定義控制台應用程序的入口點。 // #include "stdafx.h" // 運動物體檢測——幀差法 #include "opencv2/opencv.hpp" using namespace cv; #include <iostream> using namespace std; // 運動物體檢測函數聲明 Mat MoveDetect(Mat temp, Mat frame); int main() { // 定義VideoCapture類video VideoCapture video("D:\\opencv\\soft\\3.3.0\\win\\opencv\\sources\\samples\\data\\vtest.avi"); if (!video.isOpened()) //對video進行異常檢測 { cout << "video open error!" << endl; return 0; } // 獲取幀數 int frameCount = video.get(CV_CAP_PROP_FRAME_COUNT); // 獲取FPS double FPS = video.get(CV_CAP_PROP_FPS); // 存儲幀 Mat frame; // 存儲前一幀圖像 Mat temp; // 存儲結果圖像 Mat result; for (int i = 0; i < frameCount; i++) { // 讀幀進frame video >> frame; imshow("frame", frame); // 對幀進行異常檢測 if (frame.empty()) { cout << "frame is empty!" << endl; break; } // 獲取幀位置(第幾幀) int framePosition = video.get(CV_CAP_PROP_POS_FRAMES); cout << "framePosition: " << framePosition << endl; // 如果為第一幀(temp還為空) if (i == 0) { // 調用MoveDetect()進行運動物體檢測,返回值存入result result = MoveDetect(frame, frame); } //若不是第一幀(temp有值了) else { // 調用MoveDetect()進行運動物體檢測,返回值存入result result = MoveDetect(temp, frame); } imshow("result", result); // 按原FPS顯示 if (waitKey(1000.0 / FPS) == 27) { cout << "ESC退出!" << endl; break; } temp = frame.clone(); } return 0; } Mat MoveDetect(Mat temp, Mat frame) { Mat result = frame.clone(); // 1.將background和frame轉為灰度圖 Mat gray1, gray2; cvtColor(temp, gray1, CV_BGR2GRAY); cvtColor(frame, gray2, CV_BGR2GRAY); // 2.將background和frame做差 Mat diff; absdiff(gray1, gray2, diff); imshow("diff", diff); // 3.對差值圖diff_thresh進行閾值化處理 Mat diff_thresh; threshold(diff, diff_thresh, 50, 255, CV_THRESH_BINARY); imshow("diff_thresh", diff_thresh); // 4.腐蝕 Mat kernel_erode = getStructuringElement(MORPH_RECT, Size(3, 3)); Mat kernel_dilate = getStructuringElement(MORPH_RECT, Size(18, 18)); erode(diff_thresh, diff_thresh, kernel_erode); imshow("erode", diff_thresh); // 5.膨脹 dilate(diff_thresh, diff_thresh, kernel_dilate); imshow("dilate", diff_thresh); // 6.查找輪廓並繪制輪廓 vector<vector<Point>> contours; findContours(diff_thresh, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); // 在result上繪制輪廓 drawContours(result, contours, -1, Scalar(0, 0, 255), 2); // 7.查找正外接矩形 vector<Rect> boundRect(contours.size()); for (int i = 0; i < contours.size(); i++) { boundRect[i] = boundingRect(contours[i]); // 在result上繪制正外接矩形 rectangle(result, boundRect[i], Scalar(0, 255, 0), 2); } // 返回result return result; }


優點:
- 幀間差分方法簡單、運算量小且易於實現。
- 幀間差分方法進行運動目標檢測可以較強地適應動態環境的變化,有效地去除系統誤差和噪聲的影響,對場景中光照的變化不敏感而且不易受陰影的影響。
缺點:
- 不能完全提取所有相關的特征像素點,也不能得到運動目標的完整輪廓,只能得到運動區域的大致輪廓;
- 檢測到的區域大小受物體的運動速度制約:對快速運動的物體,需要選擇較小的時間間隔,如果選擇不合適,當物體在前后兩幀中沒有重疊時,會被檢測為兩個分開的物體;對於慢速運動的物體,應該選擇較大的時間差,如果時間選擇不適當,當物體在前后兩幀中幾乎完全重疊時,則檢測不到物體。
- 容易在運動實體內部差生空洞現象。
三:光流法
簡介:在計算機視覺中,Lucas–Kanade光流算法是一種兩幀差分的光流估計算法。它由Bruce D. Lucas 和 Takeo
Kanade提出。
光流的概念:(Optical flow or optic flow)
它是一種運動模式,這種運動模式指的是一個物體、表面、邊緣在一個視角下由一個觀察者(比如眼睛、攝像頭等)
和背景之間形成的明顯移動。光流技術,如運動檢測和圖像分割,時間碰撞,運動補償編碼,三維立體視差,都是
利用了這種邊緣或表面運動的技術。
二維圖像的移動相對於觀察者而言是三維物體移動的在圖像平面的投影。
有序的圖像可以估計出二維圖像的瞬時圖像速率或離散圖像轉移。
光流算法:
它評估了兩幅圖像的之間的變形,它的基本假設是體素和圖像像素守恆。它假設一個物體的顏色在前后兩幀沒有巨大
而明顯的變化。基於這個思路,我們可以得到圖像約束方程。不同的光流算法解決了假定了不同附加條件的光流問題。
Lucas–Kanade算法:
這個算法是最常見,最流行的。它計算兩幀在時間t 到t + δt之間每個每個像素點位置的移動。 由於它是基於圖像信號
的泰勒級數,這種方法稱為差分,這就是對於空間和時間坐標使用偏導數。
圖像約束方程可以寫為I (x ,y ,z ,t ) = I (x + δx ,y + δy ,z + δz ,t + δt )
I(x, y,z, t) 為在(x,y,z)位置的體素。
我們假設移動足夠的小,那么對圖像約束方程使用泰勒公式,我們可以得到:
H.O.T. 指更高階,在移動足夠小的情況下可以忽略。從這個方程中我們可以得到:
或者
我們得到:
V x ,V y ,V z 分別是I(x,y,z,t)的光流向量中x,y,z的組成。 ,
,
和
則是圖像在(x ,y ,z ,t )這一點向相應方向的差分 。
所以
I x V x + I y V y + I z V z = − I t。
寫做:
這個方程有三個未知量,尚不能被解決,這也就是所謂光流算法的光圈問題。那么要找到光流向量則需要另一套解決的方案。而Lucas-Kanade算法
是一個非迭代的算法:
假設流(Vx,Vy,Vz)在一個大小為m*m*m(m>1)的小窗中是一個常數,那么從像素1...n , n = m 3 中可以得到下列一組方程:
三個未知數但是有多於三個的方程,這個方程組自然是個超定方程,也就是說方程組內有冗余,方程組可以表示為:
記作:
為了解決這個超定問題,我們采用最小二乘法:
or
得到:
其中的求和是從1到n。
這也就是說尋找光流可以通過在四維上圖像導數的分別累加得出。我們還需要一個權重函數W(i, j,k) , 來突出窗口中心點的
坐標。高斯函數做這項工作是非常合適的,
這個算法的不足在於它不能產生一個密度很高的流向量,例如在運動的邊緣和黑大的同質區域中的微小移動方面流信息會很快的褪去。它的優點在於
有噪聲存在的魯棒性還是可以的。
簡單來說,上圖表現的就是光流,光流描述的是圖像上每個像素點的灰度的位置(速度)變化情況,光流的研究是利用圖像序列中的像素強度數據的時域變化和相關性來確定各自像素位置的“運動”。研究光流場的目的就是為了從圖片序列中近似得到不能直接得到的運動場。
光流法的前提假設:
(1)相鄰幀之間的亮度恆定;
(2)相鄰視頻幀的取幀時間連續,或者,相鄰幀之間物體的運動比較“微小”;
(3)保持空間一致性;即,同一子圖像的像素點具有相同的運動;
代碼1:
// Vedio_detect_human.cpp : 定義控制台應用程序的入口點。 // #include "stdafx.h" // 運動物體檢測——光流法--LK金字塔 #include<iostream> #include<opencv2\highgui\highgui.hpp> #include<opencv2\nonfree\nonfree.hpp> #include<opencv2\video\tracking.hpp> using namespace std; using namespace cv; Mat image1, image2; vector<Point2f> point1, point2, pointCopy; vector<uchar> status; vector<float> err; int main() { VideoCapture video("D:\\opencv\\soft\\3.3.0\\win\\opencv\\sources\\samples\\data\\vtest.avi"); // 獲取視頻幀率 double fps = video.get(CV_CAP_PROP_FPS); // 兩幅畫面中間間隔 double pauseTime = 1000 / fps; video >> image1; Mat image1Gray, image2Gray; cvtColor(image1, image1Gray, CV_RGB2GRAY); goodFeaturesToTrack(image1Gray, point1, 100, 0.01, 10, Mat()); pointCopy = point1; // 繪制特征點位 for (int i = 0; i<point1.size(); i++) { circle(image1, point1[i], 1, Scalar(0, 0, 255), 2); } namedWindow("LK--角點特征光流", 0); imshow("LK--角點特征光流", image1); while (true) { video >> image2; // 圖像為空或Esc鍵按下退出播放 if (!image2.data || waitKey(pauseTime) == 27) { break; } cvtColor(image2, image2Gray, CV_RGB2GRAY); // LK金字塔實現 calcOpticalFlowPyrLK(image1Gray, image2Gray, point1, point2, status, err, Size(20, 20), 3); for (int i = 0; i<point2.size(); i++) { circle(image2, point2[i], 1, Scalar(0, 0, 255), 2); line(image2, pointCopy[i], point2[i], Scalar(255, 0, 0), 2); } imshow("LK金字塔實現--角點特征光流", image2); swap(point1, point2); image1Gray = image2Gray.clone(); } return 0; }

代碼2:
// Vedio_detect_human.cpp : 定義控制台應用程序的入口點。 // #include "stdafx.h" // 運動物體檢測——光流法--LK金字塔 #include <iostream> #include <opencv2/opencv.hpp> #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> // Gaussian Blur #include <opencv2/ml/ml.hpp> #include <opencv2/contrib/contrib.hpp> using namespace cv; using namespace std; void duan_OpticalFlow(Mat &frame, Mat & result); bool addNewPoints(); bool acceptTrackedPoint(int i); Mat image; vector<Point2f> point1, point2, pointCopy; Mat curgray; // 當前圖片 Mat pregray; // 預測圖片 vector<Point2f> point[2]; // point0為特征點的原來位置,point1為特征點的新位置 vector<Point2f> initPoint; // 初始化跟蹤點的位置 vector<Point2f> features; // 檢測的特征 int maxCount = 1000; // 檢測的最大特征數 double qLevel = 0.01; // 特征檢測的等級 double minDist = 10.0; // 兩特征點之間的最小距離 vector<uchar> status; // 跟蹤特征的狀態,特征的流發現為1,否則為0 vector<float> err; int main() { Mat matSrc; Mat matRst; VideoCapture cap("D:\\opencv\\soft\\3.3.0\\win\\opencv\\sources\\samples\\data\\vtest.avi"); int totalFrameNumber = cap.get(CV_CAP_PROP_FRAME_COUNT); cap >> image; Mat imageGray, image2Gray; cvtColor(image, imageGray, CV_RGB2GRAY); goodFeaturesToTrack(imageGray, point1, 100, 0.01, 10, Mat()); pointCopy = point1; // 繪制特征點位 for (int i = 0; i<point1.size(); i++) { circle(image, point1[i], 1, Scalar(0, 0, 255), 2); } namedWindow("LK--角點特征光流", 0); imshow("LK--角點特征光流", image); // perform the tracking process cout << "開始檢測視頻,按下ESC鍵推出。" << endl; for (int nFrmNum = 0; nFrmNum < totalFrameNumber; nFrmNum++) { // 讀取視頻 cap >> matSrc; if (!matSrc.empty()) { duan_OpticalFlow(matSrc, matRst); cout << "該圖片幀是 " << nFrmNum << endl; } else { cout << "獲取視頻幀數錯誤!" << endl; } if (waitKey(1) == 27) break; } waitKey(0); return 0; } void duan_OpticalFlow(Mat &frame, Mat & result) { cvtColor(frame, curgray, CV_BGR2GRAY); frame.copyTo(result); // 添加特征點 if (addNewPoints()) { goodFeaturesToTrack(curgray, features, maxCount, qLevel, minDist); point[0].insert(point[0].end(), features.begin(), features.end()); initPoint.insert(initPoint.end(), features.begin(), features.end()); } if (pregray.empty()) { curgray.copyTo(pregray); } calcOpticalFlowPyrLK(pregray, curgray, point[0], point[1], status, err); // 去除部分不好的光電 int k = 0; for (size_t i = 0; i<point[1].size(); i++) { if (acceptTrackedPoint(i)) { initPoint[k] = initPoint[i]; point[1][k++] = point[1][i]; } } point[1].resize(k); initPoint.resize(k); // 現實特征點和運動軌跡 for (size_t i = 0; i<point[1].size(); i++) { line(result, initPoint[i], point[1][i], Scalar(0, 0, 255)); circle(result, point[1][i], 3, Scalar(0, 255, 0), -1); } // 更新該次結果作為下一次的參考 swap(point[1], point[0]); swap(pregray, curgray); imshow("LK--Demo", result); // waitKey(0); } bool addNewPoints() { return point[0].size() <= 10; } bool acceptTrackedPoint(int i) { return status[i] && ((abs(point[0][i].x - point[1][i].x) + abs(point[0][i].y - point[1][i].y)) > 2); }

opencv源碼給出的demo:
// Vedio_detect_human.cpp : 定義控制台應用程序的入口點。 // #include "stdafx.h" // 運動物體檢測——光流法--LK金字塔 #include <stdio.h> #include <iostream> #include <opencv2/opencv.hpp> #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> // Gaussian Blur #include <opencv2/ml/ml.hpp> #include <opencv2/contrib/contrib.hpp> #include <opencv2/video/tracking.hpp> using namespace cv; static void convertFlowToImage(const Mat &flow_x, const Mat &flow_y, Mat &img_x, Mat &img_y, double lowerBound, double higherBound) { #define CAST(v, L, H) ((v) > (H) ? 255 : (v) < (L) ? 0 : cvRound(255*((v) - (L))/((H)-(L)))) for (int i = 0; i < flow_x.rows; ++i) { for (int j = 0; j < flow_y.cols; ++j) { float x = flow_x.at<float>(i, j); float y = flow_y.at<float>(i, j); img_x.at<uchar>(i, j) = CAST(x, lowerBound, higherBound); img_y.at<uchar>(i, j) = CAST(y, lowerBound, higherBound); } } #undef CAST } static void drawOptFlowMap(const Mat& flow, Mat& cflowmap, int step, double, const Scalar& color) { for (int y = 0; y < cflowmap.rows; y += step) for (int x = 0; x < cflowmap.cols; x += step) { const Point2f& fxy = flow.at<Point2f>(y, x); line(cflowmap, Point(x, y), Point(cvRound(x + fxy.x), cvRound(y + fxy.y)), color); circle(cflowmap, Point(x, y), 2, color, -1); } } int main(int argc, char** argv) { // IO operation const char* keys = { "{ f | vidFile | ex2.avi | filename of video }" "{ x | xFlowFile | flow_x | filename of flow x component }" "{ y | yFlowFile | flow_y | filename of flow x component }" "{ i | imgFile | flow_i | filename of flow image}" "{ b | bound | 15 | specify the maximum of optical flow}" }; //CommandLineParser cmd(argc, argv, keys); //string vidFile = cmd.get<string>("vidFile"); //string xFlowFile = cmd.get<string>("xFlowFile"); //string yFlowFile = cmd.get<string>("yFlowFile"); //string imgFile = cmd.get<string>("imgFile"); //int bound = cmd.get<int>("bound"); string vidFile = "vidFile"; string xFlowFile = "xFlowFile"; string yFlowFile = "yFlowFile"; string imgFile = "imgFile"; int bound = 80; namedWindow("video", 1); namedWindow("imgX", 1); namedWindow("imgY", 1); namedWindow("Demo", 1); //VideoCapture capture(vidFile); VideoCapture capture("D:\\opencv\\soft\\3.3.0\\win\\opencv\\sources\\samples\\data\\vtest.avi"); if (!capture.isOpened()) { printf("Could not initialize capturing..\n"); return -1; } int frame_num = 0; Mat image, prev_image, prev_grey, grey, frame, flow, cflow; while (true) { capture >> frame; if (frame.empty()) break; imshow("video", frame); if (frame_num == 0) { image.create(frame.size(), CV_8UC3); grey.create(frame.size(), CV_8UC1); prev_image.create(frame.size(), CV_8UC3); prev_grey.create(frame.size(), CV_8UC1); frame.copyTo(prev_image); cvtColor(prev_image, prev_grey, CV_BGR2GRAY); frame_num++; continue; } frame.copyTo(image); cvtColor(image, grey, CV_BGR2GRAY); // calcOpticalFlowFarneback(prev_grey,grey,flow,0.5, 3, 15, 3, 5, 1.2, 0 ); calcOpticalFlowFarneback(prev_grey, grey, flow, 0.702, 5, 10, 2, 7, 1.5, cv::OPTFLOW_FARNEBACK_GAUSSIAN); prev_image.copyTo(cflow); drawOptFlowMap(flow, cflow, 12, 1.5, Scalar(0, 255, 0)); imshow("cflow", cflow); Mat flows[2]; split(flow, flows); Mat imgX(flows[0].size(), CV_8UC1); Mat imgY(flows[0].size(), CV_8UC1); convertFlowToImage(flows[0], flows[1], imgX, imgY, -bound, bound); //char tmp[20]; //sprintf(tmp, "_%04d.jpg", int(frame_num)); //imwrite(xFlowFile + tmp, imgX); //imwrite(yFlowFile + tmp, imgY); //imwrite(imgFile + tmp, image); std::swap(prev_grey, grey); std::swap(prev_image, image); frame_num = frame_num + 1; imshow("imgX", imgX); imshow("imgY", imgY); imshow("Demo", image); if (waitKey(1) == 27) break; } waitKey(0); return 0; }
