http://www.cnblogs.com/qcloud1001/p/6647080.html
成文迪, 在Web前端摸爬滾打的碼農一枚,對技術充滿熱情的菜鳥,緻力為手Q的建設添磚加瓦。
GIF(Graphics Interchange Format)原義是“圖像互換格式”,是CompuServe公司在1987年開發出的圖像檔案格式,可以說是網際網路界的老古董了。
本來,随着網絡帶寬的拓展和視訊技術的進步,這種圖像已經漸漸失去了市場。可是,近年來流行的表情封包化,讓老古董GIF圖有了新的用武之地。
表情包通常來源于手繪圖像,或是視訊截取,目前有很多友善制作表情包的小工具。
這類圖檔通常具有檔案體積小,内容簡單,相容性好(無需解碼工具即可在各類平台上檢視),對畫質要求不高的特點,剛好符合GIF圖的特性。
是以,老古董GIF圖有了新的應用場景。
新的應用場景帶來新的需求,本文所探究的問題來自于某個業務場景下——為使用者批量推送GIF表情包。
一批圖像大約有200-500張,以縮略圖清單的形式展示在用戶端。
根據我們使用測試資料進行的統計GIF圖表情包的尺寸大部分在200k-500k之間,批量推送的一個重要問題就是資料量太大,是以,我們希望能夠在清單裡展示體積較小的縮略圖,使用者點選後,再單獨拉取原圖。
傳統的GIF縮略圖是靜态的,通常是提取第一幀,但在表情包的情形下,這種方式不足以表達出圖檔中資訊。比如下面的例子
——第一幀完全看不出重點啊!
是以,我們希望縮略圖也是動态的,并盡可能和原圖相似。
對于傳統圖檔來說,檔案大小一般和圖檔分辨率(尺寸)正相關,是以,生成縮略圖最直覺的思路就是縮小尺寸,resize大法。
但是在GIF圖的場合,這個方式不再高效,因為GIF圖的檔案大小還受到一個重要的因素制約——幀數
以這張柴犬表情為例,原圖寬度200,尺寸1.44M,等比縮放到150之後,尺寸還是1.37M,等比縮放到100,相當于尺寸變為原來的四分之一,體積還是749K
可見,resize大法的壓縮率并不理想,收效甚微。
而且,我們所得到的大部分表情圖素材,分辨率已經很小了,為了保證用戶端展示效果,不能夠過度減少尺寸,不然圖檔會變得模糊。
是以,想要對GIF圖進行壓縮,隻能從别的方向入手。
想要壓縮一個檔案,首先要了解它是如何存儲的。畢竟,程式設計的事,萬變不離其宗嘛。
作為一種古老的格式,GIF的存儲規則也相對簡單,容易了解,一個GIF檔案主要由以下幾部分組成。
檔案頭
圖像幀資訊
注釋
下面我們來分别探究每個部分。
GIF格式檔案頭和一般檔案頭差别不大,也包含有
格式聲明
邏輯螢幕描述塊
全局調色盤
Signature 為“GIF”3 個字元;Version 為“87a”或“89a”3 個字元。
前兩位元組為像素機關的寬、高,用以辨別圖檔的視覺尺寸。
Packet裡是調色盤資訊,分别來看——
Global Color Table Flag為全局顔色表标志,即為1時表明全局顔色表有定義。
Color Resolution 代表顔色表中每種基色位長(需要+1),為111時,每個顔色用8bit表示,即我們熟悉的RGB表示法,一個顔色三位元組。
Sort Flag 表示是否對顔色表裡的顔色進行優先度排序,把常用的排在前面,這個主要是為了适應一些顔色解析度低的早期渲染器,現在已經很少使用了。
Global Color Table 表示顔色表的長度,計算規則是值+1作為2的幂,得到的數字就是顔色表的項數,取最大值111時,項數=256,也就是說GIF格式最多支援256色的位圖,再乘以Color Resolution算出的位元組數,就是調色盤的總長度。
這四個字段一起定義了調色盤的資訊。
Background color Index 定義了圖像透明區域的背景色在調色盤裡的索引。
Pixel Aspect Ratio 定義了像素寬高比,一般為0。
什麼是調色盤?我們先考慮最直覺的圖像存儲方式,一張分辨率M×N的圖像,本質是一張點陣,如果采用Web最常見的RGB三色方式存儲,每個顔色用8bit表示,那麼一個點就可以由三個位元組(3BYTE = 24bit)表達,比如0xFFFFFF可以表示一個白色像素點,0x000000表示一個黑色像素點。
如果我們采用最原始的存儲方式,把每個點的顔色值寫進檔案,那麼我們的圖像資訊就要占據就是3×M×N位元組,這是靜态圖的情況,如果一張GIF圖裡有K幀,點陣資訊就是3×M×N×K。
下面這張兔子snowball的表情有18幀,分辨率是200×196,如果用上述方式計算,檔案尺寸至少要689K。
但實際檔案尺寸隻有192K,它一定經曆過什麼……
我們可以使用指令行圖檔處理工具gifsicle來看看它的資訊。
我們得到下面的文本
可以看到,global color table [128]就是它的調色盤,長度128。
為了确認,我們再用二進制檢視器檢視一下它的檔案頭
可以看到Packet裡的字段的确符合我們的描述。
在實際情況中,GIF圖具有下面的特征
(1)一張圖像最多隻會包含256個RGB值。
(2)在一張連續動态GIF裡,每一幀之間資訊差異不大,顔色是被大量重複使用的。
在存儲時,我們用一個公共的索引表,把圖檔中用到的顔色提取出來,組成一個調色盤,這樣,在存儲真正的圖檔點陣時,隻需要存儲每個點在調色盤裡的索引值。
如果調色盤放在檔案頭,作為所有幀公用的資訊,就是公共(全局)調色盤,如果放在每一幀的幀資訊中,就是局部調色盤。GIF格式允許兩種調色盤同時存在,在沒有局部調色盤的情況下,使用公共調色盤來渲染。
這樣,我們可以用調色盤裡的索引來代表實際的顔色值。
一個256色的調色盤,24bit的顔色隻需要用9bit就可以表達了。
調色盤還可以進一步減少,128色,64色,etc,相應的壓縮率就會越來越大……
還是以兔子為例,我們還可以嘗試指定它的調色盤大小,對它進行重壓縮
依然使用gifsicle工具,colors參數就是調色盤的長度,得到的結果
注意到了2的時候,圖像已經變成了黑白二值圖。
居然還能看出是個兔子……
是以我們得出結論——如果可以接受犧牲圖像的部分視覺效果,就可以通過減色來對圖像做進一步壓縮。
檔案頭所包含的對我們有用的資訊就是這些了,我們繼續往後看。
幀資訊描述就是每一幀的圖像資訊和相關标志位,在逐項了解它之前,我們首先探究一下幀的存儲方式。
我們已經知道調色盤相關的定義,除了全局調色盤,每一幀可以擁有自己的局部調色盤,渲染順序更優先,它的定義方式和全局調色盤一緻,隻是作用範圍不同
直覺地說,幀資訊應該由一系列的點陣資料組成,點陣中存儲着一系列的顔色值。點陣資料本身的存儲也是可以進行壓縮的,GIF圖所采用的是LZW壓縮算法。
這樣的壓縮和圖像本身性質無關,是位元組層面的,文本資訊也可以采用(比如常見的gzip,就是LZW和哈夫曼樹的一個實作)
基于表查詢的無損壓縮是如何進行的?基本思路是,對于原始資料,将每個第一次出現的串放在一個串表中,用索引來表示串,後續遇到同樣的串,簡化為索引來存儲(串表壓縮法)
舉一個簡單的例子來說明LZW算法的核心思路。
有原始資料:ABCCAABCDDAACCDB
可以看出,原始資料裡隻包括4個字元A,B,C,D,四個字元可以用2bit的索引來表示,0-A,1-B,2-C,3-D。
原始字元串存在重複字元,比如AB,CC,都重複出現過。用4代表AB,5代表CC,上面的字元串可以替代表示為45A4CDDAA5DB
這樣就完成了壓縮,串長度從16縮減到12。對原始資訊來說,LZW壓縮是無損的。
除了采用LZW之外,幀資訊存儲過程中還采取了一些和圖像相關的優化手段,以減小檔案的體積,直覺表述就是——公共區域排除、透明區域疊加
這是ImageMagick官方範例裡的一張GIF圖。
根據直覺感受,這張圖檔的每一幀應該是這樣的。
但實際上,進行過壓縮優化的圖檔,每一幀是這樣的。
首先,對于各幀之間沒有變化的區域進行了排除,避免存儲重複的資訊。
其次,對于需要存儲的區域做了透明化處理,隻存儲有變化的像素,沒變化的像素隻存儲一個透明值。
這樣的優化在表情包中也是很常見的,舉個栗子
上面這個表情的檔案大小是278KB,幀數是14
我們試着用工具将它逐幀拆開,這裡使用另一個指令行圖像處理工具ImageMagick
<code>gm convert source.gif target_%d.gif</code>
可以看出,除了第一幀之外,後面的幀都做了不同程度的處理,檔案體積也比第一幀小。
這樣的壓縮處理也是無損的,帶來的壓縮比和原始圖像的具體情況有關,重複區域越多,壓縮效果越好,但相應地,也需要存儲一些額外的資訊,來告訴引擎如何渲染,具體包括
幀資料長寬分辨率,相對整圖的偏移位置
透明彩色索引——填充透明點所用的顔色
Disposal Method——定義該幀對于上一幀的疊加方式
Delay Time——定義該幀播放時的停留時間
其中值得額外說明的是Disposal Method,它定義的是幀之間的疊加關系,給定一個幀序列,我們用怎樣的方式把它們渲染成起來。
詳細參數定義,可以參考該網站的範例
<a href="http://www.theimage.com/animation/pages/disposal.html" target="_blank">http://www.theimage.com/animation/pages/disposal.html</a>
Disposal Method和透明顔色一起,定義了幀之間的疊加關系。在實際使用中,我們通常把第一幀當做基幀(background),其餘幀向前一幀對齊的方式來渲染,這裡不再贅述。
了解了上面的内容,我們再來看幀資訊的具體定義,主要包括
幀分隔符
幀資料說明
點陣資料(它存儲的不是顔色值,而是顔色索引)
幀資料擴充(隻有89a标準支援)
1和3比較直覺,第二部分和第四部分則是一系列的标志位,定義了對于“幀”需要說明的内容。
幀資料說明。
除了上面說過的字段之外,還多了一個Interlace Flag,表示幀點陣的存儲方式,有兩種,順序和隔行交錯,為 1 時表示圖像資料是以隔行方式存放的。最初 GIF 标準設定此标志的目的是考慮到通信裝置間傳輸速度不理想情況下,用這種方式存放和顯示圖像,就可以在圖像顯示完成之前看到這幅圖像的概貌,慢慢的變清晰,而不覺得顯示時間過長。
幀資料擴充是89a标準增加的,主要包括四個部分。
1、程式擴充結構(Application Extension)主要定義了生成該gif的程式相關資訊
2、注釋擴充結構(Comment Extension)一般用來儲存圖檔作者的簽名資訊
3、圖形控制擴充結構(Graphic Control Extension)這部分對圖檔的渲染比較重要
除了前面說過的Dispose Method、Delay、Background Color之外,User Input用來定義是否接受使用者輸入後再播放下一幀,需要圖像解碼器對應api的配合,可以用來實作一些特殊的互動效果。
4、平滑文本擴充結構(Plain Text Control Extension)
89a标準允許我們将圖檔上的文字資訊額外儲存在擴充區域裡,但實際渲染時依賴解碼器的字型環境,是以實際情況中很少使用。
以上擴充塊都是可選的,隻有Label置位的情況下,解碼器才會去渲染
說完了基本原理,來分析一下我們的實際問題。
給大量表情包生成縮略圖,在不損耗原畫質的前提下,盡可能減少圖檔體積,節省使用者流量。
之前說過,單純依靠resize大法不能滿足我們的要求,沒辦法,隻能損耗畫質了,主要有兩個思路,減少顔色和減少幀數。
減少顔色——圖檔情況各異,标準難以控制,而且會造成縮略圖和原圖視覺差異比較明顯
減少幀數——通過提取一些間隔幀,比如對于一張10幀的動畫,提取其中的提取1,3,5,7,9幀。來減少圖檔的整體體積,似乎更可行。
先看一個成果,就拿文章開頭的圖做栗子吧
看上去連貫性不如以前,但是差别不大,作為縮略圖的視覺效果可以接受,由于幀數減小,體積也可以得到明顯的優化。體積從428K縮到了140K
但是,在開發初期,我們嘗試暴力間隔提取幀,把幀重新連接配接壓成新的GIF圖,這時,會得到這樣的圖檔。
主要有兩個問題。
1、幀數過快
2、能看到明顯的殘留噪點
分析我們上面的原理,不難找到原因,正是因為大部分GIF存儲時采用了公共區域排除和透明區域疊加的優化,如果我們直接間隔抽幀,再拼起來,就破壞了原來的疊加規則,不該露出來的幀露出來了,是以才會産生噪點。
是以,我們首先要把原始資訊恢複出來。
兩個指令行工具,gifsicle和ImageMagick都提供這樣的指令。
還原之後抽幀,重建新的GIF,就可以解決問題2了。
注意重建的時候,可以應用工具再進行對透明度和公共區域的優化壓縮。
至于問題1,也是因為我們沒有對幀延遲參數Delay Time做處理,直接取原幀的參數,幀數減少了,速度一定會加快。
是以,我們需要把抽去的連續幀的總延時加起來,作為新的延遲資料,這樣可以保持縮略圖和原圖頻率一緻,看起來不會太過鬼畜,也不會太過遲緩。
提取出每一幀的delay資訊,也可以通過工具提供的指令來提取。
在實際應用中,抽幀的間隔gap是根據總幀數frame求出的
delay值的計算還做了歸一化處理,如果新生成縮略圖的幀間隔平均值大于200ms,則統一加速到均值200ms,同時保持原有節奏,這樣可以避免極端情況下,縮略圖過于遲緩。
本文介紹的算法主要應用于手Q熱圖功能的背景管理系統,使用Nodejs編寫。
ImageMagick是一個較為常用的圖像處理工具,除了gif還可以處理各類圖像檔案,有node封裝的版本可以使用。
gifsicle隻有可執行版本,在伺服器上重新編譯源碼後,采用spawn調起子程序的方式實作。
ImageMagick對于圖檔資訊的解析較為友善,可以直接得到結構化資訊。
gifsicle支援指令管道級聯,處理圖檔速度較快。
實際生産過程中,同時采用了兩個工具。
測試時,采用該算法随機選擇50張gif圖進行壓縮,原尺寸15.5M被壓縮到6.0M,壓縮比38%,不過由于該算法的壓縮比率和具體圖檔品質、幀數、圖像特征有關,測試資料僅供參考。
本文到這裡就結束了,原來看似簡單的表情包,也有不少文章可做。
謝謝觀看,希望文中介紹的知識和研究方法對你有所啟發。
本文轉自黃聰部落格園部落格,原文連結:http://www.cnblogs.com/huangcong/p/7221245.html,如需轉載請自行聯系原作者