NAL unit 的解码(一)


HEVC视频编码的数据输出都是以NAL为基本单位进行输出的

  NAL的前两个BYTE是NAL的头,这两个BYTE的数据存储了当前NAL的类型,以及LayerID和TID,这两个的作用现在还没有完全弄懂

  NAL unit 的解码(一)_第1张图片

  还有就是NAL分为VCL和non-VCL两大类,VCL是指携带编码数据的数据流,而non-VCL则是控制数据流,其中一个携带编码数据的数据流VCL包含了一个slice segment的数据。


下图为官方标准中NAL层的句法元素,且以伪代码的形式给出了解码过程:

在HM中由TAppDecTop::decode()调用byteStreamNALUnit(bytestream, nalUnit, stats)实现如上伪代码:

/** 
 * Parse an AVC AnnexB Bytestream bs to extract a single nalUnit 
 * while accumulating bytestream statistics into stats. 
 * 
 * Returns false if EOF was reached (NB, nalunit data may be valid), 
 *         otherwise true. 
 */  
Bool  
byteStreamNALUnit(  
  InputByteStream& bs,  
  vector& nalUnit,  
  AnnexBStats& stats)  
{  
  Bool eof = false;  
  try  
  {  
    _byteStreamNALUnit(bs, nalUnit, stats); //!< 实际完成NAL解析工作的函数  
  }  
  catch (...) //!< 捕获所有异常  
  {  
    eof = true;  
  }  
  stats.m_numBytesInNALUnit = UInt(nalUnit.size());  
  return eof;  
}  

在分析NAL解析过程之前,先介绍几个会被调用到的子函数,以便更好地理解解析过程。

1. Bool eofBeforeNBytes(UInt n)

如果在读码流的接下来的n字节的过程中遇到了文件结束符,则该函数返回true,否则返回false。

/** 
 * returns true if an EOF will be encountered within the next 
 * n bytes. 
 */  
Bool eofBeforeNBytes(UInt n)  
{  
  assert(n <= 4);  
  if (m_NumFutureBytes >= n) //!< m_NumFutureBytes大于等于n只会在该函数被调用2次及2次以上的情况下发生,满足该条件时无须继续读多余的字节,故返回false  
    return false;  
  
  n -= m_NumFutureBytes; //!< n先减去m_NumFutureBytes的目的是防止被函数peekBytes调用时再读入接下来的n字节数据  
  try  
  {  
    for (UInt i = 0; i < n; i++)  
    {  
      m_FutureBytes = (m_FutureBytes << 8) | m_Input.get(); //!< 每次读入一个字节,循环结束后,m_FutureBytes存放的是读入的n个字节的数据  
      m_NumFutureBytes++;  
    }  
  }  
  catch (...) //!< 出现异常即读到文件结尾,返回true  
  {  
    return true;  
  }  
  return false;  
}  

2. uint32_t peekBytes(UInt n)

该函数在不移动文件指针的前提下返回文件中接下来的n字节。实现的即是伪代码中的next_bits(n)的功能。

/** 
 * return the next n bytes in the stream without advancing 
 * the stream pointer. 
 * 
 * Returns: an unsigned integer representing an n byte bigendian 
 * word. 
 * 
 * If an attempt is made to read past EOF, an n-byte word is 
 * returned, but the portion that required input bytes beyond EOF 
 * is undefined. 
 * 
 */  
uint32_t peekBytes(UInt n)  
{  
  eofBeforeNBytes(n);  
  return m_FutureBytes >> 8*(m_NumFutureBytes - n); //!< 若m_NumFutureBytes=4, n=3,则返回m_FutureBytes左移8位后(即有效数据位为3字节)的数据  
}  

3. uint8_t readByte()

该函数读文件的一个字节并返回。


/** 
 * consume and return one byte from the input. 
 * 
 * If bytestream is already at EOF prior to a call to readByte(), 
 * an exception std::ios_base::failure is thrown. 
 */  
uint8_t readByte()  
{  
  if (!m_NumFutureBytes) //!< m_FutureBytes为NULL,则从文件中读入一个字节并返回  
  {  
    uint8_t byte = m_Input.get();  
    return byte;  
  }//! m_FutureBytes非NULL,则从它当中取出一个字节出来  
  m_NumFutureBytes--; //!< 计数值减1  
  uint8_t wanted_byte = m_FutureBytes >> 8*m_NumFutureBytes; //!< m_FutureBytes为4字节,取出有效数据中的最高字节  
  m_FutureBytes &= ~(0xff << 8*m_NumFutureBytes); //!< 对应位置的数据清零  
  return wanted_byte;  
}  

4. uint32_t readBytes(UInt n)

该函数读文件的n个字节并返回。


/** 
 * consume and return n bytes from the input.  n bytes from 
 * bytestream are interpreted as bigendian when assembling 
 * the return value. 
 */  
uint32_t readBytes(UInt n)  
{  
  uint32_t val = 0;  
  for (UInt i = 0; i < n; i++)  
    val = (val << 8) | readByte(); //!< 每次调用readByte()读入一个字节,通过对val左移8位且与输入值进行或运算实现将n个字节存储到val这个变量中  
  return val;  
}  
()
在完成了参数配置文件的解析之后,主函数中调用cTAppDecTop.decode()开始正式的解码过程。这个函数中首先调用 xCreateDecLib(); xInitDecLib();建立和初始化解码器的对象。下面的byteStreamNALUnit (bytestream, nalUnit, stats);函数进行NAL的解码过程,进入该函数,发现实际进行操作的是_byteStreamNALUnit(bs, nalUnit, stats);这个函数,看来关键就在这里。
为了更加清晰地与实验结果进行对照,用ultraedit打开之前编码生成的码流文件str.bin,查看其二进制码流结构,我们之前生成的码流数据如下图所示:
NAL unit 的解码(一)_第2张图片
程序在运行中变量的变化值会同ultraedit中显示的值进行对照,以辅助我们的理解。

_byteStreamNALUnit(bs, nalUnit, stats)中, while ((bs.eofBeforeNBytes(24/8)||bs.peekBytes(24/8) != 0x000001) &&(bs.eofBeforeNBytes(32/8) || bs.peekBytes(32/8) != 0x00000001))的这个循环寻找第一个3或4字节长度,值为1的元素,并丢弃之前的0。
if (bs.peekBytes(24/8) != 0x000001)部分的作用是找出并丢弃zero_byte字段。
uint32_t start_code_prefix_one_3bytes = bs.readBytes(24/8);将提取并舍弃start_code_prefix_one字段。
while (bs.eofBeforeNBytes(24/8) || bs.peekBytes(24/8) > 2){nalUnit.push_back(bs.readByte());}的作用是正式读取nal单元的数据。在本例中,前四个字节“00 00 00 01”都将被舍弃,正式读取的数据时从那个0x40(64)开始读取,一直读到文件末尾,或者遇到后面三个字节为0或1为止。
while ((bs.eofBeforeNBytes(24/8) || bs.peekBytes(24/8) != 0x000001)&&(bs.eofBeforeNBytes(32/8) || bs.peekBytes(32/8) != 0x00000001))将会舍弃末尾的trailing_zero_8bits字段。
在本例中,NAL分析读取完成后,nalUnit中将包含从value=64开始的24个元素,并在 read(nalu, nalUnit);函数中进行下一步处理。
read(nalu, nalUnit)这个函数调用 convertPayloadToRBSP函数,将nalUnit中的元素转化为nal的参数数据。(该函数为什么会舍弃元素3?我没太弄明白)完成后,nalUnit的buf中共20个元素。之后调用 readNalUnitHeader函数,解析nal头数据。文献“ Overview of HEVC High-Level Syntax and Reference Picture Management ”中讲述了NAL的头数据结构,也就是下图所示:
NAL unit 的解码(一)_第3张图片
这样 readNalUnitHeader函数的功能便很明朗了,其主要目的就是获取nal type和temporal_id这两部分信息。


你可能感兴趣的:(NAL unit 的解码(一))