Android RenderScript

上回泰勞介紹了處理圖片特效的方法,這回要來介紹如何提升效能,也許很多人第一時間會想到 JNI,那確實是個不錯的選擇,不過泰勞想介紹一個有點不一樣使用場合跟 JNI 不盡相同的 API,那就是 RenderScript!

參考資料:
[Developers]RenderScript Overview
[Developers]Type
[Developers]Element
[Developers]Allocation

使用 RenderScript 最大的優勢在於它可以實現「平行運算」,在擁有多核心處理器的裝置上速度一定會快上許多,同時它也支援 GPU。但因為是平行運算,有些場合是不能使用的,當然也有些場合特別適合,像是圖像處理就非常適合使用 RenderScript。

延續上一回的範例,這次改用 RenderScript 來實作負片效果,先準備一個簡單的 Context,作為主程式調用 RenderScript 的中繼站。

RenderScriptContext.java
public class RenderScriptContext {
  // for RenderScript
  private RenderScript rs;
  // ScriptC 和底線為格式所需,Invert 是我的 RS 腳本名稱
  private ScriptC_Invert scriptC;
  // 用於設定 RS 物件類型  private Type rType;
  // 將 Java 物件傳給 RS 處理的媒介
  private Allocation allocIn;
  private Allocation allocOut;
  //
  private Bitmap outputBitmap;

  public RenderScriptContext(Context context, int width, int height) {
    // 初始化 RS 的 Context
    rs = RenderScript.create(context);
    // 建立 ScriptC_Invert 的實例
    scriptC = new ScriptC_Invert(rs);
    // 定義元素類型與影像尺寸,這裡將元素類型設為 RGBA_8888
    rType = Type.createXY(rs, Element.RGBA_8888(rs), width, height);
    // 設定 Allocation 類型
    allocIn = Allocation.createTyped(rs, rType, Allocation.USAGE_SCRIPT);
    allocOut = Allocation.createTyped(rs, rType, Allocation.USAGE_SCRIPT);
    // 最後用於返回給主程式
    outputBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
  }

  public static void setPic(Bitmap bitmap) {
    // 將 Bitmap 轉為 Allocation
    allocIn = Allocation.createFromBitmap(rs, bitmap);
    // 將 allocIn 傳給 RS腳本
    // set_pic 的 pic 必須跟 RS 腳本裡宣告的物件名稱一樣

    scriptC.set_pic(allocIn);
  }

  public static Bitmap getPic() {
    // 執行 RS 腳本裡的 InvertBitmap 函式
    // allocOut 接收 RS 回傳的結果

    scriptC.forEach_InvertBitmap(allocOut);
    // 將 allocOut 轉回 Bitmap
    allocOut.copyTo(outputBitmap);
    return outputBitmap;
  }
}

接著是 RenderScript 腳本,它的語法、格式有點特別,泰勞一開始真的看不太習慣,時常懷疑這樣寫到底是不是對的。

Invert.rs
#pragma version(1)  // RenderScript 版本
#pragma rs java_package_name(JAVA)  // 調用此腳本的 package name 


// 物件名稱 pic,因此 Java 設定此物件時必須使用 set_pic 
rs_allocation pic;

uchar4 __attribute__((kernel)) InvertBitmap(uint32_t x,uint32_t y) {

  // 將 pic 拆解,uchar4 用於處理以 32 位元為單位的元素
  uchar4 color = rsGetElementAt_uchar4(pic, x, y);
  // 反轉顏色
  color.r = 255-color.r;
  color.g = 255-color.g;
  color.b = 255-color.b;

  color.a = color.a;
  // 返回
  return color;
}

最後於主程式透過 RenderScriptContext 將 Bitmap 傳給 RenderScript 腳本處理。
RenderScriptContext rsContext = new RenderScriptContext(MainActivity.this, mBitmap.getWidth(), mBitmap.getHeight());
rsContext.setPic(mBitmap);
imageView_after.setImageBitmap(rsContext.getPic());

特地找了一張色彩鮮明的圖片來進行測試,結果如下圖所示。

成功透過 RenderScript 實作負片效果,但還沒完,既然它主打的特點就是平行運算,當然要跟上回的方法做個速度比較,下方表格是泰勞使用 XZ1 Compact (硬體規格) 實際測試的結果(單位:毫秒)。

Java114142106134119133102125122107120.4
RenderScript9489919287898891989090.8

平均下來單純使用 Java 處理一次的時間是 120.4 毫秒,使用 RenderScript 處理一次的時間為 90.8 毫秒,看起來只快了一點,並沒有預期中那麼快。我們再試一次,先不管計算出來的效果如何,稍微讓計算的公式複雜一點,並且換上一張解析度為 5056x3792 的圖片,看看會不會有什麼不同,公式如下圖所示。(fac=0.003921, level=5)

第二次測試結果如下表所示。

Java258266267262265255264256254260260.7
RenderScript138137138142141131137136135134136.9

平均下來單純使用 Java 處理一次的時間增加至 260.7 毫秒,而使用 RenderScript 處理一次的時間是 136.9 毫秒,這樣看起來差異就比較明顯了,第二次的測試 RS 的處理時間幾乎是單純使用 Java 的一半,同時可以合理的推測越是複雜的運算,越能表現出 RenderScript 的優勢,當然圖片的像素越多,也越能感受到它的強大。

留言

這個網誌中的熱門文章

程式語言常用之符號與詞彙 - 中英文對照

Repo 實用指令

什麼是 Bootloader?