本文是本系列的第二篇博客,內容是分析non-VCLU解碼的相關代碼。
該系列之前的博客爲:
VVC/H.266代碼閱讀(VTM8.0)(一. NALU提取)
注:
- 考慮到從解碼端分析代碼,一是更加簡單(解碼流程無需編碼工具和編碼參數的擇優),二是可以配合Draft文本更好地理解視頻編解碼的流程(解碼端也都包含預測、量化、環路濾波、熵解碼等流程),所以本系列從解碼端入手分析VVC解碼大致流程。等到解碼端代碼分析完後,再從編碼端深入分析。
- 本文分析的bin文件是利用VTM8.0的編碼器,以All Intra配置(IBC 打開)編碼100幀得到的二進制碼流(TemporalSubsampleRatio: 8,實際編碼 ⌈100 / 8⌉ = 13幀)。
- 解碼用最簡單的:-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的內容。
- 在提取完該NALU的數據後,調用read() 、readNalUnitHeader()函數分析了該NALU的NalUnitHeader。該NALU中,除去前綴和起始碼,前兩個字節爲0x00 和 0x79 (01111 001),所以nal_unit_type爲 01111 = 15 (SPS_NUT),時域ID爲0最高級。該部分相關代碼在上一篇博客中分析過。
(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的解碼。(僅截取部分)
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劃分的相關代碼,敬請期待。