首先,HEVC的熵编码使用了两种算术编码:CABAC和CAVLC。CAVLC主要用于编码SEI、参数集、片头等,剩下的所有数据和语法元素均使用CABAC来编码。
HEVC标准文档中使用到的一些描述符(描述符也表示操作方法): 1、ae(v) 使用cabac 2、b(8) 读进连续的8 bit 3、f(n) 读进连续的n bit 4、u(n) 读进连续的n bit,解码为无符号整数 5、se(v) 有符号指数哥伦布编码 6、ue(n) 无符号哥伦布指数编码
HEVC中与CAVLC有关的类:SyntaxElementWriter、SEIWriter、TEncCavlc
1、SyntaxElementWriter语法元素写入者,定义了CAVLC的几种基本算法 2、SEIWriter,SEI写入者,由于SEI使用CAVLC来编码,因此SEIWriter继承自SyntaxElementWriter 3、TEncCavlc,CAVLC编码器,主要用于编码参数集以及slice头部等,继承自SyntaxElementWriter
CAVLC只是一个统称,它包含了多个算法: 1、零阶无符号哥伦布指数编码 2、零阶有符号指数哥伦布编码 3、不编码直接写入若干比特 4、不编码直接写入一个比特
class SyntaxElementWriter { protected: TComBitIf* m_pcBitIf; SyntaxElementWriter() :m_pcBitIf(NULL) {}; virtual ~SyntaxElementWriter() {}; // 设置比特流 Void setBitstream ( TComBitIf* p ) { m_pcBitIf = p; } Void xWriteCode ( UInt uiCode, UInt uiLength ); // 不编码直接写入若干比特 Void xWriteUvlc ( UInt uiCode ); // 零阶无符号哥伦布指数编码 Void xWriteSvlc ( Int iCode ); // 零阶有符号指数哥伦布编码 Void xWriteFlag ( UInt uiCode ); // 不编码直接写入一个比特 UInt xConvertToUInt ( Int iValue ) { return ( iValue <= 0) ? -iValue<<1 : (iValue<<1)-1; } };
指数哥伦布编码由前缀和后缀两部分构成,前缀和后缀都依赖于指数哥伦布码的阶数k。假设指数哥伦布码是N,阶数为k,下面是它的编码步骤: (1)把N转换为二进制数,去掉最低的k个比特位,然后加上1 (2)计算留下的比特数,把这个数减去1,这就是需要增加的前缀0的个数 (3)把步骤(1)中去掉的k个比特位补回比特串的尾部。
/* ** 无符号指数哥伦布编码 */ Void SyntaxElementWriter::xWriteUvlc ( UInt uiCode ) { UInt uiLength = 1; // 哥伦布码的长度 UInt uiTemp = ++uiCode; // 执行uiTemp = uiCode + 1 assert ( uiTemp ); while( 1 != uiTemp ) // 假设uiTemp(即uiCode + 1)对应的二进制长度是len,那么哥伦布码的长度 = 2 * len - 1 { uiTemp >>= 1; uiLength += 2; } m_pcBitIf->write( 0, uiLength >> 1); // 写入前缀: len - 1个0 m_pcBitIf->write( uiCode, (uiLength+1) >> 1); // 写入后缀:uiTemp对应的二进制 } 上面算法的步骤: 1、计算N+=1
2、计算N对应的二进制串bins的比特数,假设是len
3、把前缀(M-1个0)写入比特流中
4、把后缀(N对应的二进制串)写入比特流流中
写入比特流中的流程:
1、TComOutputBitstream包含两个部分:缓冲区和比特流 2、比特流就是已经处理完成的数据,存放在一个vector中 3、缓冲区暂存还没有写入比特流中的数据,TComOutputBitstream使用一个uchar类型的数据(8 bit)作为缓存区,因为很多时候写入的数据长度只有若干比特,不能直接写入比特流中,需要等到缓冲区满(达到8bit),才写入 3、m_num_held_bits表示缓冲区中已有的比特数 4、num_total_bits表示写入数据之后,缓冲区中总的比特数(可能会溢出,后面会解决这个问题) 5、next_num_held_bits有两种意思: (1)如果缓冲区没有溢出,那么它表示缓冲区中总的比特数(已有+新增) (2)如果缓冲区溢出,那么它表示数据占用完缓冲区后还需要的比特数,只能存放数据的一部分,剩下那部分需要等缓冲区的数据写入比特流之后再存放 6、next_held_bits是格式化之后的数据 (1)如果缓冲区不溢出,那它表示将要写入缓冲区中的数据 (2)如果缓冲区溢出,那它表示将一部分数据写入缓冲区之后,剩下的那部分数据 7、判断num_total_bits是否大于8,即判断新增数据之后,缓冲区是否会溢出 8、如果缓冲区不溢出,那么把数据写入缓冲区中,然后返回 9、如果缓冲区溢出,那么先写一部分数据到缓冲区中,然后把缓冲区写入比特流中,清空缓冲区,继续把剩余的数据写入缓冲区中
Void TComOutputBitstream::write ( UInt uiBits, UInt uiNumberOfBits ) { assert( uiNumberOfBits <= 32 ); assert( uiNumberOfBits == 32 || (uiBits & (~0 << uiNumberOfBits)) == 0 ); // m_num_held_bits缓存区中已经持有的比特数 // num_total_bits表示写入uiBits之后,缓存区中的总比特数 UInt num_total_bits = uiNumberOfBits + m_num_held_bits; // next_num_held_bits表示缓存区中经使用的比特数 UInt next_num_held_bits = num_total_bits % 8; // 把数据执行位移操作之后,存放进一个临时变量中, UChar next_held_bits = uiBits << (8 - next_num_held_bits); // 判断当前持有的比特数是否大于8,如果大于8,表示缓冲区已经满了,需要先写入比特流中 if (!(num_total_bits >> 3)) { // 把数据写入缓冲区的尾部 m_held_bits |= next_held_bits; m_num_held_bits = next_num_held_bits; return; } /* topword serves to justify held_bits to align with the msb of uiBits */ // 把一部分数据写入 UInt topword = (uiNumberOfBits - next_num_held_bits) & ~((1 << 3) -1); UInt write_bits = (m_held_bits << topword) | (uiBits >> next_num_held_bits); // 判断num_total_bits的长度:32,24,16,8 switch (num_total_bits >> 3) { case 4: m_fifo->push_back(write_bits >> 24); case 3: m_fifo->push_back(write_bits >> 16); case 2: m_fifo->push_back(write_bits >> 8); case 1: m_fifo->push_back(write_bits); } m_held_bits = next_held_bits; m_num_held_bits = next_num_held_bits; }
