Skip to content

使用 OpenCV 进行视频输入与相似度测量

如今,手头拥有一套数字视频录制系统已十分常见。因此,你终将面临不再处理一批图像、而是处理视频流的情况。视频流可能有两类:实时图像源(例如网络摄像头)或预录制并存储在硬盘上的文件。幸运的是,OpenCV 以同样的方式、用同一个 C++ 类来处理这两种情况。以下是你在本教程中将学到的内容:

  • 如何打开并读取视频流
  • 检查图像相似度的两种方法:PSNR 和 SSIM

为了展示如何使用 OpenCV 实现这些功能,我创建了一个小程序,它读入两个视频文件,并在它们之间执行相似度检查。你可以用这个来检验一种新的视频压缩算法的效果究竟如何。假设有一个参考(原始)视频,例如这段 Megamind 小片段和它的压缩版本。你也可以在 OpenCV 源代码库的 samples/data 文件夹中找到源代码和这些视频文件。

代码见下方示例 代码见下方示例

如何读取视频流(在线相机或离线文件)?

Section titled “如何读取视频流(在线相机或离线文件)?”

本质上,视频处理所需的所有功能都集成在 cv::VideoCapture 这个 C++ 类中。它本身构建在 FFmpeg 开源库之上。这是 OpenCV 的一个基本依赖,因此你无需为此担心。视频由一连串图像组成,我们在文献中将这些图像称为帧(frame)。对于视频文件,有一个帧率指定了两帧之间的时间间隔。而对于摄像机,通常存在每秒可数字化帧数的上限,不过这一属性不太重要,因为相机在任何时刻看到的都是世界的当前快照。

你需要做的第一件事是为 cv::VideoCapture 类指定其数据源。你可以通过 cv::VideoCapture::VideoCapture 或其 cv::VideoCapture::open 函数来完成。如果该参数是一个整数,你将把该类绑定到一台相机、一个设备上。此处传入的数字是由操作系统分配的设备 ID。如果你只连接了一台相机,其 ID 很可能为零,后续的相机编号依次递增。如果传递给这些函数的参数是字符串,它将指向一个视频文件,该字符串指明了文件的位置和名称。例如,对于上面的源代码,一个有效的命令行是:

Terminal window
video/Megamind.avi video/Megamind_bug.avi 35 10

我们执行相似度检查。这需要一个参考视频文件和一个测试用例视频文件。前两个参数即对应于此。这里我们使用相对地址。这意味着应用程序会在其当前工作目录下查找,打开 video 文件夹,并尝试在其中找到 Megamind.avi 和 Megamind_bug.avi。

const string sourceReference = argv[1],sourceCompareWith = argv[2];
VideoCapture captRefrnc(sourceReference);
// or
VideoCapture captUndTst;
captUndTst.open(sourceCompareWith);

要检查该类到视频源的绑定是否成功,使用 cv::VideoCapture::isOpened 函数:

if ( !captRefrnc.isOpened())
{
cout << "Could not open reference " << sourceReference << endl;
return -1;
}

当对象析构函数被调用时,视频会自动关闭。但是,如果你想在此之前关闭它,则需要调用其 cv::VideoCapture::release 函数。视频的帧只是普通的图像。因此,我们只需将它们从 cv::VideoCapture 对象中提取出来,放入一个 Mat 对象中即可。视频流是顺序的。你可以通过 cv::VideoCapture::read 或重载的 >> 运算符一帧接一帧地获取帧:

Mat frameReference, frameUnderTest;
captRefrnc >> frameReference;
captUndTst.read(frameUnderTest);

如果无法获取帧(可能因为视频流已关闭或已到达视频文件末尾),上面的读取操作将使 Mat 对象保持为空。我们可以用一个简单的 if 来检查:

if( frameReference.empty() || frameUnderTest.empty())
{
// exit the program
}

一个 read 方法由一次帧抓取和对其的一次解码组成。你可以通过使用 cv::VideoCapture::grab 然后是 cv::VideoCapture::retrieve 函数来显式地调用这两步。

除了帧的内容之外,视频还附带许多许多的信息。这些通常是数字,但在某些情况下也可能是短字符序列(4 字节或更少)。因此,为了获取这些信息,有一个名为 cv::VideoCapture::get 的通用函数,它返回包含这些属性的 double 值。使用按位运算从 double 类型中解码出字符,并在有效值仅为整数时进行转换。它的单一参数是所查询属性的 ID。例如,这里我们获取参考和测试用例视频文件中帧的大小,以及参考视频中帧的数量。

Size refS = Size((int) captRefrnc.get(CAP_PROP_FRAME_WIDTH),
(int) captRefrnc.get(CAP_PROP_FRAME_HEIGHT)),
cout << "Reference frame resolution: Width=" << refS.width << " Height=" << refS.height
<< " of nr#: " << captRefrnc.get(CAP_PROP_FRAME_COUNT) << endl;

在使用视频时,你可能经常想自己控制这些值。为此有一个 cv::VideoCapture::set 函数。它的第一个参数仍然是你要更改的属性名称,第二个是包含要设置的值的 double 类型。成功时返回 true,否则返回 false。一个很好的例子是在视频文件中跳转到给定的时间或帧:

captRefrnc.set(CAP_PROP_POS_MSEC, 1.2); // go to the 1.2 second in the video
captRefrnc.set(CAP_PROP_POS_FRAMES, 10); // go to the 10th frame of the video
// now a read operation would read the frame at the set position

outputVideoInput.png

对于你可以读取和更改的属性,请查阅 cv::VideoCapture::get 和 cv::VideoCapture::set 函数的文档。

我们想检查视频转换操作的进行究竟有多么不可察觉,因此我们需要一个系统来逐帧检查相似度或差异。用于此目的最常见的算法是 PSNR(即峰值信噪比,Peak signal-to-noise ratio)。它最简单的定义从均方误差出发。设有两幅图像:I1 和 I2;其二维尺寸为 i 和 j,由 c 个通道组成。

MSE=1c∗i∗j∑(I1−I2)2MSE = \frac{1}{c*i*j} \sum{(I_1-I_2)^2}

然后 PSNR 表示为:

PSNR=10⋅log⁡10(MAXI2MSE)PSNR = 10 \cdot \log_{10} \left( \frac{MAX_I^2}{MSE} \right)

这里的 MAXIMAX_I 是像素的最大有效值。对于每通道每像素单字节的简单图像,该值为 255。当两幅图像相同时,MSE 将为零,从而导致 PSNR 公式中出现无效的除以零运算。在这种情况下 PSNR 是未定义的,我们需要单独处理这种情况。之所以转换到对数尺度,是因为像素值具有非常宽的动态范围。将所有这些转换成 OpenCV 的函数如下所示:

对于视频压缩,典型结果值在 30 到 50 之间,值越大越好。如果图像差异显著,你会得到低得多的值,比如 15 等等。这种相似度检查计算简单且快速,但在实践中,它可能与人类视觉感知有些不一致。结构相似性(structural similarity)算法旨在纠正这一点。

对该方法的描述已远远超出本教程的范围。为此,我邀请你阅读介绍它的文章。不过,你可以通过查看下面的 OpenCV 实现来获得一个很好的印象。

注意: SSIM 在以下文章中有更深入的描述:“Z. Wang, A. C. Bovik, H. R. Sheikh and E. P. Simoncelli, “Image quality assessment: From error visibility to structural similarity,” IEEE Transactions on Image Processing, vol. 13, no. 4, pp. 600-612, Apr. 2004.”

这将为图像的每个通道返回一个相似度索引。该值在零到一之间,其中一对应完美匹配。不幸的是,大量的高斯模糊开销相当大,因此虽然 PSNR 可以在实时环境(每秒 24 帧)下工作,但要达到类似的性能,SSIM 所需的时间要多得多。

因此,本教程开头给出的源代码将对每一帧执行 PSNR 测量,而仅在 PSNR 低于某个输入值时才对相应帧执行 SSIM。为了便于可视化,我们在一个 OpenCV 窗口中显示这两幅图像,并将 PSNR 和 MSSIM 值打印到控制台。预期会看到类似下面的内容:

你可以在这里的 YouTube上观看一个运行实例。