相關說明: 1. 使用版本: x264-cvs-2004-05-11 2. 這次的分析基本上已經将代碼中最難了解的部分做了闡釋,對代碼的主線也做了剖析,如果這個主線了解了,就容易設定幾個區間,進行分工閱讀,将各個區間擊破了. 3. 需要學習的知識: a) 編碼器的工作流程. b) H.264的碼流結構,像x264_sps_t,x264_pps_t等參數的定義基本上都完全符合标準文檔中參數集的定義,抓住主要參數,次要參數也應該有所了解. c) 數學知識,對dct變換等與數學相關的知識的程式設計實作要有較好了解. d) C語言的知識.涉及到c語言的較多不經常用的特性,如函數指針數組,移位運算,結構體的嵌套定義等. e) 耐心,對h.264的複雜性要有清醒的認識. 3.參考資料: a) 新一代視訊壓縮編碼标準-h.264/avc 畢厚傑主編,人民郵電出版社. b) 網上的流媒體論壇,百度,google等搜尋引擎. 4. 閱讀代碼的方法: a) 較好的方法是利用vc的調試器,如果對某個函數感興趣,可以将斷點設定在它的前面.然後采用step into,step over等方法進去該函數一步步分析.當然本身要對程式執行流程要有較清楚認識,不然不知道何時step into,何時step over. b) 建議應該先對照标準弄清各個結構體成員的意義. 源代碼主要過程分析: 1. 進入x264.c中的main函數. 剛開始是讀取預設參數,如果你設定了參數的話會修改param的. i_ret = Encode( ¶m, fin, fout ); 這條語句使過程進入x264.c中的Encode函數. 2. X.264的encode函數. A. i_frame_total = 0; if( !fseek( fyuv, 0, SEEK_END ) ) { int64_t i_size = ftell( fyuv ); fseek( fyuv, 0, SEEK_SET ); i_frame_total = i_size / ( param->i_width * param->i_height * 3 / 2 ) } 上面這段計算出輸入檔案的總幀數. B. h = x264_encoder_open( param )這個函數是對不正确的參數進行修改,并對各結構體參數和cabac編碼,預測等需要的參數進行初始化. C. pic = x264_picture_new( h ); 該函數定義在/CORE/common.c中.首先分給能容納sizeof(x264_picture_t)位元組數的空間,然後進行初始化. 這裡看一下x264_picture_t和x264_frame_t的差別.前者是說明一個視訊序列中每幀的特點.後者存放每幀實際的象素值.注意區分. D. for( i_frame = 0, i_file = 0; i_ctrl_c == 0 ; i_frame++ ) { int i_nal; x264_nal_t *nal; int i; if( fread( pic->plane[0], 1, param->i_width * param->i_height, fyuv ) <= 0 || fread( pic->plane[1], 1, param->i_width * param->i_height / 4, fyuv ) <= 0 || fread( pic->plane[2], 1, param->i_width * param->i_height / 4, fyuv ) <= 0 ) { break; } //檔案位置訓示器自己變化了. if( x264_encoder_encode( h, &nal, &i_nal, pic ) < 0 ) { fprintf( stderr, "x264_encoder_encode failed/n" ); } …… } 凡是出現for循環的地方都應該認真對待,這是我的一個體會,也是進入分層結構認真分析的好方法. fread()函數一次讀入一幀,分亮度和色度分别讀取.這裡要看到c語言中的File檔案有一個檔案位置訓示器,調用fread()函數會使檔案訓示器自動移位,這就是一幀一幀讀取的實作過程. 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]; x264_frame_t *frame_next[X264_BFRAME_MAX+1]; //搞清意義,下一個幀,而不一定是B幀. x264_frame_t *frame_unused[X264_BFRAME_MAX+1]; 注意區分這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的操作,剛開使挺簡單,如我下面的注釋. h->out.i_nal = 0;//out的聲明在bs.h中. bs_init( &h->out.bs, h->out.p_bitstream, h->out.i_bitstream );//空出8位. if( i_nal_type == NAL_SLICE_IDR )//不是每次都要寫SPS and PPS,隻有碰見立即重新整理片時才寫. { x264_nal_start( h, NAL_SPS, NAL_PRIORITY_HIGHEST ); x264_sps_write( &h->out.bs, h->sps ); x264_nal_end( h ); 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;//這兩個變量是定義宏塊的位置.而不是指宏塊中元素的位置. x264_macroblock_cache_load( h, i_mb_x, i_mb_y );//是把目前宏塊的up宏塊和left宏塊的intra4x4_pred_mode,non_zero_count加載進來,放到一個數組裡面,這個數組用來直接得到目前宏塊的左側和上面宏塊的相關值.要想得到目前塊的預測值,要先知道上面,左面的預測值,它的目的是替代getneighbour函數. TIMER_START( i_mtime_analyse ); x264_macroblock_analyse( h );//定義在analyse.h中. TIMER_STOP( i_mtime_analyse ); 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; 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裡有不同定義. x264_mb_encode_i16x16( h, i_qscale );// … } 我們看到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 ){ …….前面省略. 執行到下面這條語句,看看下面是幹啥的. 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; 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] ); h->mb.i_chroma_pred_mode = x264_mb_pred_mode8x8_fix[i_mode]; } x264_mb_encode_8x8( h, !IS_INTRA( h->mb.i_type ), i_qscale );//對色度塊進行編碼了. 到這我們可以看到原來我們在這前面是對宏塊中的亮度系數進行了編碼,我們到上面那個函數才開始對色度系數進行編碼.進入x264_mb_encode_8x8()函數看到for循環裡面有個2可以證明是對2個色度系數進行編碼,想法沒錯. 那下面這些又是幹啥的呢?它們是計算cbp系數看需要對殘差(包括ac,dc)中的哪個系數進行傳輸的. 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的意義. } } } //色度的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; } } 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; } 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; } 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變換是如何實作的,是如何計算殘差的等等,這些都需要很多功夫,當然這裡也需要大家的共同學習和交流.實作分工閱讀不同代碼部分并進行交流,才有可能對代碼做到徹底的了解. 編者按:本文來源于QQ群:H.264樂園 |