封裝個 Android 的高斯模糊元件
最近基於 ofollow,noindex" target="_blank">Android StackBlur 開源庫,根據自己碰到的需求場景,封裝了個高斯模糊元件,順便記錄一下。
為什麼要自己重複造輪子?
其實也談不上重頭自己造輪子,畢竟是基於大神的開源庫,做了二次封裝。封裝的目的在於,方便外部使用。畢竟有著自己的程式設計習慣,大神的開源庫也只是提供了基礎的功能,現實程式設計中,產品的需求是各種各樣的。
導致每次使用時,都蠻麻煩的,需要額外自己處理蠻多東西。而一旦新的專案又需要接入高斯模糊了,又得重新寫一些程式碼,複製貼上也麻煩,經常由於各種業務耦合報錯。
既然如此,乾脆花時間抽個基礎、公用的高斯模糊元件,需要時直接依賴即可。
基礎理論
高斯模糊
高斯模糊的原理和演算法就不介紹了,我也不懂,沒深入,這裡就大概講講我的粗坯理解:
我們知道,一張圖片,本質上其實是一個個畫素點構成的,雖然經過計算機處理後,呈現在我們眼前的是具體的影象。但在計算機中,其實就是一堆陣列資料。
陣列中每個單位就是一個個畫素點,那麼每個畫素點是儲存什麼內容呢,其實也就是 RGB 或者 ARGB 之類格式的資料。
高斯模糊,大體上就是對這張圖片中的每個畫素點都重新進行計算,每個畫素點的新值等於以它為中心,半徑為 r 的周圍區域內所有畫素點各自按照不同權重相加之和的平均值。
可以粗坯的理解為, 本來這個畫素點是要呈現它自己本身的內容,但經過高斯模糊計算後,摻雜進它周圍區域畫素點的內容了。就像加水稀釋類似的道理,既然都摻雜進周圍的內容了,那麼它呈現的內容相比最初,肯定就不那麼清晰了 。
而如果摻雜的半徑越大,混合進的內容也就越多,那麼它本身的內容就越淡了,是不是這個理。所以,這就是為什麼每個開源的高斯模糊元件庫,使用時基本都需要我們傳入一個 radius 半徑的引數。而且,半徑越大,越模糊。
這麼一粗坯的解釋,就理解多了,是吧。
為什麼需要大概掌握這個理論基礎呢?
想想,高斯模糊是遍歷所有畫素點,對每個畫素點都重新計算。那麼,這自然是一個耗時的工作,掌握了理論基礎,我們要優化時也才有方向去優化。
效能對比
大神的開源庫中提供了三種高斯模糊方式,而我在 Blankj 的 AndroidUtilCode 開源庫中發現了另外一種,所以我將他們都整合起來,一共有四種:
- Google 官方提供的 RenderScript 方式 (RSBlur)
- C 編寫的高斯演算法 blur.c 方式 (NativeBlur)
- Java 編寫的高斯演算法方式1(JavaBlur)
- Java 編寫的高斯演算法方式2(StackBlur)
其實,大體上就三種:Google 官方提供的,大神用 C 寫的高斯模糊演算法,大神用 Java 寫的高斯模糊演算法。至於後面兩種,看了下,演算法的程式碼不一樣,我就把它們當做是兩種不同的演算法實現了。也許是一樣,但我沒深入去看,反正程式碼不一樣,我就這麼認為了。
下面我們來做個實驗,在如下相同條件下,不同高斯模糊方案的耗時比較:
- radius=10, scale=1, bitmap=200*200(11.69KB)
ps: radius 表示高斯演算法計算過程中的半徑取值,scale=1表示對 bitmap 原圖進行高斯模糊。這些前提條件需要了解一下,不然你在看網上其他類似效能比對的文章時,發現它們動不動就優化到幾 ms 級別的,然而你自己嘗試卻始終達不到。這是因為也許所使用的這些前提都不一致,不一致的前提下,耗時根本無從比較。
private void testBlur() { int sum = 0; for (int i = 0; i < 100; i++) { long time = SystemClock.uptimeMillis(); DBlur.source(this, R.drawable.image).modeRs().radius(10).sampling(1).build().doBlurSync(); long end = SystemClock.uptimeMillis(); sum += (end - time); } Log.e("DBlur", "RSBlur cast " + (sum/100) + "ms"); }
程式碼模板如上,分別執行 100 次後取平均值,四種不同方式的耗時如下表:
前提條件 | RSBlur | NativeBlur | JavaBlur | StackBlur |
---|---|---|---|---|
radius= 10 , scale=1, bitmap=200*200 | 51ms | 13ms | 162ms | 384ms |
radius= 20 , scale=1, bitmap=200*200 | 56ms | 12ms | 164ms | 435ms |
radius=10, scale= 2 , bitmap=200*200 | 48ms | 11ms | 75ms | 110ms |
radius=10, scale= 8 , bitmap=200*200 | 45ms | 7ms | 14ms | 19ms |
radius=10, scale=8, bitmap= 1920*1180 | 183ms | 143ms | 346ms | 460ms |
radius= 20 , scale=8, bitmap=1920*1180 | 204ms | 145ms | 353ms | 510ms |
radius=20, scale= 1 , bitmap=1920*1180 | 474ms | 444ms | 8663ms | 記憶體溢位 |
100 次樣本可能不多,但大體上我們也能比較出不同型別的高斯模糊之前的區別,及其適用場景:
- 總體上,NativeBlur 和 RSBlur 的耗時會少於 JavaBlur 和 StackBlur
- JavaBlur 和 StackBlur 方式,如果先對 Bitmap 進行縮小,再高斯模糊,最後再放大,耗時會大大縮短
- radius 增大會增加耗時,但影響不大,但檢視呈現效果會越模糊
- scale 對原圖縮小倍數越多,耗時越短,但檢視呈現效果同樣會越模糊
- 解析度越高的圖片,高斯模糊的就越耗時
- 對於大圖而言,如果要使用 JavaBlur 或 StackBlur,最好設定 scale 先縮小再模糊,否則將非常耗時且容易記憶體溢位
- 如果已經通過 scale 方式進行優化,那麼最好 radius 值可以相對小一點,否則兩者的值都大會對圖片的模糊效果特別強烈,也許會過了頭
效能優化
高斯模糊的優化考慮點,其實就三個:
- 選擇不同的高斯模糊方式
- 通過 scale 對原圖先縮小,再模糊,最後再放大方式
- 優化高斯模糊演算法
最後一點就不考慮了,畢竟難度太大。那麼,其實就剩下兩種,要麼是從高斯模糊的方案上選擇,要麼從待模糊的圖片上做手腳。
雖然有四種高斯模糊方案,但每種都有各自優缺點:
- RSBlur 在低端機上可能無法相容
- NativeBlur 需要生成對應 CPU 架構的 so 檔案支援
- JavaBlur 和 StackBlur 耗時會較長
優化的考慮點大體上這幾種:
- 大體上,使用 NativeBlur 或者 RSBlur 即可,如果出現一些問題,那麼此時可考慮切到 JavaBlur 或 StackBlur 方案,但記得結合 scale 方式優化處理。
- 如果高斯模糊的圖片有實時性要求,要求模糊得同步進行處理,主執行緒後續的工作需要等待高斯模糊後才能夠處理的話,那麼儘量選擇 scale 方式進行優化,減少耗時。
- 如果對實時性沒要求,但對圖片模糊程度有要求,那其實,只要後臺非同步去進行高斯模糊即可,此時 scale 可不用縮小太多,而利用 radius 來控制模糊效果,以達到理想的要求。
- 如果兩者都有要求,那就自行嘗試尋找折中點吧。
最後說一點,因為已經封裝成元件庫了,RSBlur 也是引入的 support 包,so 檔案也打包好了,那麼使用這兩種方案足夠滿足絕大部分場景了,所以,沒有特意指定,元件預設的方案為 RSBlur。
二次封裝
需求場景
為什麼要二次封裝?那肯定是因為有自己的各種需求場景的,我的需求如下:
- 要能夠對當前頁面(Activity)截圖後,進行模糊
- 要能夠對 drawable 資源圖片進行模糊、或者對指定 View 的檢視進行模糊
- 模糊完成後,要能夠自動以淡入的動畫方式顯示在指定的控制元件上
- 存在這種需求場景:對當前介面截圖、並且模糊,模糊後的圖片展示的時機可能在其他介面,因此需要支援快取功能,可以根據指定 cacheKey 值獲取快取
- 當然,可以根據各種配置使用高斯模糊,當不指定配置時,有預設配置
總結一下,其實封裝要做的事也就是要實現:
- 截圖、快取、淡入動畫、預設配置
- 可以的話,元件最好可以達到,其他人在不看文件,不看原始碼前提下,以最少的成本接入直接上手使用
實現
截圖、快取、動畫這些都屬於純功能程式碼的封裝了,具體就不說了。
這裡想來講講,如何設計,可以讓其他人以最少的成本接入直接上手使用。
我的想法是,利用 AndroidStudio 的程式碼提示功能,具體的說就是,你只需要瞭解元件的入口是 DBlur 即可,至於後續怎麼使用,全靠 AndroidStudio 來提示,跟著 AndroidStudio 走就行。
例如:
當敲完 DBlur. 時,會彈出程式碼提示框,入口很少,是吧,就兩個,看命名也能猜到作用: getCacheBitmap()
明顯是用來取快取的,那麼要高斯模糊自然是另外一個入口 source()
,這個方法有多個過載函式,看引數,其實也能知道,這就對應著要模糊的圖片的不同來源型別,如:
- 直接傳入 Bitmap 對其進行模糊
- 傳入 Activity/View,內部會對這個介面/控制元件進行截圖後再模糊
- 傳入 resId,對 drawable 資源圖片進行模糊
那麼,可能想問了,哪裡進行高斯模糊配置,哪裡設定同步或非同步,哪裡註冊回撥等等。別急,既然只給你開了一個入口,那麼就跟著入口走下去,自然會一步步引導你走到最後。如:
第一步、第二步該做什麼,我都給你規定好了,你也只能按照步驟一步步來。想要設定高斯模糊配置,你得先指定圖片來源,才能進入第二步,在這裡,可以進行的配置也都給你列出來了,想要哪個,直接設定即可。如:
-
mode()
,modeRs()
,modeNative()
等等類似 mode 開頭的方法,用於指定要使用哪種高斯模糊方案,一共四種,每種內部都有提供對應的常量標誌,但如果你不知道哪裡找,那麼直接呼叫 modeXXX 方法即可。 -
radius()
用於設定高斯模糊計算的半徑,內部預設為 4。 -
sampling()
用於設定對原圖的縮小比例,內部預設為 8,即預設先縮小 8 倍,再模糊,最後再放大。 -
cache()
用於設定快取此次模糊後的圖片,沒有呼叫預設不快取。 -
animAlpha()
用於設定使用淡入動畫,需要結合intoTarget()
使用,否則不生效。 -
intoTarget()
用於設定模糊完成後,自動顯示到指定控制元件上。
另外,看每個方法返回的類名,其實這個過程都是在設定配置項,如果有對 Builder 模式瞭解的話,應該清楚,這個大多用來解決建構函式引數過多的場景,最後一般都會有一個 build()
或者 create()
型別的方法。參考的是 Android 原始碼中 AlertDialog。
也就是說,要進入下一個步驟,需要呼叫 build()
方法,如:
顯然,已經到最後一個步驟了,這裡就是發起高斯模糊工作的地方了。
doBlur() doBlurSync()
至此,接入結束。
使用這個高斯模糊的元件,只需要知道 DBlur 入口,其他都跟隨著 AndroidStudio 程式碼提示一步步往下走即可,當然你也可以直接看原始碼,註釋裡也寫得蠻清楚的了。
這個就是我的想法,能力不足,只能想出這種方案,如果有哪裡需要改進,哪裡不合理,或者有其他思路,歡迎指點一下。
使用示例
compile 'com.dasu.image:blur:0.0.4'
//使用預設配置,最短呼叫鏈 Bitmap bitmap = DBlur.source(MainActivity.this).build().doBlurSync(); //同步模糊,將imageView控制的檢視進行模糊,完成後自動顯示到 imageView1 控制元件上,以淡入動畫方式 DBlur.source(imageView).intoTarget(imageView1).animAlpha().build().doBlurSync(); //非同步模糊,將drawable資原始檔中的圖片以 NATIVE 方式進行模糊,註冊回撥,完成時手動顯示到 imageView1 控制元件上 DBlur.source(this, R.drawable.background).mode(BlurConfig.MODE_NATIVE).build() .doBlur(new OnBlurListener() { @Override public void onBlurSuccess(Bitmap bitmap) { imageView1.setImageBitmap(bitmap); } @Override public void onBlurFailed() { //do something }});
Github
DBlur 的 Github 連結:https://github.com/woshidasusu/base-module/tree/master/blur
大家好,我是 dasu,歡迎關注我的公眾號(dasuAndroidTv),如果你覺得本篇內容有幫助到你,可以轉載但記得要關注,要標明原文哦,謝謝支援~
