我希望通过本文的介绍,能给那些目前不太了解lzw算法和该算法在gif图像中应用,但渴望了解它的人一些启发和帮助。抛砖引玉而已,更希望园子里面兄弟提出宝贵的意见。
1.LZW的全称是什么?
Lempel-Ziv-Welch (LZW).
2. LZW的简介和压缩原理是什么?
LZW压缩算法是一种新颖的压缩方法,由Lemple-Ziv-Welch 三人共同创造,用他们的名字命名。它采用了一种先进的串表压缩,将每个第一次出现的串放在一个串表中,用一个数字来表示串,压缩文件只存贮数字,则不存贮 串,从而使图象文件的压缩效率得到较大的提高。奇妙的是,不管是在压缩还是在解压缩的过程中都能正确的建立这个串表,压缩或解压缩完成后,这个串表又被丢 弃。
LZW算法中,首先建立一个字符串表,把每一个第一次出现的字符串放入串表中,并用一个数字来表示,这个数字与此字符串在串表中的位置有关,并将这个数字 存入压缩文件中,如果这个字符串再次出现时,即可用表示它的数字来代替,并将这个数字存入文件中。压缩完成后将串表丢弃。如"print" 字符串,如果在压缩时用266表示,只要再次出现,均用266表示,并将"print"字符串存入串表中,在图象解码时遇到数字266,即可从串表中查出 266所代表的字符串"print",在解压缩时,串表可以根据压缩数据重新生成。
3.在详细介绍算法之前,先列出一些与该算法相关的概念和词汇
1)'Character': 字符,一种基础数据元素,在普通文本文件中,它占用1个单独的byte,而在图像中,它却是 一种代表给定像素颜色的索引值。
2)'CharStream':数据文件中的字符流。
3)'Prefix':前缀。如这个单词的含义一样,代表着在一个字符最直接的前一个字符。一个前缀字符长度可以为0,一个prefix和一个character可以组成一个字符串(string),
4)'Suffix': 后缀,是一个字符,一个字符串可以由(A,B)来组成,A是前缀,B是后缀,当A长度为0的时候,代表Root,根
5)'Code:码,用于代表一个字符串的位置编码
6)'Entry',一个Code和它所代表的字符串(string)
4.压缩算法的简单示例,不是完全实现LZW算法,只是从最直观的角度看lzw算法的思想
对原始数据ABCCAABCDDAACCDB进行LZW压缩
原始数据中,只包括4个字符(Character),A,B,C,D,四个字符可以用一个2bit的数表示,0-A,1-B,2-C,3-D,从最直观的角度看,原始字符串存在重复字符:ABCCAABCDDAACCDB,用4代表AB,5代表CC,上面的字符串可以替代表示为:45A4CDDAA5DB,这样是不是就比原数据短了一些呢!
5.LZW算法的适用范围
为了区别代表串的值(Code)和原来的单个的数据值(String),需要使它们的数值域不重合,上面用0-3来代表A-D,那么AB就必须用大于3的 数值来代替,再举另外一个例子,原来的数值范围可以用8bit来表示,那么就认为原始的数的范围是0~255,压缩程序生成的标号的范围就不能为 0~255(如果是0-255,就重复了)。只能从256开始,但是这样一来就超过了8位的表示范围了,所以必须要扩展数据的位数,至少扩展一位,但是这样不是增加了1个字符占用的空间了么?但是却可以用一个字符代表几个字符,比如原来255是8bit,但是现在用256来表示254,255两个数,还是划得来的。从这个原理可以看出LZW算法的适用范围是原始数据串最好是有大量的子串多次重复出现,重复的越多,压缩效果越好。反之则越差,可能真的不减反增了。
6.LZW算法中特殊标记
随着新的串(string)不断被发现,标号也会不断地增长,如果原数据过大,生成的标号集(string table)会越来越大,这时候操作这个集合就会产生效率问题。如何避免这个问题呢?Gif在采用lzw算法的做法是当标号集足够大的时候,就不能增大 了,干脆从头开始再来,在这个位置要插入一个标号,就是清除标志CLEAR,表示从这里我重新开始构造字典,以前的所有标记作废,开始使用新的标记。
这时候又有一个问题出现,足够大是多大?这个标号集的大小为比较合适呢?理论上是标号集大小越大,则压缩比率就越高,但开销也越高。 一般根据处理速度和内存空间连个因素来选定。GIF规范规定的是12位,超过12位的表达范围就推倒重来,并且GIF为了提高压缩率,采用的是变长的字 长。比如说原始数据是8位,那么一开始,先加上一位再说,开始的字长就成了9位,然后开始加标号,当标号加到512时,也就是超过9为所能表达的最大数据 时,也就意味着后面的标号要用10位字长才能表示了,那么从这里开始,后面的字长就是10位了。依此类推,到了2^12也就是4096时,在这里插一个清 除标志,从后面开始,从9位再来。
GIF规定的清除标志CLEAR的数值是原始数据字长表示的最大值加1,如果原始数据字长是8,那么清除标志就是256,如果原始数据字长为4那么就是16。另外GIF还规定了一个结束标志END,它的值是清除标志CLEAR再加1。由于GIF规定的位数有1位(单色图),4位(16色)和8位(256色),而1位的情况下如果只扩展1位,只能表示4种状态,那么加上一个清除标志和结束标志就用完了,所以1位的情况下就必须扩充到3位。其它两种情况初始的字长就为5位和9位。此处参照了http://blog.csdn.net/whycadi/
7.用lzw算法压缩原始数据的示例分析
输入流,也就是原始的数据为:255,24,54,255,24,255,255,24,5,123,45,255,24,5,24,54..................
这个正好可以看到是gif文件中像素数组的一部分,如何对它进行压缩
因为原始数据可以用8bit来表示,故清除标志Clear=255+1 =256,结束标志为End=256+1=257,目前标号集为
0 1 2 3 .................................................................................255 CLEAR END
第一步,读取第一个字符为255,在标记表里面查找,255已经存在,我们已经认识255了,不做处理
第二步,取第二个字符,此时前缀为A,形成当前的Entry为(255,24),在标记集合不存在,我们并不认识255,24好,这次你小子来了,我就记住你,把它在标记集合中标记为258,然后输出前缀A,保留后缀24,并作为下一次的前缀(后缀变前缀)
第三步,取第三个字符为54,当前Entry(24,54),不认识,记录(24,54)为标号259,并输出24,后缀变前缀
第四部:取第四个字符255,Entry=(54,255),不认识,记录(54,255)为标号260,输出54,后缀变前缀
第五步 取第5个字符24,entry=(255,24),啊,认识你,这不是老258么,于是把字符串规约为258,并作为前缀
第六步 取第六个字符255,entry=(258,255),不认识,记录(258,255)为261,输出258,后缀变前缀
.......
一直处理到最后一个字符,
用一个表记录处理过程
CLEAR=256,END=257
第几步 | 前缀 | 后缀 | Entry | 认识(Y/N) | 输出 | 标号 |
1 | 255 | (,255) | ||||
2 | 255 | 24 | (255,24) | N | 255 | 258 |
3 | 24 | 54 | (24,54) | N | 24 | 259 |
4 | 54 | 255 | (54,255) | N | 54 | 260 |
5 | 255 | 24 | (255,24) | Y | ||
6 | 258 | 255 | (258,255) | N | 258 | 261 |
7 | 255 | 255 | (255,255) | N | 255 | 262 |
1 | 2 | 3 | 4 | 5 | |
A | B | C | D | Clear | End |
第几步 | 前缀 | 后缀 | Entry | 认识(Y/N) | 输出 | 标号 |
1 | A | (,A) | ||||
2 | A | B | (A,B) | N | A | 6 |
3 | B | A | (B,A) | N | B | 7 |
4 | A | B | (A,B) | Y | ||
5 | 6 | A | (6,A) | N | 6 | 8 |
6 | A | B | (A,B) | Y | ||
7 | 6 | A | (6,A) | Y | ||
8 | 8 | B | (8,B) | N | 8 | 9 |
9 | B | B | (B,B) | N | B | 10 |
10 | B | B | (B,B) | Y | ||
11 | 10 | A | (10,A) | N | 10 | 11 |
12 | A | B | (A,B) | Y |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
A | B | C | D | Clear | End | AB | BA | 6A | 8B | BB | 10A |
python 代码:
# -*- coding: gbk -*- def LZW ( inStr,narrow=False,bits=12): '''使用LZW压缩算法压缩。 narrow为True时输出字节流位紧缩 默认最大位宽14位,允许范围12~16位''' if isinstance(inStr,str): inStr=list(inStr) for i in range(len(inStr)): inStr[i]=ord(inStr[i]) sOutStr=[256] #先放一个 开始&清除 标记 mTagMap={} #字典 iTagCurrent=258 #当前的标记总数 0~255 标记256 为begin & clear , 257 为end iBitsLen=9 iTag=inStr[0] #当前标记 ii=0 cTemp=0 if bits>16 or bits<12: return None iMaxLen=(1<<bits)-1 #最多允许的标记数,由位宽决定 for ii in range(1,len(inStr)): cChar=inStr[ii] cTemp=(iTag<<8)+cChar #(前缀 后缀) if cTemp in mTagMap: #该(前缀 后缀)已存在 iTag=mTagMap[cTemp] #取出其标记 else: #不存在 sOutStr.append(iTag) #将前缀放入输出流 mTagMap[cTemp]=iTagCurrent iTagCurrent+=1 #增加一个标记,并放入字典 iTag=cChar #当前标记为后缀 if iTagCurrent>=iMaxLen: #如果到达了最大标记数,清除字典,从头开始 sOutStr.append(256) mTagMap={} iTagCurrent=258 if iTag!=0: sOutStr.append(iTag) sOutStr.append(257) #放入结束标记 if narrow: #位紧缩 return Narrow(sOutStr) else: return sOutStr #把9~16位的01码变成8位的01码,也就是一个字节... def Narrow (sOutStr): sOutN=[] iTemp=0 BitLeft=0 nowBit=9 #当前位宽 nowStag=1<<nowBit #当前位宽允许的标记数 nowTagCount=258 for cChar in sOutStr: iTemp=iTemp+(cChar<<BitLeft) nowTagCount+=1 BitLeft+=nowBit if cChar==256: nowBit=9 nowStag=1<<nowBit nowTagCount=258 if nowTagCount>=nowStag: nowBit+=1 nowStag=1<<nowBit while BitLeft>=8: sOutN.append(iTemp&0xff) iTemp=iTemp>>8 BitLeft-=8 if BitLeft>0: sOutN.append(iTemp) return sOutN def UnNarrow (inStr): sOut=[] iTemp=0 BitLeft=0 nowBit=9 nowStag=1<<nowBit mask=nowStag-1 nowTagCount=258 for cChar in inStr: iTemp=iTemp+(cChar<<BitLeft) BitLeft+=8 if BitLeft>=nowBit: cTemp=iTemp&mask iTemp=iTemp>>nowBit BitLeft-=nowBit sOut.append(cTemp) nowTagCount+=1 if nowTagCount>=nowStag: nowBit+=1 nowStag=1<<nowBit mask=nowStag-1 if cTemp==256: nowBit=9 nowStag=1<<nowBit mask=nowStag-1 nowTagCount=258 if BitLeft>0: sOut.append(iTemp) return sOut def deTag ( mTagMap,nowTag,outStr): '''将一个标记转化为元字符序列,并放入到输出流中''' if nowTag>=0: sTemp=[] while nowTag>255: pair=mTagMap[nowTag] sTemp.append(pair[1]) nowTag=pair[0] sTemp.append(nowTag) sTemp.reverse() outStr+=sTemp return nowTag def UnLZW ( inStr , narrow=False): if narrow: inStr=UnNarrow(inStr) mTagMap={} outStr=[] nowTagCount=258 sTemp=[] nowTag=-1 for cChar in inStr: if cChar==256: if nowTag>=0: deTag(mTagMap,nowTag,outStr) mTagMap={} nowTagCount=258 nowTag=-1 elif cChar==257: if nowTag>=0: deTag(mTagMap,nowTag,outStr) nowTag=-1 return outStr elif nowTag==-1: #刚开始 nowTag=cChar else: pair=[nowTag,0] mTagMap[nowTagCount]=pair nowTagCount+=1 surfix=cChar while surfix>255: if surfix not in mTagMap: print 'Thera are errors in input' return outStr surfix=mTagMap[surfix][0] pair[1]=surfix deTag(mTagMap,nowTag,outStr) nowTag=cChar def fileLZW ( inFile , outFile , bit=14 ): import os fsize=os.stat(inFile)[6] f=os.open(inFile,os.O_BINARY) s=os.read(f,fsize) os.close(f) a=LZW(s,True,bit) f=open(outFile,'wb') outs='' for i in a: outs+=chr(i) f.write(outs) f.close() def fileUnlzw ( inFile , outFile): import os fsize=os.stat(inFile)[6] f=os.open(inFile,os.O_BINARY) s=os.read(f,fsize) a=[] for i in s: a.append(ord(i)) b=UnLZW(a,True) f=open(outFile,'wb') outs='' for i in b: outs+=chr(i) f.write(outs) f.close()
python这个算法也还可以,不过比起网上流行的C些的LZW压缩算法可能要差一点,网上的算法加了哈什表,通过哈什表查找,一一对应,效率很高。这个算法利用python的查找字典。另外,把整个列表(C是数字)转成字节进行存储,它Narrow函数也跟C写的不一样,她是01码从左到右流出,而C是从右到左流出,相对应的解压时的算法也有所不同,所以用这个python算法进行压缩和用C写的压缩后的文件是不一样的。如果真想要一样,就修改里面的Narrow函数为如下:
def Narrow(sOutStr): sOutN=[] iTemp=0 BitLeft=0 nowBit=9 #当前位宽 nowStag=1<<nowBit #当前位宽允许的标记数 nowTagCount=258 for cChar in sOutStr: iTemp |= (cChar<<(32-nowBit-BitLeft)) nowTagCount+=1 BitLeft+=nowBit if cChar==256: nowBit=9 nowStag=1<<nowBit nowTagCount=258 if nowTagCount>=nowStag: nowBit+=1 nowStag=1<<nowBit while BitLeft>=8: sOutN.append(iTemp>>24) iTemp = (iTemp<<8)&0xffffffff #由于python自动转化类型,所以这里如果不与上32位数,会出错 BitLeft-=8 #python会自动转成长整型,超过32位数 if BitLeft>0: sOutN.append(iTemp>>24) return sOutN
当然解压的函数UnNarrow()一样要修改:
def UnNarrow(sInStr): sInStrUn=[] iTemp=0 BitRight=0 nowBit=9 #当前宽度 nowStag=1<<nowBit #当前位宽允许的标记数 nowTagCount=258 for cChar in sInStr: iTemp |=cChar<<(32-8-BitRight) BitRight+=8 while BitRight>=nowBit: cTemp =iTemp>>(32-nowBit) sInStrUn.append(iTemp>>(32-nowBit)) nowTagCount += 1 iTemp = (iTemp<<nowBit)&0xffffffff BitRight-=nowBit if nowTagCount>=(nowStag): nowBit+=1 nowStag=1<<nowBit if cTemp==256: nowTagCount=258 nowBit=9 nowStag=1<<nowBit if BitRight>0: sInStrUn.append(iTemp>>(32-nowBit)) return sInStrUn