CABAC(Context-based Adaptive Binary Arithmetic Coding),基于上下文的自适应二进制算术编码。CABAC是H.264/AVC标准中两种熵编码中的一种,它的编码核心算法就是算术编码(Arithmetic Coding)。
算术编码与传统的编码方法有很大的区别,传统编码是通过符号映射实现的。映射包含符号(symbol)与码字(codeword)两个要素,如下面的例子
symbol | e | h | l | o |
codeword | 00 | 01 | 10 | 11 |
通过上述的映射表,我们可以把“hello”编码成码流 01 00 10 10 11。
而诸如Haffuman,Shannon这些编码方法也没脱离这种编码模式,他们只是通过符号出现的概率对码字进行调优。
算术编码采用的并非上述这种传统的单符号映射模式进行编码,它不是将单个符号映射成一个码字,而是从全序列出发,将输入的符号依据它的概率映射到[0,1)内的一个小区间上,如此递归地进行区间映射,最后得到一个小区间,从该区间内选取一个代表性的小数作为实际的编码输出。
如下面为算术编码的例子
symbol | e | h | l | o |
probability | 0.1 | 0.2 | 0.3 | 0.4 |
假设需要编码的符号只有“e”,“h”,“l”,“o”四个,他们出现的总概率为1,各个符号出现的概率如上述表格所示。现要求“hello”经过算术编码后的码字。
算术编码有如下编码步骤:
symbol | e | h | l | o |
sum of probability | 0 | 0.1 | 0.1+0.2 | 0.1+0.2+0.3 |
interval | [0,0.1) | [0.1,0.3) | [0.3,0.6) | [0.6,1) |
“hello”的第一个符号为“h”,那么映射的区间为[0.1,0.3)。
symbol | e | h | l | o |
interval | [0.1,0.12) | [0.12,0.16) | [0.16,0.22) | [0.22,0.3) |
“hello”的第二个符号为“e”,那么映射的区间为[0.1,0.12)。
映射区间 | 区间大小 | |
初始值 | [0,1) | 1 |
编码完h后 | [0.1,0.3) | 0.2 |
编码完e后 | [0.1,0.12) | 0.02 |
编码完l后 | [0.106,0.112) | 0.006 |
编码完l后 | [0.1078,0.1096) | 0.0018 |
编码完o后 | [0.10888,0.1096) | 0.00072 |
算术编码的总体的编码流程可以参考下图
算术编码总体上可以按照如下进行描述:
当处理符号$a_k$时,区间$R$宽度根据$a_k$出现概率$p(a_k)$而变窄,符号序列越长,相应的子区间越窄,编码的位数越多。
算术解码就只是需要判断代表性的小数在哪个区间,相应地就知道输入的符号了。
二进制算术编码的编码方法跟算术编码是一样的,但是输入只有两个符号:“0”,“1”,也就是说输入的是二进制串。
除了是对二进制串进行编码这个特征外,二进制算术编码跟普通的算术编码还有一些区别,总体上可以按照如下进行描述:
CABAC采用的是二进制算术编码,在编码过程中需要传入二进制串,输出的也是二进制串。
在h.264标准中,CABAC在语法结构中用ae表示,它只用于编码slice_data中的语法元素(包括slice_data内部的子模块的语法元素,请参考h.264语法结构分析)
CABAC实现分为四个部分
初始化执行于slice开始之前,另外如果在编码过程中某个宏块是PCM宏块,那么在PCM宏块之后,编码下一个宏块之前也需要进行初始化。
初始化主要工作就是确定所有上下文的初始MPS以及初始状态pStateIdx。求解方法如下
preCtxState = Clip3( 1, 126, ( ( m ∗ Clip3( 0, 51, SliceQPY ) ) >> 4 ) + n ) if( preCtxState <= 63 ) { pStateIdx = 63 − preCtxState valMPS = 0 ( 9-5) } else { pStateIdx = preCtxState − 64 valMPS = 1 }
上面的计算依赖于SliceQPY,m,n三个变量,其中不同的上下文索引(contex Index)对应不同的m、n,具体的m、n的取值请参考标准9.3.1中的各个表格。上下文索引是基于语法元素以及二值化后的二进制串的索引binIdx,我们将在下一小节进行阐述。
在CABAC的初始化过程的结果会得到所有上下文索引对应的MPS与pStateIdx的初始值。如果确定了MPS为“0”,那么LPS为“1”,反之如果MPS为“1”,那么LPS为“0”。状态pStateIdx是什么呢?
状态pStateIdx是LPS出现的概率$p_{LPS}$的索引。算术编码中最重要的要素就是符号的概率,CABAC是自适应的算术编码,也就是说符号的概率会随着符号的输入而改变,这种变化就是一种状态机,如果输入的是LPS的话,状态(概率)会怎样变化,如果输入的是MPS的话,状态(概率)又会怎么变化。CABAC的状态机转换的规则由HOWARD与VITTER的"exponential aging"模型借鉴而来,转换规则如下
${p^{(t+1)}}_{LPS}= \left\{\begin{matrix}
\alpha \cdot {p^{t}}_{LPS}\qquad\qquad & if\ an\ MPS\ occurs\\
\alpha \cdot {p^{t}}_{LPS} + (1-\alpha) &if\ an\ LPS\ occurs
\end{matrix}\right.$
在CABAC中规定了LPS的概率取值范围是$p_{LPS}\in [0.01875,0.5]$,由于LPS是小概率符号,因此它的概率肯定是小于0.5的,如果某个小概率符号在状态转换的过程中超出了0.5,此时我们就需要把MPS与LPS进行交换。
CABAC的状态机中共有64个状态,pStateIdx = 0,1,2,…,63,分别代表64个不同的概率,除了pStateIdx = 63外,其他的63个状态都满足上述状态转换规则,其中
$\left\{\begin{matrix}
{p^{0}}_{LPS}=0.5 \qquad\\
{p^{62}}_{LPS}=0.01875
\end{matrix}\right.$
结合上述状态机的转换规则,我们可以得到状态转换参数
$\alpha = \left( \frac{0.01875}{0.5} \right)^{1/63} \approx 0.95$
CABAC状态机的状态转换如下图(黑色实线代表输入的是MPS,红色虚线代表输入的是LPS),具体的pStateIdx变换请参考标准的表9-45
CABAC编码的是slice data中的语法元素,在进行算术编码前,需要把这些语法元素按照一定的方法转换成适合进行二进制算术编码的二进制串,这个转换的过程被称为二值化(binarization)。
二值化的方案共有7种
对于一个非二进制的无符号整数值符号$x \geqslant 0$,在CABAC中的一元码码字由$x$个“1”位外加一个结尾的“0”组成,见下表。例如,输入的语法元素值为3,其二值化结果为110
value of syntax element | Unary | |||||
---|---|---|---|---|---|---|
0 | 0 | |||||
1 | 1 | 0 | ||||
2 | 1 | 1 | 0 | |||
3 | 1 | 1 | 1 | 0 | ||
4 | 1 | 1 | 1 | 1 | 0 | |
5 | 1 | 1 | 1 | 1 | 1 | 0 |
… | ||||||
binIdx | 0 | 1 | 2 | 3 | 4 | 5 |
一元码的变体,用在已知语法元素的最大值cMax的情况。对于$0\leqslant x < cMax$的范围内的取值,使用一元码进行二值化。对于$x = cMax$,其二值化的二进制串全部由“1”组成,长度为cMax。例如,当cMax=5时,语法元素值为4的二进制串为11110,语法元素值为5的二进制串为11111。
指数哥伦布编码由前缀和后缀组成。其中前缀部分由$l(x) = \left \lceil log_2(x/2^k+1) \right \rceil$的值所对应的一元码组成;后缀部分可通过使用长度为$k+l(x)$位的$x+2k(1-2l(x))$的二进制值来计算。详细的计算过程请参考指数哥伦布编码,请注意两者前缀区别。
用定长编码二进制的无符号语法元素, 语法元素的最大值cMax已知,那么定长编码的长度为$fixlength = \left \lceil log_2(cMax+1) \right \rceil$,其中值就是语法元素的值的二进制。定长编码用于近似均匀分布的语法元素的二值化。
详情请查看h.264标准中9.3.2.5小节
这种方案只用于对语法元素CBP的二值化。4位的FL(cMax=15)的前缀用于编码亮度CBP,2位的TU用于编码色度CBP(当色彩格式为4:2:0或4:2:2时才会存在这个后缀)
这种方案的前缀使用一元截断码,后缀使用k阶哥伦布编码。但是在取值较小的范围内,只用一元码表示(即只有前缀部分)。对于不同的语法元素,有不同的截断值与阶数。如下表为abs_level_minus1的二值化表(cMax=14的TU、0阶哥伦布编码)
abs_level_minus1 | Prefix (TU) | Suffix (EG0) | |||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | ||||||||||||||||||
1 | 1 | 0 | |||||||||||||||||
2 | 1 | 1 | 0 | ||||||||||||||||
3 | 1 | 1 | 1 | 0 | |||||||||||||||
4 | 1 | 1 | 1 | 1 | 0 | ||||||||||||||
… | … | … | … | … | … | … | |||||||||||||
… | … | … | … | … | … | … | … | … | … | … | … | … | |||||||
12 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | ||||||
13 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | |||||
14 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | ||||
15 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | ||
16 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | ||
17 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 |
18 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 1 |
… | |||||||||||||||||||
binIdx | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
具体哪个语法元素选择哪种二值化方案,请查看标准9.3.2中d第一个表格。
在前面初始化的时候就出现了上下文这个概念,那么上下文所指的是什么?
以JM中的上下文结构体为例
//! struct for context management struct bi_context_type { unsigned long count; byte state; //uint16 state; // index into state-table CP unsigned char MPS; // Least Probable Symbol 0/1 CP };
上下文包含两个变量:MPS、pStateIdx(count只是用于计数)。在CABAC编码的过程中会碰到需要修改这两个值的情况(如上面的状态变换),这些修改都是以上下文为单位的。
语法元素在经过二值化后形成二进制串,二进制串中不同binIdx位置上的MPS(出现频率高的符号)可能会有所不同,并且概率也可能会不同,因此需要用一个概念来表示特定语法元素的二进制串中特定binIdx的MPS与pStateIdx,上下文就是这样的概念。
在h.264标准中,用一个上下文索引ctxIdx来代表上下文,ctxIdx的取值为0~1023,就是说h.264的上下文一共有1024个。
ctxIdx的计算方式分为两种
如果语法元素为coded_block_flag、significant_coeff_flag、last_significant_coeff_flag、coeff_abs_level_minus1,即残差系数部分的语法元素,则
ctxIdx = ctxIdxOffset + BlockCatOffset(ctxBlockCat) + ctxIdxInc(ctxBlockCat)
否则
ctxIdx = ctxIdxOffset + ctxIdxInc
其中的变量有
Syntax Element | ctxIdxOffset |
mb_type (SI slices only) |
prefix: 0 suffix: 3 |
mb_type (I slices only) | 3 |
mb_skip_flag (P,SP slices only) |
11 |
mb_type (P, SP slices only) |
prefix: 14 suffix: 17 |
sub_mb_type[] (P, SP slices only) |
21 |
… | … |
ctxIdxOffset | b0 | b1 | b2 | b3 | b4 | b5 | >=b6 |
40 | 0,1,2 | 3 | 4 | 5 | 6 | 6 | 6 |
ctxIdxOffset=40代表mvd的横向分量,上述表格描述了这个语法元素各个binIdx所对应的ctxIdxInc。也就是说mvd横向分量按照binIdx得到的ctxIdx如下表
binIdx | b0 | b1 | b2 | b3 | b4 | b5 | >=b6 |
ctxIdx | 40,41,42 | 43 | 44 | 45 | 46 | 46 | 46 |
Block description | maxNumCoeff | ctxBlockCat |
block of luma DC transform coefficient levels | 16 | 0 |
block of luma AC transform coefficient levels | 15 | 1 |
block of 16 luma transform coefficient levels | 16 | 2 |
block of chroma DC transform coefficient levels when ChromaArrayType is equal to 1 or 2 | 4*2/4*4 | 3 |
block of chroma AC transform coefficient levels when ChromaArrayType is equal to 1 or 2 | 15 | 4 |
block of 64 luma transform coefficient levels | 64 | 5 |
block of Cb DC transform coefficient levels when ChromaArrayType is equal to 3 | 16 | 6 |
block of Cb AC transform coefficient levels when ChromaArrayType is equal to 3 | 15 | 7 |
block of 16 Cb transform coefficient levels when ChromaArrayType is equal to 3 | 16 | 8 |
block of 64 Cb transform coefficient levels when ChromaArrayType is equal to 3 | 64 | 9 |
block of Cr DC transform coefficient levels when ChromaArrayType is equal to 3 | 16 | 10 |
block of Cr AC transform coefficient levels when ChromaArrayType is equal to 3 | 15 | 11 |
block of 16 Cr transform coefficient levels when ChromaArrayType is equal to 3 | 16 | 12 |
block of 64 Cr transform coefficient levels when ChromaArrayType is equal to 3 | 64 | 13 |
在残差系数部分,上下文是会根据不同的残差块类型做出不同选择的,BlockCatOffset就代表了不同的残差块类型的索引偏移,具体偏移值可以查看标准中的相关表格。
算术编码是基于区间划分的,普通的概率划分需要使用到多位乘法。CABAC的算术编码为了降低计算复杂度,并便于硬件实现,采取了如下一些方法:
该过程可分为5个步骤
在区间划分结束后,如果新的区间不在$[2^8,2^9)$之内,即$R<256$,256是最大编码区间的$\frac{1}{4}$,那么就需要进行重归一化操作(incremental output and interval expansion),输出一位或多位编码比特。重归一化过程如下图所示。
在CABAC编码过程中,在输入符号后,进行区间更新,接下来就是重归一化过程。下面就以$[0,2^{10})$表示区间$[0,1)$为例,分析重归一化过程
(请注意,在该过程中,$[0,2^{10})$只起到辅助作用,实际的区间为$R$)
在编码输出“0”或者“1”的阶段,用PutBit(B)表示
关于PutBit(B)的分析,参考上面重归一化的区间图,可以看到有三种情况
另外,PutBit(B)不会编码第一个bit。原因是CABAC在初始化的时候,会以$[0,2^{10})$表示区间$[0,1)$,而在初始化区间时$R=510,L=0$,这意味着已经进行了第一次区间选择,区间为$[0,0.5)$,需要输出“0”。PutBit(B)在此阻止这个“0”的输出,这样就能得到正确的算术编码结果了。
有些语法元素在二值化后选择的可能不是上述的算术编码,而是旁路编码,具体情况请查看h.264标准9.3.2的第一个表格。旁路编码中,假设待编码的符号符合近似的均匀分布。下图给出了旁路模式下的编码过程。
旁路模式有几个特点:符号均匀分布,无需对$R$进行量化与查表;每编码完一个符号后,$R$总是会小于$2^8$,因此每次都需要对概率区间进行放大;把对$L$的移位操作提到了前面,因此旁路编码的重归一化的区间可以看作由$[0,2^{10})$变成了$[0,2^{11})$。
下面是旁路编码的一个例子
在编码语法元素end_of_slice_flag(ctxIdx = 276)以及I_PCM mb_type时会调用EncodeTerminate这个编码过程。
在EncodeTerminate中,采用的是pStateIdx = 63的状态,这个状态表示的是当前宏块是否为该slice的最后一个宏块的概率。在该状态下,对概率区间的划分跟概率区间量化值无关。在编码end_of_slice_flag以及I_PCM的mb_type时,概率区间固定为$R_{LPS} = 2$。如果当前宏块为slice的最后一个宏块(end_of_slice_flag = 1)或者当前编码为PCM宏块并且编码它的mb_type I_PCM时,结束CABAC编码,调用EncodeTerminate中的EncodeFlush。具体情况请参考标准中的9.3.4.5小节。
在编码完成slice的最后一个宏块后,将会调用字节填充过程。该过程会往NAL单元写入0个或者更多个字节(cabac_zero_word),目的是完成对NAL单元的封装(标准9.3.4.6)。这里有计算如下
$k = \left \lceil \left(\left \lceil 3\times \left(32\times BinCountsInNALunits - \frac{RawMbBits\times PicSizeInMbs}{1024}\right) \right \rceil - NumBytesInVclNalunits\right)/3 \right \rceil$
如果$k>0$,则需要将3字节长的0x000003添加到NAL单元$k$次。这里的前两字节0x0000代表了cabac_zero_word,第三个字节0x03代表一个emulation_prevention_three_byte,这两个语法元素请参考h.264语法结构分析的相关部分。
如果$k \leqslant 0$,则无需添加字节到NAL单元。
式子中的各个变量所代表的意思请查看标准