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等信息,该部分代码的具体分析会在下一篇博客展开

猜你喜欢

转载自blog.csdn.net/weixin_37524256/article/details/105929230