x264代码分析:参考帧管理

 X264是一款研究的是H.264编码的开源代码软件,相比JM而言,其编码性能有很大的提高,其支持大多数H.264的特性工具,包括:CABAC和 CAVLC高效嫡编码、多参考帧预测、所有的帧内预测宏块类型(16x6l和4x4)、所有的前向帧间预测P(帧)宏块类型 (16xl6,16x8,8xl6,8x8,8x4,4x8和4x4)、最常用的双向帧间预测(B帧)宏块类型(16xl6,16x8,8xl6和 8x8)、1/4 像素精度运动估计、率失真优化、自适应B帧选择且B帧可作为参考帧。

基于前面学习的JM15.1中的参考帧管理部分,JM中的参考帧管理的基本流程如下:
1.参考帧图像列表的初始化;
2.参考图像列表的重排序;重排序的目的主要是为了减少参考帧索引号所需要的编码,对于要不要重排序,就要看sh->b_ref_pic_list_reordering_l0和 sh->b_ref_pic_list_reordering_l1的取值
3.帧编码;
4.参考图像序列的标记;
这里是根据X264编码的流程,将参考帧作为其中的重点来跟踪:

X264的总体流程:有四个层次(用R来表示总路线):

A.主函数main函数,编码功能的主函数Encode(),其中以 i_frme_total为限制条件的for循环,进入

B.encode frame(),这是编码的第二层,调用x264_encoder_encode()进行帧层的编码,这个函数主要是进行VCL层的编码及部分NAL网络适应层的编码。在这个函数编码的顺序(因为参考帧在里面出现,所以对这个函数的流程进行仔细跟踪,用NO.表示):

NO.1 主要是将要编码的帧存储在fenc中,并对要编码的帧进行排序,fenc和fdec 分别表示表示编码和解码代表的缓存区间。在编码当前图像,先检查fdec中存储的重建帧,并将其加入到参考帧列表中。

在 x264_encoder_encode() 函数中通过调用 reference_update() 函数实现:

    // ok to call this before encoding any frames, since the initial values of fdec have b_kept_as_ref=0
    // 更新参考帧队列frames.reference[].若为B帧则不更新
    // 重建帧fdec移植参考帧列表,新建一个fdec
    if( reference_update( h ) )
        return -1;

reference_update() 函数实现:

static inline int reference_update( x264_t *h )
{
    //如果当前帧不是被参考的帧
    if( !h->fdec->b_kept_as_ref )
    {
        if( h->i_thread_frames > 1 )
        {
            x264_frame_push_unused( h, h->fdec );
            h->fdec = x264_frame_pop_unused( h, 1 );
            if( !h->fdec )
                return -1;
        }
        return 0;
    }

    /* apply mmco from previous frame. */
    for( int i = 0; i < h->sh.i_mmco_command_count; i++ )
        for( int j = 0; h->frames.reference[j]; j++ )
            if( h->frames.reference[j]->i_poc == h->sh.mmco[i].i_poc )
                x264_frame_push_unused( h, x264_frame_shift( &h->frames.reference[j] ) );

    /* move frame in the buffer */
        //重建帧加入参考帧列表,这里fdec应该存储的是刚被重建的帧的内容
    x264_frame_push( h->frames.reference, h->fdec );
    //列表满了,则要移除1帧
    if( h->frames.reference[h->sps->i_num_ref_frames] )
        x264_frame_push_unused( h, x264_frame_shift( h->frames.reference ) );
    // 从unused队列里面找出一个不用的buff用来存储新的重建帧的内容
    h->fdec = x264_frame_pop_unused( h, 1 );
    if( !h->fdec )
        return -1;
    return 0;
}

NO.2 为重建帧建立参考帧列表,也就是相对JM里面的.参考帧图像列表的初始化:

首先判断当前待编码帧的帧类型(h->fenc->i_type)重置参考帧列表

  • 如果待编码帧是 IDR,则不需要参考帧,即进入函数  reference_reset( h ),清空参考帧列表中所有的参考帧,并设置当前帧的参考优先级为最高级:NAL_PRIORITY_HIGHEST
  • 如果待编码帧是 I / P / BREF,则调用 reference_hierarchy_reset(h) 重置参考帧

目的:重置所有参考帧管理

    if( h->fenc->i_type == X264_TYPE_IDR )
    {
        // I与IDR区别
        // 注意IDR会导致参考帧列清空,而I不会
        // I图像之后的图像可以引用I图像之间的图像做运动参考
        /* reset ref pictures */
        i_nal_type    = NAL_SLICE_IDR;
        i_nal_ref_idc = NAL_PRIORITY_HIGHEST; // 参考优先级
        h->sh.i_type = SLICE_TYPE_I;
        //若是IDR帧,则清空所有参考帧
        reference_reset( h );
        h->frames.i_poc_last_open_gop = -1;
    }
    else if( h->fenc->i_type == X264_TYPE_I )
    {
        //I与IDR区别
        //注意IDR会导致参考帧列清空,而I不会
        //I图像之后的图像可以引用I图像之间的图像做运动参考
        i_nal_type    = NAL_SLICE;
        i_nal_ref_idc = NAL_PRIORITY_HIGH; /* Not completely true but for now it is (as all I/P are kept as ref)*/
        h->sh.i_type = SLICE_TYPE_I;
        reference_hierarchy_reset( h );
        if( h->param.b_open_gop )
            h->frames.i_poc_last_open_gop = h->fenc->b_keyframe ? h->fenc->i_poc : -1;
    }
    else if( h->fenc->i_type == X264_TYPE_P )
    {
        i_nal_type    = NAL_SLICE;
        i_nal_ref_idc = NAL_PRIORITY_HIGH; /* Not completely true but for now it is (as all I/P are kept as ref)*/
        h->sh.i_type = SLICE_TYPE_P;
        reference_hierarchy_reset( h );
        h->frames.i_poc_last_open_gop = -1;
    }
    else if( h->fenc->i_type == X264_TYPE_BREF )
    {
        //可以作为参考帧的B帧,这是个特色
        i_nal_type    = NAL_SLICE;
        i_nal_ref_idc = h->param.i_bframe_pyramid == X264_B_PYRAMID_STRICT ? NAL_PRIORITY_LOW : NAL_PRIORITY_HIGH;
        h->sh.i_type = SLICE_TYPE_B;
        reference_hierarchy_reset( h );
    }
    else    /* B frame */
    {
        //最普通
        i_nal_type    = NAL_SLICE;
        i_nal_ref_idc = NAL_PRIORITY_DISPOSABLE;
        h->sh.i_type = SLICE_TYPE_B;
    }

 reference_reset 函数:

static inline void reference_reset( x264_t *h )
{
    while( h->frames.reference[0] )
        x264_frame_push_unused( h, x264_frame_pop( h->frames.reference ) ); // 将参考帧弹出,放入到 unused 队列中
    h->fdec->i_poc =
    h->fenc->i_poc = 0;
}

 reference_hierarchy_reset 函数:(没太理解)

static inline void reference_hierarchy_reset( x264_t *h )
{
    int ref;
    int b_hasdelayframe = 0;

    // 非参考B帧一般是IS_DISPOSABLE
    /* look for delay frames -- chain must only contain frames that are disposable */
    for( int i = 0; h->frames.current[i] && IS_DISPOSABLE( h->frames.current[i]->i_type ); i++ )
        b_hasdelayframe |= h->frames.current[i]->i_coded
                        != h->frames.current[i]->i_frame + h->sps->vui.i_num_reorder_frames;

    /* This function must handle b-pyramid and clear frames for open-gop */
    if( h->param.i_bframe_pyramid != X264_B_PYRAMID_STRICT && !b_hasdelayframe && h->frames.i_poc_last_open_gop == -1 )
        return;

    /* Remove last BREF. There will never be old BREFs in the
     * dpb during a BREF decode when pyramid == STRICT */
     // 标记要清除的参考帧, BREF 帧 或
    // h->frames.reference[ref]->i_poc < h->frames.i_poc_last_open_gop
    // 参考帧
    // 并设置list0重序标志
    for( ref = 0; h->frames.reference[ref]; ref++ )
    {
        if( ( h->param.i_bframe_pyramid == X264_B_PYRAMID_STRICT
            && h->frames.reference[ref]->i_type == X264_TYPE_BREF )
            || ( h->frames.reference[ref]->i_poc < h->frames.i_poc_last_open_gop
            && h->sh.i_type != SLICE_TYPE_B ) )
        {
            int diff = h->i_frame_num - h->frames.reference[ref]->i_frame_num;
            h->sh.mmco[h->sh.i_mmco_command_count].i_difference_of_pic_nums = diff;
            h->sh.mmco[h->sh.i_mmco_command_count++].i_poc = h->frames.reference[ref]->i_poc;
            x264_frame_push_unused( h, x264_frame_shift( &h->frames.reference[ref] ) );
            h->b_ref_reorder[0] = 1;
            ref--;
        }
    }

    /* Prepare room in the dpb for the delayed display time of the later b-frame's */
      // 计算list0 清除参考帧的数目(从距离远者开始清除)
    if( h->param.i_bframe_pyramid )
        h->sh.i_mmco_remove_from_end = X264_MAX( ref + 2 - h->frames.i_max_dpb, 0 );
}

NO.3 在完成了上面关于不同帧的 i_nal_type, i_nal_ref_idc,h->sh.i_type的设定之后,调用reference_build_list 建立参考帧列表,并根据POC值对参考帧列表进行重排序。

static inline void reference_build_list( x264_t *h, int i_poc )
{
    int b_ok;

    /* build ref list 0/1 */
    h->mb.pic.i_fref[0] = h->i_ref[0] = 0;
    h->mb.pic.i_fref[1] = h->i_ref[1] = 0;
    if( h->sh.i_type == SLICE_TYPE_I )
        return;

    // 如果当前的slice类型为SLICE_TYPE_I, 则返回
    //初始化fref0和fref1,并得到fref0和fref1所参考的帧数,在程序中跟踪后发现,fref0会随参数的设置改变大小的
    for( int i = 0; h->frames.reference[i]; i++ )
    {
        // 如果帧被破坏, 则跳过继续
        if( h->frames.reference[i]->b_corrupt )
            continue;
        // 将参考帧队列中i_poc小于当前i_poc的帧
        // 放入参考帧队列ref[0];
        // 否则放入ref[1]中
        if( h->frames.reference[i]->i_poc < i_poc )
            h->fref[0][h->i_ref[0]++] = h->frames.reference[i];
        else if( h->frames.reference[i]->i_poc > i_poc )
            h->fref[1][h->i_ref[1]++] = h->frames.reference[i];
    }

    // h->sh.i_mmco_remove_from_end 在 x264_reference_hierarchy_reset
    // 函数中被设置 (h->sh.i_mmco_remove_from_end = X264_MAX( ref + 2 - h->frames.i_max_dpb, 0 );)
    // 标记清除参考帧列表0中的帧,从距离远的帧开始清除
    // 清除h->sh.i_mmco_remove_from_end个参考帧
    if( h->sh.i_mmco_remove_from_end )
    {
        /* Order ref0 for MMCO remove */
        do
        {
            b_ok = 1;
            for( int i = 0; i < h->i_ref[0] - 1; i++ )
            {
                if( h->fref[0][i]->i_frame < h->fref[0][i+1]->i_frame )
                {
                    XCHG( x264_frame_t*, h->fref[0][i], h->fref[0][i+1] );
                    b_ok = 0;
                    break;
                }
            }
        } while( !b_ok );

        for( int i = h->i_ref[0]-1; i >= h->i_ref[0] - h->sh.i_mmco_remove_from_end; i-- )
        {
            int diff = h->i_frame_num - h->fref[0][i]->i_frame_num;
            h->sh.mmco[h->sh.i_mmco_command_count].i_poc = h->fref[0][i]->i_poc;
            h->sh.mmco[h->sh.i_mmco_command_count++].i_difference_of_pic_nums = diff;
        }
    }


    // 依据参考帧与当前帧的poc距离来排序参考帧
    // 排序参考帧, 并且获取各自参考帧列表的最近的参考帧
    // 这里先依据POC距离来排序参考帧列表
    // 然后在x264_reference_check_reorder中
    // list0 依据 i_frame_num 来确定是否重序,
    // list1 依然依据 poc 来确定是否重序
    // 在函数x264_reference_check_reorder中,
    // 设置重序标志,在x264_slice_header_init
    // 利用这两个标志来确定将是否需要重序的标志
    // 写进slice_header里,并通过x264_slice_write
    // 写进码流
    /* Order reference lists by distance from the current frame. 按距离当前帧的距离排列参考帧列表*/
    for( int list = 0; list < 2; list++ )
    {
        h->fref_nearest[list] = h->fref[list][0];
        do
        {
            //相当于JM里面的list0,将ref0中的参考帧按照POC进行排序
            b_ok = 1;
            for( int i = 0; i < h->i_ref[list] - 1; i++ )
            {
                // 参考帧队列0放的是前向参考帧,而参考帧队列1放的是后向参考帧
                // 前向参考帧列表按降序排列,而后向参考帧列表按升序排列
                if( list ? h->fref[list][i+1]->i_poc < h->fref_nearest[list]->i_poc
                         : h->fref[list][i+1]->i_poc > h->fref_nearest[list]->i_poc )
                    h->fref_nearest[list] = h->fref[list][i+1];

                if( reference_distance( h, h->fref[list][i] ) > reference_distance( h, h->fref[list][i+1] ) )
                {
                    // 如果参考帧i到待编码帧的距离 大于 参考帧i+1到待编码帧的距离
                   // 交换在参考帧列表里的两参考帧
                    XCHG( x264_frame_t*, h->fref[list][i], h->fref[list][i+1] );
                    b_ok = 0;
                    break;
                }
            }
        } while( !b_ok );
    }

    // 检查参考帧列表是否需要重序
    reference_check_reorder( h );

    //在上面获的h->i_ref0,h->i_ref1在下面进行重新选择
    //因为在参数的初始化设置时,h->frames.i_max_ref1=1,则h->i_ref1会在0和1之间波动,
    //而h->frames.i_max_ref0=2,并且会随
    //命令行参数h->param.i_frame_reference的设置而发生变化,和都会去变化中的最小值,
    h->i_ref[1] = X264_MIN( h->i_ref[1], h->frames.i_max_ref1 );
    h->i_ref[0] = X264_MIN( h->i_ref[0], h->frames.i_max_ref0 );
    h->i_ref[0] = X264_MIN( h->i_ref[0], h->param.i_frame_reference ); // if reconfig() has lowered the limit

    /* For Blu-ray compliance, don't reference frames outside of the minigop. */
    if( IS_X264_TYPE_B( h->fenc->i_type ) && h->param.b_bluray_compat )
        h->i_ref[0] = X264_MIN( h->i_ref[0], IS_X264_TYPE_B( h->fref[0][0]->i_type ) + 1 );

    /* add duplicates */
    if( h->fenc->i_type == X264_TYPE_P )
    {
        int idx = -1;
        if( h->param.analyse.i_weighted_pred >= X264_WEIGHTP_SIMPLE )
        {
            x264_weight_t w[3];
            w[1].weightfn = w[2].weightfn = NULL;
            if( h->param.rc.b_stat_read )
                x264_ratecontrol_set_weights( h, h->fenc );

            if( !h->fenc->weight[0][0].weightfn )
            {
                h->fenc->weight[0][0].i_denom = 0;
                SET_WEIGHT( w[0], 1, 1, 0, -1 );
                idx = weighted_reference_duplicate( h, 0, w );
            }
            else
            {
                if( h->fenc->weight[0][0].i_scale == 1<<h->fenc->weight[0][0].i_denom )
                {
                    SET_WEIGHT( h->fenc->weight[0][0], 1, 1, 0, h->fenc->weight[0][0].i_offset );
                }
                weighted_reference_duplicate( h, 0, x264_weight_none );
                if( h->fenc->weight[0][0].i_offset > -128 )
                {
                    w[0] = h->fenc->weight[0][0];
                    w[0].i_offset--;
                    h->mc.weight_cache( h, &w[0] );
                    idx = weighted_reference_duplicate( h, 0, w );
                }
            }
        }
        h->mb.ref_blind_dupe = idx;
    }

    assert( h->i_ref[0] + h->i_ref[1] <= X264_REF_MAX );
    //h->mb.pic.i_fref[0]和h->mb.pic.i_fref[1] 比较重要,会在后面的宏块的编码中利用参考帧使用到
    h->mb.pic.i_fref[0] = h->i_ref[0];
    h->mb.pic.i_fref[1] = h->i_ref[1];
}

NO.4 下面这一部分就是对NAL编码部分的分析:对于 i_nal_type=NAL_SLICE_IDR,编写获得PPS,SPS,SEI的比特流,所以作为一个GOP,当编码第一个IDR是,其i_nal=4,因为这里要编写4中类型的NALU:PPS,SPS,SEI,IDR,在后面对每一个非IDR 帧,i_nal=1,即只是需要进行本类型的编码。

至此,已经对参考帧进行初始化和排序了, 参考图像的重排序 ,这里分析x264是怎么将要排序的参考帧序列写入码流中,并进行对ref0进行重排序。

之后通过h->b_ref_reorder[0] 和h->b_ref_reorder[1];来看是否要进行h->fref0和h->fref1的重排序

    if( h->param.rc.b_stat_read && h->sh.i_type != SLICE_TYPE_I )
    {
        x264_reference_build_list_optimal( h );
        reference_check_reorder( h );
    }
/* Check to see whether we have chosen a reference list ordering different
 * from the standard's default. */
static inline void reference_check_reorder( x264_t *h )
{
    /* The reorder check doesn't check for missing frames, so just
     * force a reorder if one of the reference list is corrupt. */
     // 如果有参考帧被破坏了, 则重序
    for( int i = 0; h->frames.reference[i]; i++ )
        if( h->frames.reference[i]->b_corrupt )
        {
            h->b_ref_reorder[0] = 1;
            return;
        }
    // 对于list0, 用i_frame_num来判断是否重序
    // list0 是按i_frame_num降序排列的,
    // 因此如果framenum_diff > 0, 则list0重序
    // 对于list1, 用poc来判断是否重序,
    // list1是按poc升序排列的, 因此poc < 0 则list1重序
    for( int list = 0; list <= (h->sh.i_type == SLICE_TYPE_B); list++ )
        for( int i = 0; i < h->i_ref[list] - 1; i++ )
        {
            int framenum_diff = h->fref[list][i+1]->i_frame_num - h->fref[list][i]->i_frame_num;
            int poc_diff = h->fref[list][i+1]->i_poc - h->fref[list][i]->i_poc;
            /* P and B-frames use different default orders. */
            if( h->sh.i_type == SLICE_TYPE_P ? framenum_diff > 0 : list == 1 ? poc_diff < 0 : poc_diff > 0 )
            {
                h->b_ref_reorder[list] = 1;
                return;
            }
        }
}

C.  接下来就是针对上面关于 h->b_ref_reorder[0] 和h->b_ref_reorder[1] 这两个参数的设置来看是否进行重排序,于是进入总流程的第三部分:条带层!目的就是找到重排序的函数的语法元素,并写入在slice 头中
进入slice_init( x264_t *h, int i_nal_type, int i_global_qp ) 中的 slice_header_init 函数中:

计算得到 idc 和 arg 作为参考帧的偏移量

sh->ref_pic_list_order[list][i].idc = ( diff > 0 );
sh->ref_pic_list_order[list][i].arg = (abs(diff) - 1) & ((1 << sps->i_log2_max_frame_num) - 1);
    sh->b_num_ref_idx_override = 0;
    sh->i_num_ref_idx_l0_active = 1;
    sh->i_num_ref_idx_l1_active = 1;
    //调用前面的 h->b_ref_reorder[0]和 h->b_ref_reorder[1]
    sh->b_ref_pic_list_reordering[0] = h->b_ref_reorder[0];
    sh->b_ref_pic_list_reordering[1] = h->b_ref_reorder[1];

    /* If the ref list isn't in the default order, construct reordering header */
    for( int list = 0; list < 2; list++ )
    {
        if( sh->b_ref_pic_list_reordering[list] )
        {
            int pred_frame_num = i_frame;
            for( int i = 0; i < h->i_ref[list]; i++ )
            {
                int diff = h->fref[list][i]->i_frame_num - pred_frame_num;
                //这里获得sh->ref_pic_list_order[0][i].idc
                //和 sh->ref_pic_list_order[0][i].arg ,
                //在参考帧重排序作为偏移量
                sh->ref_pic_list_order[list][i].idc = ( diff > 0 );
                sh->ref_pic_list_order[list][i].arg = (abs(diff) - 1) & ((1 << sps->i_log2_max_frame_num) - 1);
                pred_frame_num = h->fref[list][i]->i_frame_num;
            }
        }
    }

在执行完slice init 过后,进入:slices_write( x264_t *h ) 这是的第三层编码的重点函数。

参考帧重排序的在函数 slice_write里面:在这一部分找到前面我们在slice header 里面关于h->b_ref_reorder[0] 和h->b_ref_reorder[1]的部分: 目的:重排序

    /* ref pic list reordering 参考帧列表重排序 */
    if( sh->i_type != SLICE_TYPE_I )
    {
        bs_write1( s, sh->b_ref_pic_list_reordering[0] );
        if( sh->b_ref_pic_list_reordering[0] )
        {
            for( int i = 0; i < sh->i_num_ref_idx_l0_active; i++ )
            {
                bs_write_ue( s, sh->ref_pic_list_order[0][i].idc );
                bs_write_ue( s, sh->ref_pic_list_order[0][i].arg );
            }
            bs_write_ue( s, 3 );
        }
    }
    if( sh->i_type == SLICE_TYPE_B )
    {
        bs_write1( s, sh->b_ref_pic_list_reordering[1] );
        if( sh->b_ref_pic_list_reordering[1] )
        {
            for( int i = 0; i < sh->i_num_ref_idx_l1_active; i++ )
            {
                bs_write_ue( s, sh->ref_pic_list_order[1][i].idc );
                bs_write_ue( s, sh->ref_pic_list_order[1][i].arg );
            }
            bs_write_ue( s, 3 );
        }
    }

完成上面参考帧的冲排序后,回到总流程上面来,
简单介绍条带层的编码的第3部分,如下
最重要的函数是slice_write();(Encoder.c L1601)
这个函数的主要部分是在

while( (mb_xy = i_mb_x + i_mb_y * h->sps->i_mb_width) <= h->sh.i_last_mb )

在循环里面主要是对宏块进行帧内和帧间的预测,运动估计,运动补偿,4×4DCT变换,量化和zig_zag扫描,和P_skip与B_SKIP宏块模式的决定,熵编码等,这是编码的核心部分。
x264_macroblock_analyse( h ) 是对进行帧内和帧间的运动估计,保存运动矢量;
x264_macroblock_encode( h ) 进行的对残差的4×4DCT,量化,zig_zag扫描,并重建和解码端同步的参考帧。

D.  回到总流程中的编码的第四部分,对宏块层的编码:
要分析的函数:x264_macroblock_analyse( h )和
x264_macroblock_encode( h )
x264_macroblock_analyse( x264_t *h )(Analyse.c L2337)
用来分析各种可能帧内和帧间预测模式下的编码代价,以寻找最合适的预测模式。

猜你喜欢

转载自blog.csdn.net/BigDream123/article/details/125451673
今日推荐