首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android终端上视频转GIF的实现及GIF质量讨论

Android终端上视频转GIF的实现及GIF质量讨论

作者头像
天天P图攻城狮
发布2018-02-02 16:17:28
3.5K0
发布2018-02-02 16:17:28
举报
文章被收录于专栏:天天P图攻城狮天天P图攻城狮

GIF格式简介

GIF(Graphics Interchange Format)是CompuServe公司开发的一种图形文件格式,具有标准化的存储格式。图象基于自定义的调色板,最多可支持256色。 GIF文件主要可以分为三个部分:文件头(Header)、数据流(Data)和文件尾(Trailer)。

  • 文件头 文件头(Header)用于定义GIF文件格式署名和版本号。文件头的值为“GIF87a”或“GIF89a”,这两个版本的差异在于GIF中是否包含扩展内容信息。
  • 数据流 数据流(Data)主要分为逻辑屏幕标识符、全局调色板、图象标识符、局部调色板、基于调色板的图象数据、图形控制扩展等。
    • 逻辑屏幕标识符 逻辑屏幕标识符共包含7个字节,用于定义GIF的宽和高、全局调色板设置、背景色、宽高比。全局调色板设置占一个字节,各个bit位又分别用于设置全局调色板标志、颜色深度、分类标志、全局调色板大小(pixel)。
    • 全局调色板 当逻辑屏幕标识符中的置位了全局调色板标志时,需要定义全局调色板数据。全局调色板的颜色按照RGB(索引一)RGB(索引二)RGB(索引三)的顺序依次定义,列表的大小为2的pixel+1次方。
    • 图象标识符 图象标识符用于定义当前帧图象的设置,包括图象开始标志、x方向偏移量、y方向偏移量、图象宽度、图象高度、局部调色板设置。图象开始标志存在于每一帧图象的开始,固定值为0x2C。局部调色板设置占一个字节,各个bit位分别表示局部调色板标志、交织方式、分类标志、保留位(2bit,必须为0)、局部调色板大小(pixel)。当局部调色板标志置位时,图象的颜色设置以局部调色板中的颜色为准,否则以全局调色板中的颜色为准。
    • 局部调色板 当局部调色板标志置位时,需要额外定义当前图象的局部调色板。局部调色板的颜色定义方式与全局调色板一致。
    • 基于调色板的图象数据 图象数据是基于LZW编码方式对数据进行压缩。该部分数据首先包含一个LZW编码的位数,然后是LZW编码后的数据索引数,再是每个像素在调色板中的索引经过LZW编码后的值。LZW索引编码的最后包含一个终止字节为0。
    • 图形控制扩展(89a版本) 图形控制扩展主要用于设置处理方法、帧之间的延迟时间、透明色的索引值。
  • 文件尾 文件尾(trailer)表示GIF文件的结尾,固定值为0x3B。

视频转GIF的实现

使用GIFEncoder

实现思路是解析视频文件,获得视频的图象序列,再将视频的图象序列通过GIF标准的编码方式生成最终的GIF文件。其中解析视频文件并获得图象序列使用MediaMetaDataRetriever的API实现,GIF编码工作使用GIFEncoder实现。下面简要说明一下具体实现。 MediaMetaDataRetriever的getFrameAtTime方法通过传入视频当中的时间戳和获取帧的方式来获得视频中的某一帧图象。这里实现的均匀抽帧,使用的是OPTION_CLOSEST参数。在抽取图象时,可以根据自己的抽帧频率或间隔来决定EXTRACT_DURATION。

public void extractFrames() {
    MediaMetadataRetriever retriever = new MediaMetadataRetriever();
    retriever.setDataSource(INPUT_PATH);    long duration = Long.parseLong(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));    for (int i = 0; i < duration; i += EXTRACT_DURATION) {
        Bitmap bitmap = retriever.getFrameAtTime(i * 1000, MediaMetadataRetriever.OPTION_CLOSEST);
        Bitmap scaleBitmap = Bitmap.createScaledBitmap(bitmap, GIF_WIDTH, bitmap.getHeight() * GIF_WIDTH / bitmap.getWidth(), false); // 根据GIF的大小Resize Bitmap
        if (bitmap != scaleBitmap && !bitmap.isRecycled()) {
            bitmap.recycle(); // 回收原始Bitmap
        }
        addFrameToEncoder(scaleBitmap); // 将Bitmap图象传入GIFEncoder中进行编码
    }
}

获取了视频图象序列之后,只需要将图象序列编码到GIF当中即可。前面简要介绍了GIF的文件格式,因此GIFEncoder在编码过程中按照格式要求编码即可。

写入GIF文件头

public void writeHeader()  {
    writeString("GIF89a"); // 文件头,这里为了设置帧延迟时间,使用89a版本}private void writeString(String s)  {    for (int i = 0; i < s.length(); i++) {
        out.write((byte) s.charAt(i));
    }
}

写入逻辑屏幕标识符

public void writeLSD() {    // GIF宽、高
    writeShort(width);
    writeShort(height);
    out.write((0x80 | // 1 : 全局调色板标志
            0x70 | // 2-4 : 颜色深度
            0x00 | // 5 : 分类标志
            7)); // 6-8 : 256色
    out.write(0); // 背景色索引值
    out.write(0); // 默认宽高比 1:1}private void writeShort(int value) {
    out.write(value & 0xff);
    out.write((value >> 8) & 0xff);
}

写入全局调色板

public void writePalette() {
    out.write(colorTab, 0, colorTab.length); // 写入全局调色板
    int n = (3 * 256) - colorTab.length; // 256色RGB -> 3*256
    for (int i = 0; i < n; i++) {
        out.write(0);  // 当颜色不满256色时,用黑色填补
    }
}

写入图象控制扩展

public void writeGraphicCtrlExt() {
    out.write(0x21); 
    out.write(0xf9); 
    out.write(4);    int transp = 1;  // 使用透明色
    int disp = 2;  // 处理方法为回复到背景色
    disp <<= 2; // 左移2位
    out.write(0 | // 1:3 保留位
            disp | // 4:6 图形处理方法
            0 | // 7 非用户输入
            transp); // 8 是否使用透明色
    writeShort(delay); // 帧图象之间的延迟
    out.write(transIndex); // 透明色的颜色索引
    out.write(0); // 块终止标志}

写入图象标识符

public void writeImageDesc() {
    out.write(0x2c); // 帧图象开始标志0x2C
    writeShort(0); // 帧图象位置(0,0)
    writeShort(0);
    writeShort(width); // 帧图象大小
    writeShort(height);
    out.write(0x80 | // 1 使用局部调色板
            0 | // 2 交织方式为顺序方式
            0 | // 3 分类标志
            0 | // 4-5 保留位0
            7); // 局部调色板大小为256}

写入局部调色板。若当前帧图象的图象标识符中使用了局部调色板,则写入该部分内容。写入方式和全局调色板相同。

写入LZW编码后的图象数据。这里记录的是图象中每个像素点的颜色值在全局调色板或者局部调色板中的索引,经过LZW压缩后,编码到GIF文件中。

public void writePixels() {    // indexedPixels为每个像素点的颜色索引
    LZWEncoder encoder = new LZWEncoder(width, height, indexedPixels, 8);    // LZW压缩,并写入到文件中
    encoder.encode(out);
}

最后写入GIF文件尾

public void writeTrailer() {
    out.write(0x3b); // 文件尾
    out.flush();
    out.close();
}

生成GIF文件

public void initEncoder() {
    FileOutputStream out = new FileOutputStream(OUTPUT_PATH);
    encoder = new GIFEncoder();
    encoder.setDelay(FRAME_DELAY); // GIF帧延迟
    encoder.setTransparent(Color.BLACK); // 背景色
    encoder.setOutputStream(out); 
}public void addFrameToEncoder(Bitmap frame){    // 获得图象所有像素值
    getImagePixels();    // 计算调色板
    generatePalette();    // 写入图象控制扩展
    writeGraphicCtrlExt();    // 写入图象标识符
    writeImageDesc();    // 写入局部调色板
    writePalette();    // 写入图象像素对应的调色板索引
    writePixels();    // 回收bitmap
    frame.recycle();
}public void generateGif() {
    initEncoder();
    encoder.writeHeader();  // 写入GIF文件头
    encoder.writeLSD(); // 写入屏幕逻辑标识符
    encoder.writePalette(); // 写入全局调色板
    extractFrames();  // 抽取视频所有图象,并写入GIF
    encoder.writeTrailer();  // 写入GIF文件尾}

GIF图片质量提升

由于GIF最多可支持256色,将视频转成GIF时,调色板的质量以及像素与调色板的映射关系决定了最终GIF的质量。采用合适的量化算法和抖动算法,可以生成更好的调色板和像素映射索引列表。

算法介绍
  • NeuQuant NewQuant使用一维自组织网络,通过学习获得更优的颜色分布。通过设置采样因子(Sampling Factor)可以改变采样的效率。采样因子的范围在1到10之间,因子越大,采样越快,但是量化误差越大。
  • Adaptive Spatial Subdivision 基于二分法,通过计算所有像素所在区间以及区间误差来减少有效像素数,最终控制在256色以内。该方法共有3个步骤:颜色分类,颜色剔除、建立关系列表。 颜色分类。建立一个颜色Tree,Tree的节点记录一个颜色范围(ColorLow,ColorHigh)。对于每个像素点,通过二分法从根节点依次对Tree进行节点扩展,直到扩展至8层为止。扩展的同时,每个节点对应的区域都会统计该区域内的像素总数以及总的误差。 颜色剔除。迭代的每次从Tree中剔除误差最小的节点,并将该节点的颜色统计到其父节点当中,直到Tree中含有像素的节点数小于调色板的总数。 建立关系列表。首先将最终确定的颜色放置到一个列表当中,每个颜色对应一个index。然后对于图象中每个像素点,在Tree中找到包含该像素的层级最深的节点,则该该像素量化为节点对应的颜色,其index即为该节点在列表中对应的index。
  • 抖动算法:常用的有floyd-steinberg、Riemersma、bayer、heckbert等。
效果对比
  • 量化算法的影响 对比NeuQuant算法和Adaptive Spatial Subdivision算法的结果。通过对比,发现Adaptive Spatial Subdivision算法得到的颜色更加精准(领结处的绿色有明显的差异)。

NeuQuant

Adaptive Spatial Subdivision

对比256色和64色调色板的结果。通过对比,256色调色板的质量明显优于64色。

256色调色板生成的GIF

64色调色板生成的GIF

  • 抖动算法的影响。关闭抖动的GIF颜色呈现片状,Floyd-Steinberg和Riemersma的效果在局部的颜色更为丰富。使用抖动算法,GIF的大小会增大,实际情况中可以根据原始素材的颜色分布情况,选择合适的抖动算法。

关闭抖动的GIF

Floyd-Steinberg的GIF

Riemersma的GIF

使用FFMPEG

Android中也可以通过使用FFMPEG来实现视频转GIF的功能。首先可以通过FFMPEG的源码结合NDK编译出Android下的FFMPEG可执行文件,然后直接利用FFMPEG可执行文件执行相应命令即可。 FFMPEG将视频转成GIF的原理和上面相似,不过大部分实现FFMPEG都已经做好了,直接执行命令即可。执行的命令如下:

ffmpeg -i input.mp4 -vf "scale=200:-1:flags=lanczos,palettegen" -y palette.png
ffmpeg -i input.mp4 -i palette.png -lavfi "scale=200:-1:flags=lanczos,paletteuse=dither=floyd_steinberg" -y output.gif

第一个命令是生成GIF的调色板。生成时进行缩放处理,最终输出到宽度为200(维持原始宽高比),缩放算法采用lanczos。调色板使用palettegen滤镜来生成,该滤镜有3个参数:max_colors(最大支持颜色,默认256色)、reserve_transparent(是否使用最后一个颜色作为透明色,默认不使用)、stats_mode(计算调色板时,使用全部像素还是帧之间的差值,默认全部)。生成出来的调色板存在palette.png中。 第二个命令是使用生成的调色板作为全局调色板,将视频转成GIF。同样最终输出宽度控制在200,缩放算法采用lanczos。使用paletteuse滤镜来使用调色板文件,该滤镜主要的参数为dither(抖动算法),可以设置的算法有bayer、heckbert、floyd_steinberg、sierra2、sierra2_4a,默认为sierra2_4a,这里使用的是前面提到的floyd_steinberg算法。

结论

Android视频转GIF可以通过Android API和FFMPEG实现,这两种方法相比,FFMPEG的效率较高。在生成GIF的过程中,最关键的步骤就是生成调色板以及像素到调色板的映射关系。通过选用合适的量化算法和抖动算法,可以有效的提升GIF的图片质量。


作者简介:joeyxia(夏俊伟),天天P图Android工程师

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-12-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 天天P图攻城狮 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • GIF格式简介
  • 视频转GIF的实现
    • 使用GIFEncoder
      • GIF图片质量提升
        • 算法介绍
        • 效果对比
      • 使用FFMPEG
      • 结论
      相关产品与服务
      图像处理
      图像处理基于腾讯云深度学习等人工智能技术,提供综合性的图像优化处理服务,包括图像质量评估、图像清晰度增强、图像智能裁剪等。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档