首先肯定是要把 X264 编译通过了,这个我就不多说了,论坛帖子 VS2008下最新X264(svn 2009.9)编译不过的解决办法(附编译通过+修改内存泄露版本) 里讲得很清楚。编译通过之后第一步就是设置编码参数,一开始尽量从最基本的学起,因此采用最简单的选项,我的参数是:-q 28 -o test.264 f:/sequences/qcif/foreman_qcif.yuv 176x144 --no-asm,在哪里设置呢?看下面的截图(在第一个图中选择 properties 后就会出现第二个图了)。至于各个参数的意义嘛,看一看 X264 的帮助信息(x264.c 里面的 Help 函数),这里说说我用的这些参数的意思。-q 28 指定采用固定 QP = 28 进行编码,-o test.264 指定输出的码流文件名为 test.264,f:/sequences/qcif/foreman_qcif.yuv 这个当然是指定原始待编码图像序列了,176x144 指定待编码图像的宽高,--no-asm 指定不采用汇编优化,全 C 代码才方便我们跟踪噻。对了,差点忘记了,要调试运行还要设置一下下面第二个截图的红色框内的内容。
==================================================================================
好了,配置完了就要开始调试运行了哦。在 x264.c 文件中找到 main 函数,在 x264_param_default 函数所在这一行上下断点(即把光标移到这一行,然后按 F9),然后调试运行编码器(即按 F5),程序就会停在断点处(如下面截图)。下面开始单步调试了,除了我提到的东西,其他都不需要关心。
我以前多次说过,读代码首先要读框架(即粗读),任务是大致搞清楚各个函数在干什么。那么 x264_param_default 是我们第一个遇到的函数,它的功能是什么呢?顾名思义,猜测肯定是进行一些参数的设置了,因为 param 就是英文单词 parammeter 啊,按 F11 进到函数里浏览一下,果然是对一些变量赋值的操作,那么多变量啥意思?现在不必管那么多,因为你现在的任务只是搞清楚各个函数的功能,不要跑题了。x264_param_default 里的第一个函数是 memset,这个是系统函数,你要问啥功能?去查 MSDN;第二个函数是 x264_cpu_detect,啥功能?再顾名思义,因为 X264 针对不同的平台做了优化,这个肯定是用来检测你所使用的平台的,我就不进去看了。好了,直接点 debug 工具栏上的 step out(见下面第一个截图)返回 main 函数。你在你的 VC 里没有看到 debug 工具栏?那好吧,在 VC 工具栏的空白地方点鼠标右键,选中 debug(见下面第二个截图)。
==================================================================================
下面按 F10,程序执行到 Parse 函数,黄色的箭头也指向它了(见下面截图),它是啥功能?上面不是有英文注释么?——/* Parse command line */,意思就是说编码器就是在这个函数里读取我们上面设置的参数的。有兴趣进去看一下,看看就行,赶紧出来,不要又被陷在里面了,我就 pass 了。继续 F10 执行到 Encode,顾名思义,这个就是编码主函数了,因为后面再也没有函数了,所以编码一定是在这个函数里面完成的(不要问我它前面的 signal 是什么功能,我一开始说过了,我没提到的不需要关心,你问了我也不知道它是干什么的)。
好了,按 F11 进入 Encode 函数。一进去就有一些赋值语句,你要问那些变量是啥意思?——你跑题了!Encode 里第一个函数 p_get_frame_total 啥功能,顾名思义,我猜测是计算待编码图像序列一共有多少帧,要验证猜测的自己按 F11 跟进去看代码,我就 pass 了。继续 F10,到了函数 x264_encoder_open,它都做了些什么?F11 进去看看:memset,memcpy 是系统函数,F10 执行到 x264_validate_parameters,它在干什么?先想想 validate 是啥意思?然后 F11 进去,然后 F10 浏览一下好像还是在做一些参数的赋值和参数检查,浏览过程中对遇到的每个函数顾名思义一下其功能,自己考虑一下要不要跟到里面去。当我 F10 到 x264_sps_init 的时候,它的函数名让我有兴趣想知道它的具体功能——因为 SPS 跟编码直接关系。顾名思义SPS 初始化,F11 进去之后会发现是在对 SPS 结构体里的变量进行赋值。这些变量大部分在 200503 版标准 7.4.2.1 小节都能找到相应的解释。好了 step out 返回 x264_validate_parameters,继续 F10,好像其他没什么函数是我有兴趣的了,直接step out 返回 x264_encoder_open(或者在 x264_validate_parameters 里一直按 F10,最后也会返回到 x264_encoder_open)
好了,继续 F10(注意:x264_cqm_parse_file 这个函数是停不住的,它的功能顾名思义是解析量化矩阵配置文件,我们的编码选项里没有采用量化矩阵,因此它是不会被执行到的,你说我咋顾名思义知道它是跟量化矩阵相关的,因为 cqm 就是 custom quant matrix,你再要问我咋知道 cqm 是 custom quant matrix,我猜的,真是猜的!),到 x264_reduce_fraction 函数,这回我顾名思义不出来它在干什么了,那就 F11 进去,看见一小段计算,也不知道在干什么。但是再看看 x264_reduce_fraction 的输入参数,i_fps_num,顾名思义是帧率,此时把鼠标移动到这个变量上显示它的值是 25,见下面截图(要想知道它在哪里被赋值的,回头去找噻,还记得我们在 main 里遇到的第一个函数是 x264_param_default 么,当时顾名思义它的功能是什么?如果你回去找了,你会发现里面有个语句是:param->i_fps_num = 25;那到底是不是它呢?很简单,把这里 25 改成其他值,例如 27,再重新调试执行到 x264_reduce_fraction,看 h->param.i_fps_num 是否等于 27 就知道了。)
中间来了个插曲打乱了叙述的逻辑节奏。刚才我们是在考虑 x264_reduce_fraction 的功能。F11 进去没什么收获,但从输入参数上我们可以猜测到它应该是在做跟帧率相关的计算。好了,就先了解这么多吧。继续 F10,到了 x264_sps_init,咦,刚才我们不是遇到过一个 x264_sps_init 了么?为什么这里又有一个呢?读者自己试着去猜测一下原因。
继续 F10,到了 x264_pps_init,顾名思义,猜测是对 PPS 的参数设置,其中大部分变量在 200503 版标准 7.4.2.2 小节都能找到相应的解释,有兴趣的进去浏览一下。继续 F10,到 x264_validate_levels,顾名思义是检查一些变量的值是否合理。
继续 F10,执行到 x264_cqm_init,顾名思义量化矩阵初始化(记心好的朋友一定记得前面有个跟 cqm 相关的函数,那个是解析量化矩阵文件,功能不一样哈)。
继续 F10,到 x264_rdo_init,顾名思义进行 RDO 的一些初始化操作,F11 进去之后根据里面的函数顾名思义猜测是在进行 CABAC 表的初始化。
继续 F10,到 x264_predict_16x16_init,顾名思义是进行与 16*16 预测相关的一些初始化,F11 进去对里面用到的变量顾名思义发现是在设置一些帧内 16*16 预测函数的函数指针。
继续 F10,到 x264_predict_8x8c_init,顾名思义是与色度预测相关的初始化操作,F11 进去对里面用到的变量顾名思义猜测是在设置一些帧内色度预测函数的函数指针。
继续 F10,到 x264_predict_8x8_init,顾名思义是与 8*8 预测相关的初始化操作,F11 进去对里面用到的变量顾名思义猜测是在设置一些帧内 8*8 预测函数的函数指针。
继续 F10,到 x264_predict_4x4_init,顾名思义是与 4*4 预测相关的初始化操作,F11 进去对里面用到的变量顾名思义猜测是在设置一些帧内 4*4 预测函数的函数指针。
继续 F10,到 x264_init_vlc_tables,顾名思义是与 CAVLC 相关的初始化操作,F11 进去浏览下代码会知道是在进行 VLC 表初始化。同时可以从这里猜测到 h->param.b_cabac 这个变量的含义表示是否采用 CABAC 编码,因为该变量为 0 才进行 CAVLC 表初始化。
继续 F10,到 x264_pixel_init,顾名思义是像素初始化,不懂具体啥意思,一头雾水,那就 F11 进去看看,根据里面大量出现 SAD/SATD 猜测是在设置运动估计时候计算代价用的一些函数指针,因为计算代价会用到 SAD/SATD。
继续 F10,到 x264_dct_init,顾名思义是 DCT 相关的初始化,F11 进去对里面用到的变量顾名思义猜测是在设置 DCT 变换函数的函数指针。
继续 F10,到 x264_zigzag_init,顾名思义是 zigzag 扫描相关的初始化,F11 进去对里面用到的变量顾名思义猜测是在设置 zigzag 扫描函数的函数指针。
继续 F10,到 x264_mc_init,顾名思义是运动补偿(MC)相关的初始化。
继续 F10,到 x264_quant_init,顾名思义是量化相关的初始化,F11 进去会发现里面是在设置量化相关的函数的函数指针。
继续 F10,到 x264_deblock_init,顾名思义是去块滤波相关的初始化,F11 进去会发现里面是在设置去块滤波相关的函数的函数指针。
继续 F10,到 x264_dct_init_weights,顾名思义我也没猜出是啥意思,F11 进去也没获得进一步信息,那就先把里面用到的变量在脑袋里拍个快照。等以后用到这些变量的时候再根据上下文来理解。
继续 F10,到 mbcmp_init,顾名思义我也没猜出是啥意思,F11 进去看见有个宏定义 X264_ME_TESA,猜测是跟运动估计相关的操作。
继续 F10,到 x264_frame_pop_unused,F11 进去,读一下里面的代码就知道是取得一个 x264_frame_t 结构体,猜测这个结构体是用于存放当前编码帧的信息的。
继续 F10,到 h->thread[ i ]->out.p_bitstream = x264_malloc( h->out.i_bitstream );,根据 p_bitstream 和 x264_malloc 顾名思义是开辟码流存储空间的。
继续 F10,到 x264_macroblock_cache_init,F11 进去浏览下代码,猜测是开辟宏块信息存放空间,以及进行一些宏块信息初始化设置的。
继续 F10,到 x264_ratecontrol_new,顾名思义是码率控制相关的函数。
好了,x264_encoder_open 函数就这么浏览完了,各个函数的大概功能也知道了。返回到 Encode 函数,F10 继续,到了 p_set_outfile_param,F11 进去发现是个空函数。F10 继续,到了 x264_picture_alloc,结合注释并顾名思义是分配解码过程中图像要用到的存储空间的。F10 继续,到 x264_mdate,从函数名不知道什么意思,F11 进去就知道了,是获取当前时间的。继续 F10 就到了 for 循环,从 for 循环的代码以及注释可以知道这个 for 循环的功能就是循环编码每一帧了。继续 F10,到 p_read_frame,顾名思义是在读入当前帧待编码图像。继续 F10,到 Encode_frame,顾名思义,这就是执行编码一帧图像的函数了,最核心的编码代码肯定就在这里面了。这个 for 循环之后还有个 do-while 循环,其中也调用了 Encode_frame 函数,根据注释猜测是进行 B 帧编码的。x264_mdate 刚才说了是在获取当前时间,x264_picture_clean 顾名思义是在释放图像的占用内存,x264_encoder_close 顾名思义是执行关闭编码器的一些操作,F11 进去后浏览代码会发现包括输出信息、释放内存等一些操作,x264_free 也释放内存,p_close_infile 和 p_close_outfile 顾名思义是关闭输入输出文件。
至此 X264 编码器第一层函数结构就分析完了。下面接着的就是采用同样的方法分析编码主函数 Encode_frame 了。经过上面的叙述,相信不会阅读代码的朋友一定已经掌握了阅读代码的入门技巧。当然越到深层函数的分析要求对 H.264 标准和 H.264 编码原理越熟悉,所以分析编码主函数的难度也比较大,耗时也比较长。因此学习 X264 代码是需要将 H.264 的编码知识与代码实现结合起来的,这样才能更容易读懂代码,这一点在上面的叙述中也有体现。
好了,暂时写到这里吧。打这么多字,真累啊!希望你们看着不会觉得累。呵呵~~~
——天之骄子·firstime——
2009年10月31日
总是有人说自己把代码和标准对应不起来。其实是因为你要么不知道标准各个章节讲的什么,要么不知道代码中各个函数的功能,或者两者都不知道。今天再以 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 讲过,参考帖子“如何结合标准看JM代码(JM86)”。好了,继续 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、弄清楚标准各个章节讲的什么内容。当然这也是怎么看标准,怎么用标准的问题——先很粗略地了解各个章节是讲的什么,等到需要详细了解其内容时候再去细读相关章节。当然,C 语言功底在读代码过程中也是必须的,否则像函数指针这些东西你都搞不清楚怎么回事。
有了这个实际的例子,切身的体验,再回头去看看我和别人以前总结的学习方法的帖子,相信你会有更深的体会。最后还要说一句:别人不可能为你做所有的事,很多还是要靠自己努力!
——天之骄子·firstime——
2009年11月14日