用OpenCV视频输入和相似度测量
目标
今天,您可以使用数字录像系统是很常见的。因此,您最终将会遇到不再处理一批图像但视频流的情况。这些可能有两种:实时图像馈送(在网络摄像机的情况下)或预先记录的和硬盘驱动器存储的文件。幸运的是,OpenCV以相同的方式对同样的C ++类进行了威胁。所以这里是你将在本教程中学到的内容:
- 如何打开和读取视频流
- 检查图像相似性的两种方式:PSNR和SSIM
源代码
作为使用OpenCV显示这些内容的测试案例,我创建了一个小程序,读取两个视频文件,并进行相似之间的检查。这可以用来检查新的视频压缩算法的工作原理。让我们有一个参考(原始)的视频,像这个小Megamind剪辑和它的压缩版本。您也可以samples/data在OpenCV源库的文件夹中找到源代码和这些视频文件。
#include <iostream> // for standard I/O
#include <string> // for strings
#include <iomanip> // for controlling float print precision
#include <sstream> // string to number conversion
#include <opencv2/core.hpp> // Basic OpenCV structures (cv::Mat, Scalar)
#include <opencv2/imgproc.hpp> // Gaussian Blur
#include <opencv2/videoio.hpp>
#include <opencv2/highgui.hpp> // OpenCV window I/O
using namespace std;
using namespace cv;
double getPSNR ( const Mat& I1, const Mat& I2);
Scalar getMSSIM( const Mat& I1, const Mat& I2);
static void help()
{
cout
<< "------------------------------------------------------------------------------" << endl
<< "This program shows how to read a video file with OpenCV. In addition, it "
<< "tests the similarity of two input videos first with PSNR, and for the frames "
<< "below a PSNR trigger value, also with MSSIM." << endl
<< "Usage:" << endl
<< "./video-input-psnr-ssim <referenceVideo> <useCaseTestVideo> <PSNR_Trigger_Value> <Wait_Between_Frames> " << endl
<< "--------------------------------------------------------------------------" << endl
<< endl;
}
int main(int argc, char *argv[])
{
help();
if (argc != 5)
{
cout << "Not enough parameters" << endl;
return -1;
}
stringstream conv;
const string sourceReference = argv[1], sourceCompareWith = argv[2];
int psnrTriggerValue, delay;
conv << argv[3] << endl << argv[4]; // put in the strings
conv >> psnrTriggerValue >> delay; // take out the numbers
int frameNum = -1; // Frame counter
VideoCapture captRefrnc(sourceReference), captUndTst(sourceCompareWith);
if (!captRefrnc.isOpened())
{
cout << "Could not open reference " << sourceReference << endl;
return -1;
}
if (!captUndTst.isOpened())
{
cout << "Could not open case test " << sourceCompareWith << endl;
return -1;
}
Size refS = Size((int) captRefrnc.get(CAP_PROP_FRAME_WIDTH),
(int) captRefrnc.get(CAP_PROP_FRAME_HEIGHT)),
uTSi = Size((int) captUndTst.get(CAP_PROP_FRAME_WIDTH),
(int) captUndTst.get(CAP_PROP_FRAME_HEIGHT));
if (refS != uTSi)
{
cout << "Inputs have different size!!! Closing." << endl;
return -1;
}
const char* WIN_UT = "Under Test";
const char* WIN_RF = "Reference";
// Windows
namedWindow(WIN_RF, WINDOW_AUTOSIZE);
namedWindow(WIN_UT, WINDOW_AUTOSIZE);
moveWindow(WIN_RF, 400 , 0); //750, 2 (bernat =0)
moveWindow(WIN_UT, refS.width, 0); //1500, 2
cout << "Reference frame resolution: Width=" << refS.width << " Height=" << refS.height
<< " of nr#: " << captRefrnc.get(CAP_PROP_FRAME_COUNT) << endl;
cout << "PSNR trigger value " << setiosflags(ios::fixed) << setprecision(3)
<< psnrTriggerValue << endl;
Mat frameReference, frameUnderTest;
double psnrV;
Scalar mssimV;
for(;;) //Show the image captured in the window and repeat
{
captRefrnc >> frameReference;
captUndTst >> frameUnderTest;
if (frameReference.empty() || frameUnderTest.empty())
{
cout << " < < < Game over! > > > ";
break;
}
++frameNum;
cout << "Frame: " << frameNum << "# ";
psnrV = getPSNR(frameReference,frameUnderTest);
cout << setiosflags(ios::fixed) << setprecision(3) << psnrV << "dB";
if (psnrV < psnrTriggerValue && psnrV)
{
mssimV = getMSSIM(frameReference, frameUnderTest);
cout << " MSSIM: "
<< " R " << setiosflags(ios::fixed) << setprecision(2) << mssimV.val[2] * 100 << "%"
<< " G " << setiosflags(ios::fixed) << setprecision(2) << mssimV.val[1] * 100 << "%"
<< " B " << setiosflags(ios::fixed) << setprecision(2) << mssimV.val[0] * 100 << "%";
}
cout << endl;
imshow(WIN_RF, frameReference);
imshow(WIN_UT, frameUnderTest);
char c = (char)waitKey(delay);
if (c == 27) break;
}
return 0;
}
double getPSNR(const Mat& I1, const Mat& I2)
{
Mat s1;
absdiff(I1, I2, s1); // |I1 - I2|
s1.convertTo(s1, CV_32F); // cannot make a square on 8 bits
s1 = s1.mul(s1); // |I1 - I2|^2
Scalar s = sum(s1); // sum elements per channel
double sse = s.val[0] + s.val[1] + s.val[2]; // sum channels
if( sse <= 1e-10) // for small values return zero
return 0;
else
{
double mse = sse / (double)(I1.channels() * I1.total());
double psnr = 10.0 * log10((255 * 255) / mse);
return psnr;
}
}
Scalar getMSSIM( const Mat& i1, const Mat& i2)
{
const double C1 = 6.5025, C2 = 58.5225;
/***************************** INITS **********************************/
int d = CV_32F;
Mat I1, I2;
i1.convertTo(I1, d); // cannot calculate on one byte large values
i2.convertTo(I2, d);
Mat I2_2 = I2.mul(I2); // I2^2
Mat I1_2 = I1.mul(I1); // I1^2
Mat I1_I2 = I1.mul(I2); // I1 * I2
/*************************** END INITS **********************************/
Mat mu1, mu2; // PRELIMINARY COMPUTING
GaussianBlur(I1, mu1, Size(11, 11), 1.5);
GaussianBlur(I2, mu2, Size(11, 11), 1.5);
Mat mu1_2 = mu1.mul(mu1);
Mat mu2_2 = mu2.mul(mu2);
Mat mu1_mu2 = mu1.mul(mu2);
Mat sigma1_2, sigma2_2, sigma12;
GaussianBlur(I1_2, sigma1_2, Size(11, 11), 1.5);
sigma1_2 -= mu1_2;
GaussianBlur(I2_2, sigma2_2, Size(11, 11), 1.5);
sigma2_2 -= mu2_2;
GaussianBlur(I1_I2, sigma12, Size(11, 11), 1.5);
sigma12 -= mu1_mu2;
Mat t1, t2, t3;
t1 = 2 * mu1_mu2 + C1;
t2 = 2 * sigma12 + C2;
t3 = t1.mul(t2); // t3 = ((2*mu1_mu2 + C1).*(2*sigma12 + C2))
t1 = mu1_2 + mu2_2 + C1;
t2 = sigma1_2 + sigma2_2 + C2;
t1 = t1.mul(t2); // t1 =((mu1_2 + mu2_2 + C1).*(sigma1_2 + sigma2_2 + C2))
Mat ssim_map;
divide(t3, t1, ssim_map); // ssim_map = t3./t1;
Scalar mssim = mean(ssim_map); // mssim = average of ssim map
return mssim;
}
如何读取视频流(在线摄像机或离线文件)?
基本上,视频操作所需的所有功能集成在cv :: VideoCapture C ++类中。这本身就建立在FFmpeg开源库上。这是OpenCV的基本依赖,所以你不必担心这一点。视频由连续的图像组成,我们将这些在文献中称为帧。在视频文件的情况下,存在指定两帧之间多长时间的帧速率。而对于摄像机,通常每秒可以限制多少帧可以进行数字化,这个属性不太重要,因为相机会看到当前的世界快照。
您需要做的第一个任务是将其分配给cv :: VideoCapture类的源。您可以通过cv :: VideoCapture :: VideoCapture或其cv :: VideoCapture :: open函数来实现。如果这个参数是一个整数,那么你将把类绑定到一个摄像机,一个设备上。此处传递的数字是由操作系统分配的设备ID。如果您的系统附有一个摄像头,其ID可能会为零,并且从该位置增加。如果传递给这些参数的参数是字符串,它将引用视频文件,并且字符串指向文件的位置和名称。例如,对于较高的源代码,有效的命令行是:
video / Megamind.avi video / Megamind_bug.avi 35 10
我们进行相似检查。这需要一个参考和一个测试用例视频文件。前两个参数是指这个。这里我们使用一个相对地址。这意味着应用程序将查看其当前的工作目录并打开该视频文件夹,并尝试在其中查找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.open(frameUnderTest);
上述操作会留下空席的对象,如果没有框架可能被收购(或者导致视频流关闭或你到了视频文件的末尾)。我们可以用简单的方法来检查:
if( frameReference.empty() || frameUnderTest.empty())
{
// exit the program
}
读取方法由框架抓取和应用的解码构成。您可以使用cv :: VideoCapture :: grab,然后cv :: VideoCapture :: retrieve函数明确地调用这两个。
除了框架的内容,视频还附加了许多信息。这些通常是数字,但在某些情况下,它可能是短字符序列(4字节或更少)。由于获取这些信息,有一个名为cv :: VideoCapture :: get的通用函数返回包含这些属性的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
对于属性,您可以阅读并更改cv :: VideoCapture :: get和cv :: VideoCapture :: set函数的文档。
图像相似度 - PSNR和SSIM
我们要检查我们的视频转换操作是怎么看不见的,因此我们需要一个系统来逐帧检查相似或不同。用于此的最常用的算法是PSNR(也称为峰值信噪比)。最简单的定义是从平均班组错误开始。有两个图像:I1和I2; 具有二维尺寸i和j,由c个通道组成。
那么PSNR表示为:
这里MAXI是像素的最大有效值。在每个通道每像素的简单单字节图像的情况下,这是255.当两个图像相同时,MSE将给出零,导致在PSNR公式中无效除零操作。在这种情况下,PSNR是未定义的,因为我们需要分开处理这种情况。进行到对数刻度的转换是因为像素值具有非常宽的动态范围。所有这一切翻译成OpenCV和一个C ++函数看起来像:
double getPSNR(const Mat& I1, const Mat& I2)
{
Mat s1;
absdiff(I1, I2, s1); // |I1 - I2|
s1.convertTo(s1, CV_32F); // cannot make a square on 8 bits
s1 = s1.mul(s1); // |I1 - I2|^2
Scalar s = sum(s1); // sum elements per channel
double sse = s.val[0] + s.val[1] + s.val[2]; // sum channels
if( sse <= 1e-10) // for small values return zero
return 0;
else
{
double mse =sse /(double)(I1.channels() * I1.total());
double psnr = 10.0*log10((255*255)/mse);
return psnr;
}
}
通常,视频压缩的结果值在30到50之间,其中较高的是更好的。如果图像显着不同,你会得到像15这样低得多的图像。这种相似性检查是容易和快速的计算,但实际上它可能会变得与人眼感觉有些不一致。该结构相似算法旨在纠正这一点。
描述这些方法远远超出了本教程的目的。为此,我邀请你阅读文章介绍。然而,您可以通过查看下面的OpenCV实现来获得良好的图像。
- 也可以看看
- SSIM更深入地描述了:“Z. Wang,AC Bovik,HR Sheikh and EP Simoncelli,”Image quality assessment:From error visibility to structural similarity“,IEEE Transactions on Image Processing,第13卷,第4期,pp。600-612,2004年04月。“ 文章。
Scalar getMSSIM( const Mat& i1, const Mat& i2)
{
const double C1 = 6.5025, C2 = 58.5225;
/***************************** INITS **********************************/
int d = CV_32F;
Mat I1, I2;
i1.convertTo(I1, d); // cannot calculate on one byte large values
i2.convertTo(I2, d);
Mat I2_2 = I2.mul(I2); // I2^2
Mat I1_2 = I1.mul(I1); // I1^2
Mat I1_I2 = I1.mul(I2); // I1 * I2
/***********************PRELIMINARY COMPUTING ******************************/
Mat mu1, mu2; //
GaussianBlur(I1, mu1, Size(11, 11), 1.5);
GaussianBlur(I2, mu2, Size(11, 11), 1.5);
Mat mu1_2 = mu1.mul(mu1);
Mat mu2_2 = mu2.mul(mu2);
Mat mu1_mu2 = mu1.mul(mu2);
Mat sigma1_2, sigma2_2, sigma12;
GaussianBlur(I1_2, sigma1_2, Size(11, 11), 1.5);
sigma1_2 -= mu1_2;
GaussianBlur(I2_2, sigma2_2, Size(11, 11), 1.5);
sigma2_2 -= mu2_2;
GaussianBlur(I1_I2, sigma12, Size(11, 11), 1.5);
sigma12 -= mu1_mu2;
Mat t1, t2, t3;
t1 = 2 * mu1_mu2 + C1;
t2 = 2 * sigma12 + C2;
t3 = t1.mul(t2); // t3 = ((2*mu1_mu2 + C1).*(2*sigma12 + C2))
t1 = mu1_2 + mu2_2 + C1;
t2 = sigma1_2 + sigma2_2 + C2;
t1 = t1.mul(t2); // t1 =((mu1_2 + mu2_2 + C1).*(sigma1_2 + sigma2_2 + C2))
Mat ssim_map;
divide(t3, t1, ssim_map); // ssim_map = t3./t1;
Scalar mssim = mean( ssim_map ); // mssim = average of ssim map
return mssim;
}
这将返回图像的每个通道的相似性索引。该值在零和一之间,其中一个对应于完美契合。不幸的是,许多高斯模糊是相当昂贵的,因此,虽然PSNR可以像环境(24帧/秒)一样在实时工作,但这将比实现类似的性能结果要明显更多。
因此,本教程开始时提供的源代码将为每个帧执行PSNR测量,SSIM仅针对PSNR低于输入值的帧。为了可视化的目的,我们在OpenCV窗口中显示两个图像,并将PSNR和MSSIM值打印到控制台。期待看到像: