VVC/H.266代碼閱讀(VTM8.0)(一. NALU提取)

網上很多大佬對VVC的代碼進行過分析,基本都是從編碼端入手。
考慮到從解碼端分析代碼,一是更加簡單(解碼流程無需編碼工具和編碼參數的擇優),二是可以配合Draft文本更好地理解視頻編解碼的流程(解碼端也都包含預測、量化、環路濾波、熵解碼等流程),所以我想從解碼端入手分析一下VVC大致的流程。等到解碼端代碼分析完後,可以再從編碼端深入分析一下。

本文是本系列的第一篇博客,內容是分析解碼端將收到的二進制碼流bin文件提取成一個個NALU的過程。

注:

  1. 本文分析的bin文件是利用VTM8.0的編碼器,以All Intra配置(IBC 打開)編碼100幀得到的二進制碼流(TemporalSubsampleRatio: 8,實際編碼 ⌈100 / 8⌉ = 13幀)。
  2. 解碼用最簡單的:-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。
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。

NALU信息
比如上面從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);
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章