Opencv计算机视觉编程攻略-第十二节 处理视频序列

      视频由一系列图像构成,这些图像称为帧,帧是以固定时间间隔获取的(称为帧速率,通常用帧/秒表示,例如大疆无人机抽取每一帧),本文将介绍如何读取、处理和存储视频序列。如果从视频序列中提取出独立的帧,就可以对其应用各种图像处理函数,还将学习对视频序列做时序分析的算法,即比较相邻的帧并根据时间累计图像统计数据,以提取前景物体。

目录

1. 读取视频序列

3. 写入视频帧

4. 提取视频前景物体


1. 读取视频序列

        OpenCV 提供了一个便于使用的框架来提取帧,帧的来源可以是视频文件,也可以是USB 或IP 摄像机。

// 1. 打开视频文件 可以设置为相机
cv::VideoCapture capture("bike.avi");

// 2. 检查视频是否成功打开
if (!capture.isOpened())
    return 1;
// 取得帧速率 CV_CAP_PROP_FPS -> 10
double rate= capture.get(CV_CAP_PROP_FPS);
bool stop(false);

cv::Mat frame; // 当前视频帧
cv::namedWindow("Extracted Frame");
// 根据帧速率计算帧之间的等待时间,单位为ms
int delay= 1000/rate;
// 循环遍历视频中的全部帧
while (!stop) {
// 读取下一帧(如果有)
if (!capture.read(frame))
    break;
cv::imshow("Extracted Frame",frame);
// 等待一段时间,或者通过按键停止
    if (cv::waitKey(delay)>=0)
            stop= true;
    }
// 关闭视频文件
// 不是必须的,因为类的析构函数会调用
capture.release();

     

       还可以连接摄像机和计算机,读取摄像机(例如USB 摄像机)生成的视频流。只需在open
函数中指定一个ID(整数)取代原来的文件名即可。ID 为0 表示打开默认摄像机。


2. 处理视频帧

       视频帧处理通过指定一个函数(回调函数),让视频序列的每一帧都调用它。

          // current frame
		  cv::Mat frame;
		  // output frame
		  cv::Mat output;

		  // if no capture device has been set
		  if (!isOpened())
			  return;

		  stop= false;

		  while (!isStopped()) {

			  // read next frame if any
			  if (!readNextFrame(frame))
				  break;

			  // display input frame
			  if (windowNameInput.length()!=0) 
				  cv::imshow(windowNameInput,frame);

		      // calling the process function or method
			  if (callIt) {
				  
				// process the frame
				if (process)
				    process(frame, output);
				else if (frameProcessor) 
					frameProcessor->process(frame,output);
				// increment frame number
			    fnumber++;

			  } else {

				output= frame;
			  }

			  // write output sequence
			  if (outputFile.length()!=0)
				  writeNextFrame(output);

			  // display output frame
			  if (windowNameOutput.length()!=0) 
				  cv::imshow(windowNameOutput,output);
			
			  // introduce a delay
			  if (delay>=0 && cv::waitKey(delay)>=0)
				stopIt();

			  // check if we should stop
			  if (frameToStop>=0 && getFrameNumber()==frameToStop)
				  stopIt();
		  }
	  }

     视频处理性能要求较高,一般使用多线程进行处理,GitHub 上的项目https://github.com/asolis/vivaVideo  展示了一个在OpenCV中用多线程处理视频框架。


3. 写入视频帧

        Opecv::VideoWriter 类写视频文件,构建实例时需指定文件名、播放视频的帧速
率、每个帧的尺寸以及是否为彩色视频:

writer.open(outputFile, // 文件名
        codec, // 所用的编解码器
        framerate, // 视频的帧速率
        frameSize, // 帧的尺寸
        isColor); // 彩色视频?
        另外,必须指明保存视频数据的方式,即codec 参数。

       可以通过反复地调用write 方法,在视频文件中加入帧:writer.write(frame); // 在视频文件中加入帧。在把视频写入文件时,需要使用一个编解码器。编解码器是一个软件模块,用于编码和解码
视频流。
编解码器定义了文件格式和用于存储信息的压缩方案。很明显,用某种编解码器进行编
码的视频,必须用同一种编解码器才能解码。因此人们使用四个字符的代码来指定一种编解码器。
这样,软件工具在写入视频文件之前,需要先读取这个四字符代码,以决定采用哪种编解码器。


4. 提取视频前景物体

        对视频序列进行时序分析,以提取运动中的前景物体,用固定位置的像机拍摄时,背景部分基本上是保持不变的,这种情况下,可以提取出场景中的移动物体。

// 计算当前图像与背景图像之间的差异
cv::absdiff(backgroundImage,currentImage,foreground);

       首先,在计算背景之前需要存储大量的图像;其次,在为计算平均值而累计图像的时候,并没有提取到前景物体。为了计算可靠的背景模型,需要累计何时的、多少数量的图像。更好的策略是用定时更新的方式,动态地构建背景模型。实现方法是计算滑动平均值(又叫移动平均值)。这是一种计算时间信号平均值的方法,该方法还考虑了最新收到的数值。假设pt是时间t 的像素值,μt-1 是当前的平均值,那么要用下面的公式来更新平均值:

        其中参数α 称为学习速率,它决定了当前值对计算平均值有多大影响。这个值越大,滑动平
均值对当前值变化的响应速度就越快;但如果学习速率太大,缓慢移动的物体就可能会消失在背
景中。

      用cv::accumulateWeighted 函数计算图像的滑动平均值非常方便,它在图像的每个像素上应用滑动平均值计算公式。注意,作为结果的图像必须是浮点数类型的。所以,在比较背景模型与当前帧之前,必须先把前者转换成背景图像。对差异绝对值进行阈值化(先用cv::absdiff计算,再用cv::threshold)以提取前景图像。然后把这个前景图像作为cv::accumulateWeighted 函数的掩码,防止修改已被认定为前景的像素。之所以能这么做,是因为在前景图像中,已被认定为前景的像素值为false,即0。

        混合高斯方法是这些改进型算法中的一种。它的处理方式与前面介绍的基本一致,但做了几
项改进。首先,该方法适用于每个像素有不止一个模型(即不止一个滑动平均值)的情况。这样的话,如果一个背景像素在两个值之间波动,那么就会存储两个滑动平均值。只有当新的像素值不属于任何一个频繁出现的模型时,才会认为这个像素是前景。模型的数量可以在参数中设置,通常为5 个。其次,每个模型不仅保存了滑动平均值,还保存了滑动方差。它的计算方法如下所示:

      计算得到的平均值和方差用于构建高斯模型,根据高斯模型就可计算某个像素值属于背景的
概率。用概率替代绝对差值后,阈值的选择就会更加容易。这样,如果某个区域的背景波动较大,
就需要有更大的差值才能被认定为前景物体。