天天看點

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

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

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

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

F10 到 x264_stack_align( x264_slicetype_decide, h ); x264_stack_align 顧名思義無非就是平台優化方面考慮的對齊操作,是以這裡我們要關心的是函數 x264_slicetype_decide,F11 我們會發現進不到 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 到了 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_init,F11 進去後發現隻有非 I 片才進行一些操作,那暫時就不管它。

F10 到了 x264_mb_cache_fenc_satd,F11 進去。一開始是個 4*4 的雙重循環。我們現在是在對一個宏塊進行操作,這裡又出現 4*4 的循環,那麼很明顯了這個雙重循環肯定是在計算每個 4*4 的塊,下面的 2*2 的雙重循環肯定是在計算 8*8 的塊。因為宏塊的尺寸是 16*16 嘛,寬高分成 4 份不正好是 4*4,分成 2 份不正好是 8*8 麼?做視訊的人應該對 4、8、16 等常用的數字敏感。先分析第一個 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_intra,F11 進入。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_dct、x264_quant_4x4、h->zigzagf.scan_4x4、h->quantf.dequant_4x4、h->dctf.add4x4_idct,各函數功能顧名思義,的确驗證了我們對 x264_mb_encode_i4x4 這個函數的功能的猜測。那麼這些函數為什麼要以這些順序調用呢?因為編碼原理和架構就是這樣(這也再次展現了,讀代碼前要對編碼原理和架構熟悉)。

step out,跳出 x264_mb_analyse_intra 函數,繼續 F10,到了 x264_intra_rd,F11 跟進。繼續 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 語言功底在讀代碼過程中也是必須的,否則像函數指針這些東西你都搞不清楚怎麼回事。

本文轉自 fanxiaojun 51CTO部落格,原文連結:http://blog.51cto.com/2343338/1064702,如需轉載請自行聯系原作者