x264 RC - RateControl 详解

x264 RC - RateControl 详解

RC(RateControl):是一个x264的抽象层,提供了一种视频码流的控制方法。

  • HRD:hypothetical reference decoder (假定参考解码器) 。因为x264只负责编码,没有解码模块,而x264在编码的时候又需要考虑到解码的一些设置,比如bitrate的控制,帧的延迟等等,所以x264就创建了一个“假的”解码器,来作为编码时候的参考。它主要记录了比特流(bitrate),编码缓冲区大小(Code Picture Buffer -- cpb),

  • VBV:Video buffering verifier,中文都叫它“视频缓冲区验证”,直白的定义应该是:和HRD一样,它也是个“假想的”视频编码缓冲区,用来模拟真实的内存缓冲,以控制编码输出的码流大小。


RC main flow

一帧(Frame)进行编码的时候,是如何与VBV关联的:

vbv_max_rate:输出码流的比特率

VBV相当于一个虚假的内存缓冲,它的总大小是vbv_buffer_size,剩余大小是buffer_fill_final。如果有一帧(Frame)被编码进来,那么这个缓冲区的剩余大小buffer_fill_final,会有两次计算:

buffer_fill_final -= Frame.bits       
buffer_fill_final += Frame.buffer_rate

VBV的状态是动态的,当Frame被编码进来的时候,消耗了一定bits的内存来存储它。但在Frame存在的这段时间time内,又有bitrate * time的bits被传输走了。

了解这个模型,应该就明白RC是怎么回事了,它确实不包含任何真正的内存,仅仅是一种参考。

RC lookahead

然而大家最关心的,一定是:在码流控制的时候,QP是怎么选取的。

在进一步深入之前,先仔细想一想,若是让你来亲自实现这个模块,大概是这样一种:

事实上这个模型一点问题也没有,只是在具体实现的时候太粗暴了,因为每次重新选取一个QP,就需要把整个Frame重新编码,会消耗大量的计算资源。

x264 RC的实现方式,是把Frame拆成一行一行的MB,每次编码一行MB,如果编码出来的太大或者太小,就调整QP,然后重新编码。这样做虽好,但如果初始化的QP选择的不正确,还是会耗费了大量的运算资源一遍一遍的去试探,那有没有一种办法从一开始就选出合适的QP?

于是就有了lookahead运算,通过预测,提前得到一个合适的QP,这样可以有效的减少x264在正式编码阶段反复试探的消耗。

x264会编码出的I帧,P帧,B帧三种类型,I帧的size往往很大,P帧次之,B帧则很小。这里又衍生出另一个问题,在码流固定的情况下,怎么才能让B帧把自己用不掉的码流分配给I帧,否则会出现B帧的VBV资源过剩,而I帧的VBV资源很紧张的情况。

这里x264采用的平衡方式是,在lookahead阶段,不是预测一帧的大小,而是预测一组Frame的大小,它试图平衡足够长的Frame序列的码流,而不是一帧一帧的处理。

lookahead流程图如下:

具体实现过程:利用lookahead阶段计算的SATD值,预测出一组Frame的大小,让这一组Frame编码完毕之后,VBV的buffer_fill_final控制在50%-80%的范围之内。它尝试平衡的是一组Frame的码流,这样的话,I帧和B帧就能达到一种资源利用的平衡了。

  • 关于为什么要把buffer_fill_final控制在50%-80%内。

    B帧引用了I帧和P帧,受到I帧和P帧精度的影响,所以B帧的QP值必须比引用的I帧和P帧大(或者说,即便比I帧和P帧小,B帧也不会获得更好的显示图像)。基于这个事实,在lookahead预测的时候,B帧是没必要进行预测的,只对I帧和P帧进行预测。所以lookahead-RC预测完毕之后,紧接着就要开始编码P帧或者I帧了。

    由于I帧和P帧编码之后size都比较大,所以才需要预留足够的VBV空间。(这个预留的空间并不是给当前一组帧,而是预留给下一组帧的。)

在经过lookahead之后,我们就得到了一个初始的、比较合适的QP,这个QP能保证接下来一系列的编码Frame,整体的码流是合适的。

既然有了合适的QP,那么某一帧在编码的时候,我们就计划给它分配一定的VBV大小:frame_size_planned

frame_size_planned = predict_size( &rcc->pred[h->sh.i_type], q, rcc->last_satd );

predict_size()就是x264中所采用的的预测函数,它的输入参数有三个:帧的类型,QP,帧的SATD

只要每一帧的实际编码大小尽量往frame_size_planned上面靠拢,那么整体的码流就是我们期望的那样。

RC 正式编码阶段

但预测得到的frame_size_planned毕竟只是一种预测,实际编码出来的大小,仍旧有出入。这时候,对于单个帧的编码,就需要一行一行的微调了。

x264采用的手段,是把Frame分割成一行一行的MB,每编码一行的MB,就判断一次。如果发现编码出的大小不合适,那么它就会通过降低\提高下一行MB的QP,尽量往frame_size_planned上去靠拢。(在某些极端的情况下,可能会导致已经编码的一行完全报废掉,并且重头开始编码,这种情况会导致计算量暴增。)

正式编码阶段,要考虑到多线程Frame编码的并行情况,所以每个编码线程都有一个独立的context,标注在下图中:

每个thread维护独立的context变量(在x264_ratecontrol_t结构体当中):

  • buffer_fill:当前线程可以使用的VBV空间大小

  • rc_tol: 当前Frame编码之后的实际大小和frame_size_planned的最大差距。其实就是实际大小---预测大小之间的差别容忍度。

  • frame_size_planned:在lookahead阶段预测的size,也是我们期望的size,最后的实际码流越靠近这个值,那么整体的码流就越稳定。

  • frame_size_estimated: 这是一个动态变量,因为x264每编码一行MB的数据就要重新评估当前的QP是否需要微调,所以这个值是不断变化的。它= 已经编码的MB + 还未编码MB的预测值。x264用这个值来和frame_size_planned比较,如果离frame_size_planned差距较大,那么x264就会调整下一行MB的QP,以期望接近frame_size_planned的大小。

  • buffer_rate:等于Frame停留在屏幕上的时间 * bitrate。VBV的是一种动态模型,一边有Frame输入进来,一边还会把数据通过网络发送出去。当Frame进来编码的时候,会占据Frame.bits的空间,同时VBV也会传输出去Frame.buffer_rate大小的数据。

通过这样一种动态模型,基本上就实现了码流的平稳分配。

x264 RC函数导图

让我们来看看,上面说的一些策略分部在x264源代码的哪些地方:

额外的一些细节:

RC中采用的预测模型

RC中根据frame->lowres的SATD值来预测出实际编码bits的大小,因为SATD和bits存在一种线性关系,所以可以用如下公式来表达:

\[bits = SATD*k+b \]

在x264中,采用的结构如下:

typedef struct
{
    float coeff_min;    //最小的k系数,防止k过小
    float coeff;        //对应上面公式中的系数k
    float count;        //count是一种累计的记录,因为这里的k和b是动态调整的,需要一边编码,一边用实际bits更新预测参数
    float decay;        //decay是衰减系数,默认是0.5,代表旧的数据占比重只有0.5
    float offset;       //对应公式中的常量系数b
} predictor_t;

预测函数的代码如下:

static float predict_size( predictor_t *p, float q, float var )
{
    //这里的var就是SATD
    //参数q代表QP的QStep
    return (p->coeff*var + p->offset) / (q*p->count);
}

几个predict结构:

RC相关的基础变量:

RateControl的几种模式:

X264_RC_CQP:恒定质量(constant QP),说白了就是固定了画面的QP值,所有Frame根据类型设定固定的QP值,也不需要做上面提到的繁杂的lookahead预测和计算。

    rc->qp_constant[SLICE_TYPE_P] = h->param.rc.i_qp_constant;
    rc->qp_constant[SLICE_TYPE_I] = x264_clip3( h->param.rc.i_qp_constant - rc->ip_offset + 0.5, 0, QP_MAX );
    rc->qp_constant[SLICE_TYPE_B] = x264_clip3( h->param.rc.i_qp_constant + rc->pb_offset + 0.5, 0, QP_MAX );

X264_RC_ABR(adaptive bitrate)X264_RC_CRF(constant rate factor)

这两个模式,在x264中的实现有一点奇怪,二者都是在get_qscale中获取初始的QP,然后把QP传递给了clip_qscale函数做后期调整。但问题是就出在clip_qscale这个后期处理函数上。clip_qscale这个函数才不管你是X264_RC_ABR还是X264_RC_CRF,它只是单纯的根据VBV来调整一组Frame的大小,使得这一组Frame的总共输入、输出的总和是0(真实情况不可能是0,但程序设计上,最完美的当然是输入和输出完美平衡,这样码流才足够的稳定),故而不管get_qscale中以怎样的策略选出初始的QP,一旦走到clip_qscale函数中,二者的值都会被调整的十分接近,甚至变成同一个值。

所以如果想要这两种模式有区别,只能是关闭VBV的情况下才能体现出来。

  • X264_RC_ABR

    ​ 我先要说的是,这是个古怪的模式。因为在x264的代码中,你会看到X264_RC_ABR的QP调整策略是:根据当前使用掉的bits数目和最大码流的比例,进而来调整QP,简单来说,如果当前输出的码流比你期望的码流低,那就降低QP,把剩余的码流利用起来。而如果当前码流过高,就提高QP的值。

    ​ 看起来一点毛病也没有,甚至有平稳码流的目的,可实际造成的结果却是造成了码流波动的更加剧烈,一点也不平稳。具体的原因是,x264在编码的时候,会输出类似这种帧序列:I、B、B、B、P、B.....,其中I帧的size非常大,远远比B和P帧要大许多。这就出现了一种奇怪的现象,在编码I帧之后,使用的bits会很多,这时候x264就会尝试提高QP,可这个提高的QP并没有作用到I帧身上,而是作用到了接下来几个倒霉的P帧身上(B帧不会预测QP,而是从ref帧上面继承QP)。然后,因为P帧的size很小,在编码了一串B和P帧之后,刚好碰到了I帧,而这个时候因为之前B、P帧编码很小,所以QP又被x264降低了很多,这就进一步抬高了I帧的size。

    ​ 最后导致的结果,就是I帧变得比平时更大,而P帧本来就很小了但却变得比平时更小,出现了一种极端化的现象,进而导致了码流波动剧烈。(这种设计,又是否是x264想要的结果?或者设计程序的人也没预料到会这样吧……)

  • X264_RC_CRF:

​ Constant rate factor,意思是每一帧的QP都用一个固定的比例值factor来修正(修正的参数由用户来指定,算是一种宏观调整QP的策略),是个中规中矩的模式,只是它并不考虑“想办法利用所有的网络传输码流”这件事。

  1. 比起X264_RC_CQP(constant QP)X264_RC_CRF会兼顾SATD值,期望每一帧都得到公平的码流分配,但也仅仅是希望大家公平而已,X264_RC_CRF并不在乎网络传输码流有没有被有效利用起来。

    就好比家中有一大袋米,几个兄弟(帧)来分配,于是X264_RC_CRF给每个兄弟抓了一把,然后告诉众兄弟:“去吃吧,大家都是一样的数量。”众兄弟已经快要饿死,看着剩下的一大袋米没人食用,不由得露出一副莫名之色……

  2. X264_RC_ABR则一个“试图利用起所有的网络传输码流,但却把一切弄得很不稳定”的家伙。

也正是因为这样,所以X264_RC_ABR(adaptive bitrate)X264_RC_CRF(constant rate factor)这两种模式的后端,都紧跟着一个clip_qscale(不管什么模式,都试图利用所有网络传输码流的函数),只不过当clip_qscale起作用的时候,选择哪一种模式似乎也并不重要了。

后记,关于预测模型的一些细节

在这里要重点说的,是两个参数,也是初学者容易迷惑的两个东西。

h->param.rc.f_ip_factor  // 从I帧的QStep值,得到成P帧QStep值的一个比例参数,QStep = qp2qscale(QP)
h->param.rc.f_pb_factor  // 从P帧的QStep值,得到成B帧QStep值的一个比例参数,QStep = qp2qscale(QP)

初看代码的人,最容易被这两个东西搞迷糊了:怎么I帧和P帧、B帧之间的关系是固定的?那既然这样,只需要得到第一个I帧的QP值,就可以把后面的QP值一起都算出来了,还搞那么多预测模型干什么?

这么理解就错了,因为这两个参数是为了预测模型服务的,并非用来确定正式的QP值。

  • 针对B帧,因为B帧不会进行预测QP的分析,所以可以用上面的比例值,从相邻的P帧或者I帧换算出来,但也仅仅是B帧。

  • 可对于I、P两种类型,它们都是要进行lookahead预测的,需要把当前帧的size、后续的一串帧的size都累加起来,来评估大小是否合适。但显然这些帧的size我们并不知道,只能估算帧的大小。按照上面的提到的predict_size函数,是需要知道QP值的。于是我们就按照这个比例关系,按照帧的类型给后续的帧分别赋予一定的QP,然后就能顺利的预测出一组Frame的大小了。

这两个值为预测模型服务,然后预测模型给每一个被预测的帧算出frame_size_planned,最后frame_size_planned会作用到正式编码阶段,进而影响到实际的编码大小。故而这两个值是一种期望,期望最后的QStep比例关系是这样的,但最后的实际QP值还需要根据具体的Frame进行调整。

posted @ 2021-07-17 15:07  十方云山  阅读(2585)  评论(5编辑  收藏  举报