VVC/H.266代碼閱讀(VTM8.0)(二. non-VCLU解碼)

本文是本系列的第二篇博客,內容是分析non-VCLU解碼的相關代碼。
該系列之前的博客爲:
VVC/H.266代碼閱讀(VTM8.0)(一. NALU提取)

注:

  1. 考慮到從解碼端分析代碼,一是更加簡單(解碼流程無需編碼工具和編碼參數的擇優),二是可以配合Draft文本更好地理解視頻編解碼的流程(解碼端也都包含預測、量化、環路濾波、熵解碼等流程),所以本系列從解碼端入手分析VVC解碼大致流程。等到解碼端代碼分析完後,再從編碼端深入分析。
  2. 本文分析的bin文件是利用VTM8.0的編碼器,以All Intra配置(IBC 打開)編碼100幀得到的二進制碼流(TemporalSubsampleRatio: 8,實際編碼 ⌈100 / 8⌉ = 13幀)。
  3. 解碼用最簡單的:-b str.bin -o dec.yuv

在上一篇博客中,我分析瞭解碼端將收到的二進制碼流bin文件提取成一個個NALU的過程。上一篇博客的最後寫道 “ 調用DecLib::decode()進行當前NALU的核心解碼流程。該函數內,會根據當前NALU的類型進行針對性地解碼。” 本篇博客就是對該函數的部分展開,即針對non-VCLU解碼進行分析,如SPS、PPS等non-VCLU。

1. 什麼是non-VCLU?non-VCLU一般包含什麼內容?
(1) 萬帥老師在書籍《新一代高效視頻編碼H.265HEVC原理、標準與實現》的第三章51頁有過以下介紹:

NAL單元根據是否裝載視頻編碼數據被分爲VCLU和non-VCLU。
非編碼數據的參數集作爲non-VCLU進行傳輸,爲傳遞關鍵數據提供了高魯棒機制。
參數集的獨立使得其可以提前發送,也可以在需要增加新參數集的時候再發送,可以被多次重發或者採用特殊技術加以保護,甚至採用帶外(Out-of-band)發送的方式。

(2) 也就是說,non-VCLU內部一般裝載了VPS、SPS、PPS等參數信息。這部分數據非常重要,所以優先級也比較高,draft裏有過以下要求:

The value of TemporalId for non-VCL NAL units is constrained as follows:
// non-VCLU的時域ID要求如下(0爲最高級,增加優先級下降):
– If nal_unit_type is equal to DCI_NUT, VPS_NUT, or SPS_NUT, TemporalId shall be equal to 0 and the TemporalId of the AU containing the NAL unit shall be equal to 0.
// DCI、VPS、PPS的時域ID應該設爲0。
– Otherwise, if nal_unit_type is equal to PH_NUT, TemporalId shall be equal to the TemporalId of the PU containing the NAL unit.
– Otherwise, if nal_unit_type is equal to EOS_NUT or EOB_NUT, TemporalId shall be equal to 0.
– Otherwise, if nal_unit_type is equal to AUD_NUT, FD_NUT, PREFIX_SEI_NUT, or SUFFIX_SEI_NUT, TemporalId shall be equal to the TemporalId of the AU containing the NAL unit.
– Otherwise, when nal_unit_type is equal to PPS_NUT, PREFIX_APS_NUT, or SUFFIX_APS_NUT, TemporalId shall be greater than or equal to the TemporalId of the PU containing the NAL unit.

注:關於代碼和草案的下載地址,可以參考:
VVC/H.266常見資源整理(提案地址、代碼、資料等)

我們再看看draft(我參考的是JVET-Q2001-vE)裏面non-VCLU的大致內容,可以參考7.4.2.2 Table 5 – NAL unit type codes and NAL unit type classes。
在這裏插入圖片描述
2. non-VCLU解碼代碼分析
(1) 細節回顧:

  • 和上一篇博客VVC/H.266代碼閱讀(VTM8.0)(一.NALU提取)使用的bin文件相同,從00 00 00 01開始到DC 0D 56 81是一個NALU的內容。
    non-VCLU
  • 在提取完該NALU的數據後,調用read() 、readNalUnitHeader()函數分析了該NALU的NalUnitHeader。該NALU中,除去前綴和起始碼,前兩個字節爲0x00 和 0x79 (01111 001),所以nal_unit_type爲 01111 = 15 (SPS_NUT),時域ID爲0最高級。該部分相關代碼在上一篇博客中分析過。
    NALU header
    (2) 現在從DecLib::decode()分析。
    ① 首先,根據nal_unit_type調用不同的函數針對性地解碼。以該NALU爲例,nalu是SPS,調用xDecodeSPS( nalu );
switch (nalu.m_nalUnitType)
  {
    case NAL_UNIT_VPS:
      xDecodeVPS( nalu );
      return false;

    case NAL_UNIT_DPS:
      xDecodeDPS( nalu );
      return false;

    case NAL_UNIT_SPS:
      xDecodeSPS( nalu );
      return false;

    case NAL_UNIT_PPS:
      xDecodePPS( nalu );
      return false;

    case NAL_UNIT_PH:
      xDecodePicHeader(nalu);
      return !m_bFirstSliceInPicture;

    case NAL_UNIT_PREFIX_APS:
    case NAL_UNIT_SUFFIX_APS:
      xDecodeAPS(nalu);
      return false;

    case NAL_UNIT_PREFIX_SEI:
      m_prefixSEINALUs.push_back(new InputNALUnit(nalu));
      return false;

    case NAL_UNIT_SUFFIX_SEI:
      if (m_pcPic)
      {
        m_seiReader.parseSEImessage( &(nalu.getBitstream()), m_pcPic->SEIs, nalu.m_nalUnitType, nalu.m_temporalId, m_parameterSetManager.getActiveSPS(), m_HRD, m_pDecodedSEIOutputStream );
      }
      else
      {
        msg( NOTICE, "Note: received suffix SEI but no picture currently active.\n");
      }
      return false;

    case NAL_UNIT_CODED_SLICE_TRAIL:
    case NAL_UNIT_CODED_SLICE_STSA:
    case NAL_UNIT_CODED_SLICE_IDR_W_RADL:
    case NAL_UNIT_CODED_SLICE_IDR_N_LP:
    case NAL_UNIT_CODED_SLICE_CRA:
    case NAL_UNIT_CODED_SLICE_GDR:
    case NAL_UNIT_CODED_SLICE_RADL:
    case NAL_UNIT_CODED_SLICE_RASL:
      ret = xDecodeSlice(nalu, iSkipFrame, iPOCLastDisplay);
      return ret;

    case NAL_UNIT_EOS:
      m_associatedIRAPType = NAL_UNIT_INVALID;
      m_pocCRA = 0;
      m_pocRandomAccess = MAX_INT;
      m_prevLayerID = MAX_INT;
      m_prevPOC = MAX_INT;
      m_prevSliceSkipped = false;
      m_skippedPOC = 0;
      return false;

    case NAL_UNIT_ACCESS_UNIT_DELIMITER:
      {
        AUDReader audReader;
        uint32_t picType;
        audReader.parseAccessUnitDelimiter(&(nalu.getBitstream()),picType);
        return !m_bFirstSliceInPicture;
      }

    case NAL_UNIT_EOB:
      return false;

    case NAL_UNIT_RESERVED_IRAP_VCL_11:
    case NAL_UNIT_RESERVED_IRAP_VCL_12:
      msg( NOTICE, "Note: found reserved VCL NAL unit.\n");
      xParsePrefixSEIsForUnknownVCLNal();
      return false;
    case NAL_UNIT_RESERVED_VCL_4:
    case NAL_UNIT_RESERVED_VCL_5:
    case NAL_UNIT_RESERVED_VCL_6:
    case NAL_UNIT_RESERVED_NVCL_26:
    case NAL_UNIT_RESERVED_NVCL_27:
      msg( NOTICE, "Note: found reserved NAL unit.\n");
      return false;
    case NAL_UNIT_UNSPECIFIED_28:
    case NAL_UNIT_UNSPECIFIED_29:
    case NAL_UNIT_UNSPECIFIED_30:
    case NAL_UNIT_UNSPECIFIED_31:
      msg( NOTICE, "Note: found unspecified NAL unit.\n");
      return false;
    default:
      THROW( "Invalid NAL unit type" );
      break;
  }

② 進入DecLib::xDecodeSPS()。

void DecLib::xDecodeSPS( InputNALUnit& nalu )
{
  SPS* sps = new SPS();
  //創建SPS
  m_HLSReader.setBitstream( &nalu.getBitstream() );
  //將nalu內讀取出的碼流信息放入m_HLSReader中,HLS是高層語法的縮寫
  m_HLSReader.parseSPS( sps );
  //解析SPS
  m_parameterSetManager.storeSPS( sps, nalu.getBitstream().getFifo() );
  //m_parameterSetManager中存儲相關的參數集
}

此時,按照draft 7.3.2.3 和 7.4.3.3相關的章節進行SPS的解碼。(僅截取部分)
SPS

void HLSyntaxReader::parseSPS(SPS* pcSPS)
{
  uint32_t  uiCode;
  READ_CODE(4,  uiCode, "sps_decoding_parameter_set_id");    pcSPS->setDecodingParameterSetId( uiCode );
  READ_CODE(4,  uiCode, "sps_video_parameter_set_id" );      pcSPS->setVPSId( uiCode );
  READ_CODE(3, uiCode, "sps_max_sub_layers_minus1");         pcSPS->setMaxTLayers   (uiCode + 1);
  READ_CODE(4, uiCode, "sps_reserved_zero_4bits");
  READ_FLAG(uiCode, "sps_ptl_dpb_hrd_params_present_flag");  pcSPS->setPtlDpbHrdParamsPresentFlag(uiCode);
  ……

其中,READ_CODE()核心調用了InputBitstream::read() 函數,是連續讀取無符號n位。READ_UVLC()是0階指數哥倫布編碼。下面詳細分析二者代碼。

u(n): unsigned integer using n bits.
ue(v): unsigned integer 0-th order Exp-Golomb-coded syntax element with the left bit first.

void InputBitstream::read (uint32_t uiNumberOfBits, uint32_t& ruiBits)
{
  //uiNumberOfBits就是讀n位,ruiBits返回參數值。
  m_numBitsRead += uiNumberOfBits;
  //m_numBitsRead 記錄了讀過bits的位數。
  //比如說,如果用u(4)解析SPS的第一個語法sps_seq_parameter_set_id。之前讀取NALU header使用了2個Bytes = 16bits,所以解析完該4個bits後,該值會變成20。
  uint32_t retval = 0;
  if (uiNumberOfBits <= m_num_held_bits)
  {
  //m_num_held_bits 記錄了一個完整的Byte剩下未讀完的位數。
  //比如說,之前只讀了1個bit,所以m_num_held_bits = 8 - 1 = 7,如果現在要讀的位數 < 7(m_num_held_bits), 直接位操作讀取出來即可。
  //下面X表示已讀位,VH是未讀位,V是需要讀的位數
  //n=1, len(H)=7:   -X(已讀位) VHH HHHH, shift_down=6, mask=0xfe=11111110
  //n=3, len(H)=7:   -X(已讀位) VVV HHHH, shift_down=4, mask=0xf8=11111000
    retval = m_held_bits >> (m_num_held_bits - uiNumberOfBits);
    //m_held_bits表示了該Byte的數據。
    //右移m_num_held_bits - uiNumberOfBits,排除後面不用讀的bits
    retval &= ~(0xff << uiNumberOfBits);
    //利用mask讀取需要的數據
    m_num_held_bits -= uiNumberOfBits;
    //m_num_held_bits 記錄了一個完整的Byte剩下未讀完的位數。進行更新。
    ruiBits = retval;
    return;
  }

  //m_num_held_bits 記錄了一個完整的Byte剩下未讀完的位數。能進行下面步驟,說明超過了目前的Byte範圍,目前Byte留下的有效未讀bits需要全部讀取。
  //下面X表示已讀位,V是該Byte需要讀的位數, H是後續Bytes需要讀的bits
  //n=5, len(H)=3: ---- -XXXXX(已讀位) VVV HH, mask=0x07, shift_up=5-3=2,
  //n=9, len(H)=3: ---- -XXXXX(已讀位) VVV HHHHHH, mask=0x07, shift_up=9-3=6
  uiNumberOfBits -= m_num_held_bits;
  //減去剩下的bits, uiNumberOfBits變成後續Bytes需要讀的bits
  retval = m_held_bits & ~(0xff << m_num_held_bits);
  //利用mask讀取需要的數據
  retval <<= uiNumberOfBits;
  //左移uiNumberOfBits,方便和後面bits進行拼接

  /* number of whole bytes that need to be loaded to form retval */
  /* n=32, len(H)=0, load 4bytes, shift_down=0
   * n=32, len(H)=1, load 4bytes, shift_down=1
   * n=31, len(H)=1, load 4bytes, shift_down=1+1
   * n=8,  len(H)=0, load 1byte,  shift_down=0
   * n=8,  len(H)=3, load 1byte,  shift_down=3
   * n=5,  len(H)=1, load 1byte,  shift_down=1+3
   */
  uint32_t aligned_word = 0;
  uint32_t num_bytes_to_load = (uiNumberOfBits - 1) >> 3;
  //num_bytes_to_load 看看還需要後續幾個Bytes進行拼接讀取
  
  switch (num_bytes_to_load)
  {
  //根據num_bytes_to_load,從碼流裏讀取出來對應數量的Bytes
  case 3: aligned_word  = m_fifo[m_fifo_idx++] << 24;
  case 2: aligned_word |= m_fifo[m_fifo_idx++] << 16;
  case 1: aligned_word |= m_fifo[m_fifo_idx++] <<  8;
  case 0: aligned_word |= m_fifo[m_fifo_idx++];
  }

  uint32_t next_num_held_bits = (32 - uiNumberOfBits) % 8;
  //next_num_held_bits是讀取完後續Bytes後,最後一個Byte還剩幾個未讀的bits
  retval |= aligned_word >> next_num_held_bits;
  //右移next_num_held_bits,就是讀取到的後續Bytes的相關數據,進行拼接
  
  m_num_held_bits = next_num_held_bits;
  //m_num_held_bits 記錄了一個完整的Byte剩下未讀完的位數。也就是next_num_held_bits。
  m_held_bits = aligned_word;
  //截斷aligned_word的最後一個Byte,賦給m_held_bits 

  ruiBits = retval;
}

READ_UVLC()是0階指數哥倫布編碼,流程很簡單,簡單的處理前綴後綴即可。
萬帥老師在書籍《新一代高效視頻編碼H.265HEVC原理、標準與實現》的第八章236頁有過以下介紹:
在這裏插入圖片描述
在這裏插入圖片描述

void VLCReader::xReadUvlc( uint32_t& ruiVal, const char *pSymbolName)
{
  uint32_t uiVal = 0;
  uint32_t uiCode = 0;
  uint32_t uiLength;
  m_pcBitstream->read( 1, uiCode );
  //讀第一個bit
  if( 0 == uiCode )
  {
    uiLength = 0;
	
    while( ! ( uiCode & 1 ))
    {
      m_pcBitstream->read( 1, uiCode );
      uiLength++;
      //讀取前綴連續0的個數,如果有0,一直往下讀
    }

    m_pcBitstream->read( uiLength, uiVal );
    //讀取後綴,此處後綴bits數目就是前綴的連續0的個數uiLength。

    uiVal += (1 << uiLength)-1;
    //根據公式,後綴 + 前綴 - 1
  }

  ruiVal = uiVal;
}

從上可知,SPS語法主要的編碼方法u(n)和ue(v)已經分析完畢。
VPS、PPS等其他non-VCLU的解碼原理基本相同,此處不再贅述。
在下一篇博客中,會展開分析VCLU中CU劃分的相關代碼,敬請期待。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章