Android中圖片壓縮分析(上)

此文章首發:

https://mp.weixin.qq.com/s/QZ-XTsO7WnNvpnbr3DWQmg

一、前言

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

前者是在不改變圖片尺寸的情況下,改變圖片的存儲體積,而后者則是降低圖像尺寸,達到相同目的。

由于本文的篇幅問題,分為上下兩篇發布。

二、Android 質量壓縮邏輯

在Android中,對圖片進行質量壓縮,通常我們的實現方式如下所示:

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

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

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

由于項目原因和兼容性選擇了JPEG,因此接下來的分析也將是圍繞 JPEG 展開。

將 PNG 圖片轉成 JPEG 格式之后不會降低這個圖片的尺寸,但是會降低視覺質量,從而降低存儲體積。同時,由于尺寸不變,所以將這個圖片解碼成相同色彩模式的 bitmap 之后,占用的內存大小和壓縮前是一樣的。

回到最初的代碼示例,函數 compress 經過一連串的 java 層調用之后,最后來到了一個 native 函數,如下:

//Bitmap.cppstatic 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,質量壓縮前后的圖片對比,可以看到其尺寸大小并沒有變化,但是圖片從 PNG 格式轉成 JPEG 之后會丟失透明度,視覺感受也可以明顯地看到圖片變的模糊了一些。

Android中圖片壓縮分析(上)的圖1壓縮前.png

 


Android中圖片壓縮分析(上)的圖2壓縮后.jpg

通過日志也可以看到,在質量壓縮前后圖片轉成 Bitmap 之后在內存中的大小也并沒有變化,這是在保持像素的前提下,改變圖片的位深及透明度等:

//壓縮之后圖片占用的存儲體積compress.length = 7814//在內存中壓縮前后圖片占用的大小bitmap.size = 350000   compress.size = 350000

對比二者,保存前的圖片存儲體積是 106k,質量設為 50 并且保存為 JPEG 格式之后,圖片存儲大小就只有 8k 了,并且質量設的越低,保存成文件之后,文件的體積也就越小。

三、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 的圖片壓縮率不是特別高的原因之一。

四、圖像壓縮與 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
通過權重對應生成的的哈夫曼表為:

Android中圖片壓縮分析(上)的圖3image

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

所以這個算法一個很重要的思路是必須知道每一個元素出現的權重,如果我們能夠知道每一個元素的權重,那么就能夠根據權重動態生成一個最優的哈夫曼表。

但是怎么去獲取每一個元素,對于圖片就是每一個像素中 argb 的權重呢,只能去循環整個圖片的像素信息,這無疑是非常消耗性能的,所以早期 android 就使用了默認的哈夫曼表進行圖片壓縮。

五、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 JPEGAbout 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。在 SkImageDecoder_libjpeg.cpp 文件中給 optimize_code 賦值了一個默認值 TRUE。

六、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 源碼所示:

Android中圖片壓縮分析(上)的圖47.0源碼Android中圖片壓縮分析(上)的圖56.0源碼

七、Android JPEG VS. iOS JPEG

經過上面的介紹大家應該了解了為什么早期 Android 的 JPEG 圖片壓縮率為什么會差一些,那么還有另一個問題就是為什么同一張 PNG 圖片設置成同樣的壓縮質量壓縮成 JPEG 之后,Android 輸出的圖像質量會比 iOS 差一些呢,經過相關資料的查找,發現造成這個結果有兩方面的因素。

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

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

上面兩種因素第一個會造成色調偏差,第二個會造成色塊的出現,所以如果需要提高壓縮之后的圖像質量,可以考慮從這兩方面入手。

八、總結

首先,從 Android 7.0 版本開始,optimize_code 標示已經設置為了 TRUE,也就是默認使用圖像生成哈夫曼表,而不是使用默認哈夫曼表。而至于這個標志所產生的體積差距也沒有 5-10 倍那么大,大約可以在原圖的基礎上縮小 10%~50% 的體積,經過修改前后不同 Android 版本實測,數據吻合。

其次,如何提高 Android 的壓縮率,這里需要提到兩個庫,一個是 mozilla/mozjpeg,另一個是 libjpeg-turbo,前者是一個來自 Mozilla 實驗室的 JPEG 圖像編碼器項目,目標是在不降低圖像質量且兼容主流的解碼器的情況下,提供產品級的 JPEG 格式編碼器來提高壓縮率以減小 JPEG 文件的大小,后者相當于是一個 libjpeg 的增強版,前者也是基于后者,在后者的基礎上進行了一些優化。

所以想要提升圖片壓縮率的可以從這兩個庫著手,網上資料也不少,后續有機會可以測試一下這兩個庫,然后給大家分享一下。
  
最后,編碼方式除了哈夫曼之外,還有定長的算術編碼,這個算法的詳細介紹大家可以網上查閱一下。對比哈夫曼編碼和算術編碼,網上相關資料顯示算術編碼在壓縮 jpeg 方面可以比哈夫曼編碼體積小 5%~12%,所以需要提升圖片壓縮率的同樣也可以嘗試從切換成算術編碼這方面入手。

九、參考

  1. 為什么Android的圖片質量會比iPhone的差?

  2. JPEG arithmetic coding

  3. Comparison Arithmetic Coding versus Huffman



作者:Shawn_Dut
來源:簡書

登錄后免費查看全文
立即登錄
App下載
技術鄰APP
工程師必備
  • 項目客服
  • 培訓客服
  • 平臺客服

TOP