關於 LZW 算法的一些思考

                    關於 LZW 算法的一點思考

以下關於 LZW 算法的敘述轉自: http://blog.moocky.net/?action=show&id=27

 

2.LZW算法和GIF數據壓縮

  GIF文件的圖象數據使用了可變長度編碼的LZW壓縮算法(Variable-Length_Code LZW Compression),這是從LZW(Lempel Ziv Compression)壓縮算法演變過來的,通過壓縮原始數據的重複部分來達到減少文件大小的目的。

LZW的壓縮原理:

先來解釋一下幾個基本概念:
  LZW壓縮有三個重要的對象:數據流(CharStream)、編碼流(CodeStream)和編譯表(String Table)。在編碼時,數據流是輸入對象(圖象的光柵數據序列),編碼流就是輸出對象(存儲在GIF文件的圖象數據);在解碼時,編碼流則是輸入對象,數據流是輸出對象;而編譯表是在編碼和解碼時都須要用藉助的對象。

字符(Character):最基礎的數據元素,在文本文件中就是一個字節,在光柵數據中就是一個像素的顏色在指定的顏色列表中的索引值;
字符串(String):由幾個連續的字符組成;
前綴(Prefix):也是一個字符串,不過通常用在另一個字符的前面,而且它的長度可以爲0;
根(Root):單個長度的字符串;
編碼(Code):一個數字,按照固定長度(編碼長度)從編碼流中取出,也是編碼表的映射值;
圖案:一個字符串,按不定長度從數據流中讀出。

  LZW壓縮的原理,就是先提取原始圖象數據中的不同圖案,基於這些圖案創建一個編譯表,然後用編譯表中的圖案索引來替代原始光柵數據中的相應圖案,減少原始數據大小。看起來和調色板圖象的實現原理差不多,但是應該注意到的是,我們這裏的編譯表不是事先創建好的,而是根據原始圖象數據動態創建的,解碼時還要從已編碼的數據中還原出原來的編譯表(GIF文件中是不攜帶編譯表信息的),爲了更好理解編解碼原理,我們來看看具體的處理過程:

編碼器(Compressor)


  編碼數據,第一步,初始化一個編譯表,假設這個編譯表的大小是12位的,也就是最多有4096個單位,另外假設我們有32個不同的字符(也可以認爲圖象的每個像素最多有32種顏色),表示爲a,b,c,d,e...,初始化編譯表:第0項爲a,第1項爲b,第2項爲c...一直到第31項,我們把這32項就稱爲根。
  開始編譯,先定義一個前綴對象Current Prefix,記爲[.c.],現在它是空的,然後定義一個當前字符串Current String,標記爲[.c.]k,[.c.]就爲Current Prefix,k就爲當前讀取字符。現在來讀取數據流的第一個字符,假如爲p,那麼Current String就等於[.c.]p(由於[.c.]爲空,實際上值就等於p),現在在編譯表中查找有沒有Current String的值,由於p就是一個根,我們已經初始了32個根,當然可以找到,把p設爲Current Prefix的值,不做任何事繼續讀取下一個字符,假設爲q,Current String就等於[.c.]q(也就是pq),看看在編譯表中有沒有該值,當然。沒有,這時我們要做下面的事情:將Current String的值(也就是pq)添加到編譯表的第32項,把Current Prefix的值(也就是p)在編譯表中的索引輸出到編碼流,修改Current Prefix爲q。繼續往下讀,如果在編譯表中可以查找到Current String的值([.c.]k),則把Current String的值([.c.]k)賦予Current Prefix;如果查找不到,則添加Current String的值([.c.]k)到編譯表,把Current Prefix的值([.c.])在編譯表中所對應的索引輸出到編碼流,同時修改Current Prefix爲k ,這樣一直循環下去直到數據流結束。僞代碼看起來就像下面這樣:

Initialize String Table;
[.c.] = Empty;
[.c.]k = First Character in CharStream;
while ([.c.]k != EOF )
{
  if ( [.c.]k is in the StringTable)
  {
    [.c.] = [.c.]k;
  }
  else
  {
    add [.c.]k to the StringTable;
    Output the Index of [.c.] in the StringTable to the CodeStream;
    [.c.] = k;
  }
  [.c.]k = Next Character in CharStream;
}
Output the Index of [.c.] in the StringTable to the CodeStream;
 

來看一個具體的例子,我們有一個字母表a,b,c,d.有一個輸入的字符流abacaba。現在來初始化編譯表:#0=a,#1=b,#2=c,#3=d.現在開始讀取第一個字符a,[.c.]a=a,可以在在編譯表中找到,修改[.c.]=a;不做任何事繼續讀取第二個字符b,[.c.]b=ab,在編譯表中不能找,那麼添加[.c.]b到編譯表:#4=ab,同時輸出[.c.](也就是a)的索引#0到編碼流,修改[.c.]=b;讀下一個字符a,[.c.]a=ba,在編譯表中不能找到:添加編譯表#5=ba,輸出[.c.]的索引#1到編碼流,修改[.c.]=a;讀下一個字符c,[.c.]c=ac,在編譯表中不能找到:添加編譯表#6=ac,輸出[.c.]的索引#0到編碼流,修改[.c.]=c;讀下一個字符a,[.c.]c=ca,在編譯表中不能找到:添加編譯表#7=ca,輸出[.c.]的索引#2到編碼流,修改[.c.]=a;讀下一個字符b,[.c.]b=ab,編譯表的#4=ab,修改[.c.]=ab;讀取最後一個字符a,[.c.]a=aba,在編譯表中不能找到:添加編譯表#8=aba,輸出[.c.]的索引#4到編碼流,修改[.c.]=a;好了,現在沒有數據了,輸出[.c.]的值a的索引#0到編碼流,這樣最後的輸出結果就是:#0#1#0#2#4#0.


    可以看出,整個編碼過程不斷的構成新的符號,越到後面,索引代表的字符流會越長。當前綴和當前字符構成的字符串在索引表中時,將該字符串作爲新的前綴,接着讀入下一個字符,這樣就保證了從前綴第一個字符往後,會一直與索引表中的內容匹配。如果前綴和當前字符構成的字符串不在索引表中是,輸出前綴的索引,此時會把匹配最長的索引值來代替當前字符串,同時將這個新的字符串加入到索引表中。同時,也要注意到這裏一個事實,就是如果輸出了一個索引值,那麼該索引代表和字符串以及當前字符會作爲一個新的索引加入到索引表中,這是解碼的一個重要依據。


解碼器(Decompressor)


  好了,現在來看看解碼數據。數據的解碼,其實就是數據編碼的逆向過程,要從已經編譯的數據(編碼流)中找出編譯表,然後對照編譯表還原圖象的光柵數據。
  首先,還是要初始化編譯表。GIF文件的圖象數據的第一個字節存儲的就是LZW編碼的編碼大小(一般等於圖象的位數),根據編碼大小,初始化編譯表的根條目(從0到2的編碼大小次方),然後定義一個當前編碼Current Code,記作[code],定義一個Old Code,記作[old]。讀取第一個編碼到[code],這是一個根編碼,在編譯表中可以找到,把該編碼所對應的字符輸出到數據流,[old]=[code];讀取下一個編碼到[code],這就有兩種情況:在編譯表中有或沒有該編碼,我們先來看第一種情況:先輸出當前編碼[code]所對應的字符(串)到數據流,然後把[old]所對應的字符(串)當成prefix [...],當前編碼[code]所對應的字符(串)的第一個字符當成k,組合起來當前字符串Current String就爲[...]k,把[...]k添加到編譯表,讀下一個編碼;我們來看看在編譯表中找不到該編碼的情況,回想一下編碼情況:如果數據流中有一個p[...]p[...]pq這樣的字符串,p[...]在編譯表中而p[...]p不在,編譯器將輸出p[...]的索引而添加p[...]p到編譯表,下一個字符串p[...]p可以在編譯表中找到而p[...]pq不在編譯表,同樣將輸出p[...]p的索引值而添加p[...]pq到編譯表,這樣看來,解碼器總比編碼器『慢一步』,當我們遇到p[...]p所對應的索引時,我們不知到該索引對應的字符串(在解碼器的編譯表中還沒有該索引,事實上,這個索引將在下一步添加),這時需要用猜測法:現在假設上面的p[...]所對應的索引值是#58,那麼上面的字符串經過編譯之後是#58#59,我們在解碼器中讀到#59時,編譯表的最大索引只有#58,#59所對應的字符串就等於#58所對應的字符串(也就是p[...])加上它的第一個字符(也就是p),也就是p[...]p。事實上,這種猜測法是很準確(有點不好理解,仔細想一想吧)。上面的解碼過程用僞代碼表示就像下面這樣:

 

 

Initialize String Table;
[code] = First Code in the CodeStream;
Output the String for [code] to the CharStream;
[old] = [code];
[code] = Next Code in the CodeStream;
while ([code] != EOF )
{
  if ( [code] is in the StringTable)
  {
    Output the String for [code] to the CharStream; // 輸出[code]所對應的字符串
    [...] = translation for [old]; // [old]所對應的字符串
    k = first character of translation for [code]; // [code]所對應的字符串的第一個字符
    add [...]k to the StringTable;
    [old] = [code];
  }
  else
  {
    [...] = translation for [old];
    k = first character of [...];
    Output [...]k to CharStream and add it to StringTable;
    [old] = [code];
  }
  [code] = Next Code in the CodeStream;
}
 

    如果解碼時候,出現了輸入流是 #58 #59, 而此時 索引表的最大索引纔到 #58 的情況,應該怎麼辦? 首先確定 #58 所代表的肯定不是根。 那麼 #58 可以寫成 p[..] 這種形式。 現在出現了一個新的索引 #59, 在索引表中沒有, 那麼該索引肯定是剛生成的, 剛生成的索引肯定是  p[..]x, 隨後又立刻用到這個剛生成的索引了,所以字符流應該是 p[..]p[..]x , 由於 #58 , #59 連續, 所以 p[..]p = p[..]x, 所以, x一定是 p, 就是#58 的第一個字符。同時還有一個問題, 就是爲什麼 p[..]x 一定是那個最新的索引呢?如果它不是最新的,那麼在索引表中應該有 p[..]x 這一項了,這樣在編碼的時候, p[..]x 就會被編到另外一個索引,直到 p'[..]x 不再在索引表中存在,這也是爲什麼最新的索引對應的肯定是當前索引對應的字符串加上下一個索引的第一個字符。 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章