使用 FFmpeg 处理高质量 GIF 图片

大约两年前, 我尝试改进FFmpeg对GIF编码的支持,至少要很体面。尤其是要在GIF编码器中加入透明机制。然而你写的代码并不总是能使其达到最优,这种情况非常常见。但这仍然只是阻止编码器陷入尴尬的尝试。

不过最近在 Stupeflix,我们需要一个方法给 Legend app 生成高质量的 GIF,所以我决定在这上面再花些功夫。

所有在这篇博文 FFmpeg
2.6
中列举的特性都是可用的,并且在Legend app的下一版本中将使用这些特性 (大概在3月26号左右)。

文章太长不要去读: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

centerimg

这个选项默认生效,当你的图片是高度运动的或者色彩变化强烈,你应该关闭它。

另一个实现的压缩机制是剪切,剪切是仅仅重绘GIF图中一个子矩形但不改变其他地方的基本手段。当然在影片中,这么做用处不大,我们后面再谈。

除了上述机制,之前我没有再取得什么进展。也可能是其他措施不太明显,总之,在图片质量上还存在不少缺陷。

256 色的局限性

你可能知道,GIF 是受限于256色调色板。并且默认情况下,FFmpeg 只使用一个通用调色版去尝试覆盖所有的颜色区域,以此来支持含有大量内容的文件:

centerimg

有序抖动和误差扩散

使用抖动来避免陷入这个问题(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格式存储了一个全局调色板,但你可以对一张图片(或者是子画面;覆盖在前一帧上的后一帧,但它可以覆盖在一个特定的偏移位置上以获得一个更小的尺寸)重新定义一个调色板。每一帧的调色板都可以取代全局调色板来只对一帧起作用。一旦你停止定义一个调色板,它将会回落到全局调色板。这意味着你不能对一系列的帧定义一个调色板,而这恰恰是你想做的。(典型的做法是在每个场景变化的时候定义一个新的调色板)。

所以,换句话说,你需要遵守这样的模式:一个全局调色板,或者,每帧一个调色板。

每帧一个调色板 (未实现)

我最初开始在每帧中计算出一个调色板,但是我发现了这样存在如下缺陷:

  • 开销: 一个256色调色板大小为768B,并且不包括在LZW 算法机制中,所以它没有被压缩。同时因其必须储存在每帧中,这就意味着对于25FPS的序列来说会有每秒150千比特的开销( 768 *8 *25b =150 *1024b )。 虽然大多数时候这可以忽略。
  • 在我最初的测试当中由于调色板的变化产生了亮度闪烁效果,这一点儿也不好。

这就是我之所以没有使用这种方法而是选择计算一个全局调色板来代替的两个原因。现在我回想起来,它可能与重试这种方法有关,因为在某种程度上,现在的色彩量化比我当初测试的时候的状态要好一些。

对于一系列的帧的每一帧都使用相同的调色板(典型的做法是在场景变化时,就像前面提到的那样)也是可能的。或者,更好的做法是:只在子矩形变化时使用。

所有的这些都当作一个练习留给读者吧。欢迎补充,如果你对这个感兴趣的话可以随时和我联系。

一个全局调色板(已实施)

具有一个全局调色板意思是一个2-pass(二次验码)压缩方式(除非你愿意把所有的视频帧都存储在内存里)。

第一遍是对整个图片计算一个调色板,这就是新的palettegen滤波器参与进来的地方。这个滤波器对每一帧的所有颜色制作一个直方图,并且基于这些生成一个调色板。

在技术层面上还存在一些琐事:这个滤波器实现了Paul Heckbert的这篇Color Image Quantization for Frame Buffer
Display (1982)
论文中的算法的一个变种。这里是我记得的一些不同之处(或者说是关于论文中未定义的行为的特异性):

  • 它使用一个全解析度的色彩直方图。而不是论文中作为关键建议使用的下采样 RGB 5:5:5 直方图,这个滤波器对1600万种可能的 RGB 8:8:8 色彩使用了一个哈希表。
  • 对方格的分割任然是在中点上进行的,对要分割的方格的选择是根据方格中的颜色方差来进行的(一个带有大的色彩方差的方格将会优先截掉)。
  • 对方格中的颜色求平均值取决于颜色的重要性,就我而言,这在论文中并没有定义。
  • 当沿着一个维度(红,绿或蓝)进行分割方格时,假如相等,绿色是优先于红色的,然后再是蓝色。

所以不管怎样,这个滤波器都是在做色彩量化,并且生成一个调色板(通常保存在一个PNG文件里)。

它通常看起来像这个样子(upscaled):

centerimg

颜色映射与抖动

第二遍(验码)是通过paletteuse滤波器完成的,就跟它的名字一样,它将会使用这个调色板来生成最终的量化颜色流,它的任务是在生成的调色板中找出最合适的颜色来表示输入的颜色。这也是你可以选择使用哪种抖动方法的地方。

这里同样有一些技术侧面上的小问题:

  • 滤波器实现了五种抖动方法,然而最初的论文中只提出了一种方法。
  • 就像palettegen一样,色彩分辨率(将24-bit输入颜色映射到一个调色板条目上)的完成没有破坏输入。它是通过一个K-d Tree(当k = 3时,很明显,每一个维度都是RGB的组成部分)的迭代实现和一个缓存系统来达到这一目标的。

使用这两个滤波器可以让你将GIF编码成这样(单全局调色板,无抖动):

centerimg

用法

使用相同参量手动运行两遍(验码)是有点讨厌,还要对每一遍的参数进行调整。所以我推荐写一个简单的脚本,如下:

#!/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

下面是一个例子来说明它是怎样影响最终的输出的:

centerimg

centerimg

第一张GIF图片使用stats_mode=full(默认)。在整个展示过程中背景都没有变化,结果是天空因为明智的颜色得到了更多的关注。另一方面,作为结果,文本的淡出遭到了破坏:

centerimg

另一方面,第二张GIF图片是使用stats_mode=diff,这对移动物体很有帮助。事实上,文本淡出的表现更好,代价是天空的抖动产生了点小问题:

centerimg

获得最佳的颜色映射输出

paletteuse滤波器具有稍微多点的选项来操作。最明显的是抖动(dither选项)。唯一可有效预测的抖动是Bayer抖动,其它所有的抖动都是基于误差扩散。

如果你真的希望使用Bayer(因为你有速度或尺寸的限制),你可以使用bayer_scale选项来减小或增加它的方格图案。
pattern.

当然你同样可以通过使用dither=none来完全禁用抖动。

关于误差扩散抖动,你将会希望使用floyd_steinberg,sierra2和sierra2_4a。关于这些的详细信息,我将你重定向到这里DHALF.TXT.

对于懒惰的人,floyd_steinberg是最受欢迎的了,而sierra2_4a是sierra2(这个才是默认的)的一个快速/小型化的版本,扩散的原理是用三个像素来代替七个像素。heckbert是记录在我前面提到的论文中的一个,但只是作为参考文献引入的(你大概没想到)。

这里是不同的抖动模式的一个小预览:

原始的 (31.82K) :

centerimg

dither=bayer:bayer_scale=1(132.80K):

centerimg

dither=bayer:bayer_scale=2(118.80K):

centerimg

dither=bayer:bayer_scale=3(103.11K):

centerimg

dither=floyd_steinberg(101.78K):

centerimg

dither=sierra2(89.98K):

centerimg

dither=sierra2_4a(109.60K):

centerimg

dither=none(73.10K):

centerimg

最终,在使用了抖动之后, 你可能会对diff_mode选项感兴趣。在此引用一段话:

只有变化中的矩形区域会被预处理。这与 GIF的错切/偏移压缩机制比较类似。如果仅仅图像中的一部分在改变,这个选项有利于编码加速,并且该选项还通过一些方法限制误差扩散抖动的范围在矩形区域内,该矩形区域即是移动场景的边界 (如果场景变动不大,这更利于确定的输出同时,减小了移动噪声和输出更好地GIF压缩).

换句话说:如果想在图像里对背景使用误差扩散抖动,虽然背景是静态的,但可以通过这个选项限制误差在整幅图像的扩散。如下是与之相关的典型例子:

centerimg

注意仅当顶部和底部的文本同时在动的时候,猴子的脸部图像是怎么抖动的。 (注意最后面几帧).

本文文字及图片出自 OSchina

余下全文(1/3)
分享这篇文章:

请关注我们:

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注