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’ 分量表示像素的亮度,而 Cb 和 Cr 代表"色差值"(分为蓝色和红色分量)。Y’CbCr 颜色空间允许更大的压缩,而不会对感知图像质量产生重大影响。
关于"色差"
"色差"这个概念起源于电视行业,最早的电视都是黑白的,那时候传输电视信号只需要传输亮度信号,也就是Y信号即可,彩色电视出现之后,人们在Y信号之外增加了两条色差信号以传输颜色信息,这么做的目的是为了兼容黑白电视机,因为黑白电视只需要处理信号中的Y信号即可。
根据三基色原理,人们发现红绿蓝三种颜色所贡献的亮度是不同的,绿色的“亮度”最大,蓝色最暗,设红色所贡献的亮度的份额为KR,蓝色贡献的份额为KB,那么亮度为
根据经验,KR=0.299,KB=0.114,那么
蓝色和红色的色差的定义如下
最终可以得到RGB转换为YCbCr的数学公式为
-
下采样
与图像的颜色(Cb 和 Cr 分量)相比,人类眼睛对图像的亮度(Y’ 分量)在图像精细度上更敏感。
对于人眼来说,图像中明暗的变化更容易被感知到,这是由于人眼的构造引起的。视网膜上有两种感光细胞,能够感知亮度变化的视杆细胞,以及能够感知颜色的视锥细胞,由于视杆细胞在数量上远大于视锥细胞,所以我们更容易感知到明暗细节。比如说下面这张图
只保留 Y’ 分量
只保留 Cb 分量
只保留 Cr 分量
利用这个特性,可以对 Y’CbCr 颜色空间做进一步的下采样,即降低 Cb 和 Cr 分量的空间分辨率。
下采样率为 “4:4:4” 表示不进行下采样;
下采样率为 “4:2:2” 表示水平方向上减少 2 倍
下采样率为 “4:2:0” 表示水平和垂直方向上减少 2 倍(最常用)
下采样率通常表示为三部分比率
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;
如果通道数据不能被切割为整数倍的块,则通常会使用纯色去填充,例如黑色。
-
离散余弦变换
-
量化
人眼善于在相对较大的区域看到较小的亮度差异,但不能很好地区分高频亮度变化的确切强度。这使得人们可以大大减少高频分量中的信息量。只需将频域中的每个分量除以该分量的常量,然后四舍五入(有损运算)为最接近的整数即可。
这个步骤是不可逆的
-
使用无损算法(霍夫曼编码的一种变体)进一步压缩所有 8×8 块的数据。
JEPG 压缩效果
图片 | 质量([1,100]) | 大小(bytes) | 压缩比例 |
---|---|---|---|
最高质量(100) | 81447 | 2.7:1 | |
高质量(50) | 14679 | 15:1 | |
中等质量(25) | 9407 | 23:1 | |
低质量(10) | 4787 | 46:1 | |
最低质量(1) | 1523 | 144:1 |
JEPG 编码实现
-
广泛使用的 C 库,用于读取和写入 JPEG 图像文件。
-
高性能的 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 |
---|---|
JPEG | SkJpegCodec |
WebP | SkWebpCodec |
Gif | SkGifCodec |
PNG | SkPngCodec |
SkCodec 负责核心实现,SkAndroidCodec 则是 SkCodec 的包装类,用于提供一些 Android 特有的 API。同样的,SkAndroidCodec 也是根据图像格式,创建不同的 SkAndroidCodec。
图像格式 | SkAndroidCodec |
---|---|
JPEG,PNG,Gif | SkSampledCodec |
WebP | SkAndroidCodecadapter |
三. 创建内存分配器
根据是否存在可复用的 Bitmap,和是否需要缩放,使用不同的内存分配器 Allocator。
四. 分配像素内存
调用 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 是核心实现。
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) {
}
一. 创建编码器
根据图像格式创建不同的编码器。
图像格式 | 编码器 |
---|---|
JPEG | SkJpegEncoder |
PNG | SkPngEnccoder |
WebP | SkWebpEncoder |
二. 设置编码参数
Android JPEG 解码是依赖于 libjpeg 和 libjpeg-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-turbo | 80 | S444 | 2.943352 | 2.5MB | 2255ms | 71% |
mozjpeg | 80 | S444 | 2.486266 | 2.8MB | 3567ms | 67% |
mozjpeg | 80 | S420 | 2.493475(-15%) | 2.3MB | 2703ms | 73%(+2%) |
B 压缩质量 75
核心 | 压缩质量 | 色度采样模式 | 质量差异 | 文件大小 | 耗时 | 压缩率 |
---|---|---|---|---|---|---|
原图 | - | S444 | - | 8.7MB | - | - |
jpeg-turbo | 75 | S444 | 3.075884 | 2.3MB | 2252ms | 73% |
mozjpeg | 75 | S444 | 2.698983 | 2.4MB | 3188ms | 72% |
mozjpeg | 75 | S420 | 2.670076(-13%) | 2MB | 2470ms | 77%(+4%) |
C 压缩质量 70
核心 | 压缩质量 | 色度采样模式 | 质量差异 | 文件大小 | 耗时 | 压缩率 |
---|---|---|---|---|---|---|
原图 | - | S444 | - | 8.7MB | - | - |
jpeg-turbo | 70 | S444 | 2.739794 | 2.1MB | 2230ms | 75% |
mozjpeg | 70 | S444 | 2.838595 | 2.2MB | 3089ms | 74% |
mozjpeg | 70 | S420 | 2.810702(+2%) | 1.8MB | 2404ms | 79%(+4%) |
D 压缩质量 65
核心 | 压缩质量 | 色度采样模式 | 质量差异 | 文件大小 | 耗时 | 压缩率 |
---|---|---|---|---|---|---|
原图 | - | S444 | - | 8.7MB | - | - |
jpeg-turbo | 65 | S444 | 3.734105 | 1.9MB | 2227ms | 78% |
mozjpeg | 65 | S444 | 3.177706 | 2MB | 2775ms | 77% |
mozjpeg | 65 | S420 | 3.251182(-12%) | 1.6MB | 2116ms | 81%(+3%) |
E 压缩质量 60
核心 | 压缩质量 | 色度采样模式 | 质量差异 | 文件大小 | 耗时 | 压缩率 |
---|---|---|---|---|---|---|
原图 | - | S444 | - | 8.7MB | - | - |
jpeg-turbo | 60 | S444 | 4.526981 | 1.8MB | 2189ms | 79% |
mozjpeg | 60 | S444 | 3.486347 | 1.8MB | 2454ms | 79% |
mozjpeg | 60 | S420 | 3.479777(-23%) | 1.5MB | 2035ms | 82%(+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 压缩不会产生正确的图像