音視頻技術--H.264代碼與標準如何對應

 

總是有人說自己把代碼和標準對應不起來。其實是因爲你要麼不知道標準各個章節講的什麼,要麼不知道代碼中各個函數的功能,或者兩者都不知道。今天再以 X264 的幀內編碼爲例讓大家體會一下讀代碼時該如何與標準對應。此貼是帖子[原創]如何閱讀代碼的延續,因此採用的代碼與編譯環境設置與其一樣,此處不再贅述。

上貼說過 Encode_frame 函數包含最核心的編碼代碼,那麼我們現在就 F11 進去看看。遇到的第一個函數是 x264_encoder_encode,再 F11 進去,執行到 x264_reference_update,它在幹什麼呢?顧名思義猜測一定是更新幀間參考要用到的一些內存空間,因爲我們現在還沒有編碼,所以 F11 進去後沒執行什麼操作就出來了。

繼續 F10,執行到 x264_frame_pop_unusedF11 跟進,然後 F10,發現它走了 x264_frame_new 的分支(x264_frame_pop 分支幹什麼用的呢?暫時先別管。跟着流程走,管多了就迷茫了),F11 跟進 x264_frame_new 發現通篇都是對變量結構體指針 frame 裏的成員變量執行 CHECKED_MALLOC,由此我們可以初步判斷它是在爲幀結構體分配內存空間。

step out 跳出 x264_frame_pop_unusedF10 x264_frame_copy_pictureF11 進去讀讀代碼我們就知道這個函數的功能是將待編碼圖像從 pic_in 複製到 fenc->plane。繼續 F10,到了 x264_frame_push,通過閱讀該函數的代碼我們知道它的功能是將當前幀結構體從 fenc 移到 h->frames.next 中。後面的函數 x264_frame_init_lowresx264_adaptive_quant_framex264_encoder_frame_end 都未被執行。既然沒被執行,那我們現在暫時就不管它們。

F10 x264_stack_align( x264_slicetype_decide, h ); x264_stack_align 顧名思義無非就是平臺優化方面考慮的對齊操作,因此這裏我們要關心的是函數 x264_slicetype_decideF11 我們會發現進不到 x264_slicetype_decide 裏。怎麼辦呢?見下面第一個截圖,將光標點到 x264_slicetype_decide 上,點鼠標右鍵選擇 go to definition,然後先在裏面的第一行代碼下斷點(見下面第二個截圖),然後再按 F10 就可以進入到 x264_slicetype_decide 函數了。該函數顧名思義是來決定當前 slice 的編碼類型的,即到底是 I 片還是 P 片或 B 片。通過瀏覽其代碼,我們也會發現代碼所做也正是這樣。

 

 

step out 跳出 x264_slicetype_decide,繼續 F10 執行到 x264_frame_push,這裏實際要執行兩個函數,因爲 x264_frame_push 的第二個參數是函數 x264_frame_shift,所以會先執行它。F11 首先進入的就是 x264_frame_shift,然後 step out 跳出 x264_frame_shift,繼續 F11 就進入了 x264_frame_push,通過閱讀這兩個簡短的函數的代碼,我們知道它們執行的操作是將當前編碼幀結構體從 h->frames.next 移到 h->frames.current

繼續 F10,又到了一個 x264_frame_shift 函數,F11 進去通過閱讀代碼我們可以知道該函數的功能將當前幀結構體從 h->frames.current 移到 h->fenc(我有點奇怪,爲什麼 X264 要這麼麻煩地把一個變量移來移去呢?一次搞定不行麼?)。繼續 F10,到了 x264_reference_reset,其功能顧名思義,也有英文註釋,具體有什麼用,現在我還不知道,暫時不管吧。

繼續 F10,到了 x264_reference_build_list,顧名思義,參考列表構建,在 JM 裏叫做參考列表初始化(JM86 對應的函數是 init_lists)。參考列表初始化的作用即構建幀間編碼圖像所需要用到的參考圖像列表。那麼如何初始化呢?如果大家記得 H.264 標準的各個章節的功能,那麼就該知道 200503 版的 8.2.4 小節正是講的這部分內容。這樣這個函數就與標準的內容對應起來了。至於通過代碼是如何實現的,先看懂了標準的這個部分再來讀這個函數的代碼吧。

繼續 F10,到了 x264_ratecontrol_start,顧名思義進行碼率控制的一些準備工作。

繼續 F10,到了 x264_slice_init,顧名思義片初始化,做了哪些工作呢?F11 進去執行了分支 x264_slice_header_init,通過瀏覽其代碼,我們發現通篇都是對結構體指針 sh 內的成員變量的賦值操作。後面的 x264_macroblock_slice_init 函數在幹什麼,大家自己 F11 進去看,看不懂沒關係,反正就是給一些變量賦初值嘛。繼續 F10,到了 bs_init,顧名思義是對碼流相關的變量進行初始化,因爲 bs 就是 bit stream 嘛。

繼續 F10,到了 if( i_nal_type == NAL_SLICE_IDR && h->param.b_repeat_headers ),注意前面的英文註釋 /* Write SPS and PPS */,意思就是這裏在向碼流中寫 SPS PPS。這裏的三組函數,顧名思義第一組是在寫 SEI、第二組是在寫 SPS、第三組是在寫 PPS。那麼如何寫碼流呢?當然是要遵循語法表了。下面以寫 SPS 爲例簡要說明一下,如果大家記得 H.264 標準的各個章節的功能,那麼就該知道 200503 版的 7.3.2.1 小節就是 SPS 語法表。因爲 7.3.2.1 規定了 SPS 在碼流中的第一個語法元素是 profile_idc,因此當我們 F11 進入 x264_sps_write 的時候會發現該函數第一行代碼正是在寫 sps->i_profile_idc,標準規定 SPS 第二個語法元素是 constraint_set0_flag,因此該函數的第二行代碼就是在寫 sps->b_constraint_set0,其他同理。這裏說的是寫的順序,那麼寫的方式是什麼呢?H.264 中的熵編碼方式細分起來有很多,每個語法表的最後一列 descriptor 規定的就是對應的語法元素採用哪種熵編碼方法。例如:profile_idc u(8),因此它採用 8 位無符號整數編碼;constraint_set0_flag u(1),因此它採用 1 比特無符號整數編碼。各種熵編碼方法在 200503 7.2 小節最後都有說明,此處不再贅述。好了,我們知道了各個語法元素採用什麼方式編碼,自然也就知道了代碼中各個熵編碼函數對應的是什麼編碼方式。例如:對 profile_idc 編碼採用的是 bs_write 函數,當然這個函數的功能就是無符號熵編碼了,對 i_id 編碼採用的是 bs_write_ue 函數,當然這個函數的功能就是 ue(v)——無符號哥倫布編碼。其他同理。其實這些我在帖子[原創] 如何讀標準和代碼中已經講過了。

順便提一下,對碼流的讀寫操作都要依據語法表所定義的語法元素順序和熵編碼類型。上面講的是編碼的具體例子,解碼的具體例子我以前用 JM 講過,參考帖子如何結合標準看JM代碼(JM86。好了,繼續 F10,到了 x264_slices_writeF11 進入,再 F10,到了 x264_stack_align( x264_slice_write, h );我們關心的是 x264_slice_write,進入該函數,方法在上面已經說過了。x264_slice_write 第一個函數爲系統函數 memset,下一個爲 x264_nal_start,其功能看下代碼就知道是在設置將要寫入碼流的 NALU 的第一個字節的值。第二個函數 x264_slice_header_write 顧名思義是在向碼流中寫入片頭。如果大家記得 H.264 標準的各個章節的功能,那麼就該知道 200503 版的 7.3.3 小節就是片頭的語法表。寫碼流的過程與上面 SPS 的過程同理,此處不再贅述。

F10 到了 while 循環,顧名思義根據 while 循環的循環條件猜測一下該 while 循環的功能,肯定就是循環對整個圖像的每個宏塊一次編碼了。要驗證一下猜測很簡單,在 while 循環體的第一行下斷點,按一次 F5 就觀察一下 mb_xy 變量的值的變化情況。另外還有個信息說明了這一點,h->sh.i_last_mb 變量的值剛好等於待編碼圖像的總宏塊數。

F10 到了 x264_fdec_filter_row,顧名思義猜測該函數的功能是去塊濾波。如果大家記得 H.264 標準的各個章節的功能,那麼就該知道 200503 版的 8.7 小節正是講的這部分內容。要讀懂這個函數的代碼就先學習一下 8.7 小節吧。

F10 到了 x264_macroblock_cache_load,通過瀏覽代碼我們知道是在對一些變量賦值,各個變量的含義顧名思義。這也屬於編碼前的準備工作。繼續 F10 到了 x264_macroblock_analyse,看見英文註釋了吧?不用我們顧名思義就知道它的功能了,是在進行模式選擇。F11 進入該函數。第一個被調用的函數是 x264_ratecontrol_qp,顧名思義獲取當前宏塊 QP。第二個被調用的函數是 x264_mb_analyse_initF11 進去後發現只有非 I 片才進行一些操作,那暫時就不管它。

F10 到了 x264_mb_cache_fenc_satdF11 進去。一開始是個 4*4 的雙重循環。我們現在是在對一個宏塊進行操作,這裏又出現 4*4 的循環,那麼很明顯了這個雙重循環肯定是在計算每個 4*4 的塊,下面的 2*2 的雙重循環肯定是在計算 8*8 的塊。因爲宏塊的尺寸是 16*16 嘛,寬高分成 4 份不正好是 4*4,分成 2 份不正好是 8*8 麼?做視頻的人應該對 4816 等常用的數字敏感。先分析第一個 4*4 的雙重循環。注意,for 循環裏的 h->pixf.satd h->pixf.sad 都是函數指針,因此要用 F11 跟進。h->pixf.satd 的兩個輸入是 zero fenc,跟進之後的函數 pixel_satd_wxh 在計算他們之差,然後作 Hadamard 變換,然後計算 SATD。由此可以猜測 fenc 裏存放的是原始待編碼宏塊(到底是不是呢?讀者自己反回去找到 h->mb.pic.p_fenc[0] 被賦值的地方看看就知道了)。後面代碼的功能類似了,不重複敘述。總的來說,x264_mb_cache_fenc_satd 這個函數就是計算原始待編碼宏塊 4*4 8*8 STAD。算來做什麼?暫時還不知道。

step out,跳出 x264_mb_cache_fenc_satd 函數,繼續 F10,到了 x264_mb_analyse_intraF11 進入。F10 到了 predict_16x16_mode_available,顧名思義並結合該函數代碼,可以確定它是在檢查當前宏塊有幾種可用的 16*16 幀內預測模式。繼續 F10 到了 for 循環 for( i = 0; i < i_max; i++ ),其循環條件 i_max 是函數 predict_16x16_mode_available 的返回值,那麼很顯然這個 for 循環是在循環計算可用預測模式了。繼續 F10,進循環體到了 h->predict_16x16[i_mode],這又是個函數指針,顧名思義並結合改函數代碼,可以確定它是在取得 16*16 塊當前預測模式下的幀內預測塊。如果大家記得 H.264 標準的各個章節的功能,那麼就該知道 200503 版的 8.3.3 小節正是講了 16*16 塊的各種預測模式下如何進行幀內預測的。要讀懂這個函數的代碼就先學習一下 8.3.3 小節吧。繼續 F10,到了 h->pixf.mbcmp[PIXEL_16x16],又是個函數指針,其功能大家自己跟進吧。該 for 循環完成後就把 16*16 塊的最佳預測模式計算出來並存儲起來了。

繼續 F10,到了幀內 4*4 的預測模式選擇部分。for 循環 for( idx = 0;; idx++ )idx 是什麼?因爲這是幀內 4*4 預測,所以我們很自然應該聯想到 idx 就應該是 16 4*4 塊的編號,這個決定了 16 4*4 塊的處理順序,這個順序可不是亂來的哦,200503 版標準/ 6-10 對順序做了規定。繼續 F10,到了 x264_mb_predict_intra4x4_mode 顧名思義並結合該函數代碼可以確定它是在獲得最可能預測模式,如果大家記得 H.264 標準的各個章節的功能,那麼就該知道 200503 版的 8.3.1.1 小節正是講的這部分內容。要讀懂這個函數的代碼就先學習一下 8.3.1.1 小節吧。繼續 F10 到了 predict_4x4_mode_available 跟上面 16*16 塊類似,功能顧名思義就不多說了。繼續 F10,進入第二個 for 循環 for( ; i<i_max; i++ ),一看就知道該 for 循環跟上面 16*16 塊同理是在計算當前 4*4 塊的最佳預測模式。繼續 F10,進入循環體到了 h->predict_4x4[i_mode] 顧名思義並結合該函數代碼可以確定它是在取得當前 4*4 塊在當前可用預測模式下的幀內預測塊,如果大家記得 H.264 標準的各個章節的功能,那麼就該知道 200503 版的 8.3.1.2 小節正是講的這部分內容。要讀懂這個函數的代碼就先學習一下 8.3.1.2 小節吧。繼續 F10,到了 h->pixf.mbcmp[PIXEL_4x4],也跟上面 16*16 塊類似,功能顧名思義。

繼續 F10,第二個 for 循環執行完後就把當前 4*4 塊的最佳預測模式計算出來並存儲起來了,到了函數指針 h->predict_4x4[a->i_predict4x4[idx]],很顯然是在取得當前 4*4 塊的最佳預測模式下的預測塊了。算來幹什麼?從 H.264 幀內宏塊編碼的原理上我們知道幀內預測要以相鄰塊的重建值爲參考,不先計算預測塊,殘差從哪裏來?不得到殘差,又哪裏得到重建呢?(所以這裏也體現了,讀代碼前要對編碼原理和框架熟悉,否則你咋能明白這裏爲什麼要取得預測塊呢?)。繼續 F10,到了 x264_mb_encode_i4x4,顧名思義並聯想幀內編碼原理和框架,我們猜測它是在進行當前 4*4 塊的重建。F11 進去驗證一下我們的猜測是否正確。x264_mb_encode_i4x4 函數裏依次執行了 h->dctf.sub4x4_dctx264_quant_4x4h->zigzagf.scan_4x4h->quantf.dequant_4x4h->dctf.add4x4_idct,各函數功能顧名思義,的確驗證了我們對 x264_mb_encode_i4x4 這個函數的功能的猜測。那麼這些函數爲什麼要以這些順序調用呢?因爲編碼原理和框架就是這樣(這也再次體現了,讀代碼前要對編碼原理和框架熟悉)。

step out,跳出 x264_mb_analyse_intra 函數,繼續 F10,到了 x264_intra_rdF11 跟進。繼續 F10,到了函數 x264_analyse_update_cache,顧名思義無法猜測其功能,F11 跟進之後發現它只調用了一個函數 x264_mb_analyse_intra_chroma,這個函數又是什麼功能呢?留給讀者自己去跟進吧。step out,跳出 x264_analyse_update_cache 函數,繼續 F10,到了 x264_rd_cost_mb,顧名思義猜測是進行 RDO 模式選擇。這種方法的失真測度通常是使用 SSD,即原始像素與重建像素的誤差平方和。那麼如果我們對 x264_rd_cost_mb 的功能猜測正確,其函數中必然有編碼宏塊的代碼和計算 SSD 的代碼。F11 跟進去驗證我們的猜測,x264_rd_cost_mb 裏的確調用了 x264_macroblock_encode ssd_mb。這兩個函數是否是在執行編碼和計算 SSD 的功能呢?留給讀者自己去驗證吧。提醒一句,X264 在這裏用的失真測度不僅僅是 SSD,另外還有什麼成分,讀者自己去跟蹤 ssd_mb 函數。x264_rd_cost_mb 函數最後執行的函數是 x264_macroblock_size_cavlc,顧名思義是在對當前宏塊進行熵編碼了。爲什麼要熵編碼,因爲 RDO 的率失真準則中要用到編碼比特數啊。

step out,跳出 x264_rd_cost_mb 函數,後面的代碼不說大家也知道了。step out,跳出 x264_intra_rd 函數,該函數下面的 6 行代碼(見下圖)的功能大家得弄清楚。因爲算了這麼多模式,這麼多代價,最後編碼到底選哪個模式呢?答案就這裏了。

 

step out,跳出 x264_macroblock_analyse 函數,到了 x264_macroblock_encode,顧名思義並結合編碼流程可以確定這裏調用這個函數就是在用最終選定的那個最優的模式對當前宏塊進行實際編碼了。繼續 F10,到了 x264_bitstream_check_buffer,顧名思義猜測是進行寫碼流前的一些準備工作。繼續 F10,到了 x264_macroblock_write_cavlc,顧名思義並聯想編碼流程,很明顯是在將最後的編碼結果寫入碼流了。

至此,一個宏塊幀內編碼的過程就剖析完了。相信大家看完這麼長的帖子之後,應該對我以前提出的學習建議中的兩點有了深刻體會:1、讀代碼前一定要熟悉編碼原理和框架;2、弄清楚標準各個章節講的什麼內容。當然這也是怎麼看標準,怎麼用標準的問題——先很粗略地瞭解各個章節是講的什麼,等到需要詳細瞭解其內容時候再去細讀相關章節。當然,C 語言功底在讀代碼過程中也是必須的,否則像函數指針這些東西你都搞不清楚怎麼回事。

音視頻:http://ishare.iask.sina.com.cn/f/33851582.html
                http://ishare.iask.sina.com.cn/f/33852469.html
 
對於本博客有任何疑問的都可加QQ:992139738



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