JM代码阅读之一SODB RBSP EBSP NALU

来源:互联网 发布:合成视频的软件 编辑:程序博客网 时间:2024/05/16 15:47

JM版本16.0,配置文件encoder_baseline.cfg,H.264标准文档(03/2010)版。

通过对码流的第一个NALU(SPS)的形成来分析。

首先给出编码后的最终码流(SPS + PPS):
00 00 00 01 67 42 00 28 F3 05 89 C8 00 00 00 01 68 C9 4A 38 80

SPS(红色部分)转换成二进制:00000000 00000000 00000000 00000001 01100111 01000010 00000000 00101000 11110011 00000101 10001001 11001000

然后介绍一个码流分析工具:Elecard StreamEye Tools

用这个工具分析用JM编码得到的码流,它会给出各个NALU的信息

其中SPS的内容如下:
profile_idc = 66 (PROFILE_IDC_Baseline) (01000010)
constraint_set0_flag = 0 
constraint_set1_flag = 0 
constraint_set2_flag = 0 
constraint_set3_flag = 0 
reserved_zero_4bits = 0
level_idc = 40(00101000)
seq_parameter_set_id = 0 ue_v
log2_max_frame_num_minus4 = 0 ue_v
pic_order_cnt_type = 0 ue_v
log2_max_pic_order_cnt_lsb_minus4 = 0 ue_v
num_ref_frames = 5 ue_v
gaps_in_frame_num_value_allowed_flag = 0
pic_width_in_mbs_minus1 = 10 (176) ue_v
pic_height_in_map_units_minus1 = 8 (144) ue_v
frame_mbs_only_flag = 1 
direct_8x8_inference_flag = 1 
frame_cropping_flag = 0 
vui_parameters_present_flag = 0

其中每一个参数对应码流中的位置用颜色对应关系给出,其中后面标有ue_v的是采用Exp-Golomb-coded编码的,暂时还没有研究。其他没有颜色的bit为一些填充或头部,后面详细分析。

—————————————————————————————————

好吧,下面分析这个NALU是怎么形成的:00 00 00 01 67 42 00 28 F3 05 89 C8

首先形成的是String Of Data Bits (SODB),请参考标准文档7.2.3.1.1部分

01000010 00000000 00101000 11110011 00000101 10001001 1100

这个就是形成的SODB,转换成16进制,可以发现它就是上面码流的42 00 28 F3 05 89 C这一段。

然后要形成的是Raw Byte Sequence Packet (RBSP),它其实就是在SODB后面加上

RBSP trailing bits的结果,见标准文档7.2.3.1,目的是为了形成整数字节。

填充规则见标准文档的7.4.1部分,大概为先填充一个1(rbsp_stop_one_bit),然后都填充0(rbsp_alignment_zero_bit),所以对于上面的SODB,填充一个1,3个0之后,便得到了
01000010 00000000 00101000 11110011 00000101 10001001 11001000
即42 00 28 F3 05 89 C8

现在,码流的后面7个字节都得到了,现在要得到的是Extended Byte Sequence Packet (EBSP), 它在RBSP基础上填加了仿校验字节,防止与起始码冲突,如果出现连续的三个字节00000000 00000000 000000xx,着插入一个0×03,变成00000000 00000000 00000003 000000xx。在上面的RBSP中没有出现这样的序列,所以木有改变什么。

最后在EBSP前面加上一个4字节的起始码00 00 00 01和一个NAL unit type字节就形成最后的Network Abstraction Layer Unit (NALU)

NAL unit type字节包含三个字段(具体含义见7.4.1):67 <==> 0 11 00111
forbidden_zero_bit,总为0
nal_ref_idc,2个bit,表示该NAL的重要性,是00的话,说明它可以被安全的丢弃,这里SPS的这个指为3(11),即最高值。参考RFC 3984。(现在知道这个字节叫作NAL unit type octet了)
nal_unit_type,5个bit,在7.4.1中的table 7-1中有说明。这里值为7(00111),表示NAL中是SPS,验证成功:-D

——————————————————————————————————————–

在JM代码中,输出SPS和PPS的实现在函数int start_sequence(ImageParameters *p_Img, InputParameters *p_Inp)中,有兴趣的小朋友自己研究研究吧。

最后把PPS的信息也贴出来:

pic_parameter_set_id = 0
seq_parameter_set_id = 0
entropy_coding_mode_flag = 0
pic_order_present_flag = 0
num_slice_groups_minus1 = 0
num_ref_idx_L0_active_minus1 = 4
num_ref_idx_L1_active_minus1 = 4
weighted_pred_flag = 0
weighted_bipred_idc = 0
pic_init_qp_minus26 = 0
pic_init_qs_minus26 = 0
chroma_qp_index_offset = 0
deblocking_filter_control_present_flag = 0
constrained_intra_pred_flag = 0
redundant_pic_cnt_present_flag = 0

参考:http://lsharemy.com/wordpress/index.php/csit/jm-code-notes-1/

JM代码阅读之二指数哥伦布熵编码

在标准文档的语法部分,每个语法元素都有一个描述符来规定该语法元素的编码类型。

它们的含义如下(见标准文档7.2小节):

ae(v):上下文自适应算术熵编码语法元素。该描述符的解析过程在9.3节中规定。
b(8):任意形式的8比特字节。该描述符的解析过程通过函数read_bits( 8 )的返回值来规定。
ce(v):左位在先的上下文自适应可变长度熵编码语法元素。该描述符的解析过程在9.2节中规定。
f(n):n位固定模式比特串(由左至右),左位在先, 该描述符的解析过程通过函数read_bits( n )的返
回值来规定。
i(n):使用n比特的有符号整数。在语法表中,如果n是'v',其比特数由其它语法元素值确定。解析过程由函数read_bits(n)的返回值规定,该返回值用最高有效位在前的2的补码表示。
me(v):映射的指数哥伦布码编码的语法元素,左位在先。解析过程在9.1中定义。
se(v):有符号整数指数哥伦布码编码的语法元素位在先。解析过程在9.1中定义。
te(v):舍位指数哥伦布码编码语法元素,左位在先。解析过程在9.1中定义。
u(n):n位无符号整数。在语法表中,如果n是'v',其比特数由其它语法元素值确定。解析过程由函
数read_bits(n)的返回值规定,该返回值用最高有效位在前的二进制表示。
ue(v):无符号整数指数哥伦布码编码的语法元素,左位在先。解析过程在9.1中定义。

其中,ue(v) me(v) se(v)使用的是指数哥伦布编码(Exp-Golomb-coded),te(v)使用的是舍位指数哥伦布编码(trunated Exp-Golomb-coded)。

ue(v)为无符号整数的指数哥伦布码编码,编码规则如下表所示:

下表是该编码的一些举例:

再拿上篇文章中SPS码流来实例分析:

00000000 00000000 00000000 00000001 01100111 01000010 00000000 00101000 11110011 00000101 10001001 11001000

profile_idc = 66 (PROFILE_IDC_Baseline) (01000010)
constraint_set0_flag = 0 
constraint_set1_flag = 0 
constraint_set2_flag = 0 
constraint_set3_flag = 0 
reserved_zero_4bits = 0
level_idc = 40(00101000)
seq_parameter_set_id = 0 ue_v
log2_max_frame_num_minus4 = 0 ue_v
pic_order_cnt_type = 0 ue_v
log2_max_pic_order_cnt_lsb_minus4 = 0 ue_v
num_ref_frames = 5 ue_v
gaps_in_frame_num_value_allowed_flag = 0
pic_width_in_mbs_minus1 = 10 (176) ue_v
pic_height_in_map_units_minus1 = 8 (144) ue_v
frame_mbs_only_flag = 1 
direct_8x8_inference_flag = 1 
frame_cropping_flag = 0 
vui_parameters_present_flag = 0

可以看到,值为0的uv_e元素被编码为1值为5的uv_e元素被编码为00110值为10的ue_v元素被编码为0001011值为8的ue_v元素被编码为0001001。验证完毕:-D

se(v)为有符号整数的指数哥伦布码编码,它通过下表简单的映射到无符号整数:

te(v)的规则如下:如果语法元素值为0,则编码为1,如果语法元素值为1,则编码为0,如果为其他大于1的值,则按ue(v)进行编码。所以解码的时候需要先确定语法元素的长度,至于如何确定现在还不晓得,求赐教。

最后就是me(v)啦,没理解错的话,它主要用来coded_block_pattern的编码,但现在coded_block_pattern是什么还不晓得,还得继续学习。

——————————————————————————————————-

JM代码中ue(v)的编码由int ue_v (char *tracestring, int value, Bitstream *bitstream)过程来实现。

参考:http://lsharemy.com/wordpress/index.php/csit/jm-code-notes-2/

JM代码阅读之三帧内预测

帧内预测依据先前已经编码并重建好的块(左上、上、右上、左)形成一个预测块P,当前块减去这个预测块,将差值进行编码。对于4×4亮度块,有九种可选的预测模式;对于16×16亮度块,有四种可选模式;对于色度块,有四种可选预测模式。最终选择使得P和当前块的差值最小的预测模式作为当前块的预测模式。

还有一种帧内编码模式,即I_PCM,使得编码器能够直接传输图像像素值(没有预测和变换)。在某些情况下(例如不规则的图像内容或很低的量化参数),这种方式可能比通常的帧间预测、变换、量化和熵编码过程更加有效。

JM代码中,默认会调用encode_one_macroblock_high来对一个宏块编码,在该函数中:

首先,调用SetChromaPredMode来进行色度块的模式选择,先做四种模式的预测,然后计算各模式的cost值(可先简单理解为预测块与当前块的差值),选择cost值最小的模式,这里给出一个实例,使用JM16.0对foreman_part_qcif.yuv编码(baseline),第一帧(I帧)的第1行第1列(从0开始计数)的宏块(见上图的选中宏块,图中可以看出,该帧有4个宏块使用的16×16编码模式,其他都采用的4×4编码模式),它的色度块的四种模式的cost值分别为:DC_PRED_8 – 392、HOR_PRED_8 – 404、VERT_PRED_8 – 416、PLANE_8 – 440。DC模式的cost值最小,所以选择DC模式。

其次,会进行亮度块的模式选择,由于是Intra帧,要分别进行16×16、4×4、I_PCM三种编码模式的计算,选出一个最优编码模式。

先进行16×16,实现函数为Intra16x16_Mode_Decision_SAD,过程与色度块类似,也是对4种模式进行预测,然后选择cost值最小的模式,最终的计算结果:VERT_PRED_16 – 10135、HOR_PRED_16 – 10405、DC_PRED_16 – 9523、PLANE_16 – 10292。 还的DC模式的cost模式最小,所以选DC模式作为16×16块的最优模式。然后,使用该模式对该宏块进行编码和重建,算出重建块和原始图像的差值平方 和Sum of Squared Error(SSE),称为distortion,再算出编码该宏块需要的比特数,称为rate,并利用公式rdcost = (double)distortion + lambda * dmax(0.5,(double)rate)来计算一个rdcost,该值用来衡量该编码模式的优劣。这个实例中distortion为4984,rate为510,lambda 为34.269852557140545,rdcost 为22461.624804141677

再进行4×4,实现函数为Mode_Decision_for_4x4IntraBlocks_JM_High,先对每个4×4块进行9种模式的预测,然后选择出一个cost值最小的模式,作为该4×4块的最优模式,完成所有16个4×4块的模式选择后,重建、编码、计算rdcost,这个实例中,distortion为4869,rate为351,lambda 为34.269852557140545,rdcost 为16897.718247556331。由于该rdcost 比16×16的小,所以4×4取代16×16成为最佳编码模式。

最后进行I_PCM,仅仅是图像数据的复制,所以计算出来的distortion为0,而rate为3081,所以计算出rdcost 为105585.41572855003

最终正如图像中显示的,4×4凭借其最低的rdcost 成为了该宏块的编码模式,最后就剩下把宏块数据写到NAL中去的过程了。实现函数为write_macroblock。关于码流的语法规则,留到下次再更新。

哦,还忘了介绍一个工具Elecard StreamEye,上面的图就是用这个工具分析编码后的码流得到的。

以下是通过该工具分析得到的该宏块的信息:

position       : 1×1 (16×16) 位置,第1行第1列(从0开始计数,所以左上角宏块为0×0)
mb_addr        : 12 每行11个宏块,所以该宏块的地址为12(也是从0开始)
size (in bits) : 351 该宏块所占的比特数
mb_type        : 0 宏块类型,与slice的类型有关,对照表见标准文档7.4.5小节
pmode          : 0 编码模式
mb_type        : Intra(I_4x4) 宏块类型的名称
slice_number   : 0 slice的编号
transform_8x8  : 0 是否8×8变换
field\frame    : frame 帧编码
cbp bits       : 0 1101 1 00 0 00 这个有待研究
:   0111    00    00
:   1111
:   1011
quant_param    : 28 QP值
pmode          : Intra_4x4 编码模式的名称
ipred Intra_4x4: 每个4×4块的预测模式
HorzUp       DiagDwnLeft  VertLeft     DiagDwnLeft
DC           DC           HorzUp       DiagDwnLeft
HorzUp       DC           HorzUp       HorzUp
HorzUp       HorzUp       HorzUp       HorzUp
ipred chroma   : DC 色度块的预测模式

 ——————————————————————————————————————-

下面继续write_macroblock的分析,里面最重要的一个函数为write_MB_layer,这其实是一个函数指针,因为该宏块为Intra,所以最终调用的是writeMBLayerISlice。该函数的作用就是填充Macroblock layer语法结构了,关于该语法,请看标准文档的7.3.5小节。

先写入的是MBType,表示该宏块的类型,这里为I4MB

然后写入每个块的预测模式,先是16个4×4亮度块的预测模式,再是色度块的预测模式,因为两个色度块的预测模式总是一样的,所以只写一次。

再写入CBPDQUANTCBPcoded_block_pattern,具体含义见7.4.5小节,DQUANTmb_qp_delta,是当前QP和之前QP的一个差值,如果一样,则差值为0,好像没这么简单,想知道的还是看文档吧,嘿嘿,也在7.4.5小节。

最后写入的就是真正的残差数据了,残差数据是采用CAVLC编码的,该编码方法还有待研究。

   

参考:http://lsharemy.com/wordpress/index.php/csit/jm-code-notes-3-intra-prediction/

JM代码阅读之四残差变换系数块的CAVLC编码

开始研究CAVLC编码,参考:JM16.0代码、标准文档的9.2小节

还是使用(JM代码阅读之三帧内预测 | JM Code Notes 3 – Intra Prediction)中的那个宏块来进行研究。

先讲CAVLC编码变换系数块的过程。再通过分析之前那个宏块来加深理解。

CAVLC编码变换系数块(4×4的残差块)的过程如下:

1.编码coeff_tokenTotalcoeff & TrailingOnes),对应的JM函数为writeSyntaxElement_NumCoeffTrailingOnes

Totalcoeff表示非零系数的总数,TrailingOnes表示非零系数中±1的个数。4×4块中一共有16个系数,所以Totalcoeff 的范围是0-16,而TrailingOnes的范围是0-3,如果±1的个数大于3,则只有最后的3个作为特殊情况处理,其他的则作为正常系数编码。

coeff_token则是使用Totalcoeff 和TrailingOnes当索引通过查表得到的。查找表的选择还依赖一个系数nC,它取决于左边和上边已编码块(分别称为nA和nB)的Totalcoeff。nC计算如下:如果上边和左边块nB和nA都可以得到,则nC = round((nA+nB)/2);如果之后上边块可以得到,nC = nB;如果只有左边块可以得到,则nC = nA;如果两个块都得不到,则nC = 0。

有了Totalcoeff 、TrailingOnes以及nC,就可以通过查表得到coeff_token了。见标准文档的表9-5。

2.编码每个TrailingOnes的符号,对应的JM函数为writeSyntaxElement_VLC

对于步骤1中的每个TrailingOne(±1),分别编码它们的符号(0代表正,1代表负),从最高频的TrailingOne开始。

3.编码余下非零系数的级别(符号和幅度),也按倒序进行,即从高频开始,对应的JM函数为writeSyntaxElement_Level_VLC1writeSyntaxElement_Level_VLCN,前者用来编码第一个非零系数,后者则是编码除第一个以外的非零系数。

每个级别的码字包含一个前缀(prefix)和后缀(suffix)。后缀的长度(suffixLength)可以是0到6比特,而suffixLength随着每个连续编码级别的幅度自适应变化。较小的suffixLength使用于较低幅度的级别,而较大的suffixLength适用于较大幅度的级别。

具体的编码算法比较复杂,可以参考标准文档的9.2.2小节表述的解析过程,从而推出其编码过程。

4.最高频非零系数前零的总数,对应的JM函数为writeSyntaxElement_TotalZeros

5.编码每个零游程,即每个非零系数前零的个数(run_before),也是按倒序进行。对应的JM函数为writeSyntaxElement_Run

该步骤有2个例外,1.如果余下的已经没有零需要被编码,则没有必要编码任何其他的run_before;2.没有必要为最后一个非零系数编码run_before值。

——————————————————————————————————————–

下面是实例了:

该宏块(地址为12)的第一个4×4残差变换系数块经过Zigzag顺序扫描之后:

1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

根据上面的步骤

1.可以得到Totalcoeff 为1,TrailingOnes为1,nA和nB要从邻居得到分别为0和1,所以计算得nC为1,查表得coeff_token01

2.TrailingOnes只有一个,而且是正的,所以该步骤的编码为0

3.没有其他非零系数,所以该步骤不产生编码

4.最高频非零系数前零的总数为0,VCL编码结果为1

5.因为只有一个非零系数,而最后一个非零系数不需要编码run_before值,所以该步骤也不产生编码。

所以该4×4块的编码为0101

——————————————————————————————————————–

第二个4×4残差变换系数块:

0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

同样的步骤,Totalcoeff 为1,TrailingOnes为1,nC为1,步骤1编码为01,步骤2编码为0,步骤3跳过,最高频非零系数前零的总数为1,所以步骤4编码为011,步骤5跳过,所以最终的编码为011011

——————————————————————————————————————–

第三个4×4残差变换系数块:

0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

Totalcoeff 为0,TrailingOnes为0,nC为1,步骤1编码为1,之后的步骤均不产生编码,所以最终编码为1

——————————————————————————————————————–

第四个4×4残差变换系数块:

-5, 2, 5, -2, -2, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0

Totalcoeff 为8,TrailingOnes为3,nC为1,步骤1编码为0000 0001 00,步骤2编码为000(三个都为正),步骤3中,第一个-2编码为0001,第二个-2编码为01 1,5编码为00001 0,2编码为1 10,-5编码为001 01。最高频非零系数前零的总数为4,步骤4编码为11。步骤5中,最后一个1前面有0个0,编码为11,倒数第二个1前面也是0个0,编码为11,倒数第三个1前面有4个零,编码为000,由于前面不再含零,编码结束。最终编码为0000 0001 0000 0000 1011 0000 1011 0001 0111 1111 000。一共43bits

——————————————————————————————————————–

之后的12个4×4块也是类似的编码,色度块的编码也是类似,最终使用的比特数为295(亮度)+7(色度)=302。这个数跟Notes 4中用Elecard StreamEye分析工具看到的宏块所占的比特数351还有一点差异,不过正如Notes 3中分析的,在写入残差数据之前,还写入了一些其他数据,包括MBTypeCBPDQUANT、以及每个块的预测模式等信息。

那就再做个加法验证一下吧,MBType1bit,预测模式44bits(亮度43bits,色度1bit),CBP3bits,DQUANT1bit,302+1+44+3+1=351bits,完全符合,嘿嘿,验证完毕。

   

参考:http://lsharemy.com/wordpress/index.php/csit/jm-code-notes-4-cavlc/

JM代码阅读之五帧间预测

老规矩,先原理,再实例

帧间预测是采用基于块的运动补偿从一个或多个先前编码的图像帧中产生一个预测模型的。H.264与早起标准的主要不同之处在于支持不同的块尺寸(从16×164×4)以及支持精细子像素精度的运动矢量(亮度成分是1/4像素精度)

每个宏块(16×16)的亮度分量可以按四种方式划分,即按一个16×16块,或两个16×8块,或两个8×16块,或者4个8×8块的划分进行运动补偿。如果选择8×8模式,宏块中的4个8×8子宏块可以用另一种方式进一步划分,或者作为一个8×8块,或作为两个8×4块,或作为两个4×8块,或者作为四个4×4块。

每个分块或者子宏块都产生一个单独的运动矢量。每个运动矢量均需要编码和传输,同时分块模式信息需要进行编码并放在压缩比特流中。

每个色度块按照与亮度分量同样的分块方式进行划分。

编码每个分块的运动矢量需要大量比特位。由于相邻块的运动矢量高度相关,所以每个块的运动矢量都是从邻近的先前编码块中进行预测得到的。当前运动矢量与预测运动矢量MVp的差值MVD被编码和传输。

MVp的预测规则如下:

假设E是当前宏块、子宏块或子宏块分块,A是E左边的分块或子分块,B是E上边的分块或子分块,C是E右上的分块或子分块。如果E左边的分块数大于1,则最上边的分块被选为A。如果E上边的分块数大于1,则最左边的分块被选为B。

1.除了16×8和8×16两种分块尺寸的其余传输块,MVp是分块A、B、C的运动矢量的中值(不是平均值)
2.对于16×8分块,上边16×8分块的MVp是从B预测得到的,下边16×8分块的MVp是从A预测得到的。
3.对于8×16分块,左边8×16分块的MVp是从A预测得到的,右边8×16分块的MVp是从C预测得到的。
4.对于skip宏块,产生一个16×16块的MVp,和第1种情况一样。MVp的形成规则相应修改。

如果得不到一个或多个先前传输块的话(如,它在当前条带之外),则MVp的形成原则相应修改。

——————————————————————————————————————–

Yeah! 又可以看实例了:

这里对foreman_part_qcif.yuv的第二帧中地址为40的宏块(白色框框住,图贴在文章开头)进行分析,关键代码还是在encode_one_macroblock_high中,由于该帧是P帧,所以会进行帧间预测。其中最重要的函数为BlockMotionSearch,该函数为所有大小的分块完成运动搜索的过程,得到最优的MV。

1.进行skip模式,实现函数FindSkipModeMotionVector,该函数只是从周围块的MV来预测当前宏块的MVp,获得MVp的函数GetMotionVectorPredictorNormal。由于skip宏块是没有MVD的,它把MVp作为运动矢量并得到运动补偿宏块。实例中获得的MVp为(-17, 3)

2.16×16模式, 也需要先获得MVp(-17, 3),于是将(-16, 4)定为搜索中心(最近的整数像素),在一定的搜索范围(32)之内进行整像素搜索,需要搜索的位置有(32*2+1)*(32*2+1)=4225个。 对于每个位置,都要计算一个motion cost(block_sad+mv_cost),最终找到使cost最小(2311)的MV(-16, 8)。然后在该点周围9个点(包括该点)再进行半像素搜索,找到一个cost最小(3639)的MV(-16, 6),这里采用的误差度量不再是SAD(Sum of Absolute Difference),而是SATD(Sum of Absolute Transformed Difference),所以与之前的2311没可比性,这个配置文件里面可以配置,只不过默认的配置是整像素采用SAD,半像素和四分之一像素采用SATD。进行完半像素搜索后,再在cost最小的半象素点周围的9个点进行四分之一像素搜索,最后找到一个cost值最小(3523)的MV(-17, 7)

3.16×8模式, 要对上下两个16×8块进行运动搜索,先是上面的 块,MVp为(-17, 5),于是从(-16, 4)开始进行整像素搜索,得到SAD最小的是(-24, 4),半像素搜索(-26, 2),四分之一像素搜索(-27,2),最小cost为1262。然后是下面的块,类似得到最小cost(2020)的MV(-15, 5)。两个cost的和为1262+2020=3282。

4.8×16模式,和16×8的区别就是现在的分块为左右两个,分别得到左块和右块的MV为(-32, 3)(-15, 5),cost为761+1825=2586。

5.8×8模式,如上面所说,如果选择8×8模式,四个8×8子宏块可以用另外四种方式进行划分,所以需要对4个子宏块进行运动估计和模式选择。需要用RDO技术来选择。

先是第一个8×8子宏块。SMB8x8模式,cost最小(483)的MV(-30, 2)rdcost = 1827.9051343856379;8×4 模式,上块和下块最优MV分别为(-32, 2)和(-32, 3),cost为323+146=469,rdcost = 1839.9845446142001;4×8模式rdcost = 2071.8257241570759;4×4模式rdcost = 1906.9051343856379。选择rdcost最小的模式,也就是8×8模式,见图中高亮宏块的左上角8×8块。

第二个8×8子宏块。8X8 rdcost = 2192.8257241570759;8×4 rdcost = 2337.8735125141839; 4×8 rdcost = 1486.6669036999515,左块和右块MV分别为(-21, 5)(-15, 3),SATD cost = 413;4×4 rdcost = 1927.9051343856379。选择rdcost最小的模式,也就是4×8模式模式,见图中高亮宏块的右上角8×8块。

第三块和第四块采用类似的方法,分别选择了SMB8x8模式和SMB4x4模式。见图中高亮宏块的左下和右上8×8块。

——————————————————————————————————————–

所有帧间预测模式都做完了,下面就是要通过各个模式的rdcost来选择最佳模式了。

skip模式,48032.539705114279

P16x16模式,13120.288152342357

P16x8模式,12449.192575628142

P8x16模式,10033.668325899660

P8x8模式,8257.1286207853791

即使是P帧,也还是要做帧内预测的,下面是帧内模式的rdcost

I16x16模式,16021.447683899336

I4x4模式,13026.446972799482

I_PCM模式,105585.41572855003

当然是选rdcost最小的咯,也就是P8x8,而四个8×8子宏块的分块情况也在之前选择好了。

———————————————————————————————————————

之后还是把用Elecard StreamEye工具得到的该宏块的信息贴出来研究下:

position       : 7×3 (112×48)
mb_addr        : 40
size (in bits) : 146
mb_type        : 4 宏块类型,见标准文档表7-13,4表示的宏块类型名称为P_8x8ref0
pmode          : 3 预测模式,8×8
mb_type        : Inter(P_8x8ref0)
slice_number   : 0
transform_8x8  : 0
field\frame    : frame
cbp bits       : 0 1100 0 00 0 00
:   0000   00   00
:   0011
:   0001
quant_param    : 28 QP
pmode          : Part_8x8 预测模式名称
sub_pmode      : SubPart_8x8 SubPart_4x8 子宏块的分块方式
: SubPart_8x8 SubPart_4x4
sub_pdir       : Pred_L0 Pred_L0 
: Pred_L0 Pred_L0
mvL0           :  MV,与之前分析的一致
-30,   2, 0| -30,   2, 0| -21,   5, 0| -15,   3, 0
-30,   2, 0| -30,   2, 0| -21,   5, 0| -15,   3, 0
-33,   3, 0| -33,   3, 0| -26,   2, 0| -10,   3, 0
-33,   3, 0| -33,   3, 0| -26,   2, 0| -20,  43, 0

———————————————————————————————————————

下面分析码流,相关函数writeMBLayerPSlice

先写入的是一个mb_skip_run语法元素,表示之前有多少个skip宏块,这里为0,因为之前的宏块不是skip空块

再写入MBType,表示宏块类型

然后是4个8×8子宏块的模式

之后写入的是运动矢量信息

再然后写入cbp和DQuant

最后写如的是亮度和色度的残差

该实例中,mb_skip_run占1bit,MBType占5bits,子宏块的模式占10bits,运动矢量信息占82bits,cbp占9bits,DQuant占1bit,亮度残差占38bits,色度残差为空,一共1+5+10+82+9+1+38=146bits,与上面一致,忽忽

其中运动矢量信息的编码函数为write_pslice_motion_info_to_NAL,编码每个块的最佳MV和MVp的差值MVD。

参考:http://lsharemy.com/wordpress/index.php/csit/jm-code-notes-5-inter-prediction/

JM代码阅读之六条带头语法

在"JM代码阅读之三帧内预测 | JM Code Notes 3 – Intra Prediction"和"JM代码阅读之五帧间预测 | JM Code Notes 5 – Inter Prediction"中,已经分析了macroblock_layer的语法结构了,其实也就是分析完slice_data了,下面继续往上层分析,也就是slice_header语法。

请打开文档7.3.3小节:-D

这一次直接看实例,嘿嘿,不要太开心

JM中,产生slice_header的函数为start_slice

先看第一帧的slice_header

first_mb_in_slice 0
slice_type I slice
pic_parameter_set_id 0 指定使用的PPS
frame_num 0
idr_pic_id 0 标识一个IDR图像。一个IDR图像的所有条带中的idr_pic_id 值应保持不变。当按解码顺序的两个连续访问单元都是IDR 访问单元时,第一个IDR 访问单元的条带的idr_pic_id 值应与第二个IDR 访问单元的idr_pic_id 值不同。idr_pic_id 的值应在0到65535的范围内(包括0和65535)。
pic_order_cnt_lsb 0 ?
dec_ref_pic_marking() 见7.3.3.3
no_output_of_prior_pics_flag 0 表示在解码图像缓冲器中先前解码的图像在一个IDR图像解码之后是如何处理的
long_term_reference_flag 0 等于0表示变量 MaxLongTermFrameIdx 被设为等于"无长期帧序号"而且该IDR图像被标记为"用于短期参考"。long_term_reference_flag 等于1表示变量MaxLongTermFrameIdx 被设为等于0,而且当前的IDR 图像被标记为"用于长期参考"并且LongTermFrameIdx 被分配为等于0。
slice_qp_delta 2(28-26) 表示用于条带中的所有宏块的QP的初始值,该值在宏块层将被mb_qp_delta 的值修改。

下面是第二帧的:

first_mb_in_slice 0
slice_type P Slice
pic_parameter_set_id 0
frame_num 1
pic_order_cnt_lsb 2 ?
num_ref_idx_active_override_flag 1 ?
num_ref_idx_l0_active_minus1 0
ref_pic_list_reordering( ) 发现这个在最新的文档(2010/03)中已经没有了,囧
ref_pic_list_reordering_flag_l0 0
dec_ref_pic_marking()
adaptive_ref_pic_buffering_flag 0
slice_qp_delta 2(28-26)

第三帧,最后一帧:

first_mb_in_slice 0
slice_type P Slice
pic_parameter_set_id 0
frame_num 2
pic_order_cnt_lsb 4
num_ref_idx_active_override_flag 1
num_ref_idx_l0_active_minus1 1
ref_pic_list_reordering()
ref_pic_list_reordering_flag_l0 0
dec_ref_pic_marking()
adaptive_ref_pic_buffering_flag 0
slice_qp_delta 2(28-26)

好吧,还很多个语法元素不晓得什么意思,暂时就先这样了,等知道的时候再更新。

参考:http://lsharemy.com/wordpress/index.php/csit/jm-code-notes-6-slice-header/

JM代码阅读之七去块效应滤波器

参考:标准文档8.7小节经典论文Adaptive deblocking filterJM代码

滤波器被应用到每个解码块以减少由块效应所引起的失真。在编码端,进行反变换之后,再应用去块效应滤波器(在重建和存储宏块用于将来预测之前);在解码端,在重建和显示之前,应用去块效应滤波器。滤波器平滑了块的边界,改善了解码帧的质量。滤波后的宏块被用于将来帧的运动补偿预测,这能提高压缩性能,因为滤波过的图像比一个有块效应的未滤波的图像更接近原始图像。滤波器的默认操作如下,编码端可以选择滤波器的强度或禁止使用滤波器。

滤波器被应用到每个宏块的4×4块的垂直水平边界(除了块边界),如上图所示,顺序如下:

滤波亮度分量的4个垂直边界(顺序从左到右)
滤波亮度分量的4个水平边界(顺序从上到下)
滤波色度分量的2个垂直边界
滤波色度分量的2个水平边界

每个滤波操作影响边界两边的三个像素。把相邻块p和q的垂直或水平边界两边的四个像素分别称为p3, p2, p1, p0, q0, q1, q2, q3。滤波强度依赖于当前的量化器、相邻块的编码方式以及边界上图像像素的梯度。

JM代码中,DeblockFrame函数完成一帧图像的滤波,而它又调用DeblockMb对每一个宏块滤波(按光栅顺序进行)。

边界强度Boundary-Strength (BS):

滤波输出的选择依赖于边界强度和边界上图像像素的梯度。边界强度BS依据下列原则进行选择
1.p和q有一个是Intra块,同时边界是宏块边界,则BS=4(最强的滤波强度)
2.p和q有一个是Intra块,同时边界不是宏块边界,则BS=3
3.p和q都不是intra块,同时其中一个块包含残差
4.p和q都不是intra块,p和q都不包含残差,同时,p和q使用不同的参考帧或两者的运动矢量差值大于一个亮度像素,则BS=1
5.否则BS=0
应用这些原则的结果是在可能有明显块效应失真的地方滤波强度很大。

JM代码中,GetStrengthNormal用来获取一个宏块的垂直或水平边界的16个4×4块边界的BS。

滤波器判决:

集合(p2, p1, p0, q0, q1, q2)的一组像素值在下列情况下才进行滤波:
1.BS > 0
2.|p0 -q0| < α && |p1 -p0| < β && |q1-q0| < β
α和β是标准中定义的阀值,它们随着两个块p和q的量化参数QP的增长而增长。滤波器选择的效果就是当在原始图像块边界有明显的变化(梯度)时,关掉滤波器。当QP很小的时候,除了边界上的很小梯度,其他都应该是图像本身的特征(而不是块效应),所以应该保留,因此阀值α和β很小。当QP很大的时候,块效应可能很明显,所以α和β很大,需要滤波更多的边界像素点。

滤波器的实现:

可以将滤波的模式按BS来分为两种,一种为普通的(BS为1到3),一种的是强滤波(BS = 4)

1.BS ∈ {1, 2, 3}
输入是p1, p0, q0, q1,使用一个四阶滤波器,产生输出结果p'0q'0。如果|p2 – p0| < β,则使用另外一个四阶滤波器,即输入是p2, p1, p0, q0,输出是p'1;如果|q2 – q0| < β,使用一个四阶滤波器,即输入是q2, q1, q0, p0,输出是q'1

2.BS = 4
如果|p2 – p0| < β,|p0 – q0| < round(α/4),并且是亮度块,则:
p'0是对p2, p1, p0, q0, q1进行五阶滤波得到的;p'1是对p2 p1, q0, q0进行四阶滤波得到的;p'2是对p3, p2, p1, p0, q0进行五阶滤波得到的。
其余情况下:
p'0是对p1, p0, q1进行三阶滤波得到的。
边界的另外一边,即q块那边,滤波过程与p块是对称的,不再累赘。

JM代码中,EdgeLoopLumaNormal和EdgeLoopChromaNormal分别完成亮度块和色度块的滤波,具体实现请见代码,对照着Adaptive deblocking filter中的设计一起看,会更容易理解:-D

——————————————————————————————————————–

滤波相关的语法元素:

PPS中的deblocking_filter_control_present_flag,指示在slice header中是否存在控制滤波的语法元素存在,1表示存在,0表示不存在。该实例中为0。

slice header中的disable_deblocking_filter_idc,只有PPS中的deblocking_filter_control_present_flag为1的时候,该语法元素才存在,1表示不进行滤波,0表示进行frame级滤波,2表示进行slice级滤波(slice边界不进行滤波,这样各个slice的滤波可以并行),如果该语法元素不存在,则它为0。

参考:http://lsharemy.com/wordpress/index.php/csit/jm-code-notes-7-deblocking-filter/

原创粉丝点击