Android 中圖片壓縮分析(上)

做者: shawnzhao,QQ音樂技術團隊 一員
html

1、前言

在 Android 中進行圖片壓縮是很是常見的開發場景,主要的壓縮方法有兩種:其一是質量壓縮,其二是下采樣壓縮。java

前者是在不改變圖片尺寸的狀況下,改變圖片的存儲體積,然後者則是下降圖像尺寸,達到相同目的。android

因爲本文的篇幅問題,分爲上下兩篇發佈。ios

2、Android 質量壓縮邏輯

在Android中,對圖片進行質量壓縮,一般咱們的實現方式以下所示:c++

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
//quality 爲0~100,0表示最小體積,100表示最高質量,對應體積也是最大
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);

在上述代碼中,咱們選擇的壓縮格式是CompressFormat.JPEG,除此以外還有兩個選擇:git

其一,CompressFormat.PNG, PNG 格式是無損的,它沒法再進行質量壓縮,quality 這個參數就沒有做用了,會被忽略,因此最後圖片保存成的文件大小不會有變化;
其二,CompressFormat.WEBP ,這個格式是 google 推出的圖片格式,它會比 JPEG 更加省空間,通過實測大概能夠優化 30% 左右。算法

因爲項目緣由和兼容性選擇了JPEG,所以接下來的分析也將是圍繞 JPEG 展開。瀏覽器

將 PNG 圖片轉成 JPEG 格式以後不會下降這個圖片的尺寸,可是會下降視覺質量,從而下降存儲體積。同時,因爲尺寸不變,因此將這個圖片解碼成相同色彩模式的 bitmap 以後,佔用的內存大小和壓縮前是同樣的。函數

回到最初的代碼示例,函數 compress 通過一連串的 java 層調用以後,最後來到了一個 native 函數,以下:性能

//Bitmap.cpp
static jboolean Bitmap_compress(JNIEnv* env, jobject clazz, jlong bitmapHandle,
                                jint format, jint quality,
                                jobject jstream, jbyteArray jstorage) {

    LocalScopedBitmap bitmap(bitmapHandle);
    SkImageEncoder::Type fm;

    switch (format) {
    case kJPEG_JavaEncodeFormat:
        fm = SkImageEncoder::kJPEG_Type;
        break;
    case kPNG_JavaEncodeFormat:
        fm = SkImageEncoder::kPNG_Type;
        break;
    case kWEBP_JavaEncodeFormat:
        fm = SkImageEncoder::kWEBP_Type;
        break;
    default:
        return JNI_FALSE;
    }

    if (!bitmap.valid()) {
        return JNI_FALSE;
    }

    bool success = false;

    std::unique_ptr<SkWStream> strm(CreateJavaOutputStreamAdaptor(env, jstream, jstorage));
    if (!strm.get()) {
        return JNI_FALSE;
    }

    std::unique_ptr<SkImageEncoder> encoder(SkImageEncoder::Create(fm));
    if (encoder.get()) {
        SkBitmap skbitmap;
        bitmap->getSkBitmap(&skbitmap);
        success = encoder->encodeStream(strm.get(), skbitmap, quality);
    }
    return success ? JNI_TRUE : JNI_FALSE;
}

能夠看到最後調用了函數 encoder->encodeStream(....) 編碼保存本地。該函數是調用 skia 引擎來對圖片進行編碼壓縮,對 skia 的介紹將在後文展開。

一段完整的示例代碼以下:

// R.drawable.thumb 爲 png 圖片
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.thumb);
try {
    //保存壓縮圖片到本地
    File file = new File(Environment.getExternalStorageDirectory(), "aaa.jpg");
    if (!file.exists()) {
        file.createNewFile();
    }
    FileOutputStream fs = new FileOutputStream(file);
    bitmap.compress(Bitmap.CompressFormat.JPEG, 50, fs);
    Log.i(TAG, "onCreate: file.length " + file.length());
    fs.flush();
    fs.close();
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}
//查看壓縮以後的 Bitmap 大小
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, outputStream);
byte[] bytes = outputStream.toByteArray();
Bitmap compress = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
Log.i(TAG, "onCreate: bitmap.size = " + bitmap.getByteCount() + "   compress.size = " + compress.getByteCount());

首先,咱們來看看 quality 參數被設置爲 50,質量壓縮先後的圖片對比,能夠看到其尺寸大小並無變化,可是視覺感覺也能夠明顯地看到圖片變的模糊了一些。


 

經過日誌也能夠看到,在質量壓縮先後圖片轉成 Bitmap 以後在內存中的大小也並無變化,這是在保持像素的前提下,改變圖片的位深及透明度等:

//壓縮以後圖片佔用的存儲體積
compress.length = 7814
//在內存中壓縮先後圖片佔用的大小
bitmap.size = 350000   compress.size = 350000

對比兩者,保存前的圖片存儲體積是 106k,質量設爲 50 而且保存爲 JPEG 格式以後,圖片存儲大小就只有 8k 了,而且質量設的越低,保存成文件以後,文件的體積也就越小。

3、Android Skia 圖像引擎

在上文中,提到的Skia是Android 的重要組成部分。

Skia 是一個 Google 本身維護的 c++ 實現的圖像引擎,實現了各類圖像處理功能,而且普遍地應用於谷歌本身和其它公司的產品中(如:Chrome、Firefox、 Android等),基於它能夠很方便爲操做系統、瀏覽器等開發圖像處理功能。

Skia 在 Android 中提供了基本的畫圖和簡單的編解碼功能,能夠掛接其餘的第三方編碼解碼庫或者硬件編解碼庫,例如 libpng 和 libjpeg,libgif 等等。所以,這個函數調用bitmap.compress(Bitmap.CompressFormat.JPEG...),實際會調用 libjpeg.so 動態庫進行編碼壓縮。

最終 Android 編碼保存圖片的邏輯是 Java 層函數→Native 函數→Skia函數→對應第三庫函數(例如 libjpeg)。因此 skia 就像一個膠水層,用來連接各類第三方編解碼庫,不過 Android 也會對這些庫作一些修改,好比修改內存管理的方式等等。

Android 在以前從某種程度來講使用的算是 libjpeg 的功能閹割版,壓縮圖片默認使用的是 standard huffman,而不是 optimized huffman,也就是說使用的是默認的哈夫曼表,並無根據實際圖片去計算相對應的哈夫曼表,Google 在初期考慮到手機的性能瓶頸,計算圖片權重這個階段很是佔用 CPU 資源的同時也很是耗時,由於此時須要計算圖片全部像素 argb 的權重,這也是 Android 的圖片壓縮率對比 iOS 來講差了一些的緣由之一。

4、圖像壓縮與 Huffman 算法

這裏簡單介紹一下哈夫曼算法,哈夫曼算法是在多媒體處理裏經常使用的算法之一。好比一個文件中可能會出現五個值 a,b,c,d,e,它們用二進制表達是:

a. 1010
b. 1011
c. 1100
d. 1101
e. 1110

咱們能夠看到,最前面的一位數字是 1,實際上是浪費掉了,在定長算法下最優的表達式爲:

a. 010
b. 011
c. 100
d. 101
e. 110

這樣咱們就能作到節省一位的損耗,那哈夫曼算法比起定長算法改進的地方在哪裏呢?在哈夫曼算法中咱們能夠給信息賦予權重,即爲信息加權,假設 a 佔據了 60%,b 佔據了 20%, c 佔據了 20%,d,e 都是 0%:

a:010 (60%)
b:011 (20%)
c:100 (20%)
d:101 (0%)
e:110 (0%)

在這種狀況下,咱們可使用哈夫曼樹算法再次優化爲:

a:1
b:01
c:00

因此思路固然就是出現頻率高的字母使用短碼,對出現頻率低的使用長碼,不出現的直接就去掉,最後 abcde 的哈夫曼編碼就對應:1 01 00
經過權重對應生成的的哈夫曼表爲:

定長編碼下的abcde:010 011 100 101 110,使用哈夫曼樹加權後的編碼則爲 1 01 00,這就是哈夫曼算法的總體思路(關於算法的詳細介紹能夠去查閱相關資料)。

因此這個算法一個很重要的思路是必須知道每個元素出現的權重,若是咱們可以知道每個元素的權重,那麼就可以根據權重動態生成一個最優的哈夫曼表。

可是怎麼去獲取每個元素,對於圖片就是每個像素中 argb 的權重呢,只能去循環整個圖片的像素信息,這無疑是很是消耗性能的,因此早期 android 就使用了默認的哈夫曼表進行圖片壓縮。

5、libjpeg 與 optimize_coding

libjpeg 在壓縮圖像時,有一個參數叫 optimize_coding,關於這個參數,libjpeg.doc 有以下解釋:

TRUE causes the compressor to compute optimal Huffman coding tables
for the image. This requires an extra pass over the data and
therefore costs a good deal of space and time. The default is
FALSE, which tells the compressor to use the supplied or default
Huffman tables. In most cases optimal tables save only a few percent
of file size compared to the default tables. Note that when this is
TRUE, you need not supply Huffman tables at all, and any you do
supply will be overwritten.

由上可知,若是設置 optimize_coding 爲 TRUE,將會使得壓縮圖像過程當中,會先基於圖像數據計算哈弗曼表。因爲這個計算會顯著消耗空間和時間,默認值被設置爲 FALSE。

那麼 optimize_coding 參數的影響究竟會有多大呢?查閱一些博客資料介紹,使用相同的原始圖片,分別設置 optimize_coding=TRUE 和 FALSE 進行壓縮,發現 FALSE 時的圖片大小大約是 TRUE 時的 5-10 倍。換言之就是相同文件體積的圖片,不使用哈夫曼編碼圖片質量會比使用哈夫曼低 5-10 倍。

關於這個差別咱們再去查閱其餘資料,發現有兩篇討論很是熱烈:Investigate using 「optimize_coding」 when encoding to JPEG,About libjpeg optimize_coding,甚至Skia 的官方人員也參與了討論,他據此測試了兩組數據:

sample image 1 (RGB gradients):
default (80): 2.5x slower, 34% smaller
quality 0: 1.7x slower, 52% smaller
quality 20: 2.1x slower, 55% smaller
quality 40: 2.3x slower, 37% smaller
quality 60: 2.5x slower, 36% smaller
quality 100: 3.9x slower, 22% smaller

sample image 2 (photo):
default (80): 2x slower, 8% smaller
quality 0: 1.5x slower, 49% smaller
quality 20: 1.7x slower, 22% smaller
quality 40: 1.9x slower, 15% smaller
quality 60: 1.9x slower, 11% smaller
quality 100: 2x slower, 9% smaller

能夠看到效果並非 5-10 倍的體積差距,最多也就在 2 倍而已,有國人也測試了一下,結果一致:JPEG Optimized Huffman。

儘管如此,社區裏對此的疑慮並無完全打消,最終,官方人員修改了這個默認的實現:skia / skia.git / 0a35620a16b368356888d15771392fb00cbb777d(https://skia.googlesource.com/skia.git/+/0a35620a16b368356888d15771392fb00cbb777d ) 。在 SkImageDecoder_libjpeg.cpp 文件中給 optimize_code 賦值了一個默認值 TRUE。

6、Android 與 optimize_coding

那麼在 Android 中有沒有使用哈夫曼變長編碼呢?查閱了 7.0 源碼,以下:

/* Use Huffman coding, not arithmetic coding, by default */
cinfo->arith_code = FALSE;

能夠看到註釋裏面很清楚,默認是哈夫曼變長編碼,而不是算數編碼。同時去查閱 14 年時的 Android 4.4 源碼,發現依舊如此。

對於optimize_coding,早期的 Android 考慮到性能瓶頸,將其設置爲 FALSE。可是,如今 Android 手機性能比之前好不少,因此目前性能每每不是瓶頸,時間和壓縮質量反而成爲更重要的指標了。爲此,Google 在 Android 7.0 版本左右,也作了相應修改,如 7.0 和 6.0 源碼所示:

7、Android JPEG VS. iOS JPEG

通過上面的介紹你們應該瞭解了爲何 Android 的 JPEG 圖片壓縮率會比 iOS 小一些,那麼還有另外一個問題就是爲何同一張 PNG 圖片設置成一樣的壓縮質量壓縮成 JPEG 以後,Android 輸出的圖像質量會比 iOS 差一些呢,通過相關資料的查找,發現形成這個結果有兩方面的因素。

第一個因素是 JPEG 編碼過程當中有一個步驟是顏色空間 RGB -> YUV 的轉換,以前的 Android 版本一樣考慮到性能問題,skia 引擎寫了一個函數替代了原來 libjpeg 的轉換函數,好處是提升了編碼速度,壞處就是犧牲了每個像素的精度。

第二個因素是離散餘弦變換有三種方式,Skia 引擎選擇了 JDCT_IFAST,JDCT_IFAST 是最快的變換方式,固然也是精度最差的一種。

上面兩種因素第一個會形成色調誤差,第二個會形成色塊的出現,因此若是須要提升壓縮以後的圖像質量,能夠考慮從這兩方面入手。

8、總結

首先,從 Android 7.0 版本開始,optimize_code 標示已經設置爲了 TRUE,也就是默認使用圖像生成哈夫曼表,而不是使用默認哈夫曼表。而至於這個標誌所產生的體積差距也沒有 5-10 倍那麼大,大約能夠在原圖的基礎上縮小 10%~50% 的體積,通過修改先後不一樣 Android 版本實測,數據吻合。

其次,如何提升 Android 的壓縮率,這裏須要提到兩個庫,一個是 mozilla/mozjpeg,另外一個是 libjpeg-turbo,前者是一個來自 Mozilla 實驗室的 JPEG 圖像編碼器項目,目標是在不下降圖像質量且兼容主流的解碼器的狀況下,提供產品級的 JPEG 格式編碼器來提升壓縮率以減少 JPEG 文件的大小,後者至關因而一個 libjpeg 的加強版,前者也是基於後者,在後者的基礎上進行了一些優化。

因此想要提高圖片壓縮率的能夠從這兩個庫着手,網上資料也很多,後續有機會能夠測試一下這兩個庫,而後給你們分享一下。
  
最後,編碼方式除了哈夫曼以外,還有定長的算術編碼,這個算法的詳細介紹你們能夠網上查閱一下。對比哈夫曼編碼和算術編碼,網上相關資料顯示算術編碼在壓縮 jpeg 方面能夠比哈夫曼編碼體積小 5%~12%,因此須要提高圖片壓縮率的一樣也能夠嘗試從切換成算術編碼這方面入手。

9、參考

  1. 爲何Android的圖片質量會比iPhone的差?(http://blog.sina.com.cn/s/blog_12ce70a430102v1p3.html )
  2. JPEG arithmetic coding(http://www.rw-designer.com/entry/1311 )
  3. Comparison Arithmetic Coding versus Huffman(http://www.binaryessence.com/dct/en000134.htm )
  4. Investigate using "optimize_coding" when encoding to JPEG(https://bugs.chromium.org/p/skia/issues/detail?id=3460 )
  5. About libjpeg optimize_coding(https://groups.google.com/forum/#!topic/skia-discuss/p0IcyBoU8P0 )

 

相關閱讀

 
此文已由做者受權騰訊雲技術社區發佈,轉載請註明文章出處
原文連接:https://cloud.tencent.com/community/article/427566
相關文章
相關標籤/搜索