1. 程式人生 > >Android 中圖片壓縮分析(上)

Android 中圖片壓縮分析(上)

歡迎大家前往騰訊雲社群,獲取更多騰訊海量技術實踐乾貨哦~

作者: shawnzhao

一、前言

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

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

由於本文的篇幅問題,分為上下兩篇釋出。

二、Android 質量壓縮邏輯

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

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
//quality 為01000表示最小體積,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.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 了,並且質量設的越低,儲存成檔案之後,檔案的體積也就越小。

三、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 來說差了一些的原因之一。

四、影象壓縮與 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 就使用了預設的哈夫曼表進行圖片壓縮。

五、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。

六、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 JPEG VS. iOS JPEG

經過上面的介紹大家應該瞭解了為什麼 Android 的 JPEG 圖片壓縮率會比 iOS 小一些,那麼還有另一個問題就是為什麼同一張 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%,所以需要提升圖片壓縮率的同樣也可以嘗試從切換成算術編碼這方面入手。

九、參考

閱讀推薦

相關推薦

Android圖片壓縮分析

一、前言 在 Android 中進行圖片壓縮是非常常見的開發場景,主要的壓縮方法有兩種:其一是質量壓縮,其二是下采樣壓縮。 前者是在不改變圖片尺寸的情況下,改變圖片的儲存體積,而後者則是降低影象尺寸,達到相同目的。 由於本文的篇幅問題,分為上下兩篇釋出

Android 圖片壓縮分析

歡迎大家前往騰訊雲社群,獲取更多騰訊海量技術實踐乾貨哦~ 作者: shawnzhao 一、前言 在 Android 中進行圖片壓縮是非常常見的開發場景,主要的壓縮方法有兩種:其一是質量壓縮,其二是下采樣壓縮。 前者是在不改變圖片尺寸的情況下,

Android圖片壓縮分析

一、Android 尺寸壓縮邏輯 針對圖片尺寸的修改其實就是一個影象重新取樣的過程,放大影象稱為上取樣(upsamping),縮小影象稱為下采樣(downsampling),這裡我們重點討論下采樣。 在 Android 中圖片重取樣提供了兩種方法,一種叫

Android RecyclerView工作原理分析

基本使用   RecyclerView的基本使用並不複雜,只需要提供一個RecyclerView.Apdater的實現用於處理資料集與ItemView的繫結關係,和一個RecyclerView.LayoutManager的實現用於 測量並佈局 ItemView

探索Android的Parcel機制

一.先從Serialize說起 我們都知道JAVA中的Serialize機制,譯成序列化、序列化……,其作用是能將資料物件存入位元組流當中,在需要時重新生成物件。主要應用是利用外部儲存裝置儲存物件狀態,

Java/Android的網路程式設計--

網路是20世紀最偉大的發明之一,眾多的裝置可以以有線或者無線的方式連入整個網際網路,進而互相通訊。為了更好的開發、管理、接入網路,科學家設計了通訊協議,將整個網路架構分為7層(4層),並規範了每一層的功能。 網路分層 早期的OSI參考模型將網路分為7層:應用層、表示層、會

android影象處理系列之四--給圖片新增邊框

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

Android開發知識Android事件處理機制:事件分發、傳遞、攔截、處理機制的原理分析

  在我們剛開始學習安卓的時候,總會一開始就接觸到Button,也就是對按鈕進行一個事件監聽的事件,當我們點選螢幕上的按鈕時就可以觸發一個點選事件。那麼,從我們點選螢幕到按鈕觸發事件這個過程,是什麼樣子的呢?本文我們就來談一下關於事件攔截處理機制的基本知識。

Android從零開搞系列:自定義View4基本的自定義ViewPager指示器+開源專案分析

基本的自定義ViewPager的指示器 當然關於ViewPager指示器,如果只需要簡潔大方,那麼我們最簡單的方案就是使用TabLayout+ViewPager。 當然咱們也有很多非常不錯的開源框架可以選擇。 本次的記錄的內容就是

Android如何在應用層進行截圖及截圖原始碼分析

最近在看framework層程式碼時發現其中有一個是測試截圖操作的專門的包,於是潛意識的驅使下就研究了這方面的知識,今天作個總結吧!以及我們在寫上層應用時如何做截圖操作的,那麼我們先來看看截圖的原始碼分析,其實截圖操作就java這部分是放在了系統SystemUI

Android-3】Android的任務棧Task

集合 情況下 清除 bsp 生命周期方法 任務棧 保存 sin 也會 一、Android任務棧 概述:Android中的任務棧其實就是Activity的集合,在Android中退出程序的時候必須把任務棧中的所有Activity清除出棧,此時才能安全的完全的退出程序, 任務棧

2018年Android面試題含答案

密碼學 進程的地址空間 變量 細節 一段時間 設備驅動 橋梁 異常 graph 這些面試題是我在今年年初換工作的時候整理,沒有重點。包括java基礎,數據結構,網絡,Android相關等等。適合中高級工程師。由於內容過多,將會分為上下兩部分。希望能夠幫到一些朋友,如果幫助到

50-C++對象模型分析

依次 分析 sin bsp get 本質 過程 ons 結構體 回歸本質 class是一種特殊的struct: ? 在內存中class依舊可以看作變量的集合 ? class與struct遵循相同的內存對其規則 ? class中的成員函數與成員變量是分開存放的:(1)

阿裏雲PolarDB及其共享存儲PolarFS技術實現分析

並發 存儲層 操作 先來 相關操作 關於 vld lan 負載均衡 PolarDB是阿裏雲基於MySQL推出的雲原生數據庫(Cloud Native Database)產品,通過將數據庫中計算和存儲分離,多個計算節點訪問同一份存儲數據的方式來解決目前MySQL數據庫存在的運

JavaJNI的使用

android immediate 返回值 str byte 文件 field 方式 touch JNI 全稱是 Java Native Interface。是在 Java 和 Native 層(包括但不限於C/C++)相互調用的接口規範。 JNI 在 Java 1.1中正

Android的對話方塊AlertDialog

建立android中分體式對話方塊需要四個步驟: 第一:獲得AlertDialog的靜態內部類Builder物件,有該類建立對話方塊。 第二:通過Builder物件設定對話方塊的標題,按鈕UI及將要響應的事件。、 第三:呼叫Builder的Create()方法建立對對話方塊 第四

C++筆記 第八課 函式過載分析---狄泰學院

如果在閱讀過程中發現有錯誤,望評論指正,希望大家一起學習,一起進步。 學習C++編譯環境:Linux 第八課 函式過載分析(上) 1.自然語言中的上下文 你知道下面詞彙中“洗”字的含義嗎? 結論:能和“洗”字搭配的詞彙有很多 “洗”字和不同的詞彙搭配有不同的含義 2.過

阿里雲PolarDB及其共享儲存PolarFS技術實現分析

PolarDB是阿里雲基於MySQL推出的雲原生資料庫(Cloud Native Database)產品,通過將資料庫中計算和儲存分離,多個計算節點訪問同一份儲存資料的方式來解決目前MySQL資料庫存在的運維和擴充套件性問題;通過引入RDMA和SPDK等新硬體來改造傳統的網路和IO協議棧來極大提升資料庫效能。

演算法複雜度分析分析演算法執行時,時間資源及空間資源的消耗

前言 演算法複雜度是指演算法在編寫成可執行程式後,執行時所需要的資源,資源包括時間資源和記憶體資源。 複雜度也叫漸進複雜度,包括時間複雜度和空間複雜度,用來粗略分析執行效率與資料規模之間的增長趨勢關係,越高階複雜度的演算法,執行效率越低。 複雜度分析是資料結構與演算法的核心精髓,指在不依賴硬體、宿主環境

第四章 語法分析——LL(1)文法

文章目錄 概述 LL(1)文法 LL(1)文法的判定 消除左遞迴 提取左公因子 First集合 Follow集合 預測分析表的構造 表驅動推導例項 概述 語法分析器是