原文出处:
今天所要讨论的是fillReferenceSamples这个函数,它主要功能是在真正进行帧内预测之前,使用重建后的Yuv图像对当前PU的相邻样点进行赋值,为接下来进行的角度预测提供参考样点值。
这个函数实际上实现的是官方当前标准(JCTVC-J1003)draft 8.4.4.2.2(Reference sample substitution process for intra sample prediction),具体内容我这里就不重复了,有兴趣的朋友可以自己下下来去看看,我先简单把该过程复述一遍:(1)如果所有相邻点均不可用,则参考样点值均被赋值为DC值;(2)如果所有相邻点均可用,则参考样点值都会被赋值为重建Yuv图像中与其位置相同的样点值;(3)如果不满足上述两个条件,则按照从左下往左上,从左上往右上的扫描顺序进行遍历,(如下图所示),如果第一个点不可用,则使用下一个可用点对应的重建Yuv样点值对其进行赋值;对于除第一个点外的其它邻点,如果该点不可用,则使用它的前一个样点值进行赋值(前一个步骤保证了前一个样点值一定是存在的),直到遍历完毕。
以下为HM 16.0的代码解释,不能全部看懂,按照自己理解对应原博客进行了解释。
Void fillReferenceSamples( const Int bitDepth, // 位深度
#if O0043_BEST_EFFORT_DECODING
const Int bitDepthDelta,
#endif
const Pel* piRoiOrigin, //见下面
Pel* piIntraTemp,//指向当前图像的参考样点
const Bool* bNeighborFlags,
const Int iNumIntraNeighbor,
const Int unitWidth,
const Int unitHeight,
const Int iAboveUnits,
const Int iLeftUnits,
const UInt uiWidth,
const UInt uiHeight,
const Int iPicStride )
{
const Pel* piRoiTemp;!< piRoiOrgin指向重建Yuv图像对应于当前PU所在位置的首地址,piRoiTemp用于指向所感兴趣的重建Yuv的位置,是局部变量
Int i, j;
Int iDCValue = 1 << (bitDepth - 1);//左移云算符,左移n位相当于乘以2的n次方,参考书本
const Int iTotalUnits = iAboveUnits + iLeftUnits + 1; //+1 for top-left左上角 该变量代表总的参考单元
if (iNumIntraNeighbor == 0)// all samples are not available
{
// Fill border with DC value
for (i=0; i<uiWidth; i++) //!< AboveLeft + Above + AboveRight ,数组从0开始
{
piIntraTemp[i] = iDCValue;
}
for (i=1; i<uiHeight; i++) //!< Left + BelowLeft
{
piIntraTemp[i*uiWidth] = iDCValue;
}
}
else if (iNumIntraNeighbor == iTotalUnits) // all samples are available
{
// Fill top-left border and top and top right with rec. samples
piRoiTemp = piRoiOrigin - iPicStride - 1;//对局部变量赋值,指针指向重建YUV图像对应位置左上的参考点位置
for (i=0; i<uiWidth; i++)
{
#if O0043_BEST_EFFORT_DECODING
piIntraTemp[i] = piRoiTemp[i] << bitDepthDelta;
#else
piIntraTemp[i] = piRoiTemp[i];//对左上,上,右上赋予重建YUV图像对应位置的样点值
#endif
}
// Fill left and below left border with rec. samples
piRoiTemp = piRoiOrigin - 1;
for (i=1; i<uiHeight; i++)
{
#if O0043_BEST_EFFORT_DECODING
piIntraTemp[i*uiWidth] = (*(piRoiTemp)) << bitDepthDelta;
#else
piIntraTemp[i*uiWidth] = *(piRoiTemp);
#endif
piRoiTemp += iPicStride;//ipicstride代表类似图片的跨度,指向重建图像的下一行
}
}
else // reference samples are partially available
{
// all above units have "unitWidth" samples each, all left/below-left units have "unitHeight" samples each
const Int iTotalSamples = (iLeftUnits * unitHeight) + ((iAboveUnits + 1) * unitWidth);//计算总的样点数,unitWidth unitHeight代表每个块的大小,即包含的样点个数;+1表示左上的参考unit
Pel piIntraLine[5 * MAX_CU_SIZE];
Pel *piIntraLineTemp; //!<临时存储用于填充neighboring samples的样点值
const Bool *pbNeighborFlags;//该参考邻点是否可用
// Initialize
for (i=0; i<iTotalSamples; i++)
{
piIntraLine[i] = iDCValue; //!< 先将所有样点值赋值为DC值
}
// Fill top-left sample
piRoiTemp = piRoiOrigin - iPicStride - 1;//!< 指向重建Yuv左上角
piIntraLineTemp = piIntraLine + (iLeftUnits * unitHeight);//!< piAdiLine的扫描顺序为左下到左上,再从左到右上
pbNeighborFlags = bNeighborFlags + iLeftUnits;// 标记neighbor可用性的数组同样移动至左上角
if (*pbNeighborFlags)
{
#if O0043_BEST_EFFORT_DECODING
Pel topLeftVal=piRoiTemp[0] << bitDepthDelta;
#else
Pel topLeftVal=piRoiTemp[0];
#endif
for (i=0; i<unitWidth; i++)
{
piIntraLineTemp[i] = topLeftVal;//!< 如果左上角可用,则左上角4个像素点均赋值为重建Yuv左上角的样点值
}
}
// Fill left & below-left samples (downwards)
piRoiTemp += iPicStride;
piIntraLineTemp--;//!< 移动指针置左边界
pbNeighborFlags--;//!< 移动指针置左边界
for (j=0; j<iLeftUnits; j++)//向下遍历
{
if (*pbNeighborFlags)
{
for (i=0; i<unitHeight; i++)//!< 每个4x4块里的4个样点分别被赋值为对应位置的重建Yuv的样点值
{
#if O0043_BEST_EFFORT_DECODING
piIntraLineTemp[-i] = piRoiTemp[i*iPicStride] << bitDepthDelta;
#else
piIntraLineTemp[-i] = piRoiTemp[i*iPicStride];
#endif
}
}
piRoiTemp += unitHeight*iPicStride;//!< 指针挪到下一个行(以4x4块为单位,即实际上下移了4行)
piIntraLineTemp -= unitHeight;//!< 指针下移
pbNeighborFlags--;//!< 指针下移
}
// Fill above & above-right samples (left-to-right) (each unit has "unitWidth" samples)
piRoiTemp = piRoiOrigin - iPicStride;//!< piRoiTemp 指向重建Yuv的上边界
// offset line buffer by iNumUints2*unitHeight (for left/below-left) + unitWidth (for above-left)
piIntraLineTemp = piIntraLine + (iLeftUnits * unitHeight) + unitWidth;//指向上边界
pbNeighborFlags = bNeighborFlags + iLeftUnits + 1;//指向上边界
for (j=0; j<iAboveUnits; j++)//< 从左扫描至右上
{
if (*pbNeighborFlags)
{
for (i=0; i<unitWidth; i++)
{
#if O0043_BEST_EFFORT_DECODING
piIntraLineTemp[i] = piRoiTemp[i] << bitDepthDelta;
#else
piIntraLineTemp[i] = piRoiTemp[i];//pel类型为一个二级指针,每个指针所指类型为一个数组
#endif
}
}
piRoiTemp += unitWidth;//指针右移
piIntraLineTemp += unitWidth;
pbNeighborFlags++;
}
// Pad reference samples when necessary
Int iCurrJnit = 0;
Pel *piIntraLineCur = piIntraLine;//!< 指向左下角(纵坐标最大的那个位置,即扫描起点
const UInt piIntraLineTopRowOffset = iLeftUnits * (unitHeight - unitWidth);//补偿作用
if (!bNeighborFlags[0])//第一个相邻点不可用
{
// very bottom unit of bottom-left; at least one unit will be valid.
{
Int iNext = 1;
while (iNext < iTotalUnits && !bNeighborFlags[iNext]) //!< 找到第一个可用点
{
iNext++;
}
Pel *piIntraLineNext = piIntraLine + ((iNext < iLeftUnits) ? (iNext * unitHeight) : (piIntraLineTopRowOffset + (iNext * unitWidth)));//假设超过了左边的数量
const Pel refSample = *piIntraLineNext;//!< 保存该可用点的样点值
// Pad unavailable samples with new value
Int iNextOrTop = std::min<Int>(iNext, iLeftUnits);//假设用,选出inext和LeftUnits中的较小值
//!< 使用保存下来的第一个可用点的样点值赋值给在其之前被标记为不可用的点
// fill left column
while (iCurrJnit < iNextOrTop)//填充不可用点的左边列
{
for (i=0; i<unitHeight; i++)
{
piIntraLineCur[i] = refSample;
}
piIntraLineCur += unitHeight;
iCurrJnit++;
}
// fill top row
while (iCurrJnit < iNext)//填充不可用点的上边行
{
for (i=0; i<unitWidth; i++)
{
piIntraLineCur[i] = refSample;
}
piIntraLineCur += unitWidth;
iCurrJnit++;
}
}
}
// pad all other reference samples.
while (iCurrJnit < iTotalUnits) //!< 遍历所有neighboring samples
{
if (!bNeighborFlags[iCurrJnit]) // samples not available
{
{
const Int numSamplesInCurrUnit = (iCurrJnit >= iLeftUnits) ? unitWidth : unitHeight;
const Pel refSample = *(piIntraLineCur-1);//!< 当前点不可用且其不是第一个点,则使用该点的前一个可用点的样点值进行赋值
for (i=0; i<numSamplesInCurrUnit; i++)
{
piIntraLineCur[i] = refSample;
}
piIntraLineCur += numSamplesInCurrUnit;
iCurrJnit++;
}
}
else//!< 当前点可用,继续检查下一点
{
piIntraLineCur += (iCurrJnit >= iLeftUnits) ? unitWidth : unitHeight;
iCurrJnit++;
}
}
// Copy processed samples
piIntraLineTemp = piIntraLine + uiHeight + unitWidth - 2;
// top left, top and top right samples
for (i=0; i<uiWidth; i++)//!< 将最终结果拷贝到左上、上、右上边界
{
piIntraTemp[i] = piIntraLineTemp[i];
}
piIntraLineTemp = piIntraLine + uiHeight - 1;
for (i=1; i<uiHeight; i++) //!< 将最终结果拷贝到左和左下边界
{
piIntraTemp[i*uiWidth] = piIntraLineTemp[-i];//!< piAdiLineTemp下标为-i是因为赋值方向与实际存储方向是相反的,存储顺序为遍历顺序从左下到左上,而实际赋值方向是从左上到左下。
}
}
}
为了更好地理解帧内预测中的各个函数的原理过程,有必要对CU、PU地址计算方法有着较好的了解,因此,本文将对这个问题先做个讨论。
对视频编解码有一定了解的人应该会知道,有一种扫描顺序叫光栅扫描,即从左往右,由上往下,先扫描完一行,再移至下一行起始位置继续扫描。H.264使用的主要就是光栅扫描顺序,(当然它还有其它扫描顺序,被包含在FMO即灵活宏块顺序技术里)。
HEVC里同样也有光栅扫描顺序,但是,由于它对CU采用的是递归划分的方式,如果仍是采用光栅扫描顺序,对CU的寻址会很不方便,因此,HEVC定义了Z扫描顺序,如下图所示:
右图可见,这种扫描顺序保证了对于不同分割都能按照相同的遍历顺序进行寻址,有利于程序中的递归实现。
具体到代码中,为了处理的方便,并没有使用上图这种定义方式,而是以4x4块为最小单位,对CU进行分割,同时,为了简化计算,在初始化时定义了几个地址映射的数组,g_auiRasterToZscan, g_auiZscanToRaster, g_auiRasterToX, g_auiRasterToY。
第一个是从光栅扫描顺序转换为Z扫描顺序,第二个是从Z扫描顺序转换为光栅扫描顺序,第三、第四个则是得到某一个块相对于所在PU左上角的横纵坐标,且以像素为单位。
下面几张表是我在最大CU为64x64的前提下打印出来的:
上面这些值的含义,这里就不多解释了,相信大家自己画个CU,以4x4块对其进行分割,再对照上面几张表琢磨琢磨就清楚了。
值得一提的是,可能会有人有这样的疑问:图像的分辨率不同,它怎么能够保证这张表就能用呢?需要指出的是,Z扫描是针对一个CU来说的,它是用于递归扫描CU的分割。定位一幅图像中的一个CU(或其分割)大致是这么个过程,首先,由于CU的尺寸的最大值是已知的,会根据这个定位到该CU左上角相对于图像左上角的位置,即得到它的坐标,接着,才是对当前块进行Z扫描,单位是4x4块,换句话说,Z扫描地址是对一个CU有效的,不能直接使用这个地址来确定它在图像中的位置,这个地方是需要大家注意的。