開源一個 Android 圖片壓縮框架

在咱們的業務場景中,須要使用客戶端採集圖片,上傳服務器,而後對圖片信息進行識別。爲了提高程序的性能,咱們須要保證圖片上傳服務器的速度的同時,保證用於識別圖片的質量。整個優化包括兩個方面的內容:java

  1. 相機拍照的優化:包括相機參數的選擇、預覽、啓動速度和照片質量等;
  2. 圖片壓縮的優化:基於拍攝的圖片和從相冊中選擇的圖片進行壓縮,控制圖片大小和尺寸。

在本文中,咱們主要介紹圖片壓縮優化,後續咱們會介紹如何對 Android 的相機進行封裝和優化。本項目主要基於 Android 自帶的圖片壓縮 API 進行封裝,結合了 LubanCompressor 的優勢,同時提供了用戶自定義壓縮策略的接口。該項目的主要目的在於,統一圖片壓縮框庫的實現,集成經常使用的兩種圖片壓縮算法,讓你以更低的成本集成圖片壓縮功能到本身的項目中。android

一、圖片壓縮的基礎知識

對於通常業務場景,當咱們展現圖片的時候,Glide 會幫咱們處理加載的圖片的尺寸問題。但在把採集來的圖片上傳到服務器以前,爲了節省流量,咱們須要對圖片進行壓縮。git

在 Android 平臺上,默認提供的壓縮有三種方式:質量壓縮和兩種尺寸壓縮,鄰近採樣以及雙線性採樣。下面咱們簡單介紹下者三種壓縮方式都是如何使用的:github

1.1 質量壓縮

所謂的質量壓縮就是下面的這行代碼,它是 Bitmap 的方法。當咱們獲得了 Bitmap 的時候,便可使用這個方法來實現質量壓縮。它通常位於咱們全部壓縮方法的最後一步。算法

// android.graphics。Bitmap
compress(CompressFormat format, int quality, OutputStream stream)
複製代碼

該方法接受三個參數,其含義分別以下:服務器

  1. format:枚舉,有三個選項 JPEG, PNGWEBP,表示圖片的格式;
  2. quality:圖片的質量,取值在 [0,100] 之間,表示圖片質量,越大,圖片的質量越高;
  3. stream:一個輸出流,一般是咱們壓縮結果輸出的文件的流

1.2 鄰近採樣

鄰近採樣基於臨近點插值算法,用像素代替周圍的像素。鄰近採樣的核心代碼只有下面三行,微信

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);
複製代碼

這裏主要分紅兩個步驟,它們各自的含義是:框架

  1. 先經過設置 Options 的 inJustDecodeBounds 爲 true,來加載圖片,以獲得圖片的尺寸信息。此時圖片不會被加載到內存中,因此不會形成 OOM,同時咱們能夠經過 Options 獲得原圖的尺寸信息。
  2. 根據上一步中獲得的圖片的尺寸信息,計算一個 inSampleSize,而後將 inJustDecodeBounds 設置爲 false,以加載採樣以後的圖片到內存中。

關於 inSampleSize 須要簡單說明一下:inSampleSize 表明壓縮後的圖像一個像素點表明了原來的幾個像素點,例如 inSampleSize 爲 4,則壓縮後的圖像的寬高是原來的 1/4,像素點數是原來的 1/16,inSampleSize 通常會選擇 2 的指數,若是不是 2 的指數,內部計算的時候也會向 2 的指數靠近。因此,實際使用過程當中,咱們會經過明確指定 inSampleSize 爲 2 的指數,來避免內部計算致使的不肯定性。異步

1.3 雙線性採樣

鄰近採樣能夠對圖片的尺寸進行有效的控制,可是它存在幾個問題。好比,當我須要把圖片的寬度壓縮到 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 就是雙線性壓縮以後的結果。

1.4 圖片壓縮算法總結

在實際使用過程當中,咱們一般會結合三種壓縮方式使用,通常使用的步驟以下,

  1. 使用鄰近採樣對原始的圖片進行採樣,將圖片控制到比目標尺寸稍大的大小,防止 OOM;
  2. 使用雙線性採樣對圖片的尺寸進行壓縮,控制圖片的尺寸爲目標的大小;
  3. 對上述兩個步驟以後獲得的圖片 Bitmap 進行質量壓縮,並將其輸出到磁盤上。

固然,本質上 Android 圖片的編碼是由 Skia 庫來完成的,因此,除了使用 Android 自帶的庫進行壓縮,咱們還能夠調用外部的庫進行壓縮。爲了追求更高的壓縮效率,一般咱們會在 Native 層對圖片進行處理,這將涉及 JNI 的知識。筆者曾在以前的文章 《在 Android 中使用 JNI 的總結》 中介紹過 Android 平臺上 JNI 的調用的常規思路,感興趣的同窗能夠參考下。

二、Github 上的開源的圖片壓縮庫

如今 Github 上的圖片壓縮框架主要有 Luban 和 Compressor 兩個。Star 的數量也比較高,一個 9K,另外一個 4K. 可是,這兩個圖片壓縮的庫有各自的優勢和缺點。下面咱們經過一個表格總結一下:

框架 優勢 缺點
Luban 聽說是根據微信圖片壓縮逆推的算法 1.只適用於通常的圖片展現的場景,沒法對圖片的尺寸進行精準壓縮;2.內部封裝 AsyncTaks 來進行異步的圖片壓縮,對於 RxJava 支持很差。
Compressor 1.能夠對圖片的尺寸進行壓縮;2.支持 RxJava。 1.尺寸壓縮的場景有限,若是有特別的需求,則須要手動修改源代碼;2.圖片壓縮採樣的時候計算有問題,致使採樣後的圖片尺寸老是小於咱們指定的尺寸

上面的圖表已經總結得很詳細了。因此,根據上面的兩個庫各自的優缺點,咱們打算開發一個新的圖片壓縮框架。它知足下面的功能:

  1. 支持 RxJava:咱們能夠像使用 Compressor 的時候那樣,指定圖片壓縮的線程和結果監聽的線程;
  2. 支持 Luban 壓縮算法:Luban 壓縮算法核心的部分只在於 inSampleSize 的計算,所以,咱們能夠很容易得將其集成到咱們的新的庫中。之因此加入 Luban,是爲了讓咱們的庫能夠適用於通常圖片展現的場景。用戶無需指定圖片的尺寸,用起來省心省力。
  3. 支持 Compressor 壓縮算法同時指定更多的參數:Compressor 壓縮算法就是咱們上述提到的三種壓縮算法的總和。不過,當要壓縮的寬高比與原始圖片的寬高比不一致的時候,它只提供了一種情景。下文中介紹咱們框架的時候會說明進行更詳細的說明。固然,你能夠在調用框架的方法以前主動去計算出一個寬高比,可是你須要把圖片壓縮的第一個階段主動走一遍,費心費力。
  4. 提供用戶自定義壓縮算法的接口:咱們但願設計的庫能夠容許用戶自定義壓縮策略。在想要替換圖片壓縮算法的時候,經過鏈式調用的一個方法直接更換策略便可。即,咱們但願可以讓用戶以最低的成本替換項目中的圖片壓縮算法。

三、項目總體架構

如下是咱們的圖片壓縮框架的總體架構,這裏咱們只列舉除了其中核心的部分代碼。這裏的 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 作簡單的說明。

4.1 Luban 的使用

下面是 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()
複製代碼

4.2 Compressor 的使用

下面是 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_WIDTHSCALE_HEIGHT,它們具體的含義咱們會進行詳細說明。這裏咱們默認的壓縮方式是 SCALE_LARGER,也就是 Compressor 庫的壓縮方式。那麼這四個參數分別是什麼含義呢?

這裏咱們以一個例子來講明,假設有一個圖片的寬度是 1000,高度是 500,簡寫做 (W:1000, H:500),經過 setMaxHeight()setMaxWidth() 指定的參數均爲 100,那麼,就稱目標圖片的尺寸,寬度是 100,高度是 100,簡寫做 (W:100, H:100)。那麼按照上面的四種壓縮方式,最終的結果將是:

  • SCALE_LARGER:對高度和長度中較大的一個進行壓縮,另外一個自適應,所以壓縮結果是 (W:100, H:50). 也就是說,由於原始圖片寬高比 2:1,咱們須要保持這個寬高比以後再壓縮。而目標寬高比是 1:1. 而原圖的寬度比較大,因此,咱們選擇將寬度做爲壓縮的基準,寬度縮小 10 倍,高度也縮小 10 倍。這是 Compressor 庫的默認壓縮策略,顯然它只是優先使獲得的圖片更小。這在通常情景中沒有問題,可是當你想把短邊控制在 100 就機關用盡了(須要計算以後再傳參),此時可使用 SCALE_SMALLER。
  • SCALE_SMALLER:對高度和長度中較大的一個進行壓縮,另外一個自適應,所以壓縮結果是 (W:200, H:100). 也就是,高度縮小 5 倍以後,達到目標 100,而後寬度縮小 5 倍,達到 200.
  • SCALE_WIDTH:對寬度進行壓縮,高度自適應。所以獲得的結果與 SCALE_LARGER 一致。
  • SCALE_HEIGHT:對高度進行壓縮,寬度自適應,所以獲得的結果與 SCALE_HEIGHT 一致。

4.3 自定義策略

自定義一個圖片壓縮策略也是很簡單的,你能夠經過繼承 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 進行圖片處理進行講解,感興趣的關注做者呦 :)

相關文章
相關標籤/搜索