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 半分辨率宏块的最优预估 costint 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 1是COST_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;
}
}
...
}

浙公网安备 33010602011771号