x264 帧级码控分析
1. 帧级码控理论分析
给定如下变量:
- wanted:目标码率
- complex:图像复杂度
- QP:量化参数
- A:编码黑盒
视频编码可以用如下语句描述:
- 对于一副图像复杂度为 complex 的图像,使用量化参数 QP 对其进行编码,编码黑盒 A 能将这副图像映射为最终的期望码率大小 wanted

x264 ABR 码控依赖如下的一个重要先验信息:即假设这几个变量,有如下的线性关系
wanted = A * (complex / QP)
即:
- 最终编码的码率大小,只与 A、complex、QP 这几个量有关
- 且,图像复杂度一定的情况下,最终编码的码率大小,与 QP 值成反比
- 且,QP 值一定的情况下,最终编码的码率大小,与图像复杂度成正比
- A 可以认为是一个编码器黑盒。complex 和 QP 固定后,再将图像输入黑盒,出来的就是 wanted
- 很显然黑盒将 complex 和 QP 映射为 wanted 不是线性关系,但是 x264 简化了这个关系
ABR 码控模式中,用户会传递一个期望的目标码率 wanted,而 x264 要做的就是:
- 选定合适的 A、complex、QP 这三个变量的值,使得最终编码出来的码流,码率大小,与期望的 wanted 相等
有了要实现的目标,x264 只需要:
- 用合适的算法,得到这 3 个变量的值,且不难想到,这 3 个变量值随着编码的进行,是不断变化的
- 且正式编码前,需要给出的是 complex 和 QP 值,而 A 是黑盒由编码时内部决定
1.1 complex 图像复杂度
图像复杂度是最容易得到的,每次编码会输入一副图像,x264 只需要用一个合适的特征算法,计算出一个值,用以表征这副图像的编码复杂度:
- x264 在 lookahead 阶段会计算半分辨率下,每个宏块的编码开销预估值
- ABR 码控阶段,计算图像复杂度就采用了这个编码开销预估值
编码开销预估可以参考:https://www.cnblogs.com/moonwalk/p/18803483
1.2 QP 量化参数
根据前面的线性公式,有:
- QP = complex * A / wanted
这里 complex 和 wanted 都有,但是 A 还没有。问题是如何得到 A 呢?一个想法是用历史的编码结果,估计出 A 的值
实际上 x264 也是这么做的,我们再变化一下线性公式,有:
- A = wanted * QP / complex
这时,这里的 wanted、QP 和 complex 不再是本轮编码的参数,而是历史编码,得到的累计值!,即:
- A_now = wanted_history * QP_history / complex_history
- 注意 wanted_history 不再是期望码率,而是实际编码后的码率
1.3 如果不满足线性关系呢
前面有一个先验信息,即 A、complex、QP 与 wanted 是线性关系,这个先验可能在大部分时刻都成立,但是还是有如下情况需要考虑:
- 在短时间内,图像内容变化非常剧烈时,线性关系不成立
- 累计误差导致线性关系随着时间推移,不再成立
为了修复线性误差,x264 采用了一个简单粗暴的方法:
- 计算 当前一段时间编码后的实际码率 与 这段时间内的期望码率 之前的偏离程度,根据偏离程度修正 QP 值
可能有人会问,我直接先给定一个初始的 QP 值,然后只通过本节的方法迭代调整 QP 行不行呢?
- 假设前面图像都很简单,突然来了一帧很复杂的图像,这时由于没有考虑图像复杂度,那么这帧图像可能出来的效果很差
那如果每帧的 QP 只由图像复杂度得到,然后通过本节的方法调整 QP 行不行呢?
- 只考虑图像复杂度得到的 QP,与线性关系求得的最优 QP 值相差可能很远。虽然本节能够修正 QP 值,但是修正后的 QP 对单幅图像而言,也不是最优的 QP。最终可能导致编码后的码率波动过大
1.4 rate_factor 码率因子
ABR 码控的核心就是这个 rate_factor 码率因子了,这个值可以通过变化线性关系式得到:
- 由 QP = complex * A / wanted
- 变换为 QP = complex / (wanted / A)
- 得 rate_factor = wanted / A
- 即 QP = complex / rate_factor
1.5 qscale 与 qp
qp 值是最终量化时使用的参数,但是在帧级码控模块,使用的不是 qp 而是 qscale 值。
qscale 可以与 qp 按如下公式相关转化 (具体如何得到的,可以简单认为是根据实验得出来的拟合模型,这里主要关注两者是正相关的即可):

对应 ratecontrol.c::qp2qscale() 和 ratecontrol.c::qscale2qp() 源码:
static inline float qp2qscale( float qp )
{
return 0.85f * powf( 2.0f, ( qp - (12.0f + QP_BD_OFFSET) ) / 6.0f );
}
static inline float qscale2qp( float qscale )
{
return (12.0f + QP_BD_OFFSET) + 6.0f * log2f( qscale/0.85f );
}
1.6 与模式选择时 RDO (率失真优化) 的关系
个人理解码控和 RDO 相互影响,但不是同一个概念:
- 码控相当于给 RDO 一个目标码率值,RDO 的工作是在目标码率值附近,找到一个失真最小的一组模式参数
- RDO 又相当于前面说的编码黑盒 A,因为实际编码工作中一大部分可以看作是在进行 RDO 流程。而 RDO 影响了最终编码出来的码率
- RDO 中的 lambda 参数是由 QP 值映射过去的,这块的具体理论个人不太理解,还需要进一步研究
2. 帧级码控具体实现
注意,以下讨论不涉及 vbv、mb_tree 等特性。
2.1 帧级码控入口
x264 帧级码控的入口是 ratecontrol.c::x264_ratecontrol_start() 函数:
...
if( rc->b_abr )
{
// 计算 abr 模式(实际上 ABR/CBR/CRF 都会走到这里)的 qp
q = qscale2qp( rate_estimate_qscale( h ) );
}
...
else /* CQP 模式 */
{
// 直接取 cqp 模式用户指定的 constant QP (若是 BREF,则 QP = (B_QP + P_QP)/2, 其余取各自的 QP)
if( h->sh.i_type == SLICE_TYPE_B && h->fdec->b_kept_as_ref )
q = ( rc->qp_constant[ SLICE_TYPE_B ] + rc->qp_constant[ SLICE_TYPE_P ] ) / 2;
else
q = rc->qp_constant[ h->sh.i_type ];
}
...
可以看到,cqp 模式的最终 qp 直接取用户指定的值;其余模式继续走 ratecontrol.c::rate_estimate_qscale() 函数。
2.2 P 帧获取 qscale 值
2.2.1 模糊复杂度估计
根据前面所述,计算 qscale 值需要先得到图像复杂度等参数的值,参看 ratecontrol.c::rate_estimate_qscale() 函数:
double wanted_bits, overflow = 1;
// 得到当前编码帧的 satd (satd 可以理解为当前帧的编码代价,值越大说明编码可能需要用到的比特就越大. 注意,此值在 lookahead 阶段得到)
rcc->last_satd = x264_rc_analyse_slice( h );
// 对 short_term_cplxsum、short_term_cplxcount 两个值做个简单的平滑滤波
rcc->short_term_cplxsum *= 0.5; // 历史 satd 值 * 0.5
rcc->short_term_cplxcount *= 0.5; // 历史累计数量 * 0.5
rcc->short_term_cplxsum += rcc->last_satd / (CLIP_DURATION(h->fenc->f_duration) / BASE_FRAME_DURATION); // 累加当前帧 satd
rcc->short_term_cplxcount ++; // 算上当前帧,计数 +1
...
// 计算图像复杂度
rce.blurred_complexity = rcc->short_term_cplxsum / rcc->short_term_cplxcount;
...
2.2.2 根据模糊复杂度计算 qscale 值
参看 ratecontrol.c::get_qscale() 函数:
...
// CRF 和 CQP 的主要区别就在这里,即 CRF 的 qp 值会依赖图像复杂度
// blurred_complexity (图像复杂度) 是与历史图像复杂度平滑过的,实测复杂度越大,q 值越大。
// 这里的考虑是,对于复杂度高的图像,比如高速运动场景,细节不太明显。那么这里增大 qp 值即使会降低编码质量,对人眼来说也能够获得相同的感知质量
q = pow( rce->blurred_complexity, 1 - rcc->qcompress ); // 根据图像复杂度,计算 q 值 (qcompress 默认 0.6),blurred_complexity 越大,q 值越大
...
2.2.3 ABR 模式 qscale 修正
在 ratecontrol.c::get_qscale() 函数中,通过 rate_factor 变量对 qscale 进行第一次修正:
...
q /= rate_factor; // rate_factor 在这里发挥作用
...
rate_factor 变量由如下方式得到,参看 ratecontrol.c::rate_estimate_qscale() 函数:
...
// abr 模式 rate_factor = 历史期望总大小 / 系数 A
q = get_qscale( h, &rce, rcc->wanted_bits_window / rcc->cplxr_sum, h->fenc->i_frame );
...
cplxr_sum 变量即前面说的系数 A,跟 wanted_bits_window 变量一起,在 ratecontrol.c::x264_ratecontrol_end() 函数更新计算:
if( h->sh.i_type != SLICE_TYPE_B )
rc->cplxr_sum += bits * qp2qscale( rc->qpa_rc ) / rc->last_rceq;
else
{
/* Depends on the fact that B-frame's QP is an offset from the following P-frame's.
* Not perfectly accurate with B-refs, but good enough. */
rc->cplxr_sum += bits * qp2qscale( rc->qpa_rc ) / (rc->last_rceq * h->param.rc.f_pb_factor);
}
rc->cplxr_sum *= rc->cbr_decay;
rc->wanted_bits_window += h->fenc->f_duration * rc->bitrate;
rc->wanted_bits_window *= rc->cbr_decay;
其中:
- bits 是编码完当前帧后,实际占用的码率大小
- qpa_rc 是当前编码帧所有宏块实际编码的平均 QP 值
- last_rceq 是当前编码帧的图像复杂度 (不要被这个变量名迷惑,实际值就是图像复杂度)
2.2.4 CRF 模式 qscale 修正
CRF 模式中,rate_factor 变量由用户指定,并在调用 get_qscale() 函数时直接透传过去:
...
// crf 模式用户指定的 --crf 参数在此生效
q = get_qscale( h, &rce, rcc->rate_factor_constant, h->fenc->i_frame );
...
可以看到,crf 模式与 abr 模式的区别是:
- 两者调用 get_qscale() 计算 q 值时,都会先考虑图像复杂度给出一个 q 值
- 对给出的 q 值修正的时候,abr 模式会再次考虑码率限制,而 crf 模式不会考虑码率限制
- 所以不考虑 vbv 算法的前提下,crf 模式编码只考虑编码质量,不考虑码率限制
2.2.5 ABR 模式 qscale 第二次修正
在 ratecontrol.c::rate_estimate_qscale() 函数中:
// 计算 abr buffer 大小 (rate_tolerance 默认 1.0,bitrate 即用户输入期望码率)
double abr_buffer = 2 * rcc->rate_tolerance * rcc->bitrate;
// 计算 predicted_bits (total_bits 即累计已编码的 bits 数,每编码完一帧都会累加)
double predicted_bits = total_bits;
...
int i_frame_done = h->i_frame;
// 播放到该帧所需要的时间(秒)
double time_done = i_frame_done / rcc->fps;
// 得到目前为止我们所期望的比特总开销:时间 * 用户期望 bitrate = 期望 bits
wanted_bits = time_done * rcc->bitrate;
if( wanted_bits > 0 )
{
// 更新 abr_buffer
abr_buffer *= X264_MAX( 1, sqrt( time_done ) );
// 计算 abr_buffer 的上溢下溢 (计算实际编码后码率和期望码率的差,是否在 abr_buffer 范围内)
overflow = x264_clip3f( 1.0 + (predicted_bits - wanted_bits) / abr_buffer, .5, 2 );
// 根据 abr_buffer 的上溢下溢情况调整 qscale
q *= overflow;
}
...
可以看到,abr 模式下额外还有一次码率修正。该调节因子的主要思想是:
- 先计算实际编码的累计 bits 数与期望 bits 数的差
- abr_buffer 可以看作是一个差值的容忍量,或者说一个衡量差值大小的基数
- 当差值太大或者太小时,通过 overflow 的值减小或者增大 qscale 的值,实现前面所说的线性误差修正的方法
2.3 I 帧获取 qscale 值
I 帧的 qscale 值由 P 帧和 B 帧的历史平均值来得到,并且通过一个用户可配置的参数 --ipratio 来调节 I 帧的 qscale 值。
参看 ratecontrol.c::rate_estimate_qscale() 函数:
//
// 这里是对 I 帧单独设置 qp 逻辑的地方 (即 pict_type == SLICE_TYPE_I 判断)
// last_non_b_pict_type 初始化为 SLICE_TYPE_I,后面每次调用完 rate_estimate_qscale() 函数后,都设置为当前帧的帧类型
// 这里的逻辑是:对整个编码序列非第一个 I 帧的其它 I 帧,设置 qp 值
//
if( pict_type == SLICE_TYPE_I && h->param.i_keyint_max > 1
/* should test _next_ pict type, but that isn't decided yet */
&& rcc->last_non_b_pict_type != SLICE_TYPE_I )
{
// 得到历史平均 qp 值
q = qp2qscale( rcc->accum_p_qp / rcc->accum_p_norm );
// 通过 f_ip_factor 参数进行调节
q /= h->param.rc.f_ip_factor;
}
一般来说 I 帧的质量需要比较高,--ipratio 参数默认为 1.4。
2.4 B 帧获取 qscale 值
B 帧一般是双向参考的,在计算 B 帧 qscale 值的时候,会先获取前向和后向最近一个可参考帧的 qp 值,然后通过这两个已知的 qp 值,得到 B 帧的 qscale 值。
参看 ratecontrol.c::rate_estimate_qscale() 函数:
// B 帧不拥有独立的码率控制,而是通过其相邻的两个 P 帧的平均 QP + offset 来计算
// L0 中最近的参考帧是否是 I 帧
int i0 = IS_X264_TYPE_I(h->fref_nearest[0]->i_type);
// L1 中最近的参考帧是否是 I 帧
int i1 = IS_X264_TYPE_I(h->fref_nearest[1]->i_type);
// 该帧到 L0 最近的参考帧的距离
int dt0 = abs(h->fenc->i_poc - h->fref_nearest[0]->i_poc);
// 该帧到 L1 最近的参考帧的距离
int dt1 = abs(h->fenc->i_poc - h->fref_nearest[1]->i_poc);
// L0 中最近参考帧平均宏块级 qp 值
float q0 = h->fref_nearest[0]->f_qp_avg_rc;
// L1 中最近参考帧平均宏块级 qp 值
float q1 = h->fref_nearest[1]->f_qp_avg_rc;
// 检查该帧的两个参考帧是否存在 BREF, 若 L0 L1 最近的参考帧是 BREF, 则其 qp 减去一个 offset
if( h->fref_nearest[0]->i_type == X264_TYPE_BREF )
q0 -= rcc->pb_offset/2;
if( h->fref_nearest[1]->i_type == X264_TYPE_BREF )
q1 -= rcc->pb_offset/2;
// 根据两个最近参考帧来计算本帧的 qp
if( i0 && i1 ) // 如果前后参考都是 I 帧
q = (q0 + q1) / 2 + rcc->ip_offset;
else if( i0 ) // 如果前向参考为 I 帧
q = q1;
else if( i1 ) // 如果后向参考为 I 帧
q = q0;
else // 如果前后参考都不是 I 帧
q = (q0*dt1 + q1*dt0) / (dt0 + dt1);
// 根据本帧能否用作参考(BREF)来修正 qp
if( h->fenc->b_kept_as_ref )
q += rcc->pb_offset/2; // 如果本B帧能作为参考,那么 qp 值增加 pb_offset 的一半
else
q += rcc->pb_offset; // 如果本B帧不作为参考,那么 qp 值增加 pb_offset
一般来说 B 帧的质量不需要那么高,毕竟大部分情况下 B 帧不会被用作参考帧,所以可以通过一个用户可配置的调节参数 --pbratio 来增加 B 帧的 qp 值。

浙公网安备 33010602011771号