上次说了VTM中QTMTT划分的理论部分,这次准备结合代码理解,VTM完全就是C++的代码风格,由于水平有限,应该会有很多理解不到位的地方,还请大佬批评指正,欢迎交流。不同于HEVC,VTM中将CU划分方式也当做编码模式进行处理。在编码每个CU时,首先将初始化所有的编码模式,包括所有的预测模式和划分模式。这部分由lib/EncoderLib/EncCU.cpp
中xCompressCU()
完成。xCompressCU()
调用initCULevel()
初始化所有编码模式,编码模式的控制管理定义在EncModeCtrl
类中,首先看一下initCULevel()
这个函数,代码版本是VTM-4.0。
initCULevel( Partitioner &partitioner, const CodingStructure& cs )
{
// Min/max depth
unsigned minDepth = 0;
unsigned maxDepth = g_aucLog2[cs.sps->getSpsNext().getCTUSize()] - g_aucLog2[cs.sps->getSpsNext().getMinQTSize( m_slice->getSliceType(), partitioner.chType )];
if( m_pcEncCfg->getUseFastLCTU() )
{
if( auto adPartitioner = dynamic_cast<AdaptiveDepthPartitioner*>( &partitioner ) )
{
// LARGE CTU
adPartitioner->setMaxMinDepth( minDepth, maxDepth, cs ); // 根据上、左、左上、右上相邻CU深度信息设置最小最大QT深度
}
}
m_ComprCUCtxList.push_back( ComprCUCtx( cs, minDepth, maxDepth, NUM_EXTRA_FEATURES ) ); // 模式控制对象容器,存储模式及相关信息
const CodingUnit* cuLeft = cs.getCU( cs.area.blocks[partitioner.chType].pos().offset( -1, 0 ), partitioner.chType );
const CodingUnit* cuAbove = cs.getCU( cs.area.blocks[partitioner.chType].pos().offset( 0, -1 ), partitioner.chType );
// 根据上下文深度和CU尺寸调整划分执行顺序--先做QT还是后做QT
const bool qtBeforeBt = ( ( cuLeft && cuAbove && cuLeft ->qtDepth > partitioner.currQtDepth && cuAbove->qtDepth > partitioner.currQtDepth )
|| ( cuLeft && !cuAbove && cuLeft ->qtDepth > partitioner.currQtDepth )
|| ( !cuLeft && cuAbove && cuAbove->qtDepth > partitioner.currQtDepth )
|| ( !cuAbove && !cuLeft && cs.area.lwidth() >= ( 32 << cs.slice->getDepth() ) ) )
&& ( cs.area.lwidth() > ( cs.pcv->getMinQtSize( *cs.slice, partitioner.chType ) << 1 ) );
// set features
ComprCUCtx &cuECtx = m_ComprCUCtxList.back();
cuECtx.set( BEST_NON_SPLIT_COST, MAX_DOUBLE );
cuECtx.set( BEST_VERT_SPLIT_COST, MAX_DOUBLE );
cuECtx.set( BEST_HORZ_SPLIT_COST, MAX_DOUBLE );
cuECtx.set( BEST_TRIH_SPLIT_COST, MAX_DOUBLE );
cuECtx.set( BEST_TRIV_SPLIT_COST, MAX_DOUBLE );
cuECtx.set( DO_TRIH_SPLIT, cs.sps->getSpsNext().getMTTMode() & 1 );
cuECtx.set( DO_TRIV_SPLIT, cs.sps->getSpsNext().getMTTMode() & 1 );
cuECtx.set( BEST_IMV_COST, MAX_DOUBLE * .5 );
cuECtx.set( BEST_NO_IMV_COST, MAX_DOUBLE * .5 );
cuECtx.set( QT_BEFORE_BT, qtBeforeBt );
cuECtx.set( DID_QUAD_SPLIT, false );
cuECtx.set( IS_BEST_NOSPLIT_SKIP, false );
cuECtx.set( MAX_QT_SUB_DEPTH, 0 );
const bool isReusingCu = isValid( cs, partitioner );
cuECtx.set( IS_REUSING_CU, isReusingCu );
// QP
int baseQP = cs.baseQP;
if( m_pcEncCfg->getUseAdaptiveQP() )
{
baseQP = Clip3( -cs.sps->getQpBDOffset( CHANNEL_TYPE_LUMA ), MAX_QP, baseQP + xComputeDQP( cs, partitioner ) );
}
int minQP = baseQP;
int maxQP = baseQP;
if( m_pcEncCfg->getLumaLevelToDeltaQPMapping().isEnabled() )
{
if( partitioner.currDepth <= cs.pps->getMaxCuDQPDepth() )
{
CompArea clipedArea = clipArea( cs.area.Y(), cs.picture->Y() );
// keep using the same m_QP_LUMA_OFFSET in the same CTU
m_lumaQPOffset = calculateLumaDQP( cs.getOrgBuf( clipedArea ) );
}
}
xGetMinMaxQP( minQP, maxQP, cs, partitioner, baseQP, *cs.sps, *cs.pps, true );
// 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 ) ) // 如果BT before QT,先将QT放入
{
for( int qp = maxQP; qp >= minQP; qp-- )
{
m_ComprCUCtxList.back().testModes.push_back( { ETM_SPLIT_QT, SIZE_2Nx2N, ETO_STANDARD, qp, false } );
}
}
if( partitioner.canSplit( CU_TRIV_SPLIT, cs ) ) // cansplit判断是够可进行特定类别划分
{
// add split modes
for( int qp = maxQP; qp >= minQP; qp-- )
{
m_ComprCUCtxList.back().testModes.push_back( { ETM_SPLIT_TT_V, SIZE_2Nx2N, ETO_STANDARD, qp, false } ); // 三叉树垂直划分
}
}
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, SIZE_2Nx2N, ETO_STANDARD, qp, false } ); // 三叉树水平划分
}
}
if( partitioner.canSplit( CU_VERT_SPLIT, cs ) )
{
// add split modes
for( int qp = maxQP; qp >= minQP; qp-- )
{
m_ComprCUCtxList.back().testModes.push_back( { ETM_SPLIT_BT_V, SIZE_2Nx2N, ETO_STANDARD, qp, false } ); // 二叉树垂直划分
}
m_ComprCUCtxList.back().set( DID_VERT_SPLIT, true );
}
else
{
m_ComprCUCtxList.back().set( DID_VERT_SPLIT, false );
}
if( partitioner.canSplit( CU_HORZ_SPLIT, cs ) )
{
// add split modes
for( int qp = maxQP; qp >= minQP; qp-- )
{
m_ComprCUCtxList.back().testModes.push_back( { ETM_SPLIT_BT_H, SIZE_2Nx2N, ETO_STANDARD, qp, false } ); // 二叉树水平
}
m_ComprCUCtxList.back().set( DID_HORZ_SPLIT, true );
}
else
{
m_ComprCUCtxList.back().set( DID_HORZ_SPLIT, false );
}
if( cuECtx.get<bool>( QT_BEFORE_BT ) ) // 如果QT before BT,在MT后将QT放入
{
for( int qp = maxQP; qp >= minQP; qp-- )
{
m_ComprCUCtxList.back().testModes.push_back( { ETM_SPLIT_QT, SIZE_2Nx2N, ETO_STANDARD, qp, false } );
}
}
m_ComprCUCtxList.back().testModes.push_back( { ETM_POST_DONT_SPLIT } );
if( isReusingCu )
{
m_ComprCUCtxList.back().testModes.push_back( { ETM_RECO_CACHED } );
}
xGetMinMaxQP( minQP, maxQP, cs, partitioner, baseQP, *cs.sps, *cs.pps, false );
bool useLossless = false;
int lowestQP = minQP;
if( cs.pps->getTransquantBypassEnabledFlag() )
{
useLossless = true; // mark that the first iteration is to cost TQB mode.
minQP = minQP - 1; // increase loop variable range by 1, to allow testing of TQB mode along with other QPs
if( m_pcEncCfg->getCUTransquantBypassFlagForceValue() )
{
maxQP = minQP;
}
}
//////////////////////////////////////////////////////////////////////////
// Add unit coding modes: Intra, InterME, InterMerge ...
for( int qpLoop = maxQP; qpLoop >= minQP; qpLoop-- ) // 帧内预测模式
{
const int qp = std::max( qpLoop, lowestQP );
const bool lossless = useLossless && qpLoop == minQP;
// add intra modes
m_ComprCUCtxList.back().testModes.push_back( { ETM_IPCM, SIZE_2Nx2N, ETO_STANDARD, qp, lossless } );
m_ComprCUCtxList.back().testModes.push_back( { ETM_INTRA, SIZE_2Nx2N, ETO_STANDARD, qp, lossless } );
}
// add first pass modes
if( !m_slice->isIntra() ) // 帧间预测模式
{
for( int qpLoop = maxQP; qpLoop >= minQP; qpLoop-- )
{
const int qp = std::max( qpLoop, lowestQP );
const bool lossless = useLossless && qpLoop == minQP;
if( m_pcEncCfg->getIMV() )
{
if( m_pcEncCfg->getIMV() == IMV_4PEL )
{
int imv = m_pcEncCfg->getIMV4PelFast() ? 3 : 2;
m_ComprCUCtxList.back().testModes.push_back( { ETM_INTER_ME, SIZE_2Nx2N, EncTestModeOpts( imv << ETO_IMV_SHIFT ), qp, lossless } );
}
m_ComprCUCtxList.back().testModes.push_back( { ETM_INTER_ME, SIZE_2Nx2N, EncTestModeOpts( 1 << ETO_IMV_SHIFT ), qp, lossless } );
}
// add inter modes
if( m_pcEncCfg->getUseEarlySkipDetection() )
{
m_ComprCUCtxList.back().testModes.push_back( { ETM_MERGE_SKIP, SIZE_2Nx2N, ETO_STANDARD, qp, lossless } );
if( cs.sps->getSpsNext().getUseAffine() )
{
m_ComprCUCtxList.back().testModes.push_back( { ETM_AFFINE, SIZE_2Nx2N, ETO_STANDARD, qp, lossless } );
}
m_ComprCUCtxList.back().testModes.push_back( { ETM_INTER_ME, SIZE_2Nx2N, ETO_STANDARD, qp, lossless } );
}
else
{
m_ComprCUCtxList.back().testModes.push_back( { ETM_INTER_ME, SIZE_2Nx2N, ETO_STANDARD, qp, lossless } );
m_ComprCUCtxList.back().testModes.push_back( { ETM_MERGE_SKIP, SIZE_2Nx2N, ETO_STANDARD, qp, lossless } );
if( cs.sps->getSpsNext().getUseAffine() )
{
m_ComprCUCtxList.back().testModes.push_back( { ETM_AFFINE, SIZE_2Nx2N, ETO_STANDARD, qp, lossless } );
}
}
}
}
// ensure to skip unprobable modes
if( !tryModeMaster( m_ComprCUCtxList.back().testModes.back(), cs, partitioner ) ) // 判断模式执行条件
{
nextMode( cs, partitioner );
}
m_ComprCUCtxList.back().lastTestMode = EncTestMode();
}
类似于HEVC,每层CU首先进行逐个模式预测(除非判定当前CU为SKIP,将跳过剩下所有预测,直接进行划分),然后进行各种类型的CU划分。在initCULevel()
最后调用了tryModeMaster()
模式判定,同样在xcompressCU()
中进行编码模式遍历时,会调用EncModeCtrl::nextMode()
,其核心也是调用tryModeMaster()
对编码模式进行提前跳过或者终止判定。针对划分的提前终止和跳过,主要是基于SKIP模式和上下文深度,例如1. 如果编码树上层最优为SKIP模式连续超过三层,则终止划分;2. 基于梯度阈值终止划分;3. 当前最优CU是SKIP模式,且其mt深度大于阈值(根据当前帧参考帧距离决定,加入CPR模式后,阈值默认为2),划分跳过。
对CU区域的划分,VTM通过QTBTPartitioner
类进行管理,划分由splitCurrArea ()
实现
void QTBTPartitioner::splitCurrArea( const PartSplit split, const CodingStructure& cs )
{
CHECKD( !canSplit( split, cs ), "Trying to apply a prohibited split!" );
bool isImplicit = isSplitImplicit( split, cs ); // 盘算CU是否横跨边界,如果横跨边界:split 和 约定划分相同,返回true,否则返回false
bool canQtSplit = canSplit( CU_QUAD_SPLIT, cs );
switch( split )
{
case CU_QUAD_SPLIT:
m_partStack.push_back( PartLevel( split, PartitionerImpl::getCUSubPartitions( currArea(), cs ) ) );
break;
case CU_HORZ_SPLIT:
case CU_VERT_SPLIT:
m_partStack.push_back( PartLevel( split, PartitionerImpl::getCUSubPartitions( currArea(), cs, split ) ) );
break;
case CU_TRIH_SPLIT:
case CU_TRIV_SPLIT:
m_partStack.push_back( PartLevel( split, PartitionerImpl::getCUSubPartitions( currArea(), cs, split ) ) );
break;
case TU_MAX_TR_SPLIT:
m_partStack.push_back( PartLevel( split, PartitionerImpl::getMaxTuTiling( currArea(), cs ) ) );
break;
#if JVET_M0140_SBT // sub block transform
case SBT_VER_HALF_POS0_SPLIT:
case SBT_VER_HALF_POS1_SPLIT:
case SBT_HOR_HALF_POS0_SPLIT:
case SBT_HOR_HALF_POS1_SPLIT:
case SBT_VER_QUAD_POS0_SPLIT:
case SBT_VER_QUAD_POS1_SPLIT:
case SBT_HOR_QUAD_POS0_SPLIT:
case SBT_HOR_QUAD_POS1_SPLIT:
m_partStack.push_back( PartLevel( split, PartitionerImpl::getSbtTuTiling( currArea(), cs, split ) ) );
break;
#endif
default:
THROW( "Unknown split mode" );
break;
}
currDepth++; // 每次划分,深度加一
#if _DEBUG
m_currArea = m_partStack.back().parts.front();
#endif
if( split == TU_MAX_TR_SPLIT )
{
currTrDepth++;
}
#if JVET_M0140_SBT
else if( split >= SBT_VER_HALF_POS0_SPLIT && split <= SBT_HOR_QUAD_POS1_SPLIT )
{
currTrDepth++;
}
#endif
else
{
currTrDepth = 0;
}
if( split == CU_HORZ_SPLIT || split == CU_VERT_SPLIT || split == CU_TRIH_SPLIT || split == CU_TRIV_SPLIT ) // mt划分
{
currBtDepth++; // Bt depth 加1
if( isImplicit ) currImplicitBtDepth++;
currMtDepth++; // mt depth 加1
if( split == CU_TRIH_SPLIT || split == CU_TRIV_SPLIT ) // 三叉树划分,两边CU的btdepth 加一
{
// first and last part of triple split are equivalent to double bt split
currBtDepth++;
}
m_partStack.back().canQtSplit = canQtSplit;
}
else if( split == CU_QUAD_SPLIT ) // QT 划分,bt/mt 深度为0
{
CHECK( currBtDepth > 0, "Cannot split a non-square area other than with a binary split" );
CHECK( currMtDepth > 0, "Cannot split a non-square area other than with a binary split" );
currMtDepth = 0;
currBtDepth = 0;
currQtDepth++;
}
}
getCUSubPartitions()划分逻辑的具体实现,计算每个SubCU的长、宽以及位置信息。
typedef std::vector
Partitioning PartitionerImpl::getCUSubPartitions( const UnitArea &cuArea, const CodingStructure &cs, const PartSplit _splitType /*= CU_QUAD_SPLIT*/ )
{
const PartSplit splitType = _splitType;
if( splitType == CU_QUAD_SPLIT )
{
if( !cs.pcv->noChroma2x2 )
{
Partitioning sub;
sub.resize( 4, cuArea );
for( uint32_t i = 0; i < 4; i++ )
{
for( auto &blk : sub[i].blocks )
{
blk.height >>= 1;
blk.width >>= 1;
if( i >= 2 ) blk.y += blk.height;
if( i & 1 ) blk.x += blk.width;
}
CHECK( sub[i].lumaSize().height < MIN_TU_SIZE, "the split causes the block to be smaller than the minimal TU size" );
}
return sub;
}
else
{
const uint32_t minCUSize = ( cs.sps->getMaxCUWidth() >> cs.sps->getMaxCodingDepth() );
bool canSplit = cuArea.lumaSize().width > minCUSize && cuArea.lumaSize().height > minCUSize;
Partitioning ret;
if( cs.slice->getSliceType() == I_SLICE )
{
canSplit &= cuArea.lumaSize().width > cs.pcv->minCUWidth && cuArea.lumaSize().height > cs.pcv->minCUHeight;
}
if( canSplit )
{
ret.resize( 4 );
if( cuArea.chromaFormat == CHROMA_400 )
{
CompArea blkY = cuArea.Y();
blkY.width >>= 1;
blkY.height >>= 1;
ret[0] = UnitArea( cuArea.chromaFormat, blkY );
blkY.x += blkY.width;
ret[1] = UnitArea( cuArea.chromaFormat, blkY );
blkY.x -= blkY.width;
blkY.y += blkY.height;
ret[2] = UnitArea( cuArea.chromaFormat, blkY );
blkY.x += blkY.width;
ret[3] = UnitArea( cuArea.chromaFormat, blkY );
}
else
{
for( uint32_t i = 0; i < 4; i++ )
{
ret[i] = cuArea;
CompArea &blkY = ret[i].Y();
CompArea &blkCb = ret[i].Cb();
CompArea &blkCr = ret[i].Cr();
blkY.width /= 2;
blkY.height /= 2;
// TODO: get those params from SPS
if( blkCb.width > 4 )
{
blkCb.width /= 2;
blkCb.height /= 2;
blkCr.width /= 2;
blkCr.height /= 2;
}
else if( i > 0 )
{
blkCb = CompArea();
blkCr = CompArea();
}
if( ( i & 1 ) == 1 )
{
blkY.x += blkY .width;
blkCb.x += blkCb.width;
blkCr.x += blkCr.width;
}
if( i > 1 )
{
blkY.y += blkY .height;
blkCb.y += blkCb.height;
blkCr.y += blkCr.height;
}
}
}
}
return ret;
}
}
else if( splitType == CU_HORZ_SPLIT )
{
Partitioning sub;
sub.resize(2, cuArea);
for (uint32_t i = 0; i < 2; i++)
{
for (auto &blk : sub[i].blocks)
{
blk.height >>= 1;
if (i == 1) blk.y += blk.height;
}
CHECK(sub[i].lumaSize().height < MIN_TU_SIZE, "the cs split causes the block to be smaller than the minimal TU size");
}
return sub;
}
else if( splitType == CU_VERT_SPLIT )
{
Partitioning sub;
sub.resize( 2, cuArea );
for( uint32_t i = 0; i < 2; i++ )
{
for( auto &blk : sub[i].blocks )
{
blk.width >>= 1;
if( i == 1 ) blk.x += blk.width;
}
CHECK( sub[i].lumaSize().width < MIN_TU_SIZE, "the split causes the block to be smaller than the minimal TU size" );
}
return sub;
}
else if( splitType == CU_TRIH_SPLIT )
{
Partitioning sub;
sub.resize( 3, cuArea );
for( int i = 0; i < 3; i++ )
{
for( auto &blk : sub[i].blocks )
{
blk.height >>= 1;
if( ( i + 1 ) & 1 ) blk.height >>= 1;
if( i == 1 ) blk.y += blk.height / 2;
if( i == 2 ) blk.y += 3 * blk.height;
}
CHECK( sub[i].lumaSize().height < MIN_TU_SIZE, "the cs split causes the block to be smaller than the minimal TU size" );
}
return sub;
}
else if( splitType == CU_TRIV_SPLIT )
{
Partitioning sub;
sub.resize( 3, cuArea );
for( int i = 0; i < 3; i++ )
{
for( auto &blk : sub[i].blocks )
{
blk.width >>= 1;
if( ( i + 1 ) & 1 ) blk.width >>= 1;
if( i == 1 ) blk.x += blk.width / 2;
if( i == 2 ) blk.x += 3 * blk.width;
}
CHECK( sub[i].lumaSize().width < MIN_TU_SIZE, "the cs split causes the block to be smaller than the minimal TU size" );
}
return sub;
}
else
{
THROW( "Unknown CU sub-partitioning" );
return Partitioning();
}
}