目录
1. SAO原理
1.1 解决问题
SAO技术主要为解决混合编码框架下引入的振铃效应(ringing artifacts)。在混合编码框架中,采用基于块的“DCT+量化”的方法,图像中的边缘等部位在DCT后形成高频分量,高频分量在经过量化、反量化过程后,会丢失一部分高频信息,IDCT后表现为边缘部位的波纹现象,就是振铃效应。如下图所示,直虚线为原始像素值,圆圈为重构像素值,重构像素值在原始像素上下波动。
1.2 实现原理
SAO并没有尝试从频率域降低高频失真,因为对高频信息进行精细化量化,会明显降低压缩率;SAO直接从像素域入手,对重建的像素值进行分类,判断当前像素属于重构曲线中的波峰还是波谷,对波峰添加负的补偿值,对波谷添加正的补偿值,来平滑重构曲线。
目前HEVC中的SAO主要基于MediaTek联发科的JCTVC-E049提案发展而来,在综合考虑补偿性能与计算复杂度的情况下得到采纳。HEVC中的SAO以CTB为单位(亮度色度分开补偿),通过设计好的分类器(下面会详细介绍)将当前像素值分类,不同的类别会采用不同的补偿值,可以明显提升主观质量;同时由于减小了重建失真,客观压缩率也会得到提升。SAO实际包含了两个大类的补偿方式和一种参数融合方式,两大类补偿方式分别为边界补偿(Edge Offset,EO)和边带补偿(Band Offset,BO),参数融合方式也可称为SAO-Merge模式。
1.2.1 边界补偿 EO
EO是通过比较当前像素与相邻像素的大小对当前像素进行分类,同类像素采用相同的补偿值。比较模版为1维3像素。我们借鉴一篇TCSVT的SAO Overview中的EO分类图来做细节了解:
图中c表示当前像素,ab为相邻像素,根据分类时选取的相邻像素位置可以将边界补偿分为4种模式:水平方向(EO_0)、垂直方向(EO_1)、135°方向(EO_2)、45°方向(EO_3)。在任意一种模式下,还可以根据三个重构像素的大小关系,进一步分为上图TABLE I中表示的5个种类,其中前4个种类在像素图中可展现为以下的谷状、凹角、凸角、峰状:
上述将当前像素分为不同模式、不同类别的过程,即为分类器的实现过程,根据这些规则,可以将一个CTB块中的所有像素分成5类,对种类1-2进行正向补偿,对种类3-4进行负向补偿,种类0表示现在的重构曲线很好不需要补偿,因此,EO模式下仅需要传输待补偿的绝对值即可,解码器根据类别自动添加正负号。同一种类的像素必须使用相同的补偿值。
1.2.2 边带补偿 BO
边带补偿技术则是根据当前像素的强度值进行分类,在HEVC中,以编码8比特格式视频为例,边带补偿会将0~255共256个像素值等分成32条边带,每条边带内包含连续的8个像素值。在编码端进行特定边带内像素值的统计与均值计算,并将原始像素均值与重建像素均值之差传输到解码端,在解码端将该差值补偿到对应的边带内,从而达到减小像素失真的目的。由于一个CTU内大部分像素相近,大多分布在几个连续的边带内,因此HEVC规定在一个CTU内仅进行4个边带的计算,来降低一些计算开销。
若同时考虑边界补偿与边带补偿,一个像素可能有21+32=53种类别的补偿方式。
下图可以用来解释为什么 BO 可以在一些情况下起作用。图中水平轴和垂直轴分别表示样本位置和样本值。虚线是原始像素,而实线是重建像素,可能由于预测时的运动矢量偏离真实运动,也可能由量化误差导致了像素偏移,总之结果是重建的样本整体偏移到了原始样本的左侧,此时可以通过 BO 对涉及的4个边带进行不同大小的负向偏移值叠加,使之相对贴近原始曲线。
1.2.3 SAO-Merge模式
SAO-Merge模式是指当前CTB块的SAO参数可以直接使用上方/左侧相邻块的SAO参数,而不需要再传输选中的类别和待补偿值,只需要传输一个bit来标识采用了哪个相邻块的参数即可。
需要注意的是,在使用Merge模式的时候,Luma和Chroma必须使用相同的参考块参数,不允许分离;未采用Merge模式的时候,Luma和Chroma可以采用不同的SAO模式及补偿值,但是Cb和Cr需要采用相同的模式。
2. x265具体实现
自顶向下分析SAO相关函数(下述行列均代表CTU行列)
2.1 块级后处理框架
void FrameEncoder::compressFrame()
…
// filter
if (i >= m_filterRowDelay)
m_frameFilter.processRow(i - m_filterRowDelay);
在compressFrame函数中,编码完每一CTU行之后,代码紧跟了后处理过程,但是后处理有一行的delay,当编码完了row1之后才开始后处理row0。这一行的delay感觉不是很必要,开发人员估计是想要在进行水平滤波的时候能够获得最后一行的下方参考像素吧,但实际上deblock当前块的时候并没有滤波最后一行,水平滤波界限是[top, bottom),所以编码完一行就可以滤波一行。x265源码中采用的是后处理delay编码一行,后处理中V滤波先行,H滤波后行,SAO分类统计最后。表现在代码中就是下图红框的地方:
SAO延迟1行2列进行像素的分类收集,这个延迟数可以通过一条延迟链导出,如图中…HV…SHV所示,V滤波实际无需依赖相邻块状态,只需要当前块编码完就行,所以前行最快,理论上V滤波不需要延迟,不过就像上一段的猜测,还是延迟了1行;x265中H滤波延迟V滤波 1列,是因为V滤波完成的[left,right)这样一个过程,实际不能完成当前块的右边界V滤波,只有在下一个块的V中才会完成当前块的右边界V滤波,而H滤波需在当前块彻底完成V滤波之后才能开始,所以H滤波延迟V滤波一列;SAO分类统计需要处理的是HV均完成的块,所以还需要再延迟1列,也就是相比编码就有1行2列的延迟。
SAO最后的补偿过程在上述延迟的基础上,又进一步延迟列1行1列,也就是说在完成row1 CTU1的SAO分类统计之后,才将历史保存的row0 CTU0补偿值叠加到对应的重建块上。源码注释说明是为了避免线程竞争。
需要注意的是上图中存在一处SAO分类统计前的参考像素保存过程,保存的是上一行的col-1位置块的最下边一行像素值,实际此处不必纠结col-1块和现在正在进行SAO统计的col-2块的位置关系,它只是对所有块下边界像素的一个依次排下去的保存过程,保存到一大块内存后,在经过又一行一列延迟后做最后的补偿时,才把保存的对应位置的参考像素取出来,作为待补偿块的上方参考像素,用于判断补偿在第一像素行上的正负号。之所以要把上一行的最后一像素行保存下来,是因为如下图所示顺序,SAO补偿过程是最滞后的,当对一个块做补偿时,其上方的块早已做完所有后处理过程,也意味着其上方块的最后一行像素也发生了变化,再以此计算出的补偿正负号会有误差;因为一开始做SAO统计的时候,上方块仅完成了deblock,没有SAO补偿,最后一行像素并未因SAO补偿发生变化,这就造成了计算正负号、与计算幅值时使用的像素不同。所以为了避免这种case,会在每次滤波完成之后,有一个上方参考像素的拷贝缓存过程(红线部位)。
2.2 核心函数实现
入口函数
SAO处理的入口函数1为void SAO::rdoSaoUnitCu, 主要完成一个CTU块的像素分类、统计、偏移计算、rdo筛选过程。
/*主要完成一个CTU块的像素分类、统计、偏移计算、rdo筛选过程*/
void SAO::rdoSaoUnitCu(SAOParam* saoParam, int rowBaseAddr, int idxX, int addr)
{
...
// SAO distortion calculation
m_entropyCoder.load(m_rdContexts.cur); //从熵编码器load状态到sao类内,做一个伪熵编码器用于bit估计
m_entropyCoder.resetBits();
if (allowMerge[0])
m_entropyCoder.codeSaoMerge(0);
if (allowMerge[1])
m_entropyCoder.codeSaoMerge(0);
m_entropyCoder.store(m_rdContexts.temp);
memset(m_offset, 0, sizeof(m_offset));
int64_t bestCost = 0;
int64_t rateDist = 0;
bool bAboveLeftAvail = true;
for (int mergeIdx = 0; mergeIdx < 2; ++mergeIdx)
{
if (!allowMerge[mergeIdx])
continue;
SaoCtuParam* mergeSrcParam = &(saoParam->ctuParam[0][addrMerge[mergeIdx]]);
bAboveLeftAvail = bAboveLeftAvail && (mergeSrcParam->typeIdx == -1);
}
// Don't apply sao if ctu is skipped or ajacent ctus are sao off
// 快算,BSlice下,若CU0是skip块或者邻块都没有做SAO,当前块也不做SAO。但实际上在limitSAO=0情况下,bSaoOff失效了。
bool bSaoOff = (slice->m_sliceType == B_SLICE) && (cu->isSkipped(0) || bAboveLeftAvail);
// Estimate distortion and cost of new SAO params
if (saoParam->bSaoFlag[0])
{
if (!m_param->bLimitSAO || !bSaoOff)
{
calcSaoStatsCTU(addr, 0); //luma CTB 的像素分类以及类别内的误差累计m_offsetOrg m_count
saoStatsInitialOffset(addr, 0); //根据累计误差求每个模式每个类别的补偿值 m_offset
saoLumaComponentParamDist(saoParam, addr, rateDist, lambda, bestCost); //rdo 计算每种EO模式的率失真cost
}
}
SaoCtuParam* lclCtuParam = &saoParam->ctuParam[0][addr];
if (saoParam->bSaoFlag[1]) //chroma CTB 的像素分类、误差统计、偏移计算、rdo
{
if (!m_param->bLimitSAO || ((lclCtuParam->typeIdx != -1) && !bSaoOff))
{
calcSaoStatsCTU(addr, 1);
calcSaoStatsCTU(addr, 2);
saoStatsInitialOffset(addr, 1);
saoChromaComponentParamDist(saoParam, addr, rateDist, lambda, bestCost);
}