VC调用giflib(3):GIF文件编、解码

作者:马健
邮箱:stronghorse_mj@hotmail.com
主页:http://www.comicer.com/stronghorse
发布:2020.03.14

一、GIF解码

用giflib对GIF文件进行解码有两个流派:

  1. 自己循环调用DGifGetRecordType,读到一帧就解码、显示一帧,具体例子可以参见gif2rgb.c中的GIF2RGB函数。该流派实现的时候辛苦一点,但可以边解码、边显示,所以用户反应更快,做得好的话也更省内存。
  2. 直接调用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进行编码也有两个流派:

  1. 按照帧数打循环,对于每一帧,先EGifPutImageDesc,然后循环各像素行EGifPutLine。很辛苦,但编码后的数据直接进文件,不会在内存里磨磨唧唧。参见gif2rgb.c中的SaveGif函数。
  2. 先在内存里组帧,然后调用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的帧率直接复制过来用。

posted @ 2022-03-14 09:43  strnghrs  阅读(950)  评论(1编辑  收藏  举报