H.264解析器实现
前注:
1、代码最好是去看标准,我在写的时候也发现自己的代码有很多的错误。可能都没改过来。
2、在写的时候用到了解码、解析、解算,这些词语的意思都是指同一个意思即求解的意思。
3、文中所有带“标准”之意的都是指:ISO/IEC 14496-10。
4、不保证自己的所有叙述都是正确的,但是会不断改进的。
5、现在只是我在学习的过程中写的笔记和心得,之后会不断总结起来。
6、未完,因为初学,有错误请指出、感激不尽
小注:
因为接下来要先去看别的,所以这里需要先搁置一段时间。
还没补充的:CABAC普通句法元素的解析,由上下文索引 ctxIdx 求到二进制位数据的过程,实现过程的代码。
记录:
今天实在是写不下去代码了,所以来写一下这段时间的总结:解析器如何实现。
2020年05月25日22:43:37
又看到了大篇大片的if-otherwise,我感觉又要来写了。
这段时间忙的很乱,一方面想往前推进度,另一方面很多之前的工作又没做好。
而且还经常出现“怀疑正确的结果”“手抖敲错变量、数字”等问题。
很难受,一些很简单的逻辑转了大半天。
2020年05月30日22:13:27
终于从帧内16x16的残差块读取走出来了,接下来是帧内16x16的残差块的解码。
昨天用了半天 Debug ,结果却是少打了一个 2,人都傻了。
但是不得不说找到错误的时候还是挺开心的。目前也不知道自己解出来的系数对不对,虽然在矩阵结构上是对的,但是值差了300多,但是和每一位数据和 JM 运行得到的数据又是一样的,emmm。
我对“解析器”的认识
我曾经简单地看过ffmpeg的源文件,印象比较深的就是decode和parse这两个单词(源码太多了看不太懂)。
而且,h264的数据是用句法元素组织起来的,所以说在解码之前还需要将句法元素全部都读出来。
所以我就简单的理解为“解码”是将数据解算成图片的过程,而“解析”是将句法元素解算成解码用的数据的过程。
(只是个人理解)。
那么作为这个解析器,他必然需要具有读取所有句法元素的方式。
也就是前面说过的下面这些:
b(8) 读入8个bit
f(n) 读进n个bit
i(n)/i(v) 读进n个bit 解释为有符号数
u(n)/u(v) 读进n个bit 解释为无符号数
ae(v) 基于上下文自适应的二进制算术熵编码
ce(v) 基于上下文自适应的可变长熵编码
ue(v) 无符号 指数Golomb编码
se(v) 有符号 指数Golomb编码
me(v) 映射 指数Golomb编码
te(v) 截断 指数Golomb编码
可以看到,里面有很多都是作为bit单位来读取的。
所以首先需要实现一个bit单位读取的解析器,对于上下文自适应的两种编码方式,还需要单独用这个bit解析器去做解析器。
具体实现
bit读取的实现
这个其实很简单,首先需要做一个缓冲区,然后用类似FILE* 指针的那一套方法做一个出来(我不知道是不是有类似这样的标准库,就直接手动实现了)。
读取的比特拆成两部分:字节部分、比特部分(字节×8+比特 = 总的比特)
1、一位的读取
核心用这个方法: result &= buf_data[charIndex] >> (8 - bitsIndex - 1);
就是比较简单的位运算。
2、多位的读取
用的是拆分的方法:把位数拆成3个部分:不满8字节的头部、8字节的中部、不满8字节的尾部。
比特用上面的一位读取,字节直接buf_data[i]读取。
这里有个小细节就是字节部分大于等于1时,字节部分-1,比特部分+8。因为可能出现(假设前面的1是开始的地方(包括1)到后面的1是结束的地方(也包括)读取22个比特)0100 0000|0000 0000|0000 0010,也就是7+8+7的问题,如果不这样处理就会变成(6个比特2个字节)6+8+8。所以说需要拆成3部分。防止出现把多于8的比特位算到字节位里面去。头部和尾部加在一起应该是不超过14个比特而不是8个比特。同样对于7+7:0100 0000|0000 0010的问题也是如此。总之对于小于16的bit统一采用一位读取也不会很慢。对于大于16比特就一定会存在一个字节读取了。
头部比特位一位一位读取,到了字节对齐位就找字节数用字节读取,结束之后尾部继续读完剩下的比特位。
3、缓存区的处理
缓存区读完、为空的时候需要刷新缓存,索引归零。在nal层中有一个read_next(n)的函数(用来去掉0x 00 00 03中的03),做到要读取但是索引不变,考虑到可能在next()的时候发生缓存区刷新从而找不到原来的缓存区,所以这时加入第二个缓存区,读完之后把索引赋之前的值,缓冲区指针还给1号。这时刷新的时候从2号缓存区读入数据到1号,2号free()。如果只是从h264文件而不是媒体文件中读的话可能不需要这个过程。
ue(v)
ue的读取简单说来就是这样:首先读入n个0直到1,1后面再读n位解释为无符号整数v。然后就是2^n-1+v,这也有另一种解读的方法:因为2^n就是把1左移n位,所以从1开始往后到尾的n+1位解释成无符号数再-1,后面这种方法便于理解。
举个例子:010 : 2^1 -1 + 0 == 1; 00101 : 2^2 - 1 + 1 = 4;
se(v)
se(v)是从ue(v)继承来的(准确的说是从指数Golomb编码继承),但是他的值是解释成有符号整数。
num_cur = bread_ue();
result = (int64_t)pow((-1), (num_cur + 1)) * (int64_t)(ceil(num_cur / 2));
大概的意思就是把ue(v)的值区间除以二,一半用来表示负数。
比如(前面是ue后面是se):0 - (0); 1 - (1); 2 - (-1); 3 - (2) ……
pow是幂函数,ceil是取上限整数。
这两个要包含camth头文件。
me(v)
这个是用来专门读取 code_block_pattern 这个句法元素的。(不用ae(v)读取的时候就是用这个方法读取)
首先也是按照ue(v)的读取拿到值。然后去查表比如下面是前面的部分(这是不完整的表)。
codeNum | coded_block_pattern | |
---|---|---|
Intra_4x4,Intra_8x8 | Inter | |
0 | 47 | 0 |
1 | 31 | 1 |
这也是为什么他叫做“映射”指数Golomb编码。就是用指数Golomb编码方法解码然后映射查表得到值=,=。
查表这里也是做成静态表,然后按索引查表
te(v)
首先判断句法元素的值的范围[0,x],如果x > 1。那么按照ue(v)的方法读取
如果x == 1;那么只读一位,然后逻辑取反得到值。
举例子:
ue(v)中的0编码后是1,1编码后是010,2编码后是011;
te(v)中的0编码后是1,1编码后是0,2编码后就是011;
反过来就是解码了。可能主要是针对0-1的优化吧,类似于bool值这种的?
ce(v)
ce(v)就是上下文自适应的可变长编码CAVLC的描述子。
(这里我还没学)
CAVLC的原理可以现在参考oskycar的这篇:H.264的CAVLC(编码.解码)过程详解,写的很详细。跟着这篇文章的例子走一遍就能理清楚大概的实现过程。
需要注意的是,在ISO标准的句法表中,CAVLC的解码就是上文说的这个过程。因为CAVLC解出来就是一串数据而不是一个数据。
残差块中输入的参数是 (0, 15) ,所以他解出来就应该是一个16个元素的一维数组。也就是说 ce(V) 说的并不是读取一个或者几个bit,而是直接解出一串数据。
而其中也没有说必须是读多少位出来。也是要查表或者计算,最后都有唯一的0-1串对应然后就能解算出来具体的值了。
也就是说ce(v)不是具体怎么读取、读多少位的描述符,他说的是一种解析的方法。
以后回过头来写具体实现吧。这里我也没细看。(我这个时候看的264文件就全部都是算术二进制熵解码)
ae(v)
ae(v)是上下文自适应的算术二进制熵编码CABAC。
我就是看到ae(v)看到崩溃的。因为这个实在是太让人头大了。
在残差块中我们知道,解残差块的函数是一个函数指针,在之前的熵解码标志为1的时候需要使用到 C A B A C 的解码方式。也就是这里所说的 ae(v) 。简单理解的话ae(v)是一个描述子,C A B A C 是一种编码解码方式,但是我觉得在解码方面这二者其实没有多大差别,因为 ae(v) 用起来还是几乎要调用整个 CABAC 对象。
ae(v)这个描述子第一次出现不是在残差块中,在Slice中就有条件出现。
注:对原理性的东西我也不熟悉,以下都是我靠自己的读书理解和网上的参考进行简单的实现,
也就是说可能有大量错误。
算术编码有一个特点就是对整个序列编码而不是对哪一个单一的元素编码,用 h264 中的简单来讲的话就相当于把很多个句法元素编码到一个字节串中。然后解码的时候按照区间来解码。因为 CABAC 会动态调整编码区间,所以解码的时候需要不停的更新状态。
这些网上一抓一大把,我也不是很懂,也就不多写了。关键是知道CABAC读入的时候是读入一个序列的数据,而不是某一个数据就行。
CABAC 大致归纳为4个步骤。
初始化:
上下文变量初始化:Slice 中第一次遇到 ae(v) 描述子,CABAC 需要进行初始化,这里要的事情就是把初始的状态表算出来,之后就要靠这张表来读数据、还要维护这张表。因为 CABAC 是以 Slice(片) 作为生命周期的,也就是说到了下一个Slice中用到 ae(v) 的时候,上一张表就已经不能再用了,就需要初始化新的表。
算术解码引擎初始化:就两个动作:CodiRang = 510, CodiOffset = read_bits(9)(这里发生了数据读取的操作),按照程序框图来就很简单。
二值化:我不知道这个概念到底是在解码还是在编码里面的,标准文章中说的是输入句法元素的请求、输出句法元素的二值化。后来我做了一遍之后感觉二值化的概念在解码里面好像很模糊,有时候是查表、有时候是计算。
后来想了下,觉得这其实就是一种描述结果的方法。也就是描述了什么样的二进制串应该是正确的结果。
每一次解码出来的都是一个二进制数 0/1 ,通过移位求与就可以得到一个二进制串,这个二进制串在和二值化的某一个二进制串完全一致的时候,那么这就是应该输出的值,否则就继续解码下一位。
句法元素二值化之后不是一个值,而是一系列值(这也是我觉得难以理解的地方:对于编码来说就只有一个值,这个时候叫做二值化就感觉很正常)。总之,在这一系列值中,对比总可以得到输出的值。
所以有的句法元素是查表,有的是说明的二值化的一种算法。查表的我是先做好表,计算的则是最后对比的时候把算法嵌入进去。
二值化的过程还会产生在计算中需要的几个变量(也是查表得到),但是我直接写死在方法里面了。
计算值:
单单从主要架构来说的话:我们当前需要解的二进制位的索引 binIdx 从0开始每次自增1,每一个 binIdx 都可以得到 ctxIdx ,然后用 ctxIdx 去查我们初始化生成的那张表,然后经过一系列的解算过程,最后得到一个二进制数,然后就是上面所说的二值化的对比和输出的过程了。
后处理:
一个是解码完成的后处理;
还有便是解码中的后处理,之前说过,每次解出来一个二进制位之后都需要动态维护表;还有在解码区间精度不够的时候重整化等等。
总结:
去掉各种细节的总结就是:
每次解一个binIdx上的二进制位,binIdx和其他参数得到ctxIdx,ctxIdx查表解算得到二进制位,二进制位组成一个串,是二值化中的某个串,那就得到这个值。
实现过程
初始化
初始状态表的初始化
for (size_t j = ctxIdxRangOfSyntaxElements[i][ctxRangeCol][0]; j <= ctxIdxRangOfSyntaxElements[i][ctxRangeCol][1]; j++)
{ //j is ctxIdx
m = mnValToCtxIdex[j][mncol][0];
n = mnValToCtxIdex[j][mncol][1];
if(!(ctxIdxRangOfSyntaxElements[i][ctxRangeCol][0] == 0 && ctxIdxRangOfSyntaxElements[i][ctxRangeCol][1] == 0))
{
preCtxState = (uint8_t)Clip3(1, 126, ((m * Clip3(0, 51, ((int)lifeTimeSlice->ps.SliceQPY >> 4)))) + n);
if(preCtxState <= 63)
{
pStateIdx = 63 - preCtxState;
valMPS = 0;
}
else
{
pStateIdx = preCtxState - 64;
valMPS = 1;
}
//这里赋值
ctxIdxOfInitVariables->set_value(j, 0, pStateIdx);
ctxIdxOfInitVariables->set_value(j, 1, valMPS);
}
}
因为每种片的初始化的变量区间都不同,所以我先做了一个 句法元素-区间 表(这个表是标准表的改动),4种片从上到下分别从下限初始到上限,然后到下一行。最后一栏的两个数字加起来就是我自定的句法元素的表示数字比如1, 1,表示11号mb_skip_flag句法元素。
在上面我们说道ae(v)这个描述子是不需要参数的,但是实际上他是根据句法元素的种类来决定用什么方法的,所以我们还是得传一个表示句法元素是哪一个句法元素的参数。
(这里没有做成枚举类型,因为考虑到可能还有其他参数的问题)
static const uint16_t ctxIdxRangOfSyntaxElements[43][5][2] = {
//up down //pre suffic
//SI //I //P SP //B
{ {udf , udf }, {udf , udf }, {11, 13 }, {24, 26 }, {1, 1} },//slice_data() mb_skip_flag
{ {70, 72 }, {70, 72 }, {70, 72 }, {70, 72 }, {1, 2} },// mb_field_decoding_flag
{ {0, 10 }, {3, 10 }, {14, 20 }, {27, 35 }, {2, 2} },//macroblock_layer() mb_type
{ {na, na }, {399, 401 }, {399, 401 }, {399, 401 }, {2, 1} },// transform_size_8x8_flag
{ {73, 76 }, {73, 76 }, {73, 76 }, {73, 76 }, {2, 2} },// coded_block_pattern (luma)
{ {77, 84 }, {77, 84 }, {77, 84 }, {77, 84 }, {2, 3} },// coded_block_pattern (chroma
{ {60, 63 }, {60, 63 }, {60, 63 }, {60, 63 }, {2, 4} },// mb_qp_delta
{ {68, 68 }, {68, 68 }, {68, 68 }, {68, 68 }, {3, 5} },//mb_pred() prev_intra4x4_pred_mode_flag
{ {69, 69 }, {69, 69 }, {69, 69 }, {69, 69 }, {3, 1} },// rem_intra4x4_pred_mode
{ {na, na }, {68, 68 }, {68, 68 }, {68, 68 }, {3, 2} },// prev_intra8x8_pred_mode_flag
{ {na, na }, {69, 69 }, {69, 69 }, {69, 69 }, {3, 3} },// rem_intra8x8_pred_mode
{ {64, 67 }, {64, 67 }, {64, 67 }, {64, 67 }, {3, 4} },// intra_chroma_pred_mode
{ {udf , udf }, {udf , udf }, {54, 59 }, {54, 59 }, {4, 1} },//mb_pred()&sub_mb_pred() ref_idx_l0
{ {udf , udf }, {udf , udf }, {udf , udf }, {54, 59 }, {4, 2} },// ref_idx_l1
{ {udf , udf }, {udf , udf }, {40, 46 }, {40, 46 }, {4, 3} },// mvd_l0[][][0]
{ {udf , udf }, {udf , udf }, {udf , udf }, {40, 46 }, {4, 4} },// mvd_l1[][][0]
{ {udf , udf }, {udf , udf }, {47, 53 }, {47, 53 }, {4, 5} },// mvd_l0[][][1]
{ {udf , udf }, {udf , udf }, {udf , udf }, {47, 53 }, {4, 6} },// mvd_l1[][][1]
{ {udf , udf }, {udf , udf }, {21, 23 }, {36, 39 }, {5, 1} },//sub_mb_pred() sub_mb_type[]
{ {85, 104 }, {85, 104 }, {85, 104 }, {85, 104 }, {6, 1} },//residual_block_cabac( ) coded_block_flag 61
{ {460, 483 }, {460, 483 }, {460, 483 }, {460, 483 }, {6, 1} },//
{ {udf , udf }, {1012, 1023 }, {1012, 1023 }, {1012, 1023}, {6, 1} },//
{ {105, 165 }, {105, 165 }, {105, 165 }, {105, 165 }, {6, 2} },// significant_coeff_flag[ ] 62
{ {277, 337 }, {277, 337 }, {277, 337 }, {277, 337 }, {6, 2} },//
{ {udf , udf }, {402, 416 }, {402, 416 }, {402, 416 }, {6, 2} },//
{ {udf , udf }, {436, 450 }, {436, 450 }, {436, 450 }, {6, 2} },//
{ {udf , udf }, {484, 571 }, {484, 571 }, {484, 571 }, {6, 2} },//
{ {udf , udf }, {776, 863 }, {776, 863 }, {776, 863 }, {6, 2} },//
{ {udf , udf }, {660, 689 }, {660, 689 }, {660, 689 }, {6, 2} },//
{ {udf , udf }, {718, 747 }, {718, 747 }, {718, 747 }, {6, 2} },//
{ {166, 226 }, {166, 226 }, {166, 226 }, {166, 226 }, {6, 3} },// last_significant_coeff_flag[ ] 63
{ {338, 398 }, {338, 398 }, {338, 398 }, {338, 398 }, {6, 3} },//
{ {udf , udf }, {417, 425 }, {417, 425 }, {417, 425 }, {6, 3} },//
{ {udf , udf }, {451, 459 }, {451, 459 }, {451, 459 }, {6, 3} },//
{ {udf , udf }, {572, 659 }, {572, 659 }, {572, 659 }, {6, 3} },//
{ {udf , udf }, {864, 951 }, {864, 951 }, {864, 951 }, {6, 3} },//
{ {udf , udf }, {690, 707 }, {690, 707 }, {690, 707 }, {6, 3} },//
{ {udf , udf }, {748, 765 }, {748, 765 }, {748, 765 }, {6, 3} },//
{ {227, 275 }, {227, 275 }, {227, 275 }, {227, 275 }, {6, 4} },// coeff_abs_level_minus1[ ] 64
{ {udf , udf }, {426, 435 }, {426, 435 }, {426, 435 }, {6, 4} },//
{ {udf , udf }, {952, 1011 }, {952, 1011 }, {952, 1011 }, {6, 4} },//
{ {udf , udf }, {708, 717 }, {708, 717 }, {708, 717 }, {6, 4} },//
{ {udf , udf }, {766, 775 }, {766, 775 }, {766, 775 }, {6, 4} } //
};
这里的na是在标准表里面的na,表示不存在的类型,udf是我定义的,是在标准表里面 为空(没有数据)
的数据。这两个的值都是0,当遇到上下限都是0的时候就不初始化。(虽然这里的区间数据可能不对,但是我们不去使用它就行了,实际上片类型限定之后也不会使用到区间以外的数)
然后ctxIdx和m n的映射表一共是1024个数据,从0到1023。每一个ctxIdx对应一个m和一个n。上面这张表就是ctxIdx的区间,m n是在初始化中要用到的变量。因为cabac的生命周期是slice,我们还要给cabac传一个slice对象来拿到这个片的量化参数偏移。SliceQPY(或者直接把这个参数传进来,但是我的cabac对象是在slice前面定义的)
(因为 cabac 要用到很多的数据:包括片、宏块,所以我就都传进来了。)
第一句就用到了所有需要的数值:
preCtxState = (uint8_t)Clip3(1, 126, ((m * Clip3(0, 51, ((int)lifeTimeSlice->ps.SliceQPY >> 4)))) + n);
Clip3是求值在区间内的值,过上限返回上限,过下限返回下限,否则返回这个值。我用了一个模板函数
template
T Clip3(T lower, T upper, T value)
{
if(value >= upper) return upper;
else if(value <= lower) return lower;
else return value;
}
至于这张表的数据类型,因为数组指针用起来有点不懂,所以我自己做了一个模板二维数组类。
至此初始表的计算完成(也就是上下文变量的初始化完成。)ctxIdx对应pStateIdx、valMPS。
pStateIdx是决定状态的,valMPS是用来求值的。
引擎初始化
注意到这里读取到值就行了,还有一点就是codIOffset从数据流读入的数据不能是511 510
只是解码的话应该不用关心这个问题。
codIRange = 510;
codIOffset = p->read_un(9);
return 1;
二值化
U:一元二值化
Value of syntax element | Bin string | |||||
---|---|---|---|---|---|---|
0 | (I_NxN)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 |
n个1,遇到0就结束,binIdx的值就是句法元素的值
TU:截断一元二值化
需要输入一个cMax,
如果句法元素值等于cMax,那么就是二值串就是所有的位都为1,长度为cMax的二值串。
句法元素值小于cMax,那么就是一元二值化U的二值化方法。
UEGk:TU 和 k阶指数Golomb编码 串连二值化
需要一个 signedValFlag 和 uCoff。
signedValFlag 指示是否编码(正负数的)符号, uCoff 指明 TU 的最大长度 cMax 。
UEGk本身就包含一个前缀和一个后缀,
前缀是TU,前缀位数是Min( uCoff, Abs(synElVal)),用于TU计算的值为:cMax = uCoff。
后缀是k阶指数Golomb编码,这里的k来自句法元素(表中有说明),比如后面的解系数绝对值的时候是 UEG0 ,后缀的解法也有标准伪代码。所以有必要先看看标准中给出的二值化过程。
if( Abs(synElVal) >= uCoff )
{
sufS = Abs( synElVal ) − uCoff
stopLoop = 0
do
{
if( sufS >= ( 1 << k ) )
{
put( 1 )
sufS = sufS − ( 1<> k ) & 1 )
stopLoop = 1
}
} while( !stopLoop )
}
if( signedValFlag && synElVal ! = 0)
if( synElVal > 0 )
put( 0 )
else
put( 1 )
这个二值化的过程简单说明了 k 阶指数 Golomb 是如何被编码的。(前面提到二值化并不是解码的过程,而是解码完了之后的结果确认过程。)
首先是sufS = Abs( synElVal ) − uCoff;
也即句法元素的绝对值(再次注意这里的值不是要去解算的值,而是用来对比确认是不是结果的值)减去前缀的最大值(所以前缀没有到最大值的时候没有求后缀这个过程)。
再看最后一个 if if( signedValFlag && synElVal ! = 0)
也就是要不要对符号编码,如果要并且当前的值不为0的话,后面还有一位编码的符号位。
中间层的代码分析:
sufS = Abs( synElVal ) − uCoff
stopLoop = 0
do
{
if( sufS >= ( 1 << k ) )
{
put( 1 )
sufS = sufS − ( 1<> k ) & 1 )
stopLoop = 1
}
} while( !stopLoop )
还是用一个例子来说明:
我自定义的:数字前面带Bx表示一个二进制数
比如现在需要 0阶指数Golomb 编码 Bx1100 ,不编码符号位 signedValFlag = 0。
(这里略掉了前缀的截断编码)
条件 | k | 输出位 | 当前串 | 当前值 |
---|---|---|---|---|
条件 sufS >= ( 1 << k ) | k | 输出位 | 当前串 | 当前值sufS −= (1< |
Bx1100 >= Bx1 | 0 | 1 | 1 | Bx1011 |
Bx1011 >= Bx10 | 1 | 1 | 11 | Bx1001 |
Bx1001 >= Bx100 | 2 | 1 | 111 | Bx0101 |
Bx0101 >= Bx1000(不成立) | 3 | 0 | 1110 | Bx0101 |
条件 k != 0 | k | 输出位 | 当前串 | 当前值sufS |
(Bx0101 >> 2) & 1 | 2 | 1 | 11101 | Bx0101 |
(Bx0101 >> 1) & 1 | 1 | 0 | 111010 | Bx0101 |
(Bx0101 >> 0) & 1 | 0 | 1 | 1110101 | Bx0101 |
二值化之后得到 1110101 串。值为 Bx1100(十进制为12) signedValFlag = 0
所以反过来解码过程:
首先读入的是 n 个1直到0,不包含0的1串解释成无符号数 v1 ,
然后再读入 n 位解释成无符号数 v2 ,
结果就是: v1 + v2 。
如果是 1阶 指数Golomb 编码 Bx1100 ,不编码符号位 signedValFlag = 0。
条件 | k | 输出位 | 当前串 | 当前值 |
---|---|---|---|---|
条件 sufS >= ( 1 << k ) | k | 输出位 | 当前串 | 当前值sufS −= (1< |
Bx1100 >= 0x10 | 1 | 1 | 1 | Bx1010 |
Bx1010 >= 0x100 | 2 | 1 | 11 | Bx0110 |
Bx0110 >= 0x1000(不成立) | 3 | 0 | 110 | Bx0110 |
条件 k != 0 | k | 输出位 | 当前串 | 当前值sufS |
(Bx0110 >> 2) & 1 | 2 | 1 | 1101 | Bx0101 |
(Bx0110 >> 1) & 1 | 1 | 1 | 11011 | Bx0101 |
(Bx0110 >> 0) & 1 | 0 | 0 | 110110 | Bx0101 |
二值化之后得到 110110 串。值为 Bx1100 (十进制为12) signedValFlag = 0
同样:反过来,先读入 n 个1直到0,不带0的串(11)解释成无符号数v1,这里有个小细节在后面细说。
在这里:v1 = Bx11,从上面我们看到 k 是从 1 开始的。所以需要左移一位得到v1:
v1 <<= 1;(即:v1 = (Bx11 <<= 1;)) 得到 Bx110 也就是 v1 == 6;
然后读入 n + 1 为解释成无符号整数 v2:
v2 = Bx110; 所以 v2 == 6;
结果:v = v1 + v2 = 12;
小细节:二值化的时候,前面的1是从低位往高位二值化的,比如第二个例子的的串 110110 前面的两位 11,第一个 1 是在 2^1 位上,而第二个是在 2^2 位上(后面会讲到的的前缀后缀的问题),所以实际上应该反过来表示,但是因为都是1,所以反过来也是11 (所以实际上没有影响,直接读也行)。后面的 110 因为 k 是从高到低然后求与的,所以就是 1×2^2+1×2^1+1×2^0 = 6;(注意到先写入的是高位还是低位就行。不过这个也看自己的方法是怎么写的。后面的讨论会详细说。)
总结:
k 阶指数 Golomb 是先读入 n 个1直到0,然后左移 k 位得到 v1 值。
然后读入 n + k 位解释成无符号整数 v2,
v = v1 + v2;
长度:(n) + (1) + (n + k)
组成:前面的 n 位 1 + 中间的 0 位 + 后面的 n + k 位数据
带上 截断的一元二值化 前缀(这里是联合二值化),
如果没有截断就不会有会面的 k 阶 Golomb,
所以求解的时候就是:
句法元素的值 = 截断二值化的值 + k 阶指数 Golomb 二值化的值
(前面我们提到 k 阶指数 Golomb 二值化之前要先减去截断二值化的长度,而截断二值化的长度就是他的值)。
对于这种联合二值化来说:
因为二值化是先求一个句法元素的二值化,而我们现在解码这个句法元素的时候,每求出来一位,总不能把所有可能的值二值化之后然后和我们求出来的串对比吧(有些确实是和所有可能的值对比,比如 mb_type 这个句法元素就是,但是对于这种联合二值化的不行,因为他编码的基本都是很大的数而不是很少的几个数。),
所以这个时候我们直接把二值化的方法嵌入到对比的结果里面去,看我们求出来的串是不是一个在限定条件下合法的二值串就行。
FL:定长编码
需要输入一个cMax,定长的长度为fixedLength = Ceil( Log2( cMax + 1 ) )
定长的二值串的值就是句法元素的值。比如定长2,串为10,就表示 2 这个值。
关于前缀(prefix)和后缀(suffix)的问题:
由于我们解二进制位是 binIdx 从 0 开始解的,所以先解出来的位在低位,后解出来的位在高位,而前缀和后缀是从先解还是后解的方向来看的。
解完的的二进制串是从0开始索引的,我们需要反过来才是我们认知数字的顺序,这时候高位在前,低位在后,所以后缀在前,前缀在后:
举个例子:我们解出来的二进制串是 1 0 0 1 0
在程序中就是这个样子(数据不一定使用字符数组表示,这里只是这样举例而已。):
char ch[5];
ch[0] = 1; ch[1] = 0; ch[2] = 0; ch[3] = 1; ch[4] = 0;
//上面的顺序是: binIdx 从0->4 (解码的时候binIdx是从0往上自增的)
这个时候 ch[0] 是 2^0 位,值为 1, ch[3] 是 2^4 位,值为 16 。
但是在人类认知的角度上是反过来的,所以 我们需要反过来看。
就是 01001 长度为5,值为17。
所以要注意二进制串的“从低到高” 和 我们看待值的 “从低到高” 的方向,前者是从左到右,后者反之。
所以前缀先入,后缀后入。后缀就在高位上面了。(注意到这一点就行,因为二值化中的表是按照低位到高位从左往右排序的,这个时候前缀在前面 / 低位)
所以这里就有两种二进制位往数据里面写的方法:
比如现在有一个数据串 10011 在文件中,我们需要把他读出来,并且在文件中我们的文件指针FILE* fp先遇到的位是最低的位2^0 。也就是值应该是 Bx11001 也就是16进制的19、十进制的25 。
一种方法是
把读出来的二进制位左移到当前串的最高位上:也就是会先后依次得到串:1 01 001 1001 11001,假设我们每次读出来都把值赋给一个 int ,那么现在就是正常的顺序,int 的值就是这个数据的值。
另一张方法是:
把当前整个串左移一位,把新解算出来的二进制位放到最低位上,也就是依次会得到串:1 10 100 1001 10011。
同样把他赋给一个 int 那么这个 int 的值就不是这个数据的值了。
计算值
由 ctxIdx 求二进制位,这个过程在标准中有详细解释,还有框图,所以现在先不写。
后处理
如果读入的句法元素是mb_type并且值是I_PCM,那么初始化引擎。
读取过程中的后处理是
流程
uint32_t result;
//初始化
if(state == 0)
{
init_variable();
init_engine();
state = 1;
}
result = Decode(syntax);
//后处理
if(syntax == 22 && (MbTypeName)result == I_PCM) {init_engine();}
printf(">>cabac: result of |%-5d| is : |%3d|\n", syntax, result);
return result;
因为全部初始化的过程只在第一次读到描述子 ae(v) 的时候调用,所以做一个 CABAC 状态就行,让他在片的第一次使用之后就不再全部初始化。。读到片尾的时候就把状态换回来。(会有一个句法元素 end_of_slice 标志片结尾的,当然也可以做在片的方法中,总之道理都一样。)syntax == 22;
22号是我定义的 mb_type 句法元素,如果是求 mb_type 并且它的值为 I_PCM (这里是25,也是查表来的做成了枚举。)那么需要引擎初始化。
普通CABAC句法元素的解码方法:
还没写。后面补充。
残差系CABAC句法元素的解码方法:
这些句法元素有名字有一个特征就是带“cat”字样。
首先是带ctxIdxBlockCat的公式
ctxIdx = ctxInc(ctxIdxBlockCat) + ctxIdxBlockCatOffset(ctxIdxBlockCat) + ctxIdxOffset
等式右边的参数分别是:以 ctxIdxBlockCat 索引的 ctxInc、ctxOffset、和总的 ctxOffset。
这里可以理解为:带 cat 后缀的数值是分了第二级的索引和偏移:
ctxIdx:上下文一级索引
ctxIdxOffset上下文一级索引的偏移
ctxInc上下文二级索引
ctxIdxBlockCatOffset上下文二级索引的偏移
所以第一个公式解读为:
一级索引 = 二级索引 + 二级偏移 + 一级偏移
code_block_flag:
(当前块可能是16x16,块,可能是8x8块,可能是4x4块,需要根据实际情况判断,这个实际上就是cabac 在解算残差块的时候本身就会有的一个句法元素,所以每进一次残差的cabac函数,都会有一个,而残差有可能是1个DC+16个AC 或者 4个8x8 或者 16个4x4 所以这个句法元素的数量也需要根据实际判断)
需要注意的是这个句法元素需要存储起来,因为推导这个 code_block_flag 的时候用到了相邻块的code_block_flag 。
这个值用来表示当前块的系数是不是被编码的数值,
如果不是的话那么所有的系数都赋值为0。
首先
是查 P283 表确定 ctxBlockCat ,比如我们现在以亮度DC变换系数为例子查下面的表。得到他的ctxBlockCat = 0;
表我简化了一下用来举例子,(这里只需要找到对应的变量名就行了)
Block description | maxNumCoeff | ctxBlockCat |
---|---|---|
亮度DC 块变换系数 Intra16x16DCLevel | 16 | 0 |
亮度AC 块变换系数 Intra16x16ACLevel[ i ] | 15 | 1 |
16亮度 块变换系数 LumaLevel4x4[ i ] | 16 | 2 |
色度DC 块变换系数 (ChromaArrayType is equal to 1 or 2 ChromaDCLevel | 4 * NumC8x8 | 3 |
色度AC 块变换系数 (ChromaArrayType is equal to 1 or 2 ChromaACLevel | 15 | 4 |
64亮度 块变换系数 LumaLevel8x8[ i ] | 64 | 5 |
上面是4:2:0用的,一共6种。下面是4:4:4用的,因为4:4:4中三种亮度、色度Cb、色度Cr都是相同的解码方法,所以这些也相同,一共(包含上面的亮度的方法)12种。
Block description | maxNumCoeff | ctxBlockCat |
---|---|---|
Cb DC 块变换系数 (ChromaArrayType == 3) CbIntra16x16DCLevel | 6 | 6 |
Cb AC 块变换系数 (ChromaArrayType == 3)CbIntra16x16ACLevel[ i ] | 5 | 7 |
16 Cb 块变换系数 (ChromaArrayType == 3) CbLevel4x4[ i ] | 6 | 8 |
64 Cb 块变换系数 (ChromaArrayType == 3) CbLevel8x8[ i ] | 4 | 9 |
Cr DC 块变换系数 (ChromaArrayType == 3) CrIntra16x16DCLevel | 6 | 10 |
Cr AC 块变换系数 (ChromaArrayType == 3)CrIntra16x16ACLevel[ i ] | 5 | 11 |
16 Cr 块变换系数 (ChromaArrayType == 3) CrLevel4x4[ i ] | 6 | 12 |
64 Cr 块变换系数 (ChromaArrayType == 3) CrLevel8x8[ i ] | 4 | 13 |
然后去查P273的 ctxIdxBlockCatOffset-ctxBlockCat 映射表,得到 ctxIdxBlockCatOffset 的值,例如这里为0
ctxBlockCat | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
coded_block_flag | 0 | 4 | 8 | 12 | 16 | 0 | 0 | 4 | 8 | 4 | 0 | 4 | 8 | 8 |
significant_coeff_flag | 0 | 15 | 29 | 44 | 47 | 0 | 0 | 15 | 29 | 0 | 0 | 15 | 29 | 0 |
last_significant_coeff_flag | 0 | 15 | 29 | 44 | 47 | 0 | 0 | 15 | 29 | 0 | 0 | 15 | 29 | 0 |
coeff_abs_level_minus1 | 0 | 10 | 20 | 30 | 39 | 0 | 0 | 10 | 20 | 0 | 0 | 10 | 20 | 0 |
也就是ctxIdxBlockCatOffset = 0, ctxBlockCat = 0;
然后是分情况讨论:因为不同的情况输入不同。
ctxBlockCat 的值:
先求 ctxIdxInc(ctxBlockCat): 以 ctxBlockCat 为索引的 ctxIdxInc 值
输入参数:
前面是ctxBlockCat的值,后面是需要的额外参数
0 6 10:没有额外的输入参数:
这个是求16x16块的DC系数,因为就是整个宏块,所以不需要额外的参数
1 2 :4x4亮度块索引
这个是求AC变换系数 或者 4x4的亮度块变换系数,所以需要一个4x4索引。对于宏块来说,AC系数一共有15个,4x4块有16个、这两个由4x4块索引来映射。所以这两个的求算方法一样。
3:色度组件索引:
这个用来求色度块的DC系数,色度块有两个Cb、Cr。分别是0 、 1
色度块的DC系数也是直接对整个宏块的色度DC系数,所以没有额外的输入参数
4:色度块的4x4块索引、色度块组件索引
求色度的AC变换系数,
5:8x8块索引
求解8x8块变换系数。
总结:
DC系数直接对整个块求,AC系数对整个宏块的4x4块求(注意AC系数只有15个),
色度块还要色度块组件的索引来指示是Cb还是Cr
0 6 10:无额外的输入参数,
首先是推导 transBlockN :
下面的 N 意思是 A 块、 B 块。因为方法通用所以用 N 代替
相邻宏块的推导,结果注册给 mbAddrN (也就是左边的A,上面的B)
transBlockN是当前块的DC块。
if( mbAddrN 可用 && mbAddrN 为 Intra_16x16)
if(ctxBlockCat == 0) transBlockN = mbAddrN->luma DC block;
if(ctxBlockCat == 6) transBlockN = mbAddrN->Cb DC block;
if(ctxBlockCat == 10) transBlockN = mbAddrN->Cr DC block;
else transBlockN = 不可用 ;
然后求变量 condTermFlagN ;
if( {
1 mbAddrN 不可用 && mbAddrN 是使用帧间预测编码
2 mbAddrN 可用 && transBlockN 不可用 && mbAddrN不是I_PCM宏块
3 当前宏块是帧内编码 && constrained_intra_pred_flag == 1 && mbAddrN 可用 && 是帧间编码 && 使用片分割
}中的任一条件 == true
)
condTermFlagN = 0;
else if(
{
1 mbAddrN 不可用 && 当前宏块是帧内预测宏块
2 mbAddrN是I_PCM宏块
}中的任一条件 == true
)
condTermFlagN = 1;
else
condTermFlagN = transBlockN->(已经解码的)coded_block_flag;
然后求 ctxIdxInc(ctxBlockCat):
ctxIdxInc( ctxBlockCat ) = condTermFlagA + 2 * condTermFlagB
至此ctxIdxInc(ctxBlockCat) ctxIdxBlockCatOffset ctxIdxOffset 全部得到,直接求和,得到ctxIdx
ctxIdxBlockCatOffset(ctxBlock)表示对应索引的ctxIdxBlockCatOffset。
ctxIdx = ctxIdxInc(ctxBlockCat) + ctxIdxBlockCatOffset(ctxBlockCat) + ctxIdxOffset;
当然上面只是一个例子,其他情况都在标准中有说明。
对于significant_coeff_flag last_significant_coeff_flag 和 coeff_abs_level_minus1这三个都是数组。
输入ctxIdxOffset和binIdx。输出ctxIdxInc
对于 significant_coeff_flag 和 last_significant_coeff_flag
首先:levelListIdx 赋值为上面数组的相应的索引,因为这两个元素的上下文建模用到了句法元素所在的位置。
也就是说,在解这三个数组的时候,分别有for循环,循环的时候的索引i赋值给这里的levelListIdx。
这里的数组是指的从解码中得到的一维数组(仅仅是数据),为了方便理解,我们先简单认为 i 就是数组中的位置。
对于 significant_coeff_flag 和 last_significant_coeff_flag
ctxBlockCat != 3 5 9 13 的情况:ctxIdxInc = levelListIdx
对于 significant_coeff_flag 和 last_significant_coeff_flag
ctxBlockCat = = 3 的情况:ctxIdxInc = Min( levelListIdx / NumC8x8, 2)
对于significant_coeff_flag 和 last_significant_coeff_flag 在 8x8 亮度块 Cb块 Cr块
ctxBlockCat = = 5, 9, 13等等的时候查表确定ctxIdxInc
对于 coeff_abs_level_minus1, ctxIdxInc 由下面的式子指定
numDecodAbsLevelEq1表示系数绝对值等于1的累计的数量
numDecodAbsLevelGt12示系数绝对值大于1的累计的数量
这个也是对于当前块来说的,标准里面具体强调了这两个值属于同一个解码过程所在的块。
也就是说:这俩是当前块里面的数据的实时统计量。
接下来
ctxIdxInc 用下面的代码得到。
Min 表示最小值,可以直接用三目运算符,也可以做成函数。
if(binIdx == 0)
ctxIdxInc = ((numDecodAbsLevelGt1 != 0) ? 0: Min(4, 1 + numDecodAbsLevelEq1)
else
ctxIdxInc = 5 + Min(4 − ((ctxBlockCat == 3)?1 : 0), numDecodAbsLevelGt1)
输出 ctxIdxInc 之后,再根据 cat 后缀的偏移,总的偏移就得到 ctxIdx ,然后直接解码就得到值了。
这里的 ctxIdxInc 并不带(ctxBlockCat),因为这里 ctxIdxInc 算出来就是唯一的数值而不是查表出来的,所以不用 ctxBlockCat 这个变量来索引。
这个句法元素的二值化是 前缀的 ctxIdxOffset 由 UEG0 给出。参数为: signedValFlag=0, uCoff=14,其中前缀的 ctxIdxOffset = 227 ,后缀的 ctxIdxOffset 使用是旁路解码。
以 16x16 的 DC 系数为例子。
查表 9-42
ctxBlockCat = 0;句法的索引
我们先解算前缀也就是 截断一元二值化部分:
首先查二值化表 9-34 得到一级偏移:ctxIdxOffset
ctxIdxOffset = 277;
查二级偏移表 9-40 得到二级偏移:ctxIdxBlockCatOffset
ctxIdxBlockCatOffset = 0;
求解得到二级上下文索引:ctxIdxInc
对于每一个binIdx,由上面的代码得到 ctxIdxInc
由此求出上下文索引 ctxIdx
ctxIdx = ctxIdxInc + ctxIdxBlockCatOffset + ctxIdxOffset
求二进制位
然后根据 ctxIdx 用 CABAC 求出当前位上的二进制数。
结果确认(二值化对比):
然后确认结果是不是二值化,
对于由 binIdx 从 0 到 cMax 区间求出每一个二进制位。
每次求一个二进制位并确认是这个结果之后,就输出这个二进制串。否则 binIdx 自增1,继续重复上面的过程。
然后求后缀也就是 k 阶指数 Golomb 部分(求解的方法在上面给出了。)
由于后缀由旁路解码给出,所以求后缀的部分不需要参数,直接使用旁路解码得到二进制位。
联合二值化的值就是前缀的值加上后缀的值,如果我们求出来的串合法,那么就可以按照联合二值化中提到的解读方法解读我们求出来的串并且输出这个值了。
对于 coeff_sign_flag,
coeff_sign_flag也是和上面相同大小的数组,但是这个句法元素在表里面属于旁路解码模式。前面我们看到,旁路解码不需要ctxIdx这个上下文索引变量,只需要一个标志位 bypassFlag == 1 让解码进入到旁路解码就行,传参的时候ctxIdx随便传一个数,把第二个参数(也就是bypassFlag)传1就行了。(因为旁路解码只需要旁路标志位为1)
直接就可以得到他的值,因为他的二值化是FL cMax=1,计算就得到他只有二进制一位,所以可以直接解就出来了。