最近需要研究HM函数与块划分有关(TLibEncoder\TEncCu
),但是网上参考资料很少,这部分代码复杂,所以本文是基于最新的16.20版,试着分析实现原理,以后会更新x265对应部分的源码剖析。
HM15.本部分的代码分析可参考[本博客](https://blog.csdn.net/qq_21880777/article/details/78827285)。
CTU层次
基本上分为两个步骤。首先,压缩CTU,然后编码CTU
TEncCu::compressCtu
压缩CTU
- 顶层的初始化CTU数据,
m_ppcBestCU[0]->initCtu()
和m_ppcTempCU[0]->initCtu()
- 递归的调用
xCompressCU()
压缩CTU
TEncCu::encodeCtu
编码CTU
- 根据Slice的设置初始化QP的参数
- 递归调用
xEncodeCU()
编码CTU
CU层次
xCompressCU( )
从前面的x
可见这种方法是一种protected成员函数的类型,根据xCheckRDCostMerge2Nx2N
和xCheckRDCostInter
根据定位帧间预测部分,xCheckRDCostIntra
定位帧中的预测部分。( 补充:关于HM命名规则和项目结构可以参考我之前的博客。
传入参数为rpcBestCU
,rpcTempCU
,uiDepth
,其中uiDepth
即为当前CU深度(0、1、2)。
详细步骤如下
1. 参数初始化(读取BestCU用于计算数据和参数BestCU RD cost的量化参数QP)
参数初始化分为两部分,读取第一部分rpcBestCU
所代表CU的YUV存储在数据、坐标信息和有效组件中的数量m_ppcOriginYuv[yiDepth]
、uiLPelX
~uiBelY
、numberValidComponents
有效组件的类型为枚举类型:
| 枚举类型 | 值 |
| :---------------: | :--: |
| COMPONENT_Y | 0 |
| COMPONENT_Cb | 1 |
| COMPONENT_Cr | 2 |
| MAX_NUM_COMPONENT | 3 |
第二部分,解析图片参数集pps
(picture parameter set)和 序列参数集sps
(sequence parameter set),量化参数主要集中在分析参数上QP的设置,QP分别由四个参数控制iBaseQP
、iMinQP
、iMaxQP
和isAddLowestQP
。
首先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调整不同的初始化方法iQP
之后,,如果当前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
- 调整
iQP
,初始化rpcTempCU
- 计算的RD cost,分为2Nx2N,NxN,Nx2N,2NxN 和 AMP五种模式,每个模式用到的核心函数只有两个
xCheckRDCostInter()
和rpcTempCU->initEstData()
- 计算的RD cost
- 读取CBF配置,若CU的预测模式为MODE_INTER,其对应的预测残差不包含非零的变换系数,则跳过该CU的其他候选模式,即当前模式为CU的最优模式,直接进行下一步的四叉树划分
- 调用
xCheckRDCostIntra()
,对2Nx2N的CU进行预测,并计算RD Cost 调用rpcTempCU->initEstData()
初始化TempCU - PCM模式下执行和上一步类似的操作
- 汇总当前的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:
- 初始化,这里的变量命名方式和上面类似,但是都加上了
Sub
,例如pcSubBestPartCU
和pcSubTempPartCU
指向下一层 - 遍历划分后的四个子CU,分别调用四次
xCompressCU
函数,计算子CU的RD cost,并将RD cost存放到rpcTempCU
中 - 汇总遍历划分的子CU的RD cost
- 比较结果,判断是否划分
比较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 );
说了这么多,还不是如一张流程图直观: