網上很多大佬對VVC的代碼進行過分析,基本都是從編碼端入手。
考慮到從解碼端分析代碼,一是更加簡單(解碼流程無需編碼工具和編碼參數的擇優),二是可以配合Draft文本更好地理解視頻編解碼的流程(解碼端也都包含預測、量化、環路濾波、熵解碼等流程),所以我想從解碼端入手分析一下VVC大致的流程。等到解碼端代碼分析完後,可以再從編碼端深入分析一下。
本文是本系列的第一篇博客,內容是分析解碼端將收到的二進制碼流bin文件提取成一個個NALU的過程。
注:
- 本文分析的bin文件是利用VTM8.0的編碼器,以All Intra配置(IBC 打開)編碼100幀得到的二進制碼流(TemporalSubsampleRatio: 8,實際編碼 ⌈100 / 8⌉ = 13幀)。
- 解碼用最簡單的:-b str.bin -o dec.yuv
1. 入口函數是decmain.cpp 中的main()函數
(1) main函數先會打印若干信息,比如設備信息。
(2) 調用 DecAppCfg::parseCfg() 解析輸入參數,如本文所用的bin文件的名字、重建YUV文件的名字等信息。
(3) 調用 DecApp::decode() 開啓解碼流程。
2. DecApp::decode()函數
(1) 首先構造了名爲nalu的 InputNALUnit 類。
InputNALUnit nalu;
① InputNALUnit是NALUnit的子類,NALUnit類中包含NalUnitType、temporalId等相關信息、還含有判斷當前NALU是不是VCLU和SliceNALU的成員函數isVcl() 、isSlice()。
② 此外,InputNALUnit多包含了名爲m_Bitstream數據成員(InputBitstream類)。
③ InputBitstream類中的m_fifo數據成員以FIFO順序存儲當前NALU的Bytes數據。
class InputNALUnit : public NALUnit
{
private:
InputBitstream m_Bitstream;
……
};
struct NALUnit
{
NalUnitType m_nalUnitType; ///< nal_unit_type
uint32_t m_temporalId; ///< temporal_id
uint32_t m_nuhLayerId; ///< nuh_layer_id
uint32_t m_forbiddenZeroBit;
uint32_t m_nuhReservedZeroBit;
……
}
(2) 然後判斷下一個NALU是不是新圖像/AU的第一個NALU。
check if next NAL unit will be the first NAL unit from a new picture or access unit
bool bNewPicture = isNewPicture(&bitstreamFile, &bytestream);
bool bNewAccessUnit = bNewPicture && isNewAccessUnit( bNewPicture, &bitstreamFile, &bytestream );
bool DecApp::isNewPicture(ifstream *bitstreamFile, class InputByteStream *bytestream)
{
// cannot be a new picture if there haven't been any slices yet、
//該函數檢查Declib::m_bFirstSliceInPicture變量,見下段代碼。
if(m_cDecLib.getFirstSliceInPicture())
{
return false;
}
……
bool getFirstSliceInPicture () const
{ return m_bFirstSliceInPicture; }
Declib::m_bFirstSliceInPicture是一個bool值,該bool值默認爲true。在DecSlice::decompressSlice()之後,該值會被設置爲false,表示一個slice編碼完畢。然後isNewPicture()函數會預先讀取下一個NALU的NalUnitType,例如,如果下一個NALU是SPS、PPS等參數集,那就說明是新的圖像的新NALU,返回True。
3. bNewPicture == false,進入NALU的提取環節,爲本文的分析重點。否則進入第4步,完成該幀的後續處理和輸出環節。
(1) 調用byteStreamNALUnit() 解析當前NALU。該函數核心調用_byteStreamNALUnit()。
// find next NAL unit in stream
byteStreamNALUnit(bytestream, nalu.getBitstream().getFifo(), stats);
(2) 根據Draft中B.2.1的語義去提取NALU。
具體步驟爲:
① 首先,檢查連續 3 / 4 個Bytes 是不是 0x000001 / 0x00000001。如果不是,去掉前綴1個Bytes的0x00。
② 然後,檢查連續 3 個Bytes 是不是 0x000001。如果不是,去掉前綴1個Bytes的0x00。
③ 接着,讀取接下來連續3個Bytes,應該爲起始碼0x000001。
④ 核心步驟,依次連續讀取接下來連續3個Bytes,每次後移一個Bytes。
如果不是0x000000 或 0x000001 說明還是在該NALU的內容裏面,塞進當前NALU。
否則,該3個Bytes是新的NALU的內容,退出該步驟。
⑤ 最後,檢查接下來連續 3 / 4 個Bytes 是不是 0x000001 / 0x00000001。如果不是,去掉尾綴1個Bytes的0x00。
比如上面從00 00 00 01開始到DC 0D 56 81是一個NALU的內容。
- 首先,進入第①步,讀取4個Bytes爲0x00000001,所以無需去掉前綴0x00。
- 然後,進入第②步,前3個Bytes爲0x000000,不是 0x000001,去掉最前面的0x00。
- 接着,進入第③步,讀取起始碼0x000001。
- 後面是核心步驟,依次連續讀取接下來連續3個Bytes,每次後移一個Bytes。上面的數據就應該是依次讀取“00 79 00”、“79 00 01”、“00 01 02”、……。如果不是0x000000 或 0x000001 說明還是在該NALU的內容裏面,將第一個Bytes (0x00 0x79 0x01 0x02 ……) 塞進當前NALU。否則,該3個Bytes是新的NALU的內容,退出該步驟。
- 最後,檢查接下來連續 3 / 4 個Bytes 是不是 0x000001 / 0x00000001。該數據後面4個Bytes就是0x00000001,無需去除尾綴0x00。
最後,該NALU的有效信息爲:
代碼分析如下(代碼過長,刪減部分):
static void _byteStreamNALUnit(
InputByteStream& bs,
vector<uint8_t>& nalUnit,
AnnexBStats& stats)
{
//檢查連續 3 / 4 個Bytes 是不是 0x000001 / 0x00000001
//如果不是,去掉前綴1個Bytes的0x00
while ((bs.eofBeforeNBytes(24/8) || bs.peekBytes(24/8) != 0x000001)
&& (bs.eofBeforeNBytes(32/8) || bs.peekBytes(32/8) != 0x00000001))
{
uint8_t leading_zero_8bits = bs.readByte();
statBits.bits+=8;
if(leading_zero_8bits != 0) { THROW( "Leading zero bits not zero" ); }
stats.m_numLeadingZero8BitsBytes++;
}
//檢查連續 3 個Bytes 是不是 0x000001
//如果不是,去掉前綴1個Bytes的0x00
if (bs.peekBytes(24/8) != 0x000001)
{
uint8_t zero_byte = bs.readByte();
statBits.bits+=8;
CHECK( zero_byte != 0, "Zero byte not '0'" );
stats.m_numZeroByteBytes++;
}
//讀取接下來連續3個Bytes,應該爲起始碼0x000001
uint32_t start_code_prefix_one_3bytes = bs.readBytes(24/8);
statBits.bits+=24;
if(start_code_prefix_one_3bytes != 0x000001) { THROW( "Invalid code prefix" );}
stats.m_numStartCodePrefixBytes += 3;
//依次連續讀取接下來連續3個Bytes,每次後移一個Bytes。
//比如“00 79 00 01 02 ……”,依次讀取“00 79 00”、“79 00 01”、“00 01 02”、……
//如果不是0x000000 或 0x000001 說明還是在該NALU的內容裏面,塞進當前NALU。
//否則,該3個Bytes是新的NALU的內容,退出該步驟。
while (bs.eofBeforeNBytes(24/8) || bs.peekBytes(24/8) > 2)
{
nalUnit.push_back(bs.readByte());
}
//檢查接下來連續 3 / 4 個Bytes 是不是 0x000001 / 0x00000001
//如果不是,去掉尾綴1個Bytes的0x00
while ((bs.eofBeforeNBytes(24/8) || bs.peekBytes(24/8) != 0x000001)
&& (bs.eofBeforeNBytes(32/8) || bs.peekBytes(32/8) != 0x00000001))
{
uint8_t trailing_zero_8bits = bs.readByte();
statBits.bits+=8;
CHECK( trailing_zero_8bits != 0, "Trailing zero bits not '0'" );
stats.m_numTrailingZero8BitsBytes++;
}
}
(3) 讀完當前NALU信息後,調用read()、readNalUnitHeader()將EBSP轉爲RBSP去掉防止競爭的0x03、讀取NALU的頭部有關信息。
// read NAL unit header
read(nalu);
void read(InputNALUnit& nalu)
{
InputBitstream &bitstream = nalu.getBitstream();
vector<uint8_t>& nalUnitBuf=bitstream.getFifo();
// perform anti-emulation prevention
convertPayloadToRBSP(nalUnitBuf, &bitstream, (nalUnitBuf[0] & 64) == 0);
bitstream.resetToStart();
readNalUnitHeader(nalu);
}
void readNalUnitHeader(InputNALUnit& nalu)
{
InputBitstream& bs = nalu.getBitstream();
nalu.m_forbiddenZeroBit = bs.read(1); // forbidden zero bit
nalu.m_nuhReservedZeroBit = bs.read(1); // nuh_reserved_zero_bit
nalu.m_nuhLayerId = bs.read(6); // nuh_layer_id
CHECK(nalu.m_nuhLayerId > 55, "The value of nuh_layer_id shall be in the range of 0 to 55, inclusive");
nalu.m_nalUnitType = (NalUnitType) bs.read(5); // nal_unit_type
nalu.m_temporalId = bs.read(3) - 1; // nuh_temporal_id_plus1
}
(4) 最後,調用DecLib::decode()進行當前NALU的核心解碼流程。該函數內,會根據當前NALU的類型進行針對性地解碼。該部分的分析之後博客會展開介紹。
#if JVET_P0288_PIC_OUTPUT
m_cDecLib.decode(nalu, m_iSkipFrame, m_iPOCLastDisplay, m_targetOlsIdx);
#else
m_cDecLib.decode(nalu, m_iSkipFrame, m_iPOCLastDisplay);
4. bNewPicture == true,進入環路濾波處理、輸出等流程。該部分的分析之後博客會展開介紹。
if ((bNewPicture || !bitstreamFile || nalu.m_nalUnitType == NAL_UNIT_EOS) && !m_cDecLib.getFirstSliceInSequence() && !bPicSkipped)
{
if (!loopFiltered || bitstreamFile)
{
m_cDecLib.executeLoopFilters();
m_cDecLib.finishPicture( poc, pcListPic );
}
loopFiltered = (nalu.m_nalUnitType == NAL_UNIT_EOS);
if (nalu.m_nalUnitType == NAL_UNIT_EOS)
{
m_cDecLib.setFirstSliceInSequence(true);
}
}
else if ( (bNewPicture || !bitstreamFile || nalu.m_nalUnitType == NAL_UNIT_EOS ) &&
m_cDecLib.getFirstSliceInSequence () )
{
m_cDecLib.setFirstSliceInPicture (true);
}