以下文章参考于殷文杰的博客。
https://yinwenjie.blog.csdn.net/article/details/52301584
1 熵编码基本概念
- 1)“熵”这一概念原本来自于化学和热力学,用于度量能量退化的指标,即熵越高,物体或系统的做功能力越低。后来香农将这一概念引入到信息论中,用于表示消息的平均信息量。信源的熵通常可以表示信源所发出信息的不确定性,即越是随机的、前后不相关的信息,其熵越高。
- 2)在信息论中,香农提出了信源编码定理。该定理说明了香农熵与信源符号概率之间的关系,说明信息的熵为信源无损编码后的平均码字长度的下限。任何的无损编码方法都不可能使编码后的平均码长小于香农熵,只能使其尽量接近。
- 3)基于此,对信源进行熵编码的基本思想,是使其前后的码字之间尽量更加随机,尽量减小前后的相关性,更加接近其信源的香农熵。这样在表示同样的信息量时所用的数据长度更短。
- 4)在实际使用中,常用的熵编码主要有变长编码和算术编码等方法。其中变长编码相对于算术编码较为简单,但平均压缩比可能略低。常见的变长编码方法有哈夫曼编码和香农-费诺编码等。
2 哈夫曼编码(变长编码(VLC))
参考我的以下文章。
https://blog.csdn.net/weixin_44517656/article/details/105227705
或者参考上面开头殷文杰写的。
3 H.264中的熵编码基本方法
在成功从NAL Unit中获取到语法元素的码流之后,接下来就是对语法元素的码流进行解析。但是我们FFmpeg解码得到的H264,都是经过预测、变换量化等步骤后得到的H.264语法元素,他们通过熵编码器压缩为符合标准的H.264码流。因此,为了还原各个语法元素,必须对码流使用熵编码的解码器进行解码。
在H.264的标准协议中,不同的语法元素指定了不同的熵编码方法。在协议文档中共指定了10种语法元素的描述符,这些描述符表达了码流解析为语法元素值的方法,其中包含了H.264标准所支持的所有熵编码方法:
以下均是熵编码的方法,共10种。
上面有许多熵编码,但是我们下面主要讲指数哥伦布编码,简称哥伦布编码。
3.1 指数哥伦布编码(变长编码(VLC))
3.1.1 指数哥伦布编码基本概念
- 1)与上面介绍的哈夫曼编码一样,指数哥伦布编码同样属于变长编码(VLC)的一种。
指数哥伦布编码同哈夫曼编码最显著的一点不同在于,哈弗曼编码构建完成后必须在传递的信息中加入码字和码元值的对应关系,也就是编码的码表,而指数哥伦布编码则不需要。 - 2)如上表指出,常用的指数哥伦布编码通常可以分为四类。
ue(v):无符号指数哥伦布编码。
se(v):有符号指数哥伦布编码。
me(v):映射指数哥伦布编码。
te(v):截断指数哥伦布编码。 - 3)其中无符号指数哥伦布编码ue(v)是其他编码方式的基础,其余几种方法基本可以由ue(v)推导得出。
3.1.2 无符号指数哥伦布编码ue(v)
其编码方法如下:
其中公式codeNum = 2^LeadingZeroBits - 1 + (xxx):
- 1)codeNum代表求出无符号哥伦布编码的十进制数。
- 2)LeadingZeroBits代表1前面的前缀0个数。注:无符号指数哥伦布编码由"前缀+1+后缀组成"。
- 3)xxx代表后缀。
例如十进制数2时,带入公式:010(十进制)----->2^1-1+0=1(ue(v)),所以它在无符号指数哥伦布编码中的码元值为1。
并且需要注意,码元值的范围为:
2^LeadingZeroBits-1 ~ 2^LeadingZeroBits-1 + 2^LeadingZeroBits-1 即:
2^LeadingZeroBits-1到 2*(2^LeadingZeroBits-1)
例如LeadingZeroBits=5时,范围是31~62。
其余三种指数哥伦布编码不是特别重要,了解下即可,并且均可以由无符号指数哥伦布编码得到。
最后提供十进制与无符号指数哥伦布编码的换算表:
3.1.3 有符号指数哥伦布编码
有符号的指数哥伦布编码值是通过无符号的指数哥伦布编码的值通过换算得到的,其语法元素描述符为se(v)。每一个无符号指数哥伦布编码的数值通过固定的换算关系转换为有符号的值,其换算关系为:n = (-1)^(k+1) * Ceil(k/2)。下表表示了有符号和无符号指数哥伦布编码之间的换算关系:
3.1.4 截断指数哥伦布编码
截断指数哥伦布编码的语法元素描述符为te(v)。当语法元素以te(v)解码时,首先需要判断的是语法元素的取值范围,假定为[0, x], x≥1。根据x的取值情况,语法元素根据下面不同情况进行解析:
- 1)若x>1,解析方法同ue(v)相同。
- 2)若x=1,语法元素值等同于下一位bit值的取反。
3.1.5 映射指数哥伦布编码
映射指数哥伦布编码的描述符为me(v),适用于预测模式为Intra_4x4, Intra_8x8或Inter的宏块的coded_block_pattern的编码。me(v)的映射方式并无指定的换算公式,通常由查表的方式进行。下表为H.264 spec文档的表9-4的一部分:
4 指数哥伦布编码同哈夫曼编码的比较
指数哥伦布编码与哈夫曼编码都遵循了同一规律,即针对不同的码元分配了bit位长度不同的码字,因此各自都属于变长编码的一种。然而二者仍然具有较大的差别,具体如:
- 1)哈夫曼编码在编码过程中考虑了信源各个符号的概率分布特性,根据符号的概率分布进行编码,因此对于不同的信源,即使是相同的符号的哈夫曼编码的结果也是不同的。而指数哥伦布编码针对不同的信源采用的编码是统一的,因此无论是什么样的输入,输出的编码后的数据都是一致的。
- 2)由于哈夫曼编码是针对信源特性进行的编码,因此在存储或传输编码后的数据之前必须在前面保存一份码表供解码段重建原始信息使用。而指数哥伦布编码不需要存储任何额外信息就可以进行解码。
- 3)由于未考虑信源的实际特性,指数哥伦布编码的压缩比率通常比较低,对于有些信息甚至完全没有压缩效果,输出数据比原始数据更大,在这一点上哈夫曼编码作为“最优编码”在效率上更高。然而由于哈夫曼编码运算较指数哥伦布编码更为复杂,且必须保存码表信息增加了传输负荷,也对压缩比率造成了不利影响。
- 4)实际上,对于视频压缩这样的需求而言,类似于哈夫曼编码所提供的压缩比率的优势远远不够,而且像H.264等编码标准都不会指望靠这样的方式来提高压缩比率。因此在实际的视频编码方法中使用的是指数哥伦布编码,但是只作为少数的辅助语法元素的编码以及多数语法元素的二值化方法。真正贡献了高压缩比还需要后面详述的CAVLC和CABAC等。
5 无符号指数哥伦布编码demo
个人已对代码添加详细注释。
#include <iostream>
#include <deque>
#include <assert.h>
using namespace std;
typedef unsigned char UINT8;
/*
功能:获取某个字节的某位二进制
参1:码流数据
参2:第几个字节
参3:第几位
*/
static int get_bit_at_position(UINT8 *buf, UINT8 &bytePotion, UINT8 &bitPosition)
{
UINT8 mask = 0, val = 0;
mask = 1 << (7 - bitPosition);//保证从第一位开始,例如bitPosition=0,1左移7位后,1000 0000
val = ((buf[bytePotion] & mask) != 0);//!= 0用于确保val值为0-1.例若buf[bytePotion] & mask结果为00000100,不使用"!=0"的话结果为3
++bitPosition;//指向下一位bit,实际上个人觉得自增放在调用函数执行更好
if (bitPosition > 7)
{
bytePotion++;
bitPosition = 0;
}
return val;
}
/*
功能:获取哥伦布编码的十进制数
参数:与上面意思一样
*/
static int get_uev_code_num(UINT8 *buf, UINT8 &bytePotion, UINT8 &bitPosition)
{
assert(bitPosition < 8);//一个字节最多0-7共八个bit
UINT8 val = 0, prefixZeroCount = 0;
int prefix = 0, surfix = 0;
//1 求出前缀所占的0,即长度,后面用于求后缀
while (true)
{
val = get_bit_at_position(buf, bytePotion, bitPosition);
if (val == 0)
{
prefixZeroCount++;
}
else
{
break;//遇到1必须退出,然后计算1后面的后缀,即prefix+1+surfix
}
}
//2 先求出公式的前半部分
prefix = (1 << prefixZeroCount) - 1;//获取2^prefixZeroCount-1,两者表达一样
//3 再求出公式的后半部分,即surfix
for (size_t i = 0; i < prefixZeroCount; i++)
{
val = get_bit_at_position(buf, bytePotion, bitPosition);//因为上面while是根据1跳出,所以第一个val必定是1后面的位,注:没有前缀不会进该for循环,因为prefixZeroCount为0
surfix += val * (1 << (prefixZeroCount - 1 - i));//减1是因为求平方的原因,例如100,求第二个0,公式为0 * (1<<(prefixZeroCount-1)). -i是因为每次for后,bitPosition会右移一位
}
prefix += surfix;
return prefix;
}
int main(int argc, char* argv[])
{
/*
选以下六个数的原因:
0xA6:1010 0110
0x42:0100 0010
0x98:1001 1000
0xE2:1110 0010
0x04:0000 0100
0x8A:1000 1010
由于哥伦布编码由前缀+1+后缀组成,并且前后缀长度相等,所以看上面:
1)直接遇到1,所以前缀长度为0个0,后缀也同样(长度相等嘛),故为结果:1
2)然后遇到0,再遇到1,所以前缀为一个0,同理后缀长度为1,所以结果为:010
3)遇到0,然后遇到1,所以前缀为一个0,同理后缀长度为1,所以结果为:011
其余同理。当我们最终列出所有结果时,即
00100,00101,00110,00111,0001000,0001001,0001010
根据哥伦布编码的公式(实际上是将编码的码流转成十进制):codeNum=2^PrefixZeroNum - 1 + surfix
可以得出上面编码结果的十进制:
1->2^0-1+0=0
010->2^1-1+0=1
011->2^1-1+1=2
...
0001010->2^3-1+2=9
刚好这六个字节的哥伦布编码是十进制的0-9.这就是这6个字节的作用。
*/
/*总思路:从第一个字节的第一位开始算*/
UINT8 strArray[6] = { 0xA6, 0x42, 0x98, 0xE2, 0x04, 0x8A };//哥伦布编码的0-9
UINT8 bytePosition = 0, bitPosition = 0;//从第1个字节的第1位开始
UINT8 dataLengthInBits = sizeof(strArray) * 8;//码流总位数
int codeNum = 0;
while ((bytePosition * 8 + bitPosition) < dataLengthInBits)
{
codeNum = get_uev_code_num(strArray, bytePosition, bitPosition);
printf("ExpoColumb codeNum = %d\n", codeNum);
}
system("pause");
return 0;
}
结果: