此系列是为了记录自己学习VTM10.0的过程和锻炼表达能力,主要是从解码端进行入手。由于本人水平有限,出现的错误恳请大家指正,欢迎与大家一起交流进步。
接着本系列的上一篇博客继续讲,上一篇博客的末尾讲到调用slice解码器进行解码,就是m_cSliceDecoder.decompressSlice()这个函数。大致的作用就是将slice切成CTU然后继续解码,下面开始讲解具体流程。
1. decompressSlice()
//-- For time output for each slice
slice->startProcessingTimer();//slice解码开始,记录开始时间
const SPS* sps = slice->getSPS();//获得SPS
Picture* pic = slice->getPic();//获得slice所在帧
CABACReader& cabacReader = *m_CABACDecoder->getCABACReader( 0 );//获得解析语法元素的类
// setup coding structure
CodingStructure& cs = *pic->cs;//获得帧中的coding structure类
cs.slice = slice;
cs.sps = sps;
cs.pps = slice->getPPS();
memcpy(cs.alfApss, slice->getAlfAPSs(), sizeof(cs.alfApss));
cs.lmcsAps = slice->getPicHeader()->getLmcsAPS();
cs.scalinglistAps = slice->getPicHeader()->getScalingListAPS();
cs.pcv = slice->getPPS()->pcv;
cs.chromaQpAdj = 0;
cs.picture->resizeSAO(cs.pcv->sizeInCtus, 0);//清空存有SAO参数的容器
cs.resetPrevPLT(cs.prevPLT);//重置cs里面的prevPLT,PLT(palette coding mode)
startProcessingTimer():记录开始解码slice的时间
sps:Sequence Parameter set
pic:slice所在帧的Picture指针
cabacReader:解析语法元素的类实例,与CABAC有关
cs:存储解码信息的重要类实例
如果对上面三个aps相关的类指针有些疑问的话,可以看VLCReader.cpp里面的parseAPS()这个函数以及DecLib.cpp里面的xActivateParameterSets()和activateAPS()两个函数。
if (slice->getFirstCtuRsAddrInSlice() == 0)//如果解码过程处于帧中的第一个slice segment
{
//重置一些帧级的属性缓存
cs.picture->resizeAlfCtuEnableFlag( cs.pcv->sizeInCtus );
cs.picture->resizeAlfCtbFilterIndex(cs.pcv->sizeInCtus);
cs.picture->resizeAlfCtuAlternative( cs.pcv->sizeInCtus );
}
如果解码过程处于帧中的第一个slice,那么要重置一些帧级的属性缓存
//将码流切成subStreams,主要参考JVET-S2001 P203 语法元素sh_entry_point_offset_minus1
const unsigned numSubstreams = slice->getNumberOfSubstreamSizes() + 1;
// init each couple {EntropyDecoder, Substream}
// Table of extracted substreams.
std::vector<InputBitstream*> ppcSubstreams( numSubstreams );
for( unsigned idx = 0; idx < numSubstreams; idx++ )
{
ppcSubstreams[idx] = bitstream->extractSubstream( idx+1 < numSubstreams ? ( slice->getSubstreamSize(idx) << 3 ) : bitstream->getNumBitsLeft() );
}
numSubstreams:Substream的数量
ppcSubstreams:Substreams的容器
上面这一段就是将码流切成substream,与cabac有关系。具体的还是参考JVET-S2001 P203 语法元素sh_entry_point_offset_minus1。不太准确得说就是如果开启WPP,substream就是tile中的一行CTUs;不开启WPP,substream就是一个tile。
const unsigned widthInCtus = cs.pcv->widthInCtus;//帧的宽度以CTU为单位
const bool wavefrontsEnabled = cs.sps->getEntropyCodingSyncEnabledFlag();//是否开启WPP(wavefront parallel processing)
const bool entryPointPresent = cs.sps->getEntryPointsPresentFlag();//是否存在entry point
cabacReader.initBitstream( ppcSubstreams[0] );//设置解析语法元素类的输入比特流
cabacReader.initCtxModels( *slice );//初始化上下文模型
// Quantization parameter
pic->m_prevQP[0] = pic->m_prevQP[1] = slice->getSliceQp();//重置prevQP
widthInCtus:帧的宽度以CTU为单位
wavefrontsEnabled:是否开启WPP(wavefront parallel processing)
entryPointPresent:是否存在entry point,就是存不存在substream划分
initBitstream():设置解析语法元素类的输入比特流
initCtxModels():初始化上下文模型,看CABAC的时候可以仔细看看里面
m_prevQP:代表前一个QP,这里需要重置一下
//如果slice不是I slice且参考帧列表中RPL0的第一个参考帧有subpicture划分,要设置clipMv函数指针为相对应的函数
#if JVET_S0258_SUBPIC_CONSTRAINTS
if( slice->getSliceType() != I_SLICE && slice->getRefPic( REF_PIC_LIST_0, 0 )->subPictures.size() > 1 )
#else
if (slice->getSliceType() != I_SLICE && slice->getRefPic(REF_PIC_LIST_0, 0)->numSubpics > 1)
#endif
{
clipMv = clipMvInSubpic;
}
else
{
clipMv = clipMvInPic;
}
这里主要设置clipMv这个函数指针,暂时不清楚具体作用
// for every CTU in the slice segment...
unsigned subStrmId = 0;//subStream的Id
for( unsigned ctuIdx = 0; ctuIdx < slice->getNumCtuInSlice(); ctuIdx++ )
{
//...
}
subStrmId:标识解码过程处于的subStream的Id
这里的for循环就是将slice切成每个CTU进行解码的过程,下一节再具体讲。
// deallocate all created substreams, including internal buffers.
for( auto substr: ppcSubstreams )
{
delete substr;
}
slice->stopProcessingTimer();//计算解码slice的时间
这里的for就是释放存有subStreams的信息
stopProcessingTimer():计算解码slice的时间并存在slice类中的m_dProcessingTime
2.for循环解码CTU
首先来看看JVET-S2001对这个for循环的描述:
然后我们来看一下实际的代码
const unsigned ctuRsAddr = slice->getCtuAddrInSlice(ctuIdx);//CTU的帧内地址,按光栅扫描来排序的
const unsigned ctuXPosInCtus = ctuRsAddr % widthInCtus;//CTU的x轴坐标,以CTU为单位
const unsigned ctuYPosInCtus = ctuRsAddr / widthInCtus;//CTU的y轴坐标,以CTU为单位
const unsigned tileColIdx = slice->getPPS()->ctuToTileCol( ctuXPosInCtus );//CTU所在tile的列Index
const unsigned tileRowIdx = slice->getPPS()->ctuToTileRow( ctuYPosInCtus );//CTU所在tile的行Index
const unsigned tileXPosInCtus = slice->getPPS()->getTileColumnBd( tileColIdx );//CTU所在tile的左边界的x轴坐标,以CTU为单位
const unsigned tileYPosInCtus = slice->getPPS()->getTileRowBd( tileRowIdx );//CTU所在tile的上边界的y轴坐标,以CTU为单位
const unsigned tileColWidth = slice->getPPS()->getTileColumnWidth( tileColIdx );//CTU所在tile的宽度,以CTU为单位
const unsigned tileRowHeight = slice->getPPS()->getTileRowHeight( tileRowIdx );//CTU所在tile的高度,以CTU为单位
const unsigned tileIdx = slice->getPPS()->getTileIdx( ctuXPosInCtus, ctuYPosInCtus);//CTU所在tile的Index,按光栅扫描来排序的
const unsigned maxCUSize = sps->getMaxCUWidth();//CTU的宽高,也是CU的最大宽高
Position pos( ctuXPosInCtus*maxCUSize, ctuYPosInCtus*maxCUSize) ;//CTU左上角点的位置
UnitArea ctuArea(cs.area.chromaFormat, Area( pos.x, pos.y, maxCUSize, maxCUSize ) );//初始化unit类,代表所在CTU的区域
const SubPic &curSubPic = slice->getPPS()->getSubPicFromPos(pos);//获得CTU所在的subPicture类
上面设置了很多变量,具体含义都在相应的注释里面,最好仔细看一遍,后面会用到
// padding/restore at slice level
//如果所在帧有subPicture划分,所在subpicture在解码过程被视为picture(区别好像是不包括环路滤波操作),同时解码过程处于当前slice的第一个CTU
if (slice->getPPS()->getNumSubPics()>=2 && curSubPic.getTreatedAsPicFlag() && ctuIdx==0)
{
int subPicX = (int)curSubPic.getSubPicLeft();//所在subpicture的左边界的x轴坐标
int subPicY = (int)curSubPic.getSubPicTop();//所在subpicture的上边界的y轴坐标
int subPicWidth = (int)curSubPic.getSubPicWidthInLumaSample();//所在subpicture的宽度
int subPicHeight = (int)curSubPic.getSubPicHeightInLumaSample();//所在subpicture的高度
for (int rlist = REF_PIC_LIST_0; rlist < NUM_REF_PIC_LIST_01; rlist++)
{
int n = slice->getNumRefIdx((RefPicList)rlist);
for (int idx = 0; idx < n; idx++)
{
Picture *refPic = slice->getRefPic((RefPicList)rlist, idx);
#if JVET_S0258_SUBPIC_CONSTRAINTS
if( !refPic->getSubPicSaved() && refPic->subPictures.size() > 1 )//如果参考帧的subpicture的边界没有被保存,且参考帧的有subpicure划分
#else
if (!refPic->getSubPicSaved() && refPic->numSubpics > 1)
#endif
{
//下面这两句咱猜猜,第一句就是保存扩展之前的边界,第二句就是扩展边界(实在不想跟进去看= =)
refPic->saveSubPicBorder(refPic->getPOC(), subPicX, subPicY, subPicWidth, subPicHeight);
refPic->extendSubPicBorder(refPic->getPOC(), subPicX, subPicY, subPicWidth, subPicHeight);
refPic->setSubPicSaved(true);//设置表示参考帧的subpicutre的边界有被保存
}
}
}
}
这个分支触发要满足以下三个条件:
-
slice所在帧有subpicture划分
-
slice所在subpicture在解码过程被视为picture
-
解码过程处于当前slice的第一个CTU
这时就要循环参考帧列表的每一个参考帧,如果参考帧也有subpicture划分,且没有保存的边界,那么就要进行以下操作:
- saveSubPicBorder():保存未被扩展的边界
- extendSubPicBorder():扩展边界
- setSubPicSaved():设置有边界被保存
cabacReader.initBitstream( ppcSubstreams[subStrmId] );//设置对应的substream的比特流
// set up CABAC contexts' state for this CTU
if( ctuXPosInCtus == tileXPosInCtus && ctuYPosInCtus == tileYPosInCtus )
{
//如果解码过程处于tile的第一个CTU
if( ctuIdx != 0 ) // if it is the first CTU, then the entropy coder has already been reset
{
cabacReader.initCtxModels( *slice );//初始化上下文模型
cs.resetPrevPLT(cs.prevPLT);//重置一下cs里面的prevPLT
}
pic->m_prevQP[0] = pic->m_prevQP[1] = slice->getSliceQp();//重置prevQP
}
else if( ctuXPosInCtus == tileXPosInCtus && wavefrontsEnabled )//如果开启WPP,并且解码过程处于tile中一行CTU中的第一个的话
{
// Synchronize cabac probabilities with top CTU if it's available and at the start of a line.
if( ctuIdx != 0 ) // if it is the first CTU, then the entropy coder has already been reset
{
cabacReader.initCtxModels( *slice );//初始化上下文模型
cs.resetPrevPLT(cs.prevPLT);//重置一下cs里面的prevPLT
}
if( cs.getCURestricted( pos.offset(0, -1), pos, slice->getIndependentSliceIdx(), tileIdx, CH_L ) )
{
//如果上面的CU拿得到的话
// Top is available, so use it.
cabacReader.getCtx() = m_entropyCodingSyncContextState;//设置上下文模型
cs.setPrevPLT(m_palettePredictorSyncState);//设置cs里面的prevPLT
}
pic->m_prevQP[0] = pic->m_prevQP[1] = slice->getSliceQp();//重置prevQP
}
initBitstream():设置对应subStream的比特流
第一个if表示如果解码过程处于tile的第一个CTU,那就要进行以下三步:
-
初始化上下文模型
-
重置cs里面的prevPLT
-
重置prevQP
如果解码过程处于slice的第一个CTU的话,我们已经在循环外面执行了第一步和第二步,就只用执行一下第三步就行。
对应的elseif表示如果不满足上面的条件,但是解码过程处于tile的右边界的CTU,并且开启WPP,那就重复上面if的操作,只是还要再多执行一个判断。
如果上方的CU通过getCURestricted()这个函数拿得到的话,就要进行一下两步:
-
用m_entropyCodingSyncContextState设置上下文模型
-
用m_palettePredictorSyncState设置cs里面的prevPLT
上面提到的两个属性都是存储着tile中其他行的上下文模型和PLT信息。这里再说明一下getCURestricted()这个函数对能否取到对应位置的CU有一定的要求,可以跟进去看看。
//如果当前slice是B slice,且解码过程处于slice中的第一个CTU,那么重置BCW的编码顺序
bool updateBcwCodingOrder = cs.slice->getSliceType() == B_SLICE && ctuIdx == 0;
if(updateBcwCodingOrder)
{
resetBcwCodingOrder(true, cs);
}
//如果解码的CTU处于所在tile的左边界,且满足以下条件之一:(1)解码过程所处的slice不是I slice;(2)开启IBC
if ((cs.slice->getSliceType() != I_SLICE || cs.sps->getIBCFlag()) && ctuXPosInCtus == tileXPosInCtus)
{
//下面两个我就不瞎猜了,Lut是lookup table的缩写
cs.motionLut.lut.resize(0);
cs.motionLut.lutIbc.resize(0);
cs.resetIBCBuffer = true;//标志要重置IBCBuffer
}
if( !cs.slice->isIntra() )//如果解码的slice不是I slice
{
pic->mctsInfo.init( &cs, getCtuAddr( ctuArea.lumaPos(), *( cs.pcv ) ) );//貌似还没写完,目前只有设置mctsInfo的m_tileArea属性,代表所在tile的区域
}
第一个分支就是当满足以下两个条件时:
- 解码过程所处的slice是B slice
- 解码过程处于当前slice的第一个CTU
那就重置BCW的编码顺序,具体的里面的内容没有看,等看到BCW再说吧
第二个分支就可以和JVET-S2001里面的描述对应起来了,只是判断条件有些不同。
第三个分支的判断条件是当前解码的slice不是I slice,里面执行的函数还没写完。
cabacReader.coding_tree_unit( cs, ctuArea, pic->m_prevQP, ctuRsAddr );//解析CTU级语义元素,具体参考JVET-S2001 7.3.11.2 P112
m_pcCuDecoder->decompressCtu( cs, ctuArea );//调用CU解码器类解码CTU
这两句就是本篇博文最重要的两个函数,详细内容会在之后的博文。先讲一下大致的作用:
coding_tree_unit():解析CTU级的语义元素
decompressCtu():调用CU解码器类解码CTU
if( ctuXPosInCtus == tileXPosInCtus && wavefrontsEnabled )
{
//如果开启WPP,并且解码过程处于tile中一行CTU中的第一个的话
m_entropyCodingSyncContextState = cabacReader.getCtx();//存储上下文模型
cs.storePrevPLT(m_palettePredictorSyncState);//存储cs里面的prevPLT
}
这里的分支就能与上面的对应了。如果开启wpp,并且解码过程处于tile的右边界的CTU。那么就存储一下上下文模型和cs里面的prevPLT。
//下面的分支对应JVET-S2001 7.3.11.1 P111 slice_data()中coding_tree_unit()之后的部分
if( ctuIdx == slice->getNumCtuInSlice()-1 )
{
unsigned binVal = cabacReader.terminating_bit();
CHECK( !binVal, "Expecting a terminating bit" );
#if DECODER_CHECK_SUBSTREAM_AND_SLICE_TRAILING_BYTES
cabacReader.remaining_bytes( false );
#endif
}
else if( ( ctuXPosInCtus + 1 == tileXPosInCtus + tileColWidth ) &&
( ctuYPosInCtus + 1 == tileYPosInCtus + tileRowHeight || wavefrontsEnabled ) )
{
// The sub-stream/stream should be terminated after this CTU.
// (end of slice-segment, end of tile, end of wavefront-CTU-row)
unsigned binVal = cabacReader.terminating_bit();
CHECK( !binVal, "Expecting a terminating bit" );
if( entryPointPresent )
{
#if DECODER_CHECK_SUBSTREAM_AND_SLICE_TRAILING_BYTES
cabacReader.remaining_bytes( true );
#endif
subStrmId++;
}
}
这里同样可以和JVET-S2001里面的描述对应起来,里面有细微的不同。例如里面有subStrmId的自增1。
//如果当前帧都subpicture划分,所在subpicture在解码过程被视为picture,且解码过程处于当前slice的最后一个CTU
if (slice->getPPS()->getNumSubPics() >= 2 && curSubPic.getTreatedAsPicFlag() && ctuIdx == (slice->getNumCtuInSlice() - 1))
// for last Ctu in the slice
{
int subPicX = (int)curSubPic.getSubPicLeft();//所在subpicture的左边界的x轴坐标
int subPicY = (int)curSubPic.getSubPicTop();//所在subpicture的上边界的y轴坐标
int subPicWidth = (int)curSubPic.getSubPicWidthInLumaSample();//所在subpicture的宽度
int subPicHeight = (int)curSubPic.getSubPicHeightInLumaSample();//所在subpicture的高度
//
for (int rlist = REF_PIC_LIST_0; rlist < NUM_REF_PIC_LIST_01; rlist++)
{
int n = slice->getNumRefIdx((RefPicList)rlist);
for (int idx = 0; idx < n; idx++)
{
Picture *refPic = slice->getRefPic((RefPicList)rlist, idx);
if (refPic->getSubPicSaved())//如果subpicture的边界有被保存下来
{
refPic->restoreSubPicBorder(refPic->getPOC(), subPicX, subPicY, subPicWidth, subPicHeight);//复原原始的边界
refPic->setSubPicSaved(false);//设置标志表示subpicture的边界没有保存
}
}
}
}
这里同样可以和上面的对应起来。大致意思就是如果参考帧进行了subpicture的边界扩展,那么就得还原一下。