本文来自腾讯前端开发工程师“ wendygogogo”的技术分享,做者自评:“在Web前端摸爬滚打的码农一枚,对技术充满热情的菜鸟,致力为手Q的建设添砖加瓦。”php
GIF ( Graphics Interchange Format )原义是“图像互换格式”,是 CompuServe 公司在1987年开发出的图像文件格式,能够说是互联网界的老古董了。html
GIF 格式能够存储多幅彩色图像,若是将这些图像((https://www.qcloud.com/document/ ... w.59167.59167.59167)连续播放出来,就可以组成最简单的动画。因此常被用来存储“动态图片”,一般时间短,体积小,内容简单,成像相对清晰,适于在早起的慢速互联网上传播。前端
原本,随着网络带宽的拓展和视频技术的进步,这种图像已经渐渐失去了市场。但是,近年来流行的表情包文化,让老古董 GIF 图有了新的用武之地。node
表情包一般来源于手绘图像,或是视频截取,目前有不少方便制做表情包的小工具。程序员
这类图片一般具备文件体积小,内容简单,兼容性好(无需解码工具便可在各种平台上查看),对画质要求不高的特色,恰好符合 GIF 图的特性。算法
因此,老古董 GIF 图有了新的应用场景。数据库
学习交流:编程
- 即时通信开发交流3群:185926912[推荐]小程序
- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》微信小程序
(本文同步发布于:http://www.52im.net/thread-2032-1-1.html)
《QQ音乐团队分享:Android中的图片压缩技术详解(上篇)》
《QQ音乐团队分享:Android中的图片压缩技术详解(下篇)》
《腾讯原创分享(一):如何大幅提高移动网络下手机QQ的图片传输速度和成功率》
《腾讯技术分享:腾讯是如何大幅下降带宽和网络流量的(图片压缩篇)》
《基于社交网络的Yelp是如何实现海量用户图片的无损压缩的?》
新的应用场景带来新的需求,本文所要探究的技术和要解决的问题来源于某个真实的业务场景下——即为用户批量推送GIF表情包的功能需求。
一批图像大约有200-500张,以缩略图列表的形式展现在客户端。
根据咱们使用测试数据进行的统计 GIF 图表情包的尺寸大部分在200k-500k之间,批量推送的一个重要问题就是数据量太大,所以,咱们但愿可以在列表里展现体积较小的缩略图,用户点击后,再单独拉取原图。
传统的 GIF 缩略图是静态的,一般是提取第一帧,但在表情包的情形下,这种方式不足以表达出图片中信息。
好比下面的例子:
(左为原始GIF动态图,右为GIF的第一帧)
第一帧彻底看不出重点啊!
因此,咱们但愿缩略图也是动态的,并尽量和原图类似。
对于传统图片来讲,文件大小通常和图片分辨率(尺寸)正相关,因此,生成缩略图最直观的思路就是缩小尺寸,resize大法。
可是在 GIF 图的场合,这个方式再也不高效,由于 GIF 图的文件大小还受到一个重要的因素制约——帧数
以这张柴犬表情为例,原图宽度200,尺寸1.44M,等比缩放到150以后,尺寸仍是1.37M,等比缩放到100,至关于尺寸变为原来的四分之一,体积仍是749K。
可见,resize大法的压缩率并不理想,收效甚微。
并且,咱们所获得的大部分表情图素材,分辨率已经很小了,为了保证客户端展现效果,不可以过分减小尺寸,否则图片会变得模糊。
因此,想要对GIF图进行压缩,只能从别的方向入手。
想要压缩一个文件,首先要了解它是如何存储的。毕竟,编程的事,万变不离其宗嘛。
做为一种古老的格式,GIF的存储规则也相对简单,容易理解。
一个GIF文件主要由如下几部分组成:
1)文件头;
2)图像帧信息;
3)注释。
下面咱们来分别探究每一个部分。
GIF格式文件头和通常文件头差异不大,也包含有:
1)格式声明;
2)逻辑屏幕描述块;
3)全局调色盘;
格式声明:
Signature 为“GIF”3 个字符;Version 为“87a”或“89a”3 个字符。
逻辑屏幕描述块:
前两字节为像素单位的宽、高,用以标识图片的视觉尺寸。
Packet里是调色盘信息,分别来看:
1)Global Color Table Flag为全局颜色表标志,即为1时代表全局颜色表有定义;
2)Color Resolution 表明颜色表中每种基色位长(须要+1),为111时,每一个颜色用8bit表示,即咱们熟悉的RGB表示法,一个颜色三字节;
3)Sort Flag 表示是否对颜色表里的颜色进行优先度排序,把经常使用的排在前面,这个主要是为了适应一些颜色解析度低的早期渲染器,如今已经不多使用了;
4)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——定义该帧对于上一帧的叠加方式;
Delay Time——定义该帧播放时的停留时间。
其中值得额外说明的是Disposal Method,它定义的是帧之间的叠加关系,给定一个帧序列,咱们用怎样的方式把它们渲染成起来。
详细参数定义,能够参考该网站的范例:http://www.theimage.com/animation/pages/disposal.html
Disposal Method和透明颜色一块儿,定义了帧之间的叠加关系。在实际使用中,咱们一般把第一帧当作基帧(background),其他帧向前一帧对齐的方式来渲染,这里再也不赘述。
理解了上面的内容,咱们再来看帧信息的具体定义,主要包括:
1)帧分隔符;
2)帧数听说明;
3)点阵数据(它存储的不是颜色值,而是颜色索引);
4)帧数据扩展(只有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大法不能知足咱们的要求,没办法,只能损耗画质了。
主要有两个思路:减小颜色和减小帧数:
1)减小颜色——图片状况各异,标准难以控制,并且会形成缩略图和原图视觉差别比较明显。
2)减小帧数——经过提取一些间隔帧,好比对于一张10帧的动画,提取其中的提取1,3,5,7,9帧。来减小图片的总体体积,彷佛更可行。
先看一个成果,就拿文章开头的图作栗子吧:
看上去连贯性不如之前,可是差异不大,做为缩略图的视觉效果能够接受,因为帧数减少,体积也能够获得明显的优化。体积从428K缩到了140K。
可是,在开发初期,咱们尝试暴力间隔提取帧,把帧从新链接压成新的GIF图,这时,会获得这样的图片:
主要有两个问题:
1)帧数过快;
2)能看到明显的残留噪点。
分析咱们上面的原理,不难找到缘由,正是由于大部分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
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 iii
for(iii = 0; iii < val.Delay.length; iii ++) {
delayList[iii] = val.Delay[iii].replace(/x\d*/, '') * 1
totaldelay+=delayList[iii]
}
for(; iii < val.Scene.length; iii ++) {
delayList[iii] = 8
totaldelay+=delayList[iii]
}
}else{
for(let iii = 0; iii < val.Scene.length; iii ++) {
delayList[iii] = 8
totaldelay+=delayList[iii]
}
}
let totalFrame = parseInt(frames_count/gap)
//判断是否速度过慢,须要进行归一加速处理
if(totaldelay/totalFrame>20){
let scale =(totalFrame*1.0*20)/totaldelay
for(let iii = 0; iii < delayList.length; iii ++) {
delayList[iii] = parseInt(delayList[iii] * scale)
}
}
let params=[]
params.push("--colors=255")
params.push("--unoptimize")
params.push("src2/"+file)
let tempdelay = delayList[0]
for(let iii = 1; iii < frames_count; iii ++) {
if(i%gap==0){
params.push("-d"+tempdelay)
params.push("#"+(iii-gap))
tempdelay=0
}
tempdelay += delayList[iii]
}
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%,不过因为该算法的压缩比率和具体图片质量、帧数、图像特征有关,测试数据仅供参考。
本文到这里就结束了,原来看似简单的表情包,也有很多文章可作。
谢谢观看,但愿文中介绍的知识和研究方法对你有所启发。
[1] QQ、微信团队原创技术文章:
《腾讯技术分享:腾讯是如何大幅下降带宽和网络流量的(图片压缩篇)》
《腾讯技术分享:腾讯是如何大幅下降带宽和网络流量的(音视频技术篇)》
《腾讯技术分享:Android版手机QQ的缓存监控与优化实践》
《微信团队分享:iOS版微信的高性能通用key-value组件技术实践》
《微信团队分享:iOS版微信是如何防止特殊字符致使的炸群、APP崩溃的?》
《腾讯技术分享:Android手Q的线程死锁监控系统技术实践》
《QQ音乐团队分享:Android中的图片压缩技术详解(上篇)》
《QQ音乐团队分享:Android中的图片压缩技术详解(下篇)》
《腾讯团队分享 :一次手Q聊天界面中图片显示bug的追踪过程分享》
《微信团队分享:微信Android版小视频编码填过的那些坑》
《微信团队披露:微信界面卡死超级bug“15。。。。”的前因后果》
《月活8.89亿的超级IM微信是如何进行Android端兼容测试的》
《微信客户端团队负责人技术访谈:如何着手客户端性能监控和优化》
《微信团队原创分享:Android版微信的臃肿之困与模块化实践之路》
《微信团队原创分享:微信客户端SQLite数据库损坏修复实践》
《腾讯原创分享(一):如何大幅提高移动网络下手机QQ的图片传输速度和成功率》
《腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(下篇)》
《腾讯原创分享(三):如何大幅压缩移动网络下APP的流量消耗(上篇)》
《如约而至:微信自用的移动端IM网络层跨平台组件库Mars已正式开源》
《开源libco库:单机千万链接、支撑微信8亿用户的后台框架基石 [源码下载]》
《微信新一代通讯安全解决方案:基于TLS1.3的MMTLS详解》
《微信团队原创分享:Android版微信后台保活实战分享(进程保活篇)》
《微信团队原创分享:Android版微信后台保活实战分享(网络保活篇)》
《Android版微信从300KB到30MB的技术演进(PPT讲稿) [附件下载]》
《微信团队原创分享:Android版微信从300KB到30MB的技术演进》
《微信技术总监谈架构:微信之道——大道至简(PPT讲稿) [附件下载]》
《微信海量用户背后的后台系统存储架构(视频+PPT) [附件下载]》
《微信异步化改造实践:8亿月活、单机千万链接背后的后台解决方案》
《架构之道:3个程序员成就微信朋友圈日均10亿发布量[有视频]》
《微信团队原创分享:Android内存泄漏监控和优化技巧总结》
《微信团队原创Android资源混淆工具:AndResGuard [有源码]》
《移动端IM实践:Android版微信如何大幅提高交互性能(一)》
《移动端IM实践:Android版微信如何大幅提高交互性能(二)》
《移动端IM实践:WhatsApp、Line、微信的心跳策略分析》
《移动端IM实践:谷歌消息推送服务(GCM)研究(来自微信)》
《信鸽团队原创:一块儿走过 iOS10 上消息推送(APNS)的坑》
《腾讯TEG团队原创:基于MySQL的分布式数据库TDSQL十年锻造经验分享》
《微信多媒体团队访谈:音视频开发的学习、微信的音视频技术和挑战等》
《了解iOS消息推送一文就够:史上最全iOS Push技术详解》
《腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面》
《腾讯音视频实验室:使用AI黑科技实现超低码率的高清实时视频聊天》
《腾讯技术分享:微信小程序音视频与WebRTC互通的技术思路和实践》
《手把手教你读取Android版微信和手Q的聊天记录(仅做技术研究学习)》
《微信技术分享:微信的海量IM聊天消息序列号生成实践(算法原理篇)》
《微信技术分享:微信的海量IM聊天消息序列号生成实践(容灾方案篇)》
《腾讯技术分享:GIF动图技术详解及手机QQ动态表情压缩技术实践》
>> 更多同类文章 ……
[2] 有关QQ、微信的技术故事:
《技术往事:微信估值已超5千亿,雷军曾有机会收编张小龙及其Foxmail》
《2017微信数据报告:日活跃用户达9亿、日发消息380亿条》
《技术往事:创业初期的腾讯——16年前的冬天,谁动了马化腾的代码》
《技术往事:史上最全QQ图标变迁过程,追寻IM巨人的演进历史》
《开发往事:深度讲述2010到2015,微信一路风雨的背后》
《开发往事:记录微信3.0版背后的故事(距微信1.0发布9个月时)》
《前创始团队成员分享:盘点微信的前世此生——微信成功的必然和偶然》
《即时通信创业必读:解密微信的产品定位、创新思惟、设计法则等》
《[技术脑洞] 若是把14亿中国人拉到一个微信群里技术上能实现吗?》
>> 更多同类文章 ……
(本文同步发布于:http://www.52im.net/thread-2032-1-1.html)