VVC/H.266代碼閱讀(VTM8.0)(四. CU劃分 )

本文是本系列的第四篇博客,內容是分析CU劃分代碼。
該系列相關博客爲:
VVC/H.266代碼閱讀(VTM8.0)(一. NALU提取)
VVC/H.266代碼閱讀(VTM8.0)(二. non-VCLU解碼)
VVC/H.266代碼閱讀(VTM8.0)(三. Slice到CTU的處理 )
VVC/H.266代碼閱讀(VTM8.0)(四. CU劃分 )

VVC/H.266常見資源爲:
VVC/H.266常見資源整理(提案地址、代碼、資料等)

注:

  1. 考慮到從解碼端分析代碼,一是更加簡單(解碼流程無需編碼工具和編碼參數的擇優),二是可以配合Draft文本更好地理解視頻編解碼的流程(解碼端也都包含預測、量化、環路濾波、熵解碼等流程),所以本系列從解碼端入手分析VVC解碼大致流程。等到解碼端代碼分析完後,再從編碼端深入分析。
  2. 本文分析的bin文件是利用VTM8.0的編碼器,以All Intra配置(IBC 打開)編碼100幀得到的二進制碼流(TemporalSubsampleRatio: 8,實際編碼 ⌈100 / 8⌉ = 13幀)。
  3. 解碼用最簡單的:-b str.bin -o dec.yuv

上一篇博客的最後寫道“調用CABACReader::coding_tree_unit() 解碼該CTU”。所以,本篇博客從該函數開始分析。

1. 調用CABACReader::coding_tree_unit() 解碼該CTU,遵循draft內7.3.10.2 Coding tree unit syntax。

void CABACReader::coding_tree_unit( CodingStructure& cs, const UnitArea& area, int (&qps)[2], unsigned ctuRsAddr )
{
  CUCtx cuCtx( qps[CH_L] );
  QTBTPartitioner partitioner;
  //partitioner負責核心的劃分環節
  ……
  sao( cs, ctuRsAddr );
  //解碼該CTU內有關SAO的相關參數,比如SAO的類型、偏移值等。
  //遵循draft 7.3.10.3	Sample adaptive offset syntax。
……
  //關於ALF等參數的解析,此處省略
……

//接下來調用coding_tree()完成CTU的劃分。
  if ( CS::isDualITree(cs) && cs.pcv->chrFormat != CHROMA_400 && cs.pcv->maxCUWidth > 64 )
  {
    //針對I幀dualTree的128 * 128大CTU,且採樣格式不是4:0:0
    QTBTPartitioner chromaPartitioner;
    chromaPartitioner.initCtu(area, CH_C, *cs.slice);
    CUCtx cuCtxChroma(qps[CH_C]);
    //色度分割
    coding_tree(cs, partitioner, cuCtx, &chromaPartitioner, &cuCtxChroma);
    //調用coding_tree()完成CTU亮色度分量的劃分。
    //四叉分割時亮色度分量劃分保持一致
    qps[CH_L] = cuCtx.qp;
    qps[CH_C] = cuCtxChroma.qp;
  }
  else
  {
    //P、B幀以及上一步不滿足的I幀CTU
    //亮色度分量劃分樹可以不一致
    coding_tree(cs, partitioner, cuCtx);
    //調用coding_tree()完成CTU亮度分量的劃分。
    qps[CH_L] = cuCtx.qp;
    if( CS::isDualITree( cs ) && cs.pcv->chrFormat != CHROMA_400 )
    {
      //採用亮色度
      CUCtx cuCtxChroma( qps[CH_C] );
      partitioner.initCtu(area, CH_C, *cs.slice);
      coding_tree(cs, partitioner, cuCtxChroma);
      //調用coding_tree()完成CTU的劃分。
      qps[CH_C] = cuCtxChroma.qp;
    }
  }

}

2.CABACReader::coding_tree()的解析遵循draft 7.3.10.4 Coding tree syntax。

  • CABACReader::coding_tree()不斷進行遞歸調用,對當前CTU進行層層劃分,得到若干個CU。
  • 從代碼來看,進入CABACReader::coding_tree()後,調用CABACReader::split_cu_mode(),CABAC解碼得出SplitFlag(是否劃分的flag)、SplitQtFlag(是否四叉劃分的flag)、SplitHvFlag(劃分方向的flag)、Split12Flag(是否二叉劃分的flag),組合後推斷出劃分模式splitMode,進行劃分。
  • 總的來說,就是在解碼端重建劃分樹,得到一個個葉節點CU。
void CABACReader::coding_tree( CodingStructure& cs, Partitioner& partitioner, CUCtx& cuCtx, Partitioner* pPartitionerChroma, CUCtx* pCuCtxChroma)
{
  const PPS      &pps         = *cs.pps;
  const UnitArea &currArea    = partitioner.currArea();
  //當前區域信息,包括左上角座標位置、寬高、採樣格式、亮色度分量等信息。
  ……
  //delta qp等設置,因爲採用固定QP,此處省略部分代碼
  ……
  const PartSplit splitMode = split_cu_mode( cs, partitioner );
  //分析劃分的模式。
  //簡單來說,該函數內利用CABAC解碼得出SplitFlag(是否劃分的flag)、SplitQtFlag(是否四叉劃分的flag)、SplitHvFlag(劃分方向的flag)、Split12Flag(是否二叉劃分的flag),組合後推斷出劃分模式splitMode。

  if( splitMode != CU_DONT_SPLIT )
  {
     //CU需要進一步劃分時,會進入接下來的遞歸劃分,否則直接裝載CU信息。
      if (CS::isDualITree(cs) && pPartitionerChroma != nullptr && (partitioner.currArea().lwidth() >= 64 || partitioner.currArea().lheight() >= 64))
      {
      //在CABACReader::coding_tree_unit() 中,我寫到了“針對I幀dualTree的128 * 128採樣格式不是4:0:0的大CTU,調用coding_tree()完成CTU亮色度分量的劃分,且亮色度劃分QT保持一致,第一次進入CABACReader::coding_tree()就是進入了這個流程。”
        partitioner.splitCurrArea(CU_QUAD_SPLIT, cs);
        //根據四叉樹將亮度分量區域劃分成四部分。
        pPartitionerChroma->splitCurrArea(CU_QUAD_SPLIT, cs);
        //根據四叉樹將色度分量區域劃分成四部分。
        bool beContinue = true;
        bool lumaContinue = true;
        bool chromaContinue = true;
        //三個變量判斷是否需要繼續劃分。
        //因爲上面進行了四叉劃分,將128 * 128 CTU劃分成了4個不重疊的64 * 64的CU,所以需要對這4個CU分別遞歸劃分。

        while (beContinue)
        {
          if (partitioner.currArea().lwidth() > 64 || partitioner.currArea().lheight() > 64)
          {
          //老實說,這部分代碼我沒有特別看懂
          //因爲128 * 128的CTU四叉劃分後,CU寬高肯定都不會大於64,應該進不了這個部分的代碼,可能是某種配置下會進入該部分代碼,希望有大佬可以教我。
          //下面代碼省略
            ……
          else
          {
            //dual tree coding under 64x64 block
            //按上面的分析應該都進入下面的代碼,亮色度劃分分開進行
            if (cs.area.blocks[partitioner.chType].contains(partitioner.currArea().blocks[partitioner.chType].pos()))
            {
              coding_tree(cs, partitioner, cuCtx);
              //對當前的64 * 64亮度CU遞歸調用coding_tree()進行劃分
            }
            lumaContinue = partitioner.nextPart(cs);
            //只有處理完4個64 * 64亮度CU,lumaContinue纔會變成false
            if (cs.area.blocks[pPartitionerChroma->chType].contains(pPartitionerChroma->currArea().blocks[pPartitionerChroma->chType].pos()))
            {
              coding_tree(cs, *pPartitionerChroma, *pCuCtxChroma);
              //同理,對當前的64 * 64色度CU遞歸調用coding_tree()進行劃分
            }
            chromaContinue = pPartitionerChroma->nextPart(cs);
            //只有處理完4個64 * 64色度CU,chromaContinue纔會變成false
            CHECK(lumaContinue != chromaContinue, "luma chroma partition should be matched");
            beContinue = lumaContinue;
          }
        }
        partitioner.exitCurrSplit();
        pPartitionerChroma->exitCurrSplit();
        //處理完4個64 * 64CU的亮色度分量,整個CTU結束劃分流程。

        //cat the chroma CUs together
        CodingUnit* currentCu = cs.getCU(partitioner.currArea().lumaPos(), CHANNEL_TYPE_LUMA);
        //從cs(CodingStructure類)的cus(vector<CodingUnit*>)中讀取出已經劃分好的CU
        //在cus是按照FIFO順序依次存儲 左上角64*64亮度CUs、左上角64*64色度CUs、右上角64*64亮度CUs、右上角64*64色度CUs、左下角64*64亮度CUs、左下角64*64色度CUs、右上角64*64亮度CUs、右上角64*64色度CUs。
        //經過下面的操作,把CU的鏈接順序重新連接(CU有個next指針,類似數據結構的鏈表基本操作),變成全部四個64*64塊的亮度CUs->全部四個64*64塊的色度CUs
        CodingUnit* nextCu = nullptr;
        CodingUnit* tempLastLumaCu = nullptr;
        CodingUnit* tempLastChromaCu = nullptr;
        ChannelType currentChType = currentCu->chType;
        while (currentCu->next != nullptr)
        {
          nextCu = currentCu->next;
          if (currentChType != nextCu->chType && currentChType == CHANNEL_TYPE_LUMA)
          {
            tempLastLumaCu = currentCu;
            //currentCu是最後一個亮度CU,接下來nextCu是色度CU,tempLastLumaCu 保存這個亮度CU
            if (tempLastChromaCu != nullptr) //swap
            {
              tempLastChromaCu->next = nextCu;
              //之前最後一個色度CU連接nextCu,實現色度CU的串接
            }
          }
          else if (currentChType != nextCu->chType && currentChType == CHANNEL_TYPE_CHROMA)
          {
            tempLastChromaCu = currentCu;
            //currentCu是最後一個色度CU,接下來nextCu是亮度CU,tempLastChromaCu保存這個色度CU
            if (tempLastLumaCu != nullptr) //swap
            {
              tempLastLumaCu->next = nextCu;
              //之前最後一個亮度CU連接nextCu,實現亮度CU的串接
            }
          }
          currentCu = nextCu;
          currentChType = currentCu->chType;
        }

        CodingUnit* chromaFirstCu = cs.getCU(pPartitionerChroma->currArea().chromaPos(), CHANNEL_TYPE_CHROMA);
        tempLastLumaCu->next = chromaFirstCu;
        //最後一個亮度CU連接第一個色度CU,實現四個64*64塊的亮度CUs->全部四個64*64塊的色度CUs的連接

      }
      else
      {
        //對於每一個小CU,都會遞歸調用coding_tree()進行劃分,所以這部分是核心代碼
        const ModeType modeTypeParent = partitioner.modeType;
        //modeTypeParent記錄父CU的模式
        cs.modeType = partitioner.modeType = mode_constraint( cs, partitioner, splitMode ); //change for child nodes
        //關於mode_constraint(), draft關於modeTypeCondition、mode_constraint_flag等根據slice_type、CU寬高、採樣格式等信息規定了一系列內容,有興趣可以查看draft,此處不再展開
        //decide chroma split or not
        bool chromaNotSplit = modeTypeParent == MODE_TYPE_ALL && partitioner.modeType == MODE_TYPE_INTRA;
        //色度進行劃分
        CHECK( chromaNotSplit && partitioner.chType != CHANNEL_TYPE_LUMA, "chType must be luma" );
        if( partitioner.treeType == TREE_D )
        {
          cs.treeType = partitioner.treeType = chromaNotSplit ? TREE_L : TREE_D;
        }
      partitioner.splitCurrArea( splitMode, cs );
      //根據解析的splitMode對當前CU進行劃分,分成n個互不重疊的子CU
      do
      {
        if( cs.area.blocks[partitioner.chType].contains( partitioner.currArea().blocks[partitioner.chType].pos() ) )
        {
          coding_tree( cs, partitioner, cuCtx );
          //對劃分後的CU調用coding_tree()進行進一步遞歸劃分
        }
      } while( partitioner.nextPart( cs ) );
      //只有處理完全部n個子CU,劃分才結束

      partitioner.exitCurrSplit();
      //處理完n個子CU,整個CTU結束劃分流程。
      
      if( chromaNotSplit )
      {
      //根據之前的判斷,色度不再進行劃分
        CHECK( partitioner.chType != CHANNEL_TYPE_LUMA, "must be luma status" );
        partitioner.chType = CHANNEL_TYPE_CHROMA;
        //partitioner.chType臨時改成色度
        cs.treeType = partitioner.treeType = TREE_C;
        //TREE_C: separate tree only contains chroma (not split), to avoid small chroma block
        if( cs.picture->blocks[partitioner.chType].contains( partitioner.currArea().blocks[partitioner.chType].pos() ) )
        {
          coding_tree( cs, partitioner, cuCtx );
        }

        //recover treeType
        partitioner.chType = CHANNEL_TYPE_LUMA;
        cs.treeType = partitioner.treeType = TREE_D;
        //恢復成原來的設置
      }

      //recover ModeType
      cs.modeType = partitioner.modeType = modeTypeParent;
      }
      return;
  }

  //CU劃分完畢或者dont split,進入下面步驟,解析CU信息。
  CodingUnit& cu = cs.addCU( CS::getArea( cs, currArea, partitioner.chType ), partitioner.chType );
 //根據位置,在cs(CodingStructure類)的cus(vector<CodingUnit*>)中添加該CU,上面的代碼分析過,CTU分割完以後會重新對CU的連接順序排序

  partitioner.setCUData( cu );
  //設置cu的劃分信息,比如cu.depth、cu.btDepth等信息
  cu.slice   = cs.slice;
  cu.tileIdx = cs.pps->getTileIdx( currArea.lumaPos() );
  
  ……
  //使用固定QP,忽略部分代碼
  ……

  // coding unit
  coding_unit( cu, partitioner, cuCtx );
  //調用coding_unit()完成CU內相關參數,如預測模式、MV等信息的解析。這部分代碼會在下一篇博客內具體分析
  ……
}

3. 調用CABACReader::coding_unit()分析該CU的預測模式、MV等信息,該部分代碼的具體分析會在下一篇博客展開

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章