今天,您可以使用數(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)容:
作為使用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;
}
基本上,視頻操作所需的所有功能集成在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.avi和Megamind_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 :: get和cv :: VideoCapture :: set函數(shù)的文檔。
我們要檢查我們的視頻轉(zhuǎn)換操作是怎么看不見(jiàn)的,因此我們需要一個(gè)系統(tǒng)來(lái)逐幀檢查相似或不同。用于此的最常用的算法是PSNR(也稱為峰值信噪比)。最簡(jiǎn)單的定義是從平均班組錯(cuò)誤開(kāi)始。有兩個(gè)圖像:I1和I2; 具有二維尺寸i和j,由c個(gè)通道組成。
那么PSNR表示為:
這里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)獲得良好的圖像。
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)。期待看到像:
更多建議: