这两天从零开始学习视频流中运动目标检测的方法,有点心得体会,写点东西跟大家共享,同时提出我的几个疑问,期望能够共同讨论进步。
本文主要面向和我一样的图像处理初学者,先从原理上把整个检测过程走通,然后再逐步提高性能。
1、关于运动目标检测的方法总结
查阅了相关的文献,目前能够实现运动物体检测的方法主要有以下几种:
1)背景差分法:能完整快速地分割出运动图像。其不足之处是易受光线变化影响,背景的更新是关键。不适用于摄像头运动的情况;
2)光流法:能检测独立运动的图像,可用于摄像头运动的情况,但是计算复杂耗时,较难实现实时监测;
3)帧差法:受光线变化影响较小,简单快速,但不能分割出完整的运动对象,需进一步运用目标分割算法。还有一些改进的算法,主要致力于减少光照影响和检测慢速物体变化。
以上是大多数文献中对这三种方法的评价,由于是初次接触,而且项目的需求是静止相机,因此采用最简单的方法:背景差分法。
2、背景差分法实现步骤
通过这几天的摸索,可将背景差分法的实现步骤总结如下:
1)进行图像的预处理:主要包括对图像进行灰度化以及滤波。
灰度化的方法及其C语言实现可参考《Canny边缘检测算法原理及其VC实现详解(二)》一文;关于图像滤波,通常可采用的方法有中值滤波、均值滤波以及高斯滤波等。关于高斯滤波的实现详见《高斯图像滤波原理及其编程离散化实现方法》一文。
2)背景建模:这是背景差法较为重要的第一步。目前大多的思路都是根据前N帧图像的灰度值进行区间统计从而得到一个具有统计意义的初始背景。在第一次的实现过程中,采用第一幅图片作为背景图,这样比较简单。
3)前景提取:将当前最新的图像与背景做差,即可求得背景差图,然后根据一定的方法对改图进行二值化,最终获得运动前景区域,即实现图像分割。
关于图像的二值化,目前主要的难点在于阈值的选取,随着运动物体在整个监控区域内的运动,所拍摄的图片具有不同的灰度特性,因此阈值的选取是一个研究热点,目前多采用的方法有最大类间方差法,一维交叉熵阈值法,二维交叉熵阈值法以及其他的自适应阈值选取方法等,目前本人已经实现了最大类间方差法,关于其实现代码及注意事项,另文详述。在用OpenCv实现的过程中,本着从简的思路,选固定的阈值即可。采用的代码为:
- cvAbsDiff(pFrameMat, pBkMat, pFrMat); //求差
- cvThreshold(pFrMat, pFrImg, 10, 255.0, CV_THRESH_BINARY);//二值化
4)背景更新:当运动体运动后,下一帧的图像背景就会发生改变,这时候就需要进行背景更新。关于背景更新方法,目前也是一个研究热点,OpenCV中的实现方法如下:
- cvRunningAvg(pFrameMat, pBkMat, 0.003, 0);
3、实现代码总结
以上是整个OpenCV实现的过程步骤及其原理,对整个实现的代码总结如下:
- //声明IplImage指针
- IplImage* pFrame = NULL;
- IplImage* pFrImg = NULL;
- IplImage* pBkImg = NULL;
- CvMat* pFrameMat = NULL;
- CvMat* pFrMat = NULL;
- CvMat* pBkMat = NULL;
- CvCapture* pCapture = NULL;
- int nFrmNum = 0;
- //创建窗口
- cvNamedWindow("video", 1);
- cvNamedWindow("background",1);
- cvNamedWindow("foreground",1);
- //使窗口有序排列
- cvMoveWindow("video", 30, 0);
- cvMoveWindow("background", 360, 0);
- cvMoveWindow("foreground", 690, 0);
- //pCapture = cvCaptureFromAVI("2.avi"); //读入已有视频用此句
- pCapture = cvCaptureFromCAM(0); //从摄像头读入视频用此
- while(pFrame = cvQueryFrame( pCapture ))
- {
- nFrmNum++;
- //如果是第一帧,需要申请内存,并初始化
- if(nFrmNum == 1)
- {
- pBkImg = cvCreateImage(cvSize(pFrame->width, pFrame->height),IPL_DEPTH_8U,1);
- pFrImg = cvCreateImage(cvSize(pFrame->width, pFrame->height), IPL_DEPTH_8U,1);
- pBkMat = cvCreateMat(pFrame->height, pFrame->width, CV_32FC1);
- pFrMat = cvCreateMat(pFrame->height, pFrame->width, CV_32FC1);
- pFrameMat = cvCreateMat(pFrame->height, pFrame->width, CV_32FC1);
- //转化成单通道图像再处理
- cvCvtColor(pFrame, pBkImg, CV_BGR2GRAY);
- cvCvtColor(pFrame, pFrImg, CV_BGR2GRAY);
- cvConvert(pFrImg, pFrameMat);
- cvConvert(pFrImg, pFrMat);
- cvConvert(pFrImg, pBkMat);
- }
- else
- {
- cvCvtColor(pFrame, pFrImg, CV_BGR2GRAY);
- cvConvert(pFrImg, pFrameMat);
- //先做高斯滤波,以平滑图像
- cvSmooth(pFrameMat, pFrameMat, CV_GAUSSIAN, 3, 0, 0);
- //当前帧跟背景图相减
- cvAbsDiff(pFrameMat, pBkMat, pFrMat);
- //二值化前景图
- cvThreshold(pFrMat, pFrImg, 10, 255.0, CV_THRESH_BINARY);
- //更新背景
- cvRunningAvg(pFrameMat, pBkMat, 0.003, 0);
- //将背景转化为图像格式,用以显示
- cvConvert(pBkMat, pBkImg);
- pFrame->origin = IPL_ORIGIN_BL;
- pFrImg->origin = IPL_ORIGIN_BL;
- pBkImg->origin = IPL_ORIGIN_BL;
- cvShowImage("video", pFrame);
- cvShowImage("background", pBkImg);
- cvShowImage("foreground", pFrImg);
- //如果有按键事件,则跳出循环
- //此等待也为cvShowImage函数提供时间完成显示
- //等待时间可以根据CPU速度调整
- if( cvWaitKey(2) >= 0 )
- break;
- }
- }
- //销毁窗口
- cvDestroyWindow("video");
- cvDestroyWindow("background");
- cvDestroyWindow("foreground");
- //释放图像和矩阵
- cvReleaseImage(&pFrImg);
- cvReleaseImage(&pBkImg);
- cvReleaseMat(&pFrameMat);
- cvReleaseMat(&pFrMat);
- cvReleaseMat(&pBkMat);
- cvReleaseCapture(&pCapture);