GIF(Graphics Interchange Format)是CompuServe公司在1987年开发的图像文件格式,原义是图像互换格式。GIF是一种基于LZW算法的连续色调的无损压缩格式,其压缩率一般在50%左右,它不属于任何应用程序。
GIF格式自1987年由CompuServe公司引入后,因其体积小、成像相对清晰,特别适合于初期慢速的互联网,而大受欢迎。其支持透明背景图像,适用于多种操作系统,文件体积很小,目前网络上很多小动画都是GIF格式的。
GIF格式可以将多幅图像保存到一个图像文件,展示的时候将多幅图像数据逐幅读出并显示到屏幕上,从而形成了GIF动画,所以根本上GIF仍然是一种图片文件格式。
GIF只能显示256色,主要分为两个版本,即GIF 89a和GIF 87a。GIF 87a是在1987年制定的版本;GIF 89a是1989年制定的版本,在此版本中,为GIF文档扩充了图形控制区块、备注、说明、应用程序编程接口等四个区块,并提供了对透明色和多帧动画的支持。
LZW算法又叫“串表压缩算法”,全称Lempel-Ziv-Welch Encoding,是通过建立一个字符串表,用较短的代码来表示较长的字符串来实现压缩。字符串和编码的对应关系是在压缩过程中动态生成的,并且隐含在压缩数据中,解压的时候根据表来进行恢复,算是一种无损压缩。
LZW压缩算法的基本原理:提取原始文本文件数据中的不同字符,基于这些字符创建一个编译表,然后用编译表中的字符的索引来替代原始文本文件数据中的相应字符,减少原始数据大小。和调色板图像的实现原理差不多,不过这里的编译表不是事先创建好的,而是根据原始文件数据动态创建的,解码时还要从已编码的数据中还原出原来的编译表。
GIF图像编码是基于颜色列表的,最多只支持8位,也就是256种颜色;颜色表里存储的数据是每个颜色点的颜色值,以及对应于颜色列表的索引值。
GIF文件内部分成许多存储块,用来存储多幅图像或者是决定图像表现行为的控制块,用以实现动画和交互式应用。
GIF文件内部是按块划分的,包括控制块( Control Block )和数据块(Data Sub-blocks)两种。
控制块是控制数据块行为的,根据不同的控制块包含一些不同的控制参数;数据块只包含一些8-bit的字符流,由它前面的控制块来决定它的功能,每个数据块大小从0到255个字节,数据块的第一个字节指出这个数据块大小(字节数),计算数据块的大小时不包括这个字节,所以一个空的数据块有一个字节,那就是数据块的大小0x00。
一个GIF文件的结构可分为文件头(File Header)、GIF数据流(GIF Data Stream)和文件终结器(Trailer)三个部分。文件头包含GIF文件署名(Signature)和版本号(Version);GIF数据流由控制标识符、图像块(Image Block)和其他的一些扩展块组成;文件终结器只有一个值为0x3B的字符(“;”)表示文件结束。
下表显示了一个GIF文件的组成结构:
下面分别对各个模块进行介绍。
GIF署名(Signature)用来确认一个文件是否是GIF格式的文件,这一部分由三个字符组成:”GIF”。
文件版本号(Version) 也是由三个字节组成,可以为”87a”或”89a”。
这一部分由7个字节组成,定义了GIF图像的大小(Logical Screen Width & Height)、颜色深度(Color Bits)、背景色(Background Color Index)以及有无全局颜色列表(Global Color Table)和颜色列表的索引数(Index Count),具体描述见下表:
全局颜色列表必须紧跟在逻辑屏幕标识符后面,每个颜色列表索引条目由三个字节组成,按R、G、B的顺序排列。
一个GIF文件内可以包含多幅图像,一幅图像结束之后紧接着下是一幅图像的标识符,图像标识符以0x2C(‘,’)字符开始,定义紧接着它的图像的性质,包括图像相对于逻辑屏幕边界的偏移量、图像大小以及有无局部颜色列表和颜色列表大小,由10个字节组成:
如果上面的局部颜色表标志设置了的话,则需要在这里(紧跟在图像标识符之后)定义一个局部颜色列表以供紧接着它的图像使用。如果GIF文件没有全局颜色表,也没有局部颜色表,可以自己创建一个颜色表,或使用系统的颜色列表。局部颜色表的排列方式和全局颜色表一样。
两部分组成:LZW编码长度(LZW Minimum Code Size)和图像数据(Image Data)。GIF图像数据使用了LZW压缩算法,大大减小了图像数据的大小。图像数据在压缩前有两种排列格式:连续的和交织的(由图像标识符的交织标志控制)。连续方式按从左到右、从上到下的顺序排列图像的光栅数据;交织图像按下面的方法处理光栅数据:
创建四个通道(pass)保存数据,每个通道提取不同行的数据:
此为可选的(需要89a版本),可以放在一个图像块(图像标识符)或文本扩展块的前面,用来控制跟在它后面的第一个图像(或文本)的渲染(Render)形式,组成结构如下:
用户输入标志(Use Input Flag):指出是否期待用户有输入之后才继续进行下去,置位表示期待,设置否表示不期待。用户输入可以是按回车键、鼠标点击等,可以和延迟时间一起使用,在设置的延迟时间内用户有输入则马上继续进行,或者没有输入直到延迟时间到达而继续。
透明颜色标志(Transparent Color Flag):置位表示使用透明颜色。
此是可选的(需要89a版本),可以用来记录图形、版权、描述等任何的非图形和控制的纯文本数据(7-bit ASCII字符),注释扩展并不影响对图像数据流的处理,解码器完全可以忽略它。存放位置可以是数据流的任何地方,最好不要妨碍控制和数据块,推荐放在数据流的开始或结尾。具体组成:
也是可选的(需要89a版本),用来绘制一个简单的文本图像,这一部分由用来绘制的纯文本数据(7-bit ASCII字符)和控制绘制的参数等组成。绘制文本借助于一个文本框(Text Grid)来定义边界,在文本框中划分多个单元格,每个字符占用一个单元,绘制时按从左到右、从上到下的顺序依次进行,直到最后一个字符或者占满整个文本框(之后的字符将被忽略,因此定义文本框的大小时应该注意到是否可以容纳整个文本),绘制文本的颜色索引使用全局颜色列表,没有则可以使用一个已经保存的前一个颜色列表。另外,图形文本扩展块也属于图形块(Graphic Rendering Block),可以在它前面定义图形控制扩展对它的表现形式进一步修改。图形文本扩展的组成:
这是提供给应用程序自己使用的(需要89a版本),应用程序可以在这里定义自己的标识、信息等,组成如下:
这一部分只有一个值的字节,标识GIF文件结束,固定值0x3B。
GIF编码里,由于GIF限制了颜色表最多只能有256个颜色值,这样就很容易出现量化后GIF里图片数据丢失颜色的情况。所以如果颜色表生成的不合理,导致丢失太多重要颜色值,使得恢复后展示的图片失真太大;另外颜色表的生成速度及匹配速度也严重影响到GIF的生成速度;如果只有全局颜色表,那么必然不能很好地使得GIF中每一副图片都能有较少的失真度,而如果每一副图片都生成一个颜色表,那GIF文件就会更大。
QQ音乐安卓版最初的GIF生成库就是使用的中位切割量化算法来生成颜色表,使用中发现速度上比较慢,特别是大帧率下的GIF生成。为了保证GIF的颜色质量,必须是每一副图片都生成一个颜色表,至于文件大小可以通过降低帧率及调整每副图片的尺寸来控制;因此对这里进行性能上的提升,就需要研究其他量化算法。下面开始分别介绍常用量化算法以及最后的优化方案:八叉树量化算法。
计算机图形学中,常采用的一种方法是把颜色看成是基于红、绿、蓝三种颜色的混合,也可以采用色度、彩度、亮度等描述颜色,用多种不同的描述符来表示颜色,就称为颜色模型(Color Model),如果有人能量化这三种不同的描述符的数值,就可以用一个三元组来表示一种颜色,例如(R, G, B),这就形成了一个描述颜色的三维坐标系统,选择不同的颜色模型能形成不同坐标系统,坐标系统上所有颜色的集合就称为颜色空间(Color Spaces)。 在图形学中,颜色量化是为了减少一张图像中的颜色数并且使用它尽可能的与原始图像一样,在一些由于内存限制只能显示有限颜色的设备上,颜色量化就显得特别的重要。
a) 统一量化方法
直接对RGB去掉N位,这种方式简单粗暴,很明显图片的失真比较严重。
b) 流行色量化法
选取出现概率最高的颜色组成颜色表。此方法依然存在失真较大的问题,特别是流行色都是比较接近的颜色,这就导致其他颜色丢失,恢复后的图片整体偏向某一颜色;比如流行色都偏红,那么最后恢复后的图片也是偏红。
c) 中位切割量化算法
包围当前图像所有颜色的最小长方体,递归地按照长边分割,每次分割保证两边权重基本对等,直到获得256个颜色值或者全部小方块不可分为止;最终每个小方块中的颜色均值作为本块颜色值,全部提取出来就得到颜色表。这也是最初QQ音乐安卓版本GIF库所采用的方案,此方案相比流行色量化能获得更好的效果,对于方块内的取均值来达到合并相似颜色值的效果。
该算法最早见于文章最早是在1988, M. Gervautz 和 W. Purgathofer 发表的论文《A Simple Method for Color Quantization: Octree Quantization》,算法的最大优点是效率高,占用内存少(仅需要不超过(颜色数量+1)个节点,加上一些中间节点所占用的内存),选出的调色板最合理,显示效果最好。
a) 八叉树
八叉树是一种用于描述三维空间的树状数据结构。八叉树的每个节点表示一个正方体的体积元素,每个节点有零个或者八个子节点,将子节点所表示的体积元素加在一起就等于父节点的体积。
八叉树(Octree)的定义是:若不为空树的话,树中任一节点的子节点恰好只会有八个,或零个,也就是子节点不会有0与8以外的数目。八叉树可以用来把一个立方体切成8个相同等分的小立方体,比如在房间里某个角落藏着一枚金币,如果想很快的把金币找出来,就可以利用八叉树的原理,把房间当成一个立方体,先切成八个小立方体,然后排除掉没有放任何东西的小立方体,再把有可能藏金币的小立方体继续切八等份,如此重复进行,平均在Log8(房间内的所有物品数)的时间内就可找到金币。因此,八叉树可以用在3D空间中的场景管理,可以很快地定位在3D场景中物体的具体位置,或侦测与其它物体是否有碰撞以及是否在可视范围内。
b) 八叉树实现原理
当插入颜色值(109, 204, 170)的时候,插入第0层,取R、G、B颜色值的最高位,通过或操作,得到一个小于8的值3(011),作为下一次递归的位置;进入第2层,继续进行或操作,得到值6(110),第6个孩子节点就是下一层要递归的位置。依次类推,直到找到叶子节点,把颜色计算值加1,同时把(109,204,170)加到当前节点的RGB值总和中。将所有的RGB值逐层插入到八叉树中,在每个节点上,记录所有经过的节点的RGB值的总和,以及RGB颜色个数。
下图简化了过程,只演示了几层,在实现的算法中每种颜色分量占8位,所有共有9层。
插入的过程中,如果节点不存在,则需要创建新的节点,然后增加节点计数以及RGB各分量的总和。当在插入时,发现节点已经存在,且是叶子节点,则停止该颜色后续层数节点的插入。
插入完一个颜色之后,如果叶子节点数超过了我们要得到的颜色数(256色需要得到256种颜色),这时候就需要合并一些叶子节点了,使的叶子节点的个数不超过我们要得到的颜色数。
由于越底层的节点,数据的敏感度越低,所以,我们将从最底层的节点开始合并。按节点计数值小的优先合并策略,将其子节点的所有RGB分量以及节点计数全部记录到该节点中,并删除其所有子节点。依此进行,直到合并后的叶子数符合要求为止。
合并的基本思想,在第n层的节点上有两个叶节点,现在完成的合并操作就是把叶节点的颜色分量和计数值都加到它们的父节点上,同时裁剪掉两个叶节点,这步操作就减少了一个叶节点。
程序实现步骤如下:
c) 提取调色板
按照上述的步骤将所有的颜色全部插入之后,便建立起一颗叶子节点不超过256的八叉树。此时,取出叶子节点中的RGB分量的平均值(分量总和除以节点计数),即是得到的调色板颜色值。
d) 匹配调色板索引
所谓匹配调色板索引,就是根据原始的RGB值,在调色板中查找出最接近的颜色的索引。最容易想到的方案是:
对每个RGB颜色,分别对调色板数据求各分量的差值的平方和,求的最小值对应的调色板颜色的索引,即是该RGB颜色匹配到的调色板索引。
很明显此方案需要进行大量的计算和比较才能找到对应颜色值的颜色表索引,根据八叉树的构建特性,我们想到了更加快速的搜索方案,原理类似于插入颜色构建颜色表的过程:
e) 优化数据对比
下面针对八叉树和中位切割量化算法两者的生成效率对比数据,中位切割采用最大分割深度为8,这就和八叉树的层级保持一致了,保证对比条件的一致性。经过多次生成GIF进行对比,求取每一个图片编码到GIF里的耗时平均值进行,结果如下表。可以看到通过八叉树可以极大提升性能,而中位切割算法每次分割都要先找到最大边,还要保证两边权重近似一样,计算量要大很多。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。