大约两年前, 我尝试改进FFmpeg对GIF编码的支持,至少要很体面。尤其是要在GIF编码器中加入透明机制。然而你写的代码并不总是能使其达到最优,这种情况非常常见。但这仍然只是阻止编码器陷入尴尬的尝试。
不过最近在 Stupeflix ,我们需要一个方法给 Legend app 生成高质量的 GIF,所以我决定在这上面再花些功夫。
所有在这篇博文
FFmpeg
2.6
中列举的特性都是可用的,并且在Legend app的下一版本中将使用这些特性 (大概在3月26号左右)。文章太长不要去读:在 Usage 部分关注怎么使用即可。
让我们看下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图中一个子矩形但不改变其他地方的基本手段。当然在影片中,这么做用处不大,我们后面再谈。
除了上述机制,之前我没有再取得什么进展。也可能是其他措施不太明显,总之,在图片质量上还存在不少缺陷。
你可能知道,GIF 是受限于256色调色板。并且默认情况下,FFmpeg 只使用一个通用调色版去尝试覆盖所有的颜色区域,以此来支持含有大量内容的文件:
使用 抖动 来避免陷入这个问题(256色限制),在上面的邦尼大熊兔GIF中,应用了有序Bayer抖动。通过它的8×8网状图案可以很轻易地辨认出来。尽管它不是最好的方式,但是它同样具有很多优点,例如 生动 , 快速 ,实际上能够 防止带条效应 和类似的视觉小毛病。
你将会发现大多数的其它抖动方法都是基于误差的,原理是:一个单色误差(从调色板中挑选的颜色与想要的颜色之间的差异)将会传播到整个画面上。引起帧之间的一种”群集效应“,甚至是帧之间完全相同的源的区域,然而这经常会提供一个更好的质量,因为它完全抹去了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格式存储了一个全局调色板,但你可以对一张图片(或者是子画面;覆盖在前一帧上的后一帧,但它可以覆盖在一个特定的偏移位置上以获得一个更小的尺寸)重新定义一个调色板。每一帧的调色板都可以取代全局调色板来只对一帧起作用。一旦你停止定义一个调色板,它将会回落到全局调色板。这意味着你不能对一系列的帧定义一个调色板,而这恰恰是你想做的。(典型的做法是在每个场景变化的时候定义一个新的调色板)。
所以,换句话说,你需要遵守这样的模式:一个全局调色板,或者,每帧一个调色板。
我最初开始在每帧中计算出一个调色板,但是我发现了这样存在如下缺陷:
这就是我之所以没有使用这种方法而是选择计算一个全局调色板来代替的两个原因。现在我回想起来,它可能与重试这种方法有关,因为在某种程度上,现在的色彩量化比我当初测试的时候的状态要好一些。
对于一系列的帧的每一帧都使用相同的调色板(典型的做法是在场景变化时,就像前面提到的那样)也是可能的。或者,更好的做法是:只在子矩形变化时使用。
所有的这些都当作一个练习留给读者吧。欢迎补充,如果你对这个感兴趣的话可以随时和我联系。
具有一个全局调色板意思是一个2-pass(二次验码)压缩方式(除非你愿意把所有的视频帧都存储在内存里)。
第一遍是对整个图片计算一个调色板,这就是新的 palettegen 滤波器参与进来的地方。这个滤波器对每一帧的所有颜色制作一个直方图,并且基于这些生成一个调色板。
在技术层面上还存在一些琐事:这个滤波器实现了Paul Heckbert的这篇
Color Image Quantization for Frame Buffer
Display (1982)
论文中的算法的一个变种。这里是我记得的一些不同之处(或者说是关于论文中未定义的行为的特异性):所以不管怎样,这个滤波器都是在做色彩量化,并且生成一个调色板(通常保存在一个PNG文件里)。
它通常看起来像这个样子(upscaled):
第二遍(验码)是通过 paletteuse 滤波器完成的,就跟它的名字一样,它将会使用这个调色板来生成最终的量化颜色流,它的任务是在生成的调色板中找出最合适的颜色来表示输入的颜色。这也是你可以选择使用哪种抖动方法的地方。
这里同样有一些技术侧面上的小问题:
使用这两个滤波器可以让你将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变量包括:
不见得你会编码一部完整的影片,所以你可能会对使用-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"
现在我们可以开始看有趣的一部分了,在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的错切/偏移压缩机制比较类似。如果仅仅图像中的一部分在改变,这个选项有利于编码加速,并且该选项还通过一些方法限制误差扩散抖动的范围在矩形区域内,该矩形区域即是移动场景的边界 (如果场景变动不大,这更利于确定的输出同时,减小了移动噪声和输出更好地GIF压缩).
换句话说:如果想在图像里对背景使用误差扩散抖动,虽然背景是静态的,但可以通过这个选项限制误差在整幅图像的扩散。如下是与之相关的典型例子:
注意仅当顶部和底部的文本同时在动的时候,猴子的脸部图像是怎么抖动的。 (注意最后面几帧).