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と呼び習わしていたのかもしれない。