本文是本系列的第四篇博客,內容是分析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等信息,該部分代碼的具體分析會在下一篇博客展開