x264 帧级码控分析

1. 帧级码控理论分析

给定如下变量:

  • wanted:目标码率
  • complex:图像复杂度
  • QP:量化参数
  • A:编码黑盒

视频编码可以用如下语句描述:

  • 对于一副图像复杂度为 complex 的图像,使用量化参数 QP 对其进行编码,编码黑盒 A 能将这副图像映射为最终的期望码率大小 wanted
    image

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 值。

posted @ 2025-04-01 14:40  重返科韵路  阅读(190)  评论(0)    收藏  举报