在咱們的業務場景中,須要使用客戶端採集圖片,上傳服務器,而後對圖片信息進行識別。爲了提高程序的性能,咱們須要保證圖片上傳服務器的速度的同時,保證用於識別圖片的質量。整個優化包括兩個方面的內容: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 進行圖片處理進行講解,感興趣的關注做者呦 :)