本文來自於騰訊Bugly公衆號(weixinBugly),未經做者贊成,請勿轉載,原文地址:https://mp.weixin.qq.com/s/v0pffOhjFWnVbU2lXjuEmwhtml
GIF(Graphics Interchange Format)原義是「圖像互換格式」,是CompuServe公司在1987年開發出的圖像文件格式,能夠說是互聯網界的老古董了。java
GIF格式能夠存儲多幅彩色圖像,若是將這些圖像連續播放出來,就可以組成最簡單的動畫。因此常被用來存儲「動態圖片」,一般時間短,體積小,內容簡單,成像相對清晰,適於在早起的慢速互聯網上傳播。node
原本,隨着網絡帶寬的拓展和視頻技術的進步,這種圖像已經漸漸失去了市場。但是,近年來流行的表情包文化,讓老古董GIF圖有了新的用武之地。算法
表情包一般來源於手繪圖像,或是視頻截取,目前有不少方便製做表情包的小工具。編程
這類圖片一般具備文件體積小,內容簡單,兼容性好(無需解碼工具便可在各種平臺上查看),對畫質要求不高的特色,恰好符合GIF圖的特性。api
因此,老古董GIF圖有了新的應用場景。服務器
新的應用場景帶來新的需求,在本文所面臨的場景中,須要批量爲用戶推送GIF表情包,但願在運營人員上傳圖包的時候,服務器能夠自動完成縮略圖的批量生成工做。微信
一批圖像大約有200-500張,以縮略圖列表的形式展現在客戶端。網絡
根據咱們使用測試數據進行的統計GIF圖表情包的尺寸大部分在200k-500k之間,批量推送的一個重要問題就是數據量太大,所以,咱們但願可以在列表裏展現體積較小的縮略圖,用戶點擊後,再單獨拉取原圖。ide
傳統的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來看看它的信息。
gifsicle -I snowball.gif > snowball.txt
咱們獲得下面的文本
5.gif 19 images logical screen 200x196 global color table [128] background 93 loop forever extensions 1 + image #0 200x196 transparent 93 disposal asis delay 0.04s + image #1 200x188 transparent 93 disposal asis delay 0.04s ........
能夠看到,global color table [128]就是它的調色盤,長度128。
爲了確認,咱們再用二進制查看器查看一下它的文件頭
能夠看到Packet裏的字段的確符合咱們的描述。
在實際狀況中,GIF圖具備下面的特徵
(1)一張圖像最多隻會包含256個RGB值。
(2)在一張連續動態GIF裏,每一幀之間信息差別不大,顏色是被大量重複使用的。
在存儲時,咱們用一個公共的索引表,把圖片中用到的顏色提取出來,組成一個調色盤,這樣,在存儲真正的圖片點陣時,只須要存儲每一個點在調色盤裏的索引值。
若是調色盤放在文件頭,做爲全部幀公用的信息,就是公共(全局)調色盤,若是放在每一幀的幀信息中,就是局部調色盤。GIF格式容許兩種調色盤同時存在,在沒有局部調色盤的狀況下,使用公共調色盤來渲染。
這樣,咱們能夠用調色盤裏的索引來表明實際的顏色值。
一個256色的調色盤,24bit的顏色只須要用9bit就能夠表達了。
調色盤還能夠進一步減小,128色,64色,etc,相應的壓縮率就會愈來愈大……
仍是以兔子爲例,咱們還能夠嘗試指定它的調色盤大小,對它進行重壓縮
gifsicle --colors=64 5.gif > 5-64.gif gifsicle --colors=32 5.gif > 5-32.gif gifsicle --colors=16 5.gif > 5-16.gif gifsicle --colors=2 5.gif > 5-2.gif ......
依然使用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
gm convert source.gif target_%d.gif
能夠看出,除了第一幀以外,後面的幀都作了不一樣程度的處理,文件體積也比第一幀小。
這樣的壓縮處理也是無損的,帶來的壓縮比和原始圖像的具體狀況有關,重複區域越多,壓縮效果越好,但相應地,也須要存儲一些額外的信息,來告訴引擎如何渲染,具體包括
其中值得額外說明的是Disposal Method,它定義的是幀之間的疊加關係,給定一個幀序列,咱們用怎樣的方式把它們渲染成起來。
詳細參數定義,能夠參考該網站的範例
http://www.theimage.com/animation/pages/disposal.html
Disposal Method和透明顏色一塊兒,定義了幀之間的疊加關係。在實際使用中,咱們一般把第一幀當作基幀(background),其他幀向前一幀對齊的方式來渲染,這裏再也不贅述。
理解了上面的內容,咱們再來看幀信息的具體定義,主要包括
1和3比較直觀,第二部分和第四部分則是一系列的標誌位,定義了對於「幀」須要說明的內容。
幀數聽說明。
除了上面說過的字段以外,還多了一個Interlace Flag,表示幀點陣的存儲方式,有兩種,順序和隔行交錯,爲 1 時表示圖像數據是以隔行方式存放的。最初 GIF 標準設置此標誌的目的是考慮到通訊設備間傳輸速度不理想狀況下,用這種方式存放和顯示圖像,就能夠在圖像顯示完成以前看到這幅圖像的概貌,慢慢的變清晰,而不以爲顯示時間過長。
幀數據擴展是89a標準增長的,主要包括四個部分。
一、程序擴展結構(Application Extension)主要定義了生成該gif的程序相關信息
二、註釋擴展結構(Comment Extension)通常用來儲存圖片做者的簽名信息
三、圖形控制擴展結構(Graphic Control Extension)這部分對圖片的渲染比較重要
除了前面說過的Dispose Method、Delay、Background Color以外,User Input用來定義是否接受用戶輸入後再播放下一幀,須要圖像解碼器對應api的配合,能夠用來實現一些特殊的交互效果。
四、平滑文本擴展結構(Plain Text Control Extension)
89a標準容許咱們將圖片上的文字信息額外儲存在擴展區域裏,但實際渲染時依賴解碼器的字體環境,因此實際狀況中不多使用。
以上擴展塊都是可選的,只有Label置位的狀況下,解碼器纔會去渲染
說完了基本原理,來分析一下咱們的實際問題。
給大量表情包生成縮略圖,在不損耗原畫質的前提下,儘量減小圖片體積,節省用戶流量。
以前說過,單純依靠resize大法不能知足咱們的要求,沒辦法,只能損耗畫質了,主要有兩個思路,減小顏色和減小幀數。
減小顏色——圖片狀況各異,標準難以控制,並且會形成縮略圖和原圖視覺差別比較明顯
減小幀數——經過提取一些間隔幀,好比對於一張10幀的動畫,提取其中的提取1,3,5,7,9幀。來減小圖片的總體體積,彷佛更可行。
先看一個成果,就拿文章開頭的圖作栗子吧
看上去連貫性不如之前,可是差異不大,做爲縮略圖的視覺效果能夠接受,因爲幀數減少,體積也能夠獲得明顯的優化。體積從428K縮到了140K
可是,在開發初期,咱們嘗試暴力間隔提取幀,把幀從新鏈接壓成新的GIF圖,這時,會獲得這樣的圖片。
主要有兩個問題。
一、幀數過快
二、能看到明顯的殘留噪點
分析咱們上面的原理,不難找到緣由,正是由於大部分GIF存儲時採用了公共區域排除和透明區域疊加的優化,若是咱們直接間隔抽幀,再拼起來,就破壞了原來的疊加規則,不應露出來的幀露出來了,因此纔會產生噪點。
因此,咱們首先要把原始信息恢復出來。
兩個命令行工具,gifsicle和ImageMagick都提供這樣的命令。
gm convert -coalesce source.gif target_%d.gif gifsicle --unoptimize source.gif > target.gif
還原以後抽幀,重建新的GIF,就能夠解決問題2了。
注意重建的時候,能夠應用工具再進行對透明度和公共區域的優化壓縮。
至於問題1,也是由於咱們沒有對幀延遲參數Delay Time作處理,直接取原幀的參數,幀數減小了,速度必定會加快。
因此,咱們須要把抽去的連續幀的總延時加起來,做爲新的延遲數據,這樣能夠保持縮略圖和原圖頻率一致,看起來不會太過鬼畜,也不會太過遲緩。
提取出每一幀的delay信息,也能夠經過工具提供的命令來提取。
gm identify -verbose source.gif gifsicle -I source.gif
在實際應用中,抽幀的間隔gap是根據總幀數frame求出的
frame<8 gap=1 9<frame<20 gap=2 21<frame<30 gap=3 31<frame<40 gap=4 frame>40 gap=5
delay值的計算還作了歸一化處理,若是新生成縮略圖的幀間隔平均值大於200ms,則統一加速到均值200ms,同時保持原有節奏,這樣能夠避免極端狀況下,縮略圖過於遲緩。
本文介紹的算法主要應用於手Q熱圖功能的後臺管理系統,使用Nodejs編寫。
ImageMagick是一個較爲經常使用的圖像處理工具,除了gif還能夠處理各種圖像文件,有node封裝的版本可使用。
gifsicle只有可執行版本,在服務器上從新編譯源碼後,採用spawn調起子進程的方式實現。
ImageMagick對於圖片信息的解析較爲方便,能夠直接獲得結構化信息。
gifsicle支持命令管道級聯,處理圖片速度較快。
實際生產過程當中,同時採用了兩個工具。
const {spawn} = require('child_process'); const image = gm("src2/"+file) image.identify((err, val) => { if(!val.Scene){ console.log(file+" has err:"+err) return } let frames_count = val.Scene[0].replace(/\d* of /, '') * 1 let gap = countGap(frames_count) let delayList = []; let totaldelay = 0 if(val.Delay!=undefined){ let i for (i = 0; i < val.Delay.length; i ++) { delayList[i] = val.Delay[i].replace(/x\d*/, '') * 1 totaldelay+=delayList[i] } for (; i < val.Scene.length; i ++) { delayList[i] = 8 totaldelay+=delayList[i] } }else{ for (let i = 0; i < val.Scene.length; i ++) { delayList[i] = 8 totaldelay+=delayList[i] } } let totalFrame = parseInt(frames_count/gap) //判斷是否速度過慢,須要進行歸一加速處理 if(totaldelay/totalFrame>20){ let scale =(totalFrame*1.0*20)/totaldelay for (let i = 0; i < delayList.length; i ++) { delayList[i] = parseInt(delayList[i] * scale) } } let params=[] params.push("--colors=255") params.push("--unoptimize") params.push("src2/"+file) let tempdelay = delayList[0] for (let i = 1; i < frames_count; i ++) { if(i%gap==0){ params.push("-d"+tempdelay) params.push("#"+(i-gap)) tempdelay=0 } tempdelay += delayList[i] } params.push("--optimize=3") params.push("-o") params.push("src2/"+file+"gap-keepdelay.gif") spawn("gifsicle", params, { stdio: 'inherit' }) })
測試時,採用該算法隨機選擇50張gif圖進行壓縮,原尺寸15.5M被壓縮到6.0M,壓縮比38%,不過因爲該算法的壓縮比率和具體圖片質量、幀數、圖像特徵有關,測試數據僅供參考。
本文到這裏就結束了,原來看似簡單的表情包,也有很多文章可作。
謝謝觀看,但願文中介紹的知識和研究方法對你有所啓發。
更多精彩內容歡迎關注騰訊 Bugly的微信公衆帳號:
騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!