H.264码流分析

原文转自:http://blog.163.com/laorenyuhai126@126/blog/static/193507792010498440670/

H.264标准写得比较繁复,所以考虑在浏览完Whitepaper之后就开始研读X264代码。X264代码风格还是比较清晰简洁的。

根据对标准得理解,Picture Order Count在Slice解码的一开始就被提及:

I0 B1 B2 P3 B4 B5 P6
I0 P3 B1 B2 P6 B4 B5

于是I0的POC是0,P3的POC是3,B1是1……

为了支持H264复杂的帧存机制,X264以专门的一个模块frame.c进行处理。

common/frame.c中包括一组帧缓冲操作函数。包括对帧进行FILO和FIFO存取,空闲帧队列的相应操作等。

以下逐个函数分析encoder.c中编码一帧的函数x264_encoder_encode中有关frame的调用:

x264_reference_update
这个函数里最主要的工作的是将上一个参考帧放入参考帧队列,并从空闲帧队列中取出一帧作为当前的参考工作帧(即解码操作的目的帧),即h->fdec。

x264_t 结构体维护着CODEC的诸多重要信息,其中成员frames是一个指示和控制帧编码过程的结构。

其中current是已经准备就绪可以编码的帧,其类型已经确定;

next是尚未确定类型的帧;

unused用于回收不使用的frame结构体以备今后再次使用。

frames结构体中i_input指示当前输入的帧的(播放顺序)序号。i_delay设置为由B帧个数(线程个数)确定的帧缓冲延迟,在多线程情况下为i_delay = i_bframe + i_threads - 1。

而判断B帧缓冲填充是否足够则通过条件判断:h->frames.i_input <= h->frames.i_delay + 1 - h->param.i_threads。


x264_encoder_encode 每次会以参数送入一帧待编码的帧pic_in,函数首先会从空闲队列中取出一帧用于承载该新帧,而它的i_frame被设定为播放顺序计数,如:fenc->i_frame = h->frames.i_input++。
x264_encoder_encode在根据上述判据确定B帧缓冲充满的情况下才进行后续编码工作。


当当前队列(current队列)可用帧为0时,需要对next队列中的帧进行判决,需要进行如下过程:
1. 调用x264_slicetype_decide
这个函数确定当前条带(帧)的类型
其中首先调用x264_ratecontrol_slice_type,依据码率控制逐个求出next列表中所有帧的类型(虽然在当前并不全部用到,见后)。
随后统计审查并调整next列表,保证IDR帧满足有关最大关键帧间隔的要求的正常出现:

即针对frm->i_frame - h->frames.i_last_idr >= h->param.i_keyint_max作判断。审查按顺序针对所有被判定为B系或AUTO类型的帧进行(这些帧在审核过程中被确认为B帧),直到遇到第一个不是这样的帧。
如果某个帧被指定为IDR,则一个GOP在它之前结束。

2. 而后,即将next列表中已经判定的一系列帧(先后是一些B帧和一个非B帧)转移到current列表中。在这个过程中:
原始序列(播放顺序)B0, B1, B2, P,转移后的顺序为P, B0, B1, B2。在使用bframe_pyramid模式时,中间的B帧要前置,即上述顺序变为:P, B1, B0, B2。

此时,就可以从current队列中取出一帧,进行编码,现在记这帧叫h->fenc。
首先做几项和帧有关的设置工作:
1. 如果f_enc是IDR,则将最近IDR序号标记h->frames.i_last_idr设置为i_frame。
2. 根据f_enc的类型确定NAL和SLICE类型相关参数。 
3. 设置POC为2 * (h->fenc->i_frame - h->frames.i_last_idr)。并使得h->fdec和h->fenc的主要帧参数一致。

随后进行以下一些过程:
x264_reference_build_list
在这个函数中,我们将遇到参考帧列表h->frames.reference和H.264很有特色的双列表(h->fref0、h->fref1)。前者中放置了所有可用于参考的参考帧。
首先将所有reference列表中的帧按照POC和h->fenc的POC的大小关系不同复制到双列表中,其中h->fref0放置POC较小的那些。然后对双列表进行排序,使得h->fref0中的帧POC按从大到小排列,h->fref1按从小到大排列,于是这两个列表中靠前的帧的POC比较接近当前帧h->fenc。在这里,一个特殊的情况是,对于P帧(只需要用到列表h->fref0做参考),其排序依据是 i_frame_num而不是POC,因此需要对这种情况的列表h->fref0置位需重排序标记。最后对双列表的长度做限制。

x264_ratecontrol_start
码率控制初始化,在以后专门的话题中论述。
在初始化后,从码率控制体中获得全局QP值:i_global_qp = x264_ratecontrol_qp( h )。

如果当前是B条带(帧),则调用x264_macroblock_bipred_init
该函数设置一些参考帧关系矩阵,关系矩阵以fref0的索引为行坐标,以fref1的索引为列坐标。主要包括:dist_scale_factor,bipred_weight,矩阵值主要由相应帧的POC决定。后续讨论。

x264_slice_init
内部调用x264_slice_header_init,它对Header进行配置,含义以后讨论。

bs_init
初始化码流工作上下文。

随后将帧类型表现到码流中。接着写SEI,SPS和PPS。

然后就是最关键的写条带(即主要的编码工作)开始的地方:
x264_slices_write
这里通过x264_stack_align调用x264_slice_write,为了将栈做4字节对齐,以提高运行效率。

x264_slice_write函数是编码一帧的关键函数,将在下一章中论述。

 

 

宏块结构

 

 

这里主要有一下几个过程:
1. 初始化h->stat.frame,即全部清零。
2. 写条带头:x264_slice_header_write,即把刚才x264_slice_header_init设置的一些参数写入。
3. 如果是CABAC编码,则初始化CABAC。有关CABAC在后续相关章节讨论。
4. 遍历一帧中的所有宏块,这是编码的主要部分:
for( mb_xy = h->sh.i_first_mb, i_skip = 0; mb_xy < h->sh.i_last_mb; )
其中sh.i_first_mb和sh.i_last_mb在x264_slice_header_init中赋值,分别是0和宏块行列数乘积。
5. 最后输出码流尾部。
其间穿插一些必要的初始化和配置。

本章着重分析编码的主要部分,包括一下过程:
需要注意的是,这里讨论的宏块是固定尺寸的,即Y分量计为16x16。(而运动宏块的尺寸则是多变的)

1. 首先根据宏块的序号得出宏块的坐标。然后统计当前宏块的码流位置:
int mb_spos = bs_pos(&h->out.bs) + x264_cabac_pos(&h->cabac);

2. 然后出现一句:
if( i_mb_x == 0 )
    x264_fdec_filter_row( h, i_mb_y );
这是对行进行去块过程。详细情况专门论述。

3. 随后是x264_macroblock_cache_load,这里主要进行当前宏块和相关信息的提取。这个函数比较大,也在专题中论述。

4. 接下去进行解码分析操作
x264_macroblock_analyse

5. 以及解码实体:
x264_macroblock_encode

6. 而后处理码流输出,也分CABAC和CAVLC两种情况。

7. 接着出现x264_macroblock_cache_save,和x264_macroblock_cache_load对应,保存处理完的宏块。

8. 更新宏块统计


下面,首先看编码主体x264_macroblock_encode的结构。
该函数按不同宏块类型情况进行不同的处理。包括如下类型:
P_SKIP
B_SKIP
I_16x16 - 一次完成一个块h->mb.pic.p_fdec[0]的编码。
   用函数指针h->predict_16x16[i_mode]进行帧内预测
I_8x8   - 分4次完成。h->mb.pic.p_fdec[0][8 * (i&1) + 8 * (i>>1) * FDEC_STRIDE]指定了当前位置。即:
   [0][1]
   [2][3]
   用函数指针完成4个宏块的帧内预测
I_4x4 - 分16次完成。h->mb.pic.p_fdec[0][4 * block_idx_x[i] + 4 * block_idx_y[i] * FDEC_STRIDE]指定当前位置。
   根据block_idx_x和block_idx_y的定义,其顺序是:
   [0][1][4][5]
   [2][3][6][7]
   [8][9][c][d]
   [a][b][e][f]
   用函数指针完成4个宏块的帧内预测
Inter - 首先进行运动补偿。见后续讨论。
          然后分多种情形:
   1. 无损压缩情况:h->mb.b_lossless == TRUE(必须采用4x4DCT)
      根据上述I_4x4中描述的顺序遍历16个4x4块。
      这里调用的函数是zigzag_sub_4x4_field,完成残差。(无损情况直接发送残差)
   2. 指定采用8x8DCT情况:h->mb.b_transform_8x8 == TRUE
      首先是作残差并DCT的函数:sub16x16_dct8。
      然后对4个8x8的DCT系数块进行处理:包括可能的去噪x264_denoise_dct,量化(标量/矢量)。有关量化也在专题中论述。量化完成后进行系数扫描(对角平衡的普通扫描)。
   3. 一般情况
      
接着处理色差分量,如果是Intra的则进行预测。
最后写Coded Block Pattern。

 

 

帧内预测

 

环路内滤波

 

 

H.264环路内滤波顾名思义在编码侧开启后解码部分必须跟随开启,因此是该视频编码方案的不可分割的组成部分。

    以下整理了Baseline情形下环路滤波的四种情形:

    All cases that may exist for Baseline
    Bs = 4: either is intra, MB edge
    Bs = 3: either is intra, block edge
    Bs = 2: both are inter, either is coded
    Bs = 1: both are inter neither is coded, different ref pictures, either MV component is no smaller than 4
    Bs = 0: o.w.

 

    以T264为例:
    环路滤波入口函数以光栅扫描顺序对所有宏块实施deblock_mb。

    deblock_mb完成一个宏块的滤波,它包含以下步骤:

    1. 水平相邻宏块滤波,即对与mb_xy-1宏块的边界滤波,以及内部3条纵向块边界滤波;

    2. 垂直相邻宏块滤波,即对与mb_xy-mb_stride宏块的边界滤波,以及内部3条横向块边界滤波;

 

    每条边界长度均为16个像素,等分成4份,每个对应一个最小块大小的边界,以此为单元进行滤波。
    对每条边界调用get_strength,获得这4个部分的Bs值。

 

 

本章讨论的代码主要位于common/predict.c中。
x264_macroblock_cache_load函数在每个宏块解码之前初始化某些状态,在x264_slice_write函数的宏块处理循环中被调用。
i_mb_xy:   当前宏块的索引
i_mb_4x4:  当前宏块中第一个4x4块的索引
i_mb_8x8:  当前宏块中第一个8x8块的索引
i_top_y:   上方宏块的y索引
i_top_xy:  上方宏块的索引
i_top_4x4: 当前宏块中第一个4x4块上方的4x4块的索引
i_top_8x8: 当前宏块中第一个8x8块上方的8x8块的索引

这里首先初始化和当前宏块毗邻的已解码块。
这里用到x264_scan8,它指示如下扫描结构:

   0 1 2 3 4 5 6 7
 0
 1   0 1   0 1 4 5
 2   2 3   2 3 6 7
 3         8 9 C D
 4   0 1   A B E F
 5   2 3

 在空缺部分恰好可以填入相应宏块的毗邻块。

以下逐个各种分析预测情况:

对于16x16的块:
predict_16x16
共7种预测模式:H, V, DC, P, LEFT, TOP, 128
predict_16x16_h
用左侧相邻16x16块的右侧像素沿水平方向覆盖
predict_16x16_v
用上方相邻16x16块的底行像素沿竖直方向覆盖
predict_16x16_p
右上左下均值预测覆盖(注意均值生成)
predict_16x16_dc
用左侧边沿和上方边沿的均值作为DC预测值进行单一覆盖
predict_16x16_dc_left
只用左侧边沿进行DC单一覆盖
predict_16x16_dc_top
只用上方边沿进行DC单一覆盖
predict_16x16_dc_128
用128进行单一覆盖

对于8x8的色差块:
predict_8x8c_dc_128
用128进行单一覆盖
predict_8x8c_dc_left
上下两个4行分别以对应左边沿4个像素均值单一覆盖
predict_8x8c_dc_top
左右两个4列分别以对应上边沿4个像素均值单一覆盖
predict_8x8c_dc
   s0 s1
s2 b0 b1
s3 b2 b3
s?是边沿4像素组
4个4x4块分别用s0,s2均值,s1均值,s3均值和s1,s3均值单一覆盖
predict_8x8c_h
用左侧相邻8x8块的右边沿像素沿水平方向覆盖
predict_16x16_v
用上方相邻16x16块的底行像素沿竖直方向覆盖
predict_8x8c_p
右上左下均值预测覆盖(注意均值生成)

对于4x4的块:(这是H264文档经常拿来作demo的)
predict_4x4_dc_128
用128进行单一覆盖
predict_4x4_dc_left
用左边沿4个像素均值单一覆盖
predict_4x4_dc_top
用上边沿4个像素均值单一覆盖
predict_4x4_dc
用左侧和上方边沿共8个像素均值单一覆盖
predict_4x4_h
用左侧相邻4x4块的右边沿像素沿水平方向覆盖
predict_4x4_v
用上方相邻4x4块的底行像素沿竖直方向覆盖
predict_4x4_ddl
右上至左下预测覆盖(注意均值生成)
predict_4x4_ddr
左上至右下预测覆盖(注意均值生成)
predict_4x4_vr
左上到右下(偏下)预测覆盖(注意均值生成)
predict_4x4_hd
左上到右下(偏右)预测覆盖(注意均值生成)
predict_4x4_vl
右上到左下(偏下)预测覆盖(注意均值生成)
predict_4x4_hu
左下到右上(偏右)预测覆盖(注意均值生成)

对于8x8亮度块:
edge中存放
predict_8x8_dc_128
用128进行单一覆盖
predict_8x8_dc_left
用左侧块边沿8个像素均值单一覆盖
predict_8x8_dc_top
用上方块边沿8个像素均值单一覆盖
predict_8x8_dc
用左侧块和上方块边沿共16个像素均值单一覆盖
predict_8x8_h
用左侧边沿预测覆盖
predict_8x8_v
用上方边沿预测覆盖
predict_8x8_ddl
右上到左下预测覆盖(注意均值生成)
predict_8x8_ddr
左上到右下预测覆盖(注意均值生成)
predict_8x8_vr
左上到右下(偏下)预测覆盖(注意均值生成)
predict_8x8_hd
左上到右下(偏右)预测覆盖(注意均值生成)
predict_8x8_vl
右上到左下(偏下)预测覆盖(注意均值生成)
predict_8x8_hu
左下到右上(偏右)预测覆盖(注意均值生成)

以上“注意均值生成”指预测值一般由起点决定,起点由反向延长线确定,起点2倍权重和两侧点构成均值。
例如45度均值为以起始点对应45度位置的点作为中心(2倍权重),其两侧点作为补充形成的均值。 

H.264 标准规定,首先由macroblock层的mb_type导出intra块的预测类型macroblock prediction mode,其中包含子块尺度信息,有4x4,8x8和16x16三种。4x4和8x8按照标准由变换尺寸区分,4x4是最常用的模式。
在确定预测类型之后,对于含16个子块的4x4模式,每个子块的预测模式(即上述讨论)由标准规定的判决算法给出:
<...>


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值