Android JPEG 压缩那些事

Posted 冬天的毛毛雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android JPEG 压缩那些事相关的知识,希望对你有一定的参考价值。

JPEG 基础知识

JPEG(Joint Photographic Experts Group,联合图像专家小组)是一种针对照片影像广泛使用的有损压缩标准方法。

使用 JPEG 格式压缩的图片文件,最普遍使用的扩展名格式为 .jpg,其他常用的扩展名还包括 .JPEG、.jpe、.jfif 以及 .jif。

JEPG 编码原理

虽然 JEPG 文件可以以各种方式进行编码,但最常见的是使用 JFIF 编码,编码过程包括以下几个步骤:

  • 色彩空间转换

    将图像从 RGB 转换为 Y’CbCr 的不同颜色空间。Y’ 分量表示像素的亮度,而 CbCr 代表"色差值"(分为蓝色和红色分量)。Y’CbCr 颜色空间允许更大的压缩,而不会对感知图像质量产生重大影响。

    关于"色差"

    "色差"这个概念起源于电视行业,最早的电视都是黑白的,那时候传输电视信号只需要传输亮度信号,也就是Y信号即可,彩色电视出现之后,人们在Y信号之外增加了两条色差信号以传输颜色信息,这么做的目的是为了兼容黑白电视机,因为黑白电视只需要处理信号中的Y信号即可。

    根据三基色原理,人们发现红绿蓝三种颜色所贡献的亮度是不同的,绿色的“亮度”最大,蓝色最暗,设红色所贡献的亮度的份额为KR,蓝色贡献的份额为KB,那么亮度为

    1

    根据经验,KR=0.299,KB=0.114,那么

    2

    蓝色和红色的色差的定义如下

    3

    最终可以得到RGB转换为YCbCr的数学公式为

    4

  • 下采样

    与图像的颜色(Cb 和 Cr 分量)相比,人类眼睛对图像的亮度(Y’ 分量)在图像精细度上更敏感。

    对于人眼来说,图像中明暗的变化更容易被感知到,这是由于人眼的构造引起的。视网膜上有两种感光细胞,能够感知亮度变化的视杆细胞,以及能够感知颜色的视锥细胞,由于视杆细胞在数量上远大于视锥细胞,所以我们更容易感知到明暗细节。比如说下面这张图

    原图

    只保留 Y’ 分量

    Y

    只保留 Cb 分量

    Cb

    只保留 Cr 分量

    Cr

    利用这个特性,可以对 Y’CbCr 颜色空间做进一步的下采样,即降低 CbCr 分量的空间分辨率。

    下采样率为 “4:4:4” 表示不进行下采样;

    下采样率为 “4:2:2” 表示水平方向上减少 2 倍

    下采样率为 “4:2:0” 表示水平和垂直方向上减少 2 倍(最常用)

    image-20210411164013275

    下采样率通常表示为三部分比率 j:a:b,如果存在透明度则为四部分,这描述了 j个像素宽和 2 个像素高的概念区域中亮度和色度样本的数量。

    • j 表示水平方向采样率参考(概念区域的宽度)
    • a 表示第一行的色差采样(Cr,Cb)
    • b 表示第二行和第一行的色差采样(Cr,Cb)变化
  • 块分割

    在下采样之后,每个通道必须被分割为 8x8 的像素块,最小编码单位(MCU) 取决于使用的下采样。

    如果下采样率为 “4:4:4”,则最小编码单位块的大小为 8x8;

    如果下采样率为 “4:2:2”,则最小编码单位块的大小为 16x8;

    如果下采样率为 “4:2:0”,则最小编码单位块的大小为 16x16;

    image-20210509221734480

    如果通道数据不能被切割为整数倍的块,则通常会使用纯色去填充,例如黑色。

  • 离散余弦变换

  • 量化

    人眼善于在相对较大的区域看到较小的亮度差异,但不能很好地区分高频亮度变化的确切强度。这使得人们可以大大减少高频分量中的信息量。只需将频域中的每个分量除以该分量的常量,然后四舍五入(有损运算)为最接近的整数即可。

    这个步骤是不可逆的

  • 使用无损算法(霍夫曼编码的一种变体)进一步压缩所有 8×8 块的数据。

JEPG 压缩效果

图片质量([1,100])大小(bytes)压缩比例
JPEG example JPG RIP 100.jpg最高质量(100)814472.7:1
JPEG example JPG RIP 050.jpg高质量(50)1467915:1
JPEG example JPG RIP 025.jpg中等质量(25)940723:1
低质量(10)478746:1
JPEG example JPG RIP 001.jpg最低质量(1)1523144:1

JEPG 编码实现

  • libjpeg

    广泛使用的 C 库,用于读取和写入 JPEG 图像文件。

  • libjpeg-turbo

    高性能的 JEPG 图像解编码器,使用 SIMD 指令来加速在 x86、x86-64、Arm 和 PowerPC 系统上的 JEPG 文件压缩和解压缩,以及在 x86、x86-64 系统上的渐进式压缩。

    在 x86 和 x86-64 系统上,libjpeg-turbo 的速度是 libjpeg 的 2-6 倍,在其他系统上,也能大大优于 libjpeg。

android 图像解码

Android 上展示一张图像,都需要将图像解码成 Bitmap 对象,Bitmap 表示图像像素的集合,像素占用的内存大小取决 Bitmap 配置,目前 Android 支持的配置有如下:

  • ALPHA_8

    只存储透明度通道

  • ARGB_4444

    每个像素使用 2 字节存储

  • ARGB_8888

    每个像素使用 4 字节存储(默认)

  • HARDWARE

    特殊配置,Bitmap 数据存储在专门的图形内存(Native)

  • RGBA_F16

    每个像素使用 8 字节存储

  • RGB_565

    每个像素使用 2 字节存储,只有 RGB 通道。

源码解析

通常我们可以调用 BitmapFactory.decodeStream 方法从图像流中解码,Java 层只是个简单的入口,相关实现都在 Native 层的 BitmapFactory.doDecode 方法中。

// frameworks/base/libs/hwui/jni/BitmapFactory.cpp
static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream,jobject padding, jobject options, jlong inBitmapHandle,jlong colorSpaceHandle) {
  // ...  
}

一. 初始化相关参数

  • sampleSize

    采样率

  • onlyDecodeSize

    是否只解码尺寸

  • prefCodeType

    优先使用的颜色类型

  • isHardware

    是否存储在专门的图像内存

  • isMutable

    是否可变

  • scale

    缩放系数

  • requireUnpremultiplied

    颜色通道是否不需要"预乘"透明通道

  • javaBitmap

    可复用的 Bitmap

二. 创建解码器

根据解码的图像格式,创建不同的解码器 SkCodec。

图像格式SkCodec
JPEGSkJpegCodec
WebPSkWebpCodec
GifSkGifCodec
PNGSkPngCodec

SkCodec 负责核心实现,SkAndroidCodec 则是 SkCodec 的包装类,用于提供一些 Android 特有的 API。同样的,SkAndroidCodec 也是根据图像格式,创建不同的 SkAndroidCodec。

图像格式SkAndroidCodec
JPEG,PNG,GifSkSampledCodec
WebPSkAndroidCodecadapter

三. 创建内存分配器

根据是否存在可复用的 Bitmap,和是否需要缩放,使用不同的内存分配器 Allocator。

image-20210418224749597

四. 分配像素内存

调用 SkBitmap.tryAllocPixels 方法尝试分配所需的像素内存,存在以下情况,可能会导致分配失败。

  • Java Heap OOM
  • Native Heap OOM
  • 使用的可复用 Bitmap 太小

五. 执行解码

调用 SkAndroidCodec.getAndroidPixels 方法开始执行编码操作。

SkCodec::Result SkAndroidCodec::getAndroidPixels(const SkImageInfo& requestInfo,
        void* requestPixels, size_t requestRowBytes, const AndroidOptions* options) {
  // ...
return this->onGetAndroidPixels(requestInfo,requestPixels,requestRowBytes,*options);
}

SkAndroid.onGetAndroidPixels 方法有两个实现,分别是 SkSampledCodec 和 SkAndroidCodecadapter。

这里我们以 JPEG 图像解码为例,从上文可知,它使用的是 SkSampledCodec 和 SkJpegCodec,SkJpegCodec 是核心实现。

image-20210419142507664

Android 除了支持使用 BitmapFactory 进行完整的解码,也支持使用 BitmapRegionDecoder 进行局部解码,这个在处理特大的图像时特别有用。

Android JPEG 压缩

Android 在图像压缩上一直有个令人诟病的问题,同等大小的图像文件,ios 显示上总是更加细腻,也就是压缩效果更好,关于这个问题更详细的讨论,可以看这篇文章:github.com/bither/bith…

总的来说,就是 Android 底层使用的自家维护的一个开源 2D 渲染引擎 Skia,Skia 在 JPEG 图像文件的解编码上依赖的是 libjpeg 库,libjpeg 压缩参数叫:optimize_coding,这个参数为 TRUE,可以带来更好的压缩效果,同时也会消耗更多的 时间。

在 7.0 以下,Google 为了兼容性能较差的设备,而将这个值设置为 FALSE,7.0 及其以上,已经设置为 TRUE。

关于 optimize_coding 为 FALSE,更多的讨论可以看 groups.google.com/g/skia-disc…

7.0 以下:androidxref.com/6.0.1_r10/x…

7.0 及其以上:androidxref.com/7.0.0_r1/xr…

所以,现在比较主流的做法是,在 7.0 以下版本,可以基于 libjpeg-turbo 实现 JPEG 图像文件的压缩。

源码解析

可以通过调用 Bitmap.compress 方法来进行图像压缩,可选配置有:

  • format

    压缩图像格式,有 JPEG、PNG、WEBP。

  • quality

    压缩质量,可选值有 0-100。

同样的,Java 层只是提供 API 入口,实现还是在 Native 层的 Bitmap.Bitmap_comperss() 方法。

// framework/base/libs/hwui/jni/Bitmap.cpp
static jboolean Bitmap_compress(JNIEnv* env, jobject clazz, jlong bitmapHandle,jint format, jint quality,jobject jstream, jbyteArray jstorage) {
}

一. 创建编码器

根据图像格式创建不同的编码器。

图像格式编码器
JPEGSkJpegEncoder
PNGSkPngEnccoder
WebPSkWebpEncoder

二. 设置编码参数

Android JPEG 解码是依赖于 libjpeglibjpeg-turbo

在开始压缩编码之前,会先设置一系列参数。

  • 图像尺寸

  • 颜色类型

    常用的颜色类型有:

    JCS_EXT_BGRA,           /* blue/green/red/alpha */
    JCS_EXT_BGRA,           /* blue/green/red/alpha */
    
  • 下采样率

    目前 Android 支持 “4:2:0”(默认),“4:2:2” 和 “4:4:4”。

  • 最佳霍夫曼编码表

    默认为 true,表示使用最佳霍夫曼编码表,虽然会降低压缩性能,但提高了压缩效率。

    // Tells libjpeg-turbo to compute optimal Huffman coding tables
    // for the image.  This improves compression at the cost of
    // slower encode performance.
    fCInfo.optimize_coding = TRUE;
    
  • 质量

    这个参数会影响 JPEG 编码中 “量化” 这个步骤

三. 执行编码

// external/skia/src/imagess/SkImageEncoder.cpp
bool SkEncoder::encodeRows(int numRows) {
	// ...
  this->onEncodeRows(numRows);
}

JPEG 图像编码由 SkJpegEncoder 实现。

// txternal/skia/src/images/SkJpegEncoder.cpp
bool SkJpegEncoder::onEncodeRows(int numRows) {
	// ...
  for (int i = 0; i < numRows; i++) {
    // 执行 libjpeg-turbo 编码操作
  	jpeg_write_scanlines(fEncoderMgr->cinfo(), &jpegSrcRow, 1);
  }
}

采样算法

当调整图像的尺寸时,就需要对原始图像像素数据进行重新处理,这称为图像的采样处理。

目前 Android 默认支持 Nearest neighbor(邻近采样)Bilinear(双线性采样) 这两种采样算法。

  • Nearest neighbor(邻近采样)

    重新采样的栅格中每个像素获取与原始栅格中的最近像素相同的值,这个处理时间是最快的,但也会导致图像产生锯齿。

  • Bilinear(双线性采样)

    重新采样的栅格中的每个像素都是原始栅格中 2x2 4 个最近像素的加权平均值的结果。

除了以上两种,还有以下几种效果的更好的算法:

  • Bicubic(双立方采样)

    重新采样的栅格中的每个像素都是原始栅格中 4x4 16 个最近像素值的加权值的结果,更接近的像素会有更高的权重。

  • Lanczos

    高阶插值算法,它考虑了更多周围像素,并保留了最多的图像信息。

  • Magic Kernel

    快速又高效,却能产生惊人的清晰和锐利的结果,更详细的介绍:www.johncostella.com/magic/。

Spectrum

Spectrum 是 Facebook 开源的跨平台图像转码依赖库,与 Android 系统默认自带的 jpeg-turbo 相对,它有以下优势:

  • JPEG 编码基于 mozjpeg,相对于 jpeg-turbo,它提高了压缩率,但也增加了压缩处理时间。
  • 支持 Bicubic(双立方采样)和 Magic Kernel 采样算法。
  • 核心使用 CPP 实现,可以同时在 Android 和 iOS 平台实现一致的压缩效果。
  • 支持更多自定义配置,包括色度采样模式等等。

基准测试

基于 google/butteraugli 来比较原图像和压缩图像之间的质量差异,这个数值越小越好。

设备信息:华为 P20 Pro,Android 10

A 压缩质量 80

核心压缩质量色度采样模式质量差异文件大小耗时压缩率
原图-S444-8.7MB--
jpeg-turbo80S4442.9433522.5MB2255ms71%
mozjpeg80S4442.4862662.8MB3567ms67%
mozjpeg80S4202.493475(-15%)2.3MB2703ms73%(+2%)

B 压缩质量 75

核心压缩质量色度采样模式质量差异文件大小耗时压缩率
原图-S444-8.7MB--
jpeg-turbo75S4443.0758842.3MB2252ms73%
mozjpeg75S4442.6989832.4MB3188ms72%
mozjpeg75S4202.670076(-13%)2MB2470ms77%(+4%)

C 压缩质量 70

核心压缩质量色度采样模式质量差异文件大小耗时压缩率
原图-S444-8.7MB--
jpeg-turbo70S4442.7397942.1MB2230ms75%
mozjpeg70S4442.8385952.2MB3089ms74%
mozjpeg70S4202.810702(+2%)1.8MB2404ms79%(+4%)

D 压缩质量 65

核心压缩质量色度采样模式质量差异文件大小耗时压缩率
原图-S444-8.7MB--
jpeg-turbo65S4443.7341051.9MB2227ms78%
mozjpeg65S4443.1777062MB2775ms77%
mozjpeg65S4203.251182(-12%)1.6MB2116ms81%(+3%)

E 压缩质量 60

核心压缩质量色度采样模式质量差异文件大小耗时压缩率
原图-S444-8.7MB--
jpeg-turbo60S4444.5269811.8MB2189ms79%
mozjpeg60S4443.4863471.8MB2454ms79%
mozjpeg60S4203.479777(-23%)1.5MB2035ms82%(+3%)

从以上数据可知,使用 mozjpeg + S420 相对于 jpeg-turbo + S444 而言,压缩率平均有 3% 的提升,图像质量有 12% 的提升。

最后

大家如果还想了解更多Android 相关的更多知识点,可以点进我的GitHub项目中:https://github.com/733gh/GH-Android-Review-master自行查看,里面记录了许多的Android 知识点。最后还请大家点点赞支持下!!!

以上是关于Android JPEG 压缩那些事的主要内容,如果未能解决你的问题,请参考以下文章

OpenCV - arm/android 手机上的 JPEG 压缩不会产生正确的图像

Linux压缩那些事

Linux压缩那些事

关于Android架构那些事

Android 谈谈封装那些事 --BaseActivity 和 BaseFragment

Android谈谈封装那些事--BaseActivity和BaseFragment