VVC 中的帧内预测技术包括 Planar 和 DC 模式,以及与 HEVC 相比具有更多角度的更细粒度的角度预测模式,VVC 将原本的 33 种角度模式增加到 65 种。除此之外,VVC 的帧内编码技术中还包含了许多新的编码工具,本文将对 VVC 中的 Planar, DC 和角度预测三种模式结合 VTM-16.0 做深入分析。
1 参考像素构建与滤波
参考像素的填充主要包含两个步骤:
- 分析当前预测块边界,判断当前预测块左上角C、上方D、右上E、左侧B、左下A重建像素是否可用,并统计可用像素的数目
- 使用重建像素填充参考像素
这里填充参考像素时,有以下三种情况:
- 重建像素全部不可用,则参考像素全部填充1<<(bitDepth-1)
- 重建像素全部可用,则直接使用重建像素填充参考像素
- 重建像素部分可用部分不可用时,则先查看最左下角的重建像素是否可用,有以下两个规则
- 如果可用,则从下往上遍历,不可用的重建像素值用其下方最相邻的像素值填充,到达左上角后,从左到右遍历,若有某点的重建像素值不可用,则用其左边最相邻的像素填充;
- 如果不可用,则先从下往上,从左往右遍历一次直到找到第一个可用的重建像素值,将该重建值填充到最左下角的位置,然后将其之前遍历到的不可用的重建像素都使用该重建值填充,接着按照规则1填充。
对于参考样本平滑滤波,使用有限脉冲响应滤波器 {1, 2, 1}/4 对参考样本进行滤波。对参考像素进行平滑滤波的需要同时满足以下条件:
- 帧内预测模式是(−14、−12、−10、−6、0(planar)、2、34、66、72、76、78、80)模式之一
- CU中包含像素数大于32(width*height > 32
- 参考行索引为0,即使用单参考行
- 亮度分量
- 非ISP模式
参考像素的构建和滤波在initIntraPatternChType()函数中。
2 Planar 预测模式
2.1 原理
Planar 预测通过水平和垂直线性插值的平均作为当前像素的预测值,解决帧内预测而在块边界上没有不连续性的问题,适合纹理比较平滑的区域,尤其是变化趋势比较一致的区域。Planar 预测对亮度和色度分量均适用。
标准文档[1]里给出了计算公式:
其中 nTbW 和 nTbH 为块的宽度和高度,p[ x ][ y ]为参考样本 ,其中 x = -1,y = -1..nTbH 或 x = 0..nTbW,y = -1。
上图说明了 Planar 模式下预测样本值的推导过程。右上角的参考样本 p[N][-1] 用作所有水平线性插值的右参考。类似地,左下参考样本 p[-1][N] 被用作所有垂直线性插值的底部参考。通过平均水平和垂直预测来获得每个样本的最终预测值。
2.2 VTM 实现
void IntraPrediction::xPredIntraPlanar( const CPelBuf &pSrc, PelBuf &pDst )
{
const uint32_t width = pDst.width;
const uint32_t height = pDst.height;
const uint32_t log2W = floorLog2( width );
const uint32_t log2H = floorLog2( height );
int leftColumn[MAX_CU_SIZE + 1], topRow[MAX_CU_SIZE + 1], bottomRow[MAX_CU_SIZE], rightColumn[MAX_CU_SIZE];
const uint32_t offset = 1 << (log2W + log2H);
// Get left and above reference column and row
CHECK(width > MAX_CU_SIZE, "width greater than limit");
for( int k = 0; k < width + 1; k++ )
{
topRow[k] = pSrc.at( k + 1, 0 );
}
CHECK(height > MAX_CU_SIZE, "height greater than limit");
for( int k = 0; k < height + 1; k++ )
{
leftColumn[k] = pSrc.at(k + 1, 1);
}
// Prepare intermediate variables used in interpolation
int bottomLeft = leftColumn[height];
int topRight = topRow[width];
for( int k = 0; k < width; k++ )
{
bottomRow[k] = bottomLeft - topRow[k];
topRow[k] = topRow[k] << log2H;
}
for( int k = 0; k < height; k++ )
{
rightColumn[k] = topRight - leftColumn[k];
leftColumn[k] = leftColumn[k] << log2W;
}
const uint32_t finalShift = 1 + log2W + log2H;
const uint32_t stride = pDst.stride;
Pel* pred = pDst.buf;
for( int y = 0; y < height; y++, pred += stride )
{
int horPred = leftColumn[y];
for( int x = 0; x < width; x++ )
{
horPred += rightColumn[y];
topRow[x] += bottomRow[x];
int vertPred = topRow[x];
pred[x] = ( ( horPred << log2H ) + ( vertPred << log2W ) + offset ) >> finalShift;
}
}
}
VTM 的 Planar 实现基本是按照公式来的,在计算 horPred 和 vertPred 的过程中,通过累加代替乘法。如果两个公式合在一起,也可通过一步来计算,但是要注意中间变量的数据类型,避免出现溢出的情况。
3 DC 预测模式
3.1 原理
DC 模式对当前块的所有像素使用同一个预测值,即预测参考像素的平均值。这种模式适用于图像的平坦区域。DC 预测模式对亮度和色度分量均适用。
由于 VVC 中存在矩形块,在计算平均数时会引入不是 2 的幂的除数。为了减小复杂度,VVC 仅使用沿矩形块较长边的参考样本来计算平均值,而对于方形块,则使用来自两侧的参考样本。根据 Filippov[2]这种修改不会导致压缩性能的任何下降。
标准文档里给出了计算公式:
- 当 nTbW 和 nTbH 相等时
- 当 nTbW 大于 nTbH 时
- 当 nTbW 小于 nTbH 时
预测样本值为:
3.2 VTM 实现
Pel IntraPrediction::xGetPredValDc( const CPelBuf &pSrc, const Size &dstSize )
{
CHECK( dstSize.width == 0 || dstSize.height == 0, "Empty area provided" );
int idx, sum = 0;
Pel dcVal;
const int width = dstSize.width;
const int height = dstSize.height;
const auto denom = (width == height) ? (width << 1) : std::max(width,height);
const auto divShift = floorLog2(denom);
const auto divOffset = (denom >> 1);
if ( width >= height )
{
for( idx = 0; idx < width; idx++ )
{
sum += pSrc.at(m_ipaParam.multiRefIndex + 1 + idx, 0);
}
}
if ( width <= height )
{
for( idx = 0; idx < height; idx++ )
{
sum += pSrc.at(m_ipaParam.multiRefIndex + 1 + idx, 1);
}
}
dcVal = (sum + divOffset) >> divShift;
return dcVal;
}
逻辑比较简单,不再赘述。
4 角度预测
VVC 的角度预测从 HEVC 的 33 种角度扩展至65种,由于矩形块的存在,VVC又增加了广角度预测。
4.1 一般角度预测
4.1.1 原理
这里一般角度是指角度 2~66 这些角度,其中把角度2~32成为水平预测角度,把33~66成为垂直预测角度。水平预测和垂直预测过程是一样的,这里重点讨论垂直预测过程。如图4.1所示,在帧内预测模式51~66的情况下,对于每个预测方向,根据比例关系,可以利用下式计算预测点\(P(x,y)\)在上方参考像素中的投影点位置,得到投影点的横坐标相对于\(P(x,y)\)横坐标的位移\(c_x\):
\(c_x/y = d/32\)
其中,\(c_x\)表示待预测点\((x,y)\)的横坐标和点\((x,y)\)沿着预测方向投影到上参考像素行的横坐标之差,也就是 VVC 标准中定义的便宜索引 iIdx;d 表示预测模式方向和垂直方向的偏移距离(格数,其中模式66为32格,每种预测角度的格数,可由表3.1查询)。由上式可以定义偏移索引iIdx和权重因子iFact:
\(iIdx = c_x=(y \cdot d)/32\)
\(iFact=w=(y \cdot d)\&31\)
其中\(iIdx\)用来确定参考像素的位置,\(iFact\)用来确定滤波参数,
VVC 中的帧内预测有两种应用于参考样本的滤波机制,即参考样本平滑和插值滤波。参考样本平滑仅应用于亮度块中的整数斜率(\(iFact=0\))模式,而插值滤波应用于分数斜率模式。
对于插值滤波,如果给定预测方向的样本投影落在参考样本之间的分数位置上,则通过对分数样本位置周围的参考样本应用插值滤波器来获得预测样本值。对于亮度块,使用 4-tap 插值滤波器,预测样本 pred(x, y) 为:
其中\(i_0=iIdx+x\); \(p=iFact\)。
VVC 中包含两个插值滤波器,分别为基于 DCT 的插值滤波器 (DCTIF) 或 4 抽头平滑插值滤波器 (SIF)。插值滤波器的类型不写入比特流中,而是基于块的大小和帧内预测模式索引\(m\)来确定。 如果 \(min(|m-50|, |m-18|) > T\) ,则使用 SIF,否则,使用 DCTIF。 这里,\(T\) 是一个取决于块大小的阈值。对于具体系数值可以参考标准文档 Table 25。
对于色度分量,在 VVC 中使用 HEVC 的线性 2 抽头插值滤波器。
4.1.2 VTM 实现
void IntraPrediction::xPredIntraAng( const CPelBuf &pSrc, PelBuf &pDst, const ChannelType channelType, const ClpRng& clpRng)
{
int width =int(pDst.width);
int height=int(pDst.height);
const bool bIsModeVer = m_ipaParam.isModeVer; // m_ipaParam.isModeVer = predMode >= DIA_IDX;
const int multiRefIdx = m_ipaParam.multiRefIndex;
const int intraPredAngle = m_ipaParam.intraPredAngle; // tan()值
const int absInvAngle = m_ipaParam.absInvAngle; // itan()值
Pel* refMain;
Pel* refSide;
Pel refAbove[2 * MAX_CU_SIZE + 3 + 33 * MAX_REF_LINE_IDX];
Pel refLeft [2 * MAX_CU_SIZE + 3 + 33 * MAX_REF_LINE_IDX];
// Initialize the Main and Left reference array.
if (intraPredAngle < 0) // 角度19-49,需要两个方向的参考像素
{
for (int x = 0; x <= width + 1 + multiRefIdx; x++)
{
refAbove[x + height] = pSrc.at(x, 0);
}
for (int y = 0; y <= height + 1 + multiRefIdx; y++)
{
refLeft[y + width] = pSrc.at(y, 1);
}
refMain = bIsModeVer ? refAbove + height : refLeft + width;
refSide = bIsModeVer ? refLeft + width : refAbove + height;
// Extend the Main reference to the left.
int sizeSide = bIsModeVer ? height : width;
for (int k = -sizeSide; k <= -1; k++)
{
refMain[k] = refSide[std::min((-k * absInvAngle + 256) >> 9, sizeSide)];
// 将上方和左方的参考像素合并成一个一维的像素集
}
}
else // 角度-14-18, 50-80,只需要一个方向的参考像素
{
for (int x = 0; x <= m_topRefLength + multiRefIdx; x++)
{
refAbove[x] = pSrc.at(x, 0);
}
for (int y = 0; y <= m_leftRefLength + multiRefIdx; y++)
{
refLeft[y] = pSrc.at(y, 1);
}
refMain = bIsModeVer ? refAbove : refLeft;
refSide = bIsModeVer ? refLeft : refAbove;
// Extend main reference to right using replication
const int log2Ratio = floorLog2(width) - floorLog2(height);
const int s = std::max<int>(0, bIsModeVer ? log2Ratio : -log2Ratio);
const int maxIndex = (multiRefIdx << s) + 2;
const int refLength = bIsModeVer ? m_topRefLength : m_leftRefLength;
const Pel val = refMain[refLength + multiRefIdx];
for (int z = 1; z <= maxIndex; z++)
{
refMain[refLength + multiRefIdx + z] = val; // 使用最邻近像素填充参考像素
}
}
// swap width/height if we are doing a horizontal mode:
if (!bIsModeVer)
{
std::swap(width, height);
}
Pel tempArray[MAX_CU_SIZE * MAX_CU_SIZE];
const int dstStride = bIsModeVer ? pDst.stride : width;
Pel * pDstBuf = bIsModeVer ? pDst.buf : tempArray;
// compensate for line offset in reference line buffers
refMain += multiRefIdx;
refSide += multiRefIdx;
Pel *pDsty = pDstBuf;
if( intraPredAngle == 0 ) // pure vertical or pure horizontal
{
for( int y = 0; y < height; y++ )
{
for( int x = 0; x < width; x++ )
{
pDsty[x] = refMain[x + 1];
}
pDsty += dstStride;
}
}
else
{
for (int y = 0, deltaPos = intraPredAngle * (1 + multiRefIdx); y<height; y++, deltaPos += intraPredAngle, pDsty += dstStride)
{
const int deltaInt = deltaPos >> 5; // 确定参考像素位置
const int deltaFract = deltaPos & 31; // 确定滤波器参数位置
if ( !isIntegerSlope( abs(intraPredAngle) ) )
{
if( isLuma(channelType) )
{
const bool useCubicFilter = !m_ipaParam.interpolationFlag; // 选择滤波器种类
const TFilterCoeff intraSmoothingFilter[4] = {TFilterCoeff(16 - (deltaFract >> 1)), TFilterCoeff(32 - (deltaFract >> 1)), TFilterCoeff(16 + (deltaFract >> 1)), TFilterCoeff(deltaFract >> 1)};
const TFilterCoeff* const f = (useCubicFilter) ? InterpolationFilter::getChromaFilterTable(deltaFract) : intraSmoothingFilter;
for (int x = 0; x < width; x++)
{
Pel p[4];
p[0] = refMain[deltaInt + x];
p[1] = refMain[deltaInt + x + 1];
p[2] = refMain[deltaInt + x + 2];
p[3] = refMain[deltaInt + x + 3];
Pel val = (f[0] * p[0] + f[1] * p[1] + f[2] * p[2] + f[3] * p[3] + 32) >> 6;
pDsty[x] = ClipPel(val, clpRng); // always clip even though not always needed
}
}
else
{
// Do linear filtering
for (int x = 0; x < width; x++)
{
Pel p[2];
p[0] = refMain[deltaInt + x + 1];
p[1] = refMain[deltaInt + x + 2];
pDsty[x] = p[0] + ((deltaFract * (p[1] - p[0]) + 16) >> 5);
}
}
}
else
{
// Just copy the integer samples
for( int x = 0; x < width; x++ ) // 该模式下参考像素在xFilterReferenceSamples()函数中已经做过平滑滤波
{
pDsty[x] = refMain[x + deltaInt + 1];
}
}
}
}
// Flip the block if this is the horizontal mode
if( !bIsModeVer )
{
for( int y = 0; y < height; y++ )
{
for( int x = 0; x < width; x++ )
{
pDst.at( y, x ) = pDstBuf[x];
}
pDstBuf += dstStride;
}
}
}
VTM 代码基本按照上面的原理分析来的,其中一些重要的过程加入了注释,删去了其中的PDPC部分。VTM 中将水平和垂直预测做统一处理,对角度18和50这两个方向做了单独处理。
4.2 广角度预测
4.2.1 原理
传统的角度帧内预测方向定义为顺时针方向从 45 度到 -135 度。在 VVC 中,一些传统的角度帧内预测模式被自适应地替换为非方形块的广角帧内预测模式。替换的模式使用原始模式索引传输,在解析后重新映射到宽角模式的索引。帧内预测模式总数不变,即67,帧内模式编码方法不变。为了支持这些预测方向,定义了长度为 2W+1 的顶部参考样本和长度为 2H+1 的左侧参考样本,如图 所示。
广角方向模式中替换模式的数量取决于块的纵横比。替换后的帧内预测模式如表所示
如下图所示,在广角帧内预测的情况下,两个垂直相邻的预测样本可以使用两个不相邻的参考样本。因此,低通参考样本滤波和侧平滑应用于广角预测,以减少增加的间隙 \(Δp_α\)的负面影响。广角模式中有8种模式满足这个条件,分别是[-14, -12, -10, -6, 72, 76, 78, 80]。当通过这些模式预测块时,直接复制参考缓冲区中的样本而不应用任何插值。通过这种修改,减少了需要平滑的样本数量。此外,它对齐了传统预测模式和广角模式中的非分数模式的设计。
4.2.2 VTM 实现
首先在initPredIntraParams函数中调用了getModifiedWideAngle这个函数,这个函数根据块尺寸和预测角度将一般角度扩展为广角度。预测代码和一般角度预测代码一致。
int IntraPrediction::getModifiedWideAngle( int width, int height, int predMode )
{
//The function returns a 'modified' wide angle index, given that it is not necessary
//in this software implementation to reserve the values 0 and 1 for Planar and DC to generate the prediction signal.
//It should only be used to obtain the intraPredAngle parameter.
//To simply obtain the wide angle index, the function PU::getWideAngle should be used instead.
if ( predMode > DC_IDX && predMode <= VDIA_IDX )
{
int modeShift[] = { 0, 6, 10, 12, 14, 15 };
int deltaSize = abs(floorLog2(width) - floorLog2(height));
if (width > height && predMode < 2 + modeShift[deltaSize])
{
predMode += (VDIA_IDX - 1);
}
else if (height > width && predMode > VDIA_IDX - modeShift[deltaSize])
{
predMode -= (VDIA_IDX - 1);
}
}
return predMode;
}
参考资料
- Versatile Video Coding Editorial Refinements on Draft 10: Oct.2020[2]
- A. Filippov, V. Rufitskiy, and J. Chen, CE3-related: Alternative techniques for DC mode without division, document JVET-K0122 of JVET: Jul.2018.