x264 psy_trellis詳解


x264 psy_trellis詳解

定義:基於心理視覺模型的量化方法

相關參數:

  • --psy-rd float:float : Strength of psychovisual optimization

    ​ #1: RD (requires subme>=6) -------------RDO相關的參數,與本文沒有關系

    ​ #2: Trellis (requires trellis, experimental) ---------------psy_trellis的強度參數,值越大就代表越看重優化之后的bits大小,反之則更注重圖像的質量(仍舊在實驗階段的模型)

  • -t, --trellis Trellis RD quantization.

    • 0: disabled
    • 1: enabled only on the final encode of a MB -------這里的意思是,只有在最后真正編碼MB的時候,才使用trellis量化的策略
    • 2: enabled on all mode decisions --------在前期評估的時候(運動搜索階段),已經開始使用trellis量化來評估了,這種模式會增加計算量

psy_trellis到底在干什么?它真的是一種量化方法?還是一種算法?

嚴格來說,psy_trellis是一種算法,而不是一種量化矩陣,只是它的行為(或者說函數名字)偽裝成了量化矩陣。在正常編碼階段,數據會經過DCT變換->quant量化->zigzag掃描->編碼,這幾個階段。而psy_trellis干的事情,就是把quant量化之后的數據拿出來,然后對每一個系數coef進行評估,試圖找出到底是coef-1好,還是原本的 coef更優秀,然后找出一串最優的編碼。

所以經過psy_trellis量化的系數,它的解碼過程和正常的解碼一點區別也沒有,因為psy_trellis並沒有提供另一種更優秀的量化矩陣,所以也用不着特定的反量化矩陣了。它做的僅僅是評估一下“如果把系數coef減去1,會不會得到更優秀的結果”這件事。

至於為什么要這么做,或者說為什么coef-1會比原本的coef更優秀,究其原因還是因為quant量化帶來的精度損失,再加上x264采用了嚴格的四舍五入的整數運算策略,這就導致有時候coef-1反而對圖像更友好,而且還能減少編碼之后的bits。


要實現這個功能,在CABAC和CAVLC下是不同的,具體的一個一個的來分析。

在CABAC中的具體實現

psy_trellis, 這里所謂的后綴_trellis,並沒有特別的含義,只是代表是psy采用的算法是一種網格搜索算法。而網格搜索算法,秉承的原則就是:“基於局部最優,以達到全程最優”的策略。

再看CABAC的編碼過程,它首先會編碼zigzag掃描之后系數coef的mask(mask的每一位代表coef是否為0),然后再對每個系數coef采用zigzag掃描的倒序逐個編碼,最后再編碼當前系數coef的符號位。

而CABAC每編碼一個coef,它的node_ctx狀態就會發生遷移,故而就衍生出一個問題:psy_trellis在試探coef-1和coef的時候,並不能簡單的比較兩者哪一個更優,因為編碼coef-1或者編碼coef之后,CABAC的node_ctx狀態並不一樣,而這會對接下來需要編碼的數據產生影響。簡單來說,你編碼了一個數據之后,就改變了CABAC的概率分布,然后在編碼接下來參數的時候,進而影響了后續coef的大小。

這里采用的是,類似於人工智能中的路勁搜索算法,又稱為trellis搜索。構建一個網格,對每一條編碼路徑的每一種可能都保留下來,並且綜合評估最終的結果。

好在CABAC的node_ctx狀態轉換並不復雜,只有8種狀態,所以這個網格搜索算法的葉子節點頂多只有8個,只要針對每個coef依次遍歷所有葉子節點,然后找出最優的編碼方案即可。

cabac node_ctx狀態轉化數組:

static const uint8_t coeff_abs_level_transition[2][8] = {
/* 編碼一個等於1的coef之后的跳轉數組 */
    { 1, 2, 3, 3, 4, 5, 6, 7 },
/* 編碼一個大於1的coef之后的跳轉數組 */
    { 4, 4, 4, 4, 5, 6, 7, 7 }
};

簡單的分析,就能看出node_ctx是一種只會增加的數值(初始狀態node_ctx=0),它的跳轉狀態是一直遞增的,永不可能往回退。(其實這和CABAC的設計策略有關系,因為一個node_ctx就代表一種概率,CABAC想要表達的是:每個系數的coef期望概率都不一樣,因為是倒序編碼,越往后coef的期望值就越大,CABAC則通過這種方式,以不同的編碼概率來最優化每一個coef)

CABAC中的trellis搜索策略

既然知道了原理,再看x264中的具體實現。在x264中為了降低計算復雜度,把node_ctx細分成了兩種狀態,當cabac只編碼了coef==1的情況,此時node_ctx只屬於[0,1,2,3],稱為ctx0,而如果cabac處理了coef>1的情況,node_ctxs取值范圍是[1,2,3,4,5,6,7], 稱為ctx1

具體如下圖:

在具體算法實現中,主要由兩個葉子節點的數組構成nodes_prev[8]和nodes_cur[8],這兩個數組都代表了cabac中的node_ctx,如果node_ctx符合 'i' --> 'j' 的轉化狀態且此時 'i' 作為起始狀態是有效的,那么就嘗試從‘i’狀態編碼當前的系數coef,並且將結果保存在 ‘j’ 狀態中。(這個過程是嚴格符合CABAC編碼的)

為了優化計算,減少搜索的葉子節點數目,x264提供了如下幾個trellis_coef{m}_{n}的函數(其中m代表需要編碼的系數coef,n代表上圖提到的ctx{n}):

  • trellis_coef0_0:
  • trellis_coef1_0:
  • trellis_coef0_1:
  • trellis_coef1_1:
  • trellis_coefn_0:
  • trellis_coefn_1:

通過這幾個trellis_coef*系列的函數,就能對每個輸入的coef和coef-1進行試探,並構建出一個trellis樹形結構。然后只要找出哪一個樹形分支的score最低,就是最優的編碼序列了。

算法中的數據結構詳解:

樹狀結構Trellis基本類型

typedef struct
{
    uint64_t score;//當前狀態的score=編碼之后的大小+圖像損耗的評估,分值越大,結果越差
    int level_idx; //指針,指向父親節點所在的內存偏移位置
    uint8_t cabac_state[4]; // 用來保存當前cabac state的數組,因為cabac編碼的特殊性,只需要4大小的數組
} trellis_node_t;

//因為只關心搜索樹的父親節點位置和編碼系數,所以這里的樹狀結構和trellis_node_t長得並不一樣,也容易造成混淆,但這里才是構建trellis樹的地方,類似於路徑搜素算法,只是搜索的工作已經交給了trellis_node_t去維護,所以這里只需要記錄最優的結果就行了
typedef struct
{
    uint16_t next;   //父親節點位置,只要逆向搜索到trellis的起始節點,就能找出最優的cabac編碼系數
    uint16_t abs_level; //cabac編碼系數coef的絕對值
} trellis_level_t;

宏SET_LEVEL所干的事情

#define SET_LEVEL(ndst, nsrc, l) {\
    if( sizeof(trellis_level_t) == sizeof(uint32_t) )\
        M32( &level_tree[levels_used] ) = pack16to32( nsrc.level_idx, l );\
    else\
        level_tree[levels_used] = (trellis_level_t){ nsrc.level_idx, l };\
    ndst.level_idx = levels_used;\
    levels_used++;\
}
  1. 創建一個tree的節點level_tree[levels_used],用來保存nsrc這個node的數據。這里只關心nsrc的父親指針(nsrc.level_idx)和nsrc所攜帶的編碼系數(l)

  2. 將當前ndst的指針(ndst.level_idx)指向nsrc所在的tree的節點位置(就是nsrc所在tree的內存偏移levels_used)

這樣就構建了一個樹形結構。

需要詳解的函數

這個函數容易讓人頭暈,它的內容並不是很復雜,但參數太多了,而且參數的命名也不是很直觀,在這里把每一個參數的含義寫清楚,方便記憶和查閱。

int trellis_coef( int j,                   //當前編碼的cabac的node_ctx
                  int const_level,         //用來判斷當前編碼系數coef是否大於1,以選擇合適的cabac ctx
                  int abs_level,           //實際的編碼系數coef,但這里並不會真正的編碼,僅僅作為SET_LEVEL的參數傳遞給trellis tree
                  int prefix,              //在cabac編碼中,如果coef<15,那么就連續往cabac里寫入coef-1個1,而這里的prefix就是當前coef需要寫入1的個數,通過prefix可以查詢一個預先處理好的列表x264_cabac_size_unary得到這些‘1’的cabac編碼之后的大小
                  int suffix_cost,         //在cabac編碼中,coef>15的部分會被轉化成Golomb編碼,然后bypass進cabac編碼器,這部分編碼大小就=golomb(coef-15)
                  int node_ctx,            //編碼系數coef之后,node_ctx變成的結果
                  int level1_ctx,          //如果編碼的coef==1,就用這個值作為cabac的ctx,此時寫入一個0,如果coef>1,也需要用到這個ctx,此時寫入一個1
                  int levelgt1_ctx,        //編碼coef>1所用的ctx,cabac需要在這個ctx里寫入coef-1個數目的‘1’和coef>15的golomb編碼,但在這個函數里並不會這么做,因為這里並不是編碼的時候。這里只需要拿到編碼之后的大小即可,所以提前算出了x264_cabac_size_unary數組,方便查詢編碼之后的大小
                  uint64_t ssd,            //圖像的損耗程度,最終的評分score=ssd+編碼之后占用的字節數
                  int cost_siglast[3],     //在cabac編碼中,經過DCT->quant->zigzag_scan之后會得到一組數據,此時這組數據中有0,也有非0值,這就需要編碼一個mask用來標記哪些數據是0和1。如果數據是0,那么在mask中用0表示,反之用1表示。這個cost_siglast就代表編碼當前coef的mask大小,其三維數值分別代表{如果當前mask位是0的編碼大小,當前mask位是1的大小,當前mask是1且是最后一個非0系數的編碼大小}。在實現psy_trellis功能的時候,這里的編碼順序和cabac是反過來的,最后一個非0系數在一開始就編碼了,所以當j==0的時候會使用cost_siglast[2],其他時候都使用cost_siglast[1]。使用這個函數不存在編碼0的時候,所以cost_siglast[0]永遠不會被用到。
                  trellis_node_t *nodes_cur,//當前編碼的node,因為cabac編碼的時候頂多有8種node_ctx,所以這里的數組長度是8
                  trellis_node_t *nodes_prev, //前一層編碼的node,這里可以理解成一個trellis樹狀結構的葉子節點,通過這個葉子節點來搜索最優的編碼路徑
                  trellis_level_t *level_tree,//結果保存在這個tree里面,僅僅用來保存結果
                  int levels_used,            //level_tree的memory分配索引
                  int lambda2,                //編碼之后的bits需要乘以這個系數,才能和ssd處在同一個“單位/量級”下進行加權
                  uint8_t *level_state        //真正的CABAC state數組。通過這個數組可以查詢到相應ctx的概率(也就是state),這里只是模擬計算編碼之后的大小,所以不能修改這個數組,真正需要修改和保存的state會放在trellis_node_t::cabac_state[4]數據結構里面。
                );

至此關於CABAC的psy_trellis實現方法就這多了,接下來看一看CAVLC的實現方式。


CAVLC中的實現方式

CAVLC編碼的大致實現:先把[尾部abs_coef==1的系數個數 + nC + 總共的非0系數]打包起來,成為一個VAVLC的header進行統一編碼,然后編碼拖尾系數的符號位,之后編碼所有非0系數,再之后編碼系數的mask。

因為每一部分的編碼都是息息相關的,改變任何一個系數coef都會引起整體產生較大的變化,故而導致它並不能使用trellis搜索算法,只能采用暴力搜索的方式來找到最優的編碼序列。

CAVLC的實現代碼就比較直白了,大致流程如下:

for coef in DCT:
    quant = coef進行正常的量化操作,得到量化之后的值
    deadzone = 也是coef的量化,但此時量化的偏移比例從正常的0.5變成了0.25,以圖提前得到一個更合適的量化值

初始化數組:coefs= 所有coef的deadzone值

while True:
   遍歷搜索一次coefs,找出一個f,使得f采用coef-1(或者coef)之后,整體的score最小,然后就把當前的f作為一次遍歷的最優結果,並且更新到coefs數組中。如果遍歷一次列表之后沒有找到更優的結果,則結束循環
            

CAVLC和CABAC想要達成的目的都是一致的:評估一下,到底是當前的系數coef更好,還是coef-1更優秀,只不過因為編碼算法的不一樣,在CABAC下能以更快速的trellis模型得到結果,而CAVLC只能采用這種蠢辦法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM