摸一下H.264應該不會被打吧 (三)

H.264解析器實現

前注:
一、代碼最好是去看標準,我在寫的時候也發現本身的代碼有不少的錯誤。可能都沒改過來。
二、在寫的時候用到了解碼、解析、解算,這些詞語的意思都是指同一個意思即求解的意思。
三、文中全部帶「標準」之意的都是指:ISO/IEC 14496-10。
四、不保證本身的全部敘述都是正確的,可是會不斷改進的。
五、如今只是我在學習的過程當中寫的筆記和心得,以後會不斷總結起來。
六、未完,由於初學,有錯誤請指出、感激涕零html

小注:
由於接下來要先去看別的,因此這裏須要先擱置一段時間。算法

還沒補充的:CABAC普通句法元素的解析,由上下文索引 ctxIdx 求到二進制位數據的過程,實現過程的代碼。數組

記錄:
今天實在是寫不下去代碼了,因此來寫一下這段時間的總結:解析器如何實現。
2020年05月25日22:43:37
又看到了大篇大片的if-otherwise,我感受又要來寫了。
這段時間忙的很亂,一方面想往前推動度,另外一方面不少以前的工做又沒作好。
並且還常常出現「懷疑正確的結果」「手抖敲錯變量、數字」等問題。
很難受,一些很簡單的邏輯轉了大半天。
2020年05月30日22:13:27
終於從幀內16x16的殘差塊讀取走出來了,接下來是幀內16x16的殘差塊的解碼。
昨天用了半天 Debug ,結果倒是少打了一個 2,人都傻了。
可是不得不說找到錯誤的時候仍是挺開心的。目前也不知道本身解出來的係數對不對,雖然在矩陣結構上是對的,可是值差了300多,可是和每一位數據和 JM 運行獲得的數據又是同樣的,emmm。緩存

我對「解析器」的認識

我曾經簡單地看過ffmpeg的源文件,印象比較深的就是decode和parse這兩個單詞(源碼太多了看不太懂)。
並且,h264的數據是用句法元素組織起來的,因此說在解碼以前還須要將句法元素所有都讀出來。
因此我就簡單的理解爲「解碼」是將數據解算成圖片的過程,而「解析」是將句法元素解算成解碼用的數據的過程。
(只是我的理解)。
那麼做爲這個解析器,他必然須要具備讀取全部句法元素的方式。
也就是前面說過的下面這些:架構

b(8)    讀入8個bit
f(n)    讀進n個bit
i(n)/i(v)   讀進n個bit 解釋爲有符號數
u(n)/u(v)   讀進n個bit 解釋爲無符號數

ae(v)   基於上下文自適應的二進制算術熵編碼
ce(v)   基於上下文自適應的可變長熵編碼

ue(v)   無符號 指數Golomb編碼
se(v)   有符號 指數Golomb編碼
me(v)   映射   指數Golomb編碼
te(v)   截斷   指數Golomb編碼

能夠看到,裏面有不少都是做爲bit單位來讀取的。
因此首先須要實現一個bit單位讀取的解析器,對於上下文自適應的兩種編碼方式,還須要單獨用這個bit解析器去作解析器。函數

具體實現

bit讀取的實現

這個其實很簡單,首先須要作一個緩衝區,而後用相似FILE* 指針的那一套方法作一個出來(我不知道是否是有相似這樣的標準庫,就直接手動實現了)。
讀取的比特拆成兩部分:字節部分、比特部分(字節×8+比特 = 總的比特)
一、一位的讀取
核心用這個方法: result &= buf_data[charIndex] >> (8 - bitsIndex - 1); 就是比較簡單的位運算。
二、多位的讀取
用的是拆分的方法:把位數拆成3個部分:不滿8字節的頭部、8字節的中部、不滿8字節的尾部。
比特用上面的一位讀取,字節直接buf_data[i]讀取。
這裏有個小細節就是字節部分大於等於1時,字節部分-1,比特部分+8。由於可能出現(假設前面的1是開始的地方(包括1)到後面的1是結束的地方(也包括)讀取22個比特)0100 0000|0000 0000|0000 0010,也就是7+8+7的問題,若是不這樣處理就會變成(6個比特2個字節)6+8+8。因此說須要拆成3部分。防止出現把多於8的比特位算到字節位裏面去。頭部和尾部加在一塊兒應該是不超過14個比特而不是8個比特。一樣對於7+7:0100 0000|0000 0010的問題也是如此。總之對於小於16的bit統一採用一位讀取也不會很慢。對於大於16比特就必定會存在一個字節讀取了。
頭部比特位一位一位讀取,到了字節對齊位就找字節數用字節讀取,結束以後尾部繼續讀完剩下的比特位。
三、緩存區的處理
緩存區讀完、爲空的時候須要刷新緩存,索引歸零。在nal層中有一個read_next(n)的函數(用來去掉0x 00 00 03中的03),作到要讀取可是索引不變,考慮到可能在next()的時候發生緩存區刷新從而找不到原來的緩存區,因此這時加入第二個緩存區,讀完以後把索引賦以前的值,緩衝區指針還給1號。這時刷新的時候從2號緩存區讀入數據到1號,2號free()。若是隻是從h264文件而不是媒體文件中讀的話可能不須要這個過程。oop

ue(v)

ue的讀取簡單說來就是這樣:首先讀入n個0直到1,1後面再讀n位解釋爲無符號整數v。而後就是2^n-1+v,這也有另外一種解讀的方法:由於2^n就是把1左移n位,因此從1開始日後到尾的n+1位解釋成無符號數再-1,後面這種方法便於理解。
舉個例子:010 : 2^1 -1 + 0 == 1; 00101 : 2^2 - 1 + 1 = 4;學習

se(v)

se(v)是從ue(v)繼承來的(準確的說是從指數Golomb編碼繼承),可是他的值是解釋成有符號整數。優化

num_cur = bread_ue();
result = (int64_t)pow((-1), (num_cur + 1)) * (int64_t)(ceil(num_cur / 2));

大概的意思就是把ue(v)的值區間除以二,一半用來表示負數。
好比(前面是ue後面是se):0 - (0); 1 - (1); 2 - (-1); 3 - (2) ……
pow是冪函數,ceil是取上限整數。
這兩個要包含camth頭文件。ui

me(v)

這個是用來專門讀取 code_block_pattern 這個句法元素的。(不用ae(v)讀取的時候就是用這個方法讀取)
首先也是按照ue(v)的讀取拿到值。而後去查表好比下面是前面的部分(這是不完整的表)。

codeNum coded_block_pattern
Intra_4x4,Intra_8x8 Inter
0 47 0
1 31 1

這也是爲何他叫作「映射」指數Golomb編碼。就是用指數Golomb編碼方法解碼而後映射查表獲得值=,=。
查表這裏也是作成靜態表,而後按索引查表

te(v)

首先判斷句法元素的值的範圍[0,x],若是x > 1。那麼按照ue(v)的方法讀取
若是x == 1;那麼只讀一位,而後邏輯取反獲得值。
舉例子:
ue(v)中的0編碼後是1,1編碼後是010,2編碼後是011;
te(v)中的0編碼後是1,1編碼後是0,2編碼後就是011;
反過來就是解碼了。可能主要是針對0-1的優化吧,相似於bool值這種的?

ce(v)

ce(v)就是上下文自適應的可變長編碼CAVLC的描述子。
(這裏我還沒學)
CAVLC的原理能夠如今參考oskycar的這篇:H.264的CAVLC(編碼.解碼)過程詳解,寫的很詳細。跟着這篇文章的例子走一遍就能理清楚大概的實現過程。
須要注意的是,在ISO標準的句法表中,CAVLC的解碼就是上文說的這個過程。由於CAVLC解出來就是一串數據而不是一個數據。
殘差塊中輸入的參數是 (0, 15) ,因此他解出來就應該是一個16個元素的一維數組。也就是說 ce(V) 說的並非讀取一個或者幾個bit,而是直接解出一串數據。
而其中也沒有說必須是讀多少位出來。也是要查表或者計算,最後都有惟一的0-1串對應而後就能解算出來具體的值了。
也就是說ce(v)不是具體怎麼讀取、讀多少位的描述符,他說的是一種解析的方法。
之後回過頭來寫具體實現吧。這裏我也沒細看。(我這個時候看的264文件就所有都是算術二進制熵解碼)

ae(v)

ae(v)是上下文自適應的算術二進制熵編碼CABAC。
我就是看到ae(v)看到崩潰的。由於這個實在是太讓人頭大了。

在殘差塊中咱們知道,解殘差塊的函數是一個函數指針,在以前的熵解碼標誌爲1的時候須要使用到 C A B A C 的解碼方式。也就是這裏所說的 ae(v) 。簡單理解的話ae(v)是一個描述子,C A B A C 是一種編碼解碼方式,可是我以爲在解碼方面這兩者其實沒有多大差異,由於 ae(v) 用起來仍是幾乎要調用整個 CABAC 對象。

ae(v)這個描述子第一次出現不是在殘差塊中,在Slice中就有條件出現。
注:對原理性的東西我也不熟悉,如下都是我靠本身的讀書理解和網上的參考進行簡單的實現,
也就是說可能有大量錯誤。

算術編碼有一個特色就是對整個序列編碼而不是對哪個單一的元素編碼,用 h264 中的簡單來說的話就至關於把不少個句法元素編碼到一個字節串中。而後解碼的時候按照區間來解碼。由於 CABAC 會動態調整編碼區間,因此解碼的時候須要不停的更新狀態。
這些網上一抓一大把,我也不是很懂,也就很少寫了。關鍵是知道CABAC讀入的時候是讀入一個序列的數據,而不是某一個數據就行。


CABAC 大體概括爲4個步驟。

初始化:
上下文變量初始化:Slice 中第一次遇到 ae(v) 描述子,CABAC 須要進行初始化,這裏要的事情就是把初始的狀態表算出來,以後就要靠這張表來讀數據、還要維護這張表。由於 CABAC 是以 Slice(片) 做爲生命週期的,也就是說到了下一個Slice中用到 ae(v) 的時候,上一張表就已經不能再用了,就須要初始化新的表。
算術解碼引擎初始化:就兩個動做:CodiRang = 510, CodiOffset = read_bits(9)(這裏發生了數據讀取的操做),按照程序框圖來就很簡單。
二值化:我不知道這個概念究竟是在解碼仍是在編碼裏面的,標準文章中說的是輸入句法元素的請求、輸出句法元素的二值化。後來我作了一遍以後感受二值化的概念在解碼裏面好像很模糊,有時候是查表、有時候是計算。
後來想了下,以爲這其實就是一種描述結果的方法。也就是描述了什麼樣的二進制串應該是正確的結果。
每一次解碼出來的都是一個二進制數 0/1 ,經過移位求與就能夠獲得一個二進制串,這個二進制串在和二值化的某一個二進制串徹底一致的時候,那麼這就是應該輸出的值,不然就繼續解碼下一位。
句法元素二值化以後不是一個值,而是一系列值(這也是我以爲難以理解的地方:對於編碼來講就只有一個值,這個時候叫作二值化就感受很正常)。總之,在這一系列值中,對比總能夠獲得輸出的值。
因此有的句法元素是查表,有的是說明的二值化的一種算法。查表的我是先作好表,計算的則是最後對比的時候把算法嵌入進去。
二值化的過程還會產生在計算中須要的幾個變量(也是查表獲得),可是我直接寫死在方法裏面了。
計算值:
單單從主要架構來講的話:咱們當前須要解的二進制位的索引 binIdx 從0開始每次自增1,每個 binIdx 均可以獲得 ctxIdx ,而後用 ctxIdx 去查咱們初始化生成的那張表,而後通過一系列的解算過程,最後獲得一個二進制數,而後就是上面所說的二值化的對比和輸出的過程了。
後處理:
一個是解碼完成的後處理;
還有即是解碼中的後處理,以前說過,每次解出來一個二進制位以後都須要動態維護表;還有在解碼區間精度不夠的時候重整化等等。

總結
去掉各類細節的總結就是:
每次解一個binIdx上的二進制位,binIdx和其餘參數獲得ctxIdx,ctxIdx查表解算獲得二進制位,二進制位組成一個串,是二值化中的某個串,那就獲得這個值。


實現過程

初始化

初始狀態表的初始化

for (size_t j = ctxIdxRangOfSyntaxElements[i][ctxRangeCol][0]; j <= ctxIdxRangOfSyntaxElements[i][ctxRangeCol][1]; j++)
{   //j is ctxIdx
    m = mnValToCtxIdex[j][mncol][0];
    n = mnValToCtxIdex[j][mncol][1];
    if(!(ctxIdxRangOfSyntaxElements[i][ctxRangeCol][0] == 0 && ctxIdxRangOfSyntaxElements[i][ctxRangeCol][1] == 0))
    {
        preCtxState = (uint8_t)Clip3(1, 126, ((m *  Clip3(0, 51, ((int)lifeTimeSlice->ps.SliceQPY >> 4)))) + n);
        if(preCtxState <= 63)
        { 
            pStateIdx = 63 - preCtxState;
            valMPS = 0;
        }
        else 
        {
            pStateIdx = preCtxState - 64;
            valMPS = 1;
        }
        //這裏賦值
        ctxIdxOfInitVariables->set_value(j, 0, pStateIdx);
        ctxIdxOfInitVariables->set_value(j, 1, valMPS);
    }
}

由於每種片的初始化的變量區間都不一樣,因此我先作了一個 句法元素-區間 表(這個表是標準表的改動),4種片從上到下分別從下限初始到上限,而後到下一行。最後一欄的兩個數字加起來就是我自定的句法元素的表示數字好比1, 1,表示11號mb_skip_flag句法元素。
在上面咱們說道ae(v)這個描述子是不須要參數的,可是實際上他是根據句法元素的種類來決定用什麼方法的,因此咱們仍是得傳一個表示句法元素是哪個句法元素的參數。
(這裏沒有作成枚舉類型,由於考慮到可能還有其餘參數的問題)

static const uint16_t ctxIdxRangOfSyntaxElements[43][5][2] = {
                        //up       down                            //pre suffic 
         //SI             //I              //P SP          //B
    { {udf  , udf  },  {udf  , udf  }, {11, 13      }, {24, 26    }, {1, 1} },//slice_data()            mb_skip_flag
    { {70, 72      },  {70, 72      }, {70, 72      }, {70, 72    }, {1, 2} },//                        mb_field_decoding_flag
    { {0, 10       },  {3, 10       }, {14, 20      }, {27, 35    }, {2, 2} },//macroblock_layer()      mb_type 
    { {na, na      },  {399, 401    }, {399, 401    }, {399, 401  }, {2, 1} },//                        transform_size_8x8_flag
    { {73, 76      },  {73, 76      }, {73, 76      }, {73, 76    }, {2, 2} },//                        coded_block_pattern (luma)
    { {77, 84      },  {77, 84      }, {77, 84      }, {77, 84    }, {2, 3} },//                        coded_block_pattern (chroma
    { {60, 63      },  {60, 63      }, {60, 63      }, {60, 63    }, {2, 4} },//                        mb_qp_delta
    { {68, 68      },  {68, 68      }, {68, 68      }, {68, 68    }, {3, 5} },//mb_pred()               prev_intra4x4_pred_mode_flag
    { {69, 69      },  {69, 69      }, {69, 69      }, {69, 69    }, {3, 1} },//                        rem_intra4x4_pred_mode
    { {na, na      },  {68, 68      }, {68, 68      }, {68, 68    }, {3, 2} },//                        prev_intra8x8_pred_mode_flag
    { {na, na      },  {69, 69      }, {69, 69      }, {69, 69    }, {3, 3} },//                        rem_intra8x8_pred_mode
    { {64, 67      },  {64, 67      }, {64, 67      }, {64, 67    }, {3, 4} },//                        intra_chroma_pred_mode
    { {udf  , udf  },  {udf  , udf  }, {54, 59      }, {54, 59    }, {4, 1} },//mb_pred()&sub_mb_pred() ref_idx_l0                    
    { {udf  , udf  },  {udf  , udf  }, {udf  , udf  }, {54, 59    }, {4, 2} },//                        ref_idx_l1                                  
    { {udf  , udf  },  {udf  , udf  }, {40, 46      }, {40, 46    }, {4, 3} },//                        mvd_l0[][][0] 
    { {udf  , udf  },  {udf  , udf  }, {udf  , udf  }, {40, 46    }, {4, 4} },//                        mvd_l1[][][0] 
    { {udf  , udf  },  {udf  , udf  }, {47, 53      }, {47, 53    }, {4, 5} },//                        mvd_l0[][][1] 
    { {udf  , udf  },  {udf  , udf  }, {udf  , udf  }, {47, 53    }, {4, 6} },//                        mvd_l1[][][1] 
    { {udf  , udf  },  {udf  , udf  }, {21, 23      }, {36, 39    }, {5, 1} },//sub_mb_pred()           sub_mb_type[]               
    { {85, 104     },  {85, 104     }, {85, 104     }, {85, 104   }, {6, 1} },//residual_block_cabac( ) coded_block_flag    61
    { {460, 483    },  {460, 483    }, {460, 483    }, {460, 483  }, {6, 1} },//                                                                         
    { {udf  , udf  },  {1012, 1023  }, {1012, 1023  }, {1012, 1023}, {6, 1} },//                         
    { {105, 165    },  {105, 165    }, {105, 165    }, {105, 165  }, {6, 2} },//                        significant_coeff_flag[ ]  62
    { {277, 337    },  {277, 337    }, {277, 337    }, {277, 337  }, {6, 2} },//                         
    { {udf  , udf  },  {402, 416    }, {402, 416    }, {402, 416  }, {6, 2} },//                         
    { {udf  , udf  },  {436, 450    }, {436, 450    }, {436, 450  }, {6, 2} },//                         
    { {udf  , udf  },  {484, 571    }, {484, 571    }, {484, 571  }, {6, 2} },//                         
    { {udf  , udf  },  {776, 863    }, {776, 863    }, {776, 863  }, {6, 2} },//                         
    { {udf  , udf  },  {660, 689    }, {660, 689    }, {660, 689  }, {6, 2} },//                         
    { {udf  , udf  },  {718, 747    }, {718, 747    }, {718, 747  }, {6, 2} },//                         
    { {166, 226    },  {166, 226    }, {166, 226    }, {166, 226  }, {6, 3} },//                        last_significant_coeff_flag[ ] 63
    { {338, 398    },  {338, 398    }, {338, 398    }, {338, 398  }, {6, 3} },//                         
    { {udf  , udf  },  {417, 425    }, {417, 425    }, {417, 425  }, {6, 3} },//                         
    { {udf  , udf  },  {451, 459    }, {451, 459    }, {451, 459  }, {6, 3} },//                         
    { {udf  , udf  },  {572, 659    }, {572, 659    }, {572, 659  }, {6, 3} },//                         
    { {udf  , udf  },  {864, 951    }, {864, 951    }, {864, 951  }, {6, 3} },//                         
    { {udf  , udf  },  {690, 707    }, {690, 707    }, {690, 707  }, {6, 3} },//                         
    { {udf  , udf  },  {748, 765    }, {748, 765    }, {748, 765  }, {6, 3} },//                         
    { {227, 275    },  {227, 275    }, {227, 275    }, {227, 275  }, {6, 4} },//                        coeff_abs_level_minus1[ ] 64
    { {udf  , udf  },  {426, 435    }, {426, 435    }, {426, 435  }, {6, 4} },//                         
    { {udf  , udf  },  {952, 1011   }, {952, 1011   }, {952, 1011 }, {6, 4} },//                         
    { {udf  , udf  },  {708, 717    }, {708, 717    }, {708, 717  }, {6, 4} },//                         
    { {udf  , udf  },  {766, 775    }, {766, 775    }, {766, 775  }, {6, 4} } //                         
};

這裏的na是在標準表裏面的na,表示不存在的類型,udf是我定義的,是在標準表裏面 爲空(沒有數據)
的數據。這兩個的值都是0,當遇到上下限都是0的時候就不初始化。(雖然這裏的區間數據可能不對,可是咱們不去使用它就好了,實際上片類型限定以後也不會使用到區間之外的數)

而後ctxIdx和m n的映射表一共是1024個數據,從0到1023。每個ctxIdx對應一個m和一個n。上面這張表就是ctxIdx的區間,m n是在初始化中要用到的變量。由於cabac的生命週期是slice,咱們還要給cabac傳一個slice對象來拿到這個片的量化參數偏移。SliceQPY(或者直接把這個參數傳進來,可是個人cabac對象是在slice前面定義的)
(由於 cabac 要用到不少的數據:包括片、宏塊,因此我就都傳進來了。)
第一句就用到了全部須要的數值:

preCtxState = (uint8_t)Clip3(1, 126, ((m *  Clip3(0, 51, ((int)lifeTimeSlice->ps.SliceQPY >> 4)))) + n);

Clip3是求值在區間內的值,過上限返回上限,過下限返回下限,不然返回這個值。我用了一個模板函數

template <typename T>
T Clip3(T lower, T upper, T value)
{
    if(value >= upper) return upper;
    else if(value <= lower) return lower;
    else return value;
}

至於這張表的數據類型,由於數組指針用起來有點不懂,因此我本身作了一個模板二維數組類。
至此初始表的計算完成(也就是上下文變量的初始化完成。)ctxIdx對應pStateIdx、valMPS。
pStateIdx是決定狀態的,valMPS是用來求值的。

引擎初始化
注意到這裏讀取到值就好了,還有一點就是codIOffset從數據流讀入的數據不能是511 510
只是解碼的話應該不用關心這個問題。

codIRange = 510;
codIOffset = p->read_un(9);
return 1;

二值化

U:一元二值化

Value of syntax element Bin string
0 (I_NxN)0
1 1 0
2 1 1 0
3 1 1 1 0
4 1 1 1 1 0
5 1 1 1 1 1 0
binIdx 0 1 2 3 4 5

n個1,遇到0就結束,binIdx的值就是句法元素的值

TU:截斷一元二值化
須要輸入一個cMax,
若是句法元素值等於cMax,那麼就是二值串就是全部的位都爲1,長度爲cMax的二值串。
句法元素值小於cMax,那麼就是一元二值化U的二值化方法。

UEGk:TU 和 k階指數Golomb編碼 串連二值化
須要一個 signedValFlag 和 uCoff。
signedValFlag 指示是否編碼(正負數的)符號, uCoff 指明 TU 的最大長度 cMax 。
UEGk自己就包含一個前綴和一個後綴,
前綴是TU,前綴位數是Min( uCoff, Abs(synElVal)),用於TU計算的值爲:cMax = uCoff。
後綴是k階指數Golomb編碼,這裏的k來自句法元素(表中有說明),好比後面的解係數絕對值的時候是 UEG0 ,後綴的解法也有標準僞代碼。因此有必要先看看標準中給出的二值化過程。

if( Abs(synElVal) >= uCoff ) 
{ 
  sufS = Abs( synElVal ) − uCoff 
  stopLoop = 0 
  do 
  { 
      if( sufS >= ( 1 << k ) ) 
      { 
          put( 1 ) 
          sufS = sufS − ( 1<<k ) 
          k++ 
      } 
      else 
      { 
          put( 0 )  
          while( k− − ) 
            put( ( sufS >> k ) & 1 ) 
          stopLoop = 1 
      } 
  } while( !stopLoop ) 
} 
if( signedValFlag && synElVal ! = 0) 
  if( synElVal > 0 ) 
    put( 0 ) 
  else 
    put( 1 )

這個二值化的過程簡單說明了 k 階指數 Golomb 是如何被編碼的。(前面提到二值化並非解碼的過程,而是解碼完了以後的結果確認過程。)
首先是sufS = Abs( synElVal ) − uCoff;也即句法元素的絕對值(再次注意這裏的值不是要去解算的值,而是用來對比確認是否是結果的值)減去前綴的最大值(因此前綴沒有到最大值的時候沒有求後綴這個過程)。
再看最後一個 if if( signedValFlag && synElVal ! = 0) 也就是要不要對符號編碼,若是要而且當前的值不爲0的話,後面還有一位編碼的符號位。
中間層的代碼分析:

sufS = Abs( synElVal ) − uCoff 
  stopLoop = 0 
  do 
  { 
      if( sufS >= ( 1 << k ) ) 
      { 
          put( 1 ) 
          sufS = sufS − ( 1<<k ) 
          k++ 
      } 
      else 
      { 
          put( 0 )  
          while( k− − ) 
            put( ( sufS >> k ) & 1 ) 
          stopLoop = 1 
      } 
  } while( !stopLoop )

仍是用一個例子來講明:
我自定義的:數字前面帶Bx表示一個二進制數
好比如今須要 0階指數Golomb 編碼 Bx1100 ,不編碼符號位 signedValFlag = 0。
(這裏略掉了前綴的截斷編碼)

條件 k 輸出位 當前串 當前值
條件 sufS >= ( 1 << k ) k 輸出位 當前串 當前值sufS −= (1<<k)
Bx1100 >= Bx1 0 1 1 Bx1011
Bx1011 >= Bx10 1 1 11 Bx1001
Bx1001 >= Bx100 2 1 111 Bx0101
Bx0101 >= Bx1000(不成立) 3 0 1110 Bx0101
條件 k != 0 k 輸出位 當前串 當前值sufS
(Bx0101 >> 2) & 1 2 1 11101 Bx0101
(Bx0101 >> 1) & 1 1 0 111010 Bx0101
(Bx0101 >> 0) & 1 0 1 1110101 Bx0101

二值化以後獲得 1110101 串。值爲 Bx1100(十進制爲12) signedValFlag = 0

因此反過來解碼過程:
首先讀入的是 n 個1直到0,不包含0的1串解釋成無符號數 v1 ,
而後再讀入 n 位解釋成無符號數 v2 ,
結果就是: v1 + v2 。
若是是 1階 指數Golomb 編碼 Bx1100 ,不編碼符號位 signedValFlag = 0。

條件 k 輸出位 當前串 當前值
條件 sufS >= ( 1 << k ) k 輸出位 當前串 當前值sufS −= (1<<k)
Bx1100 >= 0x10 1 1 1 Bx1010
Bx1010 >= 0x100 2 1 11 Bx0110
Bx0110 >= 0x1000(不成立) 3 0 110 Bx0110
條件 k != 0 k 輸出位 當前串 當前值sufS
(Bx0110 >> 2) & 1 2 1 1101 Bx0101
(Bx0110 >> 1) & 1 1 1 11011 Bx0101
(Bx0110 >> 0) & 1 0 0 110110 Bx0101

二值化以後獲得 110110 串。值爲 Bx1100 (十進制爲12) signedValFlag = 0
一樣:反過來,先讀入 n 個1直到0,不帶0的串(11)解釋成無符號數v1,這裏有個小細節在後面細說。
在這裏:v1 = Bx11,從上面咱們看到 k 是從 1 開始的。因此須要左移一位獲得v1:
v1 <<= 1;(即:v1 = (Bx11 <<= 1;)) 獲得 Bx110 也就是 v1 == 6;
而後讀入 n + 1 爲解釋成無符號整數 v2:
v2 = Bx110; 因此 v2 == 6;
結果:v = v1 + v2 = 12;

小細節:二值化的時候,前面的1是從低位往高位二值化的,好比第二個例子的的串 110110 前面的兩位 11,第一個 1 是在 2^1 位上,而第二個是在 2^2 位上(後面會講到的的前綴後綴的問題),因此實際上應該反過來表示,可是由於都是1,因此反過來也是11 (因此實際上沒有影響,直接讀也行)。後面的 110 由於 k 是從高到低而後求與的,因此就是 1×2^2+1×2^1+1×2^0 = 6;(注意到先寫入的是高位仍是低位就行。不過這個也看本身的方法是怎麼寫的。後面的討論會詳細說。)
總結
k 階指數 Golomb 是先讀入 n 個1直到0,而後左移 k 位獲得 v1 值。
而後讀入 n + k 位解釋成無符號整數 v2,
v = v1 + v2;
長度:(n) + (1) + (n + k)
組成:前面的 n 位 1 + 中間的 0 位 + 後面的 n + k 位數據

帶上 截斷的一元二值化 前綴(這裏是聯合二值化),
若是沒有截斷就不會有會面的 k 階 Golomb,
因此求解的時候就是:
句法元素的值 = 截斷二值化的值 + k 階指數 Golomb 二值化的值
(前面咱們提到 k 階指數 Golomb 二值化以前要先減去截斷二值化的長度,而截斷二值化的長度就是他的值)。
對於這種聯合二值化來講:
由於二值化是先求一個句法元素的二值化,而咱們如今解碼這個句法元素的時候,每求出來一位,總不能把全部可能的值二值化以後而後和咱們求出來的串對比吧(有些確實是和全部可能的值對比,好比 mb_type 這個句法元素就是,可是對於這種聯合二值化的不行,由於他編碼的基本都是很大的數而不是不多的幾個數。),
因此這個時候咱們直接把二值化的方法嵌入到對比的結果裏面去,看咱們求出來的串是否是一個在限定條件下合法的二值串就行。

FL:定長編碼
須要輸入一個cMax,定長的長度爲fixedLength = Ceil( Log2( cMax + 1 ) )
定長的二值串的值就是句法元素的值。好比定長2,串爲10,就表示 2 這個值。

關於前綴(prefix)和後綴(suffix)的問題
因爲咱們解二進制位是 binIdx 從 0 開始解的,因此先解出來的位在低位,後解出來的位在高位,而前綴和後綴是從先解仍是後解的方向來看的。
解完的的二進制串是從0開始索引的,咱們須要反過來纔是咱們認知數字的順序,這時候高位在前,低位在後,因此後綴在前,前綴在後:
舉個例子:咱們解出來的二進制串是 1 0 0 1 0
在程序中就是這個樣子(數據不必定使用字符數組表示,這裏只是這樣舉例而已。):

char ch[5];
ch[0] = 1; ch[1] = 0; ch[2] = 0; ch[3] = 1; ch[4] = 0;
//上面的順序是: binIdx 從0->4 (解碼的時候binIdx是從0往上自增的)

這個時候 ch[0] 是 2^0 位,值爲 1, ch[3] 是 2^4 位,值爲 16 。
可是在人類認知的角度上是反過來的,因此 咱們須要反過來看。
就是 01001 長度爲5,值爲17。
因此要注意二進制串的「從低到高」 和 咱們看待值的 「從低到高」 的方向,前者是從左到右,後者反之。
因此前綴先入,後綴後入。後綴就在高位上面了。(注意到這一點就行,由於二值化中的表是按照低位到高位從左往右排序的,這個時候前綴在前面 / 低位)

因此這裏就有兩種二進制位往數據裏面寫的方法:

好比如今有一個數據串 10011 在文件中,咱們須要把他讀出來,而且在文件中咱們的文件指針FILE* fp先遇到的位是最低的位2^0 。也就是值應該是 Bx11001 也就是16進制的1九、十進制的25 。
一種方法是
把讀出來的二進制位左移到當前串的最高位上:也就是會前後依次獲得串:1 01 001 1001 11001,假設咱們每次讀出來都把值賦給一個 int ,那麼如今就是正常的順序,int 的值就是這個數據的值。
另外一張方法是:
把當前整個串左移一位,把新解算出來的二進制位放到最低位上,也就是依次會獲得串:1 10 100 1001 10011。
一樣把他賦給一個 int 那麼這個 int 的值就不是這個數據的值了。

計算值
由 ctxIdx 求二進制位,這個過程在標準中有詳細解釋,還有框圖,因此如今先不寫。
後處理
若是讀入的句法元素是mb_type而且值是I_PCM,那麼初始化引擎。
讀取過程當中的後處理是

流程

uint32_t result;
//初始化
if(state == 0)
{
    init_variable();
    init_engine();
    state = 1;
}
result = Decode(syntax);
//後處理
if(syntax == 22 && (MbTypeName)result == I_PCM) {init_engine();}
printf(">>cabac: result of |%-5d| is : |%3d|\n", syntax, result);
return result;

由於所有初始化的過程只在第一次讀到描述子 ae(v) 的時候調用,因此作一個 CABAC 狀態就行,讓他在片的第一次使用以後就再也不所有初始化。。讀到片尾的時候就把狀態換回來。(會有一個句法元素 end_of_slice 標誌片結尾的,固然也能夠作在片的方法中,總之道理都同樣。)
syntax == 22;22號是我定義的 mb_type 句法元素,若是是求 mb_type 而且它的值爲 I_PCM (這裏是25,也是查表來的作成了枚舉。)那麼須要引擎初始化。

普通CABAC句法元素的解碼方法:

還沒寫。後面補充。

殘差系CABAC句法元素的解碼方法:

這些句法元素有名字有一個特徵就是帶「cat」字樣。

首先是帶ctxIdxBlockCat的公式
ctxIdx = ctxInc(ctxIdxBlockCat) + ctxIdxBlockCatOffset(ctxIdxBlockCat) + ctxIdxOffset
等式右邊的參數分別是:以 ctxIdxBlockCat 索引的 ctxInc、ctxOffset、和總的 ctxOffset。
這裏能夠理解爲:帶 cat 後綴的數值是分了第二級的索引和偏移:
ctxIdx:上下文一級索引
ctxIdxOffset上下文一級索引的偏移
ctxInc上下文二級索引
ctxIdxBlockCatOffset上下文二級索引的偏移
因此第一個公式解讀爲:
一級索引 = 二級索引 + 二級偏移 + 一級偏移

code_block_flag:

(當前塊多是16x16,塊,多是8x8塊,多是4x4塊,須要根據實際狀況判斷,這個實際上就是cabac 在解算殘差塊的時候自己就會有的一個句法元素,因此每進一次殘差的cabac函數,都會有一個,而殘差有多是1個DC+16個AC 或者 4個8x8 或者 16個4x4 因此這個句法元素的數量也須要根據實際判斷)

須要注意的是這個句法元素須要存儲起來,由於推導這個 code_block_flag 的時候用到了相鄰塊的code_block_flag 。

這個值用來表示當前塊的係數是否是被編碼的數值,
若是不是的話那麼全部的係數都賦值爲0。
首先
是查 P283 表肯定 ctxBlockCat ,好比咱們如今以亮度DC變換系數爲例子查下面的表。獲得他的ctxBlockCat = 0;
表我簡化了一下用來舉例子,(這裏只須要找到對應的變量名就好了)

Block description maxNumCoeff ctxBlockCat
亮度DC 塊變換系數 Intra16x16DCLevel 16 0
亮度AC 塊變換系數 Intra16x16ACLevel[ i ] 15 1
16亮度 塊變換系數 LumaLevel4x4[ i ] 16 2
色度DC 塊變換系數 (ChromaArrayType is equal to 1 or 2 ChromaDCLevel 4 * NumC8x8 3
色度AC 塊變換系數 (ChromaArrayType is equal to 1 or 2 ChromaACLevel 15 4
64亮度 塊變換系數 LumaLevel8x8[ i ] 64 5

上面是4:2:0用的,一共6種。下面是4:4:4用的,由於4:4:4中三種亮度、色度Cb、色度Cr都是相同的解碼方法,因此這些也相同,一共(包含上面的亮度的方法)12種。

Block description maxNumCoeff ctxBlockCat
Cb DC 塊變換系數 (ChromaArrayType == 3) CbIntra16x16DCLevel 6 6
Cb AC 塊變換系數 (ChromaArrayType == 3)CbIntra16x16ACLevel[ i ] 5 7
16 Cb 塊變換系數 (ChromaArrayType == 3) CbLevel4x4[ i ] 6 8
64 Cb 塊變換系數 (ChromaArrayType == 3) CbLevel8x8[ i ] 4 9
Cr DC 塊變換系數 (ChromaArrayType == 3) CrIntra16x16DCLevel 6 10
Cr AC 塊變換系數 (ChromaArrayType == 3)CrIntra16x16ACLevel[ i ] 5 11
16 Cr 塊變換系數 (ChromaArrayType == 3) CrLevel4x4[ i ] 6 12
64 Cr 塊變換系數 (ChromaArrayType == 3) CrLevel8x8[ i ] 4 13

而後去查P273的 ctxIdxBlockCatOffset-ctxBlockCat 映射表,獲得 ctxIdxBlockCatOffset 的值,例如這裏爲0

ctxBlockCat 0 1 2 3 4 5 6 7 8 9 10 11 12 13
coded_block_flag 0 4 8 12 16 0 0 4 8 4 0 4 8 8
significant_coeff_flag 0 15 29 44 47 0 0 15 29 0 0 15 29 0
last_significant_coeff_flag 0 15 29 44 47 0 0 15 29 0 0 15 29 0
coeff_abs_level_minus1 0 10 20 30 39 0 0 10 20 0 0 10 20 0

也就是ctxIdxBlockCatOffset = 0, ctxBlockCat = 0;
而後是分狀況討論:由於不一樣的狀況輸入不一樣。
ctxBlockCat 的值:

先求 ctxIdxInc(ctxBlockCat): 以 ctxBlockCat 爲索引的 ctxIdxInc 值

輸入參數:
前面是ctxBlockCat的值,後面是須要的額外參數
0 6 10:沒有額外的輸入參數:
這個是求16x16塊的DC係數,由於就是整個宏塊,因此不須要額外的參數
1 2 :4x4亮度塊索引
這個是求AC變換系數 或者 4x4的亮度塊變換系數,因此須要一個4x4索引。對於宏塊來講,AC係數一共有15個,4x4塊有16個、這兩個由4x4塊索引來映射。因此這兩個的求算方法同樣。
3:色度組件索引:
這個用來求色度塊的DC係數,色度塊有兩個Cb、Cr。分別是0 、 1
色度塊的DC係數也是直接對整個宏塊的色度DC係數,因此沒有額外的輸入參數
4:色度塊的4x4塊索引、色度塊組件索引
求色度的AC變換系數,
5:8x8塊索引
求解8x8塊變換系數。

總結:
DC係數直接對整個塊求,AC係數對整個宏塊的4x4塊求(注意AC係數只有15個),
色度塊還要色度塊組件的索引來指示是Cb仍是Cr

0 6 10:無額外的輸入參數,
首先是推導 transBlockN :
下面的 N 意思是 A 塊、 B 塊。由於方法通用因此用 N 代替
相鄰宏塊的推導,結果註冊給 mbAddrN (也就是左邊的A,上面的B)
transBlockN是當前塊的DC塊。

if( mbAddrN 可用 && mbAddrN 爲 Intra_16x16)
    if(ctxBlockCat == 0)  transBlockN = mbAddrN->luma DC block;
    if(ctxBlockCat == 6)  transBlockN = mbAddrN->Cb   DC block;
    if(ctxBlockCat == 10) transBlockN = mbAddrN->Cr  DC block;
else  transBlockN = 不可用 ;

而後求變量 condTermFlagN ;

if( {
      1 mbAddrN 不可用 && mbAddrN 是使用幀間預測編碼 
      2 mbAddrN 可用   && transBlockN 不可用 && mbAddrN不是I_PCM宏塊
      3 當前宏塊是幀內編碼 && constrained_intra_pred_flag == 1 && mbAddrN 可用 && 是幀間編碼 && 使用片分割
    }中的任一條件 == true
  )
  condTermFlagN = 0;
else if(
        {
          1 mbAddrN 不可用 && 當前宏塊是幀內預測宏塊
          2 mbAddrN是I_PCM宏塊
        }中的任一條件 == true
       )
  condTermFlagN = 1;
else
  condTermFlagN =  transBlockN->(已經解碼的)coded_block_flag;

而後求 ctxIdxInc(ctxBlockCat):
ctxIdxInc( ctxBlockCat ) = condTermFlagA + 2 * condTermFlagB
至此ctxIdxInc(ctxBlockCat) ctxIdxBlockCatOffset ctxIdxOffset 所有獲得,直接求和,獲得ctxIdx
ctxIdxBlockCatOffset(ctxBlock)表示對應索引的ctxIdxBlockCatOffset。

ctxIdx = ctxIdxInc(ctxBlockCat) + ctxIdxBlockCatOffset(ctxBlockCat) + ctxIdxOffset;
固然上面只是一個例子,其餘狀況都在標準中有說明。

對於significant_coeff_flag last_significant_coeff_flag 和 coeff_abs_level_minus1這三個都是數組。

輸入ctxIdxOffset和binIdx。輸出ctxIdxInc

對於 significant_coeff_flaglast_significant_coeff_flag

首先:levelListIdx 賦值爲上面數組的相應的索引,由於這兩個元素的上下文建模用到了句法元素所在的位置。
也就是說,在解這三個數組的時候,分別有for循環,循環的時候的索引i賦值給這裏的levelListIdx。
這裏的數組是指的從解碼中獲得的一維數組(僅僅是數據),爲了方便理解,咱們先簡單認爲 i 就是數組中的位置。

對於 significant_coeff_flag 和 last_significant_coeff_flag
ctxBlockCat != 3 5 9 13 的狀況:ctxIdxInc = levelListIdx
對於 significant_coeff_flag 和 last_significant_coeff_flag
ctxBlockCat = = 3 的狀況:ctxIdxInc = Min( levelListIdx / NumC8x8, 2)
對於significant_coeff_flag 和 last_significant_coeff_flag 在 8x8 亮度塊 Cb塊 Cr塊
ctxBlockCat = = 5, 9, 13等等的時候查表肯定ctxIdxInc

對於 coeff_abs_level_minus1, ctxIdxInc 由下面的式子指定

numDecodAbsLevelEq1表示係數絕對值等於1的累計的數量
numDecodAbsLevelGt12示係數絕對值大於1的累計的數量
這個也是對於當前塊來講的,標準裏面具體強調了這兩個值屬於同一個解碼過程所在的塊。
也就是說:這倆是當前塊裏面的數據的實時統計量。
接下來
ctxIdxInc 用下面的代碼獲得。
Min 表示最小值,能夠直接用三目運算符,也能夠作成函數。

if(binIdx == 0)
    ctxIdxInc = ((numDecodAbsLevelGt1 != 0) ? 0: Min(4, 1 + numDecodAbsLevelEq1)
else
    ctxIdxInc = 5 + Min(4 − ((ctxBlockCat == 3)?1 : 0), numDecodAbsLevelGt1)

輸出 ctxIdxInc 以後,再根據 cat 後綴的偏移,總的偏移就獲得 ctxIdx ,而後直接解碼就獲得值了。
這裏的 ctxIdxInc 並不帶(ctxBlockCat),由於這裏 ctxIdxInc 算出來就是惟一的數值而不是查表出來的,因此不用 ctxBlockCat 這個變量來索引。

這個句法元素的二值化是 前綴的 ctxIdxOffset 由 UEG0 給出。參數爲: signedValFlag=0, uCoff=14,其中前綴的 ctxIdxOffset = 227 ,後綴的 ctxIdxOffset 使用是旁路解碼。

以 16x16 的 DC 係數爲例子。
查表 9-42
ctxBlockCat = 0;句法的索引

咱們先解算前綴也就是 截斷一元二值化部分:

首先查二值化表 9-34 獲得一級偏移:ctxIdxOffset
ctxIdxOffset = 277;
查二級偏移表 9-40 獲得二級偏移:ctxIdxBlockCatOffset
ctxIdxBlockCatOffset = 0;
求解獲得二級上下文索引:ctxIdxInc
對於每個binIdx,由上面的代碼獲得 ctxIdxInc
由此求出上下文索引 ctxIdx
ctxIdx = ctxIdxInc + ctxIdxBlockCatOffset + ctxIdxOffset
求二進制位
而後根據 ctxIdx 用 CABAC 求出當前位上的二進制數。
結果確認(二值化對比):
而後確認結果是否是二值化,
對於由 binIdx 從 0 到 cMax 區間求出每個二進制位。
每次求一個二進制位並確認是這個結果以後,就輸出這個二進制串。不然 binIdx 自增1,繼續重複上面的過程。

而後求後綴也就是 k 階指數 Golomb 部分(求解的方法在上面給出了。)
因爲後綴由旁路解碼給出,因此求後綴的部分不須要參數,直接使用旁路解碼獲得二進制位。

聯合二值化的值就是前綴的值加上後綴的值,若是咱們求出來的串合法,那麼就能夠按照聯合二值化中提到的解讀方法解讀咱們求出來的串而且輸出這個值了。

對於 coeff_sign_flag,coeff_sign_flag也是和上面相同大小的數組,可是這個句法元素在表裏面屬於旁路解碼模式。前面咱們看到,旁路解碼不須要ctxIdx這個上下文索引變量,只須要一個標誌位 bypassFlag == 1 讓解碼進入到旁路解碼就行,傳參的時候ctxIdx隨便傳一個數,把第二個參數(也就是bypassFlag)傳1就好了。(由於旁路解碼只須要旁路標誌位爲1)直接就能夠獲得他的值,由於他的二值化是FL cMax=1,計算就獲得他只有二進制一位,因此能夠直接解就出來了。

相關文章
相關標籤/搜索