OpenCV 4.13.0
开源计算机视觉库 (Open Source Computer Vision)
正在加载...
正在搜索...
未找到匹配项
使用 OpenCV 进行视频输入和相似性测量

上一个教程: 使用 GDAL 读取地理空间栅格文件
下一个教程: 使用 OpenCV 创建视频

原作者Bernát Gábor
兼容性OpenCV >= 3.0

目标

如今,拥有数字视频录制系统是很常见的。因此,您最终会遇到不再处理批量图像,而是处理视频流的情况。这些视频流可能是两种类型:实时图像馈送(在网络摄像头的情况下)或预录制并存储在硬盘驱动器上的文件。幸运的是,OpenCV 以相同的方式使用相同的 C++ 类处理这两种情况。因此,在本教程中您将学到以下内容:

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

源代码

为了展示 OpenCV 的这些用法,我创建了一个小程序,该程序读取两个视频文件并对它们进行相似性检查。您可以使用此程序来检查新的视频压缩算法的效果如何。假设有一个参考(原始)视频,例如这个小小的 Megamind 片段,以及它的一个压缩版本。您还可以在 OpenCV 源库的 samples/data 文件夹中找到源代码和这些视频文件。

如何读取视频流(在线摄像头或离线文件)?

本质上,所有视频操作所需的功能都集成在 cv::VideoCapture C++ 类中。它本身基于 FFmpeg 开源库。这是 OpenCV 的一个基本依赖项,所以您不需要担心这个问题。视频由一系列图像组成,我们在文献中称之为帧。在视频文件的情况下,有一个帧率指定了两帧之间的时间长度。而对于摄像机,通常它们每秒可以数字化的帧数是有限制的,这个属性不那么重要,因为摄像机随时都能看到世界的当前快照。

您需要做的第一件事是为 cv::VideoCapture 类分配其源。您可以通过 cv::VideoCapture::VideoCapture 或其 cv::VideoCapture::open 函数来完成此操作。如果此参数是整数,则您将该类绑定到摄像机(一个设备)。此处传递的数字是操作系统分配的设备 ID。如果您的系统连接了一个摄像头,其 ID 可能为零,后续的摄像头 ID 会递增。如果传递给这些参数的参数是字符串,则它将指向一个视频文件,并且该字符串指向文件的位置和名称。例如,对于上面的源代码,一个有效的命令行是

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

我们进行相似性检查。这需要一个参考视频文件和一个测试用例视频文件。前两个参数指的是这些。这里我们使用相对地址。这意味着应用程序将在其当前工作目录中查找并打开视频文件夹,并尝试在其中找到 Megamind.aviMegamind_bug.avi

const string sourceReference = argv[1],sourceCompareWith = argv[2];
VideoCapture captRefrnc(sourceReference);
// 或
VideoCapture captUndTst;
captUndTst.open(sourceCompareWith);
virtual bool open(const String &filename, int apiPreference=CAP_ANY)
打开视频文件、捕获设备或 IP 视频流进行视频捕获。

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

if ( !captRefrnc.isOpened())
{
cout << "无法打开参考 " << sourceReference << endl;
return -1;
}

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

Mat frameReference, frameUnderTest;
captRefrnc >> frameReference;
captUndTst.read(frameUnderTest);
virtual bool read(OutputArray image)
抓取、解码并返回下一个视频帧。

如果无法获取帧(无论是视频流已关闭还是已到达视频文件末尾),上述读取操作将使 Mat 对象为空。我们可以通过一个简单的 if 语句来检查这一点:

if( frameReference.empty() || frameUnderTest.empty())
{
// 退出程序
}
bool empty() const
如果数组没有元素,则返回 true。

读取方法由帧抓取和对其应用的解码组成。您可以通过使用 cv::VideoCapture::grabcv::VideoCapture::retrieve 函数来显式调用这两个操作。

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

Size refS = Size((int) captRefrnc.get(CAP_PROP_FRAME_WIDTH),
(int) captRefrnc.get(CAP_PROP_FRAME_HEIGHT)),
cout << "参考帧分辨率:宽度=" << refS.width << " 高度=" << refS.height
<< " 帧数#: " << captRefrnc.get(CAP_PROP_FRAME_COUNT) << endl;

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

captRefrnc.set(CAP_PROP_POS_MSEC, 1.2); // 跳转到视频的 1.2 秒处
captRefrnc.set(CAP_PROP_POS_FRAMES, 10); // 跳转到视频的第 10 帧
// 现在读取操作将读取设置位置的帧

有关您可以读取和更改的属性,请参阅 cv::VideoCapture::getcv::VideoCapture::set 函数的文档。

图像相似性 - PSNR 和 SSIM

我们想检查视频转换操作的不可察觉程度,因此我们需要一个逐帧检查相似性或差异的系统。最常用的算法是 PSNR(即峰值信噪比)。它的最简单定义始于均方误差。假设有两幅图像:I1 和 I2;二维尺寸为 i 和 j,由 c 个通道组成。

\[MSE = \frac{1}{c*i*j} \sum{(I_1-I_2)^2}\]

然后 PSNR 表示为

\[PSNR = 10 \cdot \log_{10} \left( \frac{MAX_I^2}{MSE} \right)\]

这里 \(MAX_I\) 是像素的最大有效值。在每个通道每像素简单单字节图像的情况下,它是 255。当两幅图像相同时,MSE 将为零,导致 PSNR 公式中的除零操作无效。在这种情况下,PSNR 未定义,我们需要单独处理这种情况。转换为对数刻度是因为像素值具有非常宽的动态范围。所有这些转换为 OpenCV 和一个函数如下所示:

通常,视频压缩的结果值在 30 到 50 之间,越高越好。如果图像显着不同,您将获得更低的值,例如 15 等。这种相似性检查易于计算且速度快,但实际上可能与人眼感知不一致。结构相似性算法旨在纠正这一点。

描述这些方法远远超出了本教程的目的。为此,我邀请您阅读介绍它的文章。尽管如此,您可以通过查看下面的 OpenCV 实现来很好地了解它。

注意
SSIM 在以下文章中有更深入的描述:“Z. Wang, A. C. Bovik, H. R. Sheikh 和 E. P. Simoncelli,《图像质量评估:从误差可见性到结构相似性》,IEEE Transactions on Image Processing,第 13 卷,第 4 期,第 600-612 页,2004 年 4 月。”

这将返回图像每个通道的相似度指数。该值介于零和一之间,其中一表示完美匹配。不幸的是,多次高斯模糊的成本很高,因此虽然 PSNR 可能在实时环境中(每秒 24 帧)工作,但要获得类似的性能结果,这将花费更多的时间。

因此,教程开头提供的源代码将对每一帧执行 PSNR 测量,而 SSIM 仅对 PSNR 低于输入值的帧执行。为了可视化目的,我们在 OpenCV 窗口中显示两幅图像,并将 PSNR 和 MSSIM 值打印到控制台。预计会看到类似以下内容:

您可以在此处观看其运行时实例。