【x264】码率控制模块的简单分析—帧级码控策略

2024-06-06 20:04

本文主要是介绍【x264】码率控制模块的简单分析—帧级码控策略,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

【x264】码率控制模块的简单分析—帧级码控策略

  • 1.码率控制模式
  • 2.恒定量化参数(Constant Quantization Parameter, CQP)
    • 2.1 CQP初测
    • 2.2 CQP的实现
    • 2.3 CQP存在的问题
  • 3.恒定质量因子(Constant Ratefactor, CRF)
    • 3.1 CRF初测
    • 3.2 CRF的实现
      • 3.2.1 mbtree
      • 3.2.2 qcompress
      • 3.2.3 rf_constant
    • 3.3 CRF小结
  • 4.平均码率(Average Bitrate, ABR)
    • 4.1 ABR和VBV初测
    • 4.2 ABR实现过程
      • 4.2.1 计算当前帧的SATD
      • 4.2.2 利用SATD计算当前帧的图像模糊度blurred_complexity
      • 4.2.3 根据图像模糊度计算qscale
      • 4.2.4 qscale和qp的互相转换
      • 4.2.5 根据qscale获取预测编码比特数
      • 4.2.6 预测器参数更新
    • 4.3 视频缓冲校验(Video Buffer Verifier,VBV)
    • 4.4 VBV实现流程
      • 4.4.1 VBV参数的初始化
      • 4.4.2 计算vbv的输入速率(update_vbv_plan)
      • 4.4.3 添加VBV限制(vbv_pass1)
      • 4.4.4 行级VBV控制(x264_ratecontrol_mb)
    • 4.5 ABR小结
  • 5.小结

参考:
【x264编码器】章节3——x264的码率控制
X264码率控制二(vbv码率控制)
x264 vbv-maxrate与vbv-bufsize对码率控制

参数分析:
【x264】x264编码器参数配置

流程分析:
【x264】x264编码主流程简单分析
【x264】编码核心函数(x264_encoder_encode)的简单分析
【x264】分析模块(analyse)的简单分析—帧内预测
【x264】分析模块(analyse)的简单分析—帧间预测
【x264】码率控制模块的简单分析—宏块级码控工具Mbtree和AQ

1.码率控制模式

在x264当中,默认定义了三种帧级别码控方法,分别是CQP、CRF和ABR

#define X264_RC_CQP                  0	// Constant Quanzation Parameter, CQP
#define X264_RC_CRF                  1	// Constant Ratefactor, CRF
#define X264_RC_ABR                  2	// Average Bitrate, ABR

这三者的含义分别是:

  1. X264_RC_CQP:恒定量化参数(Constant Quanzation Parameter,CQP)
  2. X264_RC_CRF:恒定量化因子(Constant Ratefactor,CRF)
  3. X264_RC_ABR:平均码率(Average Bitrate,ABR)

这三种模式是最基本的模式,其余模式如CBR、2pass是基于这三种模式衍生获得的。在进行码率控制之前,有一个普遍的认识或假设:
b i t s ∗ q s c a l e ∝ c o m p l e x i t y bits * qscale ∝ complexity bitsqscalecomplexity
其中bits为编码比特数,qscale为量化参数,complexity为编码复杂度。如果qscale恒定,编码内容越复杂,所需要的bit就越多;如果bit恒定,编码内容越复杂,qscale越高。后续各种编码模式都是基于这个假设进行设计的,这种正比例的关系可以通过大量实验的拟合确定对应的参数

2.恒定量化参数(Constant Quantization Parameter, CQP)

该模式下,会使用固定的qp参数,因为qp和qstep具有唯一对应关系,所以qstep也会固定,而qstep直接影响了量化过程。此时,如果视频场景发生了较大变化,由于qscale是固定的,所以瞬时的码率也会有较大的波动,一般不会使用该模式。264标准中,qp范围为[0, 51]。qstep影响量化过程的方式为:
Z i j = r o u n d ( Y i j Q s t e p ) Z_{ij}=round(\frac{Y_{ij}}{Q_{step}}) Zij=round(QstepYij)
其中, Z i j Z_{ij} Zij表示的是输出的量化系数, Y i j Y_{ij} Yij表示的是待量化的转换系数矩阵, Q _ s t e p Q\_step Q_step是量化步长。如果qp增大,则qstep也会增大, 此时输出的量化系数会偏小,则量化精度偏低

2.1 CQP初测

cqp模式的配置位于base.c文件中的x264_param_parse函数,如果设置了qp参数,则会配置为cqp模式,并且将输入的qp传递下去

    OPT2("qp", "qp_constant"){p->rc.i_qp_constant = atoi(value);p->rc.i_rc_method = X264_RC_CQP;}

设置编码参数 --qp 32,测试序列分辨率为1920x1200,帧数100,其余使用默认参数,测试结果为

yuv [info]: 1920x1200p 0:0 @ 25/1 fps (cfr)
x264 [info]: using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
x264 [info]: profile High, level 5.0, 4:2:0, 8-bit
x264 [info]: frame I:7     Avg QP:29.00  size:126289
x264 [info]: frame P:39    Avg QP:32.00  size: 64496
x264 [info]: frame B:54    Avg QP:33.67  size: 29591
x264 [info]: consecutive B-frames: 19.0% 18.0% 27.0% 36.0%
x264 [info]: mb I  I16..4: 28.2% 50.9% 21.0%
x264 [info]: mb P  I16..4: 16.8% 22.1% 10.0%  P16..4: 17.8% 10.7%  4.6%  0.0%  0.0%    skip:17.9%
x264 [info]: mb B  I16..4:  3.4%  3.8%  1.4%  B16..8: 35.5% 10.6%  2.3%  direct: 2.9%  skip:40.0%  L0:43.7% L1:49.6% BI: 6.8%
x264 [info]: 8x8 transform intra:46.3% inter:66.0%
x264 [info]: coded y,uvDC,uvAC intra: 49.3% 23.5% 3.6% inter: 21.2% 5.0% 0.0%
x264 [info]: i16 v,h,dc,p: 60% 24% 10%  6%
x264 [info]: i8 v,h,dc,ddl,ddr,vr,hd,vl,hu: 28% 18% 22%  5%  5%  4%  6%  4%  7%
x264 [info]: i4 v,h,dc,ddl,ddr,vr,hd,vl,hu: 41% 18% 12%  4%  5%  5%  6%  4%  5%
x264 [info]: i8c dc,h,v,p: 78% 12% 10%  1%
x264 [info]: Weighted P-Frames: Y:33.3% UV:17.9%
x264 [info]: ref P L0: 67.1% 18.4% 10.6%  3.1%  0.8%
x264 [info]: ref B L0: 94.1%  4.7%  1.2%
x264 [info]: ref B L1: 98.6%  1.4%
x264 [info]: kb/s:9994.51
encoded 100 frames, 10.88 fps, 9994.51 kb/s

可以看出编码的p帧qp固定为32,intra帧和b帧的qp有出入,其中intra帧的qp较小,而b帧的qp略大

2.2 CQP的实现

cqp的调控位于encoder.c中,函数为validate_parameters,其中p帧的qp为外部输入的qp,intra帧的qp需要减去一个intra帧传递到p帧的一个传递因子系数,b帧的qp需要减去一个p帧到b帧的传递因子系数。如果设置的p帧的qp为32,则根据计算公式可以得出intra帧的qp为i_qp=32-6log2(1.4)=29.087439,b_qp=32+6log2(1.3)= 34.271069,与真实编码qp接近,但有出入,可能是后续做了如clip或者四舍五入等操作带来的误差

static int validate_parameters( x264_t *h, int b_open )
{// ...if( h->param.rc.i_rc_method == X264_RC_CQP ){float qp_p = h->param.rc.i_qp_constant;					// 将输入的qp设置为P帧编码qpfloat qp_i = qp_p - 6*log2f( h->param.rc.f_ip_factor ); // 减去一个i帧到p帧的传递因子,默认值为1.4float qp_b = qp_p + 6*log2f( h->param.rc.f_pb_factor ); // 减去一个p帧到b帧的传递因子,默认值为1.3if( qp_p < 0 ){x264_log( h, X264_LOG_ERROR, "qp not specified\n" );return -1;}h->param.rc.i_qp_min = x264_clip3( (int)(X264_MIN3( qp_p, qp_i, qp_b )), 0, QP_MAX );h->param.rc.i_qp_max = x264_clip3( (int)(X264_MAX3( qp_p, qp_i, qp_b ) + .999), 0, QP_MAX );h->param.rc.i_aq_mode = 0;h->param.rc.b_mb_tree = 0;h->param.rc.i_bitrate = 0;}// ...
}

2.3 CQP存在的问题

cqp模式通过设定一个固定的qp实现编码,这种模式完全不考虑场景的复杂度变化,也没有考虑到码流大小,因此这种模式在实际应用中几乎不存在。如果是进行一些算法的测试,可以先通过设置cqp模式,通过设置一个固定的qp来获得一个目标码率,然后以这个目标码率作为基准,去进行其他模式的测试。例如设定qp为27,获得一个目标码率target_bitrate,然后将target_bitrate作为目标码率,来衡量qp为27条件下码控算法的控制精度

3.恒定质量因子(Constant Ratefactor, CRF)

crf是x264默认的码控模式,会使用固定的质量因子crf而不是固定qp。crf模式关注的是编码输出的质量保持恒定,不关注输出的编码码率,该模式下的码率只与视频纹理复杂度有关系。对于复杂度较高的区域,crf会提高编码qp,减小码流,对于复杂度低的区域,crf会降低qp,提高编码质量,这样能够保持整体视频质量比较稳定。但是,crf一个比较明显的缺点是无法精确的控制输出文件的大小,因此不推荐应用于流媒体视频传输

3.1 CRF初测

crf模式的配置由参数crf确定,如果配置了crf参数,则会将码控模式设置为CRF。配置仍然位于base.c中的x264_param_parse函数

OPT("crf")
{p->rc.f_rf_constant = atof(value);p->rc.i_rc_method = X264_RC_CRF;
}

配置时,设置–crf 32

yuv [info]: 1920x1200p 0:0 @ 25/1 fps (cfr)
x264 [info]: using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
x264 [info]: profile High, level 5.0, 4:2:0, 8-bit
x264 [info]: frame I:8     Avg QP:32.96  size: 58744
x264 [info]: frame P:39    Avg QP:36.09  size: 29211
x264 [info]: frame B:53    Avg QP:37.07  size: 14952
x264 [info]: consecutive B-frames: 19.0% 22.0% 27.0% 32.0%
x264 [info]: mb I  I16..4: 26.5% 56.9% 16.6%
x264 [info]: mb P  I16..4: 12.8% 25.3%  3.9%  P16..4: 24.0%  7.0%  2.5%  0.0%  0.0%    skip:24.5%
x264 [info]: mb B  I16..4:  2.8%  3.5%  0.4%  B16..8: 39.0%  5.1%  0.5%  direct: 1.8%  skip:46.9%  L0:48.3% L1:50.2% BI: 1.5%
x264 [info]: 8x8 transform intra:58.2% inter:81.3%
x264 [info]: coded y,uvDC,uvAC intra: 35.7% 15.9% 0.9% inter: 11.5% 2.7% 0.0%
x264 [info]: i16 v,h,dc,p: 59% 27%  7%  8%
x264 [info]: i8 v,h,dc,ddl,ddr,vr,hd,vl,hu: 30% 18% 29%  4%  4%  3%  5%  3%  5%
x264 [info]: i4 v,h,dc,ddl,ddr,vr,hd,vl,hu: 37% 22% 15%  5%  5%  4%  6%  3%  4%
x264 [info]: i8c dc,h,v,p: 85%  8%  6%  0%
x264 [info]: Weighted P-Frames: Y:23.1% UV:12.8%
x264 [info]: ref P L0: 72.2% 13.8%  9.5%  3.7%  0.7%
x264 [info]: ref B L0: 92.5%  6.0%  1.5%
x264 [info]: ref B L1: 98.5%  1.5%
x264 [info]: kb/s:4803.32
encoded 100 frames, 13.21 fps, 4803.32 kb/s

3.2 CRF的实现

在crf的实现过程中,使用x264_ratecontrol_init_reconfigurable来计算rate_factor_constant,依据的内容包括mb的数量,是否使用b帧,是否使用mbtree以及qcompress。因为涉及的参数在编码过程中不会变化,所以rate_factor_constant计算之后是一个定值

void x264_ratecontrol_init_reconfigurable( x264_t *h, int b_init ) 
{// ...if( h->param.rc.i_rc_method == X264_RC_CRF ){/* Arbitrary rescaling to make CRF somewhat similar to QP.* Try to compensate for MB-tree's effects as well. */double base_cplx = h->mb.i_mb_count * (h->param.i_bframe ? 120 : 80);// mb_tree默认为1,f_qcompress默认为0.6double mbtree_offset = h->param.rc.b_mb_tree ? (1.0-h->param.rc.f_qcompress)*13.5 : 0;// #define QP_BD_OFFSET (6*(BIT_DEPTH-8)),如果使用的是8bit,则为0rc->rate_factor_constant = pow( base_cplx, 1 - rc->qcompress )/ qp2qscale( h->param.rc.f_rf_constant + mbtree_offset + QP_BD_OFFSET );}// ...
}

rate_factor_constant的计算公式为:
r a t e _ f a c t o r _ c o n s t a n t = p o w ( b a s e _ c p l x , 1 − q c o m p r e s s ) q p 2 q s c a l e ( r f _ c o n s t a n t + m b t r e e _ o f f s e t + Q P _ B D _ O F F S E T ) rate\_factor\_constant = \frac{pow(base\_cplx, 1 - qcompress)}{qp2qscale(rf\_constant + mbtree\_offset + QP\_BD\_OFFSET)} rate_factor_constant=qp2qscale(rf_constant+mbtree_offset+QP_BD_OFFSET)pow(base_cplx,1qcompress)
rate_factor_constant的使用在get_qscale中,输入的rate_factor会被用于调整qscale

/*** modify the bitrate curve from pass1 for one frame*/
static double get_qscale(x264_t *h, ratecontrol_entry_t *rce, double rate_factor, int frame_num)
{x264_ratecontrol_t *rcc= h->rc;x264_zone_t *zone = get_zone( h, frame_num );double q;/*对于crf模式而言(1)如果使用mbtree,则基于duration对qscale进行调整;此时,crf模式与纹理复杂度的关联由mbtree考量mbtree会考虑前后帧的mb的重要程度,将纹理复杂度考虑在了其中(2)如果不使用mbtree,则在这里使用纹理复杂度对qscale进行调整*/if( h->param.rc.b_mb_tree ){double timescale = (double)h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale;q = pow( BASE_FRAME_DURATION / CLIP_DURATION(rce->i_duration * timescale), 1 - h->param.rc.f_qcompress );}elseq = pow( rce->blurred_complexity, 1 - rcc->qcompress );// avoid NaN's in the rc_eqif( !isfinite(q) || rce->tex_bits + rce->mv_bits == 0 )q = rcc->last_qscale_for[rce->pict_type];else{rcc->last_rceq = q;q /= rate_factor; // 如果使用crf模式,这里的rate_factor就是rate_factor_constantrcc->last_qscale = q;}// ...return q;
}

这里涉及到了3个概念,分别是mbtree、qcompress和rf_constant:

3.2.1 mbtree

宏块树mbtree是一种宏块级别的码率控制工具,能有效的降低编码的码率。mbtree的工作原理可以简单描述为,在lookahead当中通过前向预测,来获得当前mb的重要程度,重要程度由被参考的情况来衡量。如果被参考的权值很高,说明这个mb很重要,此时这个mb会按照较低的qp编码,否则按照较高的qp编码。这个权值的衡量由当前mb的intra_cost、inter_cost和前面mb赋予当前mb的传播cost决定。mbtree记录在 【x264】码率控制模块的简单分析—宏块级码控工具Mbtree和AQ

3.2.2 qcompress

qcompress在x264的介绍为

H2( "      --qcomp <float>         QP curve compression [%.2f]\n", defaults->rc.f_qcompress );

这是一个控制压缩质量的参数,用于调控宏块的量化步长,qcompress越高则视频质量越低,码率越小

3.2.3 rf_constant

在x264中,rf_constant是crf模式下调控qscale的一个参数,它是一个浮点数,其取值范围一般在0.0到51.0之间。rf_constant的值越小,编码的质量越好。rf_constant=18被认为是视觉上无损的,默认配置为23,可以外部配置。若Crf值加6,输出码率大概减少一半;若Crf值减6,输出码率翻倍。通常是在保证可接受视频质量的前提下选择一个最大的crf值,如果输出视频质量很好,可以尝试增大rf的值,否则就尝试减小rf的值

3.3 CRF小结

在crf模式当中,确定了一个固定的质量因子rate_factor_constant,保证编码的视频质量,但是由于编码的内容有波动,所以无法保证编码的码率。在确定了rate_factor之后,crf与视频复杂度的关联与mbtree有关,如果使用mbtree,则利用mbtree来考量complexity并更新qscale,否则利用blurred_complexity来更新qscale。对于一些对输出码率没有要求的应用场景,crf能够很好的保证输出质量的稳定性

4.平均码率(Average Bitrate, ABR)

平均码率abr通过给定一个码率的平均值,牺牲一定的编码质量来提供比crf更稳定的码率控制,让码率尽量维持在所设定的值附近。但是,由于编码器不知道未来的编码内容是什么,需要去猜测如何达到这个平均码率,瞬时码率会随着场景复杂度而变化,这种波动尽管会受到目标码率的约束,但有可能在短时间内引起编码质量的波动。我的理解是,尽管质量和码率的变化呈一定的相关性,但不是完全匹配(可能码率提升不足,或者降低太多),这种不匹配程度来源于编码器不知道未来的编码信息,进而导致质量波动。因此,在实际使用时,为了实现精准的码控,常使用VBV(Video Buffer Verifier,视频缓冲校验器)进行辅助

4.1 ABR和VBV初测

x264中,默认的码控模式为crf,当且仅当配置了bitrate,会使用abr模式;如果配置了bitrate,还配置了vbv的参数,则会加上vbv的控制,这里只配置bitrate参数

OPT("bitrate")
{p->rc.i_bitrate = atoi(value);p->rc.i_rc_method = X264_RC_ABR;
}

延续前面使用CRF时的码率4803 kb/s,配置项为–bitrate 4803(两个小短线),测试结果为

yuv [info]: 1920x1200p 0:0 @ 25/1 fps (cfr)
x264 [info]: using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
x264 [info]: profile High, level 5.0, 4:2:0, 8-bit
x264 [info]: frame I:8     Avg QP:30.66  size: 85319
x264 [info]: frame P:39    Avg QP:36.83  size: 26646
x264 [info]: frame B:53    Avg QP:37.22  size: 15218
x264 [info]: consecutive B-frames: 19.0% 22.0% 27.0% 32.0%
x264 [info]: mb I  I16..4: 22.2% 60.4% 17.4%
x264 [info]: mb P  I16..4: 12.1% 25.0%  3.2%  P16..4: 25.0%  6.3%  2.2%  0.0%  0.0%    skip:26.1%
x264 [info]: mb B  I16..4:  2.7%  3.4%  0.4%  B16..8: 39.5%  5.0%  0.5%  direct: 1.8%  skip:46.8%  L0:50.0% L1:47.9% BI: 2.1%
x264 [info]: final ratefactor: 31.44
x264 [info]: 8x8 transform intra:60.4% inter:81.7%
x264 [info]: coded y,uvDC,uvAC intra: 37.8% 17.2% 1.2% inter: 11.3% 2.6% 0.0%
x264 [info]: i16 v,h,dc,p: 61% 25%  7%  8%
x264 [info]: i8 v,h,dc,ddl,ddr,vr,hd,vl,hu: 29% 18% 29%  4%  4%  3%  5%  3%  5%
x264 [info]: i4 v,h,dc,ddl,ddr,vr,hd,vl,hu: 36% 22% 15%  5%  5%  4%  6%  3%  4%
x264 [info]: i8c dc,h,v,p: 84%  9%  7%  0%
x264 [info]: Weighted P-Frames: Y:23.1% UV:12.8%
x264 [info]: ref P L0: 72.6% 12.9%  9.7%  4.0%  0.7%
x264 [info]: ref B L0: 92.4%  6.0%  1.6%
x264 [info]: ref B L1: 98.5%  1.5%
x264 [info]: kb/s:5056.61
encoded 100 frames, 13.37 fps, 5056.61 kb/s

实际编码码率和目标码率误差较大(200kb/s),猜测一个主要的原因是序列存在几个突然的场景变化,而abr模式在这种情况下码控误差较大。现在加上vbv的控制参数,–bitrate 4803 --vbv-maxrate 5000 --vbv-bufsize 2000,测试的结果为

x264 [info]: using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
x264 [info]: profile High, level 5.0, 4:2:0, 8-bit
x264 [info]: frame I:8     Avg QP:33.28  size: 56388
x264 [info]: frame P:39    Avg QP:36.01  size: 29846
x264 [info]: frame B:53    Avg QP:37.09  size: 14805
x264 [info]: consecutive B-frames: 19.0% 22.0% 27.0% 32.0%
x264 [info]: mb I  I16..4: 26.5% 57.7% 15.7%
x264 [info]: mb P  I16..4: 12.8% 25.6%  4.0%  P16..4: 23.7%  6.8%  2.5%  0.0%  0.0%    skip:24.5%
x264 [info]: mb B  I16..4:  2.9%  3.5%  0.4%  B16..8: 39.0%  5.0%  0.5%  direct: 1.7%  skip:47.1%  L0:47.4% L1:51.1% BI: 1.5%
x264 [info]: 8x8 transform intra:58.4% inter:80.9%
x264 [info]: coded y,uvDC,uvAC intra: 36.0% 16.2% 0.9% inter: 11.3% 2.8% 0.0%
x264 [info]: i16 v,h,dc,p: 60% 26%  7%  8%
x264 [info]: i8 v,h,dc,ddl,ddr,vr,hd,vl,hu: 30% 18% 28%  4%  4%  3%  5%  3%  5%
x264 [info]: i4 v,h,dc,ddl,ddr,vr,hd,vl,hu: 36% 22% 15%  5%  5%  4%  6%  3%  4%
x264 [info]: i8c dc,h,v,p: 85%  8%  7%  0%
x264 [info]: Weighted P-Frames: Y:23.1% UV:12.8%
x264 [info]: ref P L0: 72.8% 13.8%  9.0%  3.7%  0.7%
x264 [info]: ref B L0: 92.6%  5.7%  1.6%
x264 [info]: ref B L1: 98.5%  1.5%
x264 [info]: kb/s:4799.55
encoded 100 frames, 13.04 fps, 4799.55 kb/s

加上了vbv的控制之后,码率控制比较精准。这里vbv参数的选择参考x264 vbv-maxrate与vbv-bufsize对码率控制,但是经过测试发现也不是那么精准,例如选择–bitrate 4803 --vbv-maxrate 5000 --vbv-bufsize 6000时,bitrate误差反而增大了,这应该与序列有关,还是需要根据实际情况调整

4.2 ABR实现过程

ABR的实现流程为:

  1. 计算当前帧的SATD(x264_rc_analyse_slice)
  2. 利用SATD计算当前帧的图像模糊度blurred_complexity(rate_estimate_qscale)
  3. 根据图像模糊度计算qscale(get_qscale)
  4. qscale和qp互相转换(流程中可能会用到,即便不用也是码控的核心)(qp2qscale、qscale2qp)
  5. 根据qscale获取预测编码比特数(predict_size)
  6. 预测器参数更新(update_predictor)

4.2.1 计算当前帧的SATD

SATD是通过一行一行的方式进行计算的,调用函数x264_rc_analyse_slice实现

4.2.2 利用SATD计算当前帧的图像模糊度blurred_complexity

b l u r r e d _ c o m p l e x i t y [ i ] = c p l x s u m [ i − 1 ] ∗ 0.5 + S A T D [ i ] c p l x c o u n t [ i − 1 ] ∗ 0.5 + 1 blurred\_complexity[i] = \frac{cplxsum[i-1] * 0.5 + SATD[i]}{cplxcount[i-1] * 0.5 + 1} blurred_complexity[i]=cplxcount[i1]0.5+1cplxsum[i1]0.5+SATD[i]
即当前的模糊度由前面帧的模糊度和当前帧的SATD组合而来
上述流程的实现过程位于ratecontrol.c中,函数rate_estimate_qscale之下

static float rate_estimate_qscale( x264_t *h )
{// ...{ // 1pass ABR/* Calculate the quantizer which would have produced the desired* average bitrate if it had been applied to all frames so far.* Then modulate that quant based on the current frame's complexity* relative to the average complexity so far (using the 2pass RCEQ).* Then bias the quant up or down if total size so far was far from* the target.* Result: Depending on the value of rate_tolerance, there is a* tradeoff between quality and bitrate precision. But at large* tolerances, the bit distribution approaches that of 2pass. */// 计算量化因子,如果该因子已经被用于之前的所有帧,则该因子能够生成期望的平均码率// 然后,根据当前帧的复杂度和之前所有帧的平均复杂度的比值来修正量化参数// 随后,如果总体大小远离目标大小,则将量化因子增大或者减小// 结果:根据码率容忍的误差值,在质量和码率控制精度之前有一个tradeoff;如果容忍度很大,则比特的分布接近2passdouble wanted_bits, overflow = 1;// ----- 1.计算当前帧的复杂度satd,按照一行计算 ----- //rcc->last_satd = x264_rc_analyse_slice( h );rcc->short_term_cplxsum *= 0.5;	// cplxsum为累积复杂度,由之前帧的cplx和当前帧的SATD组合而成rcc->short_term_cplxcount *= 0.5;	// cplxcount为统计帧数// cplxsum = cplxsum * 0.5 + SATDrcc->short_term_cplxsum += rcc->last_satd / (CLIP_DURATION(h->fenc->f_duration) / BASE_FRAME_DURATION);// cplxcount = cplxcount * 0.5 + 1rcc->short_term_cplxcount ++;rce.tex_bits = rcc->last_satd;// ----- 2.计算blured_complexity ----- //rce.blurred_complexity = rcc->short_term_cplxsum / rcc->short_term_cplxcount;rce.mv_bits = 0;rce.p_count = rcc->nmb;rce.i_count = 0;rce.s_count = 0;rce.qscale = 1;rce.pict_type = pict_type;rce.i_duration = h->fenc->i_duration;// ----- 3.根据图像模糊度计算qscale ----- //// 有可能使用CRF模式if( h->param.rc.i_rc_method == X264_RC_CRF ){q = get_qscale( h, &rce, rcc->rate_factor_constant, h->fenc->i_frame );}else{	// 使用ABR模式// rc->wanted_bits_window = rc->bitrate / fps (初始值),表示目标编码比特q = get_qscale( h, &rce, rcc->wanted_bits_window / rcc->cplxr_sum, h->fenc->i_frame );/* ABR code can potentially be counterproductive in CBR, so just don't bother.* Don't run it if the frame complexity is zero either. */if( !rcc->b_vbv_min_rate && rcc->last_satd ){// FIXME is it simpler to keep track of wanted_bits in ratecontrol_end?int i_frame_done = h->i_frame;				// 已编码的帧数double time_done = i_frame_done / rcc->fps; // 编码的时长if( h->param.b_vfr_input && i_frame_done > 0 )time_done = ((double)(h->fenc->i_reordered_pts - h->i_reordered_pts_delay)) * h->param.i_timebase_num / h->param.i_timebase_den;wanted_bits = time_done * rcc->bitrate;		// 计算已经编码的帧的bits大小if( wanted_bits > 0 ){abr_buffer *= X264_MAX( 1, sqrt( time_done ) );// 通过计算已编码比特数和预测编码比特数的差异来判断是否出现了上下溢,并使用这个比率来调整qscaleoverflow = x264_clip3f( 1.0 + (predicted_bits - wanted_bits) / abr_buffer, .5, 2 );q *= overflow;}}// ...
}

4.2.3 根据图像模糊度计算qscale

在计算qscale时,会进行qscale的两次调整
第一次调整:
q s c a l e = { ( B A S E F R A M E _ D U R A T I O N C L I P _ D U R A T I O N ( d u r a t i o n ∗ t i m e s c a l e ) ) 1 − q c o m p r e s s , 如果开启 m b t r e e ( 1 − b l u r r e d _ c o m p l e x i t y ) 1 − q c o m p r e s s , 如果不开启 m b t r e e qscale= \left\{\begin{matrix} (\frac{BASEFRAME\_DURATION}{CLIP\_DURATION(duration * timescale)})^{1-qcompress}, 如果开启mbtree \\ (1-blurred\_complexity)^{1-qcompress}, 如果不开启mbtree \end{matrix}\right. qscale={(CLIP_DURATION(durationtimescale)BASEFRAME_DURATION)1qcompress,如果开启mbtree(1blurred_complexity)1qcompress,如果不开启mbtree

第二次调整:
q s c a l e = q s c a l e r a t e _ f a c t o r , r a t e _ f a c t o r = w a n t e d _ b i t s _ w i n d o w c p l x s u m qscale = \frac{qscale}{rate\_factor},rate\_factor=\frac{wanted\_bits\_window}{cplxsum} qscale=rate_factorqscalerate_factor=cplxsumwanted_bits_window
wanted_bits_window默认值为
w a n t e d _ b i t s _ w i n d o w = b i t r a t e f p s wanted\_bits\_window=\frac{bitrate}{fps} wanted_bits_window=fpsbitrate
其中bitrate是由外部设置,wanted_bits_window是动态更新的

调用get_qscale对qscale进行调整的具体方式如下

/*** modify the bitrate curve from pass1 for one frame*/
static double get_qscale(x264_t *h, ratecontrol_entry_t *rce, double rate_factor, int frame_num)
{x264_ratecontrol_t *rcc= h->rc;x264_zone_t *zone = get_zone( h, frame_num );double q;// ---- 4.调整qscale ---- //// 如果使用了mbtree(默认使用),则使用时间信息来调整qscaleif( h->param.rc.b_mb_tree ){double timescale = (double)h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale;q = pow( BASE_FRAME_DURATION / CLIP_DURATION(rce->i_duration * timescale), 1 - h->param.rc.f_qcompress );}else // 否则使用图像模糊度来调整qscaleq = pow( rce->blurred_complexity, 1 - rcc->qcompress );// avoid NaN's in the rc_eqif( !isfinite(q) || rce->tex_bits + rce->mv_bits == 0 )q = rcc->last_qscale_for[rce->pict_type];else{rcc->last_rceq = q;// 这里传递进来的rate_factor是wanted_bits_window/cplxr_sumq /= rate_factor; rcc->last_qscale = q;}// ...
}

4.2.4 qscale和qp的互相转换

static inline float qscale2qp( float qscale )
{return (12.0f + QP_BD_OFFSET) + 6.0f * log2f( qscale/0.85f );
}

反过来,qp也可以转换成qscale

static inline float qp2qscale( float qp )
{return 0.85f * powf( 2.0f, ( qp - (12.0f + QP_BD_OFFSET) ) / 6.0f );
}

4.2.5 根据qscale获取预测编码比特数

这里就有一个疑惑,为什么不直接使用qp进行中间的计算而是使用qscale?我的理解是,在编码器中,qscale与编码的比特数有一个直接对应的关系,使用qscale能够更容易的根据视频内容的复杂度和目标比特率来调整编码参数,从而更有效地控制视频质量和文件大小。相反,如果使用qp的话,对应关系是非线性的,这样做中间计算会比较复杂,也容易产生较大的误差。根据qscale来预测使用的编码比特数的方式为

static float predict_size( predictor_t *p, float q, float var )
{return (p->coeff*var + p->offset) / (q*p->count);
}

预测编码比特数是按照一行进行的,其中q为qscale,var是复杂度complex(通常用SATD描述),p为预测器,p->coeff,p->offset和p->count都是float类型。这里将这些系数做一些简化,上述过程可以表示为
p r e d _ s i z e = c ∗ c o m p l e x q s c a l e pred\_size = \frac{c * complex}{qscale} pred_size=qscaleccomplex
即预测的比特数与纹理复杂度成正比,与qscale成反比,这里简化的系数c在编码过程中也是动态更新的

4.2.6 预测器参数更新

参数更新的函数为update_predictor,利用bits、qscale和satd(var变量)来更新coeff和offset,更新的方式为每行更新一次。其中,bits*qscale与complexity成正比,可以理解为是前面一行编码的复杂度,因此new_coeff的更新理解是前面一行编码复杂度和当前一行编码复杂度的比值,new_offset是前面一行的复杂度和当前行复杂度的偏移量。

static void update_predictor( predictor_t *p, float q, float var, float bits )
{float range = 1.5;if( var < 10 )return;float old_coeff = p->coeff / p->count;float old_offset = p->offset / p->count;// new_coeff = (前一行复杂度 - old_offset) / 当前行复杂度(satd表示) float new_coeff = X264_MAX( (bits*q - old_offset) / var, p->coeff_min );float new_coeff_clipped = x264_clip3f( new_coeff, old_coeff/range, old_coeff*range );// new_offset = 前一行复杂度 - new_coeff * 当前行复杂度float new_offset = bits*q - new_coeff_clipped * var;if( new_offset >= 0 )new_coeff = new_coeff_clipped;elsenew_offset = 0;// 乘以一个衰减系数p->decay,默认为0.5p->count  *= p->decay;p->coeff  *= p->decay;p->offset *= p->decay;p->count  ++;p->coeff  += new_coeff;p->offset += new_offset;
}

4.3 视频缓冲校验(Video Buffer Verifier,VBV)

在前面的测试当中看到,使用ABR模式可能会带来码率控制不准确的情况,因此提出了VBV的控制思想。VBV是为了解决ABR模式控制码率不准确,视频质量波动较大的一项工具,通过为码率设定一个限制,使得码率波动在一定的范围之内,从而有效的控制码率,如果应用了VBV,则当前的编码模式可以理解为是CBR,即恒定码率(Const Bitrate)。这个限制的过程是一个Buffer,用于控制接收端缓存不出现上下溢的情况,实现了对视频短时码率的限制。

如果用一段话来描述VBV这一概念,我想可以这么来说:VBV是一种控制机制,它创建了一个缓冲池,无论输入的水流有多大,总能够保持池中的水处于一个相对稳定状态。如果输入的水流很大,则加大流出的水量,如果输入的水流很小,则减小流出的水量。这样操作的结果是,池中的水在一定范围内进行波动,输出的水量也在一定范围内波动,最终实现稳定的码率控制

4.4 VBV实现流程

VBV的实现流程为:

  1. VBV参数的初始化(x264_ratecontrol_init_reconfigurable)
  2. 计算VBV的输入速率(update_vbv_plan)
  3. 添加VBV限制(vbv_pass1)
  4. 行级VBV控制(x264_ratecontrol_mb)

4.4.1 VBV参数的初始化

VBV参数的初始化位于x264_ratecontrol_init_reconfigurable之中,代码为

void x264_ratecontrol_init_reconfigurable( x264_t *h, int b_init )
{// ...if( h->param.rc.i_vbv_max_bitrate > 0 && h->param.rc.i_vbv_buffer_size > 0 ){/* We don't support changing the ABR bitrate right now,so if the stream starts as CBR, keep it CBR. *//*b_vbv_min_rate表示1pass ABR模式下,设置的水池最大的输入码率小于目标码率rc->b_vbv_min_rate = !rc->b_2pass                                        // 不是2pass&& h->param.rc.i_rc_method == X264_RC_ABR                  // 使用ABR模式&& h->param.rc.i_vbv_max_bitrate <= h->param.rc.i_bitrate; // 最大的码率小于目标码率如果设置了vbv最小的码率,则将输入的码率赋值给vbv最大的码率,此时变成了CBR模式*/if( rc->b_vbv_min_rate )h->param.rc.i_vbv_max_bitrate = h->param.rc.i_bitrate;// vbv_buffer_size为整个水池的容量,如果小于输入帧的大小,则将容量配置为输入帧的大小if( h->param.rc.i_vbv_buffer_size < (int)(h->param.rc.i_vbv_max_bitrate / rc->fps) ){h->param.rc.i_vbv_buffer_size = h->param.rc.i_vbv_max_bitrate / rc->fps;x264_log( h, X264_LOG_WARNING, "VBV buffer size cannot be smaller than one frame, using %d kbit\n",h->param.rc.i_vbv_buffer_size );}// 转换为kiloint kilobit_size = h->param.i_avcintra_class ? 1024 : 1000;int vbv_buffer_size = h->param.rc.i_vbv_buffer_size * kilobit_size;	// 整个水池的总容量int vbv_max_bitrate = h->param.rc.i_vbv_max_bitrate * kilobit_size;	// 水池最大输入码率/* Init HRD */// ...if( rc->b_vbv_min_rate ) // 将输入码率转换成为kb/src->bitrate = (double)h->param.rc.i_bitrate * kilobit_size;rc->buffer_rate = vbv_max_bitrate / rc->fps;	// 水池最大的输入水量rc->vbv_max_rate = vbv_max_bitrate;				// 水池最大的输入速率rc->buffer_size = vbv_buffer_size;				// 水池最大的容量// 输入水量 * 1.1 > 最大容量,说明水池比较小,最多容纳一帧的数据量// single_frame_vbv在后面会被用于调整qscalerc->single_frame_vbv = rc->buffer_rate * 1.1 > rc->buffer_size;	// if( rc->b_abr && h->param.rc.i_rc_method == X264_RC_ABR )rc->cbr_decay = 1.0 - rc->buffer_rate / rc->buffer_size* 0.5 * X264_MAX(0, 1.5 - rc->buffer_rate * rc->fps / rc->bitrate);// 这里表明vbv和crf模式可以共存if( h->param.rc.i_rc_method == X264_RC_CRF && h->param.rc.f_rf_constant_max ){// 如果存在rf最大值,则计算一个增量rc->rate_factor_max_increment = h->param.rc.f_rf_constant_max - h->param.rc.f_rf_constant;if( rc->rate_factor_max_increment <= 0 ){x264_log( h, X264_LOG_WARNING, "CRF max must be greater than CRF\n" );rc->rate_factor_max_increment = 0;}}if( b_init ) {if( h->param.rc.f_vbv_buffer_init > 1. )h->param.rc.f_vbv_buffer_init = x264_clip3f( h->param.rc.f_vbv_buffer_init / h->param.rc.i_vbv_buffer_size, 0, 1 );h->param.rc.f_vbv_buffer_init = x264_clip3f( X264_MAX( h->param.rc.f_vbv_buffer_init, rc->buffer_rate / rc->buffer_size ), 0, 1);// buffer_fill_final表示水池中当前时刻剩余的水量rc->buffer_fill_final =rc->buffer_fill_final_min = rc->buffer_size * h->param.rc.f_vbv_buffer_init * h->sps->vui.i_time_scale;rc->b_vbv = 1; // 启用vbvrc->b_vbv_min_rate = !rc->b_2pass&& h->param.rc.i_rc_method == X264_RC_ABR&& h->param.rc.i_vbv_max_bitrate <= h->param.rc.i_bitrate;}}
}

4.4.2 计算vbv的输入速率(update_vbv_plan)

函数在x264_ratecontrol_start中调用,先计算

/* Before encoding a frame, choose a QP for it */
void x264_ratecontrol_start( x264_t *h, int i_force_qp, int overhead )
{// ...if( rc->b_vbv ) // vbv的初始化{memset( h->fdec->i_row_bits, 0, h->mb.i_mb_height * sizeof(int) );memset( h->fdec->f_row_qp, 0, h->mb.i_mb_height * sizeof(float) );memset( h->fdec->f_row_qscale, 0, h->mb.i_mb_height * sizeof(float) );rc->row_pred = rc->row_preds[h->sh.i_type];rc->buffer_rate = h->fenc->i_cpb_duration * rc->vbv_max_rate * h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale;update_vbv_plan( h, overhead ); // 临时更新所有正在编码过程中的帧的VBV信息}// ...
}

update_vbv_plan的主要功能是,根据目前正在进行的所有帧预测的比特数临时更新VBV,考虑了多线程问题

static void update_vbv_plan( x264_t *h, int overhead )
{x264_ratecontrol_t *rcc = h->rc;rcc->buffer_fill = h->thread[0]->rc->buffer_fill_final_min / h->sps->vui.i_time_scale; // 水池当前水位if( h->i_thread_frames > 1 ) // 使用了多线程{int j = rcc - h->thread[0]->rc;for( int i = 1; i < h->i_thread_frames; i++ ){x264_t *t = h->thread[ (j+i)%h->i_thread_frames ];double bits = t->rc->frame_size_planned; if( !t->b_thread_active )continue;bits = X264_MAX(bits, t->rc->frame_size_estimated);rcc->buffer_fill -= bits;	// 出水rcc->buffer_fill = X264_MAX( rcc->buffer_fill, 0 );rcc->buffer_fill += t->rc->buffer_rate;rcc->buffer_fill = X264_MIN( rcc->buffer_fill, rcc->buffer_size );}}rcc->buffer_fill = X264_MIN( rcc->buffer_fill, rcc->buffer_size );rcc->buffer_fill -= overhead; // 减去头部的比特
}

4.4.3 添加VBV限制(vbv_pass1)

qscale的初值由get_qscale函数计算出,不开启VBV时,根据predicted_bits - wanted_bits调整qscale,开启VBV时,使用vbv_pass1,根据vbv buffer的状态调整qscale。vbv_pass1通过预测当前qscale下未来n帧的大小来估计vbv的状态,如果vbv buffer将overflow,则增大qscale;如果buffer将会underflow,则减小qscale

// apply VBV constraints
static double vbv_pass1( x264_t *h, int pict_type, double q )
{x264_ratecontrol_t *rcc = h->rc;/* B-frames are not directly subject to VBV,* since they are controlled by the P-frames' QPs. */// 检查是否进入vbv调整if( rcc->b_vbv && rcc->last_satd > 0 ){double q0 = q;double fenc_cpb_duration = (double)h->fenc->i_cpb_duration *h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale;/* Lookahead VBV: raise the quantizer as necessary such that no frames in* the lookahead overflow and such that the buffer is in a reasonable state* by the end of the lookahead. */// Lookahead VBV:根据需要提高量化器,这样在lookahead中不会有帧溢出,并且缓冲区在lookahead结束时处于合理的状态if( h->param.rc.i_lookahead ){int terminate = 0;/* Avoid an infinite loop. */// 对于qscale的调整最多持续1000次,或者terminate=3(水位满足了需求)for( int iterations = 0; iterations < 1000 && terminate != 3; iterations++ ){double frame_q[3];double cur_bits = predict_size( &rcc->pred[h->sh.i_type], q, rcc->last_satd );	// 预估当前qscale会使用的比特数double buffer_fill_cur = rcc->buffer_fill - cur_bits; // 当前水位 - 当前比特 = 出水之后剩余的水位double target_fill;double total_duration = 0;double last_duration = fenc_cpb_duration;frame_q[0] = h->sh.i_type == SLICE_TYPE_I ? q * h->param.rc.f_ip_factor : q; // p帧的qscaleframe_q[1] = frame_q[0] * h->param.rc.f_pb_factor;							 // b帧的qscaleframe_q[2] = frame_q[0] / h->param.rc.f_ip_factor;							 // i帧的qscale/* Loop over the planned future frames. */// 预估未来帧对于vbv的消耗for( int j = 0; buffer_fill_cur >= 0 && buffer_fill_cur <= rcc->buffer_size; j++ ){total_duration += last_duration;buffer_fill_cur += rcc->vbv_max_rate * last_duration;	// rate * time = bits,加水int i_type = h->fenc->i_planned_type[j];				// lookahead中未来帧的类型int i_satd = h->fenc->i_planned_satd[j];				// lookahead中未来帧的satdif( i_type == X264_TYPE_AUTO )break;i_type = IS_X264_TYPE_I( i_type ) ? SLICE_TYPE_I : IS_X264_TYPE_B( i_type ) ? SLICE_TYPE_B : SLICE_TYPE_P;cur_bits = predict_size( &rcc->pred[i_type], frame_q[i_type], i_satd );	// 预测lookahead中帧所消耗的比特数buffer_fill_cur -= cur_bits;											// 减去lookahead中每帧所消耗的比特数last_duration = h->fenc->f_planned_cpb_duration[j];}/* Try to get to get the buffer at least 50% filled, but don't set an impossible goal. */// 尝试保持水池至少满足50%的水位,但是不要设定一个不可能的目标// target_fill是目标水位target_fill = X264_MIN( rcc->buffer_fill + total_duration * rcc->vbv_max_rate * 0.5, rcc->buffer_size * 0.5 );// 当前的水位 < 目标水位,说明出水偏多,要减少出水量,需要增大qscale,这里乘以一个经验性参数1.01if( buffer_fill_cur < target_fill ){q *= 1.01;terminate |= 1;continue;}/* Try to get the buffer no more than 80% filled, but don't set an impossible goal. */// 尝试保持水池不要超过80%的水位,但是不要设定一个不可能的目标// 上面保证了水池保证50%的水位,这里不超过80%的水位,VBV将水池的水位尽量控制在50%~80%target_fill = x264_clip3f( rcc->buffer_fill - total_duration * rcc->vbv_max_rate * 0.5, rcc->buffer_size * 0.8, rcc->buffer_size );// 当前的水位 > 目标水位,说明出水偏少,要增大出水量,需要减小qscale,这里除以一个经验性参数1.01if( rcc->b_vbv_min_rate && buffer_fill_cur > target_fill ){q /= 1.01;terminate |= 2;continue;}break;}}/* Fallback to old purely-reactive algorithm: no lookahead. */else{	// 不使用lookahead// 如果是P帧或者是连续的I帧,并且当前水池水位低于50%,则需要将qscale增大if( ( pict_type == SLICE_TYPE_P ||( pict_type == SLICE_TYPE_I && rcc->last_non_b_pict_type == SLICE_TYPE_I ) ) &&rcc->buffer_fill/rcc->buffer_size < 0.5 ){// qscale除以的因子为,水位比例 * 2,由于分母小于1,所以qscale变大q /= x264_clip3f( 2.0*rcc->buffer_fill/rcc->buffer_size, 0.5, 1.0 );}/* Now a hard threshold to make sure the frame fits in VBV.* This one is mostly for I-frames. */// 使用一个强阈值来保证帧适应VBV,这个经常用于intra帧double bits = predict_size( &rcc->pred[h->sh.i_type], q, rcc->last_satd );/* For small VBVs, allow the frame to use up the entire VBV. */// 对于小的VBV,允许帧使用整个VBVdouble max_fill_factor = h->param.rc.i_vbv_buffer_size >= 5*h->param.rc.i_vbv_max_bitrate / rcc->fps ? 2 : 1;/* For single-frame VBVs, request that the frame use up the entire VBV. */// 对于单帧VBV,要求该帧使用整个VBVdouble min_fill_factor = rcc->single_frame_vbv ? 1 : 2;// 如果预测的比特 > 剩余可用的比特数,则增大qscaleif( bits > rcc->buffer_fill/max_fill_factor ){double qf = x264_clip3f( rcc->buffer_fill/(max_fill_factor*bits), 0.2, 1.0 ); // qf < 1q /= qf;bits *= qf;}// 如果预测的比特 < 剩余可用的比特数,则减小qscaleif( bits < rcc->buffer_rate/min_fill_factor ){double qf = x264_clip3f( bits*min_fill_factor/rcc->buffer_rate, 0.001, 1.0 ); // qf < 1q *= qf;}q = X264_MAX( q0, q );}/* Check B-frame complexity, and use up any bits that would* overflow before the next P-frame. */if( h->sh.i_type == SLICE_TYPE_P && !rcc->single_frame_vbv ){int nb = rcc->bframes;double bits = predict_size( &rcc->pred[h->sh.i_type], q, rcc->last_satd );	// 当前编码帧的qpdouble pbbits = bits;double bbits = predict_size( rcc->pred_b_from_p, q * h->param.rc.f_pb_factor, rcc->last_satd );	// 预测b帧的bitsdouble space;double bframe_cpb_duration = 0;	// 所有b帧的时长double minigop_cpb_duration;for( int i = 0; i < nb; i++ )bframe_cpb_duration += h->fenc->f_planned_cpb_duration[i];	// cpb(coded picture buffer)的持续时间// 如果b帧的总预测比特 > b帧的入水量,则将b帧数量置为0if( bbits * nb > bframe_cpb_duration * rcc->vbv_max_rate ){nb = 0;bframe_cpb_duration = 0;}pbbits += nb * bbits;// 计算了整个Mini-GOP(包括B帧和当前帧)的CPB持续时间minigop_cpb_duration = bframe_cpb_duration + fenc_cpb_duration;// 确定在下一个P帧之前可以填充的比特数space = rcc->buffer_fill + minigop_cpb_duration*rcc->vbv_max_rate - rcc->buffer_size;if( pbbits < space ){// 如果预测的帧大小(pbbits)小于剩余空间(space),则将量化参数(q)根据剩余空间的比例进行调整q *= X264_MAX( pbbits / space, bits / (0.5 * rcc->buffer_size) );}q = X264_MAX( q0/2, q );}if( !rcc->b_vbv_min_rate )q = X264_MAX( q0, q );}// 根据帧类型进行qscale的clipreturn clip_qscale( h, pict_type, q );
}

4.4.4 行级VBV控制(x264_ratecontrol_mb)

每编码完一行后,会利用预测模型对当前帧大小进行预测,然后根据VBV Buffer fill对QP进行微调,执行的过程在x264_ratecontrol_mb中

/* TODO:*  eliminate all use of qp in row ratecontrol: make it entirely qscale-based.*  make this function stop being needlessly O(N^2)*  update more often than once per row? */
int x264_ratecontrol_mb( x264_t *h, int bits )
{x264_ratecontrol_t *rc = h->rc;const int y = h->mb.i_mb_y; // 第y行h->fdec->i_row_bits[y] += bits; // 将当前mb的bit加到当前行的总比特上rc->qpa_aq += h->mb.i_qp; // 将当前mb的qp加到自适应量化qp之后的均值qp上// 如果没有到这一行的最后一个mb,则返回if( h->mb.i_mb_x != h->mb.i_mb_width - 1 )return 0;x264_emms();rc->qpa_rc += rc->qpm * h->mb.i_mb_width; // qpm是qscale_rate_estimate之后的qscale,加到qp ad之前的qp上// 如果不使用vbv,则直接返回if( !rc->b_vbv )return 0;float qscale = qp2qscale( rc->qpm );h->fdec->f_row_qp[y] = rc->qpm;h->fdec->f_row_qscale[y] = qscale; // 更新当前行的qscale,赋值到重建帧当中// 更新预测器update_predictor( &rc->row_pred[0], qscale, h->fdec->i_row_satd[y], h->fdec->i_row_bits[y] );if( h->sh.i_type != SLICE_TYPE_I && rc->qpm < h->fref[0][0]->f_row_qp[y] )update_predictor( &rc->row_pred[1], qscale, h->fdec->i_row_satds[0][0][y], h->fdec->i_row_bits[y] );/* update ratecontrol per-mbpair in MBAFF */// 如果mbaff, 并且为偶数行, 则直接返回if( SLICE_MBAFF && !(y&1) )return 0;/* FIXME: We don't currently support the case where there's a slice* boundary in between. */int can_reencode_row = h->sh.i_first_mb <= ((h->mb.i_mb_y - SLICE_MBAFF) * h->mb.i_mb_stride);/* tweak quality based on difference from predicted size */// 根据与预测大小的差异调整质量,使用的是重建帧float prev_row_qp = h->fdec->f_row_qp[y];float qp_absolute_max = h->param.rc.i_qp_max;if( rc->rate_factor_max_increment )qp_absolute_max = X264_MIN( qp_absolute_max, rc->qp_novbv + rc->rate_factor_max_increment );float qp_max = X264_MIN( prev_row_qp + h->param.rc.i_qp_step, qp_absolute_max );float qp_min = X264_MAX( prev_row_qp - h->param.rc.i_qp_step, h->param.rc.i_qp_min );float step_size = 0.5f;float slice_size_planned = h->param.b_sliced_threads ? rc->slice_size_planned : rc->frame_size_planned;float bits_so_far = row_bits_so_far( h, y ); // 第y行及之前已经使用了多少比特数rc->bits_so_far = bits_so_far;float max_frame_error = x264_clip3f( 1.0 / h->mb.i_mb_height, 0.05, 0.25 );float max_frame_size = rc->frame_size_maximum - rc->frame_size_maximum * max_frame_error;max_frame_size = X264_MIN( max_frame_size, rc->buffer_fill - rc->buffer_rate * max_frame_error );float size_of_other_slices = 0;if( h->param.b_sliced_threads ) // 多slice并行编码,因为在实际的使用中slice一般就是一帧,所以应该是没有到帧的结尾{float bits_so_far_of_other_slices = 0;for( int i = 0; i < h->param.i_threads; i++ )if( h != h->thread[i] ){size_of_other_slices += h->thread[i]->rc->frame_size_estimated;bits_so_far_of_other_slices += h->thread[i]->rc->bits_so_far;}float weight = x264_clip3f( (bits_so_far_of_other_slices + rc->frame_size_estimated) / (size_of_other_slices + rc->frame_size_estimated), 0.0, 1.0 );float frame_size_planned = rc->frame_size_planned - rc->frame_size_planned * max_frame_error;float size_of_other_slices_planned = X264_MIN( frame_size_planned, max_frame_size ) - rc->slice_size_planned;size_of_other_slices_planned = X264_MAX( size_of_other_slices_planned, bits_so_far_of_other_slices );size_of_other_slices = (size_of_other_slices - size_of_other_slices_planned) * weight + size_of_other_slices_planned;}if( y < h->i_threadslice_end-1 ) // 还没有到slice(frame)的最后一行{/* B-frames shouldn't use lower QP than their reference frames. */if( h->sh.i_type == SLICE_TYPE_B ){qp_min = X264_MAX( qp_min, X264_MAX( h->fref[0][0]->f_row_qp[y+1], h->fref[1][0]->f_row_qp[y+1] ) );rc->qpm = X264_MAX( rc->qpm, qp_min );}// 计算buffer在预期之外剩余的比特数量// rc->buffer_fill是预期的buffer大小,rc->frame_size_planned是预期的帧的大小float buffer_left_planned = rc->buffer_fill - rc->frame_size_planned;buffer_left_planned = X264_MAX( buffer_left_planned, 0.f );/* More threads means we have to be more cautious in letting ratecontrol use up extra bits. */// 预期之中剩余的buffer,分配给每个线程的rate_tolerance,说明还能容忍多少的比特float rc_tol = buffer_left_planned / h->param.i_threads * rc->rate_tolerance;// b1表示的是当前编码帧可能会使用的总的比特数// bits_so_far表示之前所有行已经使用的多少比特数// predict_row_size_to_end表示剩余所有行还需要的比特数// 因为编码时一般不使用多slice,则size_of_other_slices为0,因为不会有其他的slicefloat b1 = bits_so_far + predict_row_size_to_end( h, y, rc->qpm ) + size_of_other_slices;// 置信系数,使用之前已经使用的比特数除以预计使用的比特数,表示已经使用的比特数的比例float trust_coeff = x264_clip3f( bits_so_far / slice_size_planned, 0.0, 1.0 );/* Don't increase the row QPs until a sufficient amount of the bits of the frame have been processed, in case a flat *//* area at the top of the frame was measured inaccurately. */// 在处理了足够数量的帧的比特数之前,不要增加行qp,以防帧顶部的平坦区域测量不准确// 如果现在使用的比特数还不足5%,则使用前一行的qp作为最大的qp// 从赋值关系来看,prev_row_qp最初来自于rc->qpm,即当前mb所使用的mb// 这里表示后续的mb的qp都不会超过当前mb使用的qp,即后续的mb会以更高质量的编码if( trust_coeff < 0.05f )qp_max = qp_absolute_max = prev_row_qp;// 如果不是Intra帧,则将rc的容忍度减半,即P帧或者B帧不能有太大的波动if( h->sh.i_type != SLICE_TYPE_I )rc_tol *= 0.5f;if( !rc->b_vbv_min_rate )qp_min = X264_MAX( qp_min, rc->qp_novbv );// 当前mb使用的qp小于qp_max,并且当前已经使用的比特偏大,分三种情况:// (1)大于预期的frame size + 可容忍的比特增加数// (2)大于预期的frame_size,并且当前mb的qp小于不使用vbv的qp(qpm比qp_novbv小,后续编码可能会消耗更多比特)// (3)大于预期的buffer_size - 剩余buffer的一半(感觉配置比较经验,为什么要乘以0.5倍而不是0.7倍?)// 总体来说,这里是对可能出现的上溢(使用的比特超出了预期的比特)进行判断// 如果可能出现上溢,则将qpm增大,降低预期的比特消耗,并且更新b1,直到b1符合预期while( rc->qpm < qp_max&& ((b1 > rc->frame_size_planned + rc_tol) ||(b1 > rc->frame_size_planned && rc->qpm < rc->qp_novbv) ||(b1 > rc->buffer_fill - buffer_left_planned * 0.5f)) ){rc->qpm += step_size;b1 = bits_so_far + predict_row_size_to_end( h, y, rc->qpm ) + size_of_other_slices;}// 最大的比特数float b_max = b1 + ((rc->buffer_fill - rc->buffer_size + rc->buffer_rate) * 0.90f - b1) * trust_coeff;rc->qpm -= step_size;// 将qpm减去一个step_size之后,预期消耗的比特数b2float b2 = bits_so_far + predict_row_size_to_end( h, y, rc->qpm ) + size_of_other_slices;// 如果qpm大于qp_min并且小于前一行mb的qp,需要三种情况都满足:// (1)当前mb大于一行的qp,或者会执行单帧的vbv// (2)b2小于最大的帧大小// (3)b2小于预期的帧大小的0.8倍,或者b2小于最大比特数// 如果满足,则将当前mb的qp降低,增大预期的比特消耗,并且更新b2,直到满足要求while( rc->qpm > qp_min && rc->qpm < prev_row_qp&& (rc->qpm > h->fdec->f_row_qp[0] || rc->single_frame_vbv)&& (b2 < max_frame_size)&& ((b2 < rc->frame_size_planned * 0.8f) || (b2 < b_max)) ){b1 = b2;rc->qpm -= step_size;b2 = bits_so_far + predict_row_size_to_end( h, y, rc->qpm ) + size_of_other_slices;}rc->qpm += step_size;/* avoid VBV underflow or MinCR violation */// 避免VBV下溢或者是MinCR超出限制while( rc->qpm < qp_absolute_max && (b1 > max_frame_size) ) // qp小于最大的qp,但是预期消耗的总比特数大于帧最大的比特数{rc->qpm += step_size;b1 = bits_so_far + predict_row_size_to_end( h, y, rc->qpm ) + size_of_other_slices;}// b1是预测的当前帧可能会消耗的总比特数// 如果不使用多slice,则size_of_other_slices为0,此时frame_size_estimated = b1rc->frame_size_estimated = b1 - size_of_other_slices;/* If the current row was large enough to cause a large QP jump, try re-encoding it. */// 如果当前行在进行编码时出现了大的QP跳动,尝试重新编码if( rc->qpm > qp_max && prev_row_qp < qp_max && can_reencode_row ){/* Bump QP to halfway in between... close enough. */rc->qpm = x264_clip3f( (prev_row_qp + rc->qpm)*0.5f, prev_row_qp + 1.0f, qp_max );rc->qpa_rc = rc->qpa_rc_prev;rc->qpa_aq = rc->qpa_aq_prev;h->fdec->i_row_bits[y] = 0;h->fdec->i_row_bits[y-SLICE_MBAFF] = 0;return -1;}}else{ // 如果编码的是最后一行rc->frame_size_estimated = bits_so_far;/* Last-ditch attempt: if the last row of the frame underflowed the VBV,* try again. */// 如果编码最后一行时,出现了VBV的下溢情况,重新尝试if( rc->qpm < qp_max && can_reencode_row&& (bits_so_far + size_of_other_slices > X264_MIN( rc->frame_size_maximum, rc->buffer_fill )) ){rc->qpm = qp_max;rc->qpa_rc = rc->qpa_rc_prev;rc->qpa_aq = rc->qpa_aq_prev;h->fdec->i_row_bits[y] = 0;h->fdec->i_row_bits[y-SLICE_MBAFF] = 0;return -1;}}rc->qpa_rc_prev = rc->qpa_rc;rc->qpa_aq_prev = rc->qpa_aq;return 0;
}

4.5 ABR小结

abr模式将码率稳定作为控制的主要目标,以过去编码帧和当前编码帧的纹理复杂度作为考量,预测后续帧使用的编码比特数,期望将编码过程中的实时码率约束在目标码率附近。但是,由于不知道未来实际编码帧的内容,预测比特数可能与实际编码内容不匹配,这样可能会带来短时较大的码率波动(因为要调整qscale来尽量做到匹配),从而使得编码质量不稳定。

为了解决这一问题,引入了vbv缓冲池机制,通过保持缓冲池中水量的相对稳定,来保证输出码率的相对稳定,进而保证编码质量的相对稳定。如果使用了lookahead,vbv缓冲池预测未来帧消耗的比特数,从而做到整体的码率控制,如果不使用lookahead,vbv缓冲池通过设置一些经验性参数来控制qscale。最后的结果是,vbv会将池中的水量保持在50%~80%之间,输出的水量也在对应的范围内稳定波动。在abr模式中加入vbv的控制时,如果vbv最大输入码率等于目标平均码率,此时就变成了cbr模式,这种模式就能够保证实际编码码率接近目标码率

5.小结

本文记录了x264当中帧级码控策略,主要模式包括cqp模式、crf模式和abr模式,没有详细记录cbr模式、2pass模式和vbr模式。

  1. CQP模式
    恒定量化参数qp,不会进行量化参数qscale的调整,不推荐使用
  2. CRF模式
    恒定质量因子rf,以保证编码质量作为主要目标,根据前后的视频纹理信息进行qscale的调整,能够保证视频质量稳定,是x264中默认的编码模式。但是不能够控制输出码率大小
  3. ABR模式
    恒定目标码率bitrate,以保证码率稳定为主要目标,根据前后的视频纹理信息进行qscale的调整,尽量将码率约束在目标码率附近。但是,由于无法知晓未来帧的编码信息,可能会产生较大的码率波动和编码质量波动。

CBR、2pas和VBR模式
(1)CBR模式
cbr模式由abr模式和vbv机制组合而来,通过增加vbv机制,使得abr模式能够实现良好的码率控制,如果输入的最大码率等于目标码率,此时就是cbr模式
(2)2pass模式
2pass模式是基于主要模式的进行的两遍编码模式,第一遍编码模式(可用crf或abr)获取编码信息,第二遍编码模式依据记录下来的编码信息实现更精准的码控
(3)VBR模式
vbr模式是码率可变的模式,这种模式与cbr模式相对立。我理解这里并不是一种单独的编码模式,更像是一种概念,例如crf模式可以理解为是面向质量稳定的码率可变模式(quality-based vbr mode)

本文记录的码控当中,主要关注的内容为bit、qscale和complexity,不同的码率控制方法是围绕着这三者进行的,有的侧重于质量稳定,有的侧重于码率稳定,如何合理的对这几个方面进行调控,是实现良好码率控制的核心。此外,在码率控制当中使用了许多经验性的参数,这是利用实际应用中测试的参数拟合得到的,针对于不同的应用场景,这些参数应当进行特定的修正,包括外部配置的参数

CSDN : https://blog.csdn.net/weixin_42877471
Github : https://github.com/DoFulangChen

这篇关于【x264】码率控制模块的简单分析—帧级码控策略的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1037093

相关文章

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

python: 多模块(.py)中全局变量的导入

文章目录 global关键字可变类型和不可变类型数据的内存地址单模块(单个py文件)的全局变量示例总结 多模块(多个py文件)的全局变量from x import x导入全局变量示例 import x导入全局变量示例 总结 global关键字 global 的作用范围是模块(.py)级别: 当你在一个模块(文件)中使用 global 声明变量时,这个变量只在该模块的全局命名空

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

hdu2289(简单二分)

虽说是简单二分,但是我还是wa死了  题意:已知圆台的体积,求高度 首先要知道圆台体积怎么求:设上下底的半径分别为r1,r2,高为h,V = PI*(r1*r1+r1*r2+r2*r2)*h/3 然后以h进行二分 代码如下: #include<iostream>#include<algorithm>#include<cstring>#include<stack>#includ

在JS中的设计模式的单例模式、策略模式、代理模式、原型模式浅讲

1. 单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点。 示例代码: class Singleton {constructor() {if (Singleton.instance) {return Singleton.instance;}Singleton.instance = this;this.data = [];}addData(value)

usaco 1.3 Prime Cryptarithm(简单哈希表暴搜剪枝)

思路: 1. 用一个 hash[ ] 数组存放输入的数字,令 hash[ tmp ]=1 。 2. 一个自定义函数 check( ) ,检查各位是否为输入的数字。 3. 暴搜。第一行数从 100到999,第二行数从 10到99。 4. 剪枝。 代码: /*ID: who jayLANG: C++TASK: crypt1*/#include<stdio.h>bool h

uva 10387 Billiard(简单几何)

题意是一个球从矩形的中点出发,告诉你小球与矩形两条边的碰撞次数与小球回到原点的时间,求小球出发时的角度和小球的速度。 简单的几何问题,小球每与竖边碰撞一次,向右扩展一个相同的矩形;每与横边碰撞一次,向上扩展一个相同的矩形。 可以发现,扩展矩形的路径和在当前矩形中的每一段路径相同,当小球回到出发点时,一条直线的路径刚好经过最后一个扩展矩形的中心点。 最后扩展的路径和横边竖边恰好组成一个直

poj 1113 凸包+简单几何计算

题意: 给N个平面上的点,现在要在离点外L米处建城墙,使得城墙把所有点都包含进去且城墙的长度最短。 解析: 韬哥出的某次训练赛上A出的第一道计算几何,算是大水题吧。 用convexhull算法把凸包求出来,然后加加减减就A了。 计算见下图: 好久没玩画图了啊好开心。 代码: #include <iostream>#include <cstdio>#inclu