x264 lookahead階段詳解
lookahead階段,主要作用是決定輸入幀的類型,計算MB-tree兩大功能。
本章專門討論幀類型決策,當一幀Frame被傳入x264_encoder_encoode函數之后,Frame會被加入到h->lookahead->next當中,並且Frame的類型會被標記成AUTO類型,此時lookahead線程就會開始異步計算,最終輸出一串BBBPBPP的幀序列。
細節如圖所示:

mingop: 雖然x264每次分析很長的一串Frame序列,但最后截取出來的一定是連續的B + 一個非B幀這樣的mingop,若是序列的開頭被標記為P幀或者I幀,那么就把P幀或者I幀當做一個獨立的mingop輸出出去。
在進一步細說之前,讓我們先以設計者的角度,來品一品這張圖的細節。
故事是這樣的:
-
x264在正式編碼(encoder)之前需要確定每一幀的類型,所以需要一個叫做lookahead類型的線程來幫忙計算。於是lookahead勤勤懇懇的算出來一串序列BBPPBBP,然后告訴x264這種序列的排布可以達到最小的碼流效果。
-
然后x264苦着臉,說:“lookahead兄弟,我忘記告訴你,我身上有一道枷鎖,必須每隔[keyint_min, keyint_max]的Frame數目就插入一個I幀, 你這串BBPPBBP雖好,但不符合要求,奈何……”
-
lookahead聽完,當即徹夜未眠,之后請出scenecut來幫忙。scenecut在參考了[keyint_min, keyint_max]限制之后,弄出了一套方案,便是把BBPPBBP這串序列中最有可能是轉場幀的Frame挑出來當做I幀,如此一來,既能滿足需求,又可盡量減少碼流的增加。
-
x264聽完scenecut的方案,不由得大喜,但細細一思,又露出苦色:“家中有一個憨憨兄弟MB-tree,此人分析遺傳信息(propagate)時只能接受B和P兩種幀類型,若是胡亂插入I幀,MB-tree休矣。”
-
lookahead又獻計:“如此好辦也,將scenecut邏輯拆成兩分,一份放在開頭處挑選單獨的I幀,此時I幀作為獨立的mingop輸出出去,不需擔心MB-tree。而在后半段scenecut邏輯當中,若是發現轉場幀,則僅僅將前一幀設置為P幀,將轉場幀和前面的mingop強制斷開,如此一來,即在不破壞MB-tree的前提下,實現了scenecut。”
-
x264終於喜笑顏開,遂采用此方案。
好了,聽完這個小故事,想必大家都知道為什么lookahead的邏輯設計長這樣了。其實它的設計思路很單一,只是為了戴上[keyint_min, keyint_max]這個枷鎖的情況下,找出一串最優的幀序列。
先來弄清楚,最優幀序列是怎么選出來的
首先我們要明白一件事,lookahead階段僅僅是一種預測手段,故而它不會和正式編碼一樣擁有復雜的引用關系,它的引用關系可以說簡單到了極致:
-
B幀只會引用前后兩個非B幀
-
P幀只會引用緊鄰的前面一個P或者I幀
它的引用關系如下所示:

即便考慮到BREF幀,也是固定的(當h->param.i_bframe_pyramid>0的時候,會把連續的B幀中間挑一個當做BREF,BREF可以被別的B幀引用,這是一種優化手段):

所以只要確定了基本的BBBPBBP序列,就可以得到這種固定的引用關系。lookahead只需要根據選定的幀序列計算出每一幀的碼流大小,選擇一個碼流最小的幀序列即可。
所以這里沒有什么花里花哨的動作,硬核搜索,每一個搜索到的序列都計算一遍,然后找出最優的。
當然在lookahead當中,使用的數據並非原始Frame的數據,而是經過向下采樣的lowres數據,所以計算量要小很多。可即便如此,計算幀之前的引用cost的時候,也需要和正式編碼一樣進行運動預測,它的計算代價也不小,所以在這里x264設計了多線程計算模型。
硬核搜索,也是要講策略的,x264中有三種策略:
X264_B_ADAPT_TRELLIS搜索策略:
假設已經找到的最優的3個Frame的序列BPP,此時又來了一個新的Frame,我們來看看它是怎么找出4個最優序列的:

再看看X264_B_ADAPT_FAST策略:

最后是X264_B_ADAPT_NONE:
這種模式下,並不會搜素出最優的序列,lookahead只是機械式的輸出一串B和P的序列。序列的規則是:連續i_bframes數目的B幀 + 一個P,然后又是連續_bframes數目的B幀 + 一個P,如此反復循環。
經過反復計算之后,便得到了一串最優編碼的Frame序列
只有B和P當然不夠,還需要scenecut來幫忙
因為每隔一定幀就插入I幀,scenecut就像是戴上了一個枷鎖,它一邊挑選I幀的時候,還必須滿足挑出的I幀在[keyinit_min, keyint_max]的范圍內。
scenecut定義:用來判斷兩幀是否屬於轉場。
實現細節:通過計算P0幀和P1幀的inter_cost和intra_cost來判斷二者的相似度。
假設有相鄰的兩幀:P0, P1(P0在P1之前被播放出來)
先調用silicetype_frame_cost計算p1的intra_cost, 以及p1相對於p0的inter_cost,然后我們就得到了P1的兩個參數:
scenecut的目的是為了找出一個合適的I幀,以符合“每隔一段時間插入I幀”的硬性需求。故而離上一次I幀(其實是last_keyframe)越遠,那么scenecut就越急切的想找出一個合適的I幀,因為如果在一定期限內找不出合適的I幀,等達到了最大間隔h->param.i_keyint_max的時候,它就“違約”了,這時候它會不管P1合不合適作為I幀,都強制把P1設置成I幀。
當然,scenecut肯定不想“違約”,因為把不合適的P1設成I幀會浪費大量的碼流。所以在最終的期限到來之前,scenecut會一點點的降低選擇標准,爭取在期限內找出合適的I幀。
scenecut的邏輯好比一個青年,他必須在30歲之前就找到新娘,否則30的時候,家族長輩會強制讓他娶如花小姐。在20歲的時候,他的擇偶標准是很高的,但隨着年紀的增長 ,他在層層恐懼之下,擇偶標准也會越來越低……
所以僅僅用inter_cost和intra_cost來判斷scenecut是不夠的,它還需要一個變量:
i_gop_size = frame->i_frame - h->lookahead->i_last_keyframe;
i_gop_size代表距離上一次關鍵幀的間隔。
這些參數都是x264自動計算的,還有一個外部可控參數:
f_thresh_max = h->param.i_scenecut_threshold / 100.0
i_scenecut_threshold默認是40,所以f_thresh_max默認是40%
接下來就可以比較了,當滿足以下條件的時候,就認為P0和P1之間是一個轉場:
pcost >= (1.0 - f_bias) * icost
//pcost越大,就說明二者差距越大,就越可能是一個scenecut
//但f_bias似乎沒提到? 別急f_bias是scenecut的“擇偶”標准,它是一個不斷變化的參數,f_bias越小代表“擇偶”標准越嚴格。
看看f_bias是怎么計算的,我不想貼出來一大段代碼,因為那樣太亂了,讓我們一點點的來看:
f_thresh_min = f_thresh_max * 0.25;
if( i_gop_size <= h->param.i_keyint_min / 4 || h->param.b_intra_refresh )
f_bias = f_thresh_min / 4;
i_gop_size很小,這時候的scenecut沒有“滿足成親條件,仍舊是幼年階段”,那么scenecut心里一點也不着急,它的擇偶標准也高到離譜
else if( i_gop_size <= h->param.i_keyint_min )
f_bias = f_thresh_min * i_gop_size / h->param.i_keyint_min;
gop_size在一點點接近h->param.i_keyint_min(也就是scenecut的法定年紀),scenecut開始有一點着急了,降低了一點點標准。但它這時候還是很高傲,因為用的系數是f_thresh_min,f_bias仍舊很小。
最后:
else
{
f_bias = f_thresh_min
+ ( f_thresh_max - f_thresh_min )
* ( i_gop_size - h->param.i_keyint_min )
/ ( h->param.i_keyint_max - h->param.i_keyint_min );
}
scenecut徹底成年,它必須挑出合適的I幀,挑到之后立即拿去刷新key_frame;隨着i_gop_size的增長,它也會一點點的降低標准。而它在最后的關頭,也就是i_gop_size=h->param.i_keyint_max的時候,終於把標准降低到了f_bias=40%(這時候它喊着不能再降了,然后被強迫娶了如花小姐……)
除去上面的一套選擇標准,scenecut還有重要的宏觀策略:
在某些情況下,即便發現了一些合適的幀,scenecut也不能認定了對方,因為視頻畫面中除了轉場,還有一種叫做“Flash”的東西。
Flash:突然閃出來的畫面,它一閃即逝,雖然和別的幀格格不入,也完全符合scenecut的標准,但Flash出現的片段太短了,不值得將它認定成scenecut
這其中的重要原因是,scenecut每次挑出來的I幀,都應該能作為一個重要的引用幀,如果你跳出來的是Flash,那么只是平白的浪費了碼流,因為Flash與后續幀的格格不入,后續的幀也沒辦法有效的引用它,這樣可能會出現連續的I幀,顯然這不是我們想看到的。
想要識別出Flash,先要把搜索范圍延伸一下,對於P0和P1兩個相鄰的幀,首先要把搜索范圍向后擴大成[p0, p0 + i_bframes + 1]:
可以認為搜索范圍是一個mingop
-
識別AAAAAABBBAAAAAA類型的Flash,這種frame的序列,雖然中間出現了BBB三幀,但因為BBB之后又出現了A類型,而且A顯然連續性更強一些,那么就認為B是一種Flash,不會把B升級成I幀。
-
識別AAAAABBCCDDEEFFFFFF,這里的A,B,C,D,E,F都各自不同,互相之間都滿足scenecut的條件,但因為sceencut處理的是一個mingop的小區間,它不允許在小區間內出現這么多I幀,所以只把最后一個E當做scenecut。
第二種情況比較難理解,代碼中采用策略是:拿區間內的每一幀和最后一個F幀比較,直到找到最后一個E,從這里開始,后續都是FFFFF的連續一樣的幀了。所以就認定最后一個E為scenecut。
這樣選出來的scenecut比較合理,因為至少保證了后續幀是“相似的”,可以減少后續幀的碼流大小。
識別出scenecut幀之后,就要處理MB-tree了,不過MB-tree的篇幅有一點長,而且和幀的選擇策略沒有一丁點的關系,就留待其余章節再說。
在算完MB-tree之后,lookahead就正式完成了它的使命,輸出了一串符合要求的幀序列給正式編碼進程。