x265笔记_4_CompressCTU源码分析

Before compressCTU

我们先回顾一下x265中如何调用的compressCTU

Encoder::encode中的do-while循环中调用curEncoder->startCompressFrame(frameEnc),触发开始编码的事件m_enable.trigger(),开始执行帧级别的编码函数FrameEncoder::compressFrame(),该函数又调用FrameEncoder::processRowEncoder(),这个600多行的函数关键是调用了analysis.cpp中的Analysis::compressCTU(),返回最上层CU的划分结果(Does all the CU analysis, returns best topo level mode decision)——这和HM16中关于x265中块划分的函数叫法一致(关于HM16中的编码块划分流程可以看之前的文章)。

总结一下:

                      main()
                        |
                 Encoder::encode()
                        |
          curEncoder->startCompressFrame
                        |
                m_enable.trigger()
                        |
            FrameEncoder::compressFrame()
                        |
            FrameEncoder::processRowEncoder()
                        |
                Analysis::compressCTU()

compressCTU

输入参数

Mode& Analysis::compressCTU(CUData& ctu, Frame& frame, const CUGeom& cuGeom, const Entropy& initialContext)

参数中的ctu是当前要编码的编码树单元(coding tree unit),默认大小是64*64,frame是当前CTU所在的帧(frame),cuGeom则是存储CTU对应的四叉树划分结构的数据结构。

处理流程

该函数首先初始化传入参数的信息到当前命名空间的变量,然后进行slice类型判断,分为帧间预测和帧内预测,最后返回包含当前CTU最佳四叉树划分结构、PU划分结构和率失真损失等参数的bestmode。

因为将帧内和帧间的分析封装成函数,而且针对不同的RD_level都有不同的分析函数,所以实现上要比HM那个1600多行的xCompressCu简洁的多。

具体流程如下图所示,其中帧间部分参考了博客

在这里插入图片描述

重要变量

m_parame

m_param是在运行x265时输入的参数配置,在源码中有很多其分析模式有关的参数作为条件判断,例如m_param->analysisLoadm_param->bAnalysisType等等,一般都会跳过。

rd_level

rd_levelanalysis.cpp中是一个很重要的参数,作者在开头用了很大篇幅的注释说明不同等级的rd_level代表什么意思,在官方手册中也有介绍:

rd_level对应配置参数--rd,全称是 Level of RDO in mode decision,取值范围为0~6,数值越大代表分析越详尽,使用的率失真优化越多,编码速度则会越慢,比特流通常也会增加,默认值为3。

The higher the value, the more exhaustive the analysis and the more rate distortion optimization is used. The lower the value the faster the encode, the higher the value the smaller the bitstream (in general). Default 3

六种rd_level代表的含义如下表所示

LevelDescription
0sa8d mode and split decisions, intra w/ source pixels
1recon generated (better intra), RDO merge/skip selection
2RDO splits and merge/skip selection
3RDO mode and split decisions, chroma residual used for sa8d
4Currently same as 3
5Adds RDO prediction decisions
6Currently same as 5
amprect

rd_level相似,这两者也能控制图片的质量,分别对应 非对称划分模式(asymmetric motion partitioning)和 矩形划分模式(rectangular,即对半分的划分模式),对应的配置参数是--amp--rect 或者--no-amp--no-rect

在官方手册中,这两者的描述如下:

–rect, --no-rect
Enable analysis of rectangular motion partitions Nx2N and 2NxN (50/50 splits, two directions). Default disabled

启用矩形运动分区Nx2N和2NxN的分析,默认禁用。(50/50)

–amp, --no-amp
Enable analysis of asymmetric motion partitions (75/25 splits, four directions). At RD levels 0 through 4, AMP partitions are only considered at CU sizes 32x32 and below. At RD levels 5 and 6, it will only consider AMP partitions as merge candidates (no motion search) at 64x64, and as merge or inter candidates below 64x64.

The AMP partitions which are searched are derived from the current best inter partition. If Nx2N (vertical rectangular) is the best current prediction, then left and right asymmetrical splits will be evaluated. If 2NxN (horizontal rectangular) is the best current prediction, then top and bottom asymmetrical splits will be evaluated, If 2Nx2N is the best prediction, and the block is not a merge/skip, then all four AMP partitions are evaluated.

This setting has no effect if rectangular partitions are disabled. Default disabled

启用不对称运动分区的分析(75/25分割,四个方向)。**在RD级别0到4时,仅在CU尺寸为32x32及以下时才考虑AMP分区。**在RD级别5和6,它将只考虑AMP分区作为合并候选(无运动搜索)在64×64,并作为合并或候选人之间低于64×64。

**搜索的AMP分区是从当前最佳分区间派生的。**如果Nx2N(垂直矩形)是当前最佳预测,则将评估左右不对称分裂。如果2NxN(水平矩形)是最佳的当前预测,则将评估顶部和底部不对称拆分;如果2Nx2N是最佳预测,且块不是合并/跳过,则将评估所有四个AMP分区。

如果禁用矩形分区,则此设置无效。默认禁用

这说明x265为了加快处理速度,不仅默认禁用了rect模式,而且默认禁用了amp模式(毕竟如果禁用了矩形分区,则amp设置无效),相当于默认配置下(re_level=3),七种划分模式中仅开启了2Nx2N的划分模式。

重要函数

ProfileCUScope(ctu, totalCTUTime, totalCTUs);用来记录日志

compressIntraCU(ctu, cuGeom, qp);是默认的帧内块划分函数,而且还针对不同的rd_level设置了两个帧间块划分函数compressInterCU_rd0_4compressInterCU_rd5_6,以及针对多线程的compressInterCU_dist

compressIntraCU

输入参数

uint64_t Analysis::compressIntraCU(const CUData& parentCTU, const CUGeom& cuGeom, int32_t qp)

parentCTU是当前待编码的父CU,CuGeom是表达划分四叉树的数据结构,qp是量化参数(Quantization Parameter)。

处理流程

不同版本的编码器在该部分的处理流程基本一致,都可以分为四步:

  1. 初始化传入参数到当前命名空间的变量;
  2. 计算父CU的率失真损失,即划分前的率失真损失;
  3. 循环遍历子CU,递归调用自身获得划分后的率失真损失;
  4. 比较重建前后的率失真损失,更新Best mode,保存最佳预测数据和重建图像

读源码也是如此,首先自己心里有一个大致的框架,然后通过分析源码,一方面看作者的实现方法,另一方面补充调整自己的框架。

下面我们仔细分析x265中的源码实现:

1. 参数初始化
// part1
uint32_t depth = cuGeom.depth;// 当前parent CU所在深度
ModeDepth& md = m_modeDepth[depth]; // 存储该深度下的预测数据、原始YUV和best mode
md.bestMode = NULL; // 最佳模式, 后续checkBestMode会进行检查

// part2
bool mightSplit = !(cuGeom.flags & CUGeom::LEAF); 
// 当前CU不是CTU的叶子节点,设置mightSplit为True,继续分裂
// 后续根据mightSplit判断是否将split flag添加到RD cost中
bool mightNotSplit = !(cuGeom.flags & CUGeom::SPLIT_MANDATORY);
// CUGeom::SPLIT_MANDATORY=True表示如果当前CU是帧内编码而且可以分裂,则强制分裂
//mightNotSplit设置为True,表示不强制分裂

bool bAlreadyDecided = m_param->intraRefine != 4 && parentCTU.m_lumaIntraDir[cuGeom.absPartIdx] != (uint8_t)ALL_IDX && !(m_param->bAnalysisType == HEVC_INFO);
bool bDecidedDepth = m_param->intraRefine != 4 && parentCTU.m_cuDepth[cuGeom.absPartIdx] == depth;
int split = 0;

x265中首先初始化输入参数到命名空间中的变量的做法十分常见,这里初始化了当前父CU的深度depth和对应深度的模式信息md ,并初始化当前模式下的最佳模式md.bestmode为空。此处的模式信息md的数据类型是结构体ModeDepth,包含以下信息:

struct ModeDepth
{
    Mode           pred[MAX_PRED_TYPES];// 当前深度,十三种模式的预测信息
    Mode*          bestMode;// 当前深度的最佳模式
    Yuv            fencYuv;// frame encode YUV,帧级别的待编码的YUV数据
    CUDataMemPool  cuMemPool;
};

Mode结构体包含更多的信息,例如CU数据cu、原始YUVfencYuv、预测后的YUVpredYuv和重建的YUV数据reconYuv,熵编码的上下文信息contexts和率失真损失rdCost等等。

之后设置了五个标识位:

typenamemeaning
boolmightSplit如果当前CU不是叶子节点,设置为True,继续分裂
boolmightNotSplit如果当前CU没有被设置强制分裂,设置为True,不强制分裂
boolbAlreadyDecided如果帧内优化等级不等于4,且预测模式已经确定,设置为True
boolbDecidedDepth如果帧内优化等级不等于4,且当前父CU的深度信息和决策树的深度信息一致(即确认深度信息是否正确,因为越界的CU的深度信息会设置为0,在后面会说明),设置为True
intsplit如果不继续分裂设置为0/False,但是下一步会进行调整,一般设置为True。
从后续的处理可以看到,split标识位等于1,是分裂的充分不必要条件(前提是mightSplit设置为True,即当前节点不是叶子节点)

然后判断如果帧内优化等级不等于4,则设置split标识位和bAlreadyDecided标识位,前者在此处一般会设置为True。

2. 计算划分前的代价

此处根据bAlreadyDecidedbDecidedDepth分成两种情况

2.1 当存在可用的模式集合时

当存在可用的模式集合(bAlreadyDecided==True),且深度信息确定无误(bDecidedDepth==True),而且不强制分裂(mightNotSplit==True)时

  1. 初始化模式为PRED_MERGE
  2. 通过标识位reuseModes决定是否重新加载模式,对于较低的RD_level不重新加载模式,而是直接跳过;(这个地方我也不是很懂,默认RD_level等于3,会直接跳过)
  3. 对可用的帧内模式进行完整的RD search
  4. 判断是否是无失真模式,如果是无失真模式,则无失真的编码当前最佳模式;
  5. 通过mightSplit标识位,判断是否需要添加split flag的率失真。

其中后三个功能的组合,可以在当前文件中看到很多次

// full RD search for intra modes
checkIntra(mode, cuGeom, (PartSize)parentCTU.m_partSize[cuGeom.absPartIdx]);

if (m_bTryLossless)
    tryLossless(cuGeom);
    // encode current best mode loselessly, pick best RD cost

// add the RD cost of coding a split flag (0 or 1) to the given mode
if (mightSplit)
    addSplitFlagCost(*md.bestMode, cuGeom.depth);
2.2 当不存在可用的模式集合时
  1. 对当前CU在PRED_INTRA模式下进行RD search,划分方式是2Nx2N,将率失真损失、预测数据和重建数据等信息保存在md.pred[PRED_INTRA]中,并更新当前深度的best_mode;

    md.pred[PRED_INTRA].cu.initSubCU(parentCTU, cuGeom, qp);
    checkIntra(md.pred[PRED_INTRA], cuGeom, SIZE_2Nx2N);
    checkBestMode(md.pred[PRED_INTRA], depth);
    
  2. 针对8*8的CU,已经不能继续划分成更小的CU了(HEVC标准规定),这里针对4*4的帧内PU划分模式PRED_INTRA_NxN进行搜索,执行语句和上一步很相似,此时的数据保存在md.pred[PRED_INTRA_NxN]中;

    if (cuGeom.log2CUSize == 3 && m_slice->m_sps->quadtreeTULog2MinSize < 3)
    {
        md.pred[PRED_INTRA_NxN].cu.initSubCU(parentCTU, cuGeom, qp);  // PRED_INTRA_NxN:  4x4 intra PU blocks for 8x8 CU
        checkIntra(md.pred[PRED_INTRA_NxN], cuGeom, SIZE_NxN);
        checkBestMode(md.pred[PRED_INTRA_NxN], depth);
    }
    
  3. 判断是否是无失真模式

  4. 判断是否将split flag添加到RD cost

3. 计算划分后的代价
3.1 更新划分标识位mightSplit

首先更新mightSplit,判断是否到达分析决策的最大深度

mightSplit &= !(bAlreadyDecided && bDecidedDepth) || split;

如果当前CU是叶子节点,即CU的尺寸为8*8(HEVC标准规定),此时mightSplit已经是False,直接跳过该步骤,否则判断是否需要划分,只要split为True 或者 bAlreadyDecided为False 或者 bDecidedDepth为False,都会继续分裂。

注:split为True,是因为一般的想法是尽量多试一试;bAlreadyDecided为False ,是因为决策模式没有确定, 也要多试一试;bDecidedDepth为False,是因为此时的父CU可能位于边界,需要进一步划分。

3.2 计算划分后的代价

该部分是函数中最重要的部分,总的流程是,初始化划分后 子CU的预测数据的缓存区 为md.pred[PRED_SPLIT],即对应13中模式中的PRED_SPLIT;然后对四个子CU分别调用compressIntraCU函数,分别得到各自的Best mode,然后将预测结果、重建数据等信息保存到md.pred[PRED_SPLIT]中;比较md.pred[PRED_SPLIT]->rdCostmd.bestMode->rdCost,更新best mode。

初始化

Mode* splitPred = &md.pred[PRED_SPLIT]; // 保存当前CU划分后的mode,由四个子CU的返回结果拼接而成
splitPred->initCosts();  
CUData* splitCU = &splitPred->cu; 
splitCU->initSubCU(parentCTU, cuGeom, qp);

uint32_t nextDepth = depth + 1;
ModeDepth& nd = m_modeDepth[nextDepth]; // nd是md的下一层深度
invalidateContexts(nextDepth);
Entropy* nextContext = &m_rqt[depth].cur; // RQT data 保存了上下文信息,这个我也不是很懂
int32_t nextQP = qp;
uint64_t curCost = 0; // 当前子CU的累计率失真损失,用于判断是否应该提前终止
int skipSplitCheck = 0; // 是否提前终止的标识位

这里,mdnd分别是mode_depth和next_mode_depth的缩写,分别表示m_modeDepth[Depth]m_modeDepth[nextDepth],其中md->bestMode保存最佳模式,而md.pred[...]完成对不同模式的RD search。

循环遍历子CU,递归调用compressIntraCU

首先获得当前子CU的四叉树表示的引用childGeom,按照HEVC标准,在一个64*64CTU中的四叉树表达共有85种(1+4+16+64),在x265中一个CU的四叉树表达抽象成了CUGeom结构体:

struct CUGeom
{
    enum {
        INTRA           = 1<<0, // CU is intra predicted
        PRESENT         = 1<<1, // CU is not completely outside the frame
        SPLIT_MANDATORY = 1<<2, // CU split is mandatory if CU is inside frame and can be split
        LEAF            = 1<<3, // CU is a leaf node of the CTU
        SPLIT           = 1<<4, // CU is currently split in four child CUs.
    };
    
    // (1 + 4 + 16 + 64) = 85.
    // 至多有85个geoms
    enum { MAX_GEOMS = 85 };

    uint32_t log2CUSize;    // Log of the CU size.
    uint32_t childOffset;   // offset of the first child CU from current CU
    uint32_t absPartIdx;    // Part index of this CU in terms of 4x4 blocks.
    uint32_t numPartitions; // Number of 4x4 blocks in the CU
    uint32_t flags;         // CU flags.
    uint32_t depth;         // depth of this CU relative from CTU
    uint32_t geomRecurId;   // Unique geom id from 0 to MAX_GEOMS - 1 for every depth
};

在底层实现中,85个CUGeom连续分布,所以才会出现下面的代码

const CUGeom& childGeom = *(&cuGeom + cuGeom.childOffset + subPartIdx);

子CU的CUGeom的位置=父CU的CUGeom地址 + 父CU的第一个子CU的偏置+当前子CU的编号。

如果当前父CU的位置没有完全越过帧的边界且子CU存在,则进行以下操作:

  1. 将64x64的CTU的一部分YUV data复制到nd.fencYuv

  2. 加载RQT数据,其中包含CABAC context,在四叉树递归过程中,程序利用其跟踪每层的深度和残差、音系和重建数据的临时缓存区;

  3. 根据QP设置率失真损失的系数

  4. 如果输入参数设置了m_param->bEnableSplitRdSkip,则会利用curCost判断子CU的累计率失真是否大于划分前的Best mode的率失真损失,如果大于,则说明划分之后的结果肯定更差劲,所以设置skipSplitCheck为True,并立即跳出循环;

    如果没有设置m_param->bEnableSplitRdSkip,则直接调用compressIntraCU进行预测。(默认关闭,设置参数为--splitrd--skip--no-splitrd-skip

    if (m_param->bEnableSplitRdSkip)
    {
        curCost += compressIntraCU(parentCTU, childGeom, nextQP);// 递归调用
        if (m_modeDepth[depth].bestMode && curCost > m_modeDepth[depth].bestMode->rdCost)
        {
            skipSplitCheck = 1;
            break;
        }
    } 
    else
        compressIntraCU(parentCTU, childGeom, nextQP); // 递归调用
    
  5. 将子CU最佳模式的预测结果(m_modeDepth[nextDepth].bestMode->cu)保存到父CU所在深度的PRED_SPLIT中;(m_modeDepth[Depth].pred[PRED_SPLIT]->cu);将子CU的率失真损失添加到父CU所在深度的PRED_SPLIT中,并且将子CU最佳模式的重建YUV数据保存到父CU所在深度的PRED_SPLIT中。

    splitCU->copyPartFrom(nd.bestMode->cu, childGeom, subPartIdx);
    splitPred->addSubCosts(*nd.bestMode);
    nd.bestMode->reconYuv.copyToPartYuv(splitPred->reconYuv, childGeom.numPartitions * subPartIdx);
    nextContext = &nd.bestMode->contexts;
    

如果当前父CU的位置完全越过帧的边界,或者子CU不存在,则应该配置子CU的深度和尺寸(log2Size),如果存在可用的模式集合,则将不存在的CU深度设置为0,以确保获取正确的CU作为编码delta QP的参考。(在前面的深度检查中会用到,用来决定bDecidedDepth标识位,但是这里我还是很模糊,需要研究x265的量化参数设置后再详细说)

4. 更新Best mode
4.1 比较md.pred[PRED_INTRA]md.pred[PRED_SPLIT], 更新md.bestMode

这里判断是否提前终止,即如果已经计算出的子CU的累计率失真大于划分前的率失真损失,则可以提前退出;否则需要保存上下文信息,判断split Flag是否应添加到率失真损失中,然后比较md.pred[PRED_INTRA].rdCostmd.pred[PRED_SPLIT].rdCost, 更新md.bestMode

if (!skipSplitCheck)
{
    nextContext->store(splitPred->contexts);
    if (mightNotSplit)
        addSplitFlagCost(*splitPred, cuGeom.depth);
    else
        updateModeCost(*splitPred);

    checkDQPForSplitPred(*splitPred, cuGeom);
    checkBestMode(*splitPred, depth); // 更新md.bestMode
}

5. 保存并返回结果

5.1 率失真精细化、更新TU划分深度、
// 只有在RD 5/6中才开启率失真精细化,默认关闭
if (m_param->bEnableRdRefine && depth <= m_slice->m_pps->maxCuDQPDepth)
{
    int cuIdx = (cuGeom.childOffset - 1) / 3;
    cacheCost[cuIdx] = md.bestMode->rdCost;
}

// 更新了ctu中的第0、1、2层CU的TU depth, 其值等于CU所含所有的TU的最大深度
// #define X265_MAX(a, b) ((a) > (b) ? (a) : (b))
// a maxTUDepth
// b md.bestMode->cu.m_tuDepth[i]
if ((m_limitTU & X265_TU_LIMIT_NEIGH) && cuGeom.log2CUSize >= 4)
{
    CUData* ctu = md.bestMode->cu.m_encData->getPicCTU(parentCTU.m_cuAddr);
    // 获取当前父CU的最大TU深度
    int8_t maxTUDepth = -1;
    for (uint32_t i = 0; i < cuGeom.numPartitions; i++)
        maxTUDepth = X265_MAX(maxTUDepth, md.bestMode->cu.m_tuDepth[i]); // maxTUDepth取两者的最大值
    // int8_t m_refTuDepth[NUM_TU_DEPTH];  
    // TU depth of CU at depths 0, 1 and 2
    ctu->m_refTuDepth[cuGeom.geomRecurId] = maxTUDepth;
}

5.2 复制预测数据和重建数据到CTU和frame,返回结果
/* Copy best data to encData CTU and recon */
// 复制当前CU到CTU和帧级别的重建图像中
// Copy completed predicted CU to CTU in picture 
md.bestMode->cu.copyToPic(depth);
// 如果该CU没有划分,则将其重建YUV数据保存到帧级别的重建图像m_frame->m_reconPic中
if (md.bestMode != &md.pred[PRED_SPLIT])
    md.bestMode->reconYuv.copyToPicYuv(*m_frame->m_reconPic, parentCTU.m_cuAddr, cuGeom.absPartIdx);
// 返回最佳预测的率失真损失
return md.bestMode->rdCost; 

流程图

在这里插入图片描述

参考文献

[1] x265 Documentation——master

[2] x265中compressIntraCU()分析(版本2.8) - 灰信网

[3] HEVC系列笔记 - FindHao

[4] csdn_x265代码结构之模式选择compressCTU

[5] x265: 16e2f763517c (videolan.org)

[6] x265 compressCTU流程图参考来源_csdn

  • 9
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值