Edited at

RenderScriptによる画像処理のハンズオン(前編)

More than 1 year has passed since last update.


概要

Android上での画像のぼかしとかの処理をJavaで書くと、勘弁ならない遅さに直面することがある。

ちょこっと高速化するのにRenderScriptが有用だけど、なかなかすんなり動かなかった。

資料も多くないので今更ながら知見を書き残しておきます。


前提


  • RenderScriptのcomputeを利用した画像処理を自力で書く

  • ScriptIntrinsicBlurとかの既成のライブラリは利用しない


ビルド環境


  • AndroidStudio 3.1.4

  • buildToolsVersion "27.0.3"

  • compileSdkVersion 26

  • minSdkVersion 14

  • targetSdkVersion 26

  • renderscriptTargetApi 18


テストベッドアプリの用意

とりあえず画像を取り込めて動くことが保証されるテストベッドアプリを用意する。

この時点でRenderScriptは動作しない。後でいろいろ追記して試す。


プロジェクトの生成

AndroidStudioで以下の通りに操作:


  • ファイル→新規→新規プロジェクト…で、スマホ及びタブレットの空のアクティビティのプロジェクトを生成。APIレベル=任意。

  • モジュール:appのbuild.gradleに以下の記述を追記


build.gradle

    defaultConfig {

...中略...
renderscriptTargetApi 18
// renderscriptSupportModeEnabled true はサポートライブラリを使わない限り不要
}


  • AndroidManifest.xmlに以下の記述を追記



    • <intent-filter> は既存の記述とは別に追記すること

    • メモリをがばがば使うような処理を書いても落ちないように android:largeHeap="true" を追記しておく




AndroidManifest.xml

    <application

android:largeHeap="true"
...中略...
>
<activity android:name=".MainActivity">
...中略...
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/jpeg"/>
<data android:mimeType="image/jpg"/>
<data android:mimeType="image/png"/>
<data android:mimeType="image/bmp"/>
<data android:mimeType="image/bitmap"/>
</intent-filter>
</activity>
</application>



  • MainActivity.javaに以下のソースをコピペ、importは適宜追加する:


MainActivity.java

public class MainActivity extends AppCompatActivity {

private static final int MAX_SRCWIDTH = 1920;
private static final int MAX_SRCHEIGHT = 1920;

private static final int REQUEST_PICKIMAGE = 1;
private Bitmap mSource;
private Bitmap mBitmap;
private ImageView mIv;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mIv = new ImageView(this);
setContentView(mIv);

//共有で送られていなければイメージピッカーを開く
if (!processIntent()) invokeImagePicker();
}

//共有で送られた画像を処理する
private boolean processIntent() {
boolean result = false;
Uri imageUri = null;
try {
imageUri = Uri.parse(getIntent().getExtras().get("android.intent.extra.STREAM").toString());
} catch (Exception e) {
}
if (imageUri != null) result = acquireImage(imageUri);

return result;
}

//イメージピッカーを開く
private void invokeImagePicker() {
Intent i = new Intent();
i.setType("image/*");
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(i, REQUEST_PICKIMAGE); //結果はonActivityResult()に返る
}

//イメージピッカーで選択された画像を取得する
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && requestCode == REQUEST_PICKIMAGE) {
acquireImage(data.getData());
}
}

//Uriで指定された画像を読み込む
// 大きすぎる場合は端末によっては表示できないので縮小する
private boolean acquireImage(Uri uri) {
boolean result = false;

if (uri == null) return result;

Bitmap bmp = null;
try {
bmp = MediaStore.Images.Media.getBitmap(getContentResolver(), uri);
} catch (Exception e) {
e.printStackTrace();
}

if (bmp != null) {
mSource = bmp;
result = true;
//画像が大きすぎる場合はOutOfMemoryで表示できないことがあるので表示用に縮小しておく
Bitmap shrinked = createShrinedkBitmap(mSource);
if (shrinked != null) mBitmap = shrinked;
else mBitmap=mSource.copy(Bitmap.Config.ARGB_8888,true);

mIv.setImageBitmap(mBitmap);
}

return result;
}

//表示に適したサイズに縮小したBitmapを生成する
private Bitmap createShrinedkBitmap(Bitmap src) {
int wOrg = src.getWidth();
int hOrg = src.getHeight();
int w = wOrg, h = hOrg;
if (w > MAX_SRCWIDTH) {
h = (int) (((float) MAX_SRCWIDTH / w) * h);
w = MAX_SRCWIDTH;
}
if (h > MAX_SRCHEIGHT) {
w = (int) (((float) MAX_SRCHEIGHT / h) * w);
h = MAX_SRCHEIGHT;
}
if (w == wOrg && h == hOrg) return null;

return Bitmap.createScaledBitmap(src, w, h, true);
}
}



とりあえず実行

実行するとイメージピッカーが開き、そこで選択した画像がアプリに読み込まれて表示される。

また、ブラウザの画像を長押し→画像の共有 でこのアプリに画像を転送できるのでいろいろ試すのに便利。


RenderScriptを書く


フォルダの作成

とりあえず書いたものをどこに置いたらいいのかわかんない人も多いと思う。

AndroidStudioでモジュールappを右クリック→新規→フォルダー→RenderScriptフォルダー でそのまま完了、



でRenderScriptファイルを置くフォルダが作られる。


RenderScriptファイルの作成

renderscriptフォルダを右クリック→新規→ファイル でRenderScriptファイルを作成する。


  • 拡張子は .rs

  • 名前は重要。Javaから呼び出すときに参照される

ここではファイル名を firstrs.rs とする。

以下の内容をコピペする。

パッケージ名の部分はAndroidManifest.xml等に書かれている内容に置き換える。カッコは省略しないこと。


firstrs.rs

#pragma version(1)

#pragma rs java_package_name(パッケージ名)

uchar4 __attribute__((kernel)) shader(uchar4 in)
{
uchar4 ret={255-in.r,255-in.g,255-in.b,255};
return ret;
}



ビルドする

RenderScriptを呼び出すコードは書いていないが、ここで一旦ビルドする。

ビルドすることでJavaからRenderScriptを呼び出すためのコードが自動生成される。

ビルド→プロジェクトの作成 でビルド実行。

成功すればプロジェクトファイルのbuild/generated以下に ScriptC_<.rsファイル名> のJavaファイルが確認できる。


エラーが出るとき

RenderScriptの内容が適切でないと次のようなエラーが出る。

事情が分からないとビルド環境のせいなのか悩むことになる。


Process 'command 'C:\DevAndroid\sdk\build-tools\27.0.3\llvm-rs-cc.exe'' finished with non-zero exit value 1


この記事の内容に従っている場合はおそらくSyntaxErrorなので、適宜.rsファイルの内容を見直してビルドが通るまでやり直す。


RenderScriptを呼び出す


RenderScriptの操作を書く

BitmapをRenderScriptに渡して結果のBitmapを返してもらうという流れになる。

ここでは(さほどクラス化する意義はないが見やすいので)それ用のクラスを追加して処理することとする。

パッケージに新規クラス RsTest を追加する。

以下の内容をコピペ、importは適宜追加する:


RsTest.java

public class RsTest {

public static void run(Context context, Bitmap in, Bitmap out) {
RenderScript rs = RenderScript.create(context);
ScriptC_firstrs script = new ScriptC_firstrs(rs);

Allocation allocIn = Allocation.createFromBitmap(rs, in);
Allocation allocOut = Allocation.createFromBitmap(rs, out);

script.forEach_shader(allocIn, allocOut);

allocOut.copyTo(out);
}
}



呼び出して結果を得る

MainActivity.javaに呼び出し処理を追記する。

画像を読み込んで表示する直前に以下のように追記する:


MainActivity.java

    private boolean acquireImage(Uri uri) {

...中略...
if (shrinked != null) mBitmap = shrinked;
else mBitmap=mSource.copy(Bitmap.Config.ARGB_8888,true);

//RenderScript呼び出し
RsTest.run(this,mBitmap,mBitmap);

mIv.setImageBitmap(mBitmap);
}

return result;
}


画面表示用のBitmapを画像処理して、同じBitmapに結果を返してもらっている。

注意:

それなりに時間がかかる処理なので本来はメインスレッド上で実行すべきではないが、めんどくさいので わかりやすさを優先してこのように実装してある。

表舞台に出すアプリならAsyncTask等を利用してバックグラウンドで処理するよう実装すべき。


実行

こんな感じになる:

firstrs.rs を見れば想像がつくと思うが、ネガポジ反転している。


速いのか

RenderScriptで実装した場合とJavaで書いた場合のどちらが速いのか比較してみた。

対象画像の解像度は1920x1440ピクセル。端末はSnapdragon 617 (MSM8952) 1.5 GHz オクタコア…2016年のミドルスペック機。


軽い処理の場合

上で書いた画像処理をJavaでも書き、所要時間を比較した。

Java版ではBitmapをgetPixels()でintの配列にしてから処理している。

実装
所要時間 3回(ms)
所要時間 平均(ms)

RenderScript版
1019,694,111
608.0

Java版
133,126,99
119.3

…Javaの方が速い。全然速い。もう寝る。

というかRenderScriptの所要時間にばらつきがありすぎる。


RenderScriptのイニシャライズ部分を省いて所要時間を計測するとこうなる。

ついでにJavaでBitmapをint配列化せずそのまま処理するバージョンの結果も掲載する。

実装
所要時間 3回(ms)
所要時間 平均(ms)

RenderScript版(画像処理部分)
62,60,53
58.3

Java版(再掲)
133,126,99
119.3

Java版(Bitmap版)
32120,32008,31997
32041.7

RenderScript版がちょうど2倍ほど速い結果となった。ほっとした。

BitmapのままでgetPixel()やsetPixel()するとヤバいぐらいに遅いことがわかる。RenderScript版の約550倍、Java配列版の約269倍の所要時間となっている。


重い処理の場合

1ピクセルについて周辺21x21ピクセルの範囲の平均値をとる単純なぼかしを実装して、RenderScript版とJava版で比較してみた。

実装
所要時間 3回(ms)
所要時間 平均(ms)

RenderScript版(画像処理部分)
2432,2373,2387
2397.3

Java版
38473,38492,38533
38499.3

RenderScript版がちょうど16倍速い。これは値打ちがあると思う。

とはいえ2.4秒もかかる。イニシャライズ部分も含めれば3秒ほどになる。


速度についてのまとめ


  • RenderScriptは重い処理になるほどJavaでの実装を速度的に突き放す


    • とはいえすごいエフェクトをリアルタイムで、という程には速くない



  • RenderScriptはごく簡単な画像処理においては圧倒的に速いわけではない


    • RenderScriptのイニシャライズに時間がかかるので、簡単な画像処理ならJavaで書いた方が手早い



  • Javaで実装する場合でもBitmapをそのまま読み書きするのではなく、getPixels()で配列化してから処理した方がよい


上で書いたネガポジ反転の画像処理のJava版ソースは以下。

最適化は可能だが、RenderScriptと類比させるためあえて冗長に書いてある。

    public static void runJava(Context context, Bitmap in, Bitmap out) {

int w=in.getWidth();
int h=in.getHeight();
int[] array=new int[w*h];
in.getPixels(array,0,w,0,0,w,h);

for(int y=0;y<h;y++) {
int v=y*w;
for(int x=0;x<w;x++) {
int c=array[x+v];
array[x+v]=255<<24
| ((255-((c>>16)&0xFF)) <<16)
| ((255-((c>>8)&0xFF)) <<8)
| (255-(c&0xFF));
}
}

out.setPixels(array,0,w,0,0,w,h);
}


つづく

長くなってきたのでキリの良いこのあたりで一区切りします。

RenderScriptそのものについては後編:RenderScriptによる画像処理のハンズオン(後編)を参照してください。