运动目标检测是图像领域的一个经典问题,相关的算法较多。本文的运动目标检测主要基于背景消去(Background Subtraction)算法,本文将手动实现背景消去算法并检测到运动物体的实时位置。编程的基本环境是VS2019+opencv4.4,环境配置可参考:https://blog.csdn.net/weixin_38442390/article/details/109616546。
我们的基本思想是使用背景消去算法将运动物体从图片中提取出来,想象一下,一张没有运动物体的环境图,和突然出现某个物体的图,那么两张图的绝大部分都还是相同的,只有运动物体出现的区域是不同的,那么我们就能根据这个特点将运动物体和背景区分开。最简单的做法是直接比较两张图片,像素基本一致的点定为背景点,否则则定义前景,即目标。当然这样我们还是会碰到很多问题,因为即使是同一个区域,它的像素点在不同时刻,也是变化的,光照、风速都会对它有所影响,因此我们需要更进一步。
opencv中提供了两个背景消去的基本类BackgroundSubtractorKNN和BackgroundSubtractorMOG2,这两个类提供了背景消去的接口,使用起来非常简单,两行代码搞定。但是为了熟悉背景消去,我们选择手动实现中提供的算法。它的思想是,我们对每个像素点进行处理,根据在前面几帧中这个像素点是背景还是前景以及当前帧像素点和前几帧像素点的相似性来确定当前像素点是否是背景,假设在前几帧中,该像素点被认为是背景点,且该像素点的像素值与前几帧的像素值非常近似,那么我们就可以将此像素点归为背景点,否则为前景点。
#include
#include
#include
using namespace std;
using namespace cv;
其中历史帧为我们需要考察的帧总数;当历史帧中有超过3张认为当前像素点为背景,则认为它是背景点;当两个像素点的差值小于阈值,我们认为两个像素点是一致的;排序规则用于后期的轮廓大小排序,我们只选取轮廓交大的目标,很小的轮廓一般是噪声;最后我们为每个像素点定义一个结构,包含它历史帧是否为背景以及灰度值。
const int NumFrame=10;//历史帧
const int NumKnn=3;//knn的界
const double thresh=25;//阈值
//--------------------------排序规则,后期用于提取轮廓
bool cmp(vector<Point>p1,vector<Point>p2)
{
return contourArea(p1)>contourArea(p2);
}
//--------------------------历史帧
struct HistoryFrame
{
bool IsBackground[NumFrame];//是否是背景
uchar HistoryGray[NumFrame];//背景图的灰度值
};
主函数是程序算法的主体部分。首先我们读取视频,然后进行了一些初始化的工作。在while循环中,我们首先读取每一帧图片,然后统计当前点被认定为背景的,次数,如果超过3次,则认为他是背景点,否则为前景点。最后我们需要跟新历史帧的信息,当前座位历史帧写入,并删除最前面的历史信息。
VideoCapture cap("walk1.mp4");//读取视频
Mat frame;//定义帧
Mat KnnFrame;//背景消去的图
int rows=cap.get(CAP_PROP_FRAME_HEIGHT);//图片行
int cols=cap.get(CAP_PROP_FRAME_WIDTH);//图片列
int nn=rows*cols;
//为每一个像素点定义一个历史帧并初始化
HistoryFrame *HF=new HistoryFrame[nn];
for(int i=0;i<rows*cols;i++){
for(int j=0;j<NumFrame;j++){
HF[i].IsBackground[j]=0;
HF[i].HistoryGray[j]=0;
}
}
KnnFrame.create(rows,cols,CV_8UC1);//设置背景消去图为单通道
int CurFrame=0;//当前帧
int StartFrame=10;//开始帧
while(cap.read(frame)){
KnnFrame.setTo(Scalar(255));//将所有像素点设置为255
Mat frame1=frame;//记录帧(后期需要在原视频上做标记)
cvtColor(frame,frame,COLOR_BGR2GRAY);//将原图转为灰度图
//判断每个像素点是背景还是前景(KNN规则)
for(int i=0;i<rows;i++){
for(int j=0;j<cols;j++){
uchar gray=frame.at<uchar>(i,j);
int fit1=0;
int fit2=0;
for(int k=0;k<NumFrame;k++){
//如果两像素之差小于阈值,则认为相同
if(fabs(gray-HF[i*cols+j].HistoryGray[k])<thresh){
fit1++;
//如果历史帧为背景点
if(HF[i*cols+j].IsBackground[k]){
fit2++;
}
}
}
if(fit2>=NumKnn)KnnFrame.at<uchar>(i,j)=0;//如果是背景的次数超过设定值则判断为背景
//更新该像素点的历史信息
int index=CurFrame%NumFrame;
HF[i*cols+j].IsBackground[index]=fit1>=NumKnn?1:0;
HF[i*cols+j].HistoryGray[index]=gray;
}
}
CurFrame++;
medianBlur(KnnFrame,KnnFrame,5);//均值滤波,去除椒盐噪声
//舍弃前10帧,因为没有真实的历史信息
if(CurFrame>StartFrame){
vector<vector<Point>>contours;//定义轮廓
findContours(KnnFrame,contours,RETR_EXTERNAL,CHAIN_APPROX_NONE);//寻找轮廓
sort(contours.begin(),contours.end(),cmp);//按照轮廓面积大小排序
for(int i=0;i<contours.size();i++){
//如果比最大的轮廓小12倍,则作为噪声处理
if(contourArea(contours[i])<contourArea(contours[0])/12.0){
break;
}
Rect rect=boundingRect(contours[i]);
rectangle(frame1,rect,Scalar(0,0,255),1,8);//画矩形
}
imshow("frame1",frame1);
imshow("KNN",KnnFrame);
}
if((waitKey(10))==27)break;
}
#include
#include
#include
using namespace std;
using namespace cv;
const int NumFrame=10;//历史帧
const int NumKnn=3;//knn的界
const double thresh=25;//阈值
//--------------------------排序规则,后期用于提取轮廓
bool cmp(vector<Point>p1,vector<Point>p2)
{
return contourArea(p1)>contourArea(p2);
}
//--------------------------历史帧
struct HistoryFrame
{
bool IsBackground[NumFrame];//是否是背景
uchar HistoryGray[NumFrame];//背景图的灰度值
};
int main()
{
VideoCapture cap("walk1.mp4");//读取视频
Mat frame;//定义帧
Mat KnnFrame;//背景消去的图
int rows=cap.get(CAP_PROP_FRAME_HEIGHT);//图片行
int cols=cap.get(CAP_PROP_FRAME_WIDTH);//图片列
int nn=rows*cols;
//为每一个像素点定义一个历史帧并初始化
HistoryFrame *HF=new HistoryFrame[nn];
for(int i=0;i<rows*cols;i++){
for(int j=0;j<NumFrame;j++){
HF[i].IsBackground[j]=0;
HF[i].HistoryGray[j]=0;
}
}
KnnFrame.create(rows,cols,CV_8UC1);//设置背景消去图为单通道
int CurFrame=0;//当前帧
int StartFrame=10;//开始帧
while(cap.read(frame)){
KnnFrame.setTo(Scalar(255));//将所有像素点设置为255
Mat frame1=frame;//记录帧(后期需要在原视频上做标记)
cvtColor(frame,frame,COLOR_BGR2GRAY);//将原图转为灰度图
//判断每个像素点是背景还是前景(KNN规则)
for(int i=0;i<rows;i++){
for(int j=0;j<cols;j++){
uchar gray=frame.at<uchar>(i,j);
int fit1=0;
int fit2=0;
for(int k=0;k<NumFrame;k++){
if(fabs(gray-HF[i*cols+j].HistoryGray[k])<thresh){
fit1++;
if(HF[i*cols+j].IsBackground[k]){
fit2++;
}
}
}
if(fit2>=NumKnn)KnnFrame.at<uchar>(i,j)=0;//如果是背景的次数超过设定值则判断为背景
//更新该像素点的历史信息
int index=CurFrame%NumFrame;
HF[i*cols+j].IsBackground[index]=fit1>=NumKnn?1:0;
HF[i*cols+j].HistoryGray[index]=gray;
}
}
CurFrame++;
medianBlur(KnnFrame,KnnFrame,5);//均值滤波,去除椒盐噪声
//舍弃前10帧,因为没有真实的历史信息
if(CurFrame>StartFrame){
vector<vector<Point>>contours;//定义轮廓
findContours(KnnFrame,contours,RETR_EXTERNAL,CHAIN_APPROX_NONE);//寻找轮廓
sort(contours.begin(),contours.end(),cmp);
for(int i=0;i<contours.size();i++){
if(contourArea(contours[i])<contourArea(contours[0])/12.0){
break;
}
Rect rect=boundingRect(contours[i]);
rectangle(frame1,rect,Scalar(0,0,255),1,8);//画矩形
}
imshow("frame1",frame1);
imshow("KNN",KnnFrame);
}
if((waitKey(10))==27)break;
}
}
如图所示,下面第一张图是背景消去之后的效果,我们可以看到除了行人之外,还会存在一些别的噪声,因此我们在选取的时候不能所有的轮廓都选择,太小的不能选。第二张图显示了最终的检测效果。
本文主要用于记录手动实现运动物体检测的整个过程,不同的参数适用于不同的场景,如果读者需要使用的话需要自己去选择合适的参数。主要的参数是历史帧的数量、knn的界、以及阈值。历史帧的数量应该根据物体运动的速度进行选择,如果物体运动较慢可以选择更多的帧;knn的界可以根据识别效果进行调节,如果图像黑色元素(背景)太多,则应该选择更大的界,否则选择更小的界;同样地,如果阈值较小,则认定为背景的条件更加严格,那么黑色元素会少,如果图像黑色元素太多,那我们应该将阈值变得更小。
以上就是实现运动物体检测的整个流程,供读者学习参考,整个方法的思想比较简单,效果也是比较一般。读者也可以选择前文中提到的opencv内置的两个类进行背景消去,然后按照同样的步骤提取轮廓即可。