本文是本系列的第四篇博客,内容是分析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常见资源整理(提案地址、代码、资料等)
注:
- 考虑到从解码端分析代码,一是更加简单(解码流程无需编码工具和编码参数的择优),二是可以配合Draft文本更好地理解视频编解码的流程(解码端也都包含预测、量化、环路滤波、熵解码等流程),所以本系列从解码端入手分析VVC解码大致流程。等到解码端代码分析完后,再从编码端深入分析。
- 本文分析的bin文件是利用VTM8.0的编码器,以All Intra配置(IBC 打开)编码100帧得到的二进制码流(TemporalSubsampleRatio: 8,实际编码 ⌈100 / 8⌉ = 13帧)。
- 解码用最简单的:-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等信息,该部分代码的具体分析会在下一篇博客展开