1. 程式人生 > >x264裡的2pass指的是什麼意思 x264原始碼分析2 encode

x264裡的2pass指的是什麼意思 x264原始碼分析2 encode

               

A:x264裡的2pass指的是什麼意思?另外stat是什麼意思, 比如有個引數--stats <string>        Filename for 2 pass stats [/"%s/"]/n", defaults->rc.psz_stat_out );
stats在這是什麼意思?


2pass是2次編碼的意思,stats是統計文件的名稱,記錄了1pass中的資訊.2次編碼就相當於2次轉換這麼做雖然轉換時間會漫長,但壓出的片子會有更好的畫質,畫面細節更好,而且體積會更小

2-pass主要是針對非實時的視訊編碼(如檔案)來說的,通俗的說就是需要二次編碼,第一次編碼是先對整個檔案進行掃描,記錄一些統計資訊,第二次編碼時根據前面記錄的統計資訊再進行編碼,這樣的好處是可以提高編碼的質量。

A:那個達人能簡單說說x264中 rc 部分 1pass 和 2pass的思路?

就此問題在 x264 開發郵件列表裡問過。沒有得到一個明確的結論。
我看程式碼的過程中,首先關注的是 1pass ,得到的一點經驗大概可以這樣描述:
首先 x264 中將 QP 通過一個確定的公式變換為 QScale 。其主要就是針對於 QS 作分析和控制/約束變化的
x264 的位元速率控制介面是以幀為單位的,而支援的巨集塊級變化只到巨集塊行
編碼一個巨集塊行之前呼叫介面函式以得到欲使用的 QP 。
它預測某個 gomb (巨集塊組) 的耗費碼流的大小,利用在漏桶模型上,以之權衡 QP 是否需要改變並如何進行改變。
預測的時候是使用兩種預測求平均

的方法,見 predict_row_size() 函式。
它的預測方程一個比較引起混淆的是它對歷史資料有個衰減的過程。
而且,這些衰減概念,應用在不同地方所用的幾個方程上。
它的 RC 核心函式是 rate_estimate_qscale() 。
其中一個 QS 的推導是一個需要解析的輸入表示式字串,也因此在內部實現了一個表示式解析器,也許會產生一點晦澀感。
一時就想到這些。

E.      然後進入x264_encoder_encode( h, &nal, &i_nal, pic )函式,該函式定義在/Enc/encoder.c中.

開始進入比較複雜的地方了.

這個函式前面有一段註釋(如下):

****************************************************************************

* x264_encoder_encode:

* XXX: i_poc   : is the poc of the current given picture

*       i_frame : is the number of the frame being coded

* ex: type frame poc

*       I      0   2*0//poc是實際的幀的位置.

*       P      1   2*3//frame是編碼的順序.

*       B      2   2*1

*       B      3   2*2

*       P      4   2*6

*       B      5   2*4

*       B      6   2*5

****************************************************************************/

要搞清poc和frame的區別.

假設一個視訊序列如下:

I B    B    P    B     B     P

我們編碼是按I P B B P B B的順序,這就是frame的編號.

而我們視訊序列的播放序號是POC的序號,這裡是乘以了2.

函式中先定義瞭如下三個引數:

int     i_nal_type;

nal存放的資料型別, 可以是sps,pps等多種.                  

int     i_nal_ref_idc;

nal的優先順序,nal重要性的標誌位.

前面兩個引數雖然簡單,但如果不參照標準,也不容易理解,所以標準中的句法表是很重要的,可以說是最關鍵的.

int     i_slice_type;

slice的型別,在x264中我的感覺好像一幀只有一個slice.如果確定了幀的型別,slice的型別也就確定了.

我們來看看編碼器是如何區分讀入的一幀是I幀,P幀,或者B幀,這個過程需要好好理解.

還以I       B B P B   B     P為例.

if( h->i_frame % (h->param.i_iframe * h->param.i_idrframe) == 0 ){

確定這是立即重新整理片.

}

         這裡很好理解.

但到了if( h->param.i_bframe > 0 )//可以B幀編碼時.

就有問題了.

注意我們編完I幀後碰到了一個B幀,這時我們先不對它進編碼.而是採用frame = x264_encoder_frame_put_from_picture( h, h->frame_next, pic )函式將這個B幀放進h->frame_next中.

好,這裡出現了h->frame_next,在h中同時定義了下面幾個幀陣列用以實現幀的管理.

x264_frame_t   *bframe_current[X264_BFRAME_MAX]; /* store the sequence of b frame being encoded */

    x264_frame_t    *frame_next[X264_BFRAME_MAX+1];   /* store the next sequence of frames to be encoded *///搞清意義,下一個幀,而不一定是B幀.

    x264_frame_t    *frame_unused[X264_BFRAME_MAX+1]; /* store unused frames */

注意區分這3個數組.

同時還有下面4個函式(定義在/ENCODER/encoder.c中).

x264_encoder_frame_put_from_picture();

x264_encoder_frame_put();

x264_encoder_frame_get();

x264_frame_copy_picture();

這3個數組和4個函式可以說完成了整個幀的型別的判定問題.這個裡面if ,else語句較多,容易使人迷惑.但我們只要把握下面一個觀點就可以看清實質:在不對P幀進行編碼之前,我們不對B幀進行編碼,只是把B幀放進緩衝區(就是前面提到的陣列).

比如視訊序列:I     B B P B B P

先確立第一個幀的型別,然後進行編碼.然後是2個B幀,我們把它放進緩衝區陣列.然後是P幀,我們可以判定它的型別並進行編碼.同時,我們將緩衝區的B幀放進h->bframe_current[i],不過這時P幀前的兩個B幀並沒有編碼.當讀到P幀後面的第一個B幀時,我們實際上才將h->bframe_current陣列中的第一個B幀編碼,也就是將在I幀後面的第一個B幀(說成P幀前面的第一個B幀容易誤解J)編碼.

依此類推,把握好上面4個函式的呼叫流程和指標操作的用法,就可以將幀的型別判定這個問題搞明白了.

F.      然後是速率控制(先不說這個,因為它對編碼的流程影響不大),看看建立參考幀列表的操作,也就是

x264_reference_build_list( h, h->fdec->i_poc ); (定義在/ENCODER/encoder.c中).

光看這個函式是不行的,它是和後面的這個函式(如下)一起配合工作的.

if( i_nal_ref_idc != NAL_PRIORITY_DISPOSABLE )//B幀時.

    {

        x264_reference_update( h );

}

     If條件是判斷當前幀是否是B幀,如果是的話就不更新參考列表,因為B幀本來就不能作為參考幀嘛!如果是I幀或P幀的話,我們就更新參考幀列表.

我們看到了一個for迴圈,兩個do—while迴圈.這是實現的關鍵,具體看程式碼,不好用語言說明白.

G.     進入另一個複雜的領域:寫slice的操作,剛開使挺簡單,如我下面的註釋.

/* ---------------------- Write the bitstream -------------------------- */

    /* Init bitstream context */

    h->out.i_nal = 0;//out的宣告在bs.h中.

    bs_init( &h->out.bs, h->out.p_bitstream, h->out.i_bitstream );//空出8位.

    /* Write SPS and PPS */

    if( i_nal_type == NAL_SLICE_IDR )//不是每次都要寫SPS and PPS,只有碰見立即重新整理片時才寫.

    {

        /* generate sequence parameters */

        x264_nal_start( h, NAL_SPS, NAL_PRIORITY_HIGHEST );

        x264_sps_write( &h->out.bs, h->sps );

        x264_nal_end( h );

        /* generate picture parameters */

        x264_nal_start( h, NAL_PPS, NAL_PRIORITY_HIGHEST );

        x264_pps_write( &h->out.bs, h->pps );

        x264_nal_end( h );

}

不過看下面那個函式(就進入了複雜的領域).

H.     x264_slice_write()(定義在/ENCODER/encoder.c中),這裡面是編碼的最主要部分,下面仔細分析.

前面不說,看下面這個迴圈,它是採用for迴圈對一幀影象的所有塊依次進行編碼.

for( mb_xy = 0, i_skip = 0; mb_xy < h->sps->i_mb_width * h->sps->i_mb_height; mb_xy++ )//h->sps->i_mb_width指的是從寬度上說有多少個巨集快.對於寬度也就是288 / 16 = 18

    {

        const int i_mb_y = mb_xy / h->sps->i_mb_width;

        const int i_mb_x = mb_xy % h->sps->i_mb_width;//這兩個變數是定義巨集塊的位置.而不是指巨集塊中元素的位置.

        /* load cache */

        x264_macroblock_cache_load( h, i_mb_x, i_mb_y );//是把當前巨集塊的up巨集塊和left巨集塊的intra4x4_pred_mode,non_zero_count載入進來,放到一個數組裡面,這個陣列用來直接得到當前巨集塊的左側和上面巨集塊的相關值.要想得到當前塊的預測值,要先知道上面,左面的預測值,它的目的是替代getneighbour函式.

/* analyse parameters

         * Slice I: choose I_4x4 or I_16x16 mode

         * Slice P: choose between using P mode or intra (4x4 or 16x16)

         * */

        TIMER_START( i_mtime_analyse );

        x264_macroblock_analyse( h );//定義在analyse.h中.

        TIMER_STOP( i_mtime_analyse );

        /* encode this macrobock -> be carefull it can change the mb type to P_SKIP if needed */

        TIMER_START( i_mtime_encode );

        x264_macroblock_encode( h );//定義在Enc/encoder.c中.

        TIMER_STOP( i_mtime_encode );

截止到這就已經完成編碼的主要過程了,後面就是熵編碼的過程了(我也沒看到那,但認為前面才是編碼的主要過程).下面對這個過程進行分析.

A.     x264_macroblock_cache_load( h, i_mb_x, i_mb_y );它是將要編碼的巨集塊的周圍的巨集塊的值讀進來, 要想得到當前塊的預測值,要先知道上面,左面的預測值,它的作用相當於jm93中的getneighbour函式.

B.      進入x264_macroblock_analyse( h )函式(定義在/Enc/analyse.c中,這裡涉及到了函式指標陣列,需要好好複習,個人認為這也是x264程式碼最為複雜的一個地方了).既然已經將該巨集塊周圍的巨集塊的值讀了出來,我們就可以對該巨集塊進行分析了(其實主要就是通過計算sad值分析是否要將16*16的巨集塊進行分割和採用哪種分割方式合適).

看似很複雜,但我們只要把握一個東西就有利於理解了:

舉個生活中的例子來說:

如果你有2元錢,你可以去買2袋1元錢的瓜子,也可以買一袋2元錢的瓜子,如果2袋1元錢的瓜子數量加起來比1袋2元錢的瓜子數量多,你肯定會買2袋1元的.反之你會去買那2元1袋的.

具體來說,對於一個16*16的塊,

如果它是I幀的塊,我們可以將它分割成16個4*4的塊,如果這16個塊的sad加起來小於按16*16的方式計算出來的sad值,我們就將這個16*16的塊分成16個4*4的塊進行編碼(在計算每個4*4的塊的最小sad值時已經知道它採用何種編碼方式最佳了),否則採用16*16的方式編碼(同樣我們也已知道對它採用哪種編碼方式最為合適了.

如果它是P幀或B幀的塊,同樣是迴圈套迴圈,但更為複雜了,可以看我在analyse.c中的註釋.

這裡還要注意的是提到了

x264_predict_t      predict_16x16[4+3];

typedef void (*x264_predict_t)( uint8_t *src, int i_stride );

這是函式指標陣列,有很多對它的呼叫.

C.     退出x264_macroblock_analyse( h )函式,進入x264_macroblock_encode( )函式(定義在/ENCODER/macroblock.c中).

我拿巨集塊型別為I_16*16為例.

if( h->mb.i_type == I_16x16 )

    {

        const int i_mode = h->mb.i_intra16x16_pred_mode;

        /* do the right prediction */

        h->predict_16x16[i_mode]( h->mb.pic.p_fdec[0], h->mb.pic.i_fdec[0] );//這兩個引數的關係.

                                                                   //涉及到x264_predict_t(函式指標陣列),宣告在core/predict.h中,core/predict.c裡有不同定義.

        /* encode the 16x16 macroblock */

        x264_mb_encode_i16x16( h, i_qscale );//

/* fix the pred mode value */

       …     }

我們看到h->predict_16x16[i_mode]( h->mb.pic.p_fdec[0], h->mb.pic.i_fdec[0] );只調用了一次,這是因為在x264_macroblock_analyse( )中我們已經確定了採用4種方式中的哪種最合適.而在x264_macroblock_analyse( )中判定一個塊是否為I_16*16,我們呼叫了四次.這是因為當時我們需要拿最小的sad值進行比較.

繼續,是x264_mb_encode_i16x16( h, i_qscale )函式(定義在/ENCODER/macroblock.c中).在這個函式中我們就可以看到量化,zig-掃描等函式了,這些都是直來直去的,需要的只是我們的細心和對數學知識的掌握了

c)       到這裡還沒完,我們接著看

void x264_macroblock_encode( x264_t *h ){

…….前面省略.

執行到下面這條語句,看看下面是幹啥的.

    /* encode chroma */

    i_qscale = i_chroma_qp_table[x264_clip3( i_qscale + h->pps->i_chroma_qp_index_offset, 0, 51 )];

    if( IS_INTRA( h->mb.i_type ) )

    {

        const int i_mode = h->mb.i_chroma_pred_mode;

        /* do the right prediction */

        h->predict_8x8[i_mode]( h->mb.pic.p_fdec[1], h->mb.pic.i_fdec[1] );

        h->predict_8x8[i_mode]( h->mb.pic.p_fdec[2], h->mb.pic.i_fdec[2] );

        /* fix the pred mode value */

        h->mb.i_chroma_pred_mode = x264_mb_pred_mode8x8_fix[i_mode];

    }

    /* encode the 8x8 blocks */

x264_mb_encode_8x8( h, !IS_INTRA( h->mb.i_type ), i_qscale );//對色度塊進行編碼了.

到這我們可以看到原來我們在這前面是對巨集塊中的亮度係數進行了編碼,我們到上面那個函式才開始對色度係數進行編碼.進入x264_mb_encode_8x8()函式看到for迴圈裡面有個2可以證明是對2個色度係數進行編碼,想法沒錯.

那下面這些又是幹啥的呢?它們是計算cbp係數看需要對殘差(包括ac,dc)中的哪個係數進行傳輸的.

    /* Calculate the Luma/Chroma patern and non_zero_count */

    if( h->mb.i_type == I_16x16 )

    {

        h->mb.i_cbp_luma = 0x00;

        for( i = 0; i < 16; i++ )

        {

            const int nz = array_non_zero_count( h->dct.block[i].residual_ac, 15 );

            h->mb.cache.non_zero_count[x264_scan8[i]] = nz;

            if( nz > 0 )

            {

                h->mb.i_cbp_luma = 0x0f;

            }

        }

    }

    else

    {

        h->mb.i_cbp_luma = 0x00;

        for( i = 0; i < 16; i++ )

        {

            const int nz = array_non_zero_count( h->dct.block[i].luma4x4, 16 );//統計非0個數.

            h->mb.cache.non_zero_count[x264_scan8[i]] = nz;

            if( nz > 0 )

            {

                h->mb.i_cbp_luma |= 1 << (i/4);// %16的意義.

            }

        }

    }

    /* Calculate the chroma patern *///色度的cbp有3種方式.

    h->mb.i_cbp_chroma = 0x00;

    for( i = 0; i < 8; i++ )

    {

        const int nz = array_non_zero_count( h->dct.block[16+i].residual_ac, 15 );

        h->mb.cache.non_zero_count[x264_scan8[16+i]] = nz;

        if( nz > 0 )                      

        {

            h->mb.i_cbp_chroma = 0x02;    /* dc+ac (we can't do only ac) */

        }

    }

    if( h->mb.i_cbp_chroma == 0x00 &&

        ( array_non_zero_count( h->dct.chroma_dc[0], 4 ) > 0 || array_non_zero_count( h->dct.chroma_dc[1], 4 ) ) > 0 )

    {

        h->mb.i_cbp_chroma = 0x01;    /* dc only */

    }

    if( h->param.b_cabac )

    {

        if( h->mb.i_type == I_16x16 && array_non_zero_count( h->dct.luma16x16_dc, 16 ) > 0 )

            i_cbp_dc = 0x01;

        else

            i_cbp_dc = 0x00;

        if( array_non_zero_count( h->dct.chroma_dc[0], 4 ) > 0 )

            i_cbp_dc |= 0x02;

        if( array_non_zero_count( h->dct.chroma_dc[1], 4 ) > 0 )

            i_cbp_dc |= 0x04;

    }

    /* store cbp */

h->mb.cbp[h->mb.i_mb_xy] = (i_cbp_dc << 8) | (h->mb.i_cbp_chroma << 4) | h->mb.i_cbp_luma;

到這,基本上x264_macroblock_encode( h )(定義在Enc/encoder.c)基本上就分析完了.剩下的就是熵編碼的部分了.以後的部分更需要的應該是耐心和數學知識吧,相對前面來說應該簡單些.

l       總結:

1. 我對程式碼的理解應該還算比較深入,把程式碼的主線已經分析了出來,對程式碼中幾個最難理解的地方(最難理解的地方就是幀的型別的判定,參考幀是如何管理的,一個16*16的塊是採用到底需不需要分割,分割的話分成什麼大小的,子塊又採用何種預測方式,這些實際上就是整個編碼的主線.)基本上已經明白,但有些過分複雜的函式的實現(或者涉及數學知識較多的地方)還有待深入研究,但我相信沿著這條主線應該能夠繼續深入下去,自己需要的是更多的時間和耐心. 自己需要的是更多的時間和耐心,爭取以後能寫出更詳細更準確的流程分析,並儘量思考能改進的地方.

2.層次性,就像網路的7層結構一樣,每一幀影象也可以分成很多層,只有對每層的語法結構(具體來說就是各個結構體中變數的意思)有了很好的理解,才有可能真正認清程式碼,這需要對標準認真研習.比如說量化引數,就在3個地方有定義,不讀標準根本不會明白意思.

3. 很多過分複雜的東西不容易在本文中表達出來(比如說預測部分),只有通過自己的鑽研才能真正悟到,直覺也很重要,還有就是信心了.看這種程式的收穫就好像是真地肉眼看到了原子那樣.

4.由於程式碼過分複雜,對某些函式的實現過程還沒能徹底理解,比如說x264_macroblock_cache_load()函式的具體實現過程,我只是知道它的功能,實現過程還有待認真理解.dct變換是如何實現的,是如何計算殘差的等等,這些都需要很多功夫,當然這裡也需要大家的共同學習和交流.實現分工閱讀不同程式碼部分並進行交流,才有可能對程式碼做到徹底的理解.