使用 FFmpeg 处理高质量 GIF 图片
使用 FFmpeg 处理高质量 GIF 图片
大约2年前,我试着使用FFmpeg来提升GIF图片质量,这就要求在GIF编码器中加入了透明机制。在大多数情况下因为图片源并不可控,所以这些尝试仅仅是避免编码器损失太多信息而已。
最近在Stupeflix,我们需要为 Legend app 提供高质量的 GIF,所以我决定再试试。
在这篇微博中提到的所有特性都可以在 FFmpeg 2.6 中找到,他们也会被应用到Legend app的下一个版本(下一个版本可能在3月26号左右)。
TL;DR:跳转到 Usage 段看怎么使用。
初始提升(2013)
让我们看下2013年引入GIF编码器的透明机制的作用:
% ffmpeg -v warning -ss 45 -t 2 -i big_buck_bunny_1080p_h264.mov -vf scale=300:-1 -gifflags -transdiff -y bbb-notrans.gif % ffmpeg -v warning -ss 45 -t 2 -i big_buck_bunny_1080p_h264.mov -vf scale=300:-1 -gifflags +transdiff -y bbb-trans.gif % ls -l bbb-*.gif -rw-r--r-- 1 ux ux 1.1M Mar 15 22:50 bbb-notrans.gif -rw-r--r-- 1 ux ux 369K Mar 15 22:50 bbb-trans.gif
这个选项默认生效,当你的图片是高度运动的或者色彩变化强烈,你应该关闭它。
另一个实现的压缩机制是剪切,剪切是仅仅重绘GIF图中一个子矩形但不改变其他地方的基本手段。当然在影片中,这么做用处不大,我们后面再谈。
除了上述机制,之前我没有再取得什么进展。也可能是其他措施不太明显,总之,在图片质量上还存在不少缺陷。
256 色的局限性
你可能知道,GIF 是受限于256色调色板。并且默认情况下,FFmpeg 只使用一个通用调色版去尝试覆盖所有的颜色区域,以此来支持含有大量内容的文件:
有序抖动和误差扩散
使用抖动来避免陷入这个问题(256色限制),在上面的邦尼大熊兔GIF中,应用了有序Bayer抖动。通过它的8x8网状图案可以很轻易地辨认出来。尽管它不是最好的方式,但是它同样具有很多优点,例如生动,快速,实际上能够防止带条效应和类似的视觉小毛病。
你将会发现大多数的其它抖动方法都是基于误差的,原理是:一个单色误差(从调色板中挑选的颜色与想要的颜色之间的差异)将会传播到整个画面上。引起帧之间的一种”群集效应“,甚至是帧之间完全相同的源的区域,然而这经常会提供一个更好的质量,因为它完全抹去了GIF的压缩:
% ffmpeg -v warning -ss 45 -t 2 -i big_buck_bunny_1080p_h264.mov -vf scale=300:-1:sws_dither=ed -y bbb-error-diffusal.gif % ls -l bbb-error-diffusal.gif -rw-r--r-- 1 ux ux 1.3M Mar 15 23:10 bbb-error-diffusal.gif
更好的调色板
提高GIF图片质量的第一步就是定义一个更好的调色板。GIF格式存储了一个全局调色板,但你可以对一张图片(或者是子画面;覆盖在前一帧上的后一帧,但它可以覆盖在一个特定的偏移位置上以获得一个更小的尺寸)重新定义一个调色板。每一帧的调色板都可以取代全局调色板来只对一帧起作用。一旦你停止定义一个调色板,它将会回落到全局调色板。这意味着你不能对一系列的帧定义一个调色板,而这恰恰是你想做的。(典型的做法是在每个场景变化的时候定义一个新的调色板)。
所以,换句话说,你需要遵守这样的模式:一个全局调色板,或者,每帧一个调色板。
每帧一个调色板 (未实施)
我最初开始于实施一个每帧调色板的计算,但它具有如下缺点:
-
开销:一个256色调色板是768B大小,而且它不是LZW压缩算法的一部分,所以它不会被压缩。而且它被存储在每一帧里,这就意味着一个25FPS的连续镜头就需要150kbits/秒的开销。然而大部分是可以忽略的。
-
我最初始测试由于调色板的变化产生了一个亮度闪烁效果,这一点也不好。
这就是我之所以没有使用这种方法而是选择计算一个全局调色板来代替的两个原因。现在我回想起来,它可能与重试这种方法有关,因为在某种程度上,现在的色彩量化比我当初测试的时候的状态要好一些。
对于一系列的帧的每一帧都使用相同的调色板(典型的做法是在场景变化时,就像前面提到的那样)也是可能的。或者,更好的做法是:只在子矩形变化时使用。
所有的这些都当作一个练习留给读者吧。欢迎补充,如果你对这个感兴趣的话可以随时和我联系。
一个全局调色板(已实施)
具有一个全局调色板意思是一个2-pass(二次验码)压缩方式(除非你愿意把所有的视频帧都存储在内存里)。
第一遍是对整个图片计算一个调色板,这就是新的palettegen滤波器参与进来的地方。这个滤波器对每一帧的所有颜色制作一个直方图,并且基于这些生成一个调色板。
在技术层面上还存在一些琐事:这个滤波器实现了Paul Heckbert的这篇Color Image Quantization for Frame Buffer Display (1982)论文中的算法的一个变种。这里是我记得的一些不同之处(或者说是关于论文中未定义的行为的特异性):
-
它使用一个全解析度的色彩直方图。而不是论文中作为关键建议使用的下采样 RGB 5:5:5 直方图,这个滤波器对1600万种可能的 RGB 8:8:8 色彩使用了一个哈希表。
-
对方格的分割任然是在中点上进行的,对要分割的方格的选择是根据方格中的颜色方差来进行的(一个带有大的色彩方差的方格将会优先截掉)。
-
对方格中的颜色求平均值取决于颜色的重要性,就我而言,这在论文中并没有定义。
-
当沿着一个维度(红,绿或蓝)进行分割方格时,假如相等,绿色是优先于红色的,然后再是蓝色。
所以不管怎样,这个滤波器都是在做色彩量化,并且生成一个调色板(通常保存在一个PNG文件里)。
它通常看起来像这个样子(upscaled):
颜色映射与抖动
第二遍(验码)是通过paletteuse滤波器完成的,就跟它的名字一样,它将会使用这个调色板来生成最终的量化颜色流,它的任务是在生成的调色板中找出最合适的颜色来表示输入的颜色。这也是你可以选择使用哪种抖动方法的地方。
这里同样有一些技术侧面上的小问题:
-
滤波器实现了五种抖动方法,然而最初的论文中只提出了一种方法。
-
就像palettegen一样,色彩分辨率(将24-bit输入颜色映射到一个调色板条目上)的完成没有破坏输入。它是通过一个K-d Tree(当k = 3时,很明显,每一个维度都是RGB的组成部分)的迭代实现和一个缓存系统来达到这一目标的。
使用这两个滤波器可以让你将GIF编码成这样(单全局调色板,无抖动):
用法
使用相同参量手动运行两遍(验码)是有点讨厌,还要对每一遍的参数进行调整。所以我推荐写一个简单的脚本,如下:
#!/bin/sh palette="/tmp/palette.png" filters="fps=15,scale=320:-1:flags=lanczos" ffmpeg -v warning -i $1 -vf "$filters,palettegen" -y $palette ffmpeg -v warning -i $1 -i $palette -lavfi "$filters [x]; [x][1:v] paletteuse" -y $2
...可以这样使用:
% ./gifenc.sh video.mkv anim.gif
filters变量包括:
-
一个帧率的调整(减小到15会使画面看起来不平稳,但可以使最终的GIF体积更小)
-
一个取代默认(目前是bilinear)定标器的lanczos定标器缩放比例。推荐它的原因是你使用lanczos或bicubic来缩放画面要比bilinear优越的多。如果你不这样做的话,你的输入会模糊的多。
提取:仅作示例
不见得你会编码一部完整的影片,所以你可能会对使用-ss和-t选项来选择一个片段感兴趣。如果你真是这样,那么就要确保将它作为输入选项加入(在-i选项之前),例如:
#!/bin/sh start_time=12:23 duration=35 palette="/tmp/palette.png" filters="fps=15,scale=320:-1:flags=lanczos" ffmpeg -v warning -ss $start_time -t $duration -i $1 -vf "$filters,palettegen" -y $palette ffmpeg -v warning -ss $start_time -t $duration -i $1 -i $palette -lavfi "$filters [x]; [x][1:v] paletteuse" -y $2
如果不是,那么至少在第一遍它就会导致没有多个帧输出(调色板),所以不会做你想要的。
一个可供选择的是在流复制中预提取你想要编码的片段,看起来是这样:
% ffmpeg -ss 12:23 -t 35 -i full.mkv -c:v copy -map 0:v -y video.mkv
如果流复制不够精确,你可以添加一个trim滤波器。例如:
filters="trim=start_frame=12:end_frame=431,fps=15,scale=320:-1:flags=lanczos"
获得最好的调色板输出Getting the best out of the palette generation
现在我们可以开始看有趣的一部分了,在palettegen滤波器中,主要的和能让你感兴趣去尝试的大概就是stats_mode选项了。
这个选项的主要作用就是允许你指明在整部视频中你需要东西,或者只是移动的物体。如果你使用stats_mode=full(默认),所有的像素将会是颜色统计的一部分。如果你使用stats_mode=diff,只有与前一帧不同的像素会被计入。
注意:向一个滤波器中添加选项,需要这样做:filter=opt1=value1:opt2=value2
下面是一个例子来说明它是怎样影响最终的输出的:
第一张GIF图片使用stats_mode=full(默认)。在整个展示过程中背景都没有变化,结果是天空因为明智的颜色得到了更多的关注。另一方面,作为结果,文本的淡出遭到了破坏:
另一方面,第二张GIF图片是使用stats_mode=diff,这对移动物体很有帮助。事实上,文本淡出的表现更好,代价是天空的抖动产生了点小问题:
获得最佳的颜色映射输出
paletteuse滤波器具有稍微多点的选项来操作。最明显的是抖动(dither选项)。唯一可有效预测的抖动是Bayer抖动,其它所有的抖动都是基于误差扩散。
如果你真的希望使用Bayer(因为你有速度或尺寸的限制),你可以使用bayer_scale选项来减小或增加它的方格图案。 pattern.
当然你同样可以通过使用dither=none来完全禁用抖动。
关于误差扩散抖动,你将会希望使用floyd_steinberg,sierra2和sierra2_4a。关于这些的详细信息,我将你重定向到这里DHALF.TXT.
对于懒惰的人,floyd_steinberg是最受欢迎的了,而sierra2_4a是sierra2(这个才是默认的)的一个快速/小型化的版本,扩散的原理是用三个像素来代替七个像素。heckbert是记录在我前面提到的论文中的一个,但只是作为参考文献引入的(你大概没想到)。
这里是不同的抖动模式的一个小预览:
原始的 (31.82K) :
dither=bayer:bayer_scale=1(132.80K):
dither=bayer:bayer_scale=2(118.80K):
dither=bayer:bayer_scale=3(103.11K):
dither=floyd_steinberg(101.78K):
dither=sierra2(89.98K):
dither=sierra2_4a(109.60K):
dither=none(73.10K):
最后,玩过的抖动,你可能会对学习diff_mode选项感兴趣,引用文档中的一段话:
只有矩形的改变才会被再处理。这与GIF的cropping/offsetting压缩原理相似。如果图片只有一部分变化,那么对于速度来说这个选项就很有用,因而被用于这些情况,例如:对于作为移动场景边界的矩形的有限范围的误差扩散抖动(如果场景的变化不大,这种方式会导向更确定的输出。而且,作为结果,减小了移动噪声和得到更好的GIF压缩)。
或者换句话说:如果你想在你的图片的背景上使用误差扩散抖动,甚至他静态的,激活这个选项来限制误差传播到整个图片上。这里有一个典型的相关情况:
只有当上面和底部的文本同时移动的时候(也就是最后几帧),注意猴子的脸部的颜色抖动是怎样的。