HEVC中的CAVLC
CAVLC 基于上下文的自适应变长编码
首先,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有关的类
HEVC中与CAVLC有关的类:SyntaxElementWriter、SEIWriter、TEncCavlc
1、SyntaxElementWriter语法元素写入者,定义了CAVLC的几种基本算法
2、SEIWriter,SEI写入者,由于SEI使用CAVLC来编码,因此SEIWriter继承自SyntaxElementWriter
3、TEncCavlc,CAVLC编码器,主要用于编码参数集以及slice头部等,继承自SyntaxElementWriter
HEVC中CAVLC的几种基本的算法
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; }
};
指数哥伦布编码的理论
(1)把N转换为二进制数,去掉最低的k个比特位,然后加上1
(2)计算留下的比特数,把这个数减去1,这就是需要增加的前缀0的个数
(3)把步骤(1)中去掉的k个比特位补回比特串的尾部。
零阶指数哥伦布编码
根据上面的定义,可以得到零阶指数哥伦布编码的计算方法,假设输入值是N:
1、把N转换成二进制,假设转换后是bins,计算bins += 1
2、计算bins的比特数,假设是M,那么在bins的前面添加上M-1个0就得到最终的结果
或者
1、计算N += 1
2、把N转换成二进制串bins,计算二进制串的长度,假设是M,那么在bins的前面添加M-1个0
假设输入值N对应的二进制串的长度是len,那么零阶哥伦布码的长度是2*len-1
哥伦布码golomb = 前缀prefix + 后缀suffix
前缀prefix : len - 1个0
后缀suffix : (N+1)对应的二进制串
零阶指数哥伦布编码的实现
/*
** 无符号指数哥伦布编码
*/
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;
}