HM笔记_3_帧内块划分源码分析

本文深入剖析了HEVC编码器中CTU(Coding Tree Unit)划分的过程,包括参数初始化、计算RDcost、递归划分子CU及递归终止条件等内容。特别关注了不同QP值对编码的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


最近需要研究HM的与块划分有关的函数( TLibEncoder\TEncCu),但是网上可参考的资料很少,加上这部分的代码比较繁杂,所以本文基于最新的16.20版本,尝试剖析一下其实现原理,后续会更新x265对应部分的源码剖析。

CTU层次

基本分为两步,首先压缩CTU,然后编码CTU

TEncCu::compressCtu压缩CTU

  1. 初始化顶层的CTU数据,m_ppcBestCU[0]->initCtu()m_ppcTempCU[0]->initCtu()
  2. 递归的调用xCompressCU()压缩CTU

TEncCu::encodeCtu编码CTU

  1. 根据Slice的设置初始化QP的参数
  2. 递归调用xEncodeCU()编码CTU

CU层次

xCompressCU( )

从前面的x可以看出,该方法是一个protected类型的成员函数,根据xCheckRDCostMerge2Nx2NxCheckRDCostInter定位帧间预测部分,根据xCheckRDCostIntra定位帧内预测部份。( 补充:关于HM的命名规则和项目架构可以参考我之前的一篇博客。)

传入参数为rpcBestCU,rpcTempCU,uiDepth,其中uiDepth即为当前CU划分的深度(0、1、2)。

详细步骤如下

1. 参数初始化(读取BestCU数据和参数、初始化用于计算BestCU RD cost的量化参数QP)

参数初始化分为两部分,第一部分读取rpcBestCU所代表CU的YUV数据、坐标信息、有效组件的数量,分别存放在m_ppcOriginYuv[yiDepth]uiLPelX~uiBelYnumberValidComponents中,其中有效组件的类型是枚举类型:

枚举类型
COMPONENT_Y0
COMPONENT_Cb1
COMPONENT_Cr2
MAX_NUM_COMPONENT3

第二部分,解析图片参数集pps(picture parameter set)和 序列参数集sps(sequence parameter set),主要是解析参数集中有关量化参数QP的设置,QP由四个参数控制,分别是iBaseQPiMinQPiMaxQPisAddLowestQP

首先xComputeQP()根据 输入父CUrpcBestCU和 当前划分深度uiDepth计算iBaseQP,然后使用四种方法确定iMinQP和`iMaxQP:

  • 默认(可以是一个范围)
  • 由亮度等级确定QP(value)
  • 由目标码率确定QP(value,万帅)
  • TQB模式(value)
// ToDo: 注释中的参数值根据第一个CU的运行情况确定
// iMinQP = 上下限截断(iBaseQP-idQP),  iMaxQP = 上下限截断(iBaseQP+idQP)

// 方法1 (默认,根据pps和sps设置QP最大值和最小值), idQP
if( uiDepth <= pps.getMaxCuDQPDepth() )
  {
    Int idQP = m_pcEncCfg->getMaxDeltaQP(); // 0
    iMinQP = Clip3( -sps.getQpBDOffset(CHANNEL_TYPE_LUMA), MAX_QP, iBaseQP-idQP ); // clip3(minVal,maxVal,a),MAX_QP=51,-sps.getQpBDOffset(CHANNEL_TYPE_LUMA)=51
    iMaxQP = Clip3( -sps.getQpBDOffset(CHANNEL_TYPE_LUMA), MAX_QP, iBaseQP+idQP ); // 查看 iBaseQP 和 idQP的值
  }
  else
  {
	// 默认该QP的最大值和最小值相同,此时QP只能选择`rpcTempCU->getQP(0)`
    iMinQP = rpcTempCU->getQP(0);
    iMaxQP = rpcTempCU->getQP(0);
  }

  // 方法2:根据luma level计算QP offset
  // 如果可以将亮度等级转化为QP等级(`m_pcEncCfg->getLumaLevelToDeltaQPMapping().isEnabled()==True`),则进行转化
  if ( m_pcEncCfg->getLumaLevelToDeltaQPMapping().isEnabled() )
  {
    if ( uiDepth <= pps.getMaxCuDQPDepth() )
    {
      // keep using the same m_QP_LUMA_OFFSET in the same CTU
      m_lumaQPOffset = calculateLumaDQP(rpcTempCU, 0, m_ppcOrigYuv[uiDepth]);
    }
    iMinQP = Clip3(-sps.getQpBDOffset(CHANNEL_TYPE_LUMA), MAX_QP, iBaseQP - m_lumaQPOffset);
    iMaxQP = iMinQP; // force encode choose the modified QO
  }

  // 方法3:根据目标码率确定QP的值(万帅,P335)
  if ( m_pcEncCfg->getUseRateCtrl() )
  {
    iMinQP = m_pcRateCtrl->getRCQP();
    iMaxQP = m_pcRateCtrl->getRCQP();
  }

  // 方法4: TQB
  // transquant-bypass (TQB) processing loop variable initialisation 
  // TQB 跳过进行伸缩、变换和环路滤波过程,由`pps.getTransquantBypassEnabledFlag()`决定
  // 后续由isAddLowestQP判断是否是TQB模式

  const Int lowestQP = iMinQP; // For TQB, use this QP which is the lowest non TQB QP tested (rather than QP'=0) - that way delta QPs are smaller, and TQB can be tested at all CU levels.

  if ( (pps.getTransquantBypassEnabledFlag()) )
  {
    isAddLowestQP = true; // mark that the first iteration is to cost TQB mode.
    iMinQP = iMinQP - 1;  // increase loop variable range by 1, to allow testing of TQB mode along with other QPs
    if ( m_pcEncCfg->getCUTransquantBypassFlagForceValue() )
    {
      iMaxQP = iMinQP; // 此时的QP最小,失真也最少
    }
  }

2. 计算BestCU的RD cost

首先获取当前CU所在的slice,然后判断当前CU是否在Slice的边界,判断标识位为bBoundary,如果当前CU在Slice的边界,直接跳过第二步,进入第三步划分成子CU(为了控制错误传播,编码单元不能跨过Slice,一般来说也不会跨过Slice,此处相当于进一步检查)。

2.1 初始化TempCU,计算帧间模式的RD cost

遍历QP的可能值,针对QP不同的初始化方式调整iQP之后,初始化rpcTempCU,如果当前CU所在帧不是I帧(即表示当前CU是帧间模式),则针对帧间编码的三种方式计算RD cost:

// 遍历QP,初始化TempCU
for (Int iQP=iMinQP; iQP<=iMaxQP; iQP++)
{
  // 针对TQB调整iQP
  const Bool bIsLosslessMode = isAddLowestQP && (iQP == iMinQP);
  if (bIsLosslessMode)
  {
	iQP = lowestQP;
  }
  // 针对上述的方法2(根据Luma level初始化QP)调整PD cost的拉格朗日系数lambda
  if ( m_pcEncCfg->getLumaLevelToDeltaQPMapping().isEnabled() && uiDepth <= pps.getMaxCuDQPDepth() )
  {
	getSliceEncoder()->updateLambda(pcSlice, iQP);
  }
  // 针对色度块调整QP,首次运行跳过(此处luma block和chroma block的QP offset不同,详见万帅6.2)
  m_cuChromaQpOffsetIdxPlus1 = 0;
  if (pcSlice->getUseChromaQpAdj())
  {
	/* Pre-estimation of chroma QP based on input block activity may be performed
	 * here, using for example m_ppcOrigYuv[uiDepth] */
	/* To exercise the current code, the index used for adjustment is based on
	 * block position
	 */
	Int lgMinCuSize = sps.getLog2MinCodingBlockSize() +
					  std::max<Int>(0, sps.getLog2DiffMaxMinCodingBlockSize()-Int(pps.getPpsRangeExtension().getDiffCuChromaQpOffsetDepth()));
	m_cuChromaQpOffsetIdxPlus1 = ((uiLPelX >> lgMinCuSize) + (uiTPelY >> lgMinCuSize)) % (pps.getPpsRangeExtension().getChromaQpOffsetListLen() + 1);
  }

  // 针对每个iQP创建新的TempCU
  rpcTempCU->initEstData( uiDepth, iQP, bIsLosslessMode );
  // 计算帧间模式的RD cost
  // 调用xCheckRDCostInter、xCheckRDCostMerge2Nx2N
  // do inter modes, SKIP and 2Nx2N
  if( rpcBestCU->getSlice()->getSliceType() != I_SLICE )
  {
	// 2Nx2N
	if(m_pcEncCfg->getUseEarlySkipDetection())
	{
	  xCheckRDCostInter( rpcBestCU, rpcTempCU, SIZE_2Nx2N DEBUG_STRING_PASS_INTO(sDebug) );
	  rpcTempCU->initEstData( uiDepth, iQP, bIsLosslessMode );//by Competition for inter_2Nx2N
	}
	// SKIP
	xCheckRDCostMerge2Nx2N( rpcBestCU, rpcTempCU DEBUG_STRING_PASS_INTO(sDebug), &earlyDetectionSkipMode );//by Merge for inter_2Nx2N
	rpcTempCU->initEstData( uiDepth, iQP, bIsLosslessMode );

	if(!m_pcEncCfg->getUseEarlySkipDetection())
	{
	  // 2Nx2N, NxN
	  xCheckRDCostInter( rpcBestCU, rpcTempCU, SIZE_2Nx2N DEBUG_STRING_PASS_INTO(sDebug) );
	  rpcTempCU->initEstData( uiDepth, iQP, bIsLosslessMode );
	  if(m_pcEncCfg->getUseCbfFastMode())
	  {
		doNotBlockPu = rpcBestCU->getQtRootCbf( 0 ) != 0;
	  }
	}
  }
  // Restore loop variable if lossless mode was searched.
  if (bIsLosslessMode)
  {
	iQP = iMinQP;
  }
}  
2.2 计算帧间和帧内模式的PD cost

首先判断是不是early skip mode,如果是提前跳过模式,转第三步,否则继续执行。

遍历QP begin

  1. 调整iQP,初始化rpcTempCU

  2. 计算帧间模式的RD cost,分为2Nx2N,NxN,Nx2N,2NxN 和 AMP五种模式,每个模式用到的核心函数只有两个xCheckRDCostInter()rpcTempCU->initEstData()

  3. 计算帧内模式的RD cost

    1. 读取CBF配置,若CU的预测模式为MODE_INTER,其对应的预测残差不包含非零的变换系数,则跳过该CU的其他候选模式,即当前模式为CU的最优模式,直接进行下一步的四叉树划分

    2. 调用xCheckRDCostIntra(),对2Nx2N的CU进行预测,并计算RD Cost

      调用rpcTempCU->initEstData()初始化TempCU

    3. PCM模式下执行和上一步类似的操作

  4. 汇总当前的RD cost

    if( rpcBestCU->getTotalCost()!=MAX_DOUBLE ) // MAX_DOUBLE = 1.7e+308
    {
      m_pcRDGoOnSbacCoder->load(m_pppcRDSbacCoder[uiDepth][CI_NEXT_BEST]);
      m_pcEntropyCoder->resetBits();
      m_pcEntropyCoder->encodeSplitFlag( rpcBestCU, 0, uiDepth, true );
      rpcBestCU->getTotalBits() += m_pcEntropyCoder->getNumberOfWrittenBits(); // split bits
      rpcBestCU->getTotalBins() += ((TEncBinCABAC *)((TEncSbac*)m_pcEntropyCoder->m_pcEntropyCoderIf)->getEncBinIf())->getBinsCoded();
      rpcBestCU->getTotalCost()  = m_pcRdCost->calcRdCost( rpcBestCU->getTotalBits(), rpcBestCU->getTotalDistortion() );
      m_pcRDGoOnSbacCoder->store(m_pppcRDSbacCoder[uiDepth][CI_NEXT_BEST]);
    }
    

遍历QP end

3. 计算BestCU划分后的子CU的RD cost(递归调用,并保存RD cost最优的CU)

首先,PCM模式下将原始的YUV数据拷贝到PCM缓存区,然后,再次调整QP的最大值和最小值,同步骤一参数初始化中一样,分为四种方式;

bSubBraunch判断是否继续划分,如果继续划分,遍历QP:

  1. 初始化,这里的变量命名方式和上面类似,但是都加上了Sub,例如pcSubBestPartCUpcSubTempPartCU指向下一层

  2. 遍历划分后的四个子CU,分别调用四次xCompressCU函数,计算子CU的RD cost,并将RD cost存放到rpcTempCU

  3. 汇总遍历划分的子CU的RD cost

  4. 比较结果,判断是否划分

    比较rpcBestCU, rpcTempCU的RD Cost决定是否对16x16CU进行进一步的划分,即比较rpcBestCU->getTotalCost()rpcTempCU->getTotalCost(),如果划分后的RD cost更低,则rpcTemp会替换掉rpcBestCU。可见,该过程不仅有划分前后的RD cost的比较,也有划分不同模式的RD cost的比较。

    xCheckBestMode( rpcBestCU, rpcTempCU, uiDepth DEBUG_STRING_PASS_INTO(sDebug) DEBUG_STRING_PASS_INTO(sTempDebug) DEBUG_STRING_PASS_INTO(false) ); 
    // RD compare current larger prediction
    

该部分的源码:

//*******************递归划分子CU,记录最优RDcost对应的划分模式*******************//
const Bool bSubBranch = bBoundary || !( m_pcEncCfg->getUseEarlyCU() && rpcBestCU->getTotalCost()!=MAX_DOUBLE && rpcBestCU->isSkipped(0) );
// true = false || !(false && ture)
// bBoudary==true 说明该CU跨过了Slice,因此必须继续划分
// 后面三项只有一项为false即可

if( bSubBranch && uiDepth < sps.getLog2DiffMaxMinCodingBlockSize() && (!getFastDeltaQp() || uiWidth > fastDeltaQPCuMaxSize || bBoundary))
{
// 进一步划分
Double splitTotalCost = 0;
// 遍历QP
for (Int iQP=iMinQP; iQP<=iMaxQP; iQP++)
{
  const Bool bIsLosslessMode = false; 
  // False at this level. Next level down may set it to true.
    
  // 初始化Temp,存放此次划分模式
  rpcTempCU->initEstData( uiDepth, iQP, bIsLosslessMode );

  UChar       uhNextDepth         = uiDepth+1;
  TComDataCU* pcSubBestPartCU     = m_ppcBestCU[uhNextDepth]; // 在TEncCU::create()中被创建
  TComDataCU* pcSubTempPartCU     = m_ppcTempCU[uhNextDepth]; // 同上
  DEBUG_STRING_NEW(sTempDebug)
  // 遍历划分后的四个子区域
  // for循环中调用四次xCompressCU函数,计算子CU的RDcost,并将四个子CU的RDcost存放到rpcTempCU中
  for ( UInt uiPartUnitIdx = 0; uiPartUnitIdx < 4; uiPartUnitIdx++ )
  {
	//初始化下一层BestCU和TempCU,存放划分后的一个子CU
	pcSubBestPartCU->initSubCU( rpcTempCU, uiPartUnitIdx, uhNextDepth, iQP );          
	pcSubTempPartCU->initSubCU( rpcTempCU, uiPartUnitIdx, uhNextDepth, iQP );         
	//判断子CU的位置是否出界,并加载RD coder
	if( ( pcSubBestPartCU->getCUPelX() < sps.getPicWidthInLumaSamples() ) && ( pcSubBestPartCU->getCUPelY() < sps.getPicHeightInLumaSamples() ) )
	{
	  // 如果是第一个子CU,需初始化先前深度的buffer中的RD coder
	  if ( 0 == uiPartUnitIdx) //initialize RD with previous depth buffer
	  {
		m_pppcRDSbacCoder[uhNextDepth][CI_CURR_BEST]->load(m_pppcRDSbacCoder[uiDepth][CI_CURR_BEST]);
	  }
       // 其他三个子CU继承前面的RD coder
	  else
	  {
		m_pppcRDSbacCoder[uhNextDepth][CI_CURR_BEST]->load(m_pppcRDSbacCoder[uhNextDepth][CI_NEXT_BEST]);
	  }
	  // 递归调用xCompressCU函数
#if AMP_ENC_SPEEDUP
	  DEBUG_STRING_NEW(sChild)
	  if ( !(rpcBestCU->getTotalCost()!=MAX_DOUBLE && rpcBestCU->isInter(0)) )
	  {
		xCompressCU( pcSubBestPartCU, pcSubTempPartCU, uhNextDepth DEBUG_STRING_PASS_INTO(sChild), NUMBER_OF_PART_SIZES );
	  }
	  else
	  {
		xCompressCU( pcSubBestPartCU, pcSubTempPartCU, uhNextDepth DEBUG_STRING_PASS_INTO(sChild), rpcBestCU->getPartitionSize(0) );
	  }
	  DEBUG_STRING_APPEND(sTempDebug, sChild)
#else
	  xCompressCU( pcSubBestPartCU, pcSubTempPartCU, uhNextDepth );
#endif
	  // 将最佳部分数据`pcSubBestPartCU`保留为父CU的临时数据`rpcTempCU`中
	  rpcTempCU->copyPartFrom( pcSubBestPartCU, uiPartUnitIdx, uhNextDepth );  
	  // 保存YUV数据
	  xCopyYuv2Tmp( pcSubBestPartCU->getTotalNumPart()*uiPartUnitIdx, uhNextDepth );
	  // 累加子CU的RD cost
	  if ( m_pcEncCfg->getLumaLevelToDeltaQPMapping().isEnabled() && pps.getMaxCuDQPDepth() >= 1 )
	  {
		splitTotalCost += pcSubBestPartCU->getTotalCost();
	  }
	}
	else
	{
	  pcSubBestPartCU->copyToPic( uhNextDepth );
	  rpcTempCU->copyPartFrom( pcSubBestPartCU, uiPartUnitIdx, uhNextDepth );
	}// 结束位置判断
  }

  // 汇总四个子CU的RD cost
  m_pcRDGoOnSbacCoder->load(m_pppcRDSbacCoder[uhNextDepth][CI_NEXT_BEST]);
  if( !bBoundary )
  {
	m_pcEntropyCoder->resetBits();
	m_pcEntropyCoder->encodeSplitFlag( rpcTempCU, 0, uiDepth, true );
	if ( m_pcEncCfg->getLumaLevelToDeltaQPMapping().isEnabled() && pps.getMaxCuDQPDepth() >= 1 )
	{
	  Int splitBits = m_pcEntropyCoder->getNumberOfWrittenBits();
	  Double splitBitCost = m_pcRdCost->calcRdCost( splitBits, 0 );
	  splitTotalCost += splitBitCost;
	}

	rpcTempCU->getTotalBits() += m_pcEntropyCoder->getNumberOfWrittenBits(); // split bits
	rpcTempCU->getTotalBins() += ((TEncBinCABAC *)((TEncSbac*)m_pcEntropyCoder->m_pcEntropyCoderIf)->getEncBinIf())->getBinsCoded();
  }
  
  if ( m_pcEncCfg->getLumaLevelToDeltaQPMapping().isEnabled() && pps.getMaxCuDQPDepth() >= 1 )
  {
	rpcTempCU->getTotalCost() = splitTotalCost;
  }
  else
  {
	rpcTempCU->getTotalCost()  = m_pcRdCost->calcRdCost( rpcTempCU->getTotalBits(), rpcTempCU->getTotalDistortion() );
  }

  if( uiDepth == pps.getMaxCuDQPDepth() && pps.getUseDQP())
  {
	Bool hasResidual = false;
	for( UInt uiBlkIdx = 0; uiBlkIdx < rpcTempCU->getTotalNumPart(); uiBlkIdx ++)
	{
	  if( (     rpcTempCU->getCbf(uiBlkIdx, COMPONENT_Y)
			|| (rpcTempCU->getCbf(uiBlkIdx, COMPONENT_Cb) && (numberValidComponents > COMPONENT_Cb))
			|| (rpcTempCU->getCbf(uiBlkIdx, COMPONENT_Cr) && (numberValidComponents > COMPONENT_Cr)) ) )
	  {
		hasResidual = true;
		break;
	  }
	}

	if ( hasResidual )
	{
	  m_pcEntropyCoder->resetBits();
	  m_pcEntropyCoder->encodeQP( rpcTempCU, 0, false );
	  rpcTempCU->getTotalBits() += m_pcEntropyCoder->getNumberOfWrittenBits(); // dQP bits
	  rpcTempCU->getTotalBins() += ((TEncBinCABAC *)((TEncSbac*)m_pcEntropyCoder->m_pcEntropyCoderIf)->getEncBinIf())->getBinsCoded();
	  rpcTempCU->getTotalCost()  = m_pcRdCost->calcRdCost( rpcTempCU->getTotalBits(), rpcTempCU->getTotalDistortion() );

	  Bool foundNonZeroCbf = false;
	  rpcTempCU->setQPSubCUs( rpcTempCU->getRefQP( 0 ), 0, uiDepth, foundNonZeroCbf );
	  assert( foundNonZeroCbf );
	}
	else
	{
	  rpcTempCU->setQPSubParts( rpcTempCU->getRefQP( 0 ), 0, uiDepth ); // set QP to default QP
	}
  }

  m_pcRDGoOnSbacCoder->store(m_pppcRDSbacCoder[uiDepth][CI_TEMP_BEST]);

  // If the configuration being tested exceeds the maximum number of bytes for a slice / slice-segment, then
  // a proper RD evaluation cannot be performed. Therefore, termination of the
  // slice/slice-segment must be made prior to this CTU.
  // This can be achieved by forcing the decision to be that of the rpcTempCU.
  // The exception is each slice / slice-segment must have at least one CTU.
  if (rpcBestCU->getTotalCost()!=MAX_DOUBLE)
  {
	const Bool isEndOfSlice        =    pcSlice->getSliceMode()==FIXED_NUMBER_OF_BYTES
									 && ((pcSlice->getSliceBits()+rpcBestCU->getTotalBits())>pcSlice->getSliceArgument()<<3)
									 && rpcBestCU->getCtuRsAddr() != pcPic->getPicSym()->getCtuTsToRsAddrMap(pcSlice->getSliceCurStartCtuTsAddr())
									 && rpcBestCU->getCtuRsAddr() != pcPic->getPicSym()->getCtuTsToRsAddrMap(pcSlice->getSliceSegmentCurStartCtuTsAddr());
	const Bool isEndOfSliceSegment =    pcSlice->getSliceSegmentMode()==FIXED_NUMBER_OF_BYTES
									 && ((pcSlice->getSliceSegmentBits()+rpcBestCU->getTotalBits()) > pcSlice->getSliceSegmentArgument()<<3)
									 && rpcBestCU->getCtuRsAddr() != pcPic->getPicSym()->getCtuTsToRsAddrMap(pcSlice->getSliceSegmentCurStartCtuTsAddr());
										 // Do not need to check slice condition for slice-segment since a slice-segment is a subset of a slice.
	if(isEndOfSlice||isEndOfSliceSegment)
	{
	  rpcBestCU->getTotalCost()=MAX_DOUBLE;
	}
  }

  //*****************比较,决定是否进一步划分***********//
  // 比较rpcBestCU, rpcTempCU的RD Cost决定是否对16x16CU进行进一步的划分,即`rpcBestCU->getTotalCost()`和`rpcTempCU->getTotalCost()`
  // 如果需要进一步划分,则将m_ppcRecoYuvTemp[depth]中的重建YUV拷贝到m_ppcRecoYuvBest[depth]中
  xCheckBestMode( rpcBestCU, rpcTempCU, uiDepth DEBUG_STRING_PASS_INTO(sDebug) DEBUG_STRING_PASS_INTO(sTempDebug) DEBUG_STRING_PASS_INTO(false) ); // RD compare current larger prediction
}
}

4. 递归终止条件

  // Assert if Best prediction mode is NONE
  // Selected mode's RD-cost must be not MAX_DOUBLE.
  assert( rpcBestCU->getPartitionSize ( 0 ) != NUMBER_OF_PART_SIZES       );
  assert( rpcBestCU->getPredictionMode( 0 ) != NUMBER_OF_PREDICTION_MODES );
  assert( rpcBestCU->getTotalCost     (   ) != MAX_DOUBLE                 );
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值