上一篇:H.266/VVC-VTM代码学习-帧内预测17-initIntraPatternChTypeISP函数初始化ISP的帧内预测
下一篇:H.266/VVC-VTM代码学习19-CU层确定测试模式函数initCULevel
VTM是H.266/VVC视频编码标准的参考软件,研究VTM代码给研究人员解释了VVC编码标准的详细标准规范与细节。
本文是笔者对VTM代码的一点学习记录,成文于笔者刚开始接触VVC期间,期间很多概念和理论框架还很不成熟,若文中存在错误欢迎批评指正,也欢迎广大视频编码学习者沟通交流、共同进步。
VTM代码的下载及编译请参考博文:
【视频编码学习】H.266/VVC参考软件VTM配置运行(VTM-6.0版本)
自适应QP(Adaptive QP)是为每个 CU 自适应的选择 QP 以提升编码质量,由配置参数AdaptiveQP指定是否开启该功能。默认不开启。
自适应 QP 使用的 delta QP 计算原则是:对于平坦(即 activity 较小)CU 选择较大的 QP,对于变化较快(即 activity 较大)CU 选择较小的 QP。
CU 的 activity 由其亮度分量的方差计算得到。例如,对于一个 2Nx2N 的 CU ,首先计算其 4 个 NxN 的亮度子块的方差,然后由方差计算该 CU 的 activity: a c t = 1.0 + m i n V a r act = 1.0 + minVar act=1.0+minVar其中, m i n V a r minVar minVar 为 CU 四个子块方差中的最小值。
再利用下面几组公式即可得到 CU 对应的 delta QP:
n
o
r
m
=
s
c
a
l
e
⋅
a
c
t
+
a
c
t
a
v
g
a
c
t
+
s
c
a
l
e
⋅
a
c
t
a
v
g
norm = \frac {scale\cdot act + act_{avg}}{act + scale\cdot act_{avg}}
norm=act+scale⋅actavgscale⋅act+actavg其中:
再利用:
Δ
Q
P
=
i
n
t
(
6
⋅
log
2
n
o
r
m
)
\Delta QP = int(6 \cdot \log_{2}norm)
ΔQP=int(6⋅log2norm)
得到 delta QP。
得到 delta QP 后,将 slice 层的 initial QP 加上 delta QP 作为 baseQP,在以 baseQP 为中心的区间内取所有 QP 加入 RD 测试列表。
Lib\EncoderLib\AQp.cpp → \rightarrow → AQpPreanalyzer::preanalyze
void AQpPreanalyzer::preanalyze( Picture* pcEPic )
{
// 获得当前帧对应的亮度分量原始值
const CPelBuf lumaPlane = pcEPic->getOrigBuf().Y();
// 当前帧亮度分量宽
const int iWidth = lumaPlane.width;
// 当前帧亮度分量高
const int iHeight = lumaPlane.height;
// 当前帧亮度分量缓存中的 stride
const int iStride = lumaPlane.stride;
// 遍历当前帧的自适应量化层次(aqlayer),size 为 m_picHeader.getCuQpDeltaSubdivIntra()/2+1
for ( uint32_t d = 0; d < pcEPic->aqlayer.size(); d++ )
{
// 将指针移动到当前帧亮度分量原始值起始位置
const Pel* pLineY = lumaPlane.bufAt( 0, 0);
// 当前遍历到的自适应量化层次
AQpLayer* pcAQLayer = pcEPic->aqlayer[d];
// 当前量化层次下量化区域宽(sps.getMaxCUWidth() >> d)
const uint32_t uiAQPartWidth = pcAQLayer->getAQPartWidth();
// 当前量化层次下量化区域高(sps.getMaxCUWidth() >> d)
const uint32_t uiAQPartHeight = pcAQLayer->getAQPartHeight();
// 指针指向当前量化层次第一个单元
double* pcAQU = &pcAQLayer->getQPAdaptationUnit()[0];
// 用于存储量化区域内所有 CU 的 activity 之和
double dSumAct = 0.0;
// ---------------- 以下开始遍历当前帧在当前量化层次下的所有量化区域 -----------------
// 量化区域在垂直方向上的起始位置
for ( uint32_t y = 0; y < iHeight; y += uiAQPartHeight )
{
// 当前量化区域的高设定为量化区域高和当前帧剩余高度中的较小值
const uint32_t uiCurrAQPartHeight = std::min(uiAQPartHeight, iHeight-y);
// 量化区域在水平方向上的起始位置
for ( uint32_t x = 0; x < iWidth; x += uiAQPartWidth, pcAQU++ )
{
// 当前量化区域的宽设定为量化区域宽和当前帧剩余宽度中的较小值
const uint32_t uiCurrAQPartWidth = std::min(uiAQPartWidth, iWidth-x);
// 当前量化区域的第一个原始像素值
const Pel* pBlkY = &pLineY[x];
// 长度为4的数组用于存储当前量化区域内4个子块区域分别的像素和
uint64_t uiSum[4] = {0, 0, 0, 0};
// 长度为4的数组用于存储当前量化区域内4个子块区域分别的像素平方和
uint64_t uiSumSq[4] = {0, 0, 0, 0};
// by用于遍历当前量化区域子块的高
uint32_t by = 0;
// 遍历当前量化区域的上半区域两个子块
for ( ; by < uiCurrAQPartHeight>>1; by++ )
{
// bx用于遍历当前量化区域子块的宽
uint32_t bx = 0;
// 遍历当前量化区域的左上角子块
for ( ; bx < uiCurrAQPartWidth>>1; bx++ )
{
// 累计量化区域第一子块的像素值
uiSum [0] += pBlkY[bx];
// 累计量化区域第一子块的像素平方值
uiSumSq[0] += pBlkY[bx] * pBlkY[bx];
}
// 遍历当前量化区域的右上角子块
for ( ; bx < uiCurrAQPartWidth; bx++ )
{
// 累计量化区域第二子块的像素值
uiSum [1] += pBlkY[bx];
// 累计量化区域第二子块的像素平方值
uiSumSq[1] += pBlkY[bx] * pBlkY[bx];
}
// 累计下一行像素值
pBlkY += iStride;
}
// 遍历当前量化区域的下半区域两个子块
for ( ; by < uiCurrAQPartHeight; by++ )
{
// bx用于遍历当前量化区域子块的宽
uint32_t bx = 0;
// 遍历当前量化区域的左下角子块
for ( ; bx < uiCurrAQPartWidth>>1; bx++ )
{
// 累计量化区域第三子块的像素值
uiSum [2] += pBlkY[bx];
// 累计量化区域第三子块的像素平方值
uiSumSq[2] += pBlkY[bx] * pBlkY[bx];
}
// 遍历当前量化区域的右下角子块
for ( ; bx < uiCurrAQPartWidth; bx++ )
{
// 累计量化区域第三子块的像素值
uiSum [3] += pBlkY[bx];
// 累计量化区域第三子块的像素平方值
uiSumSq[3] += pBlkY[bx] * pBlkY[bx];
}
// 累计下一行像素值
pBlkY += iStride;
}
// 不支持量化区域宽或高为奇数
CHECK((uiCurrAQPartWidth&1)!=0, "Odd part width unsupported");
CHECK((uiCurrAQPartHeight&1)!=0, "Odd part height unsupported");
// 量化区域子块的宽
const uint32_t pixelWidthOfQuadrants = uiCurrAQPartWidth >>1;
// 量化区域子块的高
const uint32_t pixelHeightOfQuadrants = uiCurrAQPartHeight>>1;
// 量化区域子块像素数量
const uint32_t numPixInAQPart = pixelWidthOfQuadrants * pixelHeightOfQuadrants;
// 用于存放当前量化区域4个子块方差中最小值
double dMinVar = DBL_MAX;
// 若子块大小不为0
if (numPixInAQPart!=0)
{
// 遍历4个子块
for ( int i=0; i<4; i++)
{
// 计算当前子块的像素平均值
const double dAverage = double(uiSum[i]) / numPixInAQPart;
// 计算当前子块的像素方差
const double dVariance = double(uiSumSq[i]) / numPixInAQPart - dAverage * dAverage;
// 记录四个子块的方差最小值
dMinVar = std::min(dMinVar, dVariance);
}
}
else
{
// 若字块大小为0,则方差最小值设为0
dMinVar = 0.0;
}
// 当前量化区域的 activity 设置为 1+dMinVar
const double dActivity = 1.0 + dMinVar;
// 记录当前量化区域的 activity
*pcAQU = dActivity;
// 累计所有量化区域的 activity 之和
dSumAct += dActivity;
} // 遍历完当前水平方向的量化区域
pLineY += iStride * uiCurrAQPartHeight;
} // 遍历完当前帧的所有量化区域
// 计算并记录当前量化层次下所有量化区域的平均 activity
const double dAvgAct = dSumAct / (pcAQLayer->getNumAQPartInWidth() * pcAQLayer->getNumAQPartInHeight());
pcAQLayer->setAvgActivity( dAvgAct );
} // 遍历完所有量化层次
}
Lib\EncoderLib\EncModeCtrl.cpp → \rightarrow → EncModeCtrl::xComputeDQP
int EncModeCtrl::xComputeDQP( const CodingStructure &cs, const Partitioner &partitioner )
{
// 当前帧
Picture* picture = cs.picture;
// AQ 的深度(量化层次)为当前 CU 划分深度和最大划分层次的较小值
unsigned uiAQDepth = std::min( partitioner.currSubdiv/2, ( uint32_t ) picture->aqlayer.size() - 1 );
// 当前 CU 对应的量化层次
AQpLayer* pcAQLayer = picture->aqlayer[uiAQDepth];
// 计算 scale 值 (m_pcEncCfg->getQPAdaptationRange()为可设置参数,默认值为6)
double dMaxQScale = pow( 2.0, m_pcEncCfg->getQPAdaptationRange() / 6.0 );
// 获得当前帧当前量化层次下的 average activity
double dAvgAct = pcAQLayer->getAvgActivity();
// 获得当前 CU 的 activity
double dCUAct = pcAQLayer->getActivity( cs.area.Y().topLeft() );
// norm = (scale * act) / (act + scale * avgAct)
double dNormAct = ( dMaxQScale*dCUAct + dAvgAct ) / ( dCUAct + dMaxQScale*dAvgAct );
// delta QP = log2(norm) * 6
double dQpOffset = log( dNormAct ) / log( 2.0 ) * 6.0;
// 将 delta QP 四舍五入转为整数
int iQpOffset = int( floor( dQpOffset + 0.49999 ) );
return iQpOffset;
}
Lib\EncoderLib\EncModeCtrl.cpp → \rightarrow → EncModeCtrlMTnoRQT::initCULevel
// baseQP 为 slice 级的 initial QP
int baseQP = cs.baseQP;
// 当前划分树不是separate tree(分离树)是 joint tree(联合树)或当前为亮度分量
if (!partitioner.isSepTree(cs) || isLuma(partitioner.chType))
{
// 使用 adaptive QP
if (m_pcEncCfg->getUseAdaptiveQP())
{
// baseQP 取 -(6 * (m_bitDepth[CHANNEL_TYPE_LUMA] - 8) 和 63 和 sliceQP + delta 中的中值
baseQP = Clip3(-cs.sps->getQpBDOffset(CHANNEL_TYPE_LUMA), MAX_QP, baseQP + xComputeDQP(cs, partitioner));
}
#if SHARP_LUMA_DELTA_QP
// 另一种获得 delta QP 的机制
if (m_pcEncCfg->getLumaLevelToDeltaQPMapping().isEnabled())
{
if (partitioner.currQgEnable())
{
m_lumaQPOffset = calculateLumaDQP (cs.getOrgBuf (clipArea (cs.area.Y(), cs.picture->Y())));
}
baseQP = Clip3 (-cs.sps->getQpBDOffset (CHANNEL_TYPE_LUMA), MAX_QP, baseQP - m_lumaQPOffset);
}
#endif
}
// -------------------- 以下是在 base QP 附近设定 minQP 和 maxQP 再 RD 测试两者区间内所有取值 -------------
int minQP = baseQP;
int maxQP = baseQP;
xGetMinMaxQP( minQP, maxQP, cs, partitioner, baseQP, *cs.sps, *cs.pps, CU_QUAD_SPLIT );
bool checkIbc = true;
if (partitioner.chType == CHANNEL_TYPE_CHROMA)
{
checkIbc = false;
}
// 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 ) )
{
for( int qp = maxQP; qp >= minQP; qp-- )
{
m_ComprCUCtxList.back().testModes.push_back( { ETM_SPLIT_QT, ETO_STANDARD, qp } );
}
}
if( partitioner.canSplit( CU_TRIV_SPLIT, cs ) )
{
// add split modes
for( int qp = maxQP; qp >= minQP; qp-- )
{
m_ComprCUCtxList.back().testModes.push_back( { ETM_SPLIT_TT_V, ETO_STANDARD, qp } );
}
}
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, ETO_STANDARD, qp } );
}
}
...
...
得到 maxQP 和 minQP 后,将该区间内所有 QP 形成的模式存入 m_ComprCUCtxList.testModes 中。
xCompressCU 函数通过下列代码,提取 m_ComprCUCtxList.testModes 中的待测试模式,并调用 xCheckRDCostIntra 函数,xCheckRDCostIntra 函数内部又调用 estIntraPredLumaQT 函数,进而分别对各个待选模式选择合适的 intra 模式。
do
{
for (int i = compBegin; i < (compBegin + numComp); i++)
{
ComponentID comID = jointPLT ? (ComponentID)compBegin : ((i > 0) ? COMPONENT_Cb : COMPONENT_Y);
tempCS->prevPLT.curPLTSize[comID] = curLastPLTSize[comID];
memcpy(tempCS->prevPLT.curPLT[i], curLastPLT[i], curLastPLTSize[comID] * sizeof(Pel));
}
// 获取m_ComprCUCtxList.testModes
EncTestMode currTestMode = m_modeCtrl->currTestMode();
...
else if( currTestMode.type == ETM_INTRA )
{
if (slice.getSPS()->getUseColorTrans() && !CS::isDualITree(*tempCS))
{
bool skipSecColorSpace = false;
skipSecColorSpace = xCheckRDCostIntra(tempCS, bestCS, partitioner, currTestMode, (m_pcEncCfg->getRGBFormatFlag() ? true : false));
...
}
}
} while( m_modeCtrl->nextMode( *tempCS, partitioner ) );
上一篇:H.266/VVC-VTM代码学习-帧内预测17-initIntraPatternChTypeISP函数初始化ISP的帧内预测
下一篇:H.266/VVC-VTM代码学习19-CU层确定测试模式函数initCULevel