前注:
一、代码最好是去看标准,我在写的时候也发现本身的代码有不少的错误。可能都没改过来。
二、在写的时候用到了解码、解析、解算,这些词语的意思都是指同一个意思即求解的意思。
三、文中全部带“标准”之意的都是指:ISO/IEC 14496-10。
四、不保证本身的全部叙述都是正确的,可是会不断改进的。
五、如今只是我在学习的过程当中写的笔记和心得,以后会不断总结起来。
六、未完,由于初学,有错误请指出、感激涕零html
小注:
由于接下来要先去看别的,因此这里须要先搁置一段时间。算法
还没补充的: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解析器去作解析器。函数
这个其实很简单,首先须要作一个缓冲区,而后用相似FILE* 指针的那一套方法作一个出来(我不知道是否是有相似这样的标准库,就直接手动实现了)。
读取的比特拆成两部分:字节部分、比特部分(字节×8+比特 = 总的比特)
一、一位的读取
核心用这个方法: result &= buf_data[charIndex] >> (8 - bitsIndex - 1);
就是比较简单的位运算。
二、多位的读取
用的是拆分的方法:把位数拆成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比特就必定会存在一个字节读取了。
头部比特位一位一位读取,到了字节对齐位就找字节数用字节读取,结束以后尾部继续读完剩下的比特位。
三、缓存区的处理
缓存区读完、为空的时候须要刷新缓存,索引归零。在nal层中有一个read_next(n)的函数(用来去掉0x 00 00 03中的03),作到要读取可是索引不变,考虑到可能在next()的时候发生缓存区刷新从而找不到原来的缓存区,因此这时加入第二个缓存区,读完以后把索引赋以前的值,缓冲区指针还给1号。这时刷新的时候从2号缓存区读入数据到1号,2号free()。若是只是从h264文件而不是媒体文件中读的话可能不须要这个过程。oop
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)是从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头文件。ui
这个是用来专门读取 code_block_pattern 这个句法元素的。(不用ae(v)读取的时候就是用这个方法读取)
首先也是按照ue(v)的读取拿到值。而后去查表好比下面是前面的部分(这是不完整的表)。
codeNum | coded_block_pattern | |
---|---|---|
Intra_4x4,Intra_8x8 | Inter | |
0 | 47 | 0 |
1 | 31 | 1 |
这也是为何他叫作“映射”指数Golomb编码。就是用指数Golomb编码方法解码而后映射查表获得值=,=。
查表这里也是作成静态表,而后按索引查表
首先判断句法元素的值的范围[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)就是上下文自适应的可变长编码CAVLC的描述子。
(这里我还没学)
CAVLC的原理能够如今参考oskycar的这篇:H.264的CAVLC(编码.解码)过程详解,写的很详细。跟着这篇文章的例子走一遍就能理清楚大概的实现过程。
须要注意的是,在ISO标准的句法表中,CAVLC的解码就是上文说的这个过程。由于CAVLC解出来就是一串数据而不是一个数据。
残差块中输入的参数是 (0, 15) ,因此他解出来就应该是一个16个元素的一维数组。也就是说 ce(V) 说的并非读取一个或者几个bit,而是直接解出一串数据。
而其中也没有说必须是读多少位出来。也是要查表或者计算,最后都有惟一的0-1串对应而后就能解算出来具体的值了。
也就是说ce(v)不是具体怎么读取、读多少位的描述符,他说的是一种解析的方法。
之后回过头来写具体实现吧。这里我也没细看。(我这个时候看的264文件就所有都是算术二进制熵解码)
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 <typename T> 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 ) k++ } else { put( 0 ) while( k− − ) put( ( sufS >> 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 ) k++ } else { put( 0 ) while( k− − ) put( ( sufS >> k ) & 1 ) stopLoop = 1 } } while( !stopLoop )
仍是用一个例子来讲明:
我自定义的:数字前面带Bx表示一个二进制数
好比如今须要 0阶指数Golomb 编码 Bx1100 ,不编码符号位 signedValFlag = 0。
(这里略掉了前缀的截断编码)
条件 | k | 输出位 | 当前串 | 当前值 |
---|---|---|---|---|
条件 sufS >= ( 1 << k ) | k | 输出位 | 当前串 | 当前值sufS −= (1<<k) |
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<<k) |
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进制的1九、十进制的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,也是查表来的作成了枚举。)那么须要引擎初始化。
还没写。后面补充。
这些句法元素有名字有一个特征就是带“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,计算就获得他只有二进制一位,因此能够直接解就出来了。