H.266/VVC代码学习56:率失真优化

躲避疫情,在家也学点东西吧,解锁一波新地图——VVC的RDO。

所谓率失真优化:在给定编码比特率的情况下,如何使失真最小:J(λ) = min[ D + λR ]。其中:
率:信息数量的多少(比特率R);
失真:接收短信号与源信号的差异程度(失真D);
λ:拉格朗日参数,只与量化参数有关;
几何意义:一条斜率为λ的直线,与率失真曲线的切点即最优解。
目的:得到性价比最高的编码参数,如划分方式、预测模式、变换方式。

如需了解更多具体的理论方面的知识,可参考:率失真优化理论

1 lambda的确定及走向

之前的学习见H.266/VVC代码学习6:VTM4.0中lambda与QP的关系

直接看VTM7.0中的代码:

重要成员变量含义

  double                  m_dLambda;//最初始的lambda
  double                  m_DistScaleUnadjusted;//lambda的倒数,同时有移位(左移了15位)
  double                  m_DistScale;//lambda的倒数,同时有移位(左移了15位),后面可能有调整
  double                  m_dLambdaMotionSAD[2 /* 运动信息的lambda 0=standard, 1=for transquant bypass when mixed-lossless cost evaluation enabled*/];

重要相关函数含义

double RdCost::calcRdCost( 
	uint64_t fracBits,		//比特数R
	Distortion distortion,	//失真D
	bool useUnadjustedLambda//是否调整lambda
)
{
    
    
  if( m_costMode == COST_LOSSLESS_CODING && 0 != distortion )//如果无损编码情况下有失真,就返回最大值
  {
    
    
    return MAX_DOUBLE;
  }
  return ( useUnadjustedLambda ? m_DistScaleUnadjusted : m_DistScale ) * double( distortion ) + double( fracBits );//率失真计算

}
template<typename T, size_t N>
uint32_t updateCandList
(
	T uiMode,								//新的要参与对比的模式
	double uiCost,							//新的要参与对比的模式的代价
	static_vector<T, N>& candModeList,		//之前留下的模式
	static_vector<double, N>& candCostList	//之前留下的模式的代价
  , size_t uiFastCandNum = N,				//最后留下几个模式(长度)
	int* iserttPos = nullptr
)
{
    
    
  CHECK( std::min( uiFastCandNum, candModeList.size() ) != std::min( uiFastCandNum, candCostList.size() ), "Sizes do not match!" );
  CHECK( uiFastCandNum > candModeList.capacity(), "The vector is to small to hold all the candidates!" );

  size_t i;
  size_t shift = 0;
  size_t currSize = std::min( uiFastCandNum, candCostList.size() );//确定最后有多少个模式

  while( shift < uiFastCandNum && shift < currSize && uiCost < candCostList[currSize - 1 - shift] )//找到当前模式排在倒数第几个索引
  {
    
    
    shift++;//从大的往小的找,排在倒数第几个位置
  }

  if( candModeList.size() >= uiFastCandNum && shift != 0 )//更新列表的条件:超出总列表范围(需要往外剔除),并且,新加入的干掉了其中的一部分
  {
    
    
    for( i = 1; i < shift; i++ )//被干掉的都往后移一位
    {
    
    
      candModeList[currSize - i] = candModeList[currSize - 1 - i];
      candCostList[currSize - i] = candCostList[currSize - 1 - i];
    }
    candModeList[currSize - shift] = uiMode;//把当前这个插进入
    candCostList[currSize - shift] = uiCost;
    if (iserttPos != nullptr)
    {
    
    
      *iserttPos = int(currSize - shift);
    }
    return 1;//列表有所更新,就返回1
  }
  else if( currSize < uiFastCandNum )//否则(列表没填满):直接插入
  {
    
    
    candModeList.insert( candModeList.end() - shift, uiMode );
    candCostList.insert( candCostList.end() - shift, uiCost );
    if (iserttPos != nullptr)
    {
    
    
      *iserttPos = int(candModeList.size() - shift - 1);
    }
    return 1;//列表有所更新,就返回1
  }
  if (iserttPos != nullptr)
  {
    
    
    *iserttPos = -1;
  }
  return 0;
}

#endif

1.1 updataLambda

void EncCu::updateLambda (Slice* slice, const int dQP,
 #if WCG_EXT && ER_CHROMA_QP_WCG_PPS
                          const bool useWCGChromaControl,
 #endif
                          const bool updateRdCostLambda)
{
    
    
#if WCG_EXT && ER_CHROMA_QP_WCG_PPS
  if (useWCGChromaControl)//如果使用了WCG,比较容易,计算后返回即可
  {
    
    
    const double lambda = m_pcSliceEncoder->initializeLambda (slice, m_pcSliceEncoder->getGopId(), slice->getSliceQp(), (double)dQP);//获得lambda
    const int clippedQP = Clip3 (-slice->getSPS()->getQpBDOffset (CHANNEL_TYPE_LUMA), MAX_QP, dQP);

    m_pcSliceEncoder->setUpLambda (slice, lambda, clippedQP);//加载lambda
    return;
  }
#endif
  int iQP = dQP;
  const double oldQP     = (double)slice->getSliceQpBase();
#if ENABLE_QPA_SUB_CTU
  const double oldLambda = (m_pcEncCfg->getUsePerceptQPA() && !m_pcEncCfg->getUseRateCtrl() && slice->getPPS()->getUseDQP()) ? slice->getLambdas()[0] :
                           m_pcSliceEncoder->calculateLambda (slice, m_pcSliceEncoder->getGopId(), oldQP, oldQP, iQP);//计算
#else
  const double oldLambda = m_pcSliceEncoder->calculateLambda (slice, m_pcSliceEncoder->getGopId(), oldQP, oldQP, iQP);
#endif
  const double newLambda = oldLambda * pow (2.0, ((double)dQP - oldQP) / 3.0);//新的lambda是根据QP的变化改动的
#if RDOQ_CHROMA_LAMBDA
  const double chromaLambda = newLambda / m_pcRdCost->getChromaWeight();
  const double lambdaArray[MAX_NUM_COMPONENT] = {
    
    newLambda, chromaLambda, chromaLambda};//根据新计算的lambda更新三个分量
  m_pcTrQuant->setLambdas (lambdaArray);//设置进去三个分量的lambda
#else
  m_pcTrQuant->setLambda (newLambda);
#endif
  if (updateRdCostLambda)//如果要更新RDO的lambda,则重新设置lambda
  {
    
    
    m_pcRdCost->setLambda (newLambda, slice->getSPS()->getBitDepths());
  }
}
#endif // SHARP_LUMA_DELTA_QP || ENABLE_QPA_SUB_CTU

1.2 calculateLambda

double EncSlice::calculateLambda( const Slice*     slice, //								要处理的帧
                                  const int        GOPid, // entry in the GOP table			第几个GOP
                                  const double     refQP, // initial slice-level QP			帧的QP
                                  const double     dQP,   // initial double-precision QP	浮点型QP
                                        int       &iQP )  // returned integer QP.			整型QP
{
    
    
  double dLambda = initializeLambda (slice, GOPid, int (refQP + 0.5), dQP);//得到lambda
  iQP = Clip3 (-slice->getSPS()->getQpBDOffset (CHANNEL_TYPE_LUMA), MAX_QP, int (dQP + 0.5));//得到QP

  if( m_pcCfg->getDepQuantEnabledFlag() )//如果应用DQ,要有少量的改变
  {
    
    
    dLambda *= pow( 2.0, 0.25/3.0 ); // slight lambda adjustment for dependent quantization (due to different slope of quantizer)
  }

  // NOTE: the lambda modifiers that are sometimes applied later might be best always applied in here.
  return dLambda;
}

1.3 initializeLambda

double EncSlice::initializeLambda(const Slice* slice, const int GOPid, const int refQP, const double dQP)
{
    
    
	/*************************** 1.初始化过程 *************************/
  const int   bitDepthLuma  = slice->getSPS()->getBitDepth(CHANNEL_TYPE_LUMA);//亮度的比特深度
  const int   bitDepthShift = 6 * (bitDepthLuma - 8 - DISTORTION_PRECISION_ADJUSTMENT(bitDepthLuma)) - 12;//比特深度偏移,VVC基本都是0
  const int   numberBFrames = m_pcCfg->getGOPSize() - 1;//B帧数量
  const SliceType sliceType = slice->getSliceType();//获取该帧类型:I or B or P
#if X0038_LAMBDA_FROM_QP_CAPABILITY
  const int      temporalId = m_pcCfg->getGOPEntry(GOPid).m_temporalId;//当前帧的层级
  const std::vector<double> &intraLambdaModifiers = m_pcCfg->getIntraLambdaModifier();//?????
#endif

  double dQPFactor = m_pcCfg->getGOPEntry(GOPid).m_QPFactor;//获取QP

  double dLambda, lambdaModifier;//dlambda就是要返回的计算出的lambda的值

  /*************** 2.对I帧的操作,设出一个值为0.57 ***************/
  if (sliceType == I_SLICE)
  {
    
    
    if ((m_pcCfg->getIntraQpFactor() >= 0.0) && (m_pcCfg->getGOPEntry(GOPid).m_sliceType != I_SLICE))
    {
    
    
      dQPFactor = m_pcCfg->getIntraQpFactor();
    }
    else
    {
    
    
#if X0038_LAMBDA_FROM_QP_CAPABILITY
      if (m_pcCfg->getLambdaFromQPEnable())//如果lambda从QP得出,则固定为0.57
      {
    
    
        dQPFactor = 0.57;
      }
      else//否则比较麻烦:0.57减去一个和B帧数量相关的一个值,但范围在0.28到0.57之间
#endif
      dQPFactor = 0.57 * (1.0 - Clip3(0.0, 0.5, 0.05 * double (slice->getPic()->fieldPic ? numberBFrames >> 1 : numberBFrames)));
    }
  }
#if X0038_LAMBDA_FROM_QP_CAPABILITY
  else if (m_pcCfg->getLambdaFromQPEnable())
  {
    
    
    dQPFactor = 0.57;
  }
#endif

  /***************** 3.先通过0.57计算得到一个dlambda = 0.57 * 2 ^ (QP/3) *********************/
  dLambda = dQPFactor * pow(2.0, (dQP + bitDepthShift) / 3.0);//0.57 * 2 ^ (QP/3)
  
  /***************** 4.根据不同的情况调整lambda的数值 *********************/
#if X0038_LAMBDA_FROM_QP_CAPABILITY
  if (slice->getDepth() > 0 && !m_pcCfg->getLambdaFromQPEnable())//1.如果lambda不是从QP得到的,需要再乘上一个2~4之间的值
#else
  if (slice->getDepth() > 0)
#endif
  {
    
    
    dLambda *= Clip3(2.0, 4.0, ((refQP + bitDepthShift) / 6.0));
  }
  // if Hadamard is used in motion estimation process
  if (!m_pcCfg->getUseHADME() && (sliceType != I_SLICE))//2.如果不是I帧并且可以使用哈达玛变换,需要再乘上一个0.95
  {
    
    
    dLambda *= 0.95;
  }
#if X0038_LAMBDA_FROM_QP_CAPABILITY
  if ((sliceType != I_SLICE) || intraLambdaModifiers.empty())
  {
    
    
    lambdaModifier = m_pcCfg->getLambdaModifier(temporalId);
  }
  else
  {
    
    
    lambdaModifier = intraLambdaModifiers[temporalId < intraLambdaModifiers.size() ? temporalId : intraLambdaModifiers.size() - 1];
  }
  dLambda *= lambdaModifier;//3.乘上一个调整值
#endif

  return dLambda;//返回这一个计算出的lambda
}


1.4 setUpLambda

void EncSlice::setUpLambda( Slice* slice, const double dLambda, int iQP)
{
    
    
  // store lambda
  m_pcRdCost ->setLambda( dLambda, slice->getSPS()->getBitDepths() );

  /******************************************* 1.对于率失真优化 ************************************************/

  //在RdCost中,只有一个lambda,因为亮度和色度位没有分开,而是对色度的失真进行加权。
  double dLambdas[MAX_NUM_COMPONENT] = {
    
     dLambda };//设置了一个临时变量,默认值为dLambda,这个临时变量将在处理后应用于RDOQ和SAO

  /* 亮度分量直接确定为dLambda,色度分量将在下面的for循环中进行处理 */
  for( uint32_t compIdx = 1; compIdx < MAX_NUM_COMPONENT; compIdx++ )
  {
    
    
    const ComponentID compID = ComponentID( compIdx );//确定是Cb还是Cr分量
    int chromaQPOffset       = slice->getPPS()->getQpOffset( compID ) + slice->getSliceChromaQpDelta( compID );//获取色度QP偏移
    int qpc = slice->getSPS()->getMappedChromaQpValue(compID, iQP) + chromaQPOffset;//将偏移值应用到默认QP,获取色度实际采用的QP
    double tmpWeight         = pow( 2.0, ( iQP - qpc ) / 3.0 );  // 考虑了色度qp映射和色度qp偏移,得到权重,这个权重大于1
    if( m_pcCfg->getDepQuantEnabledFlag() && !( m_pcCfg->getLFNST() ) )//如果使用了DQ并且不使用LFNST
    {
    
    //增加色度权重以进行相关量化(以减少从色度到亮度的比特率偏移)
      tmpWeight *= ( m_pcCfg->getGOPSize() >= 8 ? pow( 2.0, 0.1/3.0 ) : pow( 2.0, 0.2/3.0 ) );  
    }
    m_pcRdCost->setDistortionWeight( compID, tmpWeight );
    dLambdas[compIdx] = dLambda / tmpWeight;//确定色度分量的lambda,比亮度的小一点
  }

  /***************************** 2.根据率失真优化得到的lambda,直接作用于RDOQ和SAO ******************************/

  // for RDOQ
  m_pcTrQuant->setLambdas( dLambdas );
  // for SAO
  slice->setLambdas( dLambdas );
}

1.5 setLambda

void RdCost::setLambda( double dLambda, const BitDepths &bitDepths )
{
    
    
  m_dLambda             = dLambda;//把参数中的lambda值赋值给RdCost类的成员变量m_dLambda

  m_DistScale           = double(1<<SCALE_BITS) / m_dLambda;//m_DistScale这个是lambda的倒数,还有移位,J(λ) = min[ D + λR ] 转化为 1/J(λ) = min[ (1/λ) D + R ]

  m_dLambdaMotionSAD[0] = sqrt(m_dLambda);//常规帧间运动信息的SAD的lambda是lambda的开方
  dLambda = 0.57
            * pow(2.0, ((LOSSLESS_AND_MIXED_LOSSLESS_RD_COST_TEST_QP_PRIME - 12
                         + 6
                             * ((bitDepths.recon[CHANNEL_TYPE_LUMA] - 8)
                                - DISTORTION_PRECISION_ADJUSTMENT(bitDepths.recon[CHANNEL_TYPE_LUMA])))
                        / 3.0));
  m_dLambdaMotionSAD[1] = sqrt(dLambda);//TS模式下帧间运动信息的SAD的lambda是上面新算得lambda的开方
}

2 率失真优化所在位置

2.1 TPM

用于TPM模式的粗选:从40种划分确定索引方式中,根据SAD找到3种最优,后续这3种模式会进入SATD筛选。

      double cost = (double)uiSad + (double)uiBitsCand * sqrtLambdaForFirstPass;
      updateCandList( mergeCand, cost, triangleRdModeList, tianglecandCostList, triangleNumMrgSATDCand );

2.2 Affine merge

用于Affine merge模式的粗选,根据SAD找到2种最优,后续这2种模式会进入SATD筛选。

     double cost = (double)uiSad + (double)uiBitsCand * sqrtLambdaForFirstPass;
     updateCandList( uiMergeCand, cost, RdModeList, candCostList, uiNumMrgSATDCand );

2.3 SAO

1)确定最佳边带补偿阶段:对比28种边带,找到最佳边带BO。
2)CTU级:确定最佳SAO模式阶段:对比以下6种:4个边界补偿方式EO,1个前面找到的最佳BO,1个不采用SAO,找到最佳SAO方式
3)CTU级:确定是否使用参数融合:对比以下3种:刚找到的最佳SAO方式,上块SAO merge,左块SAO merge,得到SAO全过程的最佳

2.4 ALF

1)CTU级:确定是否使用ALF(根据前面计算出的协方差D)
2)确定亮度是否合并滤波器分类,对比固定滤波器和计算得出的滤波器系数,得到最佳亮度滤波器系数,保存进APS
3)确定色度用哪个滤波器,得到最佳色度滤波器系数,保存进APS
4)遍历APS序列(即对比当前系数和历史系数哪个更好),得到最佳ALF系数

2.5 Intra

1、亮度预测对35种模式第一轮粗选出2或3种模式:D应用SAD和HAD中较小的
2、亮度预测对2或3种旁边的模式第二轮粗选出2或3种模式:D应用SAD和HAD中较小的
3、亮度预测对MPM列表中的进行多参考行的筛选找到最佳参考行:D应用SAD和HAD中较小的
4、亮度MIP预测的筛选得出一种最佳MIP模式,放在前面2到3种模式后面成为3或4种模式:D应用SAD和HAD中较小的
5、亮度预测选出最佳模式:D应用HAD
6、色度预测用到lambda计算的有6种,6种中选出最佳:D应用HAD

2.6 Merge

merge、mmvd、CIIP的失真计算共同粗选出5种(CIIP选出1种,merge和mmvd共同选出4种):D均应用SAD

未完待续。。

猜你喜欢

转载自blog.csdn.net/weixin_42979679/article/details/104146883