基于Linux命令行实现将视频文件快速转GIF文件
Keep Team Lv4

  本人的Blog是建在自己的私有云上的,之前在《有了云服务器可以做什么》中简单提及过我在小云上跑了不少的应用服务,其中就有为Blog提供视频播放功能NextCloud。虽然后来我觉得NextCloud并不那么实用,还很占用系统资源,于是用相对更轻量的WebDAV服务替换了NextCloud,但它仍然为一款不错的私人网盘应用服务。另外通过网盘共享资源的方式,实现视频点播对小云的系统资源消耗也是挺大的,所以我又想办法《让小云支持视频在线播放》,即便如此,对于我的1G2G的丐版配置,仍然有不小的资源压力。

  最后我的解决方案是,Blog中的视频都用GIF代替,消耗资源的事儿就交给客户端想办法吧☺。可问题又来了,我的主力电脑系统是Linux(Deepin、Ubuntu),怎么才能把视频转换成GIF呢?

使用在线转换工具

  这类在线工具,用百度搜索一下视频转GIF,就能出来很多结果。但是好用的却不太多,如果视频比较大,虽然界面上显示转换完成,但是实际上却什么都没有,比如这样的:

webGIF.jpg

  有些在线工具转换的结果又会添加水印或其它什么东西,总之,效果并不是特别理想

使用软件工具

  如果是在Windows平台上的话,有很多优秀的工具可以使用,比如FastStone Capture、Screen2GIF等,并且使用也很方便。对于Linux平台上,虽然没有那么多可选择的应用,但是仍然有优秀的工具,比如Peek就是一款优秀的Linux平台视频录制转GIF工具。

  细细研究一下Linux平台的应用软件,能发现大多都是利用各种开源组件或模块组合在一起的,比如Peek中GIF功能就是利用了gifski,那么常用的组件有哪些呢?在Linux平台上,常用的有 ffmpegconvert以及刚刚提到的gifski

使用三方组件

FFmpeg

  FFmepg算是Linux平台用途最广、使用频率最高的开源组件了,无论是视频播放、还是视频后期处理,甚至视频推流都离不开FFmpeg,几乎所有涉及多媒体的工具都可以找到FFmpeg组件。

FFmpeg is the leading multimedia framework, able to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created. It supports the most obscure ancient formats up to the cutting edge. No matter if they were designed by some standards committee, the community or a corporation. It is also highly portable: FFmpeg compiles, runs, and passes our testing infrastructure FATE across Linux, Mac OS X, Microsoft Windows, the BSDs, Solaris, etc. under a wide variety of build environments, machine architectures, and configurations.

  作为视频处理一哥,将视频转换为GIF,自然不在话下,配合简单的参数即可实现。

用法

ffmpeg -i test.mp4 ffmpeg.gif,这句命令的意思是将test.mp4按照其视频分辨率、帧率转换为ffmpeg.gif文件。如果想调整生产GIF文件的参数,可以配合以下参数实现:

-ss time_off: 从指定的时间(单位为秒)开始,支持[-]hh:mm:ss[.xxx]的格式

-t duration : 指定时长

-r rate : 帧速率(fps)

-s size : 指定分辨率

  当然FFmpeg的参数可不止这点,还支持很多的操作,比如按比例缩放-vf scale等等,有兴起的可以去网上搜索下相关的介绍,这里就不过多展开了。

安装

  一般来说主流的Linux发行版都支持命令行直接安装,网上搜索一下就能找到,比如:

apt install ffmpeg
yum install ffmpeg

  通过命令行安装,虽然方便,但是ffmpeg版本一般比较低(与主机版本能更好的兼容)。如果想尝试新的功能,请确认当前版本可支持。

  另一种方式是通过源码进行编译安装,好处在于能用上最新版本的ffmpeg,不过编译过程稍显麻烦。因为不同的Linux发行版,编译方法也略有不同,可在网上搜索一下最新的编译方式,大致步骤如下:

./configure
make
make install

Convert

  Convert并不是一个独立的组件或应用,它只是ImageMagick提供的一个图像转换功能。所以如果想在Linux平台上使用Convert,则需要完整安装ImageMagick才行。

Use ImageMagick® to create, edit, compose, or convert digital images. It can read and write images in a variety of formats (over 200) including PNG, JPEG, GIF, WebP, HEIC, SVG, PDF, DPX, EXR and TIFF. ImageMagick can resize, flip, mirror, rotate, distort, shear and transform images, adjust image colors, apply various special effects, or draw text, lines, polygons, ellipses and Bézier curves.

  convert虽然只是ImageMagick的子功能,但是其功能也是很丰富的。

用法

比如这样:

#转换图片格式,支持JPG, BMP, PCX, GIF, PNG, TIFF, XPM和XWD等类型
convert xxx.jpg xxx.png   #讲jpeg转换为png文件
#改变图像大小
convert -resize 1024x768 xxx.jpg  xxx1.jpg #将图像的像素改为1024768(注意1024与768之间是小写字母x)
*convert -sample 50%x50% xxx.jpg  xxx1.jpg #将图像缩小为原来的50%50%
#旋转图像
*convert -rotate 270 sky.jpg sky-final.jpg #将图像顺时针旋转270度 
#为图像加上文字
*convert -fill black -pointsize 60 -font helvetica -draw 'text 10,80 "hello world ! "' #xxx.jpg xxx1.jpg在图像的10,80位置用60磅全黑Helvetica字体上写hello world!
#批量文件格式转换
mogrify -path newdir -format png *.jpg

  对于视频转GIF,则可以通过将视频按帧,拆分为很多很多的单张图片,然后将这些单张图片再合并为一个GIF图片。图片合并可用下面的命令:

convert -delay 10 -loop 0 *.png convert.gif

  这句命令的意思是将所有png图片转换为一张GIF文件,这里用到了2个新的参数-delay-loop,它们的定义分别是:

-delay value         间隔多久(毫秒)显示下一张图片
-loop iterations     在文件中添加循环播放标志

  看到这里自然就会发现一个问题,假如我的png图片足够多,或者单张png图片足够大(比如4K分辨率),那么合成的GIF文件是不是就越大?假如我构造一个足够多,且文件足够大的图库,然后执行convert转换为GIF,是不是会把主机的资源全部占完?OOM?这个假设完全没毛病,实际情况也确实这样,再需要转换超大文件的时候convert常常会报cache resources exhausted异常。

异常处理

  一般来说convert报cache类的异常都是因为没有足够的系统资源用于转换图片导致。不过值得庆幸的是,convert并不是将系统所有资源消耗光了,才报这样的异常,主机系统依然可以正常工作运行。

  这得益于convert自己的内存分配策略。通过命令convert -list policy可以查询当前系统下convert的内存策略。

Path: /etc/ImageMagick-6/policy.xml
Policy: Resource
name: disk
value: 2GiB
Policy: Resource
name: map
value: 1GiB
Policy: Resource
name: memory
value: 512GiB
… …
Policy: Coder
rights: None
pattern: XPS

Path: [built-in]
Policy: Undefined
rights: None

  从返回的结果中,能看到policy配置文件的路径以及各种场景下的内存配置大小,可以根据需要修改policy配置文件中对应的子项即可。

安装

  和FFmpeg一样,主流的Linux发行版都可以通过命令行直接安装。

apt install imagemagick
yum install imagemagick

  也可以通过官网下载已编译好的二进制文件或封装包,使用安装命令进行安装。

GifSki

  GifSki是独立的开源软件,其目标就是将视频转换为更优的GIF,支持简单的参数配置,由于其目前并未部署在常见的应用仓库中,所以只能通过Rust进行安装。

gifski converts video frames to GIF animations using pngquant’s fancy features for efficient cross-frame palettes and temporal dithering. It produces animated GIFs that use thousands of colors per frame.

It’s open-source! It’s a CLI tool, but it can also be compiled as a library for seamless use in other apps ([ask me](mailto: kornel@pngquant.org?) or get a commercial license if you want to use it in a closed-source app or web service).

  上面介绍的几种工具都可以实现视频转GIF,那么他们到底谁才是最优(转换效果最好)的选择呢?接着我们就分别试试看。

用法

  GifSki和FFmepg一样,支持直接从视频转换为GIF文件。

gifski -fps 10 -width 320 -o result.gif test.mp4

  上面的例子将“test.mp4”文件转换为GIF,最大分辨率为320像素,每秒10帧。

  当然也和convert一样,支持将很多的PNG图片。按帧率生成GIF文件。

gifski -o result.gif *.png

效果对比

  那么它们转换效果到底怎么样呢?我们对同一个视频(1920 × 1080)文件(27M)进行转换,看看转换之后是什么样。

ffmpeg -i test.mp4 -r 5 ffmpeg.gif
ffmpeg -i test.mp4 -r 5  tmp/frame%04d.png
gifski --width 1920 -o gifski.gif tmp/frame*.png
convert -delay 10 -loop 0 tmp/frame*.png convert.gif

  转换之后会发现一个共性问题,无论哪一种方式转换的GIF文件,文件大小都会比原视频更大。

ray@MyDeepin:~/Videos/MyVideos/Screen Recordings$ ls -lh
-rw-r--r-- 1 ray ray 433M 10月 22 12:50 convert.gif
-rw-r--r-- 1 ray ray 150M 10月 22 12:42 ffmpeg.gif
-rw-r--r-- 1 ray ray 200M 10月 22 12:45 gifski.gif
-rw-r--r-- 1 ray ray  27M 10月 22 12:38 test.mp4

GIF压缩

  那么要怎么才能减少GIF文件的大小呢?一般从3个方面下手。

  • 图像分辨率大小

  通过缩小图像的分辨率,可以在很大程度上减少最终GIF文件的大小,这也是最有效的方法。

  • 图像帧率

  降低帧率是另一个减少GIF文件大小的方法,不过太低的帧率会让GIF的效果大打折扣。

  • 降低颜色通道与增加透明值

  简单来说就是将颜色鲜艳程度降低、并适当增加透明值,这样图片中颜色数据大小就会下降,但是会明显影响GIF的显示效果。

  如果前面2步都已无法再调整了,那么可通过convert的转换命令完成。

convert result.gif -fuzz 10% -layers Optimize result.min.gif

脚本

  最后分享一个基于上述三方组件的Shell脚本。该脚本可自动将目录下的视频文件转换为GIF文件(同时保留未压缩与压缩的文件)

#!/bin/bash
helpinfo() {
    echo "Version: GIF Creator Tools 1.0.0"
    echo "Usage: $0 [options ...] Video File Directory"
    echo ""
    echo "Video File Settings:"
    echo "-w       video size scaling width"
    echo "-r       set frame rate (Hz value, fraction or abbreviation)"
    echo "GIF File Settings:"
    echo "-f       colors within this distance are considered equal"
    exit -1
}
createDirectories(){
    if [ ! -d "$DSTPATH" ];then
        mkdir -p "$DSTPATH"
    fi
    if [ ! -d "$TMPPATH" ];then
        mkdir -p "$TMPPATH"
    fi
}
convertgif(){
    for FILE in `ls "$SRCPATH"/*.mp4 | tr " " "\?"`
    do
        FILENAME=`echo "$FILE"|awk -F "/" '{print $NF}'|awk -F "." '{print $1}'`
        ffmpeg -i "$SRCPATH/$FILENAME.mp4" -r $RATE -vf scale=$WIDTH:-1 "$TMPPATH"/TMP_%04d.png
        gifski -W $WIDTH -r 10 -o "$DSTPATH/$FILENAME.gifski.gif" "$TMPPATH"/TMP_*.png
        convert -delay 10 -loop 0 "$TMPPATH"/TMP_*.png "$DSTPATH/$FILENAME.convert.gif"
        rm -rf "$TMPPATH"/TMP_*.png
        convert "$DSTPATH/$FILENAME.gifski.gif" -fuzz $FUZZ% -layers Optimize "$DSTPATH/$FILENAME.gifski.min.gif"
        convert "$DSTPATH/$FILENAME.convert.gif" -fuzz $FUZZ% -layers Optimize "$DSTPATH/$FILENAME.convert.min.gif"
    done
    rm -rf "$TMPPATH"
}

setdefault(){
    if ["$SRCPATH" = ""]; then
        SRCPATH=`pwd`
    fi
    if ["$WIDTH" = ""]; then
        WIDTH=800
    fi
    if ["$RATE" = ""]; then
        RATE=5
    fi
    if ["$FUZZ" = ""]; then
        FUZZ=10
    fi
}

while getopts ':w:r:f:h' OPT; do
    case $OPT in
        w) WIDTH="$OPTARG";;
        r) RATE="$OPTARG";;
        f) FUZZ="$OPTARG";;
        h) helpinfo;;
    esac
done
shift $(($OPTIND - 1))
SRCPATH=$*
setdefault
DSTPATH="$SRCPATH/gif"
TMPPATH="$SRCPATH/tmp"
createDirectories
convertgif