H.266/VVC专栏传送
上一篇:H.266/VVC-VTM代码学习22-partitioner类判断划分模式是否可用函数partitioner.canSplit
下一篇:
目录
前言
VTM是H.266/VVC视频编码标准的参考软件,研究VTM代码给研究人员解释了VVC编码标准的详细标准规范与细节。
本文是笔者对VTM代码的一点学习记录,成文于笔者刚开始接触VVC期间,期间很多概念和理论框架还很不成熟,若文中存在错误欢迎批评指正,也欢迎广大视频编码学习者沟通交流、共同进步。
VTM代码的下载及编译请参考博文:
【视频编码学习】H.266/VVC参考软件VTM配置运行(VTM-6.0版本)
一、简介
本文介绍 VTM10.0 进行 RDO 过程时,选择块模式(Intra 或 Inter)部分的代码,是 initCULevel 函数中将 Intra 及 Inter 根据具体情况加入 RDO 列表的代码。
initCULevel 函数的完整注释详解请参考:
H.266/VVC-VTM代码学习19-CU层确定测试模式函数initCULevel
xCompressCU 函数的完整注释详解请参考:
H.266/VVC-VTM代码学习20-CU层进行RDO函数xCompressCU
二、代码详解
1. 将 Intra/Inter 加入 RDO 列表
initCULevel 函数(xCompressCU 函数调用 initCULevel)代码片段:
① Intra
// add intra modes
// 若尝试 Intra RDO
if( tryIntraRdo )
{
if (cs.slice->getSPS()->getPLTMode() && (partitioner.treeType != TREE_D || cs.slice->isIntra() || (cs.area.lwidth() == 4 && cs.area.lheight() == 4)) && getPltEnc())
{
// 加入 palette mode(利用CU内部的样本只是有少数几种典型的颜色组成的特性进行编码) RDO
m_ComprCUCtxList.back().testModes.push_back({ ETM_PALETTE, ETO_STANDARD, qp });
}
// 加入 Intra RDO
m_ComprCUCtxList.back().testModes.push_back( { ETM_INTRA, ETO_STANDARD, qp } );
if (cs.slice->getSPS()->getPLTMode() && partitioner.treeType == TREE_D && !cs.slice->isIntra() && !(cs.area.lwidth() == 4 && cs.area.lheight() == 4) && getPltEnc())
{
// 加入 palette mode RDO
m_ComprCUCtxList.back().testModes.push_back({ ETM_PALETTE, ETO_STANDARD, qp });
}
}
由这部分代码可见,将 Intra 加入 RDO 列表的条件是 tryIntraRdo 为 True。
② Inter
if ( !m_slice->isIntra() && !( cs.area.lwidth() == 4 && cs.area.lheight() == 4 ) && tryInterRdo )
{
for( int qpLoop = maxQP; qpLoop >= minQP; qpLoop-- )
{
const int qp = std::max( qpLoop, lowestQP );
if (m_pcEncCfg->getIMV())
{
m_ComprCUCtxList.back().testModes.push_back({ ETM_INTER_ME, EncTestModeOpts( 4 << ETO_IMV_SHIFT ), qp });
}
if( m_pcEncCfg->getIMV() || m_pcEncCfg->getUseAffineAmvr() )
{
int imv = m_pcEncCfg->getIMV4PelFast() ? 3 : 2;
m_ComprCUCtxList.back().testModes.push_back( { ETM_INTER_ME, EncTestModeOpts( imv << ETO_IMV_SHIFT ), qp } );
m_ComprCUCtxList.back().testModes.push_back( { ETM_INTER_ME, EncTestModeOpts( 1 << ETO_IMV_SHIFT ), qp } );
}
// add inter modes
if( m_pcEncCfg->getUseEarlySkipDetection() )
{
if( cs.sps->getUseGeo() && cs.slice->isInterB() )
{
m_ComprCUCtxList.back().testModes.push_back( { ETM_MERGE_GEO, ETO_STANDARD, qp } );
}
m_ComprCUCtxList.back().testModes.push_back( { ETM_MERGE_SKIP, ETO_STANDARD, qp } );
if (cs.sps->getUseAffine() || cs.sps->getSbTMVPEnabledFlag())
{
m_ComprCUCtxList.back().testModes.push_back( { ETM_AFFINE, ETO_STANDARD, qp } );
}
m_ComprCUCtxList.back().testModes.push_back( { ETM_INTER_ME, ETO_STANDARD, qp } );
}
else
{
m_ComprCUCtxList.back().testModes.push_back( { ETM_INTER_ME, ETO_STANDARD, qp } );
if( cs.sps->getUseGeo() && cs.slice->isInterB() )
{
m_ComprCUCtxList.back().testModes.push_back( { ETM_MERGE_GEO, ETO_STANDARD, qp } );
}
m_ComprCUCtxList.back().testModes.push_back( { ETM_MERGE_SKIP, ETO_STANDARD, qp } );
if (cs.sps->getUseAffine() || cs.sps->getSbTMVPEnabledFlag())
{
m_ComprCUCtxList.back().testModes.push_back( { ETM_AFFINE, ETO_STANDARD, qp } );
}
}
// 加入 Hash_Inter RDO
if (m_pcEncCfg->getUseHashME())
{
int minSize = min(cs.area.lwidth(), cs.area.lheight());
if (minSize < 128 && minSize >= 4)
{
m_ComprCUCtxList.back().testModes.push_back({ ETM_HASH_INTER, ETO_STANDARD, qp });
}
}
}
}
由这部分代码可见,将 Inter 加入 RDO 列表的条件是当前帧不为 intra, tryIntraRdo 为 True 且当前区域宽高不同时为4 且 tryInterRdo 为 True。
2. tryIntraRdo 和 tryInterRdo 两变量的确定
下面对该变量进行具体分析,以下仍是 initCULevel 函数代码片段。
// 是否尝试 Intra RDO
bool tryIntraRdo = true;
// 是否尝试 Inter RDO
bool tryInterRdo = true;
// 是否尝试 IBC(Intra block copy) RDO
bool tryIBCRdo = true;
// 若指定 isConsIntra,则不尝试 Inter RDO
if( partitioner.isConsIntra() )
{
tryInterRdo = false;
}
// 若指定 isConsInter,则不尝试 IBC RDO
else if( partitioner.isConsInter() )
{
tryIntraRdo = tryIBCRdo = false;
}
由这部分代码可见:
- tryIntraRdo 和 tryInterRdo 初始化为 True
- 仅当 partitioner.isConsInter() 为 True 时,tryIntraRdo 为 false
- 仅当 partitioner.isConsIntra() 为 True 时,tryInterRdo 为 false
而 partitioner.isConsIntra() 和 partitioner.isConsInter() 函数直接获取 partitioner.modeType,这一变量的值由 xCompressCU 函数中对分块模式的处理部分代码确定,以下是这部分代码:
// 获得当前信号模式值(LDT_MODE_TYPE_INHERIT、LDT_MODE_TYPE_INFER、LDT_MODE_TYPE_SIGNAL)
int signalModeConsVal = tempCS->signalModeCons( getPartSplit( currTestMode ), partitioner, modeTypeParent );
// 若当前信号模式值为 LDT_MODE_TYPE_SIGNAL(Need to signal mode_constraint_flag, and the modeType of the region is determined by the flag),则 RDO 轮次为2,否则为1
int numRoundRdo = signalModeConsVal == LDT_MODE_TYPE_SIGNAL ? 2 : 1;
bool skipInterPass = false;
// 遍历 RDO 轮次
for( int i = 0; i < numRoundRdo; i++ )
{
//change cons modes
// 若当前信号模式值为 LDT_MODE_TYPE_SIGNAL
// 则当前测试模式类型在第一轮设置为 INTER,在第二轮设置为 INTRA
if( signalModeConsVal == LDT_MODE_TYPE_SIGNAL )
{
CHECK( numRoundRdo != 2, "numRoundRdo shall be 2 - [LDT_MODE_TYPE_SIGNAL]" );
tempCS->modeType = partitioner.modeType = (i == 0) ? MODE_TYPE_INTER : MODE_TYPE_INTRA;
}
// 若当前信号模式值为 LDT_MODE_TYPE_INFER
// 则当前测试模式类型为 INTRA
else if( signalModeConsVal == LDT_MODE_TYPE_INFER )
{
CHECK( numRoundRdo != 1, "numRoundRdo shall be 1 - [LDT_MODE_TYPE_INFER]" );
tempCS->modeType = partitioner.modeType = MODE_TYPE_INTRA;
}
// 若当前信号模式值为 LDT_MODE_TYPE_INHERIT
// 则当前测试模式类型为父结点模式类型
else if( signalModeConsVal == LDT_MODE_TYPE_INHERIT )
{
CHECK( numRoundRdo != 1, "numRoundRdo shall be 1 - [LDT_MODE_TYPE_INHERIT]" );
tempCS->modeType = partitioner.modeType = modeTypeParent;
}
...
由此代码可见,当前块是否测试 Intra 即在当前块的父结点测试 split 模式时决定 partitioner.modeType,若该变量设置为 True 时,子块可进行 Intra RDO,否则不进行 Intra RDO。
由上部分代码可见,partitioner.modeType 的取值有以下几种情况:
- signalModeConsVal == LDT_MODE_TYPE_SIGNAL 时:子块 RDO 先测试 Inter 后测试 Intra;
- signalModeConsVal == LDT_MODE_TYPE_INFER 时:子块 RDO 仅测试 Intra;
- signalModeConsVal == LDT_MODE_TYPE_INHERIT 时:子块 RDO 测试模式(intra or inter)与父结点相同。
signalModeConsVal 的取值则在上部分代码中调用 signalModeCons 函数确定,以下是确定该取值的具体代码:
const int CodingStructure::signalModeCons( const PartSplit split, Partitioner &partitioner, const ModeType modeTypeParent ) const
{
// 若当前为 DualTree,或父结点模式类型不为inter与intra均尝试,或当前块色度格式为444,或当前块色度格式为400
// 则子块 RDO 时模式继承父结点
if (CS::isDualITree(*this) || modeTypeParent != MODE_TYPE_ALL || partitioner.currArea().chromaFormat == CHROMA_444 || partitioner.currArea().chromaFormat == CHROMA_400 )
{
return LDT_MODE_TYPE_INHERIT;
}
int minLumaArea = partitioner.currArea().lumaSize().area();
// 若划分模式为 QT 或 TH 或 TV
// 则最小亮度区域为当前区域/4
if (split == CU_QUAD_SPLIT || split == CU_TRIH_SPLIT || split == CU_TRIV_SPLIT) // the area is split into 3 or 4 parts
{
minLumaArea = minLumaArea >> 2;
}
// 若划分模式为 BV 或 BH
// 则最小亮度区域为当前区域/2
else if (split == CU_VERT_SPLIT || split == CU_HORZ_SPLIT) // the area is split into 2 parts
{
minLumaArea = minLumaArea >> 1;
}
// 最小色度块 = 最小亮度区域 >> scale
int minChromaBlock = minLumaArea >> (getChannelTypeScaleX(CHANNEL_TYPE_CHROMA, partitioner.currArea().chromaFormat) + getChannelTypeScaleY(CHANNEL_TYPE_CHROMA, partitioner.currArea().chromaFormat));
// (当前色度区域宽度为4 且 划分模式为 BV) 或 (当前色度区域宽度为8 且 划分模式为 TV)时,色度块尺寸为 2*N
bool is2xNChroma = (partitioner.currArea().chromaSize().width == 4 && split == CU_VERT_SPLIT) || (partitioner.currArea().chromaSize().width == 8 && split == CU_TRIV_SPLIT);
// 若最小色度块 >= 16 且 色度块尺寸不是 2*N 时,子块 RDO 模式继承父结点,否则在最小亮度块<32 或 当前帧为 INTRA 时子块 RDO 模式为 Intra,否则先子块先测试 Inter 再测试 Intra
return minChromaBlock >= 16 && !is2xNChroma ? LDT_MODE_TYPE_INHERIT : ((minLumaArea < 32) || slice->isIntra()) ? LDT_MODE_TYPE_INFER : LDT_MODE_TYPE_SIGNAL;
}
}
由以上代码可以得到以下结论:
- 为 DualTree,或父结点模式类型不为inter与intra均尝试,或当前块色度格式为444,或当前块色度格式为400 时:子块 RDO 的模式(intra/inter)继承父结点
- 划分后最小色度块 >= 16 且 色度块尺寸不是 2*N 时:子块 RDO 模式继承父结点
- 不满足第2点,且最小亮度块<32 或 当前帧为 intra 时:子块 RDO 模式为 intra
- 不满足第2点,也不满足第3点时:子块先测试 Inter 再测试 Intra
三、总结
RDO 测试模式(intra/inter)确定的代码在 initCULevel 函数中,其中 intra 模式是否测试直接由 tryIntraRdo 变量决定,inter 模式是否测试由当前区域尺寸和 tryInterRdo 变量共同决定。
tryIntraRdo 和 tryInterRdo 变量的确定取决于 partitioner.modeType 的取值,而 partitioner.modeType 的取值受 signalModeConsVal 取值约束。
由 signalModeCons 函数对 signalModeConsVal 变量取值的确定可以总结出以下规则:
- 为 DualTree,或父结点模式类型不为inter与intra均尝试,或当前块色度格式为444,或当前块色度格式为400 时:子块 RDO 的模式(intra/inter)继承父结点
- 划分后最小色度块 >= 16 且 色度块尺寸不是 2*N 时:子块 RDO 模式继承父结点
- 不满足第2点,且最小亮度块<32 或 当前帧为 intra 时:子块 RDO 模式为 intra
- 不满足第2点,也不满足第3点时:子块先测试 Inter 再测试 Intra
上一篇:H.266/VVC-VTM代码学习22-partitioner类判断划分模式是否可用函数partitioner.canSplit
下一篇:H.266/VVC-VTM代码学习24-根据当前块位置与尺寸确定隐藏划分模式getImplicitSplit()