x264 MB-tree實現細節
MB-tree的直白定義:幀和幀之間有引用關系,那些被引用的幀應該有更高的精度。通過這樣的調整,只需調整一幀的精度,就能有效改善一連串畫面的質量。
所以接下來要解決兩個問題:
- 怎么判斷幀和幀之間的引用關系,這里稱呼為propagate(遺傳)關系。
- 一個往往被忽略,但其實比第1個問題更重要的事情:MB-tree工作的上下文。(MB-tree什么時候發生的,在哪發生,以及它本身對上下文的要求。)
首先聊一聊MB-tree的上下文
MB-tree的設計是放在lookahead階段的,參考的數據並非原始幀,而是經過向下采樣之后的lowres數據,它所在處理階段如下:
在lookahead階段,一個幀序列被初步設定之后,它們之間的引用關系就是固定的,MB-tree所在的階段只有P和B幀,它們之間的關系也是一種固定關系:
-
P只會引用緊鄰的前一個P幀
-
B幀只會引用緊鄰的兩個非P幀
為什么這里不能有I幀?:
因為這里是lookahead階段,為了簡化計算,所以才把引用關系弄成了一種固定結構。但正式編碼階段,引用關系並非如此,B可以橫跨數個P幀直到找到最優的引用,P幀也是如此。
如果在lookahead階段就插入I幀,那么I幀因為不會引用別的幀,就會導致MB-tree的遺傳鏈發生斷裂。但在正式編碼階段,因為可以橫跨數個引用幀,即便有I幀遺傳關系也不會發生斷裂。
故而在這里,x264的做法是先不要把I幀標記出來,讓I幀先偽裝成B或者P,等MB-tree計算完畢之后,再把I幀標記出來。
確定了引用關系,模型就定下來了,接下來才可以正式的計算遺傳關系。
幀之間的繼承、遺傳關系:
對於所有幀來說,互相引用的關系都可以用如下模型表示:(P幀是b==p1的特殊情況)
在計算MB-tree模型的時候,是按照一對一對的來算,比如上面的b幀同時引用了p1和p0,那么就先計算P0傳遞給b多少信息,再計算p1傳遞給b多少信息。
首先,要調用slicetype_frame_cost,計算出b同時引用p0,p1得到的MB級別的inter_cost和intra_cost。inter_cost越小,就代表b從外部繼承的信息越多。
因為數據是MB級別的,所以在x264中看到的數據結構是一串一串的列表,分別記錄每個MB的數據。而我們將要談到的計算模型,也都是以一個個MB為單位,對一幀內的所有MB都要計算一遍,最后得到MB級別的控制。
如何計算MB-tree的propagate信息
MB-tree采用的是簡單的線性模型(b和p0的遺傳關系):
顯然這個模型是有一點問題的:
-
既然b同時從P0和P1都繼承了信息,那么是不是該有一個比例問題?
沒錯,需要一個ratio來修正上面的公式,按照距離來設置:b到p0的 distance_ratio= (b - p0) / (p1-p0)
-
之前我們討論了AQ的策略,AQ在不改變碼流的情況下,將各個MB都進行了微調,那么那些被調整的更清晰的MB(QP減小)和變得更模糊的MB(QP變大),在這里是不是也該考慮一下影響?
也完全正確!變得模糊的MB攜帶的信息更少,理應再加一個修正因素。量化的過程就是把系數除以QStep,所以這里的修正因子也很簡單:inv_qscale = 1.0/aq_offset_step
-
在VFR視屏下,也就是可變幀率Frame,這時候每一幀占據的播放時間都不一樣,有些幀可能在屏幕上停頓更長的時間,是否代表這些幀更重要?
|完全正確!讓我們再加一個修正因子:fps_factor = b幀停留的時間/平均一幀停留的時間
讓我們先解決以上三個問題,修正公式如下:
但還差一些:
讓我們再回顧一下MB-tree的目的,它想通過遺傳計算,找出那些被引用的幀,然后給這些被引用幀增加碼流(通過降低QP來增加碼流),而引用本身就是一種傳遞機制。
假設:A引用了B,然后B又引用了C,在這種情況下C和B都被人引用了,但C的重要性顯然比B更高,因為C才是一切的源頭。如果C的清晰度不夠,那么C會把它惡劣的畫面傳導給B,然后B又傳導給了A。
故而MB-tree的遺傳信息,應該是一層一層往上傳遞的。
讓我們再做最后的一點修正,讓數據累加起來,一層層的往上游傳遞,給每個Frame都增加一個propagate變量來記錄它遺傳了多少信息給別的幀(b遺傳了propagate_b的信息給別的幀),然后在計算p0遺傳信息的時候,自然要加上propagate_b:
propagate_b不應該乘以fps_factor,所以做了一些小的調整。
最后這個公式算出來的,就是p0所攜帶的propagate(遺傳)信息,當然不止b這一幀會引用p0,其余的幀也會引用到p0,所以p0最后的propagate信息應該是所有信息之和。
在lookahead階段,所有幀之間的引用是一種簡單的固定關系,我們只要按照編碼的反順序依次遍歷所有幀,就可以算出所有幀的遺傳信息。
其中,B幀並不會引用別的幀,故而B幀是沒有遺傳信息的,但是要注意BREF是包含遺傳信息的。
遍歷所有MB,以MB為最小單元進行計算:

得到了P0的propagate,是時候調整P0的清晰度了
MB-tree采用的公式依舊很簡單,是個線性轉化過程,先讓我們看一個大體的框架公式(后面還要一點點的修正它):
首先要明白一件事,在計算MB-tree的調整結果的時候,是以MB為單位的,每個MB有兩個重要的參數propagate(遺傳給后續幀的信息)和MB的intra_cost(MB自身包含的信息)。
故而,首先我們想到的MB-tree QP的方案如下(算出來的比例,需要通過log2函數轉化成QP):
這里要考慮的第一個因素是intra_cost雖然是p0的自身信息,但沒有考慮到p0在屏幕上停留的時間,p0停留的時間越長,代表intra_cost的比重應該越大,故而需要進行修正因素fps_factor = (p0在屏幕上停留的時間 ) / (平均一幀在屏幕上停留的時間),然后修正如下:
如此一來,這個公式就基本成型了,唯一麻煩的是x264在設計的時候,給使用者提供了外部的參數h->param.rc.f_qcompress:
f_qcompress被定義出來,就是為了動態調整QP變化的強度,所以MB-tree在利用QP調整精度的時候,也要參考這個變量。
在考慮到f_qcompress的影響之后,修正如下:
將固定系數設定成5(我也不知道為什么是5,為什么不是4,不是3……)
最后變成:
理論上這里已經很完善,考慮了各種因素,只是x264的lookahead設計上欠缺了一些:
PSY優化(此前的代碼解釋可能有誤,經過仔細思索后修改如下):
之前一直認為這里是Weight-P的修正,但仔細揣摩了一遍代碼,發現x264在計算frame cost的時候已經考慮到了Weight-P的影響。
在開啟PSY的時候,lookahead階段會對Weight-P的結果,根據i_subpel_refine的等級進一步優化,找出更合理的Weight-P的系數。所以如果開啟了Weight-P,那么這個更合理更優秀的結果,本該可以作用於PSY特性。可如果你沒有開啟Weight-P這個功能,那么PSY是一定會損失精度的,所以PSY就報復似的在這里提高了MB-tree參考幀的精度,以求得更優秀的畫面。
關於為什么要這么做,還沒有思索清楚,只能給一個初步的猜測……如果有誰知道它背后的理論基礎,也歡迎一起討論下
所以猜測:如果你關閉了Weight-P,那么MB-tree就會提高參考幀的精度。提高的精度比例如下:
把上述修復因子帶入正式的MB-tree結果。
考慮到PSY優化之后的結果:
MB-tree的整個流程就擺在這里了,計算完畢之后,要把MB-tree的結果存放在如下數據結構中:
f_qp_offset[mb_index] = f_qp_offset_aq[mb_index] - qp_offset;
//qp_offset: MB-tree的結果,因為qp越小代表的精度越高,所以這里是減去它
//f_qp_offset[mb_index] : 是保存MB-tree結果的地方,但是別忘了AQ的結果,這里要加上AQ的結果f_qp_offset_aq[mb_index]
所以最后f_qp_offset[mb_index]
中保存的是MB-tree和AQ的綜合結果。