x264 RC - RateControl 詳解


x264 RC - RateControl 詳解

RC(RateControl):是一個x264的抽象層,提供了一種視頻碼流的控制方法。

  • HRD:hypothetical reference decoder (假定參考解碼器) 。因為x264只負責編碼,沒有解碼模塊,而x264在編碼的時候又需要考慮到解碼的一些設置,比如bitrate的控制,幀的延遲等等,所以x264就創建了一個“假的”解碼器,來作為編碼時候的參考。它主要記錄了比特流(bitrate),編碼緩沖區大小(Code Picture Buffer -- cpb),

  • VBV:Video buffering verifier,中文都叫它“視頻緩沖區驗證”,直白的定義應該是:和HRD一樣,它也是個“假想的”視頻編碼緩沖區,用來模擬真實的內存緩沖,以控制編碼輸出的碼流大小。


RC main flow

一幀(Frame)進行編碼的時候,是如何與VBV關聯的:

vbv_max_rate:輸出碼流的比特率

VBV相當於一個虛假的內存緩沖,它的總大小是vbv_buffer_size,剩余大小是buffer_fill_final。如果有一幀(Frame)被編碼進來,那么這個緩沖區的剩余大小buffer_fill_final,會有兩次計算:

buffer_fill_final -= Frame.bits       
buffer_fill_final += Frame.buffer_rate

VBV的狀態是動態的,當Frame被編碼進來的時候,消耗了一定bits的內存來存儲它。但在Frame存在的這段時間time內,又有bitrate * time的bits被傳輸走了。

了解這個模型,應該就明白RC是怎么回事了,它確實不包含任何真正的內存,僅僅是一種參考。

RC lookahead

然而大家最關心的,一定是:在碼流控制的時候,QP是怎么選取的。

在進一步深入之前,先仔細想一想,若是讓你來親自實現這個模塊,大概是這樣一種:

事實上這個模型一點問題也沒有,只是在具體實現的時候太粗暴了,因為每次重新選取一個QP,就需要把整個Frame重新編碼,會消耗大量的計算資源。

x264 RC的實現方式,是把Frame拆成一行一行的MB,每次編碼一行MB,如果編碼出來的太大或者太小,就調整QP,然后重新編碼。這樣做雖好,但如果初始化的QP選擇的不正確,還是會耗費了大量的運算資源一遍一遍的去試探,那有沒有一種辦法從一開始就選出合適的QP?

於是就有了lookahead運算,通過預測,提前得到一個合適的QP,這樣可以有效的減少x264在正式編碼階段反復試探的消耗。

x264會編碼出的I幀,P幀,B幀三種類型,I幀的size往往很大,P幀次之,B幀則很小。這里又衍生出另一個問題,在碼流固定的情況下,怎么才能讓B幀把自己用不掉的碼流分配給I幀,否則會出現B幀的VBV資源過剩,而I幀的VBV資源很緊張的情況。

這里x264采用的平衡方式是,在lookahead階段,不是預測一幀的大小,而是預測一組Frame的大小,它試圖平衡足夠長的Frame序列的碼流,而不是一幀一幀的處理。

lookahead流程圖如下:

具體實現過程:利用lookahead階段計算的SATD值,預測出一組Frame的大小,讓這一組Frame編碼完畢之后,VBV的buffer_fill_final控制在50%-80%的范圍之內。它嘗試平衡的是一組Frame的碼流,這樣的話,I幀和B幀就能達到一種資源利用的平衡了。

  • 關於為什么要把buffer_fill_final控制在50%-80%內。

    B幀引用了I幀和P幀,受到I幀和P幀精度的影響,所以B幀的QP值必須比引用的I幀和P幀大(或者說,即便比I幀和P幀小,B幀也不會獲得更好的顯示圖像)。基於這個事實,在lookahead預測的時候,B幀是沒必要進行預測的,只對I幀和P幀進行預測。所以lookahead-RC預測完畢之后,緊接着就要開始編碼P幀或者I幀了。

    由於I幀和P幀編碼之后size都比較大,所以才需要預留足夠的VBV空間。(這個預留的空間並不是給當前一組幀,而是預留給下一組幀的。)

在經過lookahead之后,我們就得到了一個初始的、比較合適的QP,這個QP能保證接下來一系列的編碼Frame,整體的碼流是合適的。

既然有了合適的QP,那么某一幀在編碼的時候,我們就計划給它分配一定的VBV大小:frame_size_planned

frame_size_planned = predict_size( &rcc->pred[h->sh.i_type], q, rcc->last_satd );

predict_size()就是x264中所采用的的預測函數,它的輸入參數有三個:幀的類型,QP,幀的SATD

只要每一幀的實際編碼大小盡量往frame_size_planned上面靠攏,那么整體的碼流就是我們期望的那樣。

RC 正式編碼階段

但預測得到的frame_size_planned畢竟只是一種預測,實際編碼出來的大小,仍舊有出入。這時候,對於單個幀的編碼,就需要一行一行的微調了。

x264采用的手段,是把Frame分割成一行一行的MB,每編碼一行的MB,就判斷一次。如果發現編碼出的大小不合適,那么它就會通過降低\提高下一行MB的QP,盡量往frame_size_planned上去靠攏。(在某些極端的情況下,可能會導致已經編碼的一行完全報廢掉,並且重頭開始編碼,這種情況會導致計算量暴增。)

正式編碼階段,要考慮到多線程Frame編碼的並行情況,所以每個編碼線程都有一個獨立的context,標注在下圖中:

每個thread維護獨立的context變量(在x264_ratecontrol_t結構體當中):

  • buffer_fill:當前線程可以使用的VBV空間大小

  • rc_tol: 當前Frame編碼之后的實際大小和frame_size_planned的最大差距。其實就是實際大小---預測大小之間的差別容忍度。

  • frame_size_planned:在lookahead階段預測的size,也是我們期望的size,最后的實際碼流越靠近這個值,那么整體的碼流就越穩定。

  • frame_size_estimated: 這是一個動態變量,因為x264每編碼一行MB的數據就要重新評估當前的QP是否需要微調,所以這個值是不斷變化的。它= 已經編碼的MB + 還未編碼MB的預測值。x264用這個值來和frame_size_planned比較,如果離frame_size_planned差距較大,那么x264就會調整下一行MB的QP,以期望接近frame_size_planned的大小。

  • buffer_rate:等於Frame停留在屏幕上的時間 * bitrate。VBV的是一種動態模型,一邊有Frame輸入進來,一邊還會把數據通過網絡發送出去。當Frame進來編碼的時候,會占據Frame.bits的空間,同時VBV也會傳輸出去Frame.buffer_rate大小的數據。

通過這樣一種動態模型,基本上就實現了碼流的平穩分配。

x264 RC函數導圖

讓我們來看看,上面說的一些策略分部在x264源代碼的哪些地方:

額外的一些細節:

RC中采用的預測模型

RC中根據frame->lowres的SATD值來預測出實際編碼bits的大小,因為SATD和bits存在一種線性關系,所以可以用如下公式來表達:

\[bits = SATD*k+b \]

在x264中,采用的結構如下:

typedef struct
{
    float coeff_min;    //最小的k系數,防止k過小
    float coeff;        //對應上面公式中的系數k
    float count;        //count是一種累計的記錄,因為這里的k和b是動態調整的,需要一邊編碼,一邊用實際bits更新預測參數
    float decay;        //decay是衰減系數,默認是0.5,代表舊的數據占比重只有0.5
    float offset;       //對應公式中的常量系數b
} predictor_t;

預測函數的代碼如下:

static float predict_size( predictor_t *p, float q, float var )
{
    //這里的var就是SATD
    //參數q代表QP的QStep
    return (p->coeff*var + p->offset) / (q*p->count);
}

幾個predict結構:

RC相關的基礎變量:

RateControl的幾種模式:

X264_RC_CQP:恆定質量(constant QP),說白了就是固定了畫面的QP值,所有Frame根據類型設定固定的QP值,也不需要做上面提到的繁雜的lookahead預測和計算。

    rc->qp_constant[SLICE_TYPE_P] = h->param.rc.i_qp_constant;
    rc->qp_constant[SLICE_TYPE_I] = x264_clip3( h->param.rc.i_qp_constant - rc->ip_offset + 0.5, 0, QP_MAX );
    rc->qp_constant[SLICE_TYPE_B] = x264_clip3( h->param.rc.i_qp_constant + rc->pb_offset + 0.5, 0, QP_MAX );

X264_RC_ABR(adaptive bitrate)X264_RC_CRF(constant rate factor)

這兩個模式,在x264中的實現有一點奇怪,二者都是在get_qscale中獲取初始的QP,然后把QP傳遞給了clip_qscale函數做后期調整。但問題是就出在clip_qscale這個后期處理函數上。clip_qscale這個函數才不管你是X264_RC_ABR還是X264_RC_CRF,它只是單純的根據VBV來調整一組Frame的大小,使得這一組Frame的總共輸入、輸出的總和是0(真實情況不可能是0,但程序設計上,最完美的當然是輸入和輸出完美平衡,這樣碼流才足夠的穩定),故而不管get_qscale中以怎樣的策略選出初始的QP,一旦走到clip_qscale函數中,二者的值都會被調整的十分接近,甚至變成同一個值。

所以如果想要這兩種模式有區別,只能是關閉VBV的情況下才能體現出來。

  • X264_RC_ABR

    ​ 我先要說的是,這是個古怪的模式。因為在x264的代碼中,你會看到X264_RC_ABR的QP調整策略是:根據當前使用掉的bits數目和最大碼流的比例,進而來調整QP,簡單來說,如果當前輸出的碼流比你期望的碼流低,那就降低QP,把剩余的碼流利用起來。而如果當前碼流過高,就提高QP的值。

    ​ 看起來一點毛病也沒有,甚至有平穩碼流的目的,可實際造成的結果卻是造成了碼流波動的更加劇烈,一點也不平穩。具體的原因是,x264在編碼的時候,會輸出類似這種幀序列:I、B、B、B、P、B.....,其中I幀的size非常大,遠遠比B和P幀要大許多。這就出現了一種奇怪的現象,在編碼I幀之后,使用的bits會很多,這時候x264就會嘗試提高QP,可這個提高的QP並沒有作用到I幀身上,而是作用到了接下來幾個倒霉的P幀身上(B幀不會預測QP,而是從ref幀上面繼承QP)。然后,因為P幀的size很小,在編碼了一串B和P幀之后,剛好碰到了I幀,而這個時候因為之前B、P幀編碼很小,所以QP又被x264降低了很多,這就進一步抬高了I幀的size。

    ​ 最后導致的結果,就是I幀變得比平時更大,而P幀本來就很小了但卻變得比平時更小,出現了一種極端化的現象,進而導致了碼流波動劇烈。(這種設計,又是否是x264想要的結果?或者設計程序的人也沒預料到會這樣吧……)

  • X264_RC_CRF:

​ Constant rate factor,意思是每一幀的QP都用一個固定的比例值factor來修正(修正的參數由用戶來指定,算是一種宏觀調整QP的策略),是個中規中矩的模式,只是它並不考慮“想辦法利用所有的網絡傳輸碼流”這件事。

  1. 比起X264_RC_CQP(constant QP)X264_RC_CRF會兼顧SATD值,期望每一幀都得到公平的碼流分配,但也僅僅是希望大家公平而已,X264_RC_CRF並不在乎網絡傳輸碼流有沒有被有效利用起來。

    就好比家中有一大袋米,幾個兄弟(幀)來分配,於是X264_RC_CRF給每個兄弟抓了一把,然后告訴眾兄弟:“去吃吧,大家都是一樣的數量。”眾兄弟已經快要餓死,看着剩下的一大袋米沒人食用,不由得露出一副莫名之色……

  2. X264_RC_ABR則一個“試圖利用起所有的網絡傳輸碼流,但卻把一切弄得很不穩定”的家伙。

也正是因為這樣,所以X264_RC_ABR(adaptive bitrate)X264_RC_CRF(constant rate factor)這兩種模式的后端,都緊跟着一個clip_qscale(不管什么模式,都試圖利用所有網絡傳輸碼流的函數),只不過當clip_qscale起作用的時候,選擇哪一種模式似乎也並不重要了。

后記,關於預測模型的一些細節

在這里要重點說的,是兩個參數,也是初學者容易迷惑的兩個東西。

h->param.rc.f_ip_factor  // 從I幀的QStep值,得到成P幀QStep值的一個比例參數,QStep = qp2qscale(QP)
h->param.rc.f_pb_factor  // 從P幀的QStep值,得到成B幀QStep值的一個比例參數,QStep = qp2qscale(QP)

初看代碼的人,最容易被這兩個東西搞迷糊了:怎么I幀和P幀、B幀之間的關系是固定的?那既然這樣,只需要得到第一個I幀的QP值,就可以把后面的QP值一起都算出來了,還搞那么多預測模型干什么?

這么理解就錯了,因為這兩個參數是為了預測模型服務的,並非用來確定正式的QP值。

  • 針對B幀,因為B幀不會進行預測QP的分析,所以可以用上面的比例值,從相鄰的P幀或者I幀換算出來,但也僅僅是B幀。

  • 可對於I、P兩種類型,它們都是要進行lookahead預測的,需要把當前幀的size、后續的一串幀的size都累加起來,來評估大小是否合適。但顯然這些幀的size我們並不知道,只能估算幀的大小。按照上面的提到的predict_size函數,是需要知道QP值的。於是我們就按照這個比例關系,按照幀的類型給后續的幀分別賦予一定的QP,然后就能順利的預測出一組Frame的大小了。

這兩個值為預測模型服務,然后預測模型給每一個被預測的幀算出frame_size_planned,最后frame_size_planned會作用到正式編碼階段,進而影響到實際的編碼大小。故而這兩個值是一種期望,期望最后的QStep比例關系是這樣的,但最后的實際QP值還需要根據具體的Frame進行調整。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM