标签:
无损压缩是 指使用压缩后的数据进行重构(或者叫做还原,解压缩),重构后的数据与原来的数据完全相同;无损压缩用于要求重构的信号与原始信号完全一致的场合。一个很 常见的例子是磁盘文件的压缩。根据目前的技术水平,无损压缩算法一般可以把普通文件的数据压缩到原来的1/2~1/4。一些常用的无损压缩算法有霍夫曼 (Huffman)算法和LZW(Lenpel-Ziv & Welch)压缩算法,算术编码、RLE编码(行程长度编码)和词典编码。
有损压缩是指使用压缩后的数据进
行重构,重构后的数据与原来的数据有所不同,但不影响人对原始资料表达的信息造成误解。有损压缩适用于重构信号不一定非要和原始信号完全相同的场合。例
如,图像和声音的压缩就可以采用有损压缩,因为其中包含的数据往往多于我们的视觉系统和听觉系统所能接收的信息,丢掉一些数据而不至于对声音或者图像所表
达的意思产生误解,但可大大提高压缩比。
1. 数据冗余
2. 统计编码——给已知统计信息的符号分配代码的数据无损压缩方法
编码方法: 香农-范诺编码; 霍夫曼编码; 算术编码
2.1香农-范诺编码
(1).Entropy(熵)的概念
1)熵是信息量的度量方法,它表示某一事件出现的消息越多,事件发生的可能性就越小,数学上就是概率越小。
2)某个事件的信息量用Ii=-log2Pi表示, 其中pi为第i个事件的概率,0<Pi≤1
(2).信源S的熵的定义
按照仙农(Shannon)的理论,信源S的熵定义为 H(S) = η = ∑i Pilog2(1/Pi)。其中Pi是符号Si在S中出现的概率;log2(1/Pi)表示包含在Si中的信息量,也就是编码Si所需要的位数。例如,一幅用256级灰度表示的图像,如果每一个象素点灰度的概率均为Pi=1/256,编码每一个象素点就需要8位。
(3).编码流程
Shannon-Fano的树是根据旨在定义一个有效的代码表的规范而建立的。实际的算法很简单:
这个例子展示了一组字母的香浓编码结构(如图a所示)这五个可被编码的字母有如下出现次数:
Symbol | A | B | C | D | E |
---|---|---|---|---|---|
Count | 15 | 7 | 6 | 6 | 5 |
Probabilities | 0.38461538 | 0.17948718 | 0.15384615 | 0.15384615 | 0.12820513 |
从左到右,所有的符号以它们出现的次数划分。在字母B与C之间划定分割线,得到了左右两组,总次数分别为22,17。 这样就把两组的差别降到最小。通过这样的分割, A与B同时拥有了一个以0为开头的码字, C,D,E的码子则为1,如图b所示。 随后, 在树的左半边,于A,B间建立新的分割线,这样A就成为了码字为00的叶子节点,B的码子01。经过四次分割, 得到了一个树形编码。 如下表所示,在最终得到的树中, 拥有最大频率的符号被两位编码, 其他两个频率较低的符号被三位编码。
符号 | A | B | C | D | E |
---|---|---|---|---|---|
编码 | 00 | 01 | 10 | 110 | 111 |
Entropy(熵,平均码字长度):
1 #include"iostream" 2 #include "queue" 3 #include "map" 4 #include "string" 5 #include "iterator" 6 #include "vector" 7 #include "algorithm" 8 #include "math.h" 9 using namespace std; 10 11 #define NChar 8 //suppose use 8 bits to describe all symbols 12 #define Nsymbols 1<<NChar //can describe 256 symbols totally (include a-z, A-Z) 13 #define INF 1<<31-1 14 15 typedef vector<bool> SF_Code;//8 bit code of one char 16 map<char,SF_Code> SF_Dic; //huffman coding dictionary 17 int Sumvec[Nsymbols]; //record the sum of symbol count after sorting 18 19 class HTree 20 { 21 public : 22 HTree* left; 23 HTree* right; 24 char ch; 25 int weight; 26 27 HTree(){left = right = NULL; weight=0;ch =‘\0‘;} 28 HTree(HTree* l,HTree* r,int w,char c){left = l; right = r; weight=w; ch=c;} 29 ~HTree(){delete left; delete right;} 30 bool Isleaf(){return !left && !right; } 31 }; 32 33 bool comp(const HTree* t1, const HTree* t2)//function for sorting 34 { return (*t1).weight>(*t2).weight; } 35 36 typedef vector<HTree*> TreeVector; 37 TreeVector TreeArr;//record the symbol count array after sorting 38 39 void Optimize_Tree(int a,int b,HTree& root)//find optimal separate point and optimize tree recursively 40 { 41 if(a==b)//build one leaf node 42 { 43 root = *TreeArr[a-1]; 44 return; 45 } 46 else if(b-a==1)//build 2 leaf node 47 { 48 root.left = TreeArr[a-1]; 49 root.right=TreeArr[b-1]; 50 return; 51 } 52 //find optimizing point x 53 int x,minn=INF,curdiff; 54 for(int i=a;i<b;i++)//find the point that minimize the difference between left and right; this can also be implemented by dichotomy 55 { 56 curdiff = Sumvec[i]*2-Sumvec[a-1]-Sumvec[b]; 57 if(abs(curdiff)<minn){ 58 x=i; 59 minn = abs(curdiff); 60 } 61 else break;//because this algorithm has monotonicity 62 } 63 HTree*lc = new HTree; HTree *rc = new HTree; 64 root.left = lc; root.right = rc; 65 Optimize_Tree(a,x,*lc); 66 Optimize_Tree(x+1,b,*rc); 67 } 68 69 HTree* BuildTree(int* freqency)//create the tree use Optimize_Tree 70 { 71 int i; 72 for(i=0;i<Nsymbols;i++)//statistic 73 { 74 if(freqency[i]) 75 TreeArr.push_back(new HTree (NULL,NULL,freqency[i], (char)i)); 76 } 77 sort(TreeArr.begin(), TreeArr.end(), comp); 78 memset(Sumvec,0,sizeof(Sumvec)); 79 for(i=1;i<=TreeArr.size();i++) 80 Sumvec[i] = Sumvec[i-1]+TreeArr[i-1]->weight; 81 HTree* root = new HTree; 82 Optimize_Tree(1,TreeArr.size(),*root); 83 return root; 84 } 85 86 /************************************************************************/ 87 /* Give Shanno Coding to the Shanno Tree 88 /*PS: actually, this generative process is same as Huffman coding 89 /************************************************************************/ 90 void Generate_Coding(HTree* root, SF_Code& curcode) 91 { 92 if(root->Isleaf()) 93 { 94 SF_Dic[root->ch] = curcode; 95 return; 96 } 97 SF_Code lcode = curcode; 98 SF_Code rcode = curcode; 99 lcode.push_back(false); 100 rcode.push_back(true); 101 Generate_Coding(root->left,lcode); 102 Generate_Coding(root->right,rcode); 103 } 104 105 int main() 106 { 107 int freq[Nsymbols] = {0}; 108 char *str = "bbbbbbbccccccaaaaaaaaaaaaaaaeeeeedddddd";//15a,7b,6c,6d,5e 109 110 //statistic character frequency 111 while (*str!=‘\0‘) freq[*str++]++; 112 113 //build tree 114 HTree* r = BuildTree(freq); 115 SF_Code nullcode; 116 Generate_Coding(r,nullcode); 117 118 for(map<char,SF_Code>::iterator it = SF_Dic.begin(); it != SF_Dic.end(); it++) { 119 cout<<(*it).first<<‘\t‘; 120 std::copy(it->second.begin(),it->second.end(),std::ostream_iterator<bool>(cout)); 121 cout<<endl; 122 } 123 }
2.2 霍夫曼编码
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 |
1 /************************************************************************/ 2 /* File Name: Huffman.cpp 3 * @Function: Lossless Compression 4 @Author: Sophia Zhang 5 @Create Time: 2012-9-26 10:40 6 @Last Modify: 2012-9-26 12:10 7 */ 8 /************************************************************************/ 9 10 #include"iostream" 11 #include "queue" 12 #include "map" 13 #include "string" 14 #include "iterator" 15 #include "vector" 16 #include "algorithm" 17 using namespace std; 18 19 #define NChar 8 //suppose use 8 bits to describe all symbols 20 #define Nsymbols 1<<NChar //can describe 256 symbols totally (include a-z, A-Z) 21 typedef vector<bool> Huff_code;//8 bit code of one char 22 map<char,Huff_code> Huff_Dic; //huffman coding dictionary 23 24 /************************************************************************/ 25 /* Tree Class elements: 26 *2 child trees 27 *character and frequency of current node 28 */ 29 /************************************************************************/ 30 class HTree 31 { 32 public : 33 HTree* left; 34 HTree* right; 35 char ch; 36 int weight; 37 38 HTree(){left = right = NULL; weight=0;ch =‘\0‘;} 39 HTree(HTree* l,HTree* r,int w,char c){left = l; right = r; weight=w; ch=c;} 40 ~HTree(){delete left; delete right;} 41 bool Isleaf(){return !left && !right; } 42 }; 43 44 /************************************************************************/ 45 /* prepare for pointer sorting*/ 46 /*because we cannot use overloading in class HTree directly*/ 47 /************************************************************************/ 48 class Compare_tree 49 { 50 public: 51 bool operator () (HTree* t1, HTree* t2) 52 { 53 return t1->weight> t2->weight; 54 } 55 }; 56 57 /************************************************************************/ 58 /* use priority queue to build huffman tree*/ 59 /************************************************************************/ 60 HTree* BuildTree(int *frequency) 61 { 62 priority_queue<HTree*,vector<HTree*>,Compare_tree> QTree; 63 64 //1st level add characters 65 for (int i=0;i<Nsymbols;i++) 66 { 67 if(frequency[i]) 68 QTree.push(new HTree(NULL,NULL,frequency[i],(char)i)); 69 } 70 71 //build 72 while (QTree.size()>1) 73 { 74 HTree* lc = QTree.top(); 75 QTree.pop(); 76 HTree* rc = QTree.top(); 77 QTree.pop(); 78 79 HTree* parent = new HTree(lc,rc,lc->weight+rc->weight,(char)256); 80 QTree.push(parent); 81 } 82 //return tree root 83 return QTree.top(); 84 } 85 86 /************************************************************************/ 87 /* Give Huffman Coding to the Huffman Tree*/ 88 /************************************************************************/ 89 void Huffman_Coding(HTree* root, Huff_code& curcode) 90 { 91 if(root->Isleaf()) 92 { 93 Huff_Dic[root->ch] = curcode; 94 return; 95 } 96 Huff_code lcode = curcode; 97 Huff_code rcode = curcode; 98 lcode.push_back(false); 99 rcode.push_back(true); 100 101 Huffman_Coding(root->left,lcode); 102 Huffman_Coding(root->right,rcode); 103 } 104 105 int main() 106 { 107 int freq[Nsymbols] = {0}; 108 char *str = "this is the string need to be compressed"; 109 110 //statistic character frequency 111 while (*str!=‘\0‘) 112 freq[*str++]++; 113 114 //build tree 115 HTree* r = BuildTree(freq); 116 Huff_code nullcode; 117 nullcode.clear(); 118 Huffman_Coding(r,nullcode); 119 120 for(map<char,Huff_code>::iterator it = Huff_Dic.begin(); it != Huff_Dic.end(); it++) 121 { 122 cout<<(*it).first<<‘\t‘; 123 std::copy(it->second.begin(),it->second.end(),std::ostream_iterator<bool>(cout)); 124 cout<<endl; 125 } 126 }
2.3 算术编码
算术编码在图像数据压缩标准(如JPEG,JBIG)中扮演了重要的角色。在算术编码中,消息用0到1之间的实数进行编码,算术编码用到两个基本的参数:符 号的概率和它的编码间隔。信源符号的概率决定压缩编码的效率,也决定编码过程中信源符号的间隔,而这些间隔包含在0到1之间。编码过程中的间隔决定了符号压缩后的输出。
算术编码是一种无损数据压缩方法,也是一种熵编码的方法。和其它熵编码方法不同的地方在于,其他的熵编码方法通常是把输入的消息分割为符号,然后对每个符号 进行编码。而算术编码是直接把整个输入的消息编码为一个数,一个满足(0.0 ≤ n < 1.0)的小数n。算术编码用到两个基本的参数:符号的概率和它的编码间隔。信源符号的概率决定压缩编码的效率,也决定编码过程中信源符号的间隔,而这些 间隔包含在0到1之间。
(1)对一组信源符号按照符号的概率从大到小排序,将[0,1)设为当前分析区间。按信源符号的概率序列在当前分析区间划分比例间隔。
(2)检索“输入消息序列”,锁定当前消息符号(初次检索的话就是第一个消息符号)。找到当前符号在当前分析区间的比例间隔,将此间隔作为新的当前分析区间。并把当前分析区间的起点(即左端点)指示的数“补加”到编码输出数里。当前消息符号指针后移。
(3)仍然按照信源符号的概率序列在当前分析区间划分比例间隔。然后重复第二步。直到“输入消息序列”检索完毕为止。
(4)最后的编码输出数就是编码好的数据。
例子:
假设信源符号为{00,01,10,11},这些符号的概率分别为{ 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。上面的信息可综合在表中。
信源符号,概率和初始编码间隔
符号 |
00 |
01 |
10 |
11 |
概率 |
0.1 |
0.4 |
0.2 |
0.3 |
初始编码间隔 |
[0, 0.1) |
[0.1, 0.5) |
[0.5, 0.7) |
[0.7, 1) |
如果二进制消息序列的输入为:10 00 11 00 10 11 01。编码时首先输入的符号是10,找到它的编码范围是[0.5,0.7)。由于消息中第二个符号00的编码范围是[0, 0.1),因此它的间隔就取[0.5,0.7)的第一个十分之一作为新间隔[0.5,0.52)。依此类推,编码第3个符号11时取新间隔为 [0.514,0.52),编码第4个符号00时,取新间隔为[0.514,0.5146),…。消息的编码输出可以是最后一个间隔中的任意数。整个编码 过程如图4-03所示。
编码过程
步骤 |
输入符号 |
编码间隔 |
编码判决 |
1 |
10 |
[0.5, 0.7) |
符号的间隔范围[0.5, 0.7) |
2 |
00 |
[0.5, 0.52) |
[0.5, 0.7)间隔的第一个1/10 |
3 |
11 |
[0.514, 0.52) |
[0.5, 0.52)间隔的最后一个1/10 |
4 |
00 |
[0.514, 0.5146) |
[0.514, 0.52)间隔的第一个1/10 |
5 |
10 |
[0.5143, 0.51442) |
[0.514, 0.5146)间隔的第五个1/10开始,二个1/10 |
6 |
11 |
[0.514384, 0.51442) |
[0.5143, 0.51442)间隔的最后3个1/10 |
7 |
01 |
[0.5143836, 0.514402) |
[0.514384, 0.51442)间隔的4个1/10,从第1个1/10开始 |
8 |
从[0.5143876, 0.514402中选择一个数作为输出:0.5143876 |
译码过程
步骤 |
间隔 |
译码符号 |
译码判决 |
1 |
[0.5, 0.7) |
10 |
0.51439在间隔 [0.5, 0.7) |
2 |
[0.5, 0.52) |
00 |
0.51439在间隔 [0.5, 0.7)的第1个1/10 |
3 |
[0.514, 0.52) |
11 |
0.51439在间隔[0.5, 0.52)的第7个1/10 |
4 |
[0.514, 0.5146) |
00 |
0.51439在间隔[0.514, 0.52)的第1个1/10 |
5 |
[0.5143, 0.51442) |
10 |
0.51439在间隔[0.514, 0.5146)的第5个1/10 |
6 |
[0.514384, 0.51442) |
11 |
0.51439在间隔[0.5143, 0.51442)的第7个1/10 |
7 |
[0.51439, 0.5143948) |
01 |
0.51439在间隔[0.51439, 0.5143948)的第1个1/10 |
7 |
译码的消息:10 00 11 00 10 11 01 |
3. RLE(行程长度)编码
现实中有许多这样的图像,在一幅图像中具有许多颜色相同的图块。在这些图块中,许多行上都具有相同的颜色,或者在一行上有许多连续的象素都具有相同的颜色 值。在这种情况下就不需要存储每一个象素的颜色值,而仅仅存储一个象素的颜色值,以及具有相同颜色的象素数目就可以,或者存储一个象素的颜色值,以及具有相同颜色值的行数。这种压缩编码称为行程编码,常用(run length encoding,RLE)表示,具有相同颜色并且是连续的象素数目称为行程长度。
为了叙述方便,假定一幅灰度图像,第n行的象素值为:
RLE编码的概念
用RLE编码方法得到的代码为:80315084180。代码中用黑体表示的数字是行程长度,黑体字后面的数字代表象素的颜色值。例如黑体字50代表有连续50个象素具有相同的颜色值,它的颜色值是8。
对比RLE编码前后的代码数可以发现,在编码前要用73个代码表示这一行的数据,而编码后只要用11个代码表示代表原来的73个代码,压缩前后的数据量
之比约为7:1,即压缩比为7:1。这说明RLE确实是一种压缩技术,而且这种编码技术相当直观,也非常经济。RLE所能获得的压缩比有多大,这主要是取
决于图像本身的特点。如果图像中具有相同颜色的图像块越大,图像块数目越少,获得的压缩比就越高。反之,压缩比就越小。译码时按照与编码时采用的相同规则进行,还原后得到的数据与压缩前的数据完全相同。因此,RLE是无损压缩技术。
RLE压缩编码尤其适用于计算机生成的图像,对减少图像文件的存储空间非常有效。然而,RLE对颜色丰富的自然图像就显得力不从心,在同一行上具有相同
颜色的连续象素往往很少,而连续几行都具有相同颜色值的连续行数就更少。如果仍然使用RLE编码方法,不仅不能压缩图像数据,反而可能使原来的图像数据变
得更大。请注意,这并不是说RLE编码方法不适用于自然图像的压缩,相反,在自然图像的压缩中还真少不了RLE,只不过是不能单纯使用RLE一种编码方
法,需要和其他的压缩编码技术联合应用。
4. 词典编码
词典编码(dictionary encoding)的根据是数据本身包含有重复代码这个特性。例如文本文件和光栅图像就具有这种特性。词典编码法的种类很多,归纳起来大致有两类。
(1). 第一类词典法的想法是查找正在压缩的字符序列是否在以前输入的数据中出现过,然后用已经出现过的字符串替代重复的部分,它的输出仅仅是指向早期出现过的字符串的“指针”。
第一类词典法编码概念
这里所指的“词典”是指用以前处理过的数据来表示编码过程中遇到的重复部分。这类编码中的所有算法都是以 Abraham Lempel和Jakob Ziv在1977年开发和发表的称为LZ77算法为基础的,例如1982年由Storer和Szymanski改进的称为LZSS算法就是属于这种情况。
(2). 第二类算法的想法是企图从输入的数据中创建一个“短语词典(dictionary of the
phrases)”,这种短语不一定是像“严谨勤奋求实创新”和“国泰民安是坐稳总统宝座的根本”这类具有具体含义的短语,它可以是任意字符的组合。编码
数据过程中当遇到已经在词典中出现的“短语”时,编码器就输出这个词典中的短语的“索引号”,而不是短语本身。
第二类词典法编码概念
J.Ziv和A.Lempel在1978年首次发表了介绍这种编码方法的文章。在他们的研究基础上,Terry A.Weltch在1984年发表了改进这种编码算法的文章,因此把这种编码方法称为LZW(Lempel-Ziv Walch)压缩编码,首先在高速硬盘控制器上应用了这种算法。
4.1. LZ77算法
为了更好地说明LZ77算法的原理,首先介绍算法中用到的几个术语:
1>.输入数据流(input stream): 要被压缩的字符序列。
2>.字符(character): 输入数据流中的基本单元。
3>.编码位置(coding position): 输入数据流中当前要编码的字符位置,指前向缓冲存储器中的开始字符。
4>.前向缓冲存储器(Lookahead buffer): 存放从编码位置到输入数据流结束的字符序列的存储器。
5>.窗口(window): 指包含W个字符的窗口,字符是从编码位置开始向后数也就是最后处理的字符数。
6>.指针(pointer): 指向窗口中的匹配串且含长度的指针。
LZ77编码算法的核心是查找从前向缓冲存储器开始的最长的匹配串。编码算法的具体执行步骤如下:
1).把编码位置设置到输入数据流的开始位置。
2).查找窗口中最长的匹配串。
3).以“(Pointer, Length) Characters”的格式输出,其中Pointer是指向窗口中匹配串的指针,Length表示匹配字符的长度,Characters是前向缓冲存储器中的不匹配的第1个字符。
4).如果前向缓冲存储器不是空的,则把编码位置和窗口向前移(Length+1)个字符,然后返回到步骤2。
例子:
1).“步骤”栏表示编码步骤。
2).“位置”栏表示编码位置,输入数据流中的第1个字符为编码位置1。
3).“匹配串”栏表示窗口中找到的最长的匹配串。
4).“字符”栏表示匹配之后在前向缓冲存储器中的第1个字符。
5).“输出”栏以“(Back_chars, Chars_length)
Explicit_character”格式输出。其中,(Back_chars,
Chars_length)是指向匹配串的指针,告诉译码器“在这个窗口中向后退Back_chars个字符然后拷贝Chars_length个字符到输
出”,Explicit_character是真实字符。例如,表中的输出“(5,2) C”告诉译码器回退5个字符,然后拷贝2个字符“AB”
待编码的数据流
位置 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
字符 |
A |
A |
B |
C |
B |
B |
A |
B |
C |
编码过程
步骤 |
位置 |
匹配串 |
字符 |
输出 |
1 |
1 |
-- |
A |
(0,0) A |
2 |
2 |
A |
B |
(1,1) B |
3 |
4 |
-- |
C |
(0,0) C |
4 |
5 |
B |
B |
(2,1) B |
5 |
7 |
A B |
C |
(5,2) C |
4.2LZSS算法
LZ77通过输出真实字符解决了在窗口中出现没有匹配串的问题,但这个解决方案包含有冗余信息。冗余信息表现在
两个方面,一是空指针,二是编码器可能输出额外的字符,这种字符是指可能包含在下一个匹配串中的字符。LZSS算法以比较有效的方法解决这个问题,它的思想是如果匹配串的长度比指针本身的长度长就输出指针,否则就输出真实字符。由于输出的压缩数据流中包含有指针和字符本身,为了区分它们就需要有额外的标志
位,即ID位。
LZSS编码算法的具体执行步骤如下:
1>.把编码位置置于输入数据流的开始位置。
2>.前向缓冲存储器中查找与窗口中最长的匹配串
① Pointer :=匹配串指针。
② Length :=匹配串长度。
3>.判断匹配串长度Length是否大于等于最小匹配串长度(Length 3 MIN_LENGTH) ,如果“是”:输出指针,然后把编码位置向前移动Length个字符。如果“否”:输出前向缓冲存储器中的第1个字符,然后把编码位置向前移动一个字符。
4>.如果前向缓冲存储器不是空的,就返回到步骤2。
例子:
1).“步骤”栏表示编码步骤。
2).“位置”栏表示编码位置,输入数据流中的第1个字符为编码位置1。
3).“匹配”栏表示窗口中找到的最长的匹配串。
4).“字符”栏表示匹配之后在前向缓冲存储器中的第1个字符。
5).“输出”栏的输出为:
① 如果匹配串本身的长度Length 3 MIN_LENGTH,输出指向匹配串的指针,格式为(Back_chars,
Chars_length)。该指针告诉译码器“在这个窗口中向后退Back_chars个字符然后拷贝Chars_length个字符到输出”。
② 如果匹配串本身的长度Length £ MIN_LENGTH,则输出真实的匹配串。
输入数据流
位置 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
字符 |
A |
A |
B |
B |
C |
B |
B |
A |
A |
B |
C |
编码过程(MIN_LENGTH = 2)
步骤 |
位置 |
匹配串 |
输出 |
1 |
1 |
-- |
A |
2 |
2 |
A |
A |
3 |
3 |
-- |
B |
4 |
4 |
B |
B |
5 |
5 |
-- |
C |
6 |
6 |
B B |
(3,2) |
7 |
8 |
A A B |
(7,3) |
8 |
11 |
C |
C |
在相同的计算机环境下,LZSS算法比LZ77可获得比较高的压缩比,而译码同样简单。这也就是为什么这种算法
成为开发新算法的基础,许多后来开发的文档压缩程序都使用了LZSS的思想。例如,PKZip, ARJ,
LHArc和ZOO等等,其差别仅仅是指针的长短和窗口的大小等有所不同。
LZSS同样可以和熵编码联合使用,例如ARJ就与霍夫曼编码联用,而PKZip则与Shannon-Fano联用,它的后续版本也采用霍夫曼编码。
4.3 LZ78算法
在介绍LZ78算法之前,首先说明在算法中用到的几个术语和符号:
●字符流(Charstream):要被编码的数据序列。
●字符(Character):字符流中的基本数据单元。
●前缀(Prefix): 在一个字符之前的字符序列。
●缀-符串(String):前缀+字符。
●码字(Code word):码字流中的基本数据单元,代表词典中的一串字符。
●码字流(Codestream): 码字和字符组成的序列,是编码器的输出。
●词典(Dictionary): 缀-符串表。按照词典中的索引号对每条缀-符串(String)指定一个码字(Code word)。
●当前前缀(Current prefix):在编码算法中使用,指当前正在处理的前缀,用符号P表示。
●当前字符(Current character):在编码算法中使用,指当前前缀之后的字符,用符号C表示。
●.当前码字(Current code word): 在译码算法中使用,指当前处理的码字,用W表示当前码字,String.W表示当前码字的缀-符串。
编码算法
LZ78的编码思想是不断地从字符流中提取新的缀-符串(String),通俗地理解为新“词条”,然后用“代号”也就是码字(Code
word)表示这个“词条”。这样一来,对字符流的编码就变成了用码字(Code
word)去替换字符流(Charstream),生成码字流(Codestream),从而达到压缩数据的目的。
在编码开始时词典是空的,
不包含任何缀-符串(string)。在这种情况下编码器就输出一个表示空字符串的特殊码字(例如“0”)和字符流中(Charstream)的第一个字符C,并把这个字符C添加到词典中作为一个由一个字符组成的缀-符串(string)。在编码过程中,如果出现类似的情况,也照此办理。在词典中已经包含
某些缀-符串(String)之后,如果“当前前缀P
+当前字符C”已经在词典中,就用字符C来扩展这个前缀,这样的扩展操作一直重复到获得一个在词典中没有的缀-符串(String)为止。此时就输出表示当前前缀P的码字(Code
word)和字符C,并把P+C添加到词典中作为前缀(Prefix),然后开始处理字符流(Charstream)中的下一个前缀。
LZ78编码器的输出是码字-字符(W,C)对,每次输出一对到码字流中,与码字W相对应的缀-符串(String)用字符C进行扩展生成新的缀-符串(String),然后添加到词典中。LZ78编码的具体算法如下:
步骤1: 在开始时,词典和当前前缀P都是空的。
步骤2: 当前字符C:=字符流中的下一个字符。
步骤3: 判断P+C是否在词典中:
(1) 如果“是”:用C扩展P,让P := P+C ;
(2) 如果“否”:
① 输出与当前前缀P相对应的码字和当前字符C;
② 把字符串P+C 添加到词典中。
③ 令P:=空值。
(3) 判断字符流中是否还有字符需要编码
① 如果“是”:返回到步骤2。
② 如果“否”:若当前前缀P不是空的,输出相应于当前前缀P的码字,然后结束编码。
译码算法
在译码开始时译码词典是空的,它将在译码过程中从码字流中重构。每当从码字流中读入一对码字-字符(W,C)对时,码字就参考已经在词典中的缀-符串,
然后把当前码字的缀-符串string.W
和字符C输出到字符流(Charstream),而把当前缀-符串(string.W+C)添加到词典中。在译码结束之后,重构的词典与编码时生成的词典
完全相同。LZ78译码的具体算法如下:
步骤1: 在开始时词典是空的。
步骤2: 当前码字W :=码字流中的下一个码字。
步骤3: 当前字符C := 紧随码字之后的字符。
步骤4: 把当前码字的缀-符串(string.W)输出到字符流(Charstream),然后输出字符C。
步骤5: 把string.W+C添加到词典中。
步骤6: 判断码字流中是否还有码字要译
(1) 如果“是”,就返回到步骤2。
(2) 如果“否”,则结束。
例子:
●“步骤”栏表示编码步骤。
●“位置”栏表示在输入数据中的当前位置。
●“词典”栏表示添加到词典中的缀-符串,缀-符串的索引等于“步骤”序号。
●“输出”栏以(当前码字W, 当前字符C)简化为(W, C)的形式输出。
表4-13 编码字符串
位置 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
字符 |
A |
B |
B |
C |
B |
C |
A |
B |
A |
表4-14 编码过程
步骤 |
位置 |
词典 |
输出 |
1 |
1 |
A |
(0,A) |
2 |
2 |
B |
(0,B) |
3 |
3 |
B C |
(2,C) |
4 |
5 |
B C A |
(3,A) |
5 |
8 |
B A |
(2,A) |
与LZ77相比,LZ78的最大优点是在每个编码步骤中减少了缀-符串(String)比较的数目,而压缩率与LZ77类似。
4.4.LZW算法
在LZW算法中使用的术语与LZ78使用的相同,仅增加了一个术语—前缀根(Root),它是由单个字符串组成
的缀-符串(String)。在编码原理上,LZW与LZ78相比有如下差别:①LZW只输出代表词典中的缀-符串(String)的码字(code
word)。这就意味在开始时词典不能是空的,它必须包含可能在字符流出现中的所有单个字符,即前缀根(Root)。②由于所有可能出现的单个字符都事先
包含在词典中,每个编码步骤开始时都使用一字符前缀(one-character prefix),因此在词典中搜索的第1个缀-符串有两个字符。
编码算法
LZW编码是围绕称为词典的转换表来完成的。这张转换表用来存放称为前缀(Prefix)的字符序列,并且为每个表项分配一个码字(Code
word),或者叫做序号.这张转换表实际上是把8位ASCII字符集进行扩充,增加的符号用来表示在文本或图像中出现的可变长度
ASCII字符串。扩充后的代码可用9位、10位、11位、12位甚至更多的位来表示。Welch的论文中用了12位,12位可以有4096个不同的12
位代码,这就是说,转换表有4096个表项,其中256个表项用来存放已定义的字符,剩下3840个表项用来存放前缀(Prefix)。
码字(Code word) |
前缀(Prefix) |
1 |
|
… |
… |
193 |
A |
194 |
B |
… |
… |
255 |
|
… |
… |
1305 |
abcdefxyF01234 |
… |
… |
参考:
http://www.binaryessence.com/dct/en000042.htm
http://www.stringology.org/DataCompression/content.html
优秀博客:
http://blog.csdn.net/abcjennifer/article/details/8022445
http://blog.csdn.net/abcjennifer/article/details/8020695
多媒体基础:
http://www.360doc.com/content/14/0929/18/17799864_413295846.shtml
标签:
原文地址:http://www.cnblogs.com/zxqstrong/p/4597893.html