使用 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 很可能为零,后续的相机编号依次递增。如果传递给这些函数的参数是字符串,它将指向一个视频文件,该字符串指明了文件的位置和名称。例如,对于上面的源代码,一个有效的命令行是:
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);// orVideoCapture 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 videocaptRefrnc.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
对于你可以读取和更改的属性,请查阅 cv::VideoCapture::get 和 cv::VideoCapture::set 函数的文档。
图像相似度 —— PSNR 和 SSIM
Section titled “图像相似度 —— PSNR 和 SSIM”我们想检查视频转换操作的进行究竟有多么不可察觉,因此我们需要一个系统来逐帧检查相似度或差异。用于此目的最常见的算法是 PSNR(即峰值信噪比,Peak signal-to-noise ratio)。它最简单的定义从均方误差出发。设有两幅图像:I1 和 I2;其二维尺寸为 i 和 j,由 c 个通道组成。
然后 PSNR 表示为:
这里的 是像素的最大有效值。对于每通道每像素单字节的简单图像,该值为 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上观看一个运行实例。