VTM10.0代码学习3:DecSlice_decompressSlice()

此系列是为了记录自己学习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的边界扩展,那么就得还原一下。

猜你喜欢

转载自blog.csdn.net/hjhyxq2014/article/details/109249998