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的幀率直接復制過來用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM