x264、x265中cuTree原理分析

    mbtree是x264中引入的一項創新性技術,可以有效提高主客觀質量(參考文章最後的表格1)。x265繼承了這一算法,改名爲cuTree,算法本身實現較爲複雜,下面探討一下cutree原理,結合代碼來分析實現細節。

    cutree和mbtree都是根據當前塊被參考的程度調整qpOffset,要知道當前塊被參考的程度,很顯然需要一個編碼的反推過程

    對於幀間參考,參考幀的質量顯然對當前幀質量有直接影響。即參考塊的編碼代價,除了要考慮本身的編碼代價外,還需考慮對將來參考到當前塊的那些塊的影響力。因此,cutree在分析每個塊的Cost時,引入了一個PropagateInCost的概念:即每個塊的Cost,不僅是自己本身編碼的Cost,還要加上後續塊依賴於當前塊的Cost,這個Cost稱之爲PropagateInCost,所以關鍵是如何確定PropagateInCost。

    考慮以下簡化情形:假設B塊完全參考了A塊,B塊幀內幀間預測分別爲IntraCostB和InterCostB。分析趨勢:如果IntraCostB與InterCostB差不多大,說明B塊從A塊獲取的信息量很少;反之,如果IntraCostB比InterCostB大很多,說明B塊大部分信息可以從A塊獲取。基於這個思想,B塊本身從A塊獲取的信息量可以表達爲:(IntraCostB - InterCostB) 。進一步考慮,B塊也被其他塊參考了,所以B塊的Cost也包含了PropagateInCostB。綜上:B塊依賴於A塊的Cost爲:

    (IntraCostB - InterCostB) + PropagateInCostB * (IntraCostB - InterCostB) / IntraCostB = (1 + PropagateInCostB) * (IntraCostB - InterCostB) / IntraCostB

    其中:(IntraCostB - InterCostB) / IntraCostB表示B塊的PropagateInCostB有多少比例要傳遞到A塊。

    如下是x265計算PropagateCost的函數,其基本思想就是上面所述。

/* Estimate the total amount of influence on future quality that could be had if we
 * were to improve the reference samples used to inter predict any given CU. */
static void estimateCUPropagateCost(int* dst, const uint16_t* propagateIn, const int32_t* intraCosts, const uint16_t* interCosts, const int32_t* invQscales, const double* fpsFactor, int len)
{
    double fps = *fpsFactor / 256;  // range[0.01, 1.00]
    for (int i = 0; i < len; i++)
    {
        int intraCost = intraCosts[i];
        int interCost = X265_MIN(intraCosts[i], interCosts[i] & LOWRES_COST_MASK);
        double propagateIntra = intraCost * invQscales[i]; // Q16 x Q8.8 = Q24.8
        double propagateAmount = (double)propagateIn[i] + propagateIntra * fps; // Q16.0 + Q24.8 x Q0.x = Q25.0
        double propagateNum = (double)(intraCost - interCost); // Q32 - Q32 = Q33.0
        double propagateDenom = (double)intraCost;             // Q32
        dst[i] = (int)(propagateAmount * propagateNum / propagateDenom + 0.5);
    }
}

    如前所述,B塊完全參考A塊,則A塊的PropagateCostInA = (1 + PropagateInCostB) * (IntraCostB - InterCostB) / IntraCostB

    考慮更復雜的情況,由於MV不可能都指向一個完整的編碼塊,所以B塊的PropateCostB在參考幀中要被按比例地加入到對應的參考塊中。如下爲x265的cutree函數:

void Lookahead::estimateCUPropagate(Lowres **frames, double averageDuration, int p0, int p1, int b, int referenced)
{
    uint16_t *refCosts[2] = { frames[p0]->propagateCost, frames[p1]->propagateCost };
    int32_t distScaleFactor = (((b - p0) << 8) + ((p1 - p0) >> 1)) / (p1 - p0);
    int32_t bipredWeight = m_param->bEnableWeightedBiPred ? 64 - (distScaleFactor >> 2) : 32;
    int32_t bipredWeights[2] = { bipredWeight, 64 - bipredWeight };
    int listDist[2] = { b - p0 - 1, p1 - b - 1 };

    memset(m_scratch, 0, m_8x8Width * sizeof(int));

    uint16_t *propagateCost = frames[b]->propagateCost;

    x265_emms();
    double fpsFactor = CLIP_DURATION((double)m_param->fpsDenom / m_param->fpsNum) / CLIP_DURATION(averageDuration);

    /* For non-referred frames the source costs are always zero, so just memset one row and re-use it. */
    if (!referenced)
        memset(frames[b]->propagateCost, 0, m_8x8Width * sizeof(uint16_t));

    int32_t strideInCU = m_8x8Width;
    for (uint16_t blocky = 0; blocky < m_8x8Height; blocky++)
    {
        int cuIndex = blocky * strideInCU;
        // 計算frames[b]每個塊的PropagateInCost,結果存儲到m_scratch中
        if (m_param->rc.qgSize == 8)
            primitives.propagateCost(m_scratch, propagateCost,
                       frames[b]->intraCost + cuIndex, frames[b]->lowresCosts[b - p0][p1 - b] + cuIndex,
                       frames[b]->invQscaleFactor8x8 + cuIndex, &fpsFactor, m_8x8Width);
        else
            primitives.propagateCost(m_scratch, propagateCost,
                       frames[b]->intraCost + cuIndex, frames[b]->lowresCosts[b - p0][p1 - b] + cuIndex,
                       frames[b]->invQscaleFactor + cuIndex, &fpsFactor, m_8x8Width);

        if (referenced)
            propagateCost += m_8x8Width;

        // 將frames[b]中的PropagateInCost 按比例加到參考幀中每個塊裏
        for (uint16_t blockx = 0; blockx < m_8x8Width; blockx++, cuIndex++)
        {
            int32_t propagate_amount = m_scratch[blockx];
            /* Don't propagate for an intra block. */
            if (propagate_amount > 0)
            {
                /* Access width-2 bitfield. */
                int32_t lists_used = frames[b]->lowresCosts[b - p0][p1 - b][cuIndex] >> LOWRES_COST_SHIFT;
                /* Follow the MVs to the previous frame(s). */
                for (uint16_t list = 0; list < 2; list++)
                {
                    if ((lists_used >> list) & 1)
                    {
#define CLIP_ADD(s, x) (s) = (uint16_t)X265_MIN((s) + (x), (1 << 16) - 1)
                        int32_t listamount = propagate_amount;
                        /* Apply bipred weighting. */
                        if (lists_used == 3)
                            listamount = (listamount * bipredWeights[list] + 32) >> 6;

                        MV *mvs = frames[b]->lowresMvs[list][listDist[list]];

                        /* Early termination for simple case of mv0. */
                        // MV(0, 0),直接加到參考幀的PropateCost數組中
                        if (!mvs[cuIndex].word)
                        {
                            CLIP_ADD(refCosts[list][cuIndex], listamount);
                            continue;
                        }

                        // MV不爲(0, 0)時,參考塊爲四個塊的子區域,分別爲idx0, idx1, idx2, idx3,比例爲idx0weight, idx1weight, idx2weidht, idx3weidht
                        int32_t x = mvs[cuIndex].x;
                        int32_t y = mvs[cuIndex].y;
                        int32_t cux = (x >> 5) + blockx;
                        int32_t cuy = (y >> 5) + blocky;
                        int32_t idx0 = cux + cuy * strideInCU;
                        int32_t idx1 = idx0 + 1;
                        int32_t idx2 = idx0 + strideInCU;
                        int32_t idx3 = idx0 + strideInCU + 1;
                        x &= 31;
                        y &= 31;
                        int32_t idx0weight = (32 - y) * (32 - x);
                        int32_t idx1weight = (32 - y) * x;
                        int32_t idx2weight = y * (32 - x);
                        int32_t idx3weight = y * x;

                        /* We could just clip the MVs, but pixels that lie outside the frame probably shouldn't
                         * be counted. */
                        if (cux < m_8x8Width - 1 && cuy < m_8x8Height - 1 && cux >= 0 && cuy >= 0)
                        {
                            CLIP_ADD(refCosts[list][idx0], (listamount * idx0weight + 512) >> 10);
                            CLIP_ADD(refCosts[list][idx1], (listamount * idx1weight + 512) >> 10);
                            CLIP_ADD(refCosts[list][idx2], (listamount * idx2weight + 512) >> 10);
                            CLIP_ADD(refCosts[list][idx3], (listamount * idx3weight + 512) >> 10);
                        }
                        else /* Check offsets individually */
                        {
                            if (cux < m_8x8Width && cuy < m_8x8Height && cux >= 0 && cuy >= 0)
                                CLIP_ADD(refCosts[list][idx0], (listamount * idx0weight + 512) >> 10);
                            if (cux + 1 < m_8x8Width && cuy < m_8x8Height && cux + 1 >= 0 && cuy >= 0)
                                CLIP_ADD(refCosts[list][idx1], (listamount * idx1weight + 512) >> 10);
                            if (cux < m_8x8Width && cuy + 1 < m_8x8Height && cux >= 0 && cuy + 1 >= 0)
                                CLIP_ADD(refCosts[list][idx2], (listamount * idx2weight + 512) >> 10);
                            if (cux + 1 < m_8x8Width && cuy + 1 < m_8x8Height && cux + 1 >= 0 && cuy + 1 >= 0)
                                CLIP_ADD(refCosts[list][idx3], (listamount * idx3weight + 512) >> 10);
                        }
                    }
                }
            }
        }
    }

    if (m_param->rc.vbvBufferSize && m_param->lookaheadDepth && referenced)
        cuTreeFinish(frames[b], averageDuration, b == p1 ? b - p0 : 0);
}

    最後,當前Cu的QPOffset肯定是與PropagateInCost有關的,PropagateInCost越大,則CU的qp應該越小,QPOffset是負值,也應該越小,x265中cutree的QPOffset = -strength * log2(1 + PropagateInCost / IntraCost),具體代碼,參考函數cuTreeFinish,如下所示。

void Lookahead::cuTreeFinish(Lowres *frame, double averageDuration, int ref0Distance)
{
    int fpsFactor = (int)(CLIP_DURATION(averageDuration) / CLIP_DURATION((double)m_param->fpsDenom / m_param->fpsNum) * 256);
    double weightdelta = 0.0;

    if (ref0Distance && frame->weightedCostDelta[ref0Distance - 1] > 0)
        weightdelta = (1.0 - frame->weightedCostDelta[ref0Distance - 1]);

    frame->qpAvgFrmCuTreeOffset = 0.0;
    for (int cuIndex = 0; cuIndex < m_cuCount; cuIndex++)
    {
        int intracost = (frame->intraCost[cuIndex] * frame->invQscaleFactor[cuIndex] + 128) >> 8;
        if (intracost)
        {
            int propagateCost = (frame->propagateCost[cuIndex] * fpsFactor + 128) >> 8;
            double log2_ratio = X265_LOG2(intracost + propagateCost) - X265_LOG2(intracost) + weightdelta;
            frame->qpCuTreeOffset[cuIndex] = frame->qpAqOffset[cuIndex] - m_cuTreeStrength * log2_ratio;
            frame->qpAvgFrmCuTreeOffset += frame->qpCuTreeOffset[cuIndex];
        }
    }
    frame->qpAvgFrmCuTreeOffset /= m_cuCount;
}

   下面表1、表2爲x265 v2.4版本中,cutree對編碼客觀質量的影響,編碼配置爲:preset=medium, ratecontrol=ABR,BFrames = 3(or = 0),aq-mode=off,測試序列爲HEVC中的Class B(1080p)。當BFrames=3時,cuTree開啓後,Y的bitrate節省6.52%,U的碼率節省15.38%,V的碼率節省15.56%,壓縮效率提升非常明顯。當BFrames=0時,cuTree開啓後,Y的bitrate增加0.72%,U的碼率節省2.4%,V的碼率節省1.4%,壓縮效率沒什麼提升。這是因爲BFrames=3時,CuTree對I和P,QP調小的幅度大,對B-Ref,QP適當調小,對B-Non-Ref,QP不做調整,本質與HM中的Hierarchichal QP差不多;當BFrames=0時,所有P幀的QP都被調小,幅度都差不多,這樣其實相當於沒有調整QP了。

                                                              表1、x265中cuTree對編碼碼率的節省(BFrames=3)

Sequence BD-Rate Y BD-Rate U BD-Rate V
BasketballDrive -4.8% -8.5% -3.5%
Bqterrace -3.5% -19.2% -21.3%
Cactus -9.6% -15.9% -13.9%
Kimono -3.3% -13.4% -17.1%
ParkScene -11.4% -19.9% -22.0%
Average -6.52% -15.38% -15.56%

                                                              表2、x265中cuTree對編碼碼率的節省(BFrames=0)

Sequence BD-Rate Y BD-Rate U BD-Rate V
BasketballDrive 2.4% 2.3% 4.9%
Bqterrace 4.1% -0.9% 0.8%
Cactus -1.8% -4.6% -2.3%
Kimono 2.3% -0.6% -2.7%
ParkScene -3.4% -8.2% -7.7%
Average 0.72% -2.4% -1.4%

    需要注意一點:如上所述,cuTree是從後往前推導,求qpOffset。x264、x265在開啓碼控時,會啓用lookahead機制,所謂lookahead機制就是從當前幀往後看,根據後續幀的情況,給當前幀分配合適的QP,確定合適的幀類型等。代碼中,cuTree往後看的幀數就等於lookahead_num的值。比如對x265的Preset Medium,lookahead_num默認爲20,則cuTree會從當前幀之後第20幀開始往前推導,一直到當前幀,算出qpOffset,所以lookahead_num會對cuTree的結果有直接影響:不同lookahead_num,cuTree的QPOffset的值也稍有不同,但是影響不算很大。

    此外,還需要注意,x265的幀型決策以及cuTree的QPOffset的確定過程都是以MiniGop爲單位的,即每次爲一個MiniGop確定好編碼所需的參數。因此,3個B幀的情況,每4幀(bBbP)調用一次cuTree過程。而0個B幀時,則每個P幀都要調用一次cuTree過程。cuTree每次要反推20(lookahead_num)幀,計算量很可觀。所以對超高分辨率編碼時,有時0B反而比3B更慢,問題很可能出於此。

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