当前位置: 首页 > 工具软件 > QP > 使用案例 >

H.266/VVC-VTM代码学习18-自适应QP设置(Adaptive QP)

程树
2023-12-01

H.266/VVC专栏传送

上一篇:H.266/VVC-VTM代码学习-帧内预测17-initIntraPatternChTypeISP函数初始化ISP的帧内预测
下一篇:H.266/VVC-VTM代码学习19-CU层确定测试模式函数initCULevel

前言

VTM是H.266/VVC视频编码标准的参考软件,研究VTM代码给研究人员解释了VVC编码标准的详细标准规范与细节。

本文是笔者对VTM代码的一点学习记录,成文于笔者刚开始接触VVC期间,期间很多概念和理论框架还很不成熟,若文中存在错误欢迎批评指正,也欢迎广大视频编码学习者沟通交流、共同进步。

VTM代码的下载及编译请参考博文:
【视频编码学习】H.266/VVC参考软件VTM配置运行(VTM-6.0版本)

一、Adaptive QP

自适应QP(Adaptive QP)是为每个 CU 自适应的选择 QP 以提升编码质量,由配置参数AdaptiveQP指定是否开启该功能。默认不开启。

自适应 QP 使用的 delta QP 计算原则是:对于平坦(即 activity 较小)CU 选择较大的 QP,对于变化较快(即 activity 较大)CU 选择较小的 QP。

CU 的 activity 由其亮度分量的方差计算得到。例如,对于一个 2Nx2N 的 CU ,首先计算其 4 个 NxN 的亮度子块的方差,然后由方差计算该 CU 的 activity: a c t = 1.0 + m i n V a r act = 1.0 + minVar act=1.0+minVar其中, m i n V a r minVar minVar 为 CU 四个子块方差中的最小值。

再利用下面几组公式即可得到 CU 对应的 delta QP:
n o r m = s c a l e ⋅ a c t + a c t a v g a c t + s c a l e ⋅ a c t a v g norm = \frac {scale\cdot act + act_{avg}}{act + scale\cdot act_{avg}} norm=act+scaleactavgscaleact+actavg其中:

  • a c t a v g act_{avg} actavg 为当前 CU 所在量化层次下的所有量化区域的 average activity。
  • s c a l e = 2 r a n g e 6 scale = 2^{\frac{range}{6}} scale=26range 其中 range 为可设置的参数,默认值为6。

再利用:
Δ Q P = i n t ( 6 ⋅ log ⁡ 2 n o r m ) \Delta QP = int(6 \cdot \log_{2}norm) ΔQP=int(6log2norm)
得到 delta QP。

得到 delta QP 后,将 slice 层的 initial QP 加上 delta QP 作为 baseQP,在以 baseQP 为中心的区间内取所有 QP 加入 RD 测试列表。

二、代码详解

1. 计算指定帧中所有量化层次下的 activity(在 encodePrep() 中被调用)

Lib\EncoderLib\AQp.cpp → \rightarrow AQpPreanalyzer::preanalyze

void AQpPreanalyzer::preanalyze( Picture* pcEPic )
{
  // 获得当前帧对应的亮度分量原始值
  const CPelBuf lumaPlane = pcEPic->getOrigBuf().Y();
  // 当前帧亮度分量宽
  const int iWidth  = lumaPlane.width;
  // 当前帧亮度分量高
  const int iHeight = lumaPlane.height;
  // 当前帧亮度分量缓存中的 stride
  const int iStride = lumaPlane.stride;

  // 遍历当前帧的自适应量化层次(aqlayer),size 为 m_picHeader.getCuQpDeltaSubdivIntra()/2+1
  for ( uint32_t d = 0; d < pcEPic->aqlayer.size(); d++ )
  {
    // 将指针移动到当前帧亮度分量原始值起始位置
    const Pel* pLineY = lumaPlane.bufAt( 0, 0);
    // 当前遍历到的自适应量化层次
    AQpLayer* pcAQLayer = pcEPic->aqlayer[d];
    // 当前量化层次下量化区域宽(sps.getMaxCUWidth() >> d)
    const uint32_t uiAQPartWidth = pcAQLayer->getAQPartWidth();
    // 当前量化层次下量化区域高(sps.getMaxCUWidth() >> d)
    const uint32_t uiAQPartHeight = pcAQLayer->getAQPartHeight();
    // 指针指向当前量化层次第一个单元
    double* pcAQU = &pcAQLayer->getQPAdaptationUnit()[0];

    // 用于存储量化区域内所有 CU 的 activity 之和
    double dSumAct = 0.0;
    // ---------------- 以下开始遍历当前帧在当前量化层次下的所有量化区域 -----------------
    // 量化区域在垂直方向上的起始位置
    for ( uint32_t y = 0; y < iHeight; y += uiAQPartHeight )
    {
      // 当前量化区域的高设定为量化区域高和当前帧剩余高度中的较小值
      const uint32_t uiCurrAQPartHeight = std::min(uiAQPartHeight, iHeight-y);
      // 量化区域在水平方向上的起始位置
      for ( uint32_t x = 0; x < iWidth; x += uiAQPartWidth, pcAQU++ )
      {
        // 当前量化区域的宽设定为量化区域宽和当前帧剩余宽度中的较小值
        const uint32_t uiCurrAQPartWidth = std::min(uiAQPartWidth, iWidth-x);
        
        // 当前量化区域的第一个原始像素值
        const Pel* pBlkY = &pLineY[x];
        // 长度为4的数组用于存储当前量化区域内4个子块区域分别的像素和
        uint64_t uiSum[4] = {0, 0, 0, 0};
        // 长度为4的数组用于存储当前量化区域内4个子块区域分别的像素平方和
        uint64_t uiSumSq[4] = {0, 0, 0, 0};
        // by用于遍历当前量化区域子块的高
        uint32_t by = 0;
        // 遍历当前量化区域的上半区域两个子块
        for ( ; by < uiCurrAQPartHeight>>1; by++ )
        {
          // bx用于遍历当前量化区域子块的宽
          uint32_t bx = 0;
          // 遍历当前量化区域的左上角子块
          for ( ; bx < uiCurrAQPartWidth>>1; bx++ )
          {
            // 累计量化区域第一子块的像素值
            uiSum  [0] += pBlkY[bx];
            // 累计量化区域第一子块的像素平方值
            uiSumSq[0] += pBlkY[bx] * pBlkY[bx];
          }
          // 遍历当前量化区域的右上角子块
          for ( ; bx < uiCurrAQPartWidth; bx++ )
          {
            // 累计量化区域第二子块的像素值
            uiSum  [1] += pBlkY[bx];
            // 累计量化区域第二子块的像素平方值
            uiSumSq[1] += pBlkY[bx] * pBlkY[bx];
          }
          // 累计下一行像素值
          pBlkY += iStride;
        }
        // 遍历当前量化区域的下半区域两个子块
        for ( ; by < uiCurrAQPartHeight; by++ )
        {
          // bx用于遍历当前量化区域子块的宽
          uint32_t bx = 0;
          // 遍历当前量化区域的左下角子块
          for ( ; bx < uiCurrAQPartWidth>>1; bx++ )
          {
            // 累计量化区域第三子块的像素值
            uiSum  [2] += pBlkY[bx];
            // 累计量化区域第三子块的像素平方值
            uiSumSq[2] += pBlkY[bx] * pBlkY[bx];
          }
          // 遍历当前量化区域的右下角子块
          for ( ; bx < uiCurrAQPartWidth; bx++ )
          {
            // 累计量化区域第三子块的像素值
            uiSum  [3] += pBlkY[bx];
            // 累计量化区域第三子块的像素平方值
            uiSumSq[3] += pBlkY[bx] * pBlkY[bx];
          }
          // 累计下一行像素值
          pBlkY += iStride;
        }

        // 不支持量化区域宽或高为奇数
        CHECK((uiCurrAQPartWidth&1)!=0,  "Odd part width unsupported");
        CHECK((uiCurrAQPartHeight&1)!=0, "Odd part height unsupported");
        // 量化区域子块的宽
        const uint32_t pixelWidthOfQuadrants  = uiCurrAQPartWidth >>1;
        // 量化区域子块的高
        const uint32_t pixelHeightOfQuadrants = uiCurrAQPartHeight>>1;
        // 量化区域子块像素数量
        const uint32_t numPixInAQPart         = pixelWidthOfQuadrants * pixelHeightOfQuadrants;

        // 用于存放当前量化区域4个子块方差中最小值
        double dMinVar = DBL_MAX;
        // 若子块大小不为0
        if (numPixInAQPart!=0)
        {
          // 遍历4个子块
          for ( int i=0; i<4; i++)
          {
            // 计算当前子块的像素平均值
            const double dAverage = double(uiSum[i]) / numPixInAQPart;
            // 计算当前子块的像素方差
            const double dVariance = double(uiSumSq[i]) / numPixInAQPart - dAverage * dAverage;
            // 记录四个子块的方差最小值
            dMinVar = std::min(dMinVar, dVariance);
          }
        }
        else
        {
          // 若字块大小为0,则方差最小值设为0
          dMinVar = 0.0;
        }
        // 当前量化区域的 activity 设置为 1+dMinVar 
        const double dActivity = 1.0 + dMinVar;
        // 记录当前量化区域的 activity
        *pcAQU = dActivity;
        // 累计所有量化区域的 activity 之和
        dSumAct += dActivity;
      } // 遍历完当前水平方向的量化区域
      pLineY += iStride * uiCurrAQPartHeight;
    } // 遍历完当前帧的所有量化区域

    // 计算并记录当前量化层次下所有量化区域的平均 activity
    const double dAvgAct = dSumAct / (pcAQLayer->getNumAQPartInWidth() * pcAQLayer->getNumAQPartInHeight());
    pcAQLayer->setAvgActivity( dAvgAct );
  } // 遍历完所有量化层次
}

2. 计算指定 CU 的 delta QP

Lib\EncoderLib\EncModeCtrl.cpp → \rightarrow EncModeCtrl::xComputeDQP

int EncModeCtrl::xComputeDQP( const CodingStructure &cs, const Partitioner &partitioner )
{
  // 当前帧
  Picture* picture    = cs.picture;
  // AQ 的深度(量化层次)为当前 CU 划分深度和最大划分层次的较小值
  unsigned uiAQDepth  = std::min( partitioner.currSubdiv/2, ( uint32_t ) picture->aqlayer.size() - 1 );
  // 当前 CU 对应的量化层次
  AQpLayer* pcAQLayer = picture->aqlayer[uiAQDepth];

  // 计算 scale 值 (m_pcEncCfg->getQPAdaptationRange()为可设置参数,默认值为6)
  double dMaxQScale   = pow( 2.0, m_pcEncCfg->getQPAdaptationRange() / 6.0 );
  // 获得当前帧当前量化层次下的 average activity
  double dAvgAct      = pcAQLayer->getAvgActivity();
  // 获得当前 CU 的 activity
  double dCUAct       = pcAQLayer->getActivity( cs.area.Y().topLeft() );
  // norm = (scale * act) / (act + scale * avgAct)
  double dNormAct     = ( dMaxQScale*dCUAct + dAvgAct ) / ( dCUAct + dMaxQScale*dAvgAct );
  // delta QP = log2(norm) * 6
  double dQpOffset    = log( dNormAct ) / log( 2.0 ) * 6.0;
  // 将 delta QP 四舍五入转为整数
  int    iQpOffset    = int( floor( dQpOffset + 0.49999 ) );
  return iQpOffset;
}

3. CU层计算得到 QP

Lib\EncoderLib\EncModeCtrl.cpp → \rightarrow EncModeCtrlMTnoRQT::initCULevel

  // baseQP 为 slice 级的 initial QP
  int baseQP = cs.baseQP;
  // 当前划分树不是separate tree(分离树)是 joint tree(联合树)或当前为亮度分量
  if (!partitioner.isSepTree(cs) || isLuma(partitioner.chType))
  {
    // 使用 adaptive QP
    if (m_pcEncCfg->getUseAdaptiveQP())
    {
      // baseQP 取 -(6 * (m_bitDepth[CHANNEL_TYPE_LUMA] - 8) 和 63 和 sliceQP + delta 中的中值
      baseQP = Clip3(-cs.sps->getQpBDOffset(CHANNEL_TYPE_LUMA), MAX_QP, baseQP + xComputeDQP(cs, partitioner));
    }
#if SHARP_LUMA_DELTA_QP
    // 另一种获得 delta QP 的机制
    if (m_pcEncCfg->getLumaLevelToDeltaQPMapping().isEnabled())
    {
      if (partitioner.currQgEnable())
      {
        m_lumaQPOffset = calculateLumaDQP (cs.getOrgBuf (clipArea (cs.area.Y(), cs.picture->Y())));
      }
      baseQP = Clip3 (-cs.sps->getQpBDOffset (CHANNEL_TYPE_LUMA), MAX_QP, baseQP - m_lumaQPOffset);
    }
#endif
  }
  // -------------------- 以下是在 base QP 附近设定 minQP 和 maxQP 再 RD 测试两者区间内所有取值 -------------
  int minQP = baseQP;
  int maxQP = baseQP;

  xGetMinMaxQP( minQP, maxQP, cs, partitioner, baseQP, *cs.sps, *cs.pps, CU_QUAD_SPLIT );
  bool checkIbc = true;
  if (partitioner.chType == CHANNEL_TYPE_CHROMA)
  {
    checkIbc = false;
  }
  // Add coding modes here
  // NOTE: Working back to front, as a stack, which is more efficient with the container
  // NOTE: First added modes will be processed at the end.

  //
  // Add unit split modes

  if( !cuECtx.get<bool>( QT_BEFORE_BT ) )
  {
    for( int qp = maxQP; qp >= minQP; qp-- )
    {
      m_ComprCUCtxList.back().testModes.push_back( { ETM_SPLIT_QT, ETO_STANDARD, qp } );
    }
  }

  if( partitioner.canSplit( CU_TRIV_SPLIT, cs ) )
  {
    // add split modes
    for( int qp = maxQP; qp >= minQP; qp-- )
    {
      m_ComprCUCtxList.back().testModes.push_back( { ETM_SPLIT_TT_V, ETO_STANDARD, qp } );
    }
  }

  if( partitioner.canSplit( CU_TRIH_SPLIT, cs ) )
  {
    // add split modes
    for( int qp = maxQP; qp >= minQP; qp-- )
    {
      m_ComprCUCtxList.back().testModes.push_back( { ETM_SPLIT_TT_H, ETO_STANDARD, qp } );
    }
  }

  ...
  ...

得到 maxQP 和 minQP 后,将该区间内所有 QP 形成的模式存入 m_ComprCUCtxList.testModes 中。

xCompressCU 函数通过下列代码,提取 m_ComprCUCtxList.testModes 中的待测试模式,并调用 xCheckRDCostIntra 函数,xCheckRDCostIntra 函数内部又调用 estIntraPredLumaQT 函数,进而分别对各个待选模式选择合适的 intra 模式。

  do
  {
    for (int i = compBegin; i < (compBegin + numComp); i++)
    {
      ComponentID comID = jointPLT ? (ComponentID)compBegin : ((i > 0) ? COMPONENT_Cb : COMPONENT_Y);
      tempCS->prevPLT.curPLTSize[comID] = curLastPLTSize[comID];
      memcpy(tempCS->prevPLT.curPLT[i], curLastPLT[i], curLastPLTSize[comID] * sizeof(Pel));
    }
    // 获取m_ComprCUCtxList.testModes
    EncTestMode currTestMode = m_modeCtrl->currTestMode();
   
    ...

    else if( currTestMode.type == ETM_INTRA )
    {
      if (slice.getSPS()->getUseColorTrans() && !CS::isDualITree(*tempCS))
      {
        bool skipSecColorSpace = false;
        skipSecColorSpace = xCheckRDCostIntra(tempCS, bestCS, partitioner, currTestMode, (m_pcEncCfg->getRGBFormatFlag() ? true : false));

		...

	  }
	}	
  } while( m_modeCtrl->nextMode( *tempCS, partitioner ) );

上一篇:H.266/VVC-VTM代码学习-帧内预测17-initIntraPatternChTypeISP函数初始化ISP的帧内预测
下一篇:H.266/VVC-VTM代码学习19-CU层确定测试模式函数initCULevel

 类似资料: