x264 lookahead 阶段编码开销预估

参考:
https://blog.csdn.net/fanbird2008/article/details/9086669

1. 作用

编码开销预估在 x264 lookahead 阶段中完成,即在正式编码前,一帧待编码帧必须先进行编码开销预估的相关计算。其有如下作用:

  • 帧类型决策
  • scenecut 场景切换检测
  • 帧级码控
  • 行级码控
  • 预估的最优 mv 可作为正式编码时的候选 mv (当然这个没那么重要)

可见,编码开销预估是一个非常重要的模块,其影响了后续多个模块的计算过程。
本篇博客主要介绍编码开销预估是如何计算的,具体这些计算出来的值在其它模块是如何使用的,需要再深入每个模块具体分析。

2. 原理

2.1 半分辨率下采样

编码开销预估是在半分辨率下采样图像上进行的,未使用全分辨率是为了节约计算时间,且能较好的给出编码代价近似值。
半分辨率下采样插值计算入口为 mc.c::x264_frame_init_lowres(),在 lookahead 之前完成:

// 完成半像素插值
void x264_frame_init_lowres( x264_t *h, x264_frame_t *frame )
{
    // 原始 luma 像素
    pixel *src = frame->plane[0];
    int i_stride = frame->i_stride[0];
    int i_height = frame->i_lines[0];
    int i_width  = frame->i_width[0];
    ...
    // 完成插值
    h->mc.frame_init_lowres_core( src, frame->lowres[0], frame->lowres[1], frame->lowres[2], frame->lowres[3],
                                  i_stride, frame->i_stride_lowres, frame->i_width_lowres, frame->i_lines_lowres );
    ...
}

其中:

  • src 即指向原始图像 luma 像素
  • frame->lowres[4] 用于存放插值后的 4 种像素点,下面接着会作图解释
  • i_stride 即原始图像 luma 像素一行的像素长度
  • frame->i_stride_lowres 即半分辨率 luma 像素一行的像素长度
  • frame->i_width_lowres 即半分辨率像素宽度;frame->i_lines_lowres 即半分辨率像素高度

可以看到,主要调用 frame_init_lowres_core() 完成插值功能。接着来看一个 c 语言实现的版本 mc.c::frame_init_lowres_core()

// 半像素插值 c 语言版本
static void frame_init_lowres_core( pixel *src0, pixel *dst0, pixel *dsth, pixel *dstv, pixel *dstc,
                                    intptr_t src_stride, intptr_t dst_stride, int width, int height )
{
    // 遍历半分辨率每一行
    for( int y = 0; y < height; y++ )
    {
        pixel *src1 = src0+src_stride;
        pixel *src2 = src1+src_stride;
        // 遍历半分辨率每一列
        for( int x = 0; x<width; x++ )
        {
            // slower than naive bilinear, but matches asm
#define FILTER(a,b,c,d) ((((a+b+1)>>1)+((c+d+1)>>1)+1)>>1)
            dst0[x] = FILTER(src0[2*x  ], src1[2*x  ], src0[2*x+1], src1[2*x+1]);
            dsth[x] = FILTER(src0[2*x+1], src1[2*x+1], src0[2*x+2], src1[2*x+2]);
            dstv[x] = FILTER(src1[2*x  ], src2[2*x  ], src1[2*x+1], src2[2*x+1]);
            dstc[x] = FILTER(src1[2*x+1], src2[2*x+1], src1[2*x+2], src2[2*x+2]);
#undef FILTER
        }
        src0 += src_stride*2;
        dst0 += dst_stride;
        dsth += dst_stride;
        dstv += dst_stride;
        dstc += dst_stride;
    }
}

上面的代码对着如下示意图来看,就比较清晰了(参考 https://blog.csdn.net/fanbird2008/article/details/9086669):

A     B     C     D     E ...
   *     *     *     *    ...
a     b     c     d     e ...
   *     *     *     *    ...
0     1     2     3     4 ...

其中:

  • src0 指向原始 luma 像素 A B C ...
  • src1 指向原始 luma 像素 a b c ...
  • src2 指向原始 luma 像素 0 1 2 ...
  • dst0 即 A B a b 中间插值出来的那个像素
  • dsth 即 B C b c 中间插值出来的那个像素
  • dstv 即 a b 0 1 中间插值出来的那个像素
  • dstc 即 b c 1 2 中间插值出来的那个像素

这里每次可以插值出 2 x 2 = 4 个像素,但是实际上只有 dst0 才是 lookahead 阶段编码代价预估所用到的半分辨率像素(其余 3 个不太确定用处,搜索了一下代码,似乎只会在加权阶段才会用到。与正式编码时运动搜索半像素插值无关)。

2.2 关键变量解析

注意,lookahead 在处理 P 帧时,不仅会计算帧间 cost,也会计算帧内 cost,两者计算的信息都会存储起来。

2.2.1 x264_frame 内相关变量

x264_frame 结构体中,有如下相关变量,这些变量是关联每一帧的全局变量,不仅用在 lookahead 模块,也用在其它例如码控模块:

  • pixel* lowres[4] 即 2.1 节所述,用于存储半分辨率 luma 插值像素的地方
  • int16_t (*lowres_mvs[2][X264_BFRAME_MAX+1])[2] 用于存储编码预估时,每个 8x8 半分辨率宏块的最优运动矢量值
  • int lowres_mv_costs[2][X264_BFRAME_MAX+1] 用于存储编码预估时,每个 8x8 半分辨率宏块的最优预估 cost
  • int i_cost_est[X264_BFRAME_MAX+2][X264_BFRAME_MAX+2] 用于存储编码预估时,所有非边界宏块的最优预估 cost 的和,不是一行,而是整帧
  • int* i_row_satds[X264_BFRAME_MAX+2][X264_BFRAME_MAX+2] 用于存储编码预估时,每一行宏块的最优预估 cost 的和,不是整帧,而是一行. 注意 [0][0] 位置固定用于放帧内编码预估 cost

2.2.2 x264_slicetype_slice_t 内相关变量

x264_slicetype_slice_t 结构体中,有如下相关变量,这些变量只用在 lookahead 模块,是临时变量:

  • int *output_inter 既存储每一行宏块的 cost 累加值,也存储所有非边界宏块的 cost 累加值。与 i_cost_est 和 i_row_satds 区别是,此变量是一个 lookahead 内的临时变量,每个可参考帧在计算时会复用
  • int *output_intra 类似 output_inter,不过只有 intra 最优 cost 累加值

2.2.3 output_inter 变量解析

int *output_inter 变量指向一个一维数组,如何用一个一维数组来存储多个不同类型的值呢?实际上用到了如下几个宏定义:

  • #define COST_EST 0 表示所有非边界宏块 cost 累加值存储在索引 0 位置上
  • #define COST_EST_AQ 1COST_EST 的 aq 版本,存储在索引 1 位置上
  • #define INTRA_MBS 2 帧内 cost 会存储到索引 2 位置上
  • #define NUM_ROWS 3 在启用 lookahead 多线程时,用于存储当前线程处理的以宏块为单位的行数,放在索引 3 位置上
  • #define NUM_INTS 4 表示前面三个固定位置已经被使用了,接下来从索引 4 开始存储每行宏块 cost 累加值
  • #define ROW_SATD (NUM_INTS + h->mb.i_mb_y) 这里用于计算行索引,即从 4 开始,然后每处理一行递增

图示如下:

+-----------+---------+-----------+------+--------------+--------------+------
| 整帧累加和  | aq 版本 | intra 版本 |  行数 | 第 0 行累加和 |  第 n 行累加和 | ...
+-----------+---------+-----------+------+--------------+--------------+------

3 代码分析

3.1 入口

lookahead 具体计算的入口在 slicetype.c::slicetype_frame_cost() 函数:

//
// 将帧 b 以 slice 为单位计算其开销,返回其总开销
// 其中 p0 表示 b 的前向参考帧,p1 表示 b 的后向参考帧
// 若 p0 = p1 = b,则表示没有参考帧,即 b 为 I 帧
// 若 p1 = b,则表示只有前向参考帧,即 b 为 P 帧
// 
// 作为 I 帧,所有宏块的cost = intra cost
// 作为 P 帧,所有宏块的cost = min( intra cost, inter cost)
// 作为 B 帧,所有宏块的cost = inter cost
//
static int slicetype_frame_cost( x264_t *h, x264_mb_analysis_t *a,
                                 x264_frame_t **frames, int p0, int p1, int b ) {
  ...
  // 如果 p0, p1, b 序列已经计算过,就不用计算了
  if( fenc->i_cost_est[b-p0][p1-b] >= 0 && (!h->param.rc.i_vbv_buffer_size || fenc->i_row_satds[b-p0][p1-b][0] != -1) )
        i_score = fenc->i_cost_est[b-p0][p1-b];
  else {
    ...
    // 关键变量复位
    memset( output_inter[0], 0, (output_buf_size - PAD_SIZE) * sizeof(int) );
    memset( output_intra[0], 0, (output_buf_size - PAD_SIZE) * sizeof(int) );
    output_inter[0][NUM_ROWS] = output_intra[0][NUM_ROWS] = h->mb.i_mb_height;
    x264_slicetype_slice_t s = (x264_slicetype_slice_t){ h, a, frames, p0, p1, b, dist_scale_factor, do_search, w,
        output_inter[0], output_intra[0] };
    // 整帧编码代价预估
    slicetype_slice_cost( &s );
    ...
  }

  //
  // 前面一帧编码代价预估计算完了,下面主要将计算结果存储到 i_cost_est 和 i_row_satds 中
  //

  /* Sum up accumulators */
  // 如果不是 b 帧,
  if( b == p1 )
      fenc->i_intra_mbs[b-p0] = 0;
  // 第一次调用此函数时,b_intra_calculated 为 false,所以会进入 if 语句,一些变量清零
  if( !fenc->b_intra_calculated )
  {
      fenc->i_cost_est[0][0] = 0;
      fenc->i_cost_est_aq[0][0] = 0;
  }
  fenc->i_cost_est[b-p0][p1-b] = 0;
  fenc->i_cost_est_aq[b-p0][p1-b] = 0;

  int *row_satd_inter = fenc->i_row_satds[b-p0][p1-b];
  int *row_satd_intra = fenc->i_row_satds[0][0];
  // i_lookahead_threads 默认为 1 (即不启用 lookahead 多线程)
  for( int i = 0; i < h->param.i_lookahead_threads; i++ )
  {
      // 如果非 b 帧
      if( b == p1 )
          fenc->i_intra_mbs[b-p0] += output_inter[i][INTRA_MBS];
      // 第一次调用此函数时,进入 if 语句
      if( !fenc->b_intra_calculated )
      {
          // 把 intra 宏块的预估 cost 累加上
          fenc->i_cost_est[0][0] += output_intra[i][COST_EST];
          fenc->i_cost_est_aq[0][0] += output_intra[i][COST_EST_AQ];
      }

      // 把 inter 宏块的预估 cost 累加上
      fenc->i_cost_est[b-p0][p1-b] += output_inter[i][COST_EST];
      fenc->i_cost_est_aq[b-p0][p1-b] += output_inter[i][COST_EST_AQ];

      if( h->param.rc.i_vbv_buffer_size )  // 走这里
      {
          // 一行宏块的个数
          int row_count = output_inter[i][NUM_ROWS];
          // 将每一行最优 cost 的和,拷贝到 row_satd_inter 中
          memcpy( row_satd_inter, output_inter[i] + NUM_INTS, row_count * sizeof(int) );
          // 第一次调用此函数时,进入 if 语句
          if( !fenc->b_intra_calculated )
              memcpy( row_satd_intra, output_intra[i] + NUM_INTS, row_count * sizeof(int) );  // 累加 intra
          // 如果开启了 lookahead 多线程,则准备切到下一个线程的存储区域
          row_satd_inter += row_count;
          row_satd_intra += row_count;
      }
  }

  i_score = fenc->i_cost_est[b-p0][p1-b];
}

在帧类型决策阶段,特别是启用 B 帧后,为了确认一小段序列每一帧的帧类型,需要多次调用此函数,其中:

  • frames 即多帧序列,可以通过 p0, p1, b 索引到具体帧

3.2 一帧计算

slicetype_frame_cost() 通过调用 slicetype.c::slicetype_slice_cost() 函数完成主要计算过程:

static void slicetype_slice_cost( x264_slicetype_slice_t *s )
{
    x264_t *h = s->h;
    int do_edges = h->param.rc.b_mb_tree || h->param.rc.i_vbv_buffer_size || h->mb.i_mb_width <= 2 || h->mb.i_mb_height <= 2;

    // 实测 1920x1080 的原始视频,这里一般 do_edges 会因为 vbv 打开了,被设置为 1。其余 start_y = 67. end_y = 0. start_x = 119. end_x = 0.
    // 所以实际上就是对所有宏块,包括边界都进行了编码预估
    int start_y = X264_MIN( h->i_threadslice_end - 1, h->mb.i_mb_height - 2 + do_edges );
    int end_y = X264_MAX( h->i_threadslice_start, 1 - do_edges );
    int start_x = h->mb.i_mb_width - 2 + do_edges;
    int end_x = 1 - do_edges;

    // 这里 i_mb_x 和 i_mb_y 的最大值都与全分辨率相同,半分辨率下不是应该减半吗?实际上是因为半分辨率宏块大小为 8x8,所以宏块个数与全分辨率是相同的
    // 注意这里是反向遍历的,为什么呢?
    for( h->mb.i_mb_y = start_y; h->mb.i_mb_y >= end_y; h->mb.i_mb_y-- )
        for( h->mb.i_mb_x = start_x; h->mb.i_mb_x >= end_x; h->mb.i_mb_x-- )
            slicetype_mb_cost( h, s->a, s->frames, s->p0, s->p1, s->b, s->dist_scale_factor,
                               s->do_search, s->w, s->output_inter, s->output_intra );
}

3.3 单个宏块计算

对于每个宏块的计算,由 slicetype.c::slicetype_mb_cost() 函数完成:

// 宏块编码代价预估
static void slicetype_mb_cost( x264_t *h, x264_mb_analysis_t *a,
                               x264_frame_t **frames, int p0, int p1, int b,
                               int dist_scale_factor, int do_search[2], const x264_weight_t *w,
                               int *output_inter, int *output_intra )
{
    ...
    // 如果当前要编码预估的帧 b 为 p 帧,那么 b == p1, p0 < b; 如果是 b 帧,那么 p0 < b < p1; 如果是 i 帧,那么 p0 == b == p1. (这里的顺序为显示顺序,不是编码顺序)
    const int b_bidir = (b < p1);
    ...
    // 宏块索引值
    const int i_mb_xy = h->mb.i_mb_x + h->mb.i_mb_y * h->mb.i_mb_width;
    ...
    // 是否是四周边界的宏块,如果不是,则为 true
    int b_frame_score_mb = (i_mb_x > 0 && i_mb_x < h->mb.i_mb_width - 1 &&
                            i_mb_y > 0 && i_mb_y < h->mb.i_mb_height - 1) ||
                            h->mb.i_mb_width <= 2 || h->mb.i_mb_height <= 2;
    ...
    h->mb.pic.p_fenc[0] = h->mb.pic.fenc_buf;
    // 将半分辨率 yuv 数据复制到 p_fenc 中
    h->mc.copy[PIXEL_8x8]( h->mb.pic.p_fenc[0], FENC_STRIDE, &fenc->lowres[0][i_pel_offset], i_stride, 8 );
    ...
    // i 帧
    if( p0 == p1 )
        goto lowres_intra_mb;]
    ...
    // 正式运动搜索一般是 PIXEL_16x16,这里因为半分辨率,所以为 PIXEL_8x8
    m[0].i_pixel = PIXEL_8x8;
    // 复制 mv cost 预计算值,运动搜索的时候,返回的 cost 会累加上 mv 的 cost
    m[0].p_cost_mv = a->p_cost_mv;
    ...
    // 只参考一帧
    m[0].i_ref = 0;
    // 加载参考帧半像素
    LOAD_HPELS_LUMA( m[0].p_fref, fref0->lowres );
    ...
    // 如果是 b 帧走双向预测
    if( b_bidir ) {
      ...
    }
    ...
    // 调用运动搜索 (返回的 cost 包括残差和运动矢量代价)
    x264_me_search( h, &m[l], mvc, i_mvc );
    ...
lowres_intra_mb:
    // 尝试 i 宏块编码代价
    if( !fenc->b_intra_calculated )
    {
      ...
      // i 宏块关键变量赋值
      output_intra[ROW_SATD] += i_icost_aq;
      if( b_frame_score_mb )
      {
          output_intra[COST_EST] += i_icost;
          output_intra[COST_EST_AQ] += i_icost_aq;
      }
    }
    ...
    // 如果非 b 帧
    if( !b_bidir )
    {
        // 比较一下 i 宏块的 cost
        int i_icost = fenc->i_intra_cost[i_mb_xy];
        int b_intra = i_icost < i_bcost;
        // 如果 intra 编码 cost 更优,那么 i_bcost 更新为 intra 的 cost
        if( b_intra )
        {
            i_bcost = i_icost;
            list_used = 0;
        }
        // 如果不是边界宏块,那么累加 intra 编码的最优 cost
        if( b_frame_score_mb )
            output_inter[INTRA_MBS] += b_intra;
    }

    /* In an I-frame, we've already added the results above in the intra section. */
    // 如果是 p/b 宏块
    if( p0 != p1 )
    {
        int i_bcost_aq = i_bcost;
        // 如果开启了 aq
        if( h->param.rc.i_aq_mode )
            i_bcost_aq = (i_bcost_aq * fenc->i_inv_qscale_factor[i_mb_xy] + 128) >> 8;
        // 累加一行宏块最优 cost (注意,可能是 intra 或 inter)
        output_inter[ROW_SATD] += i_bcost_aq;
        // 如果不是边界宏块,则累加最优预估 cost (注意,不是一行的 cost 了)
        if( b_frame_score_mb )
        {
            /* Don't use AQ-weighted costs for slicetype decision, only for ratecontrol. */
            output_inter[COST_EST] += i_bcost;
            output_inter[COST_EST_AQ] += i_bcost_aq;
        }
    }
    ...
}
posted @ 2025-04-01 09:24  重返科韵路  阅读(65)  评论(0)    收藏  举报