Huffman编码原理
哈夫曼算法原理
Wikipedia上面说的很清楚了,这里我就不再赘述,直接贴过来了。
1952年, David A. Huffman提出了一个不同的算法,这个算法可以为任何的可能性提供出一个理想的树。香农-范诺编码(Shanno-Fano)是从树的根节点到叶子节点所进行的的编码,哈夫曼编码算法却是从相反的方向,暨从叶子节点到根节点的方向编码的。
- 为每个符号建立一个叶子节点,并加上其相应的发生频率
- 当有一个以上的节点存在时,进行下列循环:
- 把这些节点作为带权值的二叉树的根节点,左右子树为空
- 选择两棵根结点权值最小的树作为左右子树构造一棵新的二叉树,且至新的二叉树的根结点的权值为其左右子树上根结点的权值之和。
- 把权值最小的两个根节点移除
- 将新的二叉树加入队列中.
- 最后剩下的节点暨为根节点,此时二叉树已经完成。
示例
-
符号 A B C D E 计数 15 7 6 6 5 概率 0.38461538 0.17948718 0.15384615 0.15384615 0.12820513
在这种情况下,D,E的最低频率和分配分别为0和1,分组结合概率的0.28205128。现在最低的一双是B和C,所以他们就分配0和1组合结合概率的0.33333333在一起。这使得BC和DE所以0和1的前面加上他们的代码和它们结合的概率最低。然后离开只是一个和BCDE,其中有前缀分别为0和1,然后结合。这使我们与一个单一的节点,我们的算法是完整的。
可得A代码的代码长度是1比特,其余字符是3比特。
-
字符 A B C D E 代码 0 100 101 110 111
![](http://upload.wikimedia.org/math/8/e/9/8e9b62a9208942022c30535d79a22d54.png)
Pseudo-code
1: begin
2: count frequencies of single characters (source units)
3: output(frequencies using Fibonacci Codes of degree 2)
4: sort them to non-decreasing sequence
5: create a leaf node (character, frequency c, left son = NULL, right son = NULL)
6: of the tree for each character and put nodes into queue F
7: while (|F|>=2) do
8: begin
9: pop the first two nodes (u1, u2) with the lowest
10: frequencies from sorted queue
11: create a node evaluated with sum of the chosen units,
12: successors are chosen units (eps, c(u1)+c(u2), u1, u2)
13: insert new node into queue
14: end
15: node evaluate with way from root to leaf node (left son 1, right son 0)
16: create output from coded intput characters
17: end
那我们将friend bool operator >(Node node1,Node node2)修改为friend bool operator >(Node* node1,Node* node2),也就是传递的是Node的指针行不行呢?
答案是不可以,因为根据c++primer中重载操作符中讲的“程序员只能为类类型或枚举类型的操作数定义重载操作符,在把操作符声明为类的成员时,至少有一个类或枚举类型的参数按照值或者引用的方式传递”,也就是说friend bool operator >(Node* node1,Node* node2)形参中都是指针类型的是不可以的。我们只能再建一个类,用其中的重载()操作符作为优先队列的比较函数。
就得到了下面正确的代码:
![](https://img-my.csdn.net/uploads/201209/26/1348643029_6719.jpg)
格伦布编码
本文主要有以下三部分内容:
- 介绍了Golomb编码,及其两个变种:Golomb-Rice和Exp-Golomb的基本原理
- C++实现了一个简单的BitStream库,能够方便在bit流和byte数字之间进行转换
- C++实现了Golomb-Rice和Exp-Golomb的编码,并进行了测试。
在本位的最后给出了源代码的下载地址。
Golomb编码的基本原理
Golomb编码是一种无损的数据压缩方法,由数学家Solomon W.Golomb在1960年代发明。Golomb编码只能对非负整数进行编码,符号表中的符号出现的概率符合几何分布(Geometric Distribution)时,使用Golomb编码可以取得最优效果,也就是说Golomb编码比较适合小的数字比大的数字出现概率比较高的编码。它使用较短的码长编码较小的数字,较长的码长编码较大的数字。
Golomb编码是一种分组编码,需要一个正整数参数m,然后以m为单位对待编码的数字进行分组,如下图:
对于任一待编码的非负正整数N,Golomb编码将其分为两个部分:所在组的编号GroupID以及分组后余下的部分,GroupID实际是待编码数字N和参数m的商,余下的部分则是其商的余数,具体计算如下:
对于得到的组号q使用一元编码(Unary code),余下部分r则使用固定长度的二进制编码(binary encoding)。
一元编码(Unary coding)是一种简单的只能对非负整数进行编码的方法,对于任意非负整数num,它的一元编码就是num个1后面紧跟着一个0。例如:
num | Unary coding |
---|---|
0 | 0 |
1 | 10 |
2 | 110 |
3 | 1110 |
4 | 11110 |
5 | 111110 |
其编解码的伪代码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
使用一元编码编码组号也就是商q后,对于余下的部分r则有根据编码数字大小的不同有不同的处理方法。
- 如果参数m是2的次幂(这也是下面将要介绍的Golomb-Rice编码),则使用取r的二进制表示的低 log2(m) 位,作为r的码字
- 如果参数m不是2的次幂,如果m不是2的次幂,设
b=⌈log2(m)⌉
- 如果 r<2b−m ,则使用b-1位的二进制编码r。
- 如果 r≧2b−m ,则使用b位二进制对 r+2b−m 进行编码
总结,设待编码的非负整数为N,Golomb编码流程如下:
- 初始化正整数参数m
- 取得组号q以及余下部分r,计算公式为: q=N/m,r=N%m
- 使用一元编码的方式编码q
- 使用二进制的方式编码r,r所使用位数的如下:
- 如果参数m是2的次幂(这也是下面将要介绍的Golomb-Rice编码),则使用取r的二进制表示的低 log2(m) 位,作为r的码字。
- 如果参数m不是2的次幂,如果m不是2的次幂,设
b=⌈log2(m)⌉
- 如果 r<2b−m ,则使用b-1位的二进制编码r。
- 如果 r≧2b−m ,则使用b位二进制对 r+2b−m 进行编码
说明:
- ⌈a⌉ 大于a的最小整数 ceil运算
- ⌊a⌋ 小于a的最大整数 floor运算
Golomb-Rice 编码
Golomb-Rice是Golomb编码的一个变种,它给Golomb编码的参数m添加了个限制条件:m必须是2的次幂。这样有两个好处:
- 不需要做模运算即可得到余数r,r = N & (m - 1)
- 对余数r编码更为简单,只需要取r二进制的低
log2(m)
位即可。
则Golomb-Rice的编码过程更为简洁:
- 初始化参数m,m必须为2的次幂
- 计算q和r,
q = N / m ; r = N & (m - 1)
- 使用一元编码编码q
- 取r的二进制位的低 log2(m) 位作为r的码字。
解码过程如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
Exponential Golomb 指数哥伦布编码
Rice的编码方式和Golomb的方法是大同小异的,只是选择m必须为2的次幂。而Exp-Golomb则有了一个很大的改进,不再使用固定大小的分组,而使组的大小呈指数增长。如下图:
Exp-Golomb的码元结构是:* [M zeros prefix] [1] [Offset] *,其中M是分组的编号GroupID,1可以看着是分隔符,Offset是组内的偏移量。
Exp-Golomb需要一个非负整数K作为参数,称之为K阶Exp-Golomb。其中当K = 0时,称为0阶Exp-Golomb,目前比较流行的H.264视频编码标准中使用的就是0阶的Exp-Golomb,并且可以将任意的阶数K转为0阶Exp-Golomb编码。
首先来看下0阶Exp-Golomb编码,如下图:
上图是0阶Exp-Golomb编码的前几个组的分组情况,可以看出编号为m的组,其组内的最小元素的值是
2m−1
,也就是说对于非负整数N,其在编号为m的组内的充要条件是:
2m−1≤N≤2m+1−1
。所以可以由如下公式计算得到组号m以及组内的偏移量Offset
有了组号以及组内的偏移量后,其编码就比较简单了,具体过程如下:
- 首先使用公式计算组号m, m=⌊log2(num+1)⌋
- 对组号m进行编码,连续写入m个0,最后写入一个1作为结束。
- 计算组内偏移量offset, Offset=num+1−2m
- 取offset二进制形式的低m位作为offset码元
0阶Exp-Golomb的编码后的长度是: 2∗m+1 ,其解码过程和上面的Rice码类似,读入bit流,是0则继续,1则停止,然后统计0的个数m;接着读入m位的bit,就是offset,最后解码后的数值是: N=2m−1+offset 。
k阶Exp-Golomb
前面提到任意的k阶Exp-Golomb可以转换为0阶Exp-Golomb进行求解,这是为何呢。Exp-Golomb的组的大小实际上是呈2的指数增长,不同的参数k,实际控制的是起始分组的大小,具体是什么意思呢。
- k = 0,其组的大小为1,2,4,8,16,32,…
- k = 1,其组的大小为2,4,8,16,32,64,…
- k = 2,其组的大小为4,8,16,32,64,…
- …
- k = n,其组的大小为 2n,2n+1,⋯
不同的k造成了其起始分组的大小不同,所以对于任意的k阶Exp-Golomb编码都可以转化为0阶,具体如下:
设待编码数字为N,参数为k
- 使用0阶Exp-Golomb编码 N+2k−1
- 从第一步的结果中删除掉高位的k个0
以上的算法描述来自: https://en.wikipedia.org/wiki/Exponential-Golomb_coding
在搜索得到中文资料中,对于K阶Exp-Golomb的算法描述大多如下:
- 将num以二进制的形式表示(若不足k位,则在高位补0),去掉其低k位(若刚好是k位,则为0)得到数字n
- 计算n + 1的最低有效位数lsb,则M = lsb - 1。就是prefix中0的个数
- 将第1步中去掉的k位二进制串放到(n + 1)的低位,得到[1][INFO]
其实现以及描述都不如wikipedia,故在下面的实现部分使用的是Wikipedia的方法。
在资料搜集的过程中,对于Exp-Golomb算法描述不止上述的两种,还有其他的形式,但都是殊途同归,也许得到的编码是不一样的,但是其编码的长度却是一样的,也就没有过多的计较。
最后附上k = 0,1,2,3时前29个数字的编码:
注意1之前的0的个数就是该数字所在的组的编号,同一组内的编码长度是相同的。
实现
通过上面的描述可以发现,Golomb编码的实现是很简单的,唯一的难点在于bit的操作。编码过程是将对bit进行操作,然后拼凑为byte,写入buffer;解码则是相反的过程,读取byte转化为bit stream,操作一个个的bit。具体来说就是以下两个功能:
- 将bit流转换为byte数组
- 将byte数组转换为bit流
而在C/C++中最小的数据类型也是8位的byte,这就造成了对bit的进行操作有一定的难度,好在C++中std::bitset
结构能够在一定成都上简化对bit的操作。
BitBuffer / ByteBuffer
首先实现一个底层的库,实现bit流和byte之间的转换。在Golomb编码中,对bit和byte的操作只需要简单的get/put操作,因此封装了两个结构体BitBuffer
和ByteBuffer
,具体的声明如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
BitBuffer
是一个bit的缓存,无论是将bit流转换为byte还是将byte转换为bit流,都将bit放在此结构体中进行缓存。ByteBuffer
用来管理byte数组的缓存
这两个结构体中只向上层提供简单的get/put方法,不做任何的逻辑判断。也就是说只要调用了get
方法就一定会有数据返回,调用了put
方法就一定有空间存放数据。
BitStream
在编码时,需要将得到的bit流以byte的形式写出;解码则是将byte数组以bit流的形式读入。这就需要两种类型的bitstream:BitOutputStream
和BitInputStream
,其声明如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
编码时需要BitOutputStream
将bit流转换为byte数组,也就是个putBit
的过程,需要注意的一点是在编码结束的时候需要调用方法flush
,该函数有两个功能:
- 将BitBuffer中缓存的bit刷新到byte数组中
- 写入编码的编码终止符。编码终止符在解码过程中是一个很重要的判断标志,这里假定Golomb编码后码元的最大长度为64位,所以可设编码终止符为:连续64bits的0。在解码时,要判断接下来的是不是编码终止符。
- 将编码后输出的字节数填充为8(8 bytes,64 bits)的倍数,在解码时以8 bytes为单位进行解码,并且每次判断是不是编码终止符时也需要至少8 bytes。
编码/解码
有了BitStream的支持后,编解码过程是很简单的。
编码
每次编码前,首先计算编码后码元的长度,如果byte缓存空间不足以存放整个码元,则将byte buffer填充满后,剩余的部分,在bitset中缓存。返回false,指出缓存已满,需要处理缓存中的数据后才能继续编码或者更换一个新的Byte buffer存放编码后的数据.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
上述代码以Golomb-Rice编码为例。在putBit
时候的不会判断缓存是否够用,直接存放,如果Byte Buffer不足以存放本次编码的bits,则将Byte Buferr填充满后,余下的bits在BitBuffer中缓存,然后返回false,告诉调用者byte buffer已经填满,可以处理当前buffer的数据后调用resetBuffer
后继续编码;也可以直接更换一个新的byte buffer。
解码
在每次解码前,先要调用check
方法来判断byte buffer的状态,byte buffer中有以下几种状态
- 空,数据已读取完
- 编码终止符,buffer中的数据是编码终止符,解码结束
- 数据不足,buffer中的数据不足以完成本次解码,需要读取新的buffer
- 数据足够,继续解码
check
的实现如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
check的过程有些复杂,但代码中的注释已足够清晰,这里就不再详述了。
Golomb-Rice的解码过程如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
解码完成后会返回当前byte buffer的状态,
- 状态是
BUFFER_END_SYMBOL
,则解码过程已经完成 - 状态是
BUFFER_EMPTY
,byte buffer没有设置 - 状态是
BUFFER_LACK
,byte buffer中的数据不足以完成一次解码,需要读入新的数据 - 状态是
BUFFER_ENGOUGH
,byte buffer中的数据足够,继续下一次的解码
测试
仍然以Golomb-Rice编码为例,测试代码如下
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 实例编码器时,需要设定编码的参数m和以及存放编码后数据的buffer;
- 编码时,判断编码的的返回值,如果为true则继续编码,为false则buffer已满,将buffer写入文件后,
resetBuffer
继续编码。 - 编码结束后,调用
close
方法,写入编码终止符,并将整个编码后的数据填充为8的倍数。
下面代码Golomb-Rice的解码调用过程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
编码是也需要根据返回的状态,来处理byte buffer,在上面已详述。
总结
终于完成了这篇博文,本文主要对Golomb编码进行了一个比较详尽的描述,包括Golomb编码的两个变种:Golomb-Rice和Exp-Golomb。在编码实现部分,难点有三个:
- byte数组和bit流之间的转换
- 需要一个唯一的编码终止符
- 解码时,byte buffer中剩余数据不足以完成一次解码
针对上述问题,做了如下工作:
- 实现了一个简单的BitStream库,能够方便在bit流和byte数组之间进行转换
- 对编码后的码元长度做了一个假设,其最长长度不会超过64位,这样就使用64比特的0作为编码的终止符
- 在编码的时,会将编码后的总字节数填充为8的倍数,解码的过程中就以8字节为单位进行,当byte buffer中的数据不足8字节时,可以判定当前buffer中的数据并不是全部的数据,需要继续读入数据已完成解码
本文的源代码
- Github: https://github.com/brookicv/GolombCode
- CSDN: http://download.csdn.net/detail/brookicv/9740838
算术编码
早在1948年,香农就提出将信源符号依其出现的概率降序排序,用符号序列累计概率的二进值作为对芯源的编码,并从理论上论证了它的优越性。1960年, Peter Elias发现无需排序,只要编、解码端使用相同的符号顺序即可,提出了算术编码的概念。Elias没有公布他的发现,因为他知道算术编码在数学上虽然成 立,但不可能在实际中实现。1976年,R. Pasco和J. Rissanen分别用定长的寄存器实现了有限精度的算术编码。1979年Rissanen和G. G. Langdon一起将算术编码系统化,并于1981年实现了二进制编码。1987年Witten等人发表了一个实用的算术编码程序,即CACM87(后用 于ITU-T的H.263视频压缩标准)。同期,IBM公司发表了著名的Q-编码器(后用于JPEG和JBIG图像压缩标准)。从此,算术编码迅速得到了 广泛的注意。
算术编码的基本原理是将编码的消息表示成实数0和1之间的一个间隔(Interval),消息越长,编码表示它的间隔就越小,表示这一间隔所需的二进制位就越多。
算术编码用到两个基本的参数:符号的概率和它的编码间隔。信源符号的概率决定压缩编码的效率,也决定编码过程中信源符号的间隔,而这些间隔包含在0到1之间。编码过程中的间隔决定了符号压缩后的输出。
给定事件序列的算术编码步骤如下:
(1)编码器在开始时将“当前间隔” [ L, H) 设置为[0,1)。
(2)对每一事件,编码器按步骤(a)和(b)进行处理
(a)编码器将“当前间隔”分为子间隔,每一个事件一个。
(b)一个子间隔的大小与下一个将出现的事件的概率成比例,编码器选择子间隔对应于下一个确切发生的事件相对应,并使它成为新的“当前间隔”。
(3)最后输出的“当前间隔”的下边界就是该给定事件序列的算术编码。
设Low和High分别表示“当前间隔”的下边界和上边界,CodeRange为编码间隔的长度,LowRange(symbol)和HighRange(symbol)分别代表为了事件symbol分配的初始间隔下边界和上边界。上述过程的实现可用伪代码描述如下:
set Low to 0
set High to 1
while there are input symbols do
take a symbol
CodeRange = High – Low
High = Low + CodeRange *HighRange(symbol)
Low = Low + CodeRange * LowRange(symbol)
end of while
output Low
算术码解码过程用伪代码描述如下:
get encoded number
do
find symbol whose range straddles the encoded number
output the symbol
range = symbo.LowValue – symbol.HighValue
substracti symbol.LowValue from encoded number
divide encoded number by range
until no more symbols
算术编码器的编码解码过程可用例子演示和解释。
例1:假设信源符号为{A, B, C, D},这些符号的概率分别为{ 0.1, 0.4, 0.2,0.3 },根据这些概率可把间隔[0, 1]分成4个子间隔:[0, 0.1], [0.1, 0.5], [0.5, 0.7], [0.7, 1],其中[x,y]表示半开放间隔,即包含x不包含y。上面的信息可综合在表03-04-1中。
表03-04-1 信源符号,概率和初始编码间隔
符号 | A | B | C | D |
概率 | 0.1 | 0.4 | 0.2 | 0.3 |
初始编码间隔 | [0, 0.1) | [0.1, 0.5) | [0.5, 0.7) | [0.7, 1] |
如果二进制消息序列的输入为:C A D A C D B。编码时首先输入的符号是C,找到它的编码范围是[0.5,0.7]。由于消息中第二个符号A的编码范围是[0, 0.1],因此它的间隔就取[0.5, 0.7]的第一个十分之一作为新间隔[0.5,0.52]。依此类推,编码第3个符号D时取新间隔为[0.514, 0.52],编码第4个符号A时,取新间隔为[0.514, 0.5146],…。消息的编码输出可以是最后一个间隔中的任意数。整个编码过程如图03-04-1所示。
图03-04-1 算术编码过程举例
这个例子的编码和译码的全过程分别表示在表03-04-2和表03-04-3中。
表03-04-2 编码过程
步骤 | 输入符号 | 编码间隔 | 编码判决 |
1 | C | [0.5, 0.7] | 符号的间隔范围[0.5, 0.7] |
2 | A | [0.5, 0.52] | [0.5, 0.7]间隔的第一个1/10 |
3 | D | [0.514, 0.52] | [0.5, 0.52]间隔的最后一个1/10 |
4 | A | [0.514, 0.5146] | [0.514, 0.52]间隔的第一个1/10 |
5 | C | [0.5143, 0.51442] | [0.514, 0.5146]间隔的第五个1/10开始,二个1/10 |
6 | D | [0.514384, 0.51442] | [0.5143, 0.51442]间隔的最后3个1/10 |
7 | B | [0.5143836, 0.514402] | [0.514384,0.51442]间隔的4个1/10,从第1个1/10开始 |
8 | 从[0.5143876, 0.514402]中选择一个数作为输出:0.5143876 |
表03-04-3 译码过程
步骤 | 间隔 | 译码符号 | 译码判决 |
1 | [0.5, 0.7] | C | 0.51439在间隔 [0.5, 0.7) |
2 | [0.5, 0.52] | A | 0.51439在间隔 [0.5, 0.7)的第1个1/10 |
3 | [0.514, 0.52] | D | 0.51439在间隔[0.5, 0.52)的第7个1/10 |
4 | [0.514, 0.5146] | A | 0.51439在间隔[0.514, 0.52]的第1个1/10 |
5 | [0.5143, 0.51442] | C | 0.51439在间隔[0.514, 0.5146]的第5个1/10 |
6 | [0.514384, 0.51442] | D | 0.51439在间隔[0.5143, 0.51442]的第7个1/10 |
7 | [0.51439, 0.5143948] | B | 0.51439在间隔[0.51439,0.5143948]的第1个1/10 |
8 | 译码的消息:C A D A C D B |
在上面的例子中,我们假定编码器和译码器都知道消息的长度,因此译码器的译码过程不会无限制地运行下去。实际上在译码器中需要添加一个专门的终止符,当译码器看到终止符时就停止译码。
在算术编码中有几个问题需要注意:
· 由于实际的计算机的精度不可能无限长,一个明显的问题是运算中出现溢出,但多数机器都有16、32或者64位的精度,因此这个问题可使用比例缩放方法解决。
· 算术编码器对整个消息只产生一个码字,这个码字是在间隔[0,1]中的一个实数,因此译码器在接受到表示这个实数的所有位之前不能进行译码。
· 算术编码也是一种对错误很敏感的编码方法,如果有一位发生错误就会导致整个消息译错。
算术编码可以是静态的或者自适应的。在静态算术编码中,信源符号的概率是固定的。在自适应算术编码中,信源符号的概率根据编码时符号出现的频繁程度动态地 进行修改,在编码期间估算信源符号概率的过程叫做建模。需要开发动态算术编码的原因是因为事先知道精确的信源概率是很难的,而且是不切实际的。当压缩消息 时,我们不能期待一个算术编码器获得最大的效率,所能做的最有效的方法是在编码过程中估算概率。因此动态建模就成为确定编码器压缩效率的关键。
此外,在算术编码的使用中还存在版权问题。JPEG标准说明的算术编码的一些变体方案属于IBM, AT&T和Mitsubishi拥有的专利。要合法地使用JPEG算术编码必须得到这些公司的许可。
LZW编码
LZW和哈夫曼编码一样,是无损压缩中的一种。该算法通过建立字典,实现字符重用与编码,适用于source中重复率很高的文本压缩。本文首先讲下LZW的编解码原理,然后给出LZW的实现code。
*********************原理*********************
编码:
- 编码0-255用来存储Ascii码为[0,255]的字符,放在字典里。
- 编码从256开始,将出现过的字符计入字典
- 核心思想:利用字符的可重用性,每当往结果输出一个编码,就将一个新的string存入dictionary
算法流程:
举例:
解码:
编码的逆过程,若编码是string到int的映射,我们可以将解码过程描述为int到string的映射。
- LZW算法的解码无需在编码过程中存储字典(这样太浪费空间了)
- 解码初始化依旧用256个Ascii码,后面每读入一个编码(int),检查其在dictionary中的映射,并不断将新的映射加入字典
算法流程:
解码的例子建议读者用下面的代码直接调试吧~
*********************实现*********************
我用C++实现的,Compress和Decompress两个函数分别实现编解码
result:
Reference:
1. http://www.stringology.org/DataCompression/lzw-e/index_en.html
2. http://www.dspguide.com/ch27/5.htm
3. http://marknelson.us/1989/10/01/lzw-data-compression/
4. http://www.ccs.neu.edu/home/jnl22/oldsite/cshonor/jeff.html
行程编码
行程编码(Run-Length Encoding)
仅存储一个像素值以及具有相同颜色的像素数目的图象数据编码方式称为行程编码,或称游程编码,常用RLE(Run-Length Encoding)表示。该压缩编码技术相当直观和经济,运算也相当简单,因此解压缩速度很快。RLE压缩编码尤其适用于计算机生成的图形图像,对减少存储容量很有效果。
在此方式下每两个字节组成一个信息单元。第一个字节给出其后面相连的象素的个数。第二个字节给出这些象素使用的颜色索引表中的索引。例如:信息单元03 04,03表示其后的象素个数是3个,04表示这些象素使用的是颜色索引表中的第五项的值。压缩数据展开后就是04 04 04 .同理04 05 可以展开为05 05 05 05. 信息单元的第一个字节也可以是00,这种情况下信息单元并不表示数据单元,而是表示一些特殊的含义。这些含义通常由信息单元的第二个字节的值来描述。
http://www.cnblogs.com/lookof/archive/2008/07/11/1241125.html
算术编码、游程编码都属于无损压缩。
算术编码(Arithmetic coding)
算术编码是一种无损数据压缩方法,也是一种熵编码的方法。和其它熵编码方法不同的地方在于,其他的熵编码方法通常是把输入的消息分割为符号,然后对每个符号进行编码。而算术编码是直接把整个输入的消息编码为一个数,一个满足(0.0 ≤ n < 1.0)的小数n。
算术编码用到两个基本的参数:符号的概率和它的编码间隔。信源符号的概率决定压缩编码的效率,也决定编码过程中信源符号的间隔,而这些间隔包含在0到1之间。
算术编码的算法思想如下:
(1)对一组信源符号按照符号的概率从大到小排序,将[0,1)设为当前分析区间。按信源符号的概率序列在当前分析区间划分比例间隔。
(2)检索“输入消息序列”,锁定当前消息符号(初次检索的话就是第一个消息符号)。找到当前符号在当前分析区间的比例间隔,将此间隔作为新的当前分析区间。并把当前分析区间的起点(即左端点)指示的数“补加”到编码输出数里。当前消息符号指针后移。
(3)仍然按照信源符号的概率序列在当前分析区间划分比例间隔。然后重复第二步。直到“输入消息序列”检索完毕为止。
(4)最后的编码输出数就是编码好的数据。
在算术编码中需要注意几个问题:
(1)由于实际计算机的精度不可能无限长,运算中出现溢出是一个明显的问题,但多数及其都有16位,32位或者64位的精度,因此这个问题可以使用比例缩放方法解决。
(2)算术编码器对整个消息只产生一个码字,这个码字是在间隔[0,1)中的一个实数,因此译码器在接受到表示这个实数的所有位之前不能进行译码。
(3)算术编码是一种对错误很敏感的编码方法,如果有一位发生错误就会导致整个消息译错。
算术编码可以是静态的或者是自适应的。在静态算术编码中,信源符号的概率是固定的。在自适应算术编码中,信源符号的概率根据编码时符号出现的频率动态地进行修改,在编码期间估算信源符号概率的过程叫做建模。需要开发动态算术编码的原因是因为事前知道精确的信源概率是很难的,而且不切实际。当压缩消息时,不能期待一个算术编码器获得最大的效率,所能做的最有效的方法是在编码过程中估算概率。因此动态建模就成为确定编码器压缩效率的关键。
游程编码(RLE编码——Run Length Encoding)
游程编码又称“运行长度编码”或“行程编码”,是一种统计编码,该编码属于无损压缩编码。对于二值图有效。
行程编码的基本原理是:用一个符号值或串长代替具有相同值的连续符号(连续符号构成了一段连续的“行程”。行程编码因此而得名),使符号长度少于原始数据的长度。
例如:5555557777733322221111111
行程编码为:(5,6)(7,5)(3,3)(2,4)(l,7)。可见,行程编码的位数远远少于原始字符串的位数。
在对图像数据进行编码时,沿一定方向排列的具有相同灰度值的像素可看成是连续符号,用字串代替这些连续符号,可大幅度减少数据量。
行程编码分为定长行程编码和不定长行程编码两种类型。
行程编码是连续精确的编码,在传输过程中,如果其中一位符号发生错误,即可影响整个编码序列,使行程编码无法还原回原始数据。
游程编码所能获得的压缩比有多大,主要取决于图像本身的特点。如果图像中具有相同颜色的图像块越大,图像块数目越少,获得的压缩比就越高。反之,压缩比就越小。