Fork me on GitHub

H.264/AVC编解码技术及JM源码分析(二)——编解码流程与trace文件使用

本文主要讲解JM 19.0中整个编解码的流程以及其用于调试的trace文件的使用。

编码流程

编码流程的主要函数调用顺序以及所在文件如下:

  1. encode_sequence(p_Enc->p_Vid, p_Enc->p_Inp)——位于lencod.c的main函数中,顾名思义是编码一个视频序列的入口。
  2. encode_one_frame(p_Vid, p_Inp)——lencod.c。
  3. read_input_data (p_Vid)——image.c,从文件读取一帧图像。
  4. process_image(p_Vid, p_Inp)——image.c,将读取到的图像imgData0复制一份到imgData用于编码。
  5. perform_encode_frame(p_Vid)——image.c,帧编码时进行调用,若是场编码则调用perform_encode_field(p_Vid)。
  6. frame_picture(p_Vid, p_Vid->frame_pic[0], &p_Vid->imgData, 0)——image.c。
  7. code_a_picture(p_Vid, frame)——image.c。
  8. code_a_plane(p_Vid, p_Inp)——image.c,按顺序编码一帧图像的所有宏块,同时编码时区分了slice组和slice。
  9. encode_one_slice (p_Vid, SliceGroup, NumberOfCodedMBs)——slice.c,编码帧内的一个slice,返回编码的宏块数。
  10. 以下三个函数均在encode_one_slice中,是编码时最重要的三个函数:
    • currSlice->encode_one_macroblock(currMB)——编码宏块主要的函数,在rdopt.c中根据rdopt的值该函数指针指向了不同的函数,默认为encode_one_macroblock_high,该函数进行所有帧内/帧间模式的编码,通过比较后得到率失真代价最小的一种模式,来作为该宏块真正的编码模式,同时得到所有需要编码的语法元素的值。
    • end_encode_one_macroblock(currMB)——编码完一个宏块后,更新宏块参数。
    • write_macroblock(currMB, 1)——将语法元素进行真正的熵编码并写入码流。

解码流程

解码流程的主要函数调用顺序以及所在文件如下:

  1. DecodeOneFrame(&pDecPicList)——位于decode_test.c的main函数中,循环解码每一帧。
  2. decode_one_frame(pDecoder)——ldecod.c。
  3. read_new_slice(currSlice)——image.c,从码流文件中读取一个slice。
  4. decode_slice(currSlice, current_header)——image.c。
  5. decode_one_slice(currSlice)——image.c。
  6. 以下三个函数均在decode_one_slice中,是解码时最重要的三个函数:
    • start_macroblock(currSlice, &currMB)——初始化宏块的各种参数。
    • currSlice->read_one_macroblock(currMB)——从码流中读取和解析一个宏块的语法元素,根据slice类型不同可能会指向read_one_macroblock_i_slice_cavlc、read_one_macroblock_p_slice_cavlc、read_one_macroblock_b_slice_cavlc等等。
    • decode_one_macroblock(currMB, currSlice->dec_picture)——将语法元素和预测结合,从而解码还原出一个宏块的像素。
  7. WriteOneFrame()——位于decode_test.c的main函数中,将一帧图像写入YUV文件。

trace文件

当把defines.h中的# define TRACE 0改为# define TRACE 1时,就可以在运行程序时同时生成一个trace_enc.txt/trace_dec.txt文件,这两个文件输出了编解码时的许多重要信息,在某些时候对于调试非常有用。下面以trace_enc.txt为例讲解其中一些输出条目的含义。

  • @88:每一行开头出现的“@+数字”代表的是编码这一行的元素前已经编码了的比特数。
  • 01001101 ( 77):每一行末尾出现的这两个数字串,括号外的是当前编码元素在码流中的二进制表示,括号内的则是十进制值。
  • Annex B NALU w/ long startcode, len 5,
    forbidden_bit 0,
    nal_reference_idc 3,
    nal_unit_type 8:每一个NALU最后都会出现,包含了一个NALU的比特数长度和一些头信息,其中len是不包括startcode的长度的。
  • mb_type (I_SLICE) ( 0, 0) = 9:( 0, 0)代表宏块的坐标,9代表宏块编码模式,与代码中定义的枚举类型MBModeTypes一致,同时也是currMB->mb_type这个变量的值。
  • Intra 4x4 mode = predicted (context: 0)/Intra 4x4 mode = 5 (context: 7):当亮度4x4预测模式的预测值与真实值相等时为前者,不相等时为后者。
  • mb_skip_run:以CAVLC为熵编码时出现,只出现在非skip宏块中,用以表示其前面一共有几个连续的skip宏块,一个例外是当一帧的最后一个宏块是skip宏块时,它也会包含该元素,表示包括自身在内的之前出现的连续skip宏块数。另外,该元素在解码的时候会在碰到连续宏块的第一个宏块时就被解码,用以确定接下来一共有几个连续的skip宏块,因此在trace_dec.txt文件中它出现的地方和trace_enc.txt是不一样的。
  • 8x8 mode/pdir( 0) = 4/1:P8x8模式时出现,( 0)代表这是第几个8x8块(0-3);4/1与前面的mode/pdir对应,4(mode)即代表8x8的子分块模式,和MBModeTypes里的值是对应的,0:BSKIP_DIRECT,4:8x8,5:8x4,6:4x8,7:4x4;1(pdir)代表预测方向,0为前向,1为后向,2为双向;需要注意的是:这里的mode和pdir并不能涵盖程序中的b8x8[k].mode和b8x8[k].pdir两个变量的所有含义,因为它们会有更多的取值和组合含义,如b8x8[k].pdir=-1是表示是I宏块,没有帧间预测;b8x8[k].mode=1、2、3分别对应P16X16/16x8/8X16。
  • ref_idx_l0 = 0/ref_idx_l1 = 1:参考帧索引,l0/l1代表前/后向,对它们两个中的任意一个而言,在一个宏块中最多出现4个(4个8x8块),因为8x8块的参考帧索引会应用到其包含的4个4x4块中。最少一个都不用出现,因为没有参考帧或者不用编码参考帧。
  • mvd_l0 (0) = -2 (org_mv -187 pred_mv -185)
    mvd_l0 (1) = 0 (org_mv 0 pred_mv 0)
    mvd_l1 (0) = 0 (org_mv 0 pred_mv 0)
    mvd_l1 (1) = 7 (org_mv 0 pred_mv -7):运动矢量,根据分块的模式不同会有不同的个数,由于分x方向和y方向,所以一定是偶数个,16x16有2个,16x8和8x16有4个,8x8至少有8个,具体要看8x8是否还会在分成4x8/8x4/4x4;mvd_l0是前向运动矢量,mvd_l1是后向运动矢量;(0)是x方向,(1)是y方向;几个数字的含义也很清晰,分别是mvd、真实的mv和预测mv。
  • Luma # c & tr.1s(0,0) vlc=0 #c=2 #t1=1:Luma表示编码的是什么类型的DCT系数,与CAVLCBlockTypes这个枚举类型的值对应;(0,0)表示当前4x4块在一个宏块中的坐标(范围均是0-3);vlc表示查标准中表9-5的哪一列,c是非零系数个数,t1是拖尾系数个数。
  • Luma trailing ones sign (1,0):(1,0)是4x4块在一个宏块中的坐标。
  • Luma lev (1,0) k=4 vlc=0 lev= 2:(1,0)是4x4块在一个宏块中的坐标,k是除去拖尾系数后的第几个非零系数,逆序;vlc是suffixLength的值;lev是系数值。
  • Luma totalrun (0,1) vlc=9 totzeros= 4:(1,0)是4x4块在一个宏块中的坐标;vlc表示查表的哪一列;totzeros是总的零的个数。
  • Luma run (9,1) k=9 vlc=2 run= 0:(9,1)以及k=9中的9均表示这是第几个非零系数前的0游程,vlc表示查表9-10的哪一列;run是每一个非零系数前的零的个数。