算术编码是一种常用的变长编码方法,和Huffman编码类似,也是对出现概率大的符号赋予短码,出现概率小的符号赋予长码,但算术编码不是简单的将每个信源符号映射成一个码字,而是对整个输入序列分配一个码字,所以平均意义上可以为每个信源符号分配长度小于1的码字。
算术编码的原理参考:熵编码:算术编码
H.266/VVC中采用的是基于上下文的自适应二进制算术编码(Context-based Adaptive Binary Arithmetic Coding,CABAC)。CABAC将二进制算术编码和上下文模型结合起来,其主要特点为:
(1)采用二进制算术编码,将所有语法元素转化为二进制串,消除了乘法运算操作,降低了计算复杂度,提高了编码效率
(2)建立上下文模型,充分利用了符号间的相关性,根据已编码的符号自适应的进行模型更新,进一步提高了编码效率。
VVC并不是对所有语法元素使用算术编码的方法,而是对于不同的语法元素使用不同的编码方法。如对比特流高层特性的语法元素,本身信息量较小,采用定长编码;对于比特流中比例较大的残差系数等信息,使用CABAC编码。
CABAC主要经过以下三个步骤:
- 二进制化
- 上下文建模
- 二进制算术编码
CABAC首先对输入的非二进制语法元素进行二进制化处理,将其唯一地转换为二进制串。如果输入的本身就是二进制语法元素,则可以跳过二进制化步骤。二进制化之后,进入二进制算术编码阶段。二进制算术编码过程中分为常规编码模式和旁路编码模式。在常规编码模式中,首先根据先前编码的语法元素为其选择一个上下文模型,接着,将上下文模型和二进制值一起送到常规编码器中进行编码,并将结果输出到码流中,同时,根据当前编码的二进制值更新上下文模型。在旁路编码模式中,无需对概率模型进行更新,将二进制值0和1看作等概分布,使用各占1/2的固定概率进行编码。
1、二进制化
CABAC仅对二进制符号0或1进行编码,因此对于非二进制符号需要将其转换为二进制串。VVC中常用的二进制化方法包括截断莱码(truncated Rice (TR) )、截断二进制码(truncated binary (TB) )、k阶指数哥伦布码(the k-th order Exp-Golomb (EGk) binarization )和定长码( fixed-length (FL))。
H.266/VVC中的二进制化参考:H.266/VVC熵编码之二进制化
2、二进制算术编码
算术编码流程:将输入的语法元素经过二进制化转换为一个或者多个bin值,根据bin值对应的ctxId选择上下文模型,并根据当前的bin值选择上下文模型,编码完后,再根据bin值更新上下文模型(更新区间起始点和区间宽度)。
参数介绍:
- m_low:区间起始点(区间左端点)
- m_range:区间宽度
- ctxId:模型对应的索引号
- lps:小概率符号
- mps:大概率符号
- LPS:小概率符号对应的区间宽度
- MPS:大概率符号对应的区间宽度
- m_state[0],m_state[1]:模型状态概率,H.266 中采用多概率模型。
- m_rate:用于更新模型状态概率
CABAC的算术编码的编码区间宽度m_range的取值为(),通常区间左侧子区间为mps对应的子区间,右侧子区间为lps对应的子区间。
CABAC的常规编码流程:
- 确定区间起始点m_low和区间宽度m_range
- 根据ctxId选择相应的上下文模型,再根据m_range和模型对应的状态概率m_state确定LPS,更新m_range=m_range-LPS
- 根据模型状态概率确定mps,再判断待编码的bin值是mps还是lps
- 更新m_low和m_range,如果bin=lps,则m_Low=m_Low+m_Range,m_range=LPS;若bin=mps,则m_Low=m_Low,m_Range=m_Range
- 根据bin值和m_rate更新上下文模型状态概率
- 如果新的编码区间宽度m_range超过了(
)的范围,需要对m_range进行重归一化,即对m_low和m_range同时左移一定位数,并根据需要输出一定比特。
VVC解码端的常规编码流程如下图所示:
(其中pStateidx1和pStateIdx2指代m_state[0],m_state[1],ivlCurrRange指代m_range,valMps指代mps,ivlLpsRange指代LPS,ivlOffset指代m_low)
VVC解码端的重归一化流程如下图所示:
VTM中代码实现:
template <class BinProbModel>
void TBinEncoder<BinProbModel>::encodeBin( unsigned bin, unsigned ctxId ) //常规编码
{
BinCounter::addCtx( ctxId );
BinProbModel& rcProbModel = m_Ctx[ctxId]; //获得相应上下文模型
uint32_t LPS = rcProbModel.getLPS( m_Range ); //获得LPS的区间长度
DTRACE( g_trace_ctx, D_CABAC, "%d" " %d " "%d" " " "[%d:%d]" " " "%2d(MPS=%d)" " " " - " "%d" "\n", DTRACE_GET_COUNTER( g_trace_ctx, D_CABAC ), ctxId, m_Range, m_Range - LPS, LPS, ( unsigned int ) ( rcProbModel.state() ), bin == rcProbModel.mps(), bin );
m_Range -= LPS; //更新 range = range - LPS(即MPS)
if( bin != rcProbModel.mps() ) //如果当前符号是lps
{
int numBits = rcProbModel.getRenormBitsLPS( LPS ); // 通过查表获取需要左移的位数(用于重归一化)
m_bitsLeft -= numBits;
m_Low += m_Range; //移动m_low
m_Low = m_Low << numBits;
m_Range = LPS << numBits;
if( m_bitsLeft < 12 ) // m_bitsLeft 表示存储(m_Low)的寄存器中还剩余多少 bit
{
writeOut();
}
}
else //如果当前符号是mps,则不需要更新m_low和m_range
{
if( m_Range < 256 ) // 如果更新后的区间宽度小于256,则需要重归一化
{
int numBits = rcProbModel.getRenormBitsRange( m_Range ); // 获取需要左移的位数:1
m_bitsLeft -= numBits;
m_Low <<= numBits; // 输出比特
m_Range <<= numBits; // 扩大区间宽度
if( m_bitsLeft < 12 )
{
writeOut();
}
}
}
rcProbModel.update( bin ); //更新模型概率状态
BinEncoderBase::m_BinStore.addBin( bin, ctxId );
}
其中,getLPS函数用来得到lps的区间宽度,mps函数用来计算大概率符号值
uint8_t getLPS(unsigned range) const
{
uint16_t q = state();
if (q & 0x80) //如果 q > 128
q = q ^ 0xff; //q按位取反后小于127
return ((q >> 2) * (range >> 5) >> 1) + 4; // 返回LPS的区间长度
}
uint8_t state() const { return (m_state[0] + m_state[1]) >> 8; } // 表示P(1)的概率 pState
uint8_t mps() const { return state() >> 7; } // valMps 返回MPS的值0 or 1
上下文模型的初始化与更新
在算术编码开始前,需要初始化上下文模型,上下文模型的初始化是为了利用每个模型的 initValue 得到一个初始的模型概率状态(m_state[0], m_state[1]),并在模型中加入了一个窗口参数(WindowSize),由此得到了一个控制上下文模型更新的参数 m_rate。
void BinProbModel_Std::init( int qp, int initId )
{
int slope = (initId >> 3) - 4;
int offset = ((initId & 7) * 18) + 1;
int inistate = ((slope * (qp - 16)) >> 1) + offset;
int state_clip = inistate < 1 ? 1 : inistate > 127 ? 127 : inistate;
const int p1 = (state_clip << 8);
m_state[0] = p1 & MASK_0;
m_state[1] = p1 & MASK_1;
}
void setLog2WindowSize(uint8_t log2WindowSize) //根据WindowSize设置pstate的更新速率
{
int rate0 = 2 + ((log2WindowSize >> 2) & 3);
int rate1 = 3 + rate0 + (log2WindowSize & 3);
m_rate = 16 * rate0 + rate1;
CHECK(rate1 > 9, "Second window size is too large!");
}
上下文模型的更新主要是根据当前编码bin值和更新速率m_rate更新m_state[0],m_state[1]。
update函数用来更新模型状态概率
void update(unsigned bin) // 上下文状态概率更新过程
{
int rate0 = m_rate >> 4;
int rate1 = m_rate & 15;
m_state[0] -= (m_state[0] >> rate0) & MASK_0;
m_state[1] -= (m_state[1] >> rate1) & MASK_1;
if (bin)
{
m_state[0] += (0x7fffu >> rate0) & MASK_0;
m_state[1] += (0x7fffu >> rate1) & MASK_1;
}
}
3、旁路编码
旁路编码假定符号0或1各占1/2的概率进行编码,其中 0 区间在前,1 区间在后,同时不需要对概率模型进行更新。
为了使区间划分更加简单,不采用直接对区间长度二等分的方法,而是采用保持编码区间长度不变方法使区间下限 m_low加倍的方法实现区间划分,可以直接通过将 m_low左移 1 位实现,这样既达到了同样的效果又省去了在重归一化过程中同时对 m_range 和 m_low 进行的加倍操作。
VVC解码端的旁路编码流程如下:
VTM代码实现:
void BinEncoderBase::encodeBinEP( unsigned bin )
{
DTRACE( g_trace_ctx, D_CABAC, "%d" " " "%d" " EP=%d \n", DTRACE_GET_COUNTER( g_trace_ctx, D_CABAC ), m_Range, bin );
BinCounter::addEP();
// 这里,由于m_range范围为0-510,旁路编码0和1各占一半,因此每编码一次bin需要进行一次重归一化
// 左移位数为 1,并输出 1 个比特。这里为了更简便,将对 m_Low 的移位操作提前,并保持m_Range 不变。
m_Low <<= 1;
if( bin )// 如果编码的符号是1的话(旁路编码中,0的区间在前,1的区间在后,因此需要移动m_low)
{
m_Low += m_Range;
}
m_bitsLeft--;
if( m_bitsLeft < 12 )
{
writeOut();
}
}
其中常规编码和旁路编码的最后,当 m_bitsLeft<12 都会调用writeOut函数进行比特输出
VTM代码具体如下:
void BinEncoderBase::writeOut()
{
unsigned leadByte = m_Low >> ( 24 - m_bitsLeft ); // 将m_low的高8位存在LeadByte中
m_bitsLeft += 8;
m_Low &= 0xffffffffu >> m_bitsLeft;
if( leadByte == 0xff )
{
m_numBufferedBytes++;
}
else
{
if( m_numBufferedBytes > 0 )
{
unsigned carry = leadByte >> 8;
unsigned byte = m_bufferedByte + carry; // 将上次缓存的8位bit输出
m_bufferedByte = leadByte & 0xff;// 将本次m_low的高8位存入缓存器中
m_Bitstream->write( byte, 8 ); //输出
byte = ( 0xff + carry ) & 0xff;
while( m_numBufferedBytes > 1 )
{
m_Bitstream->write( byte, 8 );
m_numBufferedBytes--;
}
}
else
{
m_numBufferedBytes = 1;
m_bufferedByte = leadByte;
}
}
}