基於opencv3實現運動物體識別
一:背景減法
對於一個穩定的監控場景而言,在沒有運動目標,光照沒有變化的情況下,視訊影象中各個畫素點的灰度值是符合隨機概率分佈的。由於攝像機在採集影象的過程中,會不可避免地引入噪聲,這些灰度值以某一個均值為基準線,在附近做一定範圍內的隨機振盪,這種場景就是所謂的“背景”。
背景減法(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;
}