用OpenCV視頻輸入和相似度測(cè)量

2018-10-02 10:32 更新

目標(biāo)

今天,您可以使用數(shù)字錄像系統(tǒng)是很常見(jiàn)的。因此,您最終將會(huì)遇到不再處理一批圖像但視頻流的情況。這些可能有兩種:實(shí)時(shí)圖像饋送(在網(wǎng)絡(luò)攝像機(jī)的情況下)或預(yù)先記錄的和硬盤驅(qū)動(dòng)器存儲(chǔ)的文件。幸運(yùn)的是,OpenCV以相同的方式對(duì)同樣的C ++類進(jìn)行了威脅。所以這里是你將在本教程中學(xué)到的內(nèi)容:

  • 如何打開(kāi)和讀取視頻流
  • 檢查圖像相似性的兩種方式:PSNR和SSIM

源代碼

作為使用OpenCV顯示這些內(nèi)容的測(cè)試案例,我創(chuàng)建了一個(gè)小程序,讀取兩個(gè)視頻文件,并進(jìn)行相似之間的檢查。這可以用來(lái)檢查新的視頻壓縮算法的工作原理。讓我們有一個(gè)參考(原始)的視頻,像這個(gè)小Megamind剪輯它的壓縮版本。您也可以samples/data在OpenCV源庫(kù)的文件夾中找到源代碼和這些視頻文件。

#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;
}

如何讀取視頻流(在線攝像機(jī)或離線文件)?

基本上,視頻操作所需的所有功能集成在cv :: VideoCapture C ++類中。這本身就建立在FFmpeg開(kāi)源庫(kù)上。這是OpenCV的基本依賴,所以你不必?fù)?dān)心這一點(diǎn)。視頻由連續(xù)的圖像組成,我們將這些在文獻(xiàn)中稱為幀。在視頻文件的情況下,存在指定兩幀之間多長(zhǎng)時(shí)間的幀速率。而對(duì)于攝像機(jī),通常每秒可以限制多少幀可以進(jìn)行數(shù)字化,這個(gè)屬性不太重要,因?yàn)橄鄼C(jī)會(huì)看到當(dāng)前的世界快照。

您需要做的第一個(gè)任務(wù)是將其分配給cv :: VideoCapture類的源。您可以通過(guò)cv :: VideoCapture :: VideoCapture或其cv :: VideoCapture :: open函數(shù)來(lái)實(shí)現(xiàn)。如果這個(gè)參數(shù)是一個(gè)整數(shù),那么你將把類綁定到一個(gè)攝像機(jī),一個(gè)設(shè)備上。此處傳遞的數(shù)字是由操作系統(tǒng)分配的設(shè)備ID。如果您的系統(tǒng)附有一個(gè)攝像頭,其ID可能會(huì)為零,并且從該位置增加。如果傳遞給這些參數(shù)的參數(shù)是字符串,它將引用視頻文件,并且字符串指向文件的位置和名稱。例如,對(duì)于較高的源代碼,有效的命令行是:

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

我們進(jìn)行相似檢查。這需要一個(gè)參考和一個(gè)測(cè)試用例視頻文件。前兩個(gè)參數(shù)是指這個(gè)。這里我們使用一個(gè)相對(duì)地址。這意味著應(yīng)用程序?qū)⒉榭雌洚?dāng)前的工作目錄并打開(kāi)該視頻文件夾,并嘗試在其中查找Megamind.aviMegamind_bug.avi。

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

要檢查類與視頻源的綁定是否成功,請(qǐng)使用cv :: VideoCapture :: isOpened函數(shù):

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

當(dāng)調(diào)用對(duì)象析構(gòu)函數(shù)時(shí),關(guān)閉視頻是自動(dòng)的。但是,如果要在此之前關(guān)閉它,則需要調(diào)用其cv :: VideoCapture :: release函數(shù)。視頻的幀只是簡(jiǎn)單的圖像。因此,我們只需要從cv :: VideoCapture對(duì)象中提取它們,并將它們放在Mat中。視頻流是連續(xù)的。你可以通過(guò)cv :: VideoCapture :: read或者重載的>>操作符來(lái)接收幀:

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

上述操作會(huì)留下空席的對(duì)象,如果沒(méi)有框架可能被收購(gòu)(或者導(dǎo)致視頻流關(guān)閉或你到了視頻文件的末尾)。我們可以用簡(jiǎn)單的方法來(lái)檢查:

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

讀取方法由框架抓取和應(yīng)用的解碼構(gòu)成。您可以使用cv :: VideoCapture :: grab,然后cv :: VideoCapture :: retrieve函數(shù)明確地調(diào)用這兩個(gè)。

除了框架的內(nèi)容,視頻還附加了許多信息。這些通常是數(shù)字,但在某些情況下,它可能是短字符序列(4字節(jié)或更少)。由于獲取這些信息,有一個(gè)名為cv :: VideoCapture :: get的通用函數(shù)返回包含這些屬性的double值。使用按位操作來(lái)解碼來(lái)自雙重類型的字符和有效值僅為整數(shù)的轉(zhuǎn)換。其唯一的參數(shù)是被查詢屬性的ID。例如,這里我們得到參考和測(cè)試用例視頻文件中的幀大小; 加上參考文獻(xiàn)中的幀數(shù)。

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;

當(dāng)您使用視頻時(shí),您可能經(jīng)常希望自己控制這些值。要做到這一點(diǎn),有一個(gè)cv :: VideoCapture :: set函數(shù)。它的第一個(gè)參數(shù)仍然是您要更改的屬性的名稱,而另一個(gè)double類型包含要設(shè)置的值。如果成功則返回true,否則返回false。一個(gè)很好的例子是在一個(gè)給定的時(shí)間或框架的視頻文件中尋找:

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

對(duì)于屬性,您可以閱讀并更改cv :: VideoCapture :: getcv :: VideoCapture :: set函數(shù)的文檔。

圖像相似度 - PSNR和SSIM

我們要檢查我們的視頻轉(zhuǎn)換操作是怎么看不見(jiàn)的,因此我們需要一個(gè)系統(tǒng)來(lái)逐幀檢查相似或不同。用于此的最常用的算法是PSNR(也稱為峰值信噪比)。最簡(jiǎn)單的定義是從平均班組錯(cuò)誤開(kāi)始。有兩個(gè)圖像:I1和I2; 具有二維尺寸i和j,由c個(gè)通道組成。

用OpenCV視頻輸入和相似度測(cè)量


那么PSNR表示為:

用OpenCV視頻輸入和相似度測(cè)量

這里MAXI是像素的最大有效值。在每個(gè)通道每像素的簡(jiǎn)單單字節(jié)圖像的情況下,這是255.當(dāng)兩個(gè)圖像相同時(shí),MSE將給出零,導(dǎo)致在PSNR公式中無(wú)效除零操作。在這種情況下,PSNR是未定義的,因?yàn)槲覀冃枰珠_(kāi)處理這種情況。進(jìn)行到對(duì)數(shù)刻度的轉(zhuǎn)換是因?yàn)橄袼刂稻哂蟹浅挼膭?dòng)態(tài)范圍。所有這一切翻譯成OpenCV和一個(gè)C ++函數(shù)看起來(lái)像:

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;
 }
}

通常,視頻壓縮的結(jié)果值在30到50之間,其中較高的是更好的。如果圖像顯著不同,你會(huì)得到像15這樣低得多的圖像。這種相似性檢查是容易和快速的計(jì)算,但實(shí)際上它可能會(huì)變得與人眼感覺(jué)有些不一致。該結(jié)構(gòu)相似算法旨在糾正這一點(diǎn)。

描述這些方法遠(yuǎn)遠(yuǎn)超出了本教程的目的。為此,我邀請(qǐng)你閱讀文章介紹。然而,您可以通過(guò)查看下面的OpenCV實(shí)現(xiàn)來(lái)獲得良好的圖像。

也可以看看
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月?!?span> 文章。
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;
}

這將返回圖像的每個(gè)通道的相似性索引。該值在零和一之間,其中一個(gè)對(duì)應(yīng)于完美契合。不幸的是,許多高斯模糊是相當(dāng)昂貴的,因此,雖然PSNR可以像環(huán)境(24幀/秒)一樣在實(shí)時(shí)工作,但這將比實(shí)現(xiàn)類似的性能結(jié)果要明顯更多。

因此,本教程開(kāi)始時(shí)提供的源代碼將為每個(gè)幀執(zhí)行PSNR測(cè)量,SSIM僅針對(duì)PSNR低于輸入值的幀。為了可視化的目的,我們?cè)贠penCV窗口中顯示兩個(gè)圖像,并將PSNR和MSSIM值打印到控制臺(tái)。期待看到像:

用OpenCV視頻輸入和相似度測(cè)量

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)