Levels in Renderscript(超訳)
Levels in RenderscriptというRenderscriptを解説した記事が本家に上がったので、久々に超訳。誤訳などあればご指摘を。
ICSではRenderscript(RS)がアップデートされた。いくつかの新しい機能が加えられ、アプリケーションで計算を簡単に高速化出来るようになっている。大量の処理が必要な大きなデータバッファがある場合、計算の高速化のためにRSは興味深い。この例ではレベル/サチュレーション処理をビットマップに施してみる。
この場合、サチュレーションはすべてのピクセルと色行列のかけ算として実装され、レベルはいくつかの演算で実装されるのが常套だ。
1. 入力レベルの調整
2. ガンマ補正
3. 出力レベルの調整
4. 有効値へのクランプ
単純な実装はこのようになる。
for (int i=0; i < mInPixels.length; i++) { float r = (float)(mInPixels[i] & 0xff); float g = (float)((mInPixels[i] >> 8) & 0xff); float b = (float)((mInPixels[i] >> 16) & 0xff); float tr = r * m[0] + g * m[3] + b * m[6]; float tg = r * m[1] + g * m[4] + b * m[7]; float tb = r * m[2] + g * m[5] + b * m[8]; r = tr; g = tg; b = tb; if (r < 0.f) r = 0.f; if (r > 255.f) r = 255.f; if (g < 0.f) g = 0.f; if (g > 255.f) g = 255.f; if (b < 0.f) b = 0.f; if (b > 255.f) b = 255.f; r = (r - mInBlack) * mOverInWMinInB; g = (g - mInBlack) * mOverInWMinInB; b = (b - mInBlack) * mOverInWMinInB; if (mGamma != 1.0f) { r = (float)java.lang.Math.pow(r, mGamma); g = (float)java.lang.Math.pow(g, mGamma); b = (float)java.lang.Math.pow(b, mGamma); } r = (r * mOutWMinOutB) + mOutBlack; g = (g * mOutWMinOutB) + mOutBlack; b = (b * mOutWMinOutB) + mOutBlack; if (r < 0.f) r = 0.f; if (r > 255.f) r = 255.f; if (g < 0.f) g = 0.f; if (g > 255.f) g = 255.f; if (b < 0.f) b = 0.f; if (b > 255.f) b = 255.f; mOutPixels[i] = ((int)r) + (((int)g) << 8) + (((int)b) << 16) + (mInPixels[i] & 0xff000000); }
このコードはビットマップがすでにロードされていて、処理のために整数配列に移されていることを想定している。ビットマップはすでにロードされているので、これは簡単だ。
mInPixels = new int[mBitmapIn.getHeight() * mBitmapIn.getWidth()]; mOutPixels = new int[mBitmapOut.getHeight() * mBitmapOut.getWidth()]; mBitmapIn.getPixels(mInPixels, 0, mBitmapIn.getWidth(), 0, 0, mBitmapIn.getWidth(), mBitmapIn.getHeight());
データ処理のループが終われば、描画のためにビットマップに戻すことも簡単だ。
mBitmapOut.setPixels(mOutPixels, 0, mBitmapOut.getWidth(), 0, 0, mBitmapOut.getWidth(), mBitmapOut.getHeight());
フィルター本体の定数計算や、(ボタンなどの)コントロールの制御、画像表示などのコードを含めて、アプリケーション全体のコード量は232行程度になる。手元の実機では800x423の画像処理におおよそ140-180msecかかる。
もしこれで十分でなかったらどうする?
画像処理のコア部分をRSに移植するのはとても簡単だ。上記のピクセル処理本体をRSで実装しなおすとこうなる。コードはhttp://code.google.com/p/android-renderscript-samples/source/browse/Levelsにある。
void root(const uchar4 *in, uchar4 *out, uint32_t x, uint32_t y) { float3 pixel = convert_float4(in[0]).rgb; // 3要素ベクトル pixel = rsMatrixMultiply(&colorMat, pixel); // ライブラリ関数 pixel = clamp(pixel, 0.f, 255.f); // ライブラリ関数 pixel = (pixel - inBlack) * overInWMinInB; // ベクトル演算 if (gamma != 1.0f) pixel = pow(pixel, (float3)gamma); // ライブラリ関数 pixel = pixel * outWMinOutB + outBlack; // ベクトル演算 pixel = clamp(pixel, 0.f, 255.f); // ライブラリ関数 out->xyz = convert_uchar3(pixel); // ライブラリ関数 }
コード行数が極端に少なくて済むのは、浮動小数点のベクトルや行列演算、フォーマット変換があらかじめ組み込まれているからだ。また、ループが存在しないことに着目して欲しい。
準備のためのコードは、スクリプトをロードする必要があるので、ほんの少し複雑になる。
mRS = RenderScript.create(this); mInPixelsAllocation = Allocation.createFromBitmap(mRS, mBitmapIn, Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT); mOutPixelsAllocation = Allocation.createFromBitmap(mRS, mBitmapOut, Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT); mScript = new ScriptC_levels(mRS, getResources(), R.raw.levels);
このコードはRSのコンテキストを生成している。続いて、2つのメモリアロケーションをこのコンテキストを使って生成し、ビットマップデータのRS用コピーを保持する。最後にデータ処理のためにスクリプトをロードする。
ソースコードには、他にもいくつか小さなコードの塊があり、定数に変更があった時に計算し直してスクリプトへコピーしている。グローバル変数はスクリプトからリフレクションされているので、簡単に行える。
mScript.set_inBlack(mInBlack); mScript.set_outBlack(mOutBlack); mScript.set_inWMinInB(mInWMinInB); mScript.set_outWMinOutB(mOutWMinOutB); mScript.set_overInWMinInB(mOverInWMinInB);
先に述べたように、すべてのピクセルを処理するためのループがない。ビットマップデータを処理し、結果をコピーするRSコードはこうなる。
mScript.forEach_root(mInPixelsAllocation, mOutPixelsAllocation); mOutPixelsAllocation.copyTo(mBitmapOut);
最初の行はスクリプトと入力アロケーションを取り出して、結果を保持する出力アロケーションを設定している。たったこれだけで、ネイティブにコンパイルされたバージョンのスクリプトを、アロケーションに入っているすべてのピクセルに対して、1つづつ一度だけ呼び出す。しかし、Dalvikの実装とは異なり、プリミティブ(コンパイルされたスクリプト)は自動的に複数のスレッドを生成して処理を行う。ネイティブコードの効率と合わさることで、大きなパフォーマンス向上を生んでいる。ガンマ関数のありなしで計算コストが大きく異なるので、両方の結果を下に示す。
800x423の画像
Device | Dalvik | RS | Gain |
Xoom | 174ms | 39ms | 4.5x |
Galaxy Nexus | 139ms | 30ms | 4.6x |
Tegra 30 device | 136ms | 19ms | 7.2x |
ガンマ補正を加えた場合の800x423の画像
Device | Dalvik | RS | Gain |
Xoom | 994ms | 259ms | 3.8x |
Galaxy Nexus | 787ms | 213ms | 3.7x |
Tegra 30 device | 783ms | 104ms | 7.5x |
ゲインが大きいほど、簡単なコーディングで得られる見返りが大きいことを示す。
訳注: Xoom, GN, Tegra 3というのは理にかなった選択。
Xoom: 2コア、VFP3
GN: 2コア、NEON
Tegra 3: 4コア、NEON
Tegra 30というのは、単なる記憶違いもしくはTegra 3が正式名称になるまえに30と呼び習わしていたのかもしれない。