HEVC(High Efficiency Video Coding) 是2013年提出的最新的視頻編碼標準,它的核心是將視頻中的每一幀圖像分割成 Coding Tree Unit(CTU) 个初,然后對每個CTU確定最佳的分割深度怎披,關于具體的算法可以參考:
- Overview of the High Efficiency Video Coding(HEVC) Standard
- H.265 Terms - CTU, CU, PU, TU
- 2.H.265/HEVC —— 幀內預測
在了解了一些HEVC內部的術語如CTU, CU, TU...后疲牵,我們來試著利用HEVC的源代碼輸出CTU的分割信息。
代碼的下載和配置
首先疏尿,需要下載源代碼,源代碼是使用SVN托管的易桃,因此需要用SVN下載褥琐,我們可以使用TortoiseSVN進行下載。安裝好TortoiseSVN后晤郑,打開SVN browser踩衩,在地址欄輸入
https://hevc.hhi.fraunhofer.de/svn/svn_HEVCSoftware/
就可以看到源代碼的目錄結構,然后選擇export贩汉,就可以將源代碼保存到本地。注意這個目錄下面有很多個版本锚赤,我們這里選擇/trunk/
目錄下的主版本導出匹舞。
導出到本地之后,進入/build/
文件夾线脚,里面就是編譯所需的文件赐稽,支持不同的平臺:
我使用的是VS2015編譯,打開這個工程之后浑侥,可以看到所有的項目姊舵,對應的源代碼在/source/
文件夾下:
選擇F7或者Build Solution就可以進行編譯,生成exe文件寓落,但是如果你直接去運行的話會發(fā)現程序輸出一段說明文字之后就結束了括丁,這是因為我們需要先進行配置,指定輸入的視頻和其他的一些參數伶选。
配置文件在/cfg/
目錄下史飞,其中最主要的兩個配置文件是bitstream.cfg
和encoder_intra_main.cfg
,我們把這兩個文件復制到一個自定義工作目錄下仰税,比如E:\HM\trunk\workspace
构资,接下來在Visual Studio中,右擊解決方案中“TAppEncoder”->“設為啟動項目”
再右擊“TAppEncoder”->”屬性”->”配置屬性”->”調試” 陨簇,在工作目錄欄指定工作目錄路徑E:\HM\trunk\workspace
吐绵,在命令參數欄中填寫-c encoder_intra_main.cfg -c bitstream.cfg
,如圖:
對于encoder_intra_main.cfg
河绽,我們需要進行配置的參數就只有:
#======== Quantization =============
QP : 32 # Quantization parameter(0-51)
這個值從0到51都可以己单。而在bitstream.cfg
中,我們需要配置以下的參數:
InputFile
后面寫上輸入的.YUV
視頻文件的絕對路徑葵姥,FramesToBeEncoded
是指你想編碼多少幀荷鼠,至于FrameRate
, SourceWidth
, SourceHeight
這些信息都在視頻文件名中,比如我這里使用的BasketballDrill_832x480_50.yuv
榔幸,文件名中就能看出寬度允乐、高度和幀率矮嫉。
接下來就可以運行了,運行結束后牍疏,工作目錄下會出現兩個文件:str.bin和rec.yuv蠢笋,其中rec.yuv是編碼過程中重建的yuv圖像,str.bin則是壓縮后的碼流鳞陨。
YUV文件
首先提供一些YUV視頻資源的下載地址:
對于HEVC的測試輸入昨寞,一般都是采用YUV文件,推薦安裝一個YUV播放器:YUView厦滤,可以看到每一幀的圖片援岩。
YUV文件就是由一幀一幀的圖像構成的,在之前提到的配置文件中的參數FramesToBeEncoded
中掏导,你指定的編碼多少幀享怀,HEVC就會取前面的多少幀進行編碼。如果想提取出YUV文件中的每一幀趟咆,可以使用FFmpeg:
ffmpeg -video_size 832x480 -r 50 -pixel_format yuv420p -i BasketballDrill_832x480_50.yuv output-%d.png
當然添瓷,使用的時候這個命令中的視頻大小、幀率值纱、像素格式鳞贷、視頻名稱都需要根據具體的視頻修改。其中像素格式(-pixel_format)這個參數可能很多人不知道該選什么虐唠,在命令行中輸入:
ffmpeg -pix_fmts
可以看到FFmpeg支持的格式搀愧,這里使用的yuv420p
是一般采用的格式,420代表的是 YCbCr color space with 4:2:0 sampling.
This separates a color representation into three components called Y, Cb, and Cr. The Y component is also called luma, and represents brightness. The two chroma components Cb and Cr represent the extent to which the color deviates from gray toward blue and red, respectively.
關于color space疆偿,可以參考:
colorspace – FFmpeg
如果不想了解這么多妈橄,只想直接知道自己所使用的YUV應該對應哪種 pixel format,方法是使用YUView打開YUV文件翁脆,在右側的properties
中有一個YUV Format眷蚓,然后用這個format,去對應:
Most commonly used formats | pixel_format |
---|---|
8-bit 4:2:0 | yuv420p |
8-bit 4:2:2 | yuv422p |
8-bit 4:4:4 | yuv444p |
10-bit 4:2:0 | yuv420p10le |
10-bit 4:2:2 | yuv422p10le |
參考自:Chroma Subsampling – FFmpeg
CTU分割信息的輸出
這一部分是在Encoder部分完成的反番,所以我們主要看TAppEncoder這個項目沙热。在TEncGOP.cpp
中,有一個precompressSlice()
和一個compressSlice()
函數罢缸,前者可以不用管篙贸,而后者則計算出了每個ctu的最佳分割深度,轉到compressSlice()
函數的定義枫疆,來到了TEncSlice.cpp
爵川,其中對ctu進行最佳深度計算的關鍵函數是compressCtu()
這個函數。
在compressCtu()
這個函數內部息楔,當xCompressCU()
這個函數運行完成之后寝贡,最佳的分割深度就已經得到了扒披,此時,如果想要輸出當前ctu最佳的分割圃泡,可以在xCompressCU()
這個函數后面碟案,加上下面的語句:
xCompressCU( m_ppcBestCU[0], m_ppcTempCU[0], 0 DEBUG_STRING_PASS_INTO(sDebug) );
//============== add code from here ===============
TComDataCU* DepthCU = m_ppcBestCU[0];
UInt tempDepth;
ofstream outfile("PartitionInfo.txt", ios::in | ios::app);
for (UInt iPartitionNum = 0; iPartitionNum < DepthCU->getTotalNumPart(); iPartitionNum++)
{
if (iPartitionNum % 16 == 0) {
outfile << endl;
}
tempDepth = DepthCU->getDepth(g_auiRasterToZscan[iPartitionNum]);
outfile << " " << tempDepth;
}
outfile << endl;
// =============code added end here===============
最佳的分割保存在m_ppcBestCU[0]
里面,使用getDepth()[i]
就可以得到颇蜡。
接下來价说,如果我們不僅想輸出所有的ctu分割信息,還想輸出當前是第幾個ctu风秤,這時候就要去找哪個變量記錄了當前ctu的編號鳖目,我們可以回到compressCtu()
函數開始的地方,看到這個函數接收了一個參數TComDataCU* pCtu
缤弦,而我們可以通過pCtu->getCtuRsAddr()
這個語句得到當前CTU的編號疑苔,所以,可以在xCompressCU()
之前甸鸟,加上下面的語句:
//============== add code from here ===============
ofstream outfile("PartitionInfo.txt", ios::in | ios::app);
UInt temp_ctu_addr = pCtu->getCtuRsAddr();
outfile << "ctu:" << temp_ctu_addr << endl;
outfile.close();
// =============code added end here===============
xCompressCU(m_ppcBestCU[0], m_ppcTempCU[0], 0 DEBUG_STRING_PASS_INTO(sDebug));
除了輸出當前ctu的編號,我們還想要輸出當前編碼到了第幾幀(frame)兵迅,這時仍然可以通過Debug的方式找到存儲這一信息的變量抢韭。最終發(fā)現當前編碼的frame數量存儲在變量m_iFrameRcvd
中,而這一變量是在TAppEncTop.cpp
文件中恍箭。因此刻恭,如果要輸出當前是第幾幀,可以在TAppEncTop.cpp
文件中找到
m_iFrameRcvd++;
這一語句扯夭,并在后面加入:
ofstream outfile("PartitionInfo.txt", ios::in | ios::app);
outfile<< "frame:" << m_iFrameRcvd << endl;
這樣鳍贾,我們就可以輸出每個frame對應的每個ctu的分割信息了,像這樣:
然后我們可以使用Matlab畫出每一幀的PU分割信息交洗,參考:
最后補充一下骑科,HEVC在幀內預測的時候,對于每個depth构拳,都要向下分成4個depth遞歸尋找最優(yōu)分割咆爽,因此我們還可以把每個depth對應的RD-cost打印出來。Depth0會分成4個Depth1置森,Depth1會分成4個Depth2,Depth2分成4個Depth3,Depth3向下判斷是8x8還是4x4的PU
打印出每種分割的RD-cost的好處就是愧膀,當我們訓練好了神經網絡模型旅东,用模型生成CU分割深度信息的時候,我們可以用RD-cost來和HEVC編碼器比較RD-cost變化了多少行贪,而神經網絡模型得到的分割方法的RD-cost的計算就可以用之前打印出來的每個depth的RD-cost
在xCompressCU()
函數內部漾稀,有兩個變量:m_ppcBestCU
和m_ppcTempCU
,想要得到當前深度的RD-cost可以通過調用它們的getTotalCost()
方法围橡,不過m_ppcBestCU
只存放了最佳的分割深度翁授,所以不能得到全面的RD-cost晾咪,因此只能調用m_ppcTempCU
的getTotalCost()
方法谍倦。但m_ppcTempCU
在xCompressCU()
函數內部并沒有存放或者被賦值RD-cost信息昼蛀,我們只能到xCheckRDCostIntra()
函數內部來獲取。
在xCheckRDCostIntra()
函數內部仇哆,當前所在的深度存放在變量uiDepth
中讹剔。我們可以在
rpcTempCU->getTotalCost() = m_pcRdCost->calcRdCost( rpcTempCU->getTotalBits(), rpcTempCU->getTotalDistortion() );
之后延欠,添加如下代碼沈跨,輸出每一層深度的RD-cost:
rpcTempCU->getTotalBits() = m_pcEntropyCoder->getNumberOfWrittenBits();
rpcTempCU->getTotalBins() = ((TEncBinCABAC *)((TEncSbac*)m_pcEntropyCoder->m_pcEntropyCoderIf)->getEncBinIf())->getBinsCoded();
rpcTempCU->getTotalCost() = m_pcRdCost->calcRdCost( rpcTempCU->getTotalBits(), rpcTempCU->getTotalDistortion() );
// ==============added code to print depth and RDcost==========
ofstream outfile("rdcost.txt", ios::in | ios::app);
double temp_rdcost;
temp_rdcost = rpcTempCU->getTotalCost();
outfile << "depth:" << uiDepth << endl;
outfile << temp_rdcost << endl;
outfile.close();
// ===============code added end here=====================
xCheckDQP( rpcTempCU );
同樣饿凛,我們還是需要輸出是在哪一幀,以及哪個CTU笤喳。輸出之后,在進行一些處理蒙畴,就能方便地計算自己的模型的CU劃分對應的RD-cost了。