前言

最近在划水摸鱼的时候,看到有位大佬发了一篇 GIF 压缩思路的文章。

让我突然想起来,很久以前我在我的项目 隐云图解制作 中就实现了一个动图工具箱,其中一个功能就是压缩GIF。

不过这位大佬只介绍了其中几种使用方法,还有一些方法他没有说到,正好我可以拆解我的项目对此做一个补全。

压缩方法介绍

降低分辨率

和静态图片以及视频一样,GIF文件的尺寸和分辨率呈正相关关系,分辨率越高需要储存的图像信息越多,所以GIF文件大小就会越大。

因此我们可以通过降低GIF的分辨率来减小文件体积,但是实际上并不是所有场景都适用于减少分辨率。

如果是表情包之类的GIF,那么就无所谓,只要还能看见就可以随意减少分辨率;如果是用于固定场景(例如商城头图)则不能随便改分辨率,因为在这些场景下对分辨率有严格要求。

降低颜色深度

由于GIF这个格式已经十分古老了,所以它在今天也还是只支持256色,对于颜色简单的动画来说勉强够用,对于实际拍摄的视频转成的GIF现在的256色都已经有点捉襟见肘了,更别说继续降低颜色位数。

所以这个方法只适用于颜色比较简单的GIF文件。

降低帧率

虽然一般来说,需要帧率达到24人眼看起来才会觉得流畅,但是实际上,GIF的帧率只要在10左右都还是比较流畅的。

并且大多数动图的动画其实并不需要高帧率,因此降低GIF帧率不失为一种减少文件体积的好办法。

更多方法

根据GIF格式的原理,我们还可以使用仅储存变化内容、使用透明度帧、合理应用调色板、借助第三方工具的压缩算法等方法来实现降低GIF文件大小。

压缩效果预览

下面是我使用不同压缩方法压缩后的效果:

压缩方法 图像 大小 图像参数
原图 5.49mb 分辨率: 540x532 ; 帧率: 33FPS ; 颜色深度: 256
降低分辨率 3.47mb 分辨率: 270x266 ; 帧率: 33FPS ; 颜色深度: 256
降低颜色深度 4.41mb 分辨率: 540x532 ; 帧率: 33FPS ; 颜色深度: 128
降低帧率 6.79mb 分辨率: 540x532 ; 帧率: 16FPS ; 颜色深度: 256
Gifsicle无损压缩 4.69mb 分辨率: 540x532 ; 帧率: 33FPS ; 颜色深度: 256

从上面的表格中,可以看出降低分辨率能够大幅减少文件大小。

降低颜色深度虽然也能减少大小,但是图像失真严重。

降低帧率后文件大小不减反增,其实降低帧率应该是可以减小文件大小的,只是因为这里我降低分辨率后没有重新做压缩优化,导致大小反而增加了。(压缩优化即上面提到的仅储存变化内容和透明度,原图已经进行过压缩优化,但是这里我降低帧率后反而把压缩优化全丢失了。)

使用 Gifsicle 无损压缩也能够大幅减少文件大小,并且图像质量几乎没有损失。

其实 Gifsicle 还可以进行有损压缩,虽然名字叫有损压缩,但是实际肉眼几乎看不出来差别。

另外,这里列举的只是单一压缩方法,实际使用时不会只使用一种压缩,而是多种压缩方法混合使用。

压缩方法实现

使用 FFmpeg

对于降低帧率,我们这里使用的是 FFmpeg 来实现,关于怎么在安卓上使用 FFmpeg 可以参考我的这篇文章: 在安卓项目中使用 FFmpeg 实现 GIF 拼接

命令十分简单:

val gifPath = "input.gif"
val savePath = "output.gif"
val frameRate = 12 // 新帧率
val cmd = FFMpegArgumentsBuilder.Builder().setOverride(true).setInput(gifPath).setFrameRate(frameRate).setOutput(savePath).build().cmd
FFmpegKit.executeWithArguments(cmd)

可以看到,我们这里直接使用了 FFmpeg 进行抽帧,而没有做任何的优化处理,这也是为什么在上面的测试中,降低帧率反而会使得文件体积更大。

使用 Gifsicle

对于除帧率外的压缩,我们均使用 Gifsicle 来实现,关于如何在安卓上使用 Gifsicle 可以看我的文章: 在安卓项目中使用gifsicle编辑GIF动图-Android NDK 编译 Gifsicle 为可执行文件

需要注意的是,其实使用 Gifsicle 也可以完成抽帧的需求,但是 Gifsicle 抽帧需要自己计算并明确指定抽出哪些帧,相比于 FFmpeg 会自动计算并删除帧,我们只需要指定最终导出图像需要多少帧即可,所以我偷懒直接使用 FFmpeg 来抽帧了。

虽然 FFmpeg 抽帧后反而会导致体积增大,但是不用担心,接下来我们就会说如何避免这个情况。

Gifsicle 为我们提供了非常多的 GIF 操作命令,对于压缩 GIF 这个需求,我们可以使用:

  1. –resize 更改分辨率
  2. –lossy 有损压缩
  3. –colors 或者 -k 更改颜色位数
  4. -Ox 无损优化压缩

更改分辨率和更改颜色位数不用过多介绍,这里着重介绍一下 Gifsicle 提供的无损压缩(优化)指令:-O1 -O2 -O3;以及有损压缩指令 --lossy 。

无损压缩

无损压缩使用指令 -O[level] 其中的 level 为压缩级别,可以填写1-3,数字越大,压缩效果越强:

-O1 : 仅储存每帧之间变化的部分

-O2 : 仅储存每帧之间变化的部分,并启用透明度。

-O3 : 同时尝试多种优化方式。

无损压缩的原理即通过对比帧与帧之间的图像区别,后面的帧储存的不是完整的图像,而是相对于前面的帧的不同的地方。

例如这张 gif :

解开每帧后实际是这样的:

可以看到除了第一帧储存的是完整的图像,后面储存的都只是相对于前一帧有变化的部分。

这对于动图中有大量静态部分的图片压缩效果非常明显,并且对动图质量几乎没有任何影响。

需要注意的是,开启 O3 级别压缩后,因为混合使用了多种优化算法,所以对于某些GIF也可能出现体积不降反增的现象(例如将已优化过后的GIF使用相同指令再优化一次就大概率会使得文件大小增加)

有损压缩

使用 --lossy[=lossiness] 可以对 GIF 进行有损压缩。

其中 lossiness 为压缩值,它是一个整数。

该选项默认值是 20,当值为 200 时就已经是非常大的压缩值了。

但是需要注意的是,由于算法限制,并不是值越大压缩效果越好:

It works best when only little loss is introduced, and due to limitation of the compression algorithm very high loss levels won’t give as much gain.

它的实现原理:

GIF’s LZW compression is based on a “dictionary” of strings of pixels seen. Normal encoder searches the dictionary for the longest string of pixels that exactly matches pixels in the image. Lossy encoder picks longest string of pixels that’s “similar enough” to pixels in the image (plus some magic to hide the distortions with dithering).

简单理解就是通过优化 GIF 的压缩算法,原压缩算法在在编码时需要匹配完全一致的数据,但是 lossy 通过更改为匹配 “足够相似” 的数据来进行压缩。当然,这意味着会造成数据的丢失,表现在图像上就是会产生一些抖动和噪点。

效果如下:

  1. 未压缩 3.3 MB
  2. 压缩后 1.2 MB

混合多种压缩方法

在介绍完上述压缩方法和参数后,我在项目中实际应用时其实是混合了多种方式来压缩的。

例如,在我提到的这个 GIF工具 功能中,有一个一键压缩至指定大小,或预设大小的功能:

该功能我在实现时会优先使用无损压缩方法压缩,如果无损压缩后尺寸不能满足则依次使用对质量影响较小的方法尝试压缩,直至尺寸达到预设值:

suspend fun compressGif2Size(activity: FragmentActivity?,sourcePath: String,targetSize: Long,resultPath: String,gifDrawable: GifDrawable): Boolean {// ……return compressByGifsicleOptimization(gifsicle, sourcePath, resultPath, targetSize, gifDrawable)
}// 使用 Gifsicle -O3 压缩
private suspend fun compressByGifsicleOptimization(gifsicle: File,sourcePath: String,resultPath: String,targetSize: Long,gifDrawable: GifDrawable): Boolean {val cmd = "$gifsicle -i $sourcePath -O3 -o $resultPath"// …… 执行 gifsickle 命令if (resultFile.length() < targetSize) {log2text("compress success!", "d")return true}return compressByReduceFrameRate(sourcePath, resultPath, targetSize, gifDrawable, gifsicle)
}// 使用 FFmpeg 降低帧率
private suspend fun compressByReduceFrameRate(sourcePath: String,resultPath: String,targetSize: Long,gifDrawable: GifDrawable,gifsicle: File): Boolean {// ……while (rate >= CompressGifFrameRateMinValue) {var ffmpegCmd = FFMpegArgumentsBuilder.Builder().setOverride(true).setInput(sourcePath).setFrameRate(currentRate.toString()).setOutput(resultPath).build(false).cmd// …… 执行 FFmpeg 命令if (resultFile.length() < targetSize) {return true}// ……rate--}return compressByReduceResolution(resultPath, gifDrawable, gifsicle, targetSize)
}// 使用 Gifsicle 减少分辨率
private suspend fun compressByReduceResolution(resultPath: String,gifDrawable: GifDrawable,gifsicle: File,targetSize: Long): Boolean {// ……while (scale >= minScale) {val cmd = "$gifsicle -i $tempOutFile --scale $scale -O3 -o $resultPath"// …… 执行 gifsickle 命令if (resultFile.length() < targetSize) {return true}// ……scale--}// ……return compressByLossy(gifsicle, resultPath, targetSize)
}// 使用 Gifsicle lossy 压缩
private suspend fun compressByLossy(gifsicle: File,resultPath: String,targetSize: Long): Boolean {// ……for (i in 20..CompressGifLossyMaxValue step CompressGifLossyStepValue) {val cmd = "$gifsicle -i $resultPath --lossy=$i  -O3 -o $resultPath"// …… 执行 gifsickle 命令if (resultFile.length() < targetSize) {return true}// ……}// ……return compressByReduceColorBit(gifsicle, resultPath, targetSize)
}// 使用 Gifsicle 减少颜色位数
private suspend fun compressByReduceColorBit(gifsicle: File,resultPath: String,targetSize: Long): Boolean {// ……for (i in 256 downTo CompressGifMinColorNum step CompressGifMinColorStepValue) {val cmd = "$gifsicle -i $resultPath -k $i --lossy=$CompressGifLossyMaxValue -O3 -o $resultPath"// …… 执行 gifsickle 命令if (resultFile.length() < targetSize) {return true}}// ……// 所有方法都试过后还是无法满足文件大小要求则认为压缩失败,返回 falsereturn false
}

总结

总的来说,为了降低GIF文件的大小,我们有以下几种方法:

而这些方法都可以使用 Gifsicle 来实现。

其中,除了使用 -Ox 优化外,其他均是有损压缩,或多或少会影响压缩后的动图质量。

参考

  1. Glide库里,藏了一套你心心念念的GIF压缩工具集
  2. 如何正确压缩GIF格式文件?来看京东设计师的总结!
  3. Lossy Gif Compressor

在安卓中压缩GIF的几种方法(附实例代码)相关推荐

  1. matlab中多元线性回归regress函数精确剖析(附实例代码)

    matlab中多元线性回归regress函数精确剖析(附实例代码) 目录 前言 一.何为regress? 二.regress函数中的参数 三.实例分析 总结 前言 regress函数功能十分强大,它可 ...

  2. centos 卸载软件_一篇看懂!详解-Linux系统中安装软件的三种方法

    Linux系统中安装软件的三种方法 注:本文主要以CentOS为例介绍常用的安装方式,其他版本linux在文章底部 Linux系统中怎么安装软件,首先说一下应用程序与系统命令的区别: 1.文件位置 系 ...

  3. 【引用】在VB6.0中实现弹出式菜单的几种方法

    在Windows应用程序中,利用弹出式菜单(Pop-up Menu)是增加软件易用性的一个重要方式.本文将向大家介绍在VB6.0中实现弹出式菜单的几种方法. 利用VB6.0内置的PopupMenu方法 ...

  4. 华为n3计算机在哪里,在华为nova3i中连接电脑的两种方法介绍

    大家知道怎么在华为nova3i中连接电脑吗?不知道没有关系,小编今天介绍在华为nova3i中连接电脑的两种方法,希望可以帮助到你哦. 在华为nova3i中连接电脑的两种方法介绍 方法一: 1.下载并且 ...

  5. linux将一段时间内文件压缩,在 Linux 上压缩文件的 5 种方法

    在 Linux 上压缩文件的 5 种方法时间:2020-05-07 11:18    浏览次数: 发布者:润天教育    来源:金信润天 0 在 Linux 上有不少用于压缩文件的命令.最新最有效的一 ...

  6. LaTeX 中处理参考文献的三种方法总结

    LaTeX 中处理参考文献的三种方法总结 方法一:用BibLaTeX处理 分成如下四步: 第一步:制作生成bib文件: 第二步:在导言区需要加入biblatex宏包: \usepackage[格式控制 ...

  7. android 系统升级 方法,安卓系统怎么升级 浅谈安卓系统更新升级的几种方法

    最近有网友问小编"安卓系统怎么升级?",针对该问题,笔者也在网上查找了下相关资料,不过并没有找到什么有价值的相关介绍,多数都是介绍如何自动升级.或者下载升级版包等等方法,对于一些常 ...

  8. LaTeX中处理参考文献的三种方法总结

    LaTeX 中处理参考文献的三种方法总结 1.摘要 2.关键词 3.处理参考文献常用的三种方法 3.1 方法一:用BibLaTeX处理 3.2 方法二:用BibTeX处理 3.3 方法三:用thebi ...

  9. android修改密码功能,安卓手机设置开机密码几种方法介绍

    安卓系统的手机设置开机密码一般有2-3种,其中包括密码锁.图案锁.PIN密码锁等几项.这几项都能实现开机需要输入相应的密码才能使用手机的,所以也算是开机锁了.好了,下面小编就教大家怎么设置这几种锁. ...

最新文章

  1. 说说Request.Params[key]和Request[key]
  2. JavaWeb:AJAX
  3. Python 库兼容性问题-fromstring() has been removed. Please call frombytes() instead.原因及解决办法
  4. .NET Framework 4.7发布,支持Windows 10创作者更新
  5. 信息学奥赛一本通(1411:区间内的真素数)
  6. Python中提供的各种队列结构
  7. springBoot项目启动去掉多余的启动日志
  8. Opengl_9_复合变换
  9. php任意地方关闭弹窗,vue和jq中实现点击任意地方关闭弹窗
  10. python基于大数据的食物推荐系统
  11. Nginx+rtmp模块实现直播推流
  12. 关于深度学习的研究综述
  13. windows上传ipa文件到苹果开发者中心的教程
  14. Python的excel工作簿写入与读取操作
  15. 编写一个掷色子猜大小的游戏
  16. 客户关系管理CRM系统源码PHP开源软件源码
  17. 黑小米真不应该,如今它已是国货之光,成为国产手机在海外领军者
  18. TOPSIS和熵权法的应用(Matlab实现,包括数据预处理)
  19. 访问控制列表 ACL
  20. CloudComparePCL 点云点匹配(基于点到面的距离)

热门文章

  1. 概览丨“E起AI”-人工智能10强项目!
  2. python调用第三方接口获取数据_python 接口实现 供第三方调用的例子
  3. Midway-ModelProxy — 轻量级的接口配置建模框架
  4. 计算机操作系统引论(操作系统)
  5. keil的数据波形如何在电脑示波器软件keil array visualization显示
  6. 4种搜集来的Axure原型上传+在线预览的方法
  7. 如何比对两个版本的Word文档
  8. Python输出CSV乱码
  9. 计算机项目化教学,计算机教学中项目化教学的应用的论文
  10. RFID辐射测试小结