博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
音视频技术--H.264代码与标准如何对应
阅读量:6091 次
发布时间:2019-06-20

本文共 8021 字,大约阅读时间需要 26 分钟。

总是有人说自己把
代码
和标准对应不起来。其实是因为你要么不知道标准各个章节讲的什么,要么不知道代码中各个
函数
的功能,或者两者都不知道。今天再以
 
X264
 
的帧内
编码
为例让大家体会一下读代码时该如何与标准对应。此贴是帖子
“”
的延续,因此采用的代码与
编译
环境设置与其一样,此处不再赘述。


上贴说过
 Encode_frame 
函数包含最核心的编码代码,那么我们现在就
 F11 
进去看看。遇到的第一个函数是
 x264_encoder_encode
,再
 F11 
进去,执行到
 x264_reference_update
,它在干什么呢?顾名思义猜测一定是更新帧间参考要用到的一些内存空间,因为我们现在还没有编码,所以
 F11 
进去后没执行什么操作就出来了。


继续
 F10
,执行到
 x264_frame_pop_unused
F11 
跟进,然后
 F10
,发现它走了
 x264_frame_new 
的分支(
x264_frame_pop 
分支干什么用的呢?暂时先别管。跟着流程走,管多了就迷茫了),
F11 
跟进
 x264_frame_new 
发现通篇都是对变量
结构
体指针
 frame 
里的成员变量执行
 CHECKED_MALLOC
,由此我们可以初步判断它是在为帧结构体分配内存空间。


step out 
跳出
 x264_frame_pop_unused
F10 
 x264_frame_copy_picture
F11 
进去读读代码我们就知道这个函数的功能是将待编码图像从
 pic_in 
复制到
 fenc->plane
。继续
 F10
,到了
 x264_frame_push
,通过阅读该函数的代码我们知道它的功能是将当前帧结构体从
 fenc 
移到
 h->frames.next 
中。后面的函数
 x264_frame_init_lowres
x264_adaptive_quant_frame
x264_encoder_frame_end 
都未被执行。既然没被执行,那我们现在暂时就不管它们。


F10 
 x264_stack_align( x264_slicetype_decide, h ); x264_stack_align 
顾名思义无非就是平台优化方面考虑的对齐操作,因此这里我们要关心的是函数
 x264_slicetype_decide
F11 
我们会发现进不到
 x264_slicetype_decide 
里。怎么办呢?见下面第一个截图,将光标点到
 x264_slicetype_decide 
上,点鼠标右键选择
 go to definition
,然后先在里面的第一行代码下断点(见下面第二个截图),然后再按
 F10 
就可以进入到
 x264_slicetype_decide 
函数了。该函数顾名思义是来决定当前
 slice 
的编码类型的,即到底是
 I 
片还是
 P 
片或
 B 
片。通过浏览其代码,我们也会发现代码所做也正是这样。


 


 


step out 
跳出
 x264_slicetype_decide
,继续
 F10 
执行到
 x264_frame_push
,这里实际要执行两个函数,因为
 x264_frame_push 
的第二个参数是函数
 x264_frame_shift
,所以会先执行它。
F11 
首先进入的就是
 x264_frame_shift
,然后
 step out 
跳出
 x264_frame_shift
,继续
 F11 
就进入了
 x264_frame_push
,通过阅读这两个简短的函数的代码,我们知道它们执行的操作是将当前编码帧结构体从
 h->frames.next 
移到
 h->frames.current


继续
 F10
,又到了一个
 x264_frame_shift 
函数,
F11 
进去通过阅读代码我们可以知道该函数的功能将当前帧结构体从
 h->frames.current 
移到
 h->fenc
(我有点奇怪,为什么
 X264 
要这么麻烦地把一个变量移来移去呢?一次搞定不行么?)。继续
 F10
,到了
 x264_reference_reset
,其功能顾名思义,也有英文注释,具体有什么用,现在我还不知道,暂时不管吧。


继续
 F10
,到了
 x264_reference_build_list
,顾名思义,参考列表构建,在
 JM 
里叫做参考列表初始化(
JM86
 
对应的函数是
 init_lists
)。参考列表初始化的作用即构建帧间编码图像所需要用到的参考图像列表。那么如何初始化呢?如果大家记得
 H.264 
标准的各个章节的功能,那么就该知道
 200503 
版的
 8.2.4 
小节正是讲的这部分内容。这样这个函数就与标准的内容对应起来了。至于通过代码是如何实现的,先看懂了标准的这个部分再来读这个函数的代码吧。


继续
 F10
,到了
 x264_ratecontrol_start
,顾名思义进行码率控制的一些准备工作。


继续
 F10
,到了
 x264_slice_init
,顾名思义片初始化,做了哪些工作呢?
F11 
进去执行了分支
 x264_slice_header_init
,通过浏览其代码,我们发现通篇都是对结构体指针
 sh 
内的成员变量的赋值操作。后面的
 x264_macroblock_slice_init 
函数在干什么,大家自己
 F11 
进去看,看不懂没关系,反正就是给一些变量赋初值嘛。继续
 F10
,到了
 bs_init
,顾名思义是对码流相关的变量进行初始化,因为
 bs 
就是
 bit stream 
嘛。


继续
 F10
,到了
 if( i_nal_type == NAL_SLICE_IDR && h->param.b_repeat_headers )
,注意前面的英文注释
 /* Write SPS and PPS */
,意思就是这里在向码流中写
 SPS 
 PPS
。这里的三组函数,顾名思义第一组是在写
 SEI
、第二组是在写
 SPS
、第三组是在写
 PPS
。那么如何写码流呢?当然是要遵循语法表了。下面以写
 SPS 
为例简要说明一下,如果大家记得
 H.264 
标准的各个章节的功能,那么就该知道
 200503 
版的
 7.3.2.1 
小节就是
 SPS 
语法表。因为
 7.3.2.1 
规定了
 SPS 
在码流中的第一个语法元素是
 profile_idc
,因此当我们
 F11 
进入
 x264_sps_write 
的时候会发现该函数第一行代码正是在写
 sps->i_profile_idc
,标准规定
 SPS 
第二个语法元素是
 constraint_set0_flag
,因此该函数的第二行代码就是在写
 sps->b_constraint_set0
,其他同理。这里说的是写的顺序,那么写的方式是什么呢?
H.264 
中的熵编码方式细分起来有很多,每个语法表的最后一列
 descriptor 
规定的就是对应的语法元素采用哪种熵编码方法。例如:
profile_idc 
 u(8)
,因此它采用
 8 
位无符号整数编码;
constraint_set0_flag 
 u(1)
,因此它采用
 1 
比特无符号整数编码。
各种熵编码方法在
 200503 
 7.2 
小节最后都有说明,此处不再赘述。好了,我们知道了各个语法元素采用什么方式编码,自然也就知道了代码中各个熵编码函数对应的是什么编码方式。例如:对
 profile_idc 
编码采用的是
 bs_write 
函数,当然这个函数的功能就是无符号熵编码了,对
 i_id 
编码采用的是
 bs_write_ue 
函数,当然这个函数的功能就是
 ue(v)——
无符号哥伦布编码。其他同理。其实这些我在帖子
“”
中已经讲过了。


顺便提一下,对码流的读写操作都要依据语法表所定义的语法元素顺序和熵编码类型。上面讲的是编码的具体例子,
解码
的具体例子我以前用
 JM 
讲过,参考帖子
“”
。好了,继续
 F10
,到了
 x264_slices_write
F11 
进入,再
 F10
,到了
 x264_stack_align( x264_slice_write, h );
我们关心的是
 x264_slice_write
,进入该函数,方法在上面已经说过了。
x264_slice_write 
第一个函数为系统函数
 memset
,下一个为
 x264_nal_start
,其功能看下代码就知道是在设置将要写入码流的
 NALU 
的第一个字节的值。第二个函数
 x264_slice_header_write 
顾名思义是在向码流中写入片头。如果大家记得
 H.264 
标准的各个章节的功能,那么就该知道
 200503 
版的
 7.3.3 
小节就是片头的语法表。写码流的过程与上面
 SPS 
的过程同理,此处不再赘述。


F10 
到了
 while 
循环,顾名思义根据
 while 
循环的循环条件猜测一下该
 while 
循环的功能,肯定就是循环对整个图像的每个宏块一次编码了。要验证一下猜测很简单,在
 while 
循环体的第一行下断点,按一次
 F5 
就观察一下
 mb_xy 
变量的值的变化情况。另外还有个信息说明了这一点,
h->sh.i_last_mb 
变量的值刚好等于待编码图像的总宏块数。


F10 
到了
 x264_fdec_
filter
_row
,顾名思义猜测该函数的功能是去块滤波。如果大家记得
 H.264 
标准的各个章节的功能,那么就该知道
 200503 
版的
 8.7 
小节正是讲的这部分内容。要读懂这个函数的代码就先学习一下
 8.7 
小节吧。


F10 
到了
 x264_macroblock_cache_load
,通过浏览代码我们知道是在对一些变量赋值,各个变量的含义顾名思义。这也属于编码前的准备工作。继续
 F10 
到了
 x264_macroblock_analyse
,看见英文注释了吧?不用我们顾名思义就知道它的功能了,是在进行模式选择。
F11 
进入该函数。第一个被调用的函数是
 x264_ratecontrol_qp
,顾名思义获取当前宏块
 QP
。第二个被调用的函数是
 x264_mb_analyse_init
F11 
进去后发现只有非
 I 
片才进行一些操作,那暂时就不管它。


F10 
到了
 x264_mb_cache_fenc_satd
F11 
进去。一开始是个
 4*4 
的双重循环。我们现在是在对一个宏块进行操作,这里又出现
 4*4 
的循环,那么很明显了这个双重循环肯定是在计算每个
 4*4 
的块,下面的
 2*2 
的双重循环肯定是在计算
 8*8 
的块。因为宏块的尺寸是
 16*16 
嘛,宽高分成
 4 
份不正好是
 4*4
,分成
 2 
份不正好是
 8*8 
么?做
视频
的人应该对
 4
8
16 
等常用的数字敏感。先分析第一个
 4*4 
的双重循环。注意,
for 
循环里的
 h->pixf.satd 
 h->pixf.sad 
都是函数指针,因此要用
 F11 
跟进。
h->pixf.satd 
的两个输入是
 zero 
 fenc
,跟进之后的函数
 pixel_satd_wxh 
在计算他们之差,然后作
 Hadamard 
变换,然后计算
 SATD
。由此可以猜测
 fenc 
里存放的是原始待编码宏块(到底是不是呢?读者自己反回去找到
 h->mb.pic.p_fenc[0] 
被赋值的地方看看就知道了)。后面代码的功能类似了,不重复叙述。总的来说,
x264_mb_cache_fenc_satd 
这个函数就是计算原始待编码宏块
 4*4 
 8*8 
 STAD
。算来做什么?暂时还不知道。


step out
,跳出
 x264_mb_cache_fenc_satd 
函数,继续
 F10
,到了
 x264_mb_analyse_intra
F11 
进入。
F10 
到了
 predict_16x16_
mode
_available
,顾名思义并结合该函数代码,可以确定它是在检查当前宏块有几种可用的
 16*16 
帧内
预测
模式。继续
 F10 
到了
 for 
循环
 for( i = 0; i < i_max; i++ )
,其循环条件
 i_max 
是函数
 predict_16x16_mode_available 
的返回值,那么很显然这个
 for 
循环是在循环计算可用预测模式了。继续
 F10
,进循环体到了
 h->predict_16x16[i_mode]
,这又是个函数指针,顾名思义并结合改函数代码,可以确定它是在取得
 16*16 
块当前预测模式下的帧内预测块。如果大家记得
 H.264 
标准的各个章节的功能,那么就该知道
 200503 
版的
 8.3.3 
小节正是讲了
 16*16 
块的各种预测模式下如何进行帧内预测的。要读懂这个函数的代码就先学习一下
 8.3.3 
小节吧。继续
 F10
,到了
 h->pixf.mbcmp[PIXEL_16x16]
,又是个函数指针,其功能大家自己跟进吧。该
 for 
循环完成后就把
 16*16 
块的最佳预测模式计算出来并存储起来了。


继续
 F10
,到了帧内
 4*4 
的预测模式选择部分。
for 
循环
 for( idx = 0;; idx++ )
idx 
是什么?因为这是帧内
 4*4 
预测,所以我们很自然应该联想到
 idx 
就应该是
 16 
 4*4 
块的编号,这个决定了
 16 
 4*4 
块的处理顺序,这个顺序可不是乱来的哦,
200503 
版标准
/
 6-10 
对顺序做了规定。继续
 F10
,到了
 x264_mb_predict_intra4x4_mode 
顾名思义并结合该函数代码可以确定它是在获得最可能预测模式,如果大家记得
 H.264 
标准的各个章节的功能,那么就该知道
 200503 
版的
 8.3.1.1 
小节正是讲的这部分内容。要读懂这个函数的代码就先学习一下
 8.3.1.1 
小节吧。继续
 F10 
到了
 predict_4x4_mode_available 
跟上面
 16*16 
块类似,功能顾名思义就不多说了。继续
 F10
,进入第二个
 for 
循环
 for( ; i<i_max; i++ )
,一看就知道该
 for 
循环跟上面
 16*16 
块同理是在计算当前
 4*4 
块的最佳预测模式。继续
 F10
,进入循环体到了
 h->predict_4x4[i_mode] 
顾名思义并结合该函数代码可以确定它是在取得当前
 4*4 
块在当前可用预测模式下的帧内预测块,如果大家记得
 H.264 
标准的各个章节的功能,那么就该知道
 200503 
版的
 8.3.1.2 
小节正是讲的这部分内容。要读懂这个函数的代码就先学习一下
 8.3.1.2 
小节吧。继续
 F10
,到了
 h->pixf.mbcmp[PIXEL_4x4]
,也跟上面
 16*16 
块类似,功能顾名思义。


继续
 F10
,第二个
 for 
循环执行完后就把当前
 4*4 
块的最佳预测模式计算出来并存储起来了,到了函数指针
 h->predict_4x4[a->i_predict4x4[idx]]
,很显然是在取得当前
 4*4 
块的最佳预测模式下的预测块了。算来干什么?从
 H.264 
帧内宏块编码的原理上我们知道帧内预测要以相邻块的重建值为参考,不先计算预测块,残差从哪里来?不得到残差,又哪里得到重建呢?(所以这里也体现了,读代码前要对编码原理和框架熟悉,否则你咋能明白这里为什么要取得预测块呢?)。继续
 F10
,到了
 x264_mb_encode_i4x4
,顾名思义并联想帧内编码原理和框架,我们猜测它是在进行当前
 4*4 
块的重建。
F11 
进去验证一下我们的猜测是否正确。
x264_mb_encode_i4x4 
函数里依次执行了
 h->dctf.sub4x4_dct
x264_quant_4x4
h->zigzagf.scan_4x4
h->quantf.dequant_4x4
h->dctf.add4x4_idct
,各函数功能顾名思义,的确验证了我们对
 x264_mb_encode_i4x4 
这个函数的功能的猜测。那么这些函数为什么要以这些顺序调用呢?因为编码原理和框架就是这样(这也再次体现了,读代码前要对编码原理和框架熟悉)。


step out
,跳出
 x264_mb_analyse_intra 
函数,继续
 F10
,到了
 x264_intra_rd
F11 
跟进。继续
 F10
,到了函数
 x264_analyse_update_cache
,顾名思义无法猜测其功能,
F11 
跟进之后发现它只调用了一个函数
 x264_mb_analyse_intra_chroma
,这个函数又是什么功能呢?留给读者自己去跟进吧。
step out
,跳出
 x264_analyse_update_cache 
函数,继续
 F10
,到了
 x264_rd_cost_mb
,顾名思义猜测是进行
 RDO 
模式选择。这种方法的失真测度通常是使用
 SSD
,即原始像素与重建像素的误差平方和。那么如果我们对
 x264_rd_cost_mb 
的功能猜测正确,其函数中必然有编码宏块的代码和计算
 SSD 
的代码。
F11 
跟进去验证我们的猜测,
x264_rd_cost_mb 
里的确调用了
 x264_macroblock_encode 
ssd_mb
。这两个函数是否是在执行编码和计算
 SSD 
的功能呢?留给读者自己去验证吧。提醒一句,
X264 
在这里用的失真测度不仅仅是
 SSD
,另外还有什么成分,读者自己去跟踪
 ssd_mb 
函数。
x264_rd_cost_mb 
函数最后执行的函数是
 x264_macroblock_size_cavlc
,顾名思义是在对当前宏块进行熵编码了。为什么要熵编码,因为
 RDO 
的率失真准则中要用到编码比特数啊。


step out
,跳出
 x264_rd_cost_mb 
函数,后面的代码不说大家也知道了。
step out
,跳出
 x264_intra_rd 
函数,该函数下面的
 6 
行代码(见下图)的功能大家得弄清楚。因为算了这么多模式,这么多代价,最后编码到底选哪个模式呢?答案就这里了。


 


step out
,跳出
 x264_macroblock_analyse 
函数,到了
 x264_macroblock_encode
,顾名思义并结合编码流程可以确定这里调用这个函数就是在用最终选定的那个最优的模式对当前宏块进行实际编码了。继续
 F10
,到了
 x264_bitstream_check_buffer
,顾名思义猜测是进行写码流前的一些准备工作。继续
 F10
,到了
 x264_macroblock_write_cavlc
,顾名思义并联想编码流程,很明显是在将最后的编码结果写入码流了。


至此,一个宏块帧内编码的过程就剖析完了。相信大家看完这么长的帖子之后,应该对我以前提出的学习建议中的两点有了深刻体会:1、读代码前一定要熟悉编码原理和框架;2、弄清楚标准各个章节讲的什么内容。当然这也是怎么看标准,怎么用标准的问题——先很粗略地了解各个章节是讲的什么,等到需要详细了解其内容时候再去细读相关章节。当然,语言功底在读代码过程中也是必须的,否则像函数指针这些东西你都搞不清楚怎么回事。

本文转自 fanxiaojun 51CTO博客,原文链接:http://blog.51cto.com/2343338/1064702,如需转载请自行联系原作者

你可能感兴趣的文章
# C 语言编写二进制/十六进制编辑器
查看>>
EMS SQL Management Studio for MySQL
查看>>
我的友情链接
查看>>
做母亲不容易
查看>>
详细的文档(吐槽)
查看>>
DEVEXPRESS 随记
查看>>
Ember.js 入门指南——{{action}} 助手
查看>>
VMware下安装QT Creator
查看>>
Linux时间同步设置
查看>>
Measure Graphics Performance
查看>>
RetrunMoreRow
查看>>
Redis学习笔记(3)-Hash
查看>>
Git使用的常用命令
查看>>
微软职位内部推荐-Senior Software Engineer
查看>>
多线程开发
查看>>
成功搞定一个通用的Extjs增删改查模块
查看>>
暴力屏蔽80访问失败的用户
查看>>
营销型后台系统开发应该考虑到的
查看>>
vue-admin-template 切换回中文
查看>>
java模式之模板模式——抽象类
查看>>