类似于以往的视频编码标准,HEVC仍采用基于块的混合编码框架,一些失真效应仍然存在,如方块效应、振铃效应、颜色偏差以及图像模糊等等。为了解决这些问题,HEVC中采用了环路滤波技术,它其实是一种用于解码端的后处理滤波技术,主要包括去块滤波(Deblocking Filter,DBF)和样点自适应补偿(Sample Adaptive Offset,SAO)。其中,DBF的作用与H.264类似,主要是去除块效应,但是相比于H.264,其决策与滤波过程大大地被简化了,而SAO是HEVC中的新技术。
此处有一点需要注意的是,帧内预测采用的是解码宏块像素作为下个帧内预测的参考,而帧间预测则是采用经环路滤波后的解码宏块像素作为运动预测参考图像。这一点可以由环路滤波这个模块所处在编码框架的位置加以验证(如下图红色圈圈内)。当然这样做(经过环路滤波的重构像素才能作为后续编码像素的参考使用)是有原因的,即环路滤波处理后的重建像素更有利于参考,进一步减小后续编码像素的预测残差,有效地提高了视频的主客观质量。
下面对环路滤波中的去块滤波技术和样点自适应补偿技术做重点解析。
一、去块滤波技术
去块滤波(Deblocking Filter,DBF)用于降低方块效应(所谓方块效应就是图像中编码块边界的不连续性),造成方块效应的主要原因有三个:
①、各个块的变换、量化编码过程相互独立(相当于对各个块使用了不同参数的滤波器分别滤波,因此各块引入的量化误差大小及其分布特性相互独立,导致相邻块边界的不连续);
②、运动补偿预测过程中,相邻块的预测值可能来自于不同图像的不同位置,导致预测残差信号在块边界产生数值的不连续;
③、时域预测技术使得参考图像中存在的边界不连续可能会传递到后续编码图像。
正是由于块效应的产生原因才使得DBF只应用于块边界上的样本,即被用于所有与PU或TU边界相邻的样本,该选项可以在编码器中进行设置(设置的位置在编码结构配置文件中,如encoder_lowdelay_P_main.cfg文件的“Deblock Filter”部分,如下所示),需要注意的是,需要同时考虑PU和TU的边界,因为在某些帧间预测CB中,PU边界不一定总能和TU边界对齐。
#=========== Deblock Filter ============
DeblockingFilterControlPresent: 0 # Dbl control params present (0=not present, 1=present)
LoopFilterOffsetInPPS : 0 # Dbl params: 0=varying params in SliceHeader, param = base_param + GOP_offset_param; 1=constant params in PPS, param = base_param)
LoopFilterDisable : 0 # Disable deblocking filter (0=Filter, 1=No Filter)
LoopFilterBetaOffset_div2 : 0 # base_param: -6 ~ 6
LoopFilterTcOffset_div2 : 0 # base_param: -6 ~ 6
DeblockingFilterMetric : 0 # blockiness metric (automatically configures deblocking parameters in bitstream)
有没有使能DBF,得到的效果图如下图所示(此处需要插一句话,经本人在HM平台上测试,发现DBF的效果并不是很明显,貌似几乎没什么改变,这一点的具体原因是去块滤波器的强度受限于很多因素,并不是每次试验都能成功得到与理论结论完全契合的结果)。
在H.264中,DBF应用于4x4大小块,而在HEVC中,无论亮度还是色度样本均只应用于8x8大小块。这一限定可以在不影响视觉质量的情况下,降低计算复杂度,同时通过防止相邻滤波操作之间的交互,便于并行处理的实现。
在HEVC中,DBF的处理顺序是:首先对整个图像的垂直边缘进行水平滤波,然后对水平边缘进行垂直滤波。该顺序使得多次水平滤波或者垂直滤波过程可以通过并行处理实现,或者仍可以以逐CTB的方式执行,这时会引入很小的处理延迟。
总结一句,对块边界进行平滑滤波可以有效地降低、去除方块效应。
二、样点自适应补偿技术
SAO是HEVC中的新技术,所以是我们重点学习的对象。
样点自适应补偿(Sample Adaptive Offset,SAO)用于改善振铃效应,SAO被自适应地用于所有满足特定条件的样本上。
造成振铃效应的原因是:高频信息的丢失(HEVC仍采用基于块的DCT变换,并在频域对变换系数进行量化,对于图像里的强边缘,由于高频交流系数的量化失真,解码后会在边缘周围产生波纹现象,即吉布斯现象,如下图所示,这种失真就是振铃效应,振铃效应会严重影响视频的主客观质量)。
正是由于高频信息的丢失才导致的振铃效应,因此要抑制振铃效应,就必须减小高频分量的失真,而直接精细量化高频分量势必导致压缩效率的降低。
SAO的解决方法如下(基本原理):从像素域入手降低振铃效应,对重构曲线中出现的波峰像素添加负值进行补偿,波谷添加正值进行补偿,由于在解码端只能得到重构图像信息,因此可以根据重构图像的特征点,通过将其划分类别,然后在像素域进行补偿处理。
在HEVC中,SAO以CTB为基本单位,通过选择一个合适的分类器将重建像素划分类别,然后对不同类别像素使用不同的补偿值,可以有效提高视频的主客观质量。它包括两大类补偿形式,分别是边界补偿(Edge Offset,EO)和边带补偿(Bang Offset,BO),此外还引入了参数融合技术。
(1)、边界补偿(Edge Offset,EO)
通过比较当前像素值与相邻像素值的大小,对当前像素进行分类,然后对同类像素补偿相同数值。为了均衡复杂度与编码效率,边界补偿选用了一维三像素分类模式,根据选取像素位置的差异,分为4种模式,即水平方向(EO_0)、垂直方向(EO_1)、135度方向(EO_2)和45度方向(EO_3)。在任意一种模式下,EO根据一个规则将所有的像素分成5类,然后对种类1至种类4进行补偿,即增加或减少一定数值(补偿值),而对于种类0的像素不进行补偿。并且还要遵循一个原则:不同种类的像素值可以采用不同的补偿值,但同一种类的像素必须采用相同的补偿。
对于边界补偿来讲,只需要传递补偿值的绝对值即可,解码器会根据像素补偿种类即可判断它的符号(原因是实验结果表明超过90%的补偿值,其符号与种类相匹配,因此按照不同种类对补偿值的符号进行了限制)。
(2)、边带补偿(Bang Offset,BO)
BO根据像素强度进行归类,它将像素范围等分成32条边带。然后每个条带根据自身像素特点进行补偿,且同一个边带使用相同的补偿值。HEVC中规定了一个CTB只能选择4条连续的边带,并只对属于这4个边带的像素进行补偿,这样边带补偿值数量与边界补偿值数量进行了统一,可以减少对线性存储器的要求,最终选择哪4条边带可以通过率失真优化方法来确定,然后将最小边带号以及4个补偿值传至解码端即可。
(3)、SAO参数融合
参数融合(Merge)是指对一个CTB块,其SAO参数直接使用相邻块的SAO参数,这时只需要标识采用了哪个相邻的SAO参数即可。
(4)、SAO在HM中的实现过程
SAO过程的重点是利用拉格朗日优化选择最优的SAO参数,为了降低计算复杂度,该过程采用了快速模式判别方法。一个CTU的SAO过程如下图所示:
SAO技术对应于HM中的代码如下:
TComSampleAdaptiveOffset.cpp
/* The copyright in this software is being made available under the BSD
* License, included below. This software may be subject to other third party
* and contributor rights, including patent rights, and no such rights are
* granted under this license.
*
* Copyright (c) 2010-2014, ITU/ISO/IEC
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of the ITU/ISO/IEC nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
/** \file TComSampleAdaptiveOffset.cpp
\brief sample adaptive offset class
*/
#include "TComSampleAdaptiveOffset.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
//! \ingroup TLibCommon
//! \{
UInt g_saoMaxOffsetQVal[NUM_SAO_COMPONENTS];
SAOOffset::SAOOffset()
{
reset();
}
SAOOffset::~SAOOffset()
{
}
Void SAOOffset::reset()
{
modeIdc = SAO_MODE_OFF;
typeIdc = -1;
typeAuxInfo = -1;
::memset(offset, 0, sizeof(Int)* MAX_NUM_SAO_CLASSES);
}
const SAOOffset& SAOOffset::operator= (const SAOOffset& src)
{
modeIdc = src.modeIdc;
typeIdc = src.typeIdc;
typeAuxInfo = src.typeAuxInfo;
::memcpy(offset, src.offset, sizeof(Int)* MAX_NUM_SAO_CLASSES);
return *this;
}
SAOBlkParam::SAOBlkParam()
{
reset();
}
SAOBlkParam::~SAOBlkParam()
{
}
Void SAOBlkParam::reset()
{
for(Int compIdx=0; compIdx< 3; compIdx++)
{
offsetParam[compIdx].reset();
}
}
const SAOBlkParam& SAOBlkParam::operator= (const SAOBlkParam& src)
{
for(Int compIdx=0; compIdx< 3; compIdx++)
{
offsetParam[compIdx] = src.offsetParam[compIdx];
}
return *this;
}
TComSampleAdaptiveOffset::TComSampleAdaptiveOffset()
{
m_tempPicYuv = NULL;
for(Int compIdx=0; compIdx < NUM_SAO_COMPONENTS; compIdx++)
{
m_offsetClipTable[compIdx] = NULL;
}
#if !SAO_SGN_FUNC
m_signTable = NULL;
#endif
m_lineBufWidth = 0;
m_signLineBuf1 = NULL;
m_signLineBuf2 = NULL;
}
TComSampleAdaptiveOffset::~TComSampleAdaptiveOffset()
{
destroy();
if (m_signLineBuf1) delete[] m_signLineBuf1; m_signLineBuf1 = NULL;
if (m_signLineBuf2) delete[] m_signLineBuf2; m_signLineBuf2 = NULL;
}
Void TComSampleAdaptiveOffset::create( Int picWidth, Int picHeight, UInt maxCUWidth, UInt maxCUHeight, UInt maxCUDepth )
{
destroy();
m_picWidth = picWidth;
m_picHeight= picHeight;
m_maxCUWidth= maxCUWidth;
m_maxCUHeight= maxCUHeight;
m_numCTUInWidth = (m_picWidth/m_maxCUWidth) + ((m_picWidth % m_maxCUWidth)?1:0);
m_numCTUInHeight= (m_picHeight/m_maxCUHeight) + ((m_picHeight % m_maxCUHeight)?1:0);
m_numCTUsPic = m_numCTUInHeight*m_numCTUInWidth;
//temporary picture buffer
if ( !m_tempPicYuv )
{
m_tempPicYuv = new TComPicYuv;
m_tempPicYuv->create( m_picWidth, m_picHeight, m_maxCUWidth, m_maxCUHeight, maxCUDepth );
}
//bit-depth related
for(Int compIdx =0; compIdx < NUM_SAO_COMPONENTS; compIdx++)
{
Int bitDepthSample = (compIdx == SAO_Y)?g_bitDepthY:g_bitDepthC;
m_offsetStepLog2 [compIdx] = max(bitDepthSample - MAX_SAO_TRUNCATED_BITDEPTH, 0);
g_saoMaxOffsetQVal[compIdx] = (1<<(min(bitDepthSample,MAX_SAO_TRUNCATED_BITDEPTH)-5))-1; //Table 9-32, inclusive
}
#if !SAO_SGN_FUNC
//look-up table for clipping
Int overallMaxSampleValue=0;
#endif
for(Int compIdx =0; compIdx < NUM_SAO_COMPONENTS; compIdx++)
{
Int bitDepthSample = (compIdx == SAO_Y)?g_bitDepthY:g_bitDepthC; //exclusive
Int maxSampleValue = (1<< bitDepthSample); //exclusive
Int maxOffsetValue = (g_saoMaxOffsetQVal[compIdx] << m_offsetStepLog2[compIdx]);
#if !SAO_SGN_FUNC
if (maxSampleValue>overallMaxSampleValue) overallMaxSampleValue=maxSampleValue;
#endif
m_offsetClipTable[compIdx] = new Int[(maxSampleValue + maxOffsetValue -1)+ (maxOffsetValue)+1 ]; //positive & negative range plus 0
m_offsetClip[compIdx] = &(m_offsetClipTable[compIdx][maxOffsetValue]);
//assign clipped values
Int* offsetClipPtr = m_offsetClip[compIdx];
for(Int k=0; k< maxSampleValue; k++)
{
*(offsetClipPtr + k) = k;
}
for(Int k=0; k< maxOffsetValue; k++ )
{
*(offsetClipPtr + maxSampleValue+ k) = maxSampleValue-1;
*(offsetClipPtr -k -1 ) = 0;
}
}
#if !SAO_SGN_FUNC
m_signTable = new Short[ 2*(overallMaxSampleValue-1) + 1 ];
m_sign = &(m_signTable[overallMaxSampleValue-1]);
m_sign[0] = 0;
for(Int k=1; k< overallMaxSampleValue; k++)
{
m_sign[k] = 1;
m_sign[-k]= -1;
}
#endif
}
Void TComSampleAdaptiveOffset::destroy()
{
if ( m_tempPicYuv )
{
m_tempPicYuv->destroy();
delete m_tempPicYuv;
m_tempPicYuv = NULL;
}
for(Int compIdx=0; compIdx < NUM_SAO_COMPONENTS; compIdx++)
{
if(m_offsetClipTable[compIdx])
{
delete[] m_offsetClipTable[compIdx]; m_offsetClipTable[compIdx] = NULL;
}
}
#if !SAO_SGN_FUNC
if( m_signTable )
{
delete[] m_signTable; m_signTable = NULL;
}
#endif
}
Void TComSampleAdaptiveOffset::invertQuantOffsets(Int compIdx, Int typeIdc, Int typeAuxInfo, Int* dstOffsets, Int* srcOffsets)
{
Int codedOffset[MAX_NUM_SAO_CLASSES];
::memcpy(codedOffset, srcOffsets, sizeof(Int)*MAX_NUM_SAO_CLASSES);
::memset(dstOffsets, 0, sizeof(Int)*MAX_NUM_SAO_CLASSES);
if(typeIdc == SAO_TYPE_START_BO)
{
for(Int i=0; i< 4; i++)
{
dstOffsets[(typeAuxInfo+ i)%NUM_SAO_BO_CLASSES] = codedOffset[(typeAuxInfo+ i)%NUM_SAO_BO_CLASSES]*(1<<m_offsetStepLog2[compIdx]);
}
}
else //EO
{
for(Int i=0; i< NUM_SAO_EO_CLASSES; i++)
{
dstOffsets[i] = codedOffset[i] *(1<<m_offsetStepLog2[compIdx]);
}
assert(dstOffsets[SAO_CLASS_EO_PLAIN] == 0); //keep EO plain offset as zero
}
}
Int TComSampleAdaptiveOffset::getMergeList(TComPic* pic, Int ctu, SAOBlkParam* blkParams, std::vector<SAOBlkParam*>& mergeList)
{
Int ctuX = ctu % m_numCTUInWidth;
Int ctuY = ctu / m_numCTUInWidth;
Int mergedCTUPos;
Int numValidMergeCandidates = 0;
for(Int mergeType=0; mergeType< NUM_SAO_MERGE_TYPES; mergeType++)
{
SAOBlkParam* mergeCandidate = NULL;
switch(mergeType)
{
case SAO_MERGE_ABOVE:
{
if(ctuY > 0)
{
mergedCTUPos = ctu- m_numCTUInWidth;
if( pic->getSAOMergeAvailability(ctu, mergedCTUPos) )
{
mergeCandidate = &(blkParams[mergedCTUPos]);
}
}
}
break;
case SAO_MERGE_LEFT:
{
if(ctuX > 0)
{
mergedCTUPos = ctu- 1;
if( pic->getSAOMergeAvailability(ctu, mergedCTUPos) )
{
mergeCandidate = &(blkParams[mergedCTUPos]);
}
}
}
break;
default:
{
printf("not a supported merge type");
assert(0);
exit(-1);
}
}
mergeList.push_back(mergeCandidate);
if (mergeCandidate != NULL)
{
numValidMergeCandidates++;
}
}
return numValidMergeCandidates;
}
Void TComSampleAdaptiveOffset::reconstructBlkSAOParam(SAOBlkParam& recParam, std::vector<SAOBlkParam*>& mergeList)
{
for(Int compIdx=0; compIdx< NUM_SAO_COMPONENTS; compIdx++)
{
SAOOffset& offsetParam = recParam[compIdx];
if(offsetParam.modeIdc == SAO_MODE_OFF)
{
continue;
}
switch(offsetParam.modeIdc)
{
case SAO_MODE_NEW:
{
invertQuantOffsets(compIdx, offsetParam.typeIdc, offsetParam.typeAuxInfo, offsetParam.offset, offsetParam.offset);
}
break;
case SAO_MODE_MERGE:
{
SAOBlkParam* mergeTarget = mergeList[offsetParam.typeIdc];
assert(mergeTarget != NULL);
offsetParam = (*mergeTarget)[compIdx];
}
break;
default:
{
printf("Not a supported mode");
assert(0);
exit(-1);
}
}
}
}
Void TComSampleAdaptiveOffset::reconstructBlkSAOParams(TComPic* pic, SAOBlkParam* saoBlkParams)
{
m_picSAOEnabled[SAO_Y] = m_picSAOEnabled[SAO_Cb] = m_picSAOEnabled[SAO_Cr] = false;
for(Int ctu=0; ctu< m_numCTUsPic; ctu++)
{
std::vector<SAOBlkParam*> mergeList;
getMergeList(pic, ctu, saoBlkParams, mergeList);
reconstructBlkSAOParam(saoBlkParams[ctu], mergeList);
for(Int compIdx=0; compIdx< NUM_SAO_COMPONENTS; compIdx++)
{
if(saoBlkParams[ctu][compIdx].modeIdc != SAO_MODE_OFF)
{
m_picSAOEnabled[compIdx] = true;
}
}
}
}
Void TComSampleAdaptiveOffset::offsetBlock(Int compIdx, Int typeIdx, Int* offset
, Pel* srcBlk, Pel* resBlk, Int srcStride, Int resStride, Int width, Int height
, Bool isLeftAvail, Bool isRightAvail, Bool isAboveAvail, Bool isBelowAvail, Boo