《H.264/AVC視訊編解碼技術詳解》視訊教程已經在“CSDN學院”上線,視訊中詳述了H.264的背景、標準協議和實現,並通過一個實戰工程的形式對H.264的標準進行解析和實現,歡迎觀看!

“紙上得來終覺淺,絕知此事要躬行”,只有自己按照標準文件以程式碼的形式操作一遍,才能對視訊壓縮編碼標準的思想和方法有足夠深刻的理解和體會!

GitHub程式碼地址:點選這裡

上下文自適應的變長編碼(Context-based Adaptive Variable Length Coding, CAVLC)

1. 引言

在前述的幾章節的博文/視訊中,我們已經瞭解到熵編碼是利用資訊的統計冗餘進行資料壓縮的無損編碼方法,並且已經討論過了熵編碼的基本原理、H.264中使用的語法元素解析演算法“指數哥倫布編碼”的演算法與實踐:

在我們已經實現的H.264碼流結構(如NAL Unit、Slice Header等)的解析中,大多使用定長編碼或者指數哥倫布編碼實現。而例如預測殘差等佔據碼流大量體積的資料則必須使用壓縮率更高的演算法,如CAVLC和CABAC等。前者是我們將在本文中討論的內容,後者將在後續內容中詳述。

2. CAVLC的基本原理

我們知道,CAVLC的全稱叫做“上下文自適應的變長編碼Context-based Adaptive Variable Length Coding”。所謂“上下文自適應”,說明了CAVLC演算法不是像指數哥倫布編碼那樣採用固定的碼流-碼字對映的編碼,而是一種動態編碼的演算法,因而壓縮比遠遠超過固定變長編碼UVLC等演算法。

在H.264標準中,CAVLC主要用於預測殘差的編碼。在本系列第二篇博文中我們給出了H.264的編碼流圖,其中可知,熵編碼的輸入為幀內/幀間預測殘差經過變換-量化後的係數矩陣。以4×4大小的係數矩陣為例,經過變換-量化後,矩陣通常呈現以下特性:

  1. 經過變換量化後的矩陣通常具有稀疏的特性,即矩陣中大多數的資料已0為主。CAVLC可以通過遊程編碼高效壓縮連續的0係數串;
  2. 經過zig-zag掃描的係數矩陣的最高頻非0係數通常是值為±1的資料串。CAVLC可以通過傳遞連續的+1或-1的長度來高效編碼高頻分量;
  3. 非零係數的幅值通常在靠近DC(即直流分量)部分較大,而在高頻部分較小;
  4. 矩陣內非0係數的個數同相鄰塊相關;

鑑於上述的特性3和4,針對待編碼的係數在係數矩陣中不同的位置,以及相鄰塊的有關資訊,在編碼時採用不同的碼錶進行編碼。CAVLC的這種特性,體現了命名中的“上下文自適應”的方法。

3. CAVLC的編碼流程

在CAVLC中,熵編碼不是像哈夫曼編碼等演算法一樣針對某一個碼元進行編碼,而是針對一個係數矩陣進行。假設我們希望對一個如下變換系數塊進行CAVLC編碼:

{
    3,  2, -1,  0,
    1,  0,  1,  0,
    -1, 0,  0,  0,
    0,  0,  0,  0,
}

對於一個4×4大小的變換系數矩陣進行CAVLC編碼,首先需要對其進行掃描,將二維矩陣轉化為一維陣列。如前一節所講,掃描按照zig-zag順序進行,即按照如下順序:

因此,掃描之後變換系數將進行重新排列,得到的結果為:

[3, 2, 1, -1, 0, -1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]

在編碼過程中需要注意以下重要的語法元素:

  • 非零係數的個數(TotalCoeffs):取值範圍為[0, 16],即當前係數矩陣中包括多少個非0值的元素;
  • 拖尾係數的個數(TrailingOnes):取值範圍為[0, 3],表示最高頻的幾個值為±1的係數的個數。拖尾係數最多不超過3個,若超出則只有最後3個被認為是拖尾係數,其他被作為普通的非0係數;
  • 拖尾係數的符號:以1 bit表示,0表示+,1表示-;
  • 當前塊值(numberCurrent):用於選擇編碼碼錶,由上方和左側的相鄰塊的非零係數個數計算得到。設當前塊值為nC,上方相鄰塊非零係數個數為nA,左側相鄰塊非零係數個數為nB,計算公式為nC = round((nA + nB)/2);對於色度的直流係數,nC = -1;
  • 普通非0係數的幅值(level):幅值的編碼分為prefix和suffix兩個部分進行編碼。編碼過程按照反序編碼,即從最高頻率非零係數開始。
  • 最後一個非0係數之前的0的個數(TotalZeros);
  • 每個非0係數之前0的個數(RunBefore):按照反序編碼,即從最高頻非零係數開始;對於最後一個非零係數(即最低頻的非零係數)前的0的個數,以及沒有剩餘的0係數需要編碼時,不需要再繼續進行編碼。

在上述各型別資料中,編碼非零係數的level相對最為複雜。其主要過程為:

  1. 確定suffixLength的值:
    • suffixLength初始化:通常情況下初始化為0;當TotalCoeffs大於10且TrailingOnes小於3時,初始化為1;
    • 若已經編碼好的非零係數大於閾值,則suffixLength加1;該閾值定義為3 << ( suffixLength − 1 );編碼第一個level後,suffixLength應加1;
  2. 將有符號的Level值轉換為無符號的levelCode:
    • 若level > 0,levelCode = (level << 1) - 2;
    • 若level < 0,levelCode = -(level << 1) - 1;
  3. 編碼level_prefix:level_prefix的計算方法為:level_prefix = levelCode/(1 << suffixLength);level_prefix到碼流的對應關係由9-6表示;
  4. 確定字尾的長度:字尾的長度levelSuffixSize通常情況下等於suffixLength,例外情況有:
    • level_prefix = 14時,suffixLength = 0, levelSuffixSize = 4;
    • level_prefix = 15時,levelSuffixSize = 12;
  5. 計算level_suffix的值:level_suffix = levelCode%(1 << suffixLength);
  6. 按照levelSuffixSize的長度編碼level_suffix;

在上述的係數矩陣中,非零係數個數TotalCoeffs=6,拖尾係數個數TrailingOnes=3,最後一個非零係數之前0的個數TotalZeros=2;假設nC=0。

  1. 在H.264標準協議文件的表9-5中查得,coeff_token的值為0x00000100;
  2. 編碼拖尾係數的符號,從高頻到低頻,拖尾係數符號為+、-、-,因此符號的碼流為011
  3. 編碼非零係數的幅值,三個普通非零係數分別為1、2、3;
    1. 編碼1:suffixLength初始化為0;levelCode=0;level_prefix=0,查表得對應的碼流為1;suffixLength=0,因此不對字尾編碼;
    2. 編碼2:suffixLength自增1等於1;levelCode=2;level_prefix=1,查表可知對應的碼流為01;suffixLength=1,level_suffix=0,因此後綴碼流為0
    3. 編碼3:suffixLength不滿足自增條件,依然為1;levelCode=4;level_prefix=2,查表可知對應的碼流為001;suffixLength=1,level_suffix=0,因此後綴碼流為0
    4. 綜上所述,非零係數的幅值部分的碼流為10100010
  4. 編碼最後非零係數之前0的個數TotalZeros: TotalCoeffs=6,TotalZeros=2時,在表9-7中可知碼流為111
  5. 編碼每個非零係數前0的個數:從高頻到低頻,每個非零係數前0的總個數(zerosLeft)分別為2、1、0、0、0、0,每個非0係數前連續0的個數(run_before)分別為1、1、0、0、0、0。根據標準文件表9-10可得:
    • run_before=1,zerosLeft=2,對應碼流為01
    • run_before=1,zerosLeft=1,對應碼流為0
    • 所有的0係數都已經編碼完成,無需再繼續進行編碼;

綜上所述,整個4×4係數矩陣經過CAVLC編碼之後,輸出碼流為:0000010001110100010111010。