【x265】码率控制模块的简单分析—帧级码控模式(CQP、CRF和ABR)

news2024/9/20 17:46:05

目录

  • 1.x265中的帧级码控模式
  • 2.CQP模式
  • 3.CRF模式
    • 3.1 质量因子的计算
    • 3.2 CRF模式的实现
  • 4.ABR模式
    • 4.1 ABR的实现
    • 4.2 VBV技术
      • 4.2.1 VBV初始化
      • 4.2.2 VBV在lookahead中的计算
      • 4.2.3 计算VBV输入速率(updateVbvPlan)
      • 4.2.4 添加Vbv限制(clipQscale)
      • 4.2.5 行级VBV控制(rowVbvRateControl)

x265相关:
【x265】x265编码器参数配置
【x265】预测模块的简单分析—帧内预测
【x265】预测模块的简单分析—帧间预测
【x265】码率控制模块的简单分析—块级码控工具(AQ和cuTree)
【x265】码率控制模块的简单分析—帧级码控模式(CQP、CRF和ABR)

1.x265中的帧级码控模式

在x265当中,主要的码控模式如下

/* rate tolerance method */
typedef enum
{
    X265_RC_ABR,	// Average Bitrate
    X265_RC_CQP,	// Constant QP
    X265_RC_CRF		// Constant Rate Factor
} X265_RC_METHODS;

X265_RC_ABR:表示使用平均码率进行编码
X265_RC_CQP:表示使用固定的量化参数进行编码
X265_RC_CRF:表示使用固定的质量因子进行编码

上述三种模式为最基础的模式,其余的模式例如CBR、2pass等模式都是基于这几种模式演化而来的。与x264类似,x265当中的码率控制主要思想是基于一种假设
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表示纹理复杂度。这里表示的含义是:
(1)如果希望量化参数qscale恒定,视频越复杂(complexity越高),则使用的bits越多
(2)如果希望使用的bits恒定,视频越复杂,则量化参数应该越高,此时编码的质量就越低

这种关系具体的数值,可以通过大量的数据拟合来实现

2.CQP模式

CQP模式全称为Constant QP模式,在这个模式下使用固定的qp值进行编码,编码过程中qp值不会发生变化。在使用x265时,如果指定了qp,则将模式自动配置为CQP模式

OPT("qp")
{
    p->rc.qp = atoi(value);
    p->rc.rateControlMode = X265_RC_CQP;
}

如果配置了CQP模式,则不会使用如AQ和cuTree等码控工具

if (p->rc.rateControlMode == X265_RC_CQP)
{
    p->rc.aqMode = X265_AQ_NONE;
    p->rc.hevcAq = 0;
    p->rc.bitrate = 0;
    p->rc.cuTree = 0;
    p->rc.aqStrength = 0;
}

如果使用CQP模式,则将输入的qp进行赋值。如果是P帧,直接赋值;如果是I帧和B帧,需要根据对应的offset调整qp值

if (m_param->rc.rateControlMode == X265_RC_CQP)
{
	// 不使用无损模式
    if (m_qp && !m_param->bLossless)
    {
    	// 直接赋值给P帧
        m_qpConstant[P_SLICE] = m_qp;
        /*
			对于I帧和B帧,需要调整qp值
			(1)I帧qp需要根据m_ipOffset进行调整
				(a)m_ipOffset = 6.0 * X265_LOG2(m_param->rc.ipFactor);
				(b)ipFactor默认为1.4
			(2)B帧qp需要根据m_pbOffset进行调整
				(a)m_pbOffset = 6.0 * X265_LOG2(m_param->rc.pbFactor);
				(b)pbFactor默认为1.3
		*/
        m_qpConstant[I_SLICE] = x265_clip3(QP_MIN, QP_MAX_MAX, (int)(m_qp - m_ipOffset + 0.5));
        m_qpConstant[B_SLICE] = x265_clip3(QP_MIN, QP_MAX_MAX, (int)(m_qp + m_pbOffset + 0.5));
    }
    else
    {
        m_qpConstant[P_SLICE] = m_qpConstant[I_SLICE] = m_qpConstant[B_SLICE] = m_qp;
    }
}

3.CRF模式

前面的CQP模式让编码器按照固定QP进行编码,这样的策略让编码器没有灵活性。CRF模式全称为Constant Rate Factor,表示固定质量因子,是一种期望让编码器按照固定质量进行编码的模式,也是x265编码器默认使用的模式。质量因子使用rfConstant描述,可以通过外部设置质量因子。rfConstant在x265当中默认为28,rfConstant增大则编码质量

OPT("crf")
{
    p->rc.rfConstant = atof(value);
    p->rc.rateControlMode = X265_RC_CRF;
}

3.1 质量因子的计算

如果使用CRF模式,在进行RateControl初始化时,会使用rfConstant去计算一个内部质量因子m_rateFactorConstant。下面代码实现的位置为RateControl::RateControl(),位于encoder\ratecontrol.cpp中。下面计算m_rateFactorConstant的公式为
m _ 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 − m _ q C o m p r e s s ) x 265 _ 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 ) m\_rateFactorConstant = \frac{pow(baseCplx, 1 - m\_qCompress)}{x265\_qp2qscale(rfConstant + mbtree\_offset)} m_rateFactorConstant=x265_qp2qscale(rfConstant+mbtree_offset)pow(baseCplx,1m_qCompress)
如果控制变量,我对这个公式中的部分参数的简单理解是:
(1)m_rateFactorConstant描述了一个调整qscale的比例,这个比例由基本复杂度(由图像尺寸决定),量化曲线压缩因子(qCompress),质量因子(rfConstant)和cuTree影响因子(mbtree_offset)共同决定

(2)baseCplx越大,则m_rateFactorConstant越大。这表明为了满足更高分辨率的感知质量,需要以更大的力度调整qscale。m_qCompress用于调整复杂度的影响程度

(3)rfConstant越大,则m_rateFactorConstant越小。由于rfConstant是用户可以配置的(x265中默认为28),所以这表明用户希望降低qscale调整的幅度

(4)mbtree_offset如果不为0,即使用了cuTree技术,说明进行qscale调整时还需要考虑到其他帧的影响,应该适当降低qscale的调整幅度,因为cuTree(可能还包括aq)后续可能还会对qscale进行调整

PS:m_rateFactorConstant在使用时,作为分母来调整qscale,即 qscale /= m_rateFactorConstant。如果m_rateFactorConstant越大,则qscale越小

3.2 CRF模式的实现

代码实现的位置为RateControl::RateControl(),位于encoder\ratecontrol.cpp中

if (m_param->rc.rateControlMode == X265_RC_CRF)
{
    m_param->rc.qp = (int)m_param->rc.rfConstant;
    m_param->rc.bitrate = 0;
	// 如果输入为1280x720,m_ncu = lowresCuWidth * lowresCuHeight = 1280x720 / 16x16 = 3600
    double baseCplx = m_ncu * (m_param->bframes ? 120 : 80);
    /*
		(1)qCompress是量化曲线压缩因子,它根据残差的复杂度(通过lookahead计算)对帧量化器进行加权
		(2)如果使用cuTree,mbtree_offset = 5.4
		m_rateFactorConstant表示质量因子,即恒定的质量因子,后续是基于此来调整qscale
	*/
    double mbtree_offset = m_param->rc.cuTree ? (1.0 - m_param->rc.qCompress) * 13.5 : 0;
    m_rateFactorConstant = pow(baseCplx, 1 - m_qCompress) /
        x265_qp2qScale(m_param->rc.rfConstant + mbtree_offset);
    if (m_param->rc.rfConstantMax) // rfConstantMax默认为0
    {
        m_rateFactorMaxIncrement = m_param->rc.rfConstantMax - m_param->rc.rfConstant;
        if (m_rateFactorMaxIncrement <= 0)
        {
            x265_log(m_param, X265_LOG_WARNING, "CRF max must be greater than CRF\n");
            m_rateFactorMaxIncrement = 0;
        }
    }
    if (m_param->rc.rfConstantMin)	// rfConstantMin默认为0
        m_rateFactorMaxDecrement = m_param->rc.rfConstant - m_param->rc.rfConstantMin;
}

这个内部质量因子m_rateFactorConstant会在rateEstimateQscale()中被使用,计算qp

if (m_param->rc.rateControlMode == X265_RC_CRF)
{
	// 根据质量因子计算QScale
    q = getQScale(rce, m_rateFactorConstant);
    x265_zone* zone = getZone();
    if (zone)
    {
        if (zone->bForceQp)
            q = x265_qp2qScale(zone->qp);
        else
            q /= zone->bitrateFactor;
    }
}

getQScale()的实现方式如下,首先根据是否使用cuTree和hevcAq来获取qscale初始值,然后根据rateFactor调整qscale。获取qscale初始值有两种情况:
(1)使用cuTree且不使用hevcAq,直接利用fps和qcompress获取qscale
(2)否则,利用blurredComplexity和qcompress获取qscale

/* modify the bitrate curve from pass1 for one frame */
double RateControl::getQScale(RateControlEntry *rce, double rateFactor)
{
    double q;
    /*
		我理解使用这种计算方式的原因是,qscale的大小与当前图像的复杂度和fps有直接的关系
		(1)如果使用了cuTree且不使用hevcAq,说明存在别的帧会影响当前qscale的计算,这里先用fps计算qscale的初值,
			后续使用cuTree进行调整,因为cuTree当中就包括了复杂度的信息
		(2)否则,使用blurredComplexity复杂度直接计算qscale初值
	*/
    if (m_param->rc.cuTree && !m_param->rc.hevcAq)
    {	// 如果使用cuTree并且不启用hevcAq,使用时间尺度(fps)来调整qscale
        // Scale and units are obtained from rateNum and rateDenom for videos with fixed frame rates.
        double timescale = (double)m_param->fpsDenom / (2 * m_param->fpsNum);
        q = pow(BASE_FRAME_DURATION / CLIP_DURATION(2 * timescale), 1 - m_param->rc.qCompress);
    }
    else	// 否则使用复杂度来调整qscale
        q = pow(rce->blurredComplexity, 1 - m_param->rc.qCompress);

    // avoid NaN's in the Rceq
    if (rce->coeffBits + rce->mvBits == 0)
        q = m_lastQScaleFor[rce->sliceType];
    else
    {
        m_lastRceq = q;
        q /= rateFactor;	// 调整qscale
    }

    return q;
}

4.ABR模式

ABR模式全称为Average Bitrate模式,这种模式期望在编码过程中使用平均编码码率,即整体码率平稳,利于网络传输。与CRF模式相比,ABR模式的码率更加稳定,但可能会有一些质量的牺牲(主要体现在视频的质量波动)。如果调用时设置了bitrate,则会使用ABR模式

OPT("bitrate")
{
    p->rc.bitrate = atoi(value);
    p->rc.rateControlMode = X265_RC_ABR;
}

4.1 ABR的实现

ABR模式由rateEstimateQScale()实现,略去一部分代码之后,ABR相关代码如下所示

{
    /* 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
     * trade-off between quality and bitrate precision. But at large
     * tolerances, the bit distribution approaches that of 2pass. */
	/*
	(1)计算qscale,它将产生所需的平均比特率
	(2)根据当前帧的复杂度相对于到目前为止的平均复杂度(使用2pass RCEQ)来调制qscale
	(3)如果到目前为止总bit与目标bit相差甚远,还需要调整qscale
	
	结果:根据rate_tolerance的值,在质量和比特率精度之间存在权衡。
		但在较大的容差下,比特分配接近2pass
	*/
    double overflow = 1;
    double lqmin = m_lmin[m_sliceType];
    double lqmax = m_lmax[m_sliceType];
	/*
		(1) m_shortTermCplxSum自乘0.5,表示前一个cplx的权重系数为0.5
		(2) m_shortTermCplxSum加上当前的SATD损失(增加了fps的考虑)
		(3) blurredComplexity表示cplx的平均值,即平均复杂度
	*/
    m_shortTermCplxSum *= 0.5;
    m_shortTermCplxCount *= 0.5;
    m_shortTermCplxSum += m_currentSatd / (CLIP_DURATION(m_frameDuration) / BASE_FRAME_DURATION);
    m_shortTermCplxCount++;
    /* coeffBits to be used in 2-pass */
    rce->coeffBits = (int)m_currentSatd;
    rce->blurredComplexity = m_shortTermCplxSum / m_shortTermCplxCount;
    rce->mvBits = 0;
    rce->sliceType = m_sliceType;

    if (m_param->rc.rateControlMode == X265_RC_CRF)
    {
        q = getQScale(rce, m_rateFactorConstant);
        x265_zone* zone = getZone();
        if (zone)
        {
            if (zone->bForceQp)
                q = x265_qp2qScale(zone->qp);
            else
                q /= zone->bitrateFactor;
        }
    }
    else
    {	
        if (!m_param->rc.bStatRead)
            checkAndResetABR(rce, false);
		/*
			在ABR模式下,期望保持平均码率的恒定
			(1) m_wantedBitsWindow += m_bitrate * m_frameDuration; 
				表示目标编码比特(窗口时间内)
			(2) m_cplxrSum += (bits * x265_qp2qScale(rce->qpaRc) / rce->qRceq) - (rce->rowCplxrSum);
				表示sum of bits * qscale/rceq,其中rceq是一个码控调控因子,所以
				bits * qscale / rceq表示的是复杂度,这是基于一个假设,即 bits * qscale ∝ complexity
			(3) m_wantedBitsWindow / m_cplxrSum 可以理解为单位复杂度需要的比特数,作为一个调控因子
		*/
        double initialQScale = getQScale(rce, m_wantedBitsWindow / m_cplxrSum);
        x265_zone* zone = getZone();
        if (zone)
        {
            if (zone->bForceQp)
                initialQScale = x265_qp2qScale(zone->qp);
            else
                initialQScale /= zone->bitrateFactor;
        }
		// 根据feedback调整abr的qscale
        double tunedQScale = tuneAbrQScaleFromFeedback(initialQScale);
		// 计算溢出比例
        overflow = tunedQScale / initialQScale;
		/* 
		m_partialResidualFrames使得编码器可以将部分I帧的编码成本分摊到后续的帧中 ,
		这样可以避免在码率控制过程中过度补偿关键帧的高成本,实现更平滑的码率控制
		(1) m_partialResidualFrames = X265_MIN((int)rce->amortizeFrames, m_param->keyframeMax);
		默认取值
			(a) rce->amortizeFrames = 75;
			(b) m_param->keyframeMax = 250;
		(2) 如果使用m_partialResidualFrames,则使用修正之后的tunedQscale
		*/ 
        q = !m_partialResidualFrames? tunedQScale : initialQScale;
		// 检查是否完成编码帧数的75%,即编码的结尾
        bool isEncodeEnd = (m_param->totalFrames && 
            m_framesDone > 0.75 * m_param->totalFrames) ? 1 : 0;
		// 检查是否是编码的开始
        bool isEncodeBeg = m_framesDone < (int)(m_fps + 0.5);
        if (m_isGrainEnabled) // m_isGrainEnabled默认为0
        {
            if(m_sliceType!= I_SLICE && m_framesDone && !isEncodeEnd &&
                ((overflow < 1.05 && overflow > 0.95) || isEncodeBeg))
            {
                q = tuneQScaleForGrain(overflow);
            }
        }
    }

4.2 VBV技术

VBV全称为Video Buffer Verifier,即视频缓冲区校验,通常用于调控ABR模式的技术。因为ABR模式不能保证完全的平均码率,由于视频质量的突变或其他一些原因,ABR模式下的码率不一定能够保持平稳,所以添加了VBV这一技术,用于控制码率的平稳。如果当前使用ABR模式,并且使用了VBV技术,则当前的模式就是CBR(Const Bitrate)模式

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

举例来说,如果视频场景出现了突变,导致编码码率突然增大,VBV此时会增大qscale值,降低编码码率;反过来,视频编码码率偏低时,VBV会减小qscale值,以高质量编码,增大编码码率

VBV的实现分为几个步骤:
(1)VBV初始化(init)
(2)VBV在Lookahead中的计算,获取预期的framecost和frametype(vbvLookahead)
(3)计算VBV输入速率(updateVbvPlan)
(4)添加Vbv限制(clipQscale)
(5)行级VBV控制(rowVbvRateControl)

4.2.1 VBV初始化

VBV的初始化位于RateControl::init()函数中

bool RateControl::init(const SPS& sps)
{
    if (m_isVbv && !m_initVbv) // 使用vbv但没有初始化vbv信息
    {
        /* We don't support changing the ABR bitrate right now,
         * so if the stream starts as CBR, keep it CBR. */
		// VBV缓冲区大小不能小于单帧大小, vbvBufferSize为VBV整个缓冲区的大小
        if (m_param->rc.vbvBufferSize < (int)(m_param->rc.vbvMaxBitrate / m_fps))
        {
            m_param->rc.vbvBufferSize = (int)(m_param->rc.vbvMaxBitrate / m_fps);
            x265_log(m_param, X265_LOG_WARNING, "VBV buffer size cannot be smaller than one frame, using %d kbit\n",
                     m_param->rc.vbvBufferSize);
        }
		// 转换成kilo单位
        int vbvBufferSize = m_param->rc.vbvBufferSize * 1000;
        int vbvMaxBitrate = m_param->rc.vbvMaxBitrate * 1000;

        if (m_param->bEmitHRDSEI && !m_param->decoderVbvMaxRate) // bEmitHRDSEI默认为0
        {
            const HRDInfo* hrd = &sps.vuiParameters.hrdParameters;
            vbvBufferSize = hrd->cpbSizeValue << (hrd->cpbSizeScale + CPB_SHIFT);
            vbvMaxBitrate = hrd->bitRateValue << (hrd->bitRateScale + BR_SHIFT);
        }
		// 每帧后添加到buffer_fill的比特数
        m_bufferRate = vbvMaxBitrate / m_fps;	// 水池最大每帧输入水量 (处理每帧消耗的最大bit (bits/frame))
        m_vbvMaxRate = vbvMaxBitrate;			// 水池最大输入速率 (单位时间输入到VBV缓冲区的bit)
        m_bufferSize = vbvBufferSize;			// 水池最大容量 (bits)
		// 如果每帧消耗的比特数 * 1.1 大于VBV缓冲区大小,说明水池比较小,仅能容纳一帧
        m_singleFrameVbv = m_bufferRate * 1.1 > m_bufferSize; // 是否是单帧VBV
		
        if (m_param->rc.vbvBufferInit > 1.)	// vbvBufferInit表示VBV初始水量比例,默认为0.9
            m_param->rc.vbvBufferInit = x265_clip3(0.0, 1.0, m_param->rc.vbvBufferInit / m_param->rc.vbvBufferSize);
        if (m_param->vbvBufferEnd > 1.)		// vbvBufferEnd表示VBV结束水量比例,默认为0
            m_param->vbvBufferEnd = x265_clip3(0.0, 1.0, m_param->vbvBufferEnd / m_param->rc.vbvBufferSize);
        if (m_param->vbvEndFrameAdjust > 1.)	// vbvEndFrameAdjust表示允许编码器在编码最后一帧时,调整qp从而确保VBV缓冲区的水平符合预期, 默认为0
            m_param->vbvEndFrameAdjust = x265_clip3(0.0, 1.0, m_param->vbvEndFrameAdjust);
        m_param->rc.vbvBufferInit = x265_clip3(0.0, 1.0, X265_MAX(m_param->rc.vbvBufferInit, m_bufferRate / m_bufferSize));
        m_bufferFillFinal = m_bufferSize * m_param->rc.vbvBufferInit;	// 当前时刻剩余的水量
        m_bufferFillActual = m_bufferFillFinal;
        m_bufferExcess = 0;
        m_initVbv = true;	// 已初始化Vbv信息
    }
    // ...
}

4.2.2 VBV在lookahead中的计算

在lookahead模块中,会计算低分辨率的用于VBV的帧级损失,用于后续码率控制模块的VBV调控,具体使用vbvFrameCost()来实现。
(1)跳过连续的B帧
(2)遍历lookahead中的帧,计算损失及预测类型(vbvFrameCost)
(3)下一帧的类型待定,即AUTO

代码会考虑到B帧的计算,在这里我只简单考虑 I P P P … 这种结构

void Lookahead::vbvLookahead(Lowres **frames, int numFrames, int keyframe)
{
    int prevNonB = 0, curNonB = 1, idx = 0;
	// 1.找到第一个非B帧
    while (curNonB < numFrames && IS_X265_TYPE_B(frames[curNonB]->sliceType))
        curNonB++;
    int nextNonB = keyframe ? prevNonB : curNonB;
    int nextB = prevNonB + 1;
    int nextBRef = 0, curBRef = 0;
    if (m_param->bBPyramid && curNonB - prevNonB > 1)
        curBRef = (prevNonB + curNonB + 1) / 2;
    /*
		miniGopEnd表示一个小gop的结束
		(1) 如果是keyframe,说明gop结束的位置位于前一个节点,即prevNonB
		(2) 如果不是keyframe,则将当前节点作为gop结束的位置,即curNonB
		
		PS: 如果是 I P P P P ...结构,则miniGopEnd为0 (keyframe) 或1 (not keyframe)
	*/
    int miniGopEnd = keyframe ? prevNonB : curNonB;
	// 2.遍历lookahead中的帧,计算损失及预测帧类型
    while (curNonB <= numFrames)
    {
        /* P/I cost: This shouldn't include the cost of nextNonB */
        if (nextNonB != curNonB)
        {
        	// 如果当前帧为I帧,则p0 = curNonB
            int p0 = IS_X265_TYPE_I(frames[curNonB]->sliceType) ? curNonB : prevNonB;
			// 预估SATD损失,如果p0 = curNonB,则计算I帧的损失
            frames[nextNonB]->plannedSatd[idx] = vbvFrameCost(frames, p0, curNonB, curNonB);
			// 预估帧类型
            frames[nextNonB]->plannedType[idx] = frames[curNonB]->sliceType;

            /* Save the nextNonB Cost in each B frame of the current miniGop */
            // 如果当前节点已经不再属于前一个gop,说明进入新的gop
            if (curNonB > miniGopEnd) 
            {
            	// nextB = 1, 如果是 IPPPPP 结构,miniGopEnd为0或1,不会进入分支
                for (int j = nextB; j < miniGopEnd; j++)
                {
                    frames[j]->plannedSatd[frames[j]->indB] = frames[nextNonB]->plannedSatd[idx];
                    frames[j]->plannedType[frames[j]->indB++] = frames[nextNonB]->plannedType[idx];
                }
            }
            idx++;
        }

        /* Handle the B-frames: coded order */
        if (m_param->bBPyramid && curNonB - prevNonB > 1)
            nextBRef = (prevNonB + curNonB + 1) / 2;
		// 按照编码顺序来处理B帧
        for (int i = prevNonB + 1; i < curNonB; i++, idx++)
        {
            int64_t satdCost = 0;
            int type = X265_TYPE_B;
            if (nextBRef)
            {
                if (i == nextBRef)
                {
                    satdCost = vbvFrameCost(frames, prevNonB, curNonB, nextBRef);
                    type = X265_TYPE_BREF;
                }
                else if (i < nextBRef)
                    satdCost = vbvFrameCost(frames, prevNonB, nextBRef, i);
                else
                    satdCost = vbvFrameCost(frames, nextBRef, curNonB, i);
            }
            else
                satdCost = vbvFrameCost(frames, prevNonB, curNonB, i);
            frames[nextNonB]->plannedSatd[idx] = satdCost;
            frames[nextNonB]->plannedType[idx] = type;
            /* Save the nextB Cost in each B frame of the current miniGop */

            for (int j = nextB; j < miniGopEnd; j++)
            {
                if (curBRef && curBRef == i)
                    break;
                if (j >= i && j !=nextBRef)
                    continue;
                frames[j]->plannedSatd[frames[j]->indB] = satdCost;
                frames[j]->plannedType[frames[j]->indB++] = type;
            }
        }
        prevNonB = curNonB;
        curNonB++;
        while (curNonB <= numFrames && IS_X265_TYPE_B(frames[curNonB]->sliceType))
            curNonB++;
    }
	// 3.下一帧的类型待定
    frames[nextNonB]->plannedType[idx] = X265_TYPE_AUTO;
}

其中,vbvFrameCost()的实现方式如下,首先计算intra&inter损失,随后根据aq和cuTree的使用情况来重新计算损失,使用的函数是frameCostRecalculate()

/*
  (1) p0 : 前向参考帧
  (2) p1 : 后向参考帧
  (3) b  : 当前帧
  
  (1) 如果p0 = p1 = b,说明是Intra帧
  (2) 如果p1 = b,说明是前向参考的P帧
*/
int64_t Lookahead::vbvFrameCost(Lowres **frames, int p0, int p1, int b)
{
    CostEstimateGroup estGroup(*this, frames);
	// 计算单帧损失 (intra, inter cost)
    int64_t cost = estGroup.singleCost(p0, p1, b);

	// 如果使用了aq或cuTree,需要重新计算损失
    if (m_param->rc.aqMode || m_param->bAQMotion)
    {
        if (m_param->rc.cuTree)
            return frameCostRecalculate(frames, p0, p1, b);
        else
            return frames[b]->costEstAq[b - p0][p1 - b];
    }

    return cost;
}

frameCostRecalculate()的实现如下所示,会根据前面计算的qp_adj来调整帧的损失值

/* If MB-tree changes the quantizers, we need to recalculate the frame cost without
 * re-running lookahead. */
int64_t Lookahead::frameCostRecalculate(Lowres** frames, int p0, int p1, int b)
{
    if (frames[b]->sliceType == X265_TYPE_B)
        return frames[b]->costEstAq[b - p0][p1 - b];

    int64_t score = 0;
    int *rowSatd = frames[b]->rowSatds[b - p0][p1 - b];

    x265_emms();

    if (m_param->rc.hevcAq) // 一般不使用hevcAq模式
    {
        // ...
    }
    else
    {
        double *qp_offset = frames[b]->qpCuTreeOffset;

        for (int cuy = m_8x8Height - 1; cuy >= 0; cuy--)
        {
            rowSatd[cuy] = 0;
            for (int cux = m_8x8Width - 1; cux >= 0; cux--)
            {
                int cuxy = cux + cuy * m_8x8Width;
                int cuCost = frames[b]->lowresCosts[b - p0][p1 - b][cuxy] & LOWRES_COST_MASK;
                double qp_adj;
                if (m_param->rc.qgSize == 8)
                    qp_adj = (qp_offset[cux * 2 + cuy * m_8x8Width * 4] +
                    qp_offset[cux * 2 + cuy * m_8x8Width * 4 + 1] +
                    qp_offset[cux * 2 + cuy * m_8x8Width * 4 + frames[b]->maxBlocksInRowFullRes] +
                    qp_offset[cux * 2 + cuy * m_8x8Width * 4 + frames[b]->maxBlocksInRowFullRes + 1]) / 4;
                else 
                    qp_adj = qp_offset[cuxy];
				// 根据qp_adj来调整帧的损失
                cuCost = (cuCost * x265_exp2fix8(qp_adj) + 128) >> 8;
                rowSatd[cuy] += cuCost;
                if ((cuy > 0 && cuy < m_8x8Height - 1 &&
                    cux > 0 && cux < m_8x8Width - 1) ||
                    m_8x8Width <= 2 || m_8x8Height <= 2)
                {
                    score += cuCost;
                }
            }
        }
    }

    return score;
}

4.2.3 计算VBV输入速率(updateVbvPlan)

updateVBVPlan()根据目前正在进行的所有帧预测的比特数临时更新VBV,在码率控制开始的位置rateControlStart()中被调用

void RateControl::updateVbvPlan(Encoder* enc)
{
	/*  (1) m_bufferFill表示当前水位,
		(2) m_bufferFillFinal表示经过前面计算之后的最终水位
		将之前的最终水位作为新一轮Vbv的初始水位 */
    m_bufferFill = m_bufferFillFinal; 
    enc->updateVbvPlan(this);
}

enc->updateVbvPlan的实现如下,考虑了多线程问题

void Encoder::updateVbvPlan(RateControl* rc)
{
	/*  frameNumThreads表示含义为
		(1) 帧级并行线程数,通常为2-6之间
		(2) 使用不止1个线程,会影响运动搜索的下方搜索,但其他行为不影响
		(3) 使用CQP模式时,即便使用帧级并行,不影响输出码流
		(4) 线程数量增加,可能对RC产生负面影响,因为额外的并发性增加了比特率估计的不确定性
		(5) 帧并行性通常受限于cpu的行数 */
    for (int i = 0; i < m_param->frameNumThreads; i++)
    {
        FrameEncoder *encoder = m_frameEncoder[i];
		// 如果当前encoder的RC被激活,并且poc不等于当前slice的poc
        if (encoder->m_rce.isActive && encoder->m_rce.poc != rc->m_curSlice->m_poc)
        {
			/*
				(1) bEnableConstVbv默认为0
				(2) frameSizePlanned表示根据预估的qscale,获取的预估的frame消耗的比特
				(3) frameSizeEstimated表示根据CU级RC更新的比特
			*/
            int64_t bits = m_param->rc.bEnableConstVbv ? (int64_t)encoder->m_rce.frameSizePlanned : (int64_t)X265_MAX(encoder->m_rce.frameSizeEstimated, encoder->m_rce.frameSizePlanned);
            rc->m_bufferFill -= bits;	// m_bufferFill表示当前水位,bits表示出水
            // 检查出水之后,当前水位是否低于0
            rc->m_bufferFill = X265_MAX(rc->m_bufferFill, 0); 
            rc->m_bufferFill += encoder->m_rce.bufferRate;		// 入水
            // 检查入水之后,当前水位是否超出总容量
            rc->m_bufferFill = X265_MIN(rc->m_bufferFill, rc->m_bufferSize);
            if (rc->m_2pass)
                rc->m_predictedBits += bits;
        }
    }
}

4.2.4 添加Vbv限制(clipQscale)

实际执行VBV限制的函数为clipQscale(),粗略来说,根据已编码的信息对VBV进行帧级调控
(1)如果使用Lookahead,会尝试将VBV中的水量控制在50%~80%之间,并以此为目标来调整qscale
(2)如果不使用Lookahead,会按照当前水位情况来粗略调整qscale

double RateControl::clipQscale(Frame* curFrame, RateControlEntry* rce, double q)
{
    // B-frames are not directly subject to VBV,
    // since they are controlled by referenced P-frames' QPs.
	// b帧不直接受到VBV的影响,因为它们是由参考p帧的qp控制的
    double lmin = m_lmin[rce->sliceType];
    double lmax = m_lmax[rce->sliceType];
    double q0 = q;
	// 进行VBV处理
    if (m_isVbv && m_currentSatd > 0 && curFrame)
    {
        if (m_param->lookaheadDepth || m_param->rc.cuTree ||
            (m_param->scenecutThreshold || m_param->bHistBasedSceneCut) ||
            (m_param->bFrameAdaptive && m_param->bframes))
        {
           /* Lookahead VBV: If lookahead is done, 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. */
			/*
				1.Lookahead VBV: 如果完成了lookahead,必要时提高量化器,
				这样在lookahead结束时,没有帧溢出,缓冲区处于合理的状态
			*/
            int loopTerminate = 0;
            /* Avoid an infinite loop. */
            /*  根据当前的qscale,计算应用Vbv之后的情况
				(1) 执行至多1000次的循环
				(2) 或者loopTerminate = 3 (水位满足要求)
					(a) 水位不低于50%
					(b) 水位不高于80% */
            for (int iterations = 0; iterations < 1000 && loopTerminate != 3; iterations++)
            {
                double frameQ[3];
                double curBits;
				// 根据当前的qscale和不同帧类型的预测器,来预测消耗比特数
                curBits = predictSize(&m_pred[m_predType], q, (double)m_currentSatd);
				// m_bufferFill表示当前的水位
                double bufferFillCur = m_bufferFill - curBits;  // 剩余水位 = 当前水位 - 输出水量
                double targetFill;
                double totalDuration = m_frameDuration;
				// 计算3种帧类型的qscale
                frameQ[P_SLICE] = m_sliceType == I_SLICE ? q * m_param->rc.ipFactor : (m_sliceType == B_SLICE ? q / m_param->rc.pbFactor : q);
                frameQ[B_SLICE] = frameQ[P_SLICE] * m_param->rc.pbFactor;
                frameQ[I_SLICE] = frameQ[P_SLICE] / m_param->rc.ipFactor;
                /* Loop over the planned future frames. */
				// 预估未来帧的消耗
                bool iter = true;
                for (int j = 0; bufferFillCur >= 0 && iter ; j++)
                {
                    int type = curFrame->m_lowres.plannedType[j];
                    if (type == X265_TYPE_AUTO || totalDuration >= 1.0)
                        break;
                    totalDuration += m_frameDuration;
                    double wantedFrameSize = m_vbvMaxRate * m_frameDuration; // wantedFrameSize表示要加的水量
                    if (bufferFillCur + wantedFrameSize <= m_bufferSize) // 检查是否可以加水
                        bufferFillCur += wantedFrameSize;
                    // plannedSatd在vbvLookahead当中计算获得
                    int64_t satd = curFrame->m_lowres.plannedSatd[j] >> (X265_DEPTH - 8);
                    type = IS_X265_TYPE_I(type) ? I_SLICE : IS_X265_TYPE_B(type) ? B_SLICE : P_SLICE;
                    // plannedType在vbvLookahead当中计算获得
                    int predType = getPredictorType(curFrame->m_lowres.plannedType[j], type);
                    curBits = predictSize(&m_pred[predType], frameQ[type], (double)satd); // 预估需要的比特数
                    bufferFillCur -= curBits; // 减去当前输入水量
                    if (!m_param->bResetZoneConfig && ((uint64_t)j == (m_param->reconfigWindowSize - 1)))
                        iter = false;
                }
				// 是否允许进行最后一帧的qp调整,vbvEndAdj默认为0
                if (rce->vbvEndAdj)
                {
                    bool loopBreak = false;
                    double bufferDiff = m_param->vbvBufferEnd - (m_bufferFill / m_bufferSize);
                    rce->targetFill = m_bufferFill + m_bufferSize * (bufferDiff / (m_param->totalFrames - rce->encodeOrder));
                    if (bufferFillCur < rce->targetFill)
                    {
                        q *= 1.01;
                        loopTerminate |= 1;
                        loopBreak = true;
                    }
                    if (bufferFillCur > m_param->vbvBufferEnd * m_bufferSize)
                    {
                        q /= 1.01;
                        loopTerminate |= 2;
                        loopBreak = true;
                    }
                    if (!loopBreak)
                        break;
                }
                else
                {
                    /* Try to get the buffer at least 50% filled, but don't set an impossible goal. */
					// 尽量保持不低于50%的水位
                    double finalDur = 1;
                    if (m_param->rc.bStrictCbr)
                    {
                        finalDur = x265_clip3(0.4, 1.0, totalDuration);
                    }
                    targetFill = X265_MIN(m_bufferFill + totalDuration * m_vbvMaxRate * 0.5, m_bufferSize * ((m_param->minVbvFullness / 100) * finalDur));
                    // 如果当前水位 < 目标水位,说明出水偏多,需要减少出水,则适当增加qscale
					if (bufferFillCur < targetFill)
                    {
                        q *= 1.01;	// qscale增大的量为经验性的1%
                        loopTerminate |= 1;	// 满足水位不低于50%的条件
                        continue;
                    }
                    /* Try to get the buffer not more than 80% filled, but don't set an impossible goal. */
					// 尽量保持不高于80%的水位
                    targetFill = x265_clip3(m_bufferSize * ((m_param->maxVbvFullness / 100) * finalDur), m_bufferSize, m_bufferFill - totalDuration * m_vbvMaxRate * 0.5);
                    // 如果是CBR模式,并且当前水位 > 目标水位,说明出水偏少,需要增加出水,则适当降低qscale
					if ((m_isCbr || m_2pass) && bufferFillCur > targetFill && !m_isSceneTransition)
                    {
                        q /= 1.01;
                        loopTerminate |= 2;	// 满足水位不高于80%的条件
                        continue;
                    }
                    break;
                }
            }
            q = X265_MAX(q0 / 2, q);
        }
        else // 不使用lookahead
        {
            /* Fallback to old purely-reactive algorithm: no lookahead. */
			// 如果是P或B帧,或连续的I帧
            if ((m_sliceType == P_SLICE || m_sliceType == B_SLICE ||
                    (m_sliceType == I_SLICE && m_lastNonBPictType == I_SLICE)) &&
                m_bufferFill / m_bufferSize < (m_param->minVbvFullness / 100))
            {
                q /= x265_clip3(0.5, 1.0, 2.0 * m_bufferFill / m_bufferSize);
            }
            // Now a hard threshold to make sure the frame fits in VBV.
            // This one is mostly for I-frames.
			// 使用一个强阈值来保证帧适应VBV,这个经常用于intra帧
            double bits = predictSize(&m_pred[m_predType], q, (double)m_currentSatd);

            // For small VBVs, allow the frame to use up the entire VBV.
			// 对于小VBV,允许帧使用整个VBV
            double maxFillFactor;
            maxFillFactor = m_bufferSize >= 5 * m_bufferRate ? 2 : 1;
            // For single-frame VBVs, request that the frame use up the entire VBV.
			// 对于单帧VBV,要求帧使用整个VBV
            double minFillFactor = m_singleFrameVbv ? 1 : 2;

            for (int iterations = 0; iterations < 10; iterations++)
            {
                double qf = 1.0;
				// 如果预测的比特 > 剩余可用的比特数,需要减小qscale
                if (bits > m_bufferFill / maxFillFactor)
                    qf = x265_clip3(0.2, 1.0, m_bufferFill / (maxFillFactor * bits));
                q /= qf;
                bits *= qf;
				// 如果预测的比特 < 剩余可用的比特数,需要增大qscale
                if (bits < m_bufferRate / minFillFactor)
                    q *= bits * minFillFactor / m_bufferRate;
                bits = predictSize(&m_pred[m_predType], q, (double)m_currentSatd);
            }

            q = X265_MAX(q0, q);
        }

        /* Apply MinCR restrictions */
		// MinCR : Minimum Compression Ratio
		// 根据前面预测的qscale,预测消耗的比特数
        double pbits = predictSize(&m_pred[m_predType], q, (double)m_currentSatd);
        if (pbits > rce->frameSizeMaximum)
            q *= pbits / rce->frameSizeMaximum;
        /* To detect frames that are more complex in SATD costs compared to prev window, yet 
         * lookahead vbv reduces its qscale by half its value. Be on safer side and avoid drastic 
         * qscale reductions for frames high in complexity */
		// 为了检测更复杂的帧,lookahead vbv将其qscale折半。对于复杂的帧,尽量不要大幅降低qscale
        bool mispredCheck = rce->movingAvgSum && m_currentSatd >= rce->movingAvgSum && q <= q0 / 2;
        if (!m_isCbr || ((m_isAbr || m_2pass) && mispredCheck))
            q = X265_MAX(q0, q);

        if (m_rateFactorMaxIncrement)
        {
            double qpNoVbv = x265_qScale2qp(q0);
            double qmax = X265_MIN(lmax,x265_qp2qScale(qpNoVbv + m_rateFactorMaxIncrement));
            return x265_clip3(lmin, qmax, q);
        }
    }
    if (!curFrame && m_2pass) // 2pass
    {
        double min = log(lmin);
        double max = log(lmax);
        q = (log(q) - min) / (max - min) - 0.5;
        q = 1.0 / (1.0 + exp(-4 * q));
        q = q*(max - min) + min;
        return exp(q);
    }
	// 修正qscale
    return x265_clip3(lmin, lmax, q);
}

在上面的实现过程中,使用了predictSize()来预测使用的比特数,其实现方式如下

double RateControl::predictSize(Predictor *p, double q, double var)
{
    return (p->coeff * var + p->offset) / (q * p->count);
}

用公式来描述如下,即比特数与纹理复杂度、量化参数之间的关系。这种比较简易的关系使得qscale成为码率控制中的一个通用数据存储变量(或数据传递变量)
r = c ∗ c o m p l e x q s c a l e r = c * \frac{complex}{qscale} r=cqscalecomplex

4.2.5 行级VBV控制(rowVbvRateControl)

前面进行了帧级别的VBV控制,在行级也存在VBV控制,实现函数为rowVbvRateControl()。每编码完一行后,会利用预测模型对当前帧大小进行预测,然后对QP进行微调

int RateControl::rowVbvRateControl(Frame* curFrame, uint32_t row, RateControlEntry* rce, double& qpVbv, uint32_t* m_sliceBaseRow, uint32_t sliceId)
{
    FrameData& curEncData = *curFrame->m_encData;
    double qScaleVbv = x265_qp2qScale(qpVbv);
    uint64_t rowSatdCost = curEncData.m_rowStat[row].rowSatd;	// 行级SATD损失
    double encodedBits = curEncData.m_rowStat[row].encodedBits; // 已编码的比特

    rowSatdCost >>= X265_DEPTH - 8;
	// 根据行级SATD损失和已编码比特来更新预测器
    updatePredictor(rce->rowPred[0], qScaleVbv, (double)rowSatdCost, encodedBits);
	// 当前帧不为Intra帧,并且不使用const vbv,还会使用参考帧信息来更新预测器
    if (curEncData.m_slice->m_sliceType != I_SLICE && !m_param->rc.bEnableConstVbv)
    {
        Frame* refFrame = curEncData.m_slice->m_refFrameList[0][0];
        if (qpVbv < refFrame->m_encData->m_rowStat[row].rowQp)
        {
            uint64_t intraRowSatdCost = curEncData.m_rowStat[row].rowIntraSatd;
            intraRowSatdCost >>= X265_DEPTH - 8;
            updatePredictor(rce->rowPred[1], qScaleVbv, (double)intraRowSatdCost, encodedBits);
        }
    }

    int canReencodeRow = 1;
    /* tweak quality based on difference from predicted size */
    double prevRowQp = qpVbv;
    double qpAbsoluteMax = m_param->rc.qpMax;
    double qpAbsoluteMin = m_param->rc.qpMin;
    if (m_rateFactorMaxIncrement)
        qpAbsoluteMax = X265_MIN(qpAbsoluteMax, rce->qpNoVbv + m_rateFactorMaxIncrement);

    if (m_rateFactorMaxDecrement)
        qpAbsoluteMin = X265_MAX(qpAbsoluteMin, rce->qpNoVbv - m_rateFactorMaxDecrement);
	// qp clip
    double qpMax = X265_MIN(prevRowQp + m_param->rc.qpStep, qpAbsoluteMax);
    double qpMin = X265_MAX(prevRowQp - m_param->rc.qpStep, qpAbsoluteMin);
    double stepSize = 0.5;
    double bufferLeftPlanned = rce->bufferFill - rce->frameSizePlanned; // 当前水位 - 预计要消耗的水量 = 剩余水量

    const SPS& sps = *curEncData.m_slice->m_sps;
    double maxFrameError = X265_MAX(0.05, 1.0 / sps.numCuInHeight);
	// 是否为最后一行
    if (row < m_sliceBaseRow[sliceId + 1] - 1)
    {
        /* More threads means we have to be more cautious in letting ratecontrol use up extra bits. */
		// 根据VBV剩余水量, 计算RC tolerance
        double rcTol = bufferLeftPlanned / m_param->frameNumThreads * m_rateTolerance;
        int32_t encodedBitsSoFar = 0;
		// 预测当前帧消耗比特(accFrameBits),并且获取当前行及之前的行已经使用了多少比特(encodedBitsSoFar)
        double accFrameBits = predictRowsSizeSum(curFrame, rce, qpVbv, encodedBitsSoFar);
        double vbvEndBias = 0.95;

        /* * Don't increase the row QPs until a sufficent amount of the bits of
         * the frame have been processed, in case a flat area at the top of the
         * frame was measured inaccurately. */
        // 如果已编码比特不足预估比特的 5%,则将前一行的qp作为qp的最大值
        // 这表示后续的qp都会小于等于前一行的qp,即后续的行都会以更高质量进行编码
        if (encodedBitsSoFar < 0.05f * rce->frameSizePlanned)
            qpMax = qpAbsoluteMax = prevRowQp;
		// 如果当前帧不是I帧,将rc容忍度减半
        if (rce->sliceType != I_SLICE || (m_param->rc.bStrictCbr && rce->poc > 0))
            rcTol *= 0.5;

        if (!m_isCbr)
            qpMin = X265_MAX(qpMin, rce->qpNoVbv);
		// m_wantedBitsWindow表示窗口期内期望的比特数
        double totalBitsNeeded = m_wantedBitsWindow;
        // totalFrames表示需要编码的总帧数
        if (m_param->totalFrames)
            totalBitsNeeded = (m_param->totalFrames * m_bitrate) / m_fps;
        // ABR模式下超出预期比特数的比例 = (当前帧可能使用的总比特 + 所有已编码帧使用的比特 - 窗口期内期望的比特数 ) / 总共需要的比特数
        double abrOvershoot = (accFrameBits + m_totalBits - m_wantedBitsWindow) / totalBitsNeeded;
		/*
			当前qp小于最大qp,并且满足以下条件之一时,会增大qp
			(1) 当前帧可能使用的总比特 > 预估使用的比特 + 容忍误差
			(2) 当前帧可能使用的总比特 > 当前buffer水位 - buffer预估剩余水位 * 0.5
			(3) 当前帧可能使用的总比特 > 预估使用的比特 && qp < qpNoVbv
			(4) 允许vbvEndAdj && 当前帧可能使用的总比特 > 当前buffer水位 - 目标水位 * vbvEndBias 
			
			必须满足条件:
			使用bStrictCbr 或 abrOverShoot > 0.1
			PS: bStrictCbr表示使用严格Cbr,否则允许一定量的码率波动,默认为0
		*/
        while (qpVbv < qpMax
               && (((accFrameBits > rce->frameSizePlanned + rcTol) ||
                   (rce->bufferFill - accFrameBits < bufferLeftPlanned * 0.5) || // accFrameBits > rce->bufferFill - bufferLeftPlanned * 0.5
                   (accFrameBits > rce->frameSizePlanned && qpVbv < rce->qpNoVbv) ||
                   (rce->vbvEndAdj && ((rce->bufferFill - accFrameBits) < (rce->targetFill * vbvEndBias)))) // accFrameBits > rce->bufferFill - rce->targetFill * vbvEndBias
                   && (!m_param->rc.bStrictCbr ? 1 : abrOvershoot > 0.1)))
        {
        	// 增大qp,增加的单位为stepSize
            qpVbv += stepSize;
            // 根据新的qp
            accFrameBits = predictRowsSizeSum(curFrame, rce, qpVbv, encodedBitsSoFar);
            abrOvershoot = (accFrameBits + m_totalBits - m_wantedBitsWindow) / totalBitsNeeded;
        }
		/*
			当前qp > qpMin,并且满足以下所有条件,则减小qp
			(1) qp大于当前行qp 或 使用单帧VBV
			(2) 当前帧可能使用的总比特 < (当前水位 - 总水位 + 输入水量) * 1.1
				或 使用vbvEndAdj && 当前帧可能使用的总比特 < 当前水位 - 目标水位 * vbvEndBias
			
			必须满足条件:
			使用bStrictCbr 或 abrOverShoot < 0
		*/
        while (qpVbv > qpMin
               && (qpVbv > curEncData.m_rowStat[0].rowQp || m_singleFrameVbv)
               && (((accFrameBits < rce->frameSizePlanned * 0.8f && qpVbv <= prevRowQp)
                   || accFrameBits < (rce->bufferFill - m_bufferSize + m_bufferRate) * 1.1
                   // accFrameBits < rce->bufferFill - rce->targetFill * vbvEndBias
                   || (rce->vbvEndAdj && ((rce->bufferFill - accFrameBits) > (rce->targetFill * vbvEndBias))))
                   && (!m_param->rc.bStrictCbr ? 1 : abrOvershoot < 0)))
        {
        	// 降低qp
            qpVbv -= stepSize;
            // 根据新计算的qp来预测当前帧可能使用的比特数
            accFrameBits = predictRowsSizeSum(curFrame, rce, qpVbv, encodedBitsSoFar);
            abrOvershoot = (accFrameBits + m_totalBits - m_wantedBitsWindow) / totalBitsNeeded;
        }
		// 如果使用严格的CBR
        if (m_param->rc.bStrictCbr && m_param->totalFrames)
        {
            double timeDone = (double)(m_framesDone) / m_param->totalFrames;
            // timeDone > 0.75表示接近视频的末尾,abrOverShoot表示未来可能使用的比特略比预期多一些,此时适当增大qp
            while (qpVbv < qpMax && (qpVbv < rce->qpNoVbv + (m_param->rc.qpStep * timeDone)) &&
                   (timeDone > 0.75 && abrOvershoot > 0))
            {
                qpVbv += stepSize;
                accFrameBits = predictRowsSizeSum(curFrame, rce, qpVbv, encodedBitsSoFar);
                abrOvershoot = (accFrameBits + m_totalBits - m_wantedBitsWindow) / totalBitsNeeded;
            }
            // 再适当减小一点qp
            if (qpVbv > curEncData.m_rowStat[0].rowQp &&
                abrOvershoot < -0.1 && timeDone > 0.5 && accFrameBits < rce->frameSizePlanned - rcTol)
            {
                qpVbv -= stepSize;
                accFrameBits = predictRowsSizeSum(curFrame, rce, qpVbv, encodedBitsSoFar);
            }
        }

        /* avoid VBV underflow or MinCr violation */
        // 避免VBV下溢
        while ((qpVbv < qpAbsoluteMax)
               && ((rce->bufferFill - accFrameBits < m_bufferRate * maxFrameError) ||
                   (rce->frameSizeMaximum - accFrameBits < rce->frameSizeMaximum * maxFrameError)))
        {
            qpVbv += stepSize;
            accFrameBits = predictRowsSizeSum(curFrame, rce, qpVbv, encodedBitsSoFar);
        }

        rce->frameSizeEstimated = accFrameBits;
		
        /* If the current row was large enough to cause a large QP jump, try re-encoding it. */
        // 如果当前行的qp过大,可能会产生qp大幅跳变,尝试重新编码
        if (qpVbv > qpMax && prevRowQp < qpMax && canReencodeRow)
        {
            /* Bump QP to halfway in between... close enough. */
            qpVbv = x265_clip3(prevRowQp + 1.0f, qpMax, (prevRowQp + qpVbv) * 0.5);
            return -1;
        }

        if (m_param->rc.rfConstantMin)
        {
            if (qpVbv < qpMin && prevRowQp > qpMin && canReencodeRow)
            {
                qpVbv = x265_clip3(qpMin, prevRowQp, (prevRowQp + qpVbv) * 0.5);
                return -1;
            }
        }
    }
    else
    {	// 如果当前行为最后一行
        int32_t encodedBitsSoFar = 0;
        rce->frameSizeEstimated = predictRowsSizeSum(curFrame, rce, qpVbv, encodedBitsSoFar);

        /* Last-ditch attempt: if the last row of the frame underflowed the VBV,
         * try again. */
        // 如果最后一行使得VBV下溢,再尝试一次编码
        if ((rce->frameSizeEstimated > (rce->bufferFill - m_bufferRate * maxFrameError) &&
             qpVbv < qpMax && canReencodeRow))
        {
            qpVbv = qpMax;
            return -1;
        }
    }
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2149560.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

阿里云容器服务Kubernetes部署新服务

这里部署的是前端项目 1.登录控制台-选择集群 2.选择无状态-命名空间-使用镜像创建 3.填写相关信息 应用基本信息&#xff1a; 容器配置&#xff1a; 高级配置&#xff1a; 创建成功后就可以通过30006端口访问项目了

XML:DOM4j解析XML

XML简介&#xff1a; 什么是XML&#xff1a;XML 是独立于软件和硬件的信息传输工具。 XML 的设计宗旨是传输数据&#xff0c;而不是显示数据。XML 标签没有被预定义。您需要自行定义标签。XML不会做任何事情&#xff0c;XML被设计用来结构化、存储以及传输信息。 XML可以发明…

再次理解UDP协议

一、再谈端口号 在 TCP / IP 协议中&#xff0c;用 "源 IP", "源端口号", "目的 IP", "目的端口号", "协议号" 这样一个五元组来标识一个通信(可以通过 netstat -n 查看) 我们需要端口号到进程的唯一性&#xff0c;所以一个…

工业控制系统等保2.0定级备案经验分享

工业控制系统和传统IT系统有所差异&#xff0c;须单独划分定级对象 工业控制系统定级时将现场采集/执行、现场控制和过程控制等要素应作为一个整体对象定级&#xff0c;各要素不单独定级&#xff1b;生产管理要素可单独定级。对于大型工业控制系统&#xff0c;可以根据系统功能…

Node-red 某一时间范围内满足条件的数据只返回一次

厂子里有个业务需求增加一段逻辑&#xff0c;根据点位数值&#xff0c;判断是否让mes执行之后的逻辑。 网关采集周期5s/次&#xff0c;及数据上报周期5s/次; iot通过网关写入时间为8s左右&#xff1b; 同类设备共用一条规则链&#xff1b; 想当触发条件时修改”完成上传“不…

SimpleAISearch:C# + DuckDuckGo 实现简单的AI搜索

最近AI搜索很火爆&#xff0c;有Perplexity、秘塔AI、MindSearch、Perplexica、memfree、khoj等等。 在使用大语言模型的过程中&#xff0c;或许你也遇到了这种局限&#xff0c;就是无法获取网上最新的信息&#xff0c;导致回答的内容不是基于最新的信息&#xff0c;为了解决这…

[Linux#55][网络协议] 序列化与反序列化 | TcpCalculate为例

目录 1. 理解协议 1.1 结构化数据的传输 序列化与反序列化 代码感知&#xff1a; Request 类 1. 构造函数 2. 序列化函数&#xff1a;Serialize() 3. 反序列化函数&#xff1a;DeSerialize() 补充 4. 成员变量 Response 类 1. 构造函数 2. 序列化函数&#xff1a;…

力扣 中等 2300.咒语和药水的成功对数

文章目录 题目介绍解法 题目介绍 解法 class Solution {public int[] successfulPairs(int[] spells, int[] potions, long success){Arrays.sort(potions);int n spells.length, m potions.length;int[] pairs new int[n];for (int i 0; i < n; i) {int left 0, righ…

无消息传递的图变换器中的图归纳偏差

人工智能咨询培训老师叶梓 转载标明出处 在处理小规模数据集时&#xff0c;图变换器的性能通常不尽如人意&#xff0c;特别是在需要明显的归纳偏好时。为了引入这些偏好&#xff0c;早期的图变换器一般会利用消息传递组件或位置编码。然而&#xff0c;依赖消息传递的图变换器在…

C# AutoResetEvent ManualResetEvent Mutex 对比

三个函数功能类似&#xff0c;都是线程同步的主要函数。但在使用上有一些差别。 关于代码的使用&#xff0c;帖子很多。形象的用图来描述一下。

【Meta分析】IF=12.1!人工智能预测模型Meta分析怎么做?

预测模型的Meta分析 人工智能&#xff08;AI&#xff09;是计算机科学的一个重要分支&#xff0c;其主要目标是让算法执行通常由人类完成的任务。机器学习是指一组允许算法从数据中学习并自我优化的技术&#xff0c;而无需明确编程。深度学习这一术语常与机器学习互换使用&…

怿星设计分享丨设计师与AI的情感化HMI

在当今科技迅速发展的时代背景下&#xff0c;人机交互&#xff08;HMI&#xff09;的设计正从传统的功能性层面转向更加注重用户体验与情感交流的方向。设计师们不再仅仅关注界面的功能性&#xff0c;而是更加重视如何通过设计传递情感&#xff0c;使用户在使用产品时能够感受到…

EsDA,一站式嵌入式软件

EsDA是一套面向工业智能物联领域的嵌入式系统设计自动化工具集&#xff0c;包含实时操作系统AWorksLP、低代码开发平台AWStudio、资源管理平台AXPI、跨平台GUI引擎AWTK和云服务平台ZWS&#xff0c;旨在提高嵌入式软件开发的效率、性能和可扩展性。 EsDA全称是嵌入式系统设计自动…

回归预测|基于饥饿游戏搜索优化随机森林的数据回归预测Matlab程序HGS-RF 多特征输入单输出 高引用先用先创新

回归预测|基于饥饿游戏搜索优化随机森林的数据回归预测Matlab程序HGS-RF 多特征输入单输出 高引用先用先创新 文章目录 一、基本原理1. 饥饿游戏搜索优化算法&#xff08;HGS&#xff09;简介2. 随机森林&#xff08;RF&#xff09;简介3. HGS-RF回归预测流程1. 初始化2. 随机森…

三维手势 handpose 3D RGB 手势3D建模 三维建模-手势舞 >> DataBall

请关注即将发布 handpose x plus 项目 三维手势 handpose 3D RGB 单目相机手势识别 手语 歌曲 Friends 手势检测 手势3D建模 三维建模 咨询合作 DataBall 项目&#xff0c;欢迎加以下微信。 助力快速掌握数据集的信息和使用方式。

sourceTree保姆级教程7:(合并某次提交)

在日常开发过程中&#xff0c;大家有时候并非都是在同一个分支进行开发&#xff0c;可能存在多人的情况下开发。根据上线的需求依次提交代码。当然也可能存在交叉提交的情况。此时应如何在master分支去合并具体某一次的提交呢&#xff1f;下面就开始了&#xff1a; 1.打开本地…

巧用联合与枚举:解锁自定义类型的无限潜力

嘿嘿,家人们,今天咱们来详细剖析C语言中的联合与枚举,好啦,废话不多讲,开干! 目录 1.:联合体 1.1:联合体类型的声明 1.1.1:代码1 1.1.2:代码2(计算机联合体的大小) 1.1.3:代码3 1.2:联合体的特点 1.2.1:代码1 1.2.2:代码2 1.3:相同成员的结构体与联合体进行对比 1.3…

前端界面搜索部分,第一个选择框的值,影响第二个选择框的值

1.字段声明 {title: 单位名称,dataIndex: departmentId,align: center,width: 100,hideInTable: true,renderFormItem: (item, { defaultRender, ...rest }) > (<ProFormSelectname"departmentId"// label"单位名称"options{hospitaltData}onChange…

旋转矩阵乘法,自动驾驶中的点及坐标系变换推导

目录 1. 矩阵乘法的内项相消 2. 左右乘&#xff0c;内外旋与动静坐标系 3. 点变换 3.1 点旋转后的点坐标表示 3.2 坐标系旋转后的点坐标表示 4. 坐标变换的实质 1. 矩阵乘法的内项相消 关于旋转变换&#xff0c;离不开矩阵的乘法&#xff0c;而矩阵乘法的物理意义和本身数…

电脑usb控制软件有哪些?6款软件帮你轻松解决USB端口泄密烦恼!

在数字化时代&#xff0c;企业的信息安全成为重中之重。 然而&#xff0c;USB端口泄密事件频发&#xff0c;给企业的数据安全和业务连续性带来了巨大威胁。 此前&#xff0c;某大型制造企业&#xff0c;由于员工在日常工作中频繁使用U盘等USB存储设备&#xff0c;导致公司核心…