HEVC中的CAVLC
CAVLC 基於上下文的自適應變長編碼
首先,HEVC的熵編碼使用了兩種算術編碼:CABAC和CAVLC。CAVLC主要用於編碼SEI、參數集、片頭等,剩下的所有數據和語法元素均使用CABAC來編碼。
HEVC標準文檔中使用到的一些描述符(描述符也表示操作方法):
1、ae(v) 使用cabac
2、b(8) 讀進連續的8 bit
3、f(n) 讀進連續的n bit
4、u(n) 讀進連續的n bit,解碼爲無符號整數
5、se(v) 有符號指數哥倫布編碼
6、ue(n) 無符號哥倫布指數編碼
HEVC中與CAVLC有關的類
HEVC中與CAVLC有關的類:SyntaxElementWriter、SEIWriter、TEncCavlc
1、SyntaxElementWriter語法元素寫入者,定義了CAVLC的幾種基本算法
2、SEIWriter,SEI寫入者,由於SEI使用CAVLC來編碼,因此SEIWriter繼承自SyntaxElementWriter
3、TEncCavlc,CAVLC編碼器,主要用於編碼參數集以及slice頭部等,繼承自SyntaxElementWriter
HEVC中CAVLC的幾種基本的算法
CAVLC只是一個統稱,它包含了多個算法:
1、零階無符號哥倫布指數編碼
2、零階有符號指數哥倫布編碼
3、不編碼直接寫入若干比特
4、不編碼直接寫入一個比特
class SyntaxElementWriter
{
protected:
TComBitIf* m_pcBitIf;
SyntaxElementWriter()
:m_pcBitIf(NULL)
{};
virtual ~SyntaxElementWriter() {};
// 設置比特流
Void setBitstream ( TComBitIf* p ) { m_pcBitIf = p; }
Void xWriteCode ( UInt uiCode, UInt uiLength ); // 不編碼直接寫入若干比特
Void xWriteUvlc ( UInt uiCode ); // 零階無符號哥倫布指數編碼
Void xWriteSvlc ( Int iCode ); // 零階有符號指數哥倫布編碼
Void xWriteFlag ( UInt uiCode ); // 不編碼直接寫入一個比特
UInt xConvertToUInt ( Int iValue ) { return ( iValue <= 0) ? -iValue<<1 : (iValue<<1)-1; }
};
指數哥倫布編碼的理論
(1)把N轉換爲二進制數,去掉最低的k個比特位,然後加上1
(2)計算留下的比特數,把這個數減去1,這就是需要增加的前綴0的個數
(3)把步驟(1)中去掉的k個比特位補回比特串的尾部。
零階指數哥倫布編碼
根據上面的定義,可以得到零階指數哥倫布編碼的計算方法,假設輸入值是N:
1、把N轉換成二進制,假設轉換後是bins,計算bins += 1
2、計算bins的比特數,假設是M,那麼在bins的前面添加上M-1個0就得到最終的結果
或者
1、計算N += 1
2、把N轉換成二進制串bins,計算二進制串的長度,假設是M,那麼在bins的前面添加M-1個0
假設輸入值N對應的二進制串的長度是len,那麼零階哥倫布碼的長度是2*len-1
哥倫布碼golomb = 前綴prefix + 後綴suffix
前綴prefix : len - 1個0
後綴suffix : (N+1)對應的二進制串
零階指數哥倫布編碼的實現
/*
** 無符號指數哥倫布編碼
*/
Void SyntaxElementWriter::xWriteUvlc ( UInt uiCode )
{
UInt uiLength = 1; // 哥倫布碼的長度
UInt uiTemp = ++uiCode; // 執行uiTemp = uiCode + 1
assert ( uiTemp );
while( 1 != uiTemp ) // 假設uiTemp(即uiCode + 1)對應的二進制長度是len,那麼哥倫布碼的長度 = 2 * len - 1
{
uiTemp >>= 1;
uiLength += 2;
}
m_pcBitIf->write( 0, uiLength >> 1); // 寫入前綴: len - 1個0
m_pcBitIf->write( uiCode, (uiLength+1) >> 1); // 寫入後綴:uiTemp對應的二進制
}
上面算法的步驟:1、計算N+=1
2、計算N對應的二進制串bins的比特數,假設是len
3、把前綴(M-1個0)寫入比特流中
4、把後綴(N對應的二進制串)寫入比特流流中
把零階指數哥倫布碼寫入比特流中
寫入比特流中的流程:
1、TComOutputBitstream包含兩個部分:緩衝區和比特流
2、比特流就是已經處理完成的數據,存放在一個vector中
3、緩衝區暫存還沒有寫入比特流中的數據,TComOutputBitstream使用一個uchar類型的數據(8 bit)作爲緩存區,因爲很多時候寫入的數據長度只有若干比特,不能直接寫入比特流中,需要等到緩衝區滿(達到8bit),才寫入
3、m_num_held_bits表示緩衝區中已有的比特數
4、num_total_bits表示寫入數據之後,緩衝區中總的比特數(可能會溢出,後面會解決這個問題)
5、next_num_held_bits有兩種意思:
(1)如果緩衝區沒有溢出,那麼它表示緩衝區中總的比特數(已有+新增)
(2)如果緩衝區溢出,那麼它表示數據佔用完緩衝區後還需要的比特數,只能存放數據的一部分,剩下那部分需要等緩衝區的數據寫入比特流之後再存放
6、next_held_bits是格式化之後的數據
(1)如果緩衝區不溢出,那它表示將要寫入緩衝區中的數據
(2)如果緩衝區溢出,那它表示將一部分數據寫入緩衝區之後,剩下的那部分數據
7、判斷num_total_bits是否大於8,即判斷新增數據之後,緩衝區是否會溢出
8、如果緩衝區不溢出,那麼把數據寫入緩衝區中,然後返回
9、如果緩衝區溢出,那麼先寫一部分數據到緩衝區中,然後把緩衝區寫入比特流中,清空緩衝區,繼續把剩餘的數據寫入緩衝區中
Void TComOutputBitstream::write ( UInt uiBits, UInt uiNumberOfBits )
{
assert( uiNumberOfBits <= 32 );
assert( uiNumberOfBits == 32 || (uiBits & (~0 << uiNumberOfBits)) == 0 );
// m_num_held_bits緩存區中已經持有的比特數
// num_total_bits表示寫入uiBits之後,緩存區中的總比特數
UInt num_total_bits = uiNumberOfBits + m_num_held_bits;
// next_num_held_bits表示緩存區中經使用的比特數
UInt next_num_held_bits = num_total_bits % 8;
// 把數據執行位移操作之後,存放進一個臨時變量中,
UChar next_held_bits = uiBits << (8 - next_num_held_bits);
// 判斷當前持有的比特數是否大於8,如果大於8,表示緩衝區已經滿了,需要先寫入比特流中
if (!(num_total_bits >> 3))
{
// 把數據寫入緩衝區的尾部
m_held_bits |= next_held_bits;
m_num_held_bits = next_num_held_bits;
return;
}
/* topword serves to justify held_bits to align with the msb of uiBits */
// 把一部分數據寫入
UInt topword = (uiNumberOfBits - next_num_held_bits) & ~((1 << 3) -1);
UInt write_bits = (m_held_bits << topword) | (uiBits >> next_num_held_bits);
// 判斷num_total_bits的長度:32,24,16,8
switch (num_total_bits >> 3)
{
case 4: m_fifo->push_back(write_bits >> 24);
case 3: m_fifo->push_back(write_bits >> 16);
case 2: m_fifo->push_back(write_bits >> 8);
case 1: m_fifo->push_back(write_bits);
}
m_held_bits = next_held_bits;
m_num_held_bits = next_num_held_bits;
}