阅读(3719) (0)

用OpenCV视频输入和相似度测量

2017-09-29 10:34:42 更新

目标

今天,您可以使用数字录像系统是很常见的。因此,您最终将会遇到不再处理一批图像但视频流的情况。这些可能有两种:实时图像馈送(在网络摄像机的情况下)或预先记录的和硬盘驱动器存储的文件。幸运的是,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.aviMegamind_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 :: getcv :: VideoCapture :: set函数的文档。

图像相似度 - PSNR和SSIM

我们要检查我们的视频转换操作是怎么看不见的,因此我们需要一个系统来逐帧检查相似或不同。用于此的最常用的算法是PSNR(也称为峰值信噪比)。最简单的定义是从平均班组错误开始。有两个图像:I1和I2; 具有二维尺寸i和j,由c个通道组成。

用OpenCV视频输入和相似度测量


那么PSNR表示为:

用OpenCV视频输入和相似度测量

这里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值打印到控制台。期待看到像:

用OpenCV视频输入和相似度测量