高斯模糊實現方案探究

現在越來越多的app在背景圖中使用高斯模糊效果,如yahoo天氣,效果做得很炫。 這裡就用一個demo來談談它的不同實現方式及各自的優缺點。

1. RenderScript

談到高斯模糊,第一個想到的就是RenderScript。RenderScript是由Android3.0引入,用來在Android上編寫高效能程式碼的一種語言(使用C99標準)。 引用官方文件的描述:

RenderScript runtime will parallelize work across all processors available on a device, such as multi-core CPUs, GPUs, or DSPs, allowing you to focus on expressing algorithms rather than scheduling work or load balancing.

為了在Android中使用RenderScript,我們需要(直接貼官方文件,比直譯更通俗易懂):

  • High-performance compute kernels are written in a C99-derived language.
  • A Java API is used for managing the lifetime of RenderScript resources and controlling kernel execution.

上面兩點總結成一句話為:我們需要一組compute kernels(.rs檔案中編寫),及一組用於控制renderScript相關的java api(.rs檔案自動生成為java類)。 由於compute kernels的編寫需要一定的學習成本,從JELLY_BEAN_MR1開始,Androied內建了一些compute kernels用於常用的操作,其中就包括了Gaussian blur

下面,通過實操來講解一下RenderScript來實現高斯模糊,最終實現效果(講文字背景進行模糊處理):

佈局:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >

    <ImageView 
        android:id="@+id/picture" 
        android:layout_width="match_parent" 
        android:layout_height="match_parent" 
        android:src="@drawable/splash" 
        android:scaleType="centerCrop" />

    <TextView 
        android:id="@+id/text"
        android:gravity="center_horizontal" 
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Gaussian Blur"
        android:textColor="@android:color/black"
        android:layout_gravity="center_vertical"
        android:textStyle="bold"
        android:textSize="48sp" />

    <LinearLayout 
        android:id="@+id/controls" 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content" 
        android:background="#7f000000" 
        android:orientation="vertical"
        android:layout_gravity="bottom" />
</FrameLayout>

核心程式碼:

private void applyBlur() {
    image.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {

        @Override
        public boolean onPreDraw() {
            image.getViewTreeObserver().removeOnPreDrawListener(this);
            image.buildDrawingCache();
            Bitmap bmp = image.getDrawingCache();
            blur(bmp, text, true);
            return true;
        }
    });
}

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
private void blur(Bitmap bkg, View view) {
    long startMs = System.currentTimeMillis();
    float radius = 20;

    Bitmap overlay = Bitmap.createBitmap((int)(view.getMeasuredWidth()), (int)(view.getMeasuredHeight()), Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(overlay);
    canvas.translate(-view.getLeft(), -view.getTop());
    canvas.drawBitmap(bkg, 0, 0, null);

    RenderScript rs = RenderScript.create(SecondActivity.this);

    Allocation overlayAlloc = Allocation.createFromBitmap(rs, overlay);
    ScriptIntrinsicBlur blur = ScriptIntrinsicBlur.create(rs, overlayAlloc.getElement());
    blur.setInput(overlayAlloc);
    blur.setRadius(radius);
    blur.forEach(overlayAlloc);
    overlayAlloc.copyTo(overlay);
    view.setBackground(new BitmapDrawable(getResources(), overlay));
    rs.destroy();

    statusText.setText("cost " + (System.currentTimeMillis() - startMs) + "ms");
}

當ImageView開始載入背景圖時,取出它的drawableCache,進行blur處理,Gaussian blur的主要邏輯在blur函式中。對於在Java中使用RenderScript,文件中也有詳細描述,對應到我們的程式碼,步驟為:

  • 初始化一個RenderScript Context.
  • 至少建立一個Allocation物件用於儲存需要處理的資料.
  • 建立compute kernel的例項,本例中是內建的ScriptIntrinsicBlur物件.
  • 設定ScriptIntrinsicBlur例項的相關屬性,包括Allocation, radius等.
  • 開始blur操作,對應(forEach).
  • 將blur後的結果拷貝回bitmap中。

此時,我們便得到了一個經過高斯模糊的bitmap。

從上圖可以看到,模糊處理花費了38ms(測試機為小米2s),由於Android假設每一幀的處理時間不能超過16ms(螢幕重新整理頻率60fps),因此,若在主執行緒裡執行RenderScript操作,可能會造成卡頓現象。最好的方式是將其放入AsyncTask中執行。

此外,RenderScript在3.0引入,而一些內建的compute kernelJELLY_BEAN_MR1中引入,為了在低版本手機中使用這些特性,我們不得不引入renderscript_v8相容包,對於手Q安裝包增量的硬性指標,貌似只能放棄JELLY_BEAN_MR1以下的使用者?

有點不甘心,想想別的解決方案吧。

2. FastBlur

由於高斯模糊歸根結底是畫素點的操作,也許在java層可以直接操作畫素點來進行模糊化處理。google一下,果不其然,一個名為stackblur的開源專案提供了名為fastBlur的方法在java層直接進行高斯模糊處理。

ok,現在來改造我們的程式.

private void blur(Bitmap bkg, View view) {
    long startMs = System.currentTimeMillis();
    float radius = 20;

    Bitmap overlay = Bitmap.createBitmap((int)(view.getMeasuredWidth()), (int)(view.getMeasuredHeight()), Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(overlay);
    canvas.translate(-view.getLeft(), -view.getTop());
    canvas.drawBitmap(bkg, 0, 0, null);
    overlay = FastBlur.doBlur(overlay, (int)radius, true);
    view.setBackground(new BitmapDrawable(getResources(), overlay));
    statusText.setText("cost " + (System.currentTimeMillis() - startMs) + "ms");
}  

這裡,僅僅是把RenderScript相關的操作換成了FastBlur提供的api。效果圖如下:

效果還不錯,與RenderScript的實現差不多,但花費的時間卻整整多了2倍多,這完全是無法接受的。好吧,只能繼續探究。

3. AdvancedFastBlur

stackOverflow對於程式設計師來說永遠是最大的寶藏。http://stackoverflow.com/questions/2067955/fast-bitmap-blur-for-android-sdk這篇提問帖終於提供了新的解決思路:

This is a shot in the dark, but you might try shrinking the image and then enlarging it again. This can be done with Bitmap.createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter). Make sure and set the filter parameter to true. It'll run in native code so it might be faster.

它所表述的原理為先通過縮小圖片,使其丟失一些畫素點,接著進行模糊化處理,然後再放大到原來尺寸。由於圖片縮小後再進行模糊處理,需要處理的畫素點和半徑都變小,從而使得模糊處理速度加快。 瞭解原理,繼續改善:

private void blur(Bitmap bkg, View view) {
    long startMs = System.currentTimeMillis();
    float radius = 2;
    float scaleFactor = 8;

    Bitmap overlay = Bitmap.createBitmap((int)(view.getMeasuredWidth()/scaleFactor), (int)(view.getMeasuredHeight()/scaleFactor), Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(overlay);
    canvas.translate(-view.getLeft()/scaleFactor, -view.getTop()/scaleFactor);
    canvas.scale(1 / scaleFactor, 1 / scaleFactor);
    Paint paint = new Paint();
    paint.setFlags(Paint.FILTER_BITMAP_FLAG);
    canvas.drawBitmap(bkg, 0, 0, paint);
    overlay = FastBlur.doBlur(overlay, (int)radius, true);
    view.setBackground(new BitmapDrawable(getResources(), overlay));
    statusText.setText("cost " + (System.currentTimeMillis() - startMs) + "ms");
} 

最新的程式碼所建立的bitmap為原圖的1/8大小,接著,同樣使用fastBlur來進行模糊化處理,最後再為textview設定背景,此時,背景圖會自動放大到初始大小。注意,由於這裡進行了縮放,radius的取值也要比之前小得多(這裡將原始取值除以8得到近似值2)。下面是效果圖:

驚呆了有木有!!效果一樣,處理速度卻快得驚人。它相對於renderScript方案來說,節省了拷貝bitmap到Allocation中,處理完後再拷貝回來的時間開銷。

4. Warning

由於FastBlur是將整個bitmap拷貝到一個臨時的buffer中進行畫素點操作,因此,它不適合處理一些過大的背景圖(很容導致OOM有木有~)。對於開發者來說,RenderScript方案和FastBlur方案的選擇,需要你根據具體業務來衡量!

.