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