在咱们的业务场景中,须要使用客户端采集图片,上传服务器,而后对图片信息进行识别。为了提高程序的性能,咱们须要保证图片上传服务器的速度的同时,保证用于识别图片的质量。整个优化包括两个方面的内容:java
在本文中,咱们主要介绍图片压缩优化,后续咱们会介绍如何对 Android 的相机进行封装和优化。本项目主要基于 Android 自带的图片压缩 API 进行封装,结合了 Luban 和 Compressor 的优势,同时提供了用户自定义压缩策略的接口。该项目的主要目的在于,统一图片压缩框库的实现,集成经常使用的两种图片压缩算法,让你以更低的成本集成图片压缩功能到本身的项目中。android
对于通常业务场景,当咱们展现图片的时候,Glide 会帮咱们处理加载的图片的尺寸问题。但在把采集来的图片上传到服务器以前,为了节省流量,咱们须要对图片进行压缩。git
在 Android 平台上,默认提供的压缩有三种方式:质量压缩和两种尺寸压缩,邻近采样以及双线性采样。下面咱们简单介绍下者三种压缩方式都是如何使用的:github
所谓的质量压缩就是下面的这行代码,它是 Bitmap 的方法。当咱们获得了 Bitmap 的时候,便可使用这个方法来实现质量压缩。它通常位于咱们全部压缩方法的最后一步。算法
// android.graphics。Bitmap
compress(CompressFormat format, int quality, OutputStream stream)
复制代码
该方法接受三个参数,其含义分别以下:服务器
JPEG
, PNG
和 WEBP
,表示图片的格式;[0,100]
之间,表示图片质量,越大,图片的质量越高;邻近采样基于临近点插值算法,用像素代替周围的像素。邻近采样的核心代码只有下面三行,微信
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 1;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.blue_red, options);
复制代码
邻近采样核心的地方在于 inSampleSize
的计算。它一般是咱们使用的压缩算法的第一步。咱们能够经过设置 inSampleSize 来获得原始图片采样以后的结果,而不是将原始的图片所有加载到内存中,以防止 OOM。标准使用姿式以下:架构
// 获取原始图片的尺寸
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
options.inSampleSize = 1;
BitmapFactory.decodeStream(srcImg.open(), null, options);
this.srcWidth = options.outWidth;
this.srcHeight = options.outHeight;
// 进行图片加载,此时会将图片加载到内存中
options.inJustDecodeBounds = false;
options.inSampleSize = calInSampleSize();
Bitmap bitmap = BitmapFactory.decodeStream(srcImg.open(), null, options);
复制代码
这里主要分红两个步骤,它们各自的含义是:框架
inJustDecodeBounds
为 true,来加载图片,以获得图片的尺寸信息。此时图片不会被加载到内存中,因此不会形成 OOM,同时咱们能够经过 Options 获得原图的尺寸信息。关于 inSampleSize 须要简单说明一下:inSampleSize 表明压缩后的图像一个像素点表明了原来的几个像素点,例如 inSampleSize 为 4,则压缩后的图像的宽高是原来的 1/4,像素点数是原来的 1/16,inSampleSize 通常会选择 2 的指数,若是不是 2 的指数,内部计算的时候也会向 2 的指数靠近。因此,实际使用过程当中,咱们会经过明确指定 inSampleSize 为 2 的指数,来避免内部计算致使的不肯定性。异步
邻近采样能够对图片的尺寸进行有效的控制,可是它存在几个问题。好比,当我须要把图片的宽度压缩到 1200 左右的时候,若是原始的图片的宽度压是 3200,那么我只能经过设置 inSampleSize 将采样率设置为 2 来将其压缩到 1600. 此时图片的尺寸比咱们的要求要大。就是说,邻近采样没法对图片的尺寸进行更加精准的控制。若是须要对图片尺寸进行更加精准的控制,那么就须要使用双线性压缩了。
双线性采样采用双线性插值算法,相比邻近采样简单粗暴的选择一个像素点代替其余像素点,双线性采样参考源像素相应位置周围 2x2 个点的值,根据相对位置取对应的权重,通过计算获得目标图像。
它在 Android 中的使用也比较简单,
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.blue_red);
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
Bitmap sclaedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth()/2, bitmap.getHeight()/2, matrix, true);
复制代码
也就是对获得的 Bitmap 应用 createBitmap()
进行处理,并传入 Matrix 指定图片尺寸放缩的比例。该方法返回的 Bitmap 就是双线性压缩以后的结果。
在实际使用过程当中,咱们一般会结合三种压缩方式使用,通常使用的步骤以下,
固然,本质上 Android 图片的编码是由 Skia 库来完成的,因此,除了使用 Android 自带的库进行压缩,咱们还能够调用外部的库进行压缩。为了追求更高的压缩效率,一般咱们会在 Native 层对图片进行处理,这将涉及 JNI 的知识。笔者曾在以前的文章 《在 Android 中使用 JNI 的总结》 中介绍过 Android 平台上 JNI 的调用的常规思路,感兴趣的同窗能够参考下。
如今 Github 上的图片压缩框架主要有 Luban 和 Compressor 两个。Star 的数量也比较高,一个 9K,另外一个 4K. 可是,这两个图片压缩的库有各自的优势和缺点。下面咱们经过一个表格总结一下:
框架 | 优势 | 缺点 |
---|---|---|
Luban | 听说是根据微信图片压缩逆推的算法 | 1.只适用于通常的图片展现的场景,没法对图片的尺寸进行精准压缩;2.内部封装 AsyncTaks 来进行异步的图片压缩,对于 RxJava 支持很差。 |
Compressor | 1.能够对图片的尺寸进行压缩;2.支持 RxJava。 | 1.尺寸压缩的场景有限,若是有特别的需求,则须要手动修改源代码;2.图片压缩采样的时候计算有问题,致使采样后的图片尺寸老是小于咱们指定的尺寸 |
上面的图表已经总结得很详细了。因此,根据上面的两个库各自的优缺点,咱们打算开发一个新的图片压缩框架。它知足下面的功能:
如下是咱们的图片压缩框架的总体架构,这里咱们只列举除了其中核心的部分代码。这里的 Compress 是咱们的链式调用的起点,咱们能够用它来指定图片压缩的基本参数。而后,当咱们使用它的 strategy()
方法以后,方法将进入到图片压缩策略中,此时,咱们继续链式调用压缩策略的自定义方法,个性化地设置各压缩策略本身的参数:
这里的全部的压缩策略都继承自抽线的基类 AbstractStrategy,它提供了两个默认的实现 Luban 和 Compressor. 接口 CompressListener 和 CacheNameFactory 分别用来监听图片压缩进度和自定义压缩的图片的名称。下面的三个是图片相关的工具类,用户能够调用它们来实现本身压缩策略。
首先,在项目的 Gradle 中加入个人 Maven 仓库的地址:
maven { url "https://dl.bintray.com/easymark/Android" }
复制代码
而后,在你的项目的依赖中,添加该库的依赖:
implementation 'me.shouheng.compressor:compressor:0.0.1'
复制代码
而后,就能够在项目中使用了。你能够参考 Sample 项目的使用方式。不过,下面咱们仍是对它的一些 API 作简单的说明。
下面是 Luban 压缩策略的使用示例,它与 Luban 库的使用相似。只是在 Luban 的库的基础上,咱们增长了一个 copy 的选项,用来表示当图片由于小于指定的大小而没有被压缩以后,是否将原始的图片拷贝到指定的目录。由于,好比当你使用回调获取图片压缩结果的时候,若是按照 Luban 库的逻辑,你获得的是原始的图片,因此,此时你须要额外进行判断。所以,咱们增长了这个布尔类型的参数,你能够经过它指定将原始文件进行拷贝,这样你就不须要在回调中对是不是原始图片进行判断了。
// 在 Compress 的 with() 方法中指定 Context 和 要压缩文件 File
val luban = Compress.with(this, file)
// 这里添加一个回调,若是你不使用 RxJava,那么能够用它来处理压缩的结果
.setCompressListener(object : CompressListener{
override fun onStart() {
LogUtils.d(Thread.currentThread().toString())
Toast.makeText(this@MainActivity, "Compress Start", Toast.LENGTH_SHORT).show()
}
override fun onSuccess(result: File?) {
LogUtils.d(Thread.currentThread().toString())
displayResult(result?.absolutePath)
Toast.makeText(this@MainActivity, "Compress Success : $result", Toast.LENGTH_SHORT).show()
}
override fun onError(throwable: Throwable?) {
LogUtils.d(Thread.currentThread().toString())
Toast.makeText(this@MainActivity, "Compress Error :$throwable", Toast.LENGTH_SHORT).show()
}
})
// 压缩图片的名称工厂方法,用来指定压缩结果的文件名
.setCacheNameFactory { System.currentTimeMillis().toString() }
// 图片的质量
.setQuality(80)
// 上面基本的配置完了,下面指定图片的压缩策略为 Luban
.strategy(Strategies.luban())
// 指定若是图片小于等于 100K 就不压缩了,这里的参数 copy 表示,若是不压缩的话要不要拷贝文件
.setIgnoreSize(100, copy)
// 按上面那样获得了 Luban 实例以后有下面两种方式启动图片压缩
// 启动方式 1:使用 RxJava 进行处理
val d = luban.asFlowable()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { displayResult(it.absolutePath) }
// 启动方式 2:直接启动,此时使用内部封装的 AsyncTask 进行压缩,压缩结果只能在上面的回调中进行处理了
luban.launch()
复制代码
下面是 Compressor 压缩策略的基本的使用,在调用 strategy()
方法指定压缩策略以前,你的任务与 Luban 一致。因此,若是你须要更换图片压缩算法的时候,直接使用 strategy()
方法更换策略便可,前面部分的逻辑无需改动,所以,能够下降你更换压缩策略的成本。
val compressor = Compress.with(this, file)
.setQuality(60)
.setTargetDir("")
.setCompressListener(object : CompressListener {
override fun onStart() {
LogUtils.d(Thread.currentThread().toString())
Toast.makeText(this@MainActivity, "Compress Start", Toast.LENGTH_SHORT).show()
}
override fun onSuccess(result: File?) {
LogUtils.d(Thread.currentThread().toString())
displayResult(result?.absolutePath)
Toast.makeText(this@MainActivity, "Compress Success : $result", Toast.LENGTH_SHORT).show()
}
override fun onError(throwable: Throwable?) {
LogUtils.d(Thread.currentThread().toString())
Toast.makeText(this@MainActivity, "Compress Error :$throwable", Toast.LENGTH_SHORT).show()
}
})
.strategy(Strategies.compressor())
.setMaxHeight(100f)
.setMaxWidth(100f)
.setScaleMode(Configuration.SCALE_SMALLER)
.launch()
复制代码
这里的 setMaxHeight(100f)
和 setMaxWidth(100f)
用来表示图片压缩的目标大小。具体的大小是如何计算的呢?在 Compressor 库中你是没法肯定的,可是在咱们的库中,你能够经过 setScaleMode()
方法来指定。这个方法接收一个整数类型的枚举,它的取值范围有 4 个,即 SCALE_LARGER
, SCALE_SMALLER
, SCALE_WIDTH
和 SCALE_HEIGHT
,它们具体的含义咱们会进行详细说明。这里咱们默认的压缩方式是 SCALE_LARGER,也就是 Compressor 库的压缩方式。那么这四个参数分别是什么含义呢?
这里咱们以一个例子来讲明,假设有一个图片的宽度是 1000,高度是 500,简写做 (W:1000, H:500),经过 setMaxHeight()
和 setMaxWidth()
指定的参数均为 100,那么,就称目标图片的尺寸,宽度是 100,高度是 100,简写做 (W:100, H:100)。那么按照上面的四种压缩方式,最终的结果将是:
自定义一个图片压缩策略也是很简单的,你能够经过继承 SimpleStrategy 或者直接继承 AbstractStrategy 来实现:
class MySimpleStrategy: SimpleStrategy() {
override fun calInSampleSize(): Int {
return 2
}
fun myLogic(): MySimpleStrategy {
return this
}
}
复制代码
注意下,若是想要实现链式的调用,自定义压缩策略的方法须要返回自身。
由于咱们的项目中,须要把图片的短边控制到 1200,长变只适应,只经过改变 Luban 来改变采样率只能把边长控制到一个范围中,没法精准压缩。因此,咱们想到了 Compressor,并提出了 SCALE_SMALLER 的压缩模式. 可是 Luban 也不是用不到,通常用来展现的图片的压缩,它用起来更加方便。所以,咱们在库中综合了两个框架,其实代码量并不大。固然,为了让咱们的库功能更加丰富,所以咱们提出了自定义压缩策略的接口,也是用来下降压缩策略的更换成本吧。
最后项目开源在 Github,地址是:github.com/Shouheng88/…. 欢迎 Star 和 Fork,为该项目贡献代码或者提出 issue :)
后续,笔者会对 Android 端的相机优化和 JNI 操做 OpenCV 进行图片处理进行讲解,感兴趣的关注做者呦 :)