需求分析
读入一个视频流,对视频流中的白块进行计数.
案例方法:实现运动目标检测(追踪)
待选方法:
①帧差法
基本原理就是在图像序列相邻两帧或三帧间采用基于像素的时间差分通过闭值化来提取出图像中的运动区域。帧差法仅仅做运动检测。网上经常有人做个运动检测,再找个轮廓,拟合个椭圆就说跟踪了,并没有建立帧与帧之间目标联系的,没有判断目标产生和目标消失的都不能算是跟踪吧。
首先,将相邻帧图像对应像素值相减得到差分图像,然后对差分图像二值化,在环境亮度变化不大的情况下,如果对应像素值变化小于事先确定的阈值时,可以认为此处为背景像素:如果图像区域的像素值变化很大,可以认为这是由于图像中运动物体引起的,将这些区域标记为前景像素,利用标记的像素区域可以确定运动目标在图像中的位置。
由于相邻两帧间的时间间隔非常短,用前一帧图像作为当前帧的背景模型具有较好的实时性,其背景不积累,且更新速度快、算法简单、计算量小。算法的不足在于对环境噪声较为敏感,阈值的选择相当关键,选择过低不足以抑制图像中的噪声,过高则忽略了图像中有用的变化。对于比较大的、颜色一致的运动目标,有可能在目标内部产生空洞,无法完整地提取运动目标。
②背景减除法
基本思想是利用背景的参数模型来近似背景图像的像素值,将当前帧与背景图像进行差分比较实现对运动区域的检测,其中区别较大的像素区域被认为是运动区域,而区别较小的像素区域被认为是背景区域。
背景减除法必须要有背景图像,并且背景图像必须是随着光照或外部环境的变化而实时更新的,因此背景减除法的关键是背景建模及其更新。针对如何建立对于不同场景的动态变化均具有自适应性的背景模型,减少动态场景变化对运动分割的影响,研究人员已提出了许多背景建模算法,但总的来讲可以概括为非回归递推和回归递推两类。非回归背景建模算法是动态的利用从某一时刻开始到当前一段时间内存储的新近观测数据作为样本来进行背景建模。非回归背景建模方法有最简单的帧间差分、中值滤波方法、Toyama等利用缓存的样本像素来估计背景模型的线性滤波器、Elg~al等提出的利用一段时间的历史数据来计算背景像素密度的非参数模型等。回归算法在背景估计中无需维持保存背景估计帧的缓冲区,它们是通过回归的方式基于输入的每一帧图像来更新某个时刻的背景模型。这类方法包括广泛应用的线性卡尔曼滤波法、Stauffe:与Grimson提出的混合高斯模型等。
在opencv中有个BackgroundSubtractorMOG2函数,是以高斯混合模型为基础的背景/前景分割算法,但算法只实现了检测部分。这个算法的一个特点是它为每一个像素选择一个合适数目的高斯分布,其对由于亮度等发生变化引起的场景变化产生更好的适应。
混合高斯模型算法原理,不重复赘述:
初步构思
首先已经实现了一个想法:由于drawcontours的填充特性,会对轮廓满足条件的方块进行涂色,一旦进入屏幕,白块内原本是空的,然后就会迅速被填充,根据这个比较明显的差异,我们可以以此为依据进行侦差法
该类算法对时间上连续的两帧或三帧图像进行差分运算,不同帧对应的像素点相减,判断灰度差的绝对值,当绝对值超过一定阈值时,即可判断为运动目标,从而实现目标的检测功能。
那么对本题而言,可以采取以下的办法:
在遍历视频流的过程中,保存上一帧,把这一帧和上一帧对比,我们要找的是这两帧的差别,由于白块移动的幅度,如果出现了新的白块填充现象,那么两帧发生"白块填充"的那个区域,就会出现大量的白色像素,那么据此可以断定出现了白块然后cnt++,至于继续读取下一帧,这个新的白块会不会被检测到而cnt++呢,是不会的,这一点需要测试者在处理的时候进行打印轮廓区域,打擂找到阈值,根据这个思路就可以初步写出代码。
需要考虑的问题有:如果是单纯地作差,那么如果说上一帧是被填充的,而下一帧的白块是被划走了的,那么就会出现负像素值,我们要剔除这种像素点,直接将其设置为0即可。
通过测试代码
#include <iostream>
#include <opencv2/opencv.hpp>
#include <algorithm>
using namespace std;
using namespace cv;
const int MAX = 0x3f3f3f3f;
int main()
{
//首先读取图像:将图像中的每一帧都读出来
VideoCapture capture ("E:/computer view/2021.9.4/???.mkv");
Mat frame,last_frame;//获取上一帧图像
capture.read(frame);
//预处理
last_frame = Mat::zeros(frame.size(), frame.type());
cvtColor(last_frame, last_frame, COLOR_BGR2GRAY);
int cnt = 0;
int MIN = MAX;
//目标:检测目标轮廓内的颜色变换,如果 原本什么都没有后来出现了大面积的填充,那么就证明出现了一个白块
//因此需要对比,需要保存上一帧的图像与这一帧图像进行比较
while (!frame.empty())
{
//转化为二值图像便于处理
Mat gray,canny,sub;//分别为初步处理的灰度图像,Canny检测后的图像,作差后图像
cvtColor(frame,gray,COLOR_BGR2GRAY);
//检测边缘,获取方块的形状
//1.Canny边缘检测
Canny(gray, canny, 50, 150);//灰度值图像进行Canny边缘检测
//2.findContours的写法
std::vector<std::vector <Point> > contours;
vector<Vec4i> hierarchy;
cv::findContours(canny,contours,hierarchy,RETR_TREE,CHAIN_APPROX_SIMPLE,Point(0,0));
//设置单通道的画布,用于描出基础的轮廓
Mat drawing = Mat::zeros(canny.size(), CV_8UC1);
//设置单通道的画布,用于放置作差处理后的图像,初始均为0像素灰度值
sub = Mat::zeros(canny.size(), CV_8UC1);
//3.drawContours写法
for (int i = 0; i < contours.size(); i++)
{
Scalar color = Scalar(255, 255, 255);
drawContours(drawing,contours,i,color,-1,8, hierarchy,0,Point());
//这两步用来调试的,找Area的阈值
//int Area = (int)(abs)(cv::contourArea(contours[i]));//如果是由暗转明,那么就一次
//cout << Area << endl;
}
//初步轮廓的显示,调试
imshow("drawing", drawing);
//通过这一步过滤掉不需要的变化,只保留我们想要的那个变化
//4.遍历这一帧图像和上一帧图像
//如果说这一帧图像的像素点灰度值-上一帧图像的像素点灰度值 是小于0的(原图像只有0或255)
//证明:这一帧为0,上一帧为255,是那种滑块从屏幕划走的情况,不要了直接跳过处理
//如果不是,那么就是滑块出现了,把作差处理画布对应像素值设置为255
for (int i = 0; i < drawing.rows; i++)
{
for (int j = 0; j < drawing.cols; j++)
{
if (drawing.at<uchar>(i, j) - last_frame.at<uchar>(i, j) <= 0)
{
continue;
}
sub.at<uchar>(i, j) = drawing.at<uchar>(i, j);
}
}
//测试作差的处理的输出图像
imshow("sub", sub);
//5.画作差图像的轮廓
std::vector<std::vector <Point> > contours_sub;
vector<Vec4i> hierarchy_sub;
cv::findContours(sub, contours_sub, hierarchy_sub, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));
Mat drawing_sub = Mat::zeros(canny.size(), CV_8UC1);
//6.遍历作差图像轮廓,算轮廓面积,把之前用到的MIN值用上来筛选
for (int i = 0; i < contours_sub.size(); i++)
{
Scalar color = Scalar(255, 255, 255);
drawContours(drawing_sub, contours_sub, i, color, -1, 8, hierarchy, 0, Point());
int Area = (int)abs(cv::contourArea(contours_sub[i]));
if (Area >= 10000)
{
MIN = std::min(Area, MIN);
//cout << Area<<endl;
cnt++;
}
}
//观察者c
char c = waitKey(10);
//设置迭代
last_frame = drawing.clone();
capture.read(frame);
if (c == 27)
{
break;
}
if (c == '0')
{
waitKey(0);
}
}
cout << MIN<<endl<<"检测到方块总数为:"<< cnt;
return 0;
}
算法分析
这个方法实际上很稳定,算法很精准,但是非常耗时,对于本题所用的测试数据来说,12分钟视频需要计算7e次(every frame),那么也就是说,对于视频流的处理基本上没有优化。
总之就是算法效率非常低下,进行了很多不必要的计算,有很多次的全图像素操作,Canny->findcontours->drawcontours->for for这些无疑都会加长计算时间,(好像给老师跑,用显卡都跑了好久),最根本的原因就是大量的像素遍历问题。
那么根据这个可以改进算法,是不是非要遍历所有像素呢?与同学交流之后认为他们的算法思想是比较先进的:设置一条检测线,同样的检测这一帧和上一帧的像素,如果这条线出现了本来是白像素点,后面变成了黑像素点,那么就是出现了白块了,这样避免了大量的遍历像素点的操作。据此可以进行改进,本文用来记录学习过程,就不扩展代码了。