Edited at

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

More than 1 year has passed since last update.


概要

RenderScriptを初めて利用してみようとする上で引っかかったところをまとめています。

いろいろ試してみようとするたたき台を用意する作業については、RenderScriptによる画像処理のハンズオン(前編)を参照してください。


前提

この記事はRGBA_8888のビットマップを画像処理する前提で書かれています。


基礎知識


カーネル


概念

JavaでBitmapをネガポジ反転させる場合、次のように書ける:


java

    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];
c=invert(c);
array[x+v]=c;
}
}

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

public static int invert(int c) {
return 255<<24
| ((255-((c>>16)&0xFF)) <<16)
| ((255-((c>>8)&0xFF)) <<8)
| (255-(c&0xFF));
}


invert() で、与えられた色を反転して返している。

この関数がRenderScriptにおけるカーネルに相当する。

これをRenderScript化すると次のようになる:


java

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

RenderScript rs = RenderScript.create(context);
ScriptC_invret script = new ScriptC_invert(rs);

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

script.forEach_invert(allocIn, allocOut);

allocOut.copyTo(out);
}



invert.rs

#pragma version(1)

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

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

Java側のforEach_invert()呼び出しで出力画像(=allocOut)の全画素に対するinvert()が呼び出される

RenderScriptはカーネルの処理を超特急かつ複数同時並行で回してくれるので高速な処理が実現できる。

RenderScriptの文法はC言語に準ずる。Javaではないことに注意。


カーネルの関数定義

ネットで調べているとカーネルの関数定義が様々で混乱する。

void root()


  • 古い書き方。この記事では触れない

  • 関数名は"root"固定

  • renderscriptTargetApi 16以前はこの書式しか利用できない模様

__attribute__((kernel)) function() , RS_KERNEL function()

この記事ではこちらの書式を採用している。


  • 両者は等価

  • 任意の関数名を利用可能

  • renderscriptTargetApi 17以降で利用できる

__attribute__((kernel)) または RS_KERNEL の付記によってカーネル関数であることを宣言する。


基本の構文

uchar4 RS_KERNEL kernelFunction(uchar4 in, uint32_t x, uint32_t y) 

{
//...省略...
return ret;
}

引数は省略可能で、省略により動作が異なるバリエーションが発生する。


引数の意味


  • uchar4 in …入力画像の画素値

  • uint32_t x,y …画素値 in の座標値


引数の並び順、数


  • 画素値,座標値1,座標2 の順に並ぶ必要がある

  • いずれの引数も省略可能

  • 画素値の引数は1つのみ、座標値の引数は0~2個の可変


画素値の引数が存在するカーネル


  • 入力画像と出力画像を別個に指定する必要がある


    • ただし両者が同一でも構わない



  • 戻り値は出力画像に書き込まれ、入力画像は保持される

  • 複数画像をミックスする処理などに適する

カーネル
呼び出し

uchar4 RS_KERNEL kernel_ixy(uchar4 in, uint32_t x, uint32_t y)
forEach_kernel_ixy(ain, aout)

uchar4 RS_KERNEL kernel_i(uchar4 in)
forEach_kernel_i(ain, aout)

uchar4 RS_KERNEL kernel_ix(uchar4 in, uint32_t x)
forEach_kernel_ix(ain, aout)


画素値の引数を省略したカーネル


  • 入力画像 = 出力画像として扱われる

  • 戻り値は入力画像に上書きされる


    • 単純な色調変更などに適する



  • 入力と出力で画像の寸法が異なるものを処理したい場合にこのカーネルを利用できる


    • 本来の入力画像をあらかじめ別に用意しておいてカーネル内から参照する



カーネル
呼び出し

uchar4 RS_KERNEL kernel_xy(uint32_t x, uint32_t y)
forEach_kernel_xy(aout)

uchar4 RS_KERNEL kernel_x(uint32_t x)
forEach_kernel_x(aout)

uchar4 RS_KERNEL kernel_()
forEach_kernel_(aout)

引数なしのカーネルも定義できる。

すべての画素を単色で塗りつぶす等の用途に利用できる。


入力画像/出力画像


  • Allocationオブジェクトに格納してRenderScriptとの受け渡しを行う

  • AllocationオブジェクトはBitmapなどから生成できる

  • Allocationオブジェクトには生成元のBitmapなどの内容がコピーされている。どちらかの内容を変更してももう一方には影響しない


    • すなわち、画像処理完了後には結果の格納されたAllocationオブジェクトをBitmapオブジェクトにコピーし戻す必要がある



  • 入力/出力の各Allocationオブジェクトは、寸法などの属性が一致している必要がある



典型的なコードは次の通り:


java

        Allocation alloc = Allocation.createFromBitmap(rs, bitmap); //入出力画像のAllocationを用意

script.forEach_shader(alloc); //画像処理実行
alloc.copyTo(bitmap); //Bitmapに書き戻す



サポートライブラリ

build.gradleのdefaultConfigに"renderscriptSupportModeEnabled true" を書くことでサポートモードが有効になる。

以下のような影響がある:


  • Android2.2(API8)以降でもRenderScriptが使えるようになる


    • 標準で用意されたのはAndroid3.0(API11)以降



  • RenderScriptから自動生成されるコードがサポートライブラリを利用したものになる


    • 必然的に、自分で書くコードもサポートライブラリを用いなければならなくなる



  • サポートライブラリを利用している分にはおおむねAPIレベルを意識せずコードを書ける

  • リリースビルドのAPKが2MB強大きくなる

APKサイズの肥大という悪影響もあるので、必要とする機能が搭載されたAPIレベルより以前の端末を動作対象としないのであればサポートライブラリを使用しない(=サポートモードを無効)でもよいかもしれない。


基本テクニック


任意位置の画素を取得したい

ぼかし処理などでは処理対象の画素の周りの画素も得なければならない。

rsGetElementAt_uchar4(rs_allocation a, uint32_t x, uint32_t y)で実現できる。

任意のAllocationの任意位置の画素値を得ることができる。

カーネルが処理しているAllocationを知る方法は無いようなので、対象のAllocationはグローバル変数で指示する必要がある。

周りの21*21ピクセルの平均値を戻り値としてぼかすカーネルの例:


simpleBlur.rs

#pragma version(1)

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

rs_allocation allocIn;

uchar4 RS_KERNEL shader(uchar4 in, uint32_t x, uint32_t y)
{
int w=rsAllocationGetDimX(allocIn);
int h=rsAllocationGetDimY(allocIn);
uint4 sum={0,0,0,0};
for(int i=-10;i<=10;i++){
int iy=min(h-1,max((int)y+i,0));
for(int j=-10;j<=10;j++) {
int ix=min(w-1,max((int)x+j,0));
uchar4 c=rsGetElementAt_uchar4(allocIn,ix,iy);
sum.r+=(uint)c.r;
sum.g+=(uint)c.g;
sum.b+=(uint)c.b;
sum.a+=(uint)c.a;
}
}
uchar4 ret={(uchar)(sum.r/441),(uchar)(sum.g/441),(uchar)(sum.b/441),(uchar)(sum.a/441)};
return ret;
}



java

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

RenderScript rs = RenderScript.create(context);
ScriptC_simpleBlur script = new ScriptC_simpleBlur(rs);

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

script.set_allocIn(allocIn);
script.forEach_shader(allocIn, allocOut);

allocOut.copyTo(out);
}



カーネル内で画像の寸法を知りたい



  • rsAllocationGetDimX(allocation) , rsAllocationGetDimY(allocation) で寸法を取得できる


カーネル内で画素の処理毎に取得するのは非効率的なので、次のような手もある。

Javaからは直接forEach_alphaShader()を呼ばずinvoke_callForEach()を呼ぶ。

ただしminSdkVersionによってカーネルの書式に制約が生じる。

API26(Android6.0)以降では次のように書ける:


rs(API24~)

static rs_allocation allocIn;

static uint32_t width;
static uint32_t height;

uchar4 RS_KERNEL shader(uchar4 in, uint32_t x, uint32_t y)
{
//width,heightでallocInの寸法にアクセスできる
//...内容省略...
}

void callForEach(rs_allocation in,rs_allocation out)
{
allocIn=in;
width=rsAllocationGetDimX(in);
height=rsAllocationGetDimY(in);

rsForEach(shader,in,out); //APIレベル24~
}


API23以前ではrsForEach()で任意のカーネルを呼べないので旧書式の"root"カーネルを使う必要がある。


rs(~API23)

static rs_allocation allocIn;

static uint32_t width;
static uint32_t height;

void root(const uchar4 *v_in, uchar4 *v_out)
{
//width,heightでallocInの寸法にアクセスできる
//...内容省略...
}

void callForEach(rs_script script, rs_allocation in,rs_allocation out)
{
allocIn=in;
width=rsAllocationGetDimX(in);
height=rsAllocationGetDimY(in);

rsForEach(script,in,out);
}



一部分だけを処理したい

Script.LaunchOptionsをforEach呼び出しの引数に与えることで、処理対象とする矩形領域を指定できる。


java

        Script.LaunchOptions opt=new Script.LaunchOptions().setX(rect.x,rect.getWidth()).setY(rect.y,rect.getHeight());

script.forEach_shader(allocOut,opt);

不定形の領域を処理対象としたい場合は、マスク画像を用意するなどの工夫が必要となる。


入力と出力で異なる寸法の画像を処理したい

画像の拡大/縮小を行う場合や、入力画像の一部分を切り出すような場合にこのような状況が発生する。

入力画像のAllocationをRenderScriptのグローバル変数にセットして、出力画像のみ受け付けるカーネルで処理する。

入力画像を縦横半分に縮小にするサンプルを示す:


java

    //与えられたBitmapを縦横半分に縮小したものを生成する

// 単純に間引いて縮小、エラー処理は省略
public static Bitmap shrink(Context context, Bitmap in) {
//結果用に入力画像の縦横半分のBitmapを用意
Bitmap result=Bitmap.createBitmap(in.getWidth()/2, in.getHeight()/2, in.getConfig());

//RenderScriptを用意
RenderScript rs = RenderScript.create(context);
ScriptC_shrink script = new ScriptC_shrink(rs);

//Allocationを生成
Allocation allocIn = Allocation.createFromBitmap(rs, in);
Allocation allocOut = Allocation.createFromBitmap(rs, result);

//処理実行
script.set_allocIn(allocIn);
script.forEach_shader(allocOut);

//結果をBitmapに戻す
allocOut.copyTo(result);
return result;
}



rs

#pragma version(1)

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

rs_allocation allocIn;

uchar4 RS_KERNEL shader(uint32_t x,uint32_t y)
{
uchar4 in=rsGetElementAt_uchar4(allocIn,x*2,y*2);
return in;
}



intの配列から直接Allocationを生成したい

Bitmap.getPixels()で得られるintの配列をカーネルで処理したい場合がある。

ネットで見かけるサンプルでは押しなべてAllocationをcreateFromBitmap()で生成していて参考にならない。

中継ぎとしてint配列からBitmapを生成する方法もあるが合理的ではない。

特に、メモリの少ない端末ではBitmapを多量に生成するとOutOfMemoryで落ちることがあり致命的影響がある。

以下の方法でint配列から直接Allocationを生成できる:


java

    Type t = new Type.Builder(rs, Element.U8_4(rs)).setX(width).setY(height)).create();

Allocation alloc = Allocation.createTyped(rs, t);
alloc.copyFromUnchecked(intArray);

script.set_alloc(alloc);
script.forEach_shader(alloc);

alloc.copy1DRangeToUnchecked(0,intArray.length,intArray);



rs

rs_allocation alloc;

uchar4 RS_KERNEL shader(uint32_t x, uint32_t y)
{
uchar4 in=rsGetElementAt_uchar4(alloc,x,y);
in=in.bgra; //from Bitmap.getPixels() int array

uchar4 result;

//...中略...

result=result.bgra; //to Bitmap.getPixels() int array
return result;
}



注意点


RGBの転置

この方法で得られるAllocationとcreateFromBitmap()から得られるものとでは、RとBが入れ替わっている

このため、上記のrsコードでのin=in.bgraのような対処が必要となる。

createFromBitmap()と共用できるカーネルにしたい場合はグローバル変数のフラグを設けるなどして対処する必要がある。


16ピクセルのアライメント

この方法で用いるintの配列は、水平方向に16ピクセル単位の幅を持たなければならない。

余りが出る場合は余白を設けて16ピクセル単位に補正する必要がある。

さもなければカーネルに正しいx,y座標が渡されない。

Bitmap.getPixel()を用いる場合は以下のような措置が必要:

int w=bitmap.getWidth();

int h=bitmap.getHeight();
int stride=( (w/16)*16 + ((w%16)==0) ? 1:0 ) *16;
int[] array=new int[stride*h];
bitmap.getPixels(array, 0, stride, 0, 0, w, h);


RenderScript内からデバッグ出力したい

rsDebug() でLogCatに出力できる。

利用にはスクリプトの先頭付近に #include "rs_debug.rsh" を記載する必要がある。

rsDebug("メッセージ",変数);

で変数の内容を見やすいフォーマットで出力してくれる。

カーネルの内部で使用するととんでもない量の出力が行われるため、覚悟の上で実行する必要がある。


RenderScriptの?なところ

フレームワーク側のバグなのか仕様なのか自分の不手際なのか悩むところがいっぱいあった。


ビルド


.rsファイルを更新しても動作に反映されない


  • 端末のキャッシュに残った旧バージョンのコンパイル結果が残ったままで更新されないことが原因


    • 端末側でアプリのキャッシュをクリアすることで更新される

    • 端末種類によっては発生しないかもしれない

    • 同じ端末、ビルド環境でも発生したりしなかったりするので厄介



この現象が起こると次のような症状で悩むことになる:


  • グローバル変数の出現順序を変えると異常終了する


    • 呼び出し側のコードは更新されるため

    • const変数は影響を受けない



  • グローバル変数や関数を追加、変更しても挙動が変わらない

  • rsDebug()を追記しても出力されない

などなど……


ビルドすると"llvm-rs-cc.exe"がエラーを返す



  • exit value=1のこんなようなエラーが出る場合、.rsファイルがC言語的に文法エラーを起こしている:


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




  • 要するにコンパイルできないような書き間違えがあるので直す。


  • 本来知りたいエラー内容はどこを見ればいいのかわからなかった



ビルド時にワーニングがいっぱい出る



  • warning: Linking two modules of different data layouts:... , warning: Linking two modules of different target triples: がたくさん出る

  • 気にしなくてもよい模様


RenderScriptの文法


一つの.rsファイルに複数のカーネルは書けるのか


  • 書ける


戻り値のある関数は書けるのか


  • カーネル以外では書けない


    • Javaから利用できないだけではなく、カーネルのサブルーチンとして戻り値のある関数を設けることもできない



  • 戻り値のない関数で代用できる


    • 引数にポインタを渡して応答を格納させることはできる

    • カーネルからも呼び出せる

    • static関数にすればJava側に対して隠蔽できる

    • Javaと違って、呼び出す箇所より前にその関数のプロトタイプ宣言あるいは実装が書かれていなければエラーになるので注意




includeで他のファイルを取り込めるのか


  • 取り込める

  • 取り込むファイルは拡張子が".rs"であってはならない

  • 取り込むファイルには通常の.rsファイルに書くpragma等は記述不要

  • rsフォルダにサブフォルダを設けてインクルードするファイルを置くこともできる



    • #include "folder/include.c" のように書ける





C言語忘れた|わかんね


  • Javaが書けるならなんとかなる