前面几篇文章,我们了解了一些关于图片压缩的基础知识以及Android的Bitmap相关的知识,这篇文章我们主要讲解一下当下主流图片压缩算法鲁班压缩的算法逻辑。
下面让我们一起走进鲁班压缩算法吧。
我们已经知道了,bitmap已经给我们提供了质量压缩和采样压缩这俩中方式,那为什么又出现了鲁班压缩算法了,那就要先从鲁班压缩的背景说起。
鲁班压缩 —— Android常用的图片压缩工具算法之一,他是仿微信朋友圈压缩策略的一种体现。
目前做App开发总绕不开图片这个元素。但是随着手机拍照分辨率的提升,图片的压缩成为一个很重要的问题,随便一张图片都是好几M,甚至几十M,这样的照片加载到app,可想而知,随便加载几张图片,手机内存就不够用了,自然而然就造成了OOM ,所以,Android的图片压缩异常重要。
单纯对图片进行裁切,压缩已经有很多文章介绍。但是裁切成多少,压缩成多少却很难控制好,裁切过头图片太小,质量压缩过头则显示效果太差。于是自然想到App巨头——微信会是怎么处理,Luban(鲁班)就是通过在微信朋友圈发送近100张不同分辨率图片,对比原图与微信压缩后的图片逆向推算出来的压缩算法。
下面看看鲁班压缩效果的实际效果。因为是逆向推算,效果还没法跟微信一模一样,但是已经很接近微信朋友圈压缩后的效果,具体看以下对比!
图片内容 | 原图 | Luban压缩 | |
---|---|---|---|
截屏 720P | 720*1280,390k | 720*1280,87k | 720*1280,56k |
截屏 1080P | 1080*1920,2.21M | 1080*1920,104k | 1080*1920,112k |
拍照 13M(4:3) | 3096*4128,3.12M | 1548*2064,141k | 1548*2064,147k |
拍照 9.6M(16:9) | 4128*2322,4.64M | 1032*581,97k | 1032*581,74k |
滚动截屏 | 1080*6433,1.56M | 1080*6433,351k | 1080*6433,482k |
下面简单介绍一下鲁班的简单实用。
implementation ‘top.zibin:Luban:1.1.8’
方法 | 描述 |
---|---|
常用方法 | 方法描述 |
load | 传入原图 |
filter | 设置开启压缩条件 |
ignoreBy | 不压缩的阈值,单位为K |
setFocusAlpha | 设置是否保留透明通道 |
setTargetDir | 缓存压缩图片路径 |
setCompressListener | 压缩回调接口 |
setRenameListener | 压缩前重命名接口 |
Luban.with(this)
.load(photos)
.ignoreBy(100)
.setTargetDir(getPath())
.filter(new CompressionPredicate() {
@Override
public boolean apply(String path) {
return !(TextUtils.isEmpty(path) || path.toLowerCase().endsWith(".gif"));
}
})
.setCompressListener(new OnCompressListener() {
@Override
public void onStart() {
// TODO 压缩开始前调用,可以在方法内启动 loading UI
}
@Override
public void onSuccess(File file) {
// TODO 压缩成功后调用,返回压缩后的图片文件
}
@Override
public void onError(Throwable e) {
// TODO 当压缩过程出现问题时调用
}
}).launch();
Flowable.just(photos)
.observeOn(Schedulers.io())
.map(new Function<List<String>, List<File>>() {
@Override public List<File> apply(@NonNull List<String> list) throws Exception {
// 同步方法直接返回压缩后的文件
return Luban.with(MainActivity.this).load(list).get();
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
看了基本的使用,我们也只是仅仅会使用而已,接下来,我们走进源码深层分析一下:
我们知道鲁班压缩是更具WeChat逆向分析而出的一个算法框架,因此,他们有很多相识的地方,我们先解析一下微信的压缩:
经过这四步的处理,基本上和微信朋友圈的效果一致,包括文件大小和显示效果基本就一致了。
Luban的算法解析
Luban压缩目前只占了微信算法中的第二与第三步,算法逻辑如下:
判断图片比例值,是否处于以下区间内;
[1, 0.5625) 即图片处于 [1:1 ~ 9:16) 比例范围内
[0.5625, 0.5) 即图片处于 [9:16 ~ 1:2) 比例范围内
[0.5, 0) 即图片处于 [1:2 ~ 1:∞) 比例范围内
简单解释一下:获取图片的比例系数,如果在区间 [1, 0.5625) 中即图片处于 [1:1 ~ 9:16)比例范围内,比例以此类推,如果这个系数小于0.5,那么就给它放到 [1:2 ~ 1:∞) 比例范围内。
判断图片最长边是否过边界值;
[1, 0.5625) 边界值为:1664 * n(n=1), 4990 * n(n=2), 1280 * pow(2, n-1)(n≥3)
[0.5625, 0.5) 边界值为:1280 * pow(2, n-1)(n≥1)
[0.5, 0) 边界值为:1280 * pow(2, n-1)(n≥1)
步骤二:乍一看一脸懵,1664是什么,n是什么,pow又是什么。。。这写的估计只有作者自己能看懂了。其实就是判断图片最长边是否过边界值,此边界值是模仿微信的一个经验值,就是说1664、4990都是经验值,模仿微信的策略。
至于n,是返回的是 options.inSampleSize的值,就是采样压缩的系数,是int型,Google建议是2的倍数,所以为了配合这个建议,代码中出现了小于10240返回的是4这种操作。最后说一下pow,其实是 (长边/1280), 这个1280也是个经验值,逆向推出来的,解释到这里逻辑也清晰了。
计算压缩图片实际边长值,以第2步计算结果为准,超过某个边界值则:
width / pow(2, n-1),
height/ pow(2, n-1)
步骤三:这个感觉没什么用,还是计算压缩图片实际边长值,其实还是以第2步计算结果为准。
size = (newW * newH) / (width * height) * m;
步骤五:这一步也没啥用,也是为了后面循环压缩使用。 这个size就是上面计算出来的,最小 size 对应的值公式为:size = (newW * newH) / (width * height) * m,对应的三个值,就是上面根据图片的比例分成的三组,然后计算出来的。
将前面求到的值压缩图片 width, height, size 传入压缩流程,压缩图片直到满足以上数值
最后一步也没啥用,看字就知道是为了循环压缩,或许是微信也这样做?既然你已经有了预期,为什么不根据预期直接一步到位呢?但是裁剪的系数和压缩的系数怎么调整会达到最优一个效果,我想后期稳定后会开源给大家使用,大家可以静待。
走进Engine.java
直接看算法所在类 Engine.java
// 计算采样压缩的值,也就是模仿微信的经验值,核心内容
private int computeSize() {
// 补齐宽度和长度
srcWidth = srcWidth % 2 == 1 ? srcWidth + 1 : srcWidth;
srcHeight = srcHeight % 2 == 1 ? srcHeight + 1 : srcHeight;
// 获取长边和短边
int longSide = Math.max(srcWidth, srcHeight);
int shortSide = Math.min(srcWidth, srcHeight);
// 获取图片的比例系数,如果在区间[1, 0.5625) 中即图片处于 [1:1 ~ 9:16) 比例
float scale = ((float) shortSide / longSide);
// 开始判断图片处于那种比例中,就是上面所说的第一个步骤
if (scale <= 1 && scale > 0.5625) {
// 判断图片最长边是否过边界值,此边界值是模仿微信的一个经验值,就是上面所说的第二个步骤
if (longSide < 1664) {
// 返回的是 options.inSampleSize的值,就是采样压缩的系数,是int型,Google建议是2的倍数
return 1;
} else if (longSide < 4990) {
return 2;
// 这个10240上面的逻辑没有提到,也是经验值,不用去管它,你可以随意调整
} else if (longSide > 4990 && longSide < 10240) {
return 4;
} else {
return longSide / 1280 == 0 ? 1 : longSide / 1280;
}
// 这些判断都是逆向推导的经验值,也可以说是一种策略
} else if (scale <= 0.5625 && scale > 0.5) {
return longSide / 1280 == 0 ? 1 : longSide / 1280;
} else {
// 此时图片的比例是一个长图,采用策略向上取整
return (int) Math.ceil(longSide / (1280.0 / scale));
}
}
// 图片旋转方法
private Bitmap rotatingImage(Bitmap bitmap, int angle) {
Matrix matrix = new Matrix();
// 将传入的bitmap 进行角度旋转
matrix.postRotate(angle);
// 返回一个新的bitmap
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
// 压缩方法,返回一个File
File compress() throws IOException {
// 创建一个option对象
BitmapFactory.Options options = new BitmapFactory.Options();
// 获取采样压缩的值
options.inSampleSize = computeSize();
// 把图片进行采样压缩后放入一个bitmap, 参数1是bitmap图片的格式,前面获取的
Bitmap tagBitmap = BitmapFactory.decodeStream(srcImg.open(), null, options);
// 创建一个输出流的对象
ByteArrayOutputStream stream = new ByteArrayOutputStream();
// 判断是否是JPG图片
if (Checker.SINGLE.isJPG(srcImg.open())) {
// Checker.SINGLE.getOrientation这个方法是检测图片是否被旋转过,对图片进行矫正
tagBitmap = rotatingImage(tagBitmap, Checker.SINGLE.getOrientation(srcImg.open()));
}
// 对图片进行质量压缩,参数1:通过是否有透明通道来判断是PNG格式还是JPG格式,
// 参数2:压缩质量固定为60,参数3:压缩完后将bitmap写入到字节流中
tagBitmap.compress(focusAlpha ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG, 60, stream);
// bitmap用完回收掉
tagBitmap.recycle();
// 将图片流写入到File中,然后刷新缓冲区,关闭文件流和Byte流
FileOutputStream fos = new FileOutputStream(tagImg);
fos.write(stream.toByteArray());
fos.flush();
fos.close();
stream.close();
return tagImg;
}
Luban压缩当初出来的时候号称 “可能是最接近微信朋友圈的图片压缩算法” ,但这个库已经三四年没有维护了,随着产品的迭代微信已经也不是当初的那个微信了,Luban压缩的库也要进行更新了。所以,Luban还有一个turbo分支,这个分支主要是为了兼容Android 7.0以前的系统版本,导入libjpeg-turbo的jni版本。
libjpeg-turbo是一个C语音编写的高效JPEG图像处理库,Android系统在7.0版本之前内部使用的是libjpeg非turbo版,并且为了性能关闭了Huffman编码。在7.0之后的系统内部使用了libjpeg-turbo库并且启用Huffman编码。
那么什么是Huffman编码呢?前面提到的skio引擎又是什么东西呢?在下一篇我将深入讲解底层哈夫曼压缩。