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

news2024/12/25 0:28:22

前言

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

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

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

压缩方法介绍

降低分辨率

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

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

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

降低颜色深度

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

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

降低帧率

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

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

更多方法

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

压缩效果预览

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

压缩方法图像大小图像参数
原图p1.gif5.49mb分辨率: 540x532 ; 帧率: 33FPS ; 颜色深度: 256
降低分辨率p2.gif3.47mb分辨率: 270x266 ; 帧率: 33FPS ; 颜色深度: 256
降低颜色深度p3.gif4.41mb分辨率: 540x532 ; 帧率: 33FPS ; 颜色深度: 128
降低帧率p4.gif6.79mb分辨率: 540x532 ; 帧率: 16FPS ; 颜色深度: 256
Gifsicle无损压缩p5.gif4.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 :

pig

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

export

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

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

需要注意的是,开启 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 p6.gif
  2. 压缩后 1.2 MB p7.gif

混合多种压缩方法

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

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

s1.jpg

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

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
        }
    }

    // ……
    
    // 所有方法都试过后还是无法满足文件大小要求则认为压缩失败,返回 false
    return false
}

总结

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

s2.jpg

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

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

参考

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/549854.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java动态类型语言支持

JDK7发布字节码首位新成员——invokedynamic指令。以实现动态类型语言支持。也是为JDK8里可以顺利实现Lambda表达式而做的技术储备。我们将在本文详细了解动态语言支持这项特性出现的背景和它的意义与价值。 1 动态类型语言 动态类型语言的关键特征是它的类型检查的主体过程是…

MJ discord 添加应用配置

discord 添加机器人 https://discord.com/developers/applications 刷新token后显示&#xff0c;即机器人Token&#xff0c;后续配置到 mj.discord.bot-token 如图勾选后&#xff0c;打开url进行授权 选择Midjourney Bot所在的服务器 勾上这两个选项&#xff0c;点击 Save Cha…

图片转Excel表格,识别准确率的重要性

摘要&#xff1a;随着科技的不断发展&#xff0c;图片转Excel表格的应用越来越广泛。通过OCR技术实现图片转Excel表格&#xff0c;OCR识别准确率的提高对于信息录入的精度以及后续数据分析的可靠性具有非常重要的作用。本文探讨了影响OCR识别准确率的因素&#xff0c;并提出了提…

2.MATLAB篇——基本操作与矩阵输入

>> cos(((12345)^5)^0.5)ans -0.3623>> help sinsin - 参数的正弦&#xff0c;以弧度为单位此 MATLAB 函数 返回 X 的元素的正弦。sin 函数按元素处理数组。该函数同时接受实数和复数输入。 对于 X 的实数值&#xff0c;sin(X) 返回区间 [-1, 1] 内的实数值。 对于…

数据结构-单调栈2

这里是解了一个新的题目&#xff0c;然后对于有重复值的单调栈做了一些改进&#xff08;只适用于特殊题目&#xff09;&#xff1a;有重复值的单调栈不再使用ArrayList或者LinkedList&#xff0c;而是像无重复值的那样直接使用下标&#xff0c;这种方法能保证最终的正确性&…

Python图片转字符画,太好玩啦(46)

小朋友们好&#xff0c;大朋友们好&#xff01; 我是猫妹&#xff0c;一名爱上Python编程的小学生。 和猫妹学Python&#xff0c;一起趣味学编程。 今日主题 什么是字符画呢&#xff1f; 如何用Python将图片转为字符画&#xff1f; 我们先找一幅原画&#xff0c;比如它吧&…

亚马逊云科技 一周回顾 – 2022 年 7 月 18 日

上周&#xff0c;Amazon 峰会&#xff08;纽约&#xff09;在贾维茨中心线下举办&#xff0c;有数千名与会者以及 100 多家赞助商和合作伙伴参加。在主题演讲中&#xff0c;Amazon 首席开发人员倡导者 Martin Beeby 谈到了云基础设施的创新如何帮助客户适应挑战并抓住新的机遇。…

知识积累(1)

&#xff08;1&#xff09; 当您在Git中看到消息 "HEAD is now at 1343ccb FAB-17419 Fix off_chain_data sample error (#146)" 时&#xff0c;这是Git告知您当前所在的分支和最新的提交哈希。 这条消息通常出现在使用Git命令后&#xff0c;如git pull或git check…

【Java校招面试】实战面经(四)

目录 前言一、Http协议状态码301和302的区别二、Time Wait状态的作用是什么&#xff1f;三、ConcurrentHashMap在JDK1.7和JDK1.8的区别四、MySQL的优化&#xff1a;怎么优化SQL、用过MySQL的性能分析工具吗&#xff1f;五、反转数组的算法六、JDBC怎么使用的&#xff0c;什么是…

Redis三种模式——主从复制、哨兵模式、集群

目录 一、Redis模式二、Redis主从复制2.1 主从复制概述2.2 主从复制2.3 Redis主从复制过程2.4 搭建Redis主从复制2.4-1 环境部署2.4-2 安装Redis2.4-3 修改 Redis 配置文件&#xff08;Master节点操作&#xff09;2.4-4 修改 Redis 配置文件&#xff08;Slave节点操作&#xff…

『手撕 Mybatis 源码』03 - 解析映射配置文件

解析映射配置文件 SQL 映射文件只有很少的几个顶级元素&#xff08;按照定义顺序列出&#xff09; select 元素允许你配置很多属性来配置每条语句的行为细节 <selectid"select"parameterType"int"parameterMap"deprecated"resultType&quo…

Qt之界面 自定义标题栏、无边框、可移动、缩放

实现效果 注意&#xff1a;由于需要调用 Windows 上的头文件与库&#xff0c;所以不能跨平台&#xff0c;只支持 Windows 系统。如果想要跨平台&#xff0c;可以使用鼠标等事件实现&#xff0c;具体百度搜索参考下 自定义标题栏 titleBar.h #ifndef TITLEBAR_H #define TITL…

[Nacos] Nacos Client向Server发送注册请求和心跳请求 (二)

文章目录 1.Nacos Client的自动注册原理和实现2.Naocs Client向Server发送注册请求3.Nacos Client向Server发送心跳请求 Nacos Client的任务: 向Server发送注册请求, 向Server发送心跳请求, Client获取所有的服务, Client定时更新本地服务, Client获取要调用服务的提供者列表 …

Robot Dynamics Lecture Notes学习笔记之关节空间动力学控制

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 关节空间动力学 关节空间动力学控制关节阻抗调节重力补偿逆动力学控制 关节空间动力学控制 目前的工业机器人几乎完全依赖于关节位置控制的概念。它们建立在PID控制器的基础…

MySQL数据库期末实验报告(含实验步骤和实验数据)

MYSQL实验 实验步骤 1.创建数据库salesmanage 2.创建数据表&#xff1a;员工表&#xff0c;部门表&#xff0c;销售表&#xff1b; &#xff08;1&#xff09;员工表&#xff08;(员工号(CHAR)&#xff0c;员工姓名(CHAR)&#xff0c;性别(CHAR)&#xff0c;年龄(INT)&…

前端部署项目后nginx转发接口404(页面正常)

目录 1.前言 2. 场景复现&#xff1a; 3.问题的原因&#xff1a; 4.使用nginx一般要注意的小细节&#xff1a; 1. location / 写在下面&#xff0c;其他的转发如/v1写在上面​编辑 2.如何查看nginx转发请求到哪里了&#xff1f; 3.怎么写自己的前端路径&#xff1f; 5.使…

实验六 自动驾驶建模与仿真

【实验目的】 了解Matlab/Simulink软件环境&#xff0c;熟悉Simulink建模步骤&#xff1b;了解车辆运动控制的基本原理&#xff0c;学会简单的车辆运动控制建模及仿真&#xff1b;了解自动驾驶建模的基本过程&#xff0c;了解典型ADAS系统模型的应用特点。了解自动驾驶相关函数…

【SpringCloud组件——Nacos】

前置准备&#xff1a; 分别提供订单系统&#xff08;OrderService&#xff09;和用户系统&#xff08;UserService&#xff09;。订单系统主要负责订单相关信息的处理&#xff0c;用户系统主要负责用户相关信息的处理。 一、服务注册与发现 1.1、在父工程当中引入Nacos依赖 …

JavaScript实现输入数值判断是否为质数、合数的代码

以下为实现输入数值判断是否为质数、合数的程序代码和运行截图 目录 前言 一、输入数值判断是否为质数、合数 1.1 运行流程及思想 1.2 代码段 1.3 JavaScript语句代码 1.4 运行截图 前言 1.若有选择&#xff0c;您可以在目录里进行快速查找&#xff1b; 2.本博文代码可…

通讯录实现的需求分析和架构设计

本文实现的是通讯录产品的需求分析和架构设计&#xff0c;重点在于结构层次的设计&#xff0c;方便代码阅读和维护。 一、通讯录实现的需求分析 1、通讯录的功能清单 添加一个人员打印显示所有人员删除一个人员查找一个人员保存文件加载文件 2&#xff0c;数据存储信息 人员…