VC调用giflib(3):GIF文件编、解码
作者:马健
邮箱:stronghorse_mj@hotmail.com
主页:http://www.comicer.com/stronghorse
发布:2020.03.14
一、GIF解码
用giflib对GIF文件进行解码有两个流派:
- 自己循环调用DGifGetRecordType,读到一帧就解码、显示一帧,具体例子可以参见gif2rgb.c中的GIF2RGB函数。该流派实现的时候辛苦一点,但可以边解码、边显示,所以用户反应更快,做得好的话也更省内存。
- 直接调用DGifSlurp,一次解码所有帧,然后再慢慢对各帧进行处理、显示。具体例子可以参见libwebp下的例子代码anim_util.c,其中的ReadAnimatedGIF函数。
libwebp下的这个ReadAnimatedGIF函数,可能是我见过最用心的解动画GIF代码,而且与解动画webp兼容,所以我一见就喜欢。如果非要说有什么瑕疵的话,可能就是google程序员还是太年轻了一点,对社会之复杂、道德之沦丧、人性之卑劣认识不足,还需要在
if (image->canvas_width == 0 || image->canvas_height == 0) {
……
}
这个块后面增加一段
// 对画布尺寸进行修正:某些GIF的SWidth、SHeight小于帧的宽、高 for (int i = 0; i < frame_count; i++) { const GifImageDesc& ImageDesc = gif->SavedImages[i].ImageDesc; if ((ImageDesc.Width + ImageDesc.Left) > gif->SWidth) gif->SWidth = ImageDesc.Width + ImageDesc.Left; if ((ImageDesc.Height + ImageDesc.Top) > gif->SHeight) gif->SHeight = ImageDesc.Height + ImageDesc.Top; }
我真不是在讲笑话——我从网上下载的一些GIF文件是真有这个问题,而且数量不少。碰到这种GIF文件头与帧头数据不一致的情况,CxImage是直接退出解码,老牌的商业版看图软件ACDSee 2022旗舰版则是按照画布尺寸显示一个小黑方块,libwebp如果不做上述修正就直接缓冲区溢出了。
二、GIF编码
与解码一样,用giflib对GIF进行编码也有两个流派:
- 按照帧数打循环,对于每一帧,先EGifPutImageDesc,然后循环各像素行EGifPutLine。很辛苦,但编码后的数据直接进文件,不会在内存里磨磨唧唧。参见gif2rgb.c中的SaveGif函数。
- 先在内存里组帧,然后调用EGifSpew函数一次写到文件里。内存占用略多,但更灵活。网上搜索EGifSpew能找到一些使用的例子,此处从略。
需要注意的是,如果网上找到的代码在插入帧时是这个路数:
GifImageDesc *imageDesc = (GifImageDesc *) malloc(sizeof(GifImageDesc)); …… image->ImageDesc = *imageDesc; GraphicsControlBlock *GCB = (GraphicsControlBlock *) malloc(sizeof(GraphicsControlBlock)); …… EGifGCBToSavedExtension(GCB, GifFile, i);
那么就一定要自己free掉所分配的imageDesc、GCB指针,否则就会产生内存泄漏。其实上面的动态内存分配根本就没道理:需要分配的内存长度是定长,而且没几个字节,giflib还自动对缓冲区内容进行备份,不需要长期保存,所以直接使用局部变量更省事,就像这样:
GifImageDesc imageDesc; …… image->ImageDesc = imageDesc; GraphicsControlBlock GCB; …… EGifGCBToSavedExtension(&GCB, GifFile, i);
另外giflib原版的EGifSpew函数中存在严重的内存泄漏,但giflib的内存泄漏不止这一处,所以在下一篇笔记中专门说这个事情。
但在我看来,giflib对动画GIF文件的编码只是解决了LZW数据压缩的问题,即只能实现最基本的GIF文件编码、写入功能,但以下关键功能是缺失的:
1、颜色量化、抖动
giflib生成的GIF文件最多只能有256色,因此在真彩图像转GIF时,就需要进行颜色量化,即把真彩图像所使用的宽广色域,合理地减少到256色,甚至更少的色。具体可参见百度百科“颜色量化算法”词条:颜色量化算法_百度百科 (baidu.com)
不过这个词条中所列的算法都太老了。giflib的quantize.c就实现了其中的中位切分(median cut)算法,结果连giflib的作者自己都嫌弃,甚至一度被移出giflib的源代码,见文件头的注释。
我个人比较喜欢FreeImage库实现的Xiaolin Wu算法和神经网络算法。在256色时各种算法的差距可能不太明显,但在减色到更少的色彩数,比如16色或16色以下时,不同算法的结果可能差距巨大。
在把宽广色域量化减少到较少的颜色数时,最常造成的一个不良后果是“等高线”现象,即原先色彩丰富、过渡自然的地方,减色后就成了界限明显的色块,看起来就像地图上的等高线。而解决这个问题的手段就是抖动(dither)。
如果经常看网上视频转出来的GIF动图,就可以看出抖动对转换软件的影响——以前的转换软件没有抖动功能,所以转出来的动图有明显的等高线(色块),后来采用有序抖动(ordered bdithering),等高线看不到了,但Bayer矩阵形成的网点却很明显,现在则基本上都采用误差扩散(error diffusion)抖动算法,已经基本上看不出网点,视觉效果大大改善。
对于大多数彩色图像而言,我认为误差扩散抖动采用最经典的Floyd–Steinberg矩阵就够了。需要注意的是,中文网页中有一些采用的是二阶假冒Floyd–Steinberg矩阵,擦亮眼睛别被骗了。另外减色、抖动的时候需要尽量避免图像透明区域的影响,尤其是抖动的时候,如果把透明区域的背景色也抖进来就好看了。
2、计算各帧共享的全局调色板
按照GIF标准规定,动画GIF中的各帧可以共享一个全局调色板,也可以每一帧都有自己的调色板。
当各帧颜色相差不大时,共享全局调色板可以减小最终GIF文件的长度。以256色为例,调色板长度是0.75 KB,在使用全局调色板的情况下,不论图像是10帧还是20帧,调色板占用空间就是0.75 KB。如果各帧都有自己的256色调色板,则10帧占用的调色板空间就是7.5 KB,20帧就是15 KB。
如果各帧之间颜色差距明显,则使用共享调色板就会影响图像效果,即相当于在颜色差异明显的情况下强行统一了颜色。另外如果有些帧的颜色数明显小于其他帧,这些帧也不宜采用全局调色板。例如某动画GIF大多数帧都可以共享256全局色调色板,但开头、结尾的帧可能因为过渡的需要只有背景色或少数几种色,,这时如果这些帧采用自己的调色板,则一方面因为颜色数较少,调色板占用不了多少空间,另外一方面可以有效压缩像素编码位数(2色只需1 bit,4色只需2 bit,而256色需8 bit),从而降低编码后的帧数据长度。
所以好的动画GIF生成软件,既要能计算出各帧共享的全局调色板,又要能具体分析每一帧究竟应该使用公共调色板,还是应该用自己的私有调色板。
我自己是在计算出全局调色板后,在往GIF里插帧的时候再看该帧是否适用全局调色板,不适用就另外计算该帧的调色板。如果全部帧都有自己的调色板,则在存盘前删除全局调色板。所以我只能先在内存里组帧,再调用EGifSpew函数一次性存盘。
3、运动检测
运动检测是指从视频、动画中识别出发生变化或移动的区域,这样在动画GIF中,后一帧只需要存储相对前一帧发生了变化的区域,没变化的区域就不用存储,从而可以有效压缩GIF文件长度。
很明显giflib中没有提供运动检测功能,有兴趣的可以去看libwebp源代码,他家生成动画webp的时候做了运动检测。
我自己懒得去扒libwebp的源代码,所以做了一个最简单的:根据每一帧图像的透明区域对帧图像进行裁剪,只存非透明区域的图像。压缩效果肯定不如运动检测,但比没有强。
4、可变帧率
GIF文件格式允许对每一帧单独指定帧间隔,这样如果动画GIF制作软件比较有理想、有追求,就可以在画面变化激烈时缩小帧间隔,在画面变化不大时延长帧间隔,相当于视频中的可变码率,也能有效压缩最终GIF文件长度。
giflib中仍然没有提供这个功能。我也懒得扒代码,而是继续滑向无耻的深渊:如果静态图像系列转动画GIF,简单点就用固定帧率;如果动画webp直接转GIF,就把webp的帧率直接复制过来用。