Android
JNI
RenderScript

RGB-YUV変換パフォーマンス比較(Java, JNI, RenderScript)

More than 1 year has passed since last update.

(2016/12/26 追記)
コメント頂いたlibyuvを使用した場合の測定結果も追記しました。

カメラプレビューや MediaCodec から得られる画像を解析・編集する際に
RGB-YUV間での変換が必要になることもあるかと思います。
そこで、 Pure Java、JNI、RenderScript、libyuvを使用した場合、それぞれの処理時間を測定してみました。

測定環境

Nexus 5X (arm64-v8a), Android 6.0.1

測定結果

各サイズのARGB8888画像をYUV420 Semi-Planar(NV21)に10回変換して、
最小値、最大値を除いた平均値を出しています。
単位はいずれもusec.

リリースビルド

画像サイズ Pure Java JNI libyuv RenderScript
320x180 4,428 1,633 362 10,860
640x360 17,505 6,542 2,353 13,954
1280x720 70,808 26,115 6,706 25,159
1920x1080 157,301 58,643 15,000 39,918
3840x2160 625,875 234,650 58,470 162,353

デバッグビルド

画像サイズ Pure Java JNI libyuv RenderScript
320x180 29,979 8,580 337 9,219
640x360 119,860 33,908 2,002 12,535
1280x720 477,142 136,232 6,731 38,462
1920x1080 1,073,609 305,510 14,922 38,462
3840x2160 4,287,866 1,228,023 58,094 120,606

libyuvとRenderScriptはどちらのビルドでも最適化されているため、
デバッグビルド・リリースビルドに大きな違いはありません。

Pure Java、JNIどちらもリリースビルドの効果が出ており、
Pure Javaの場合は320x180、
JNIの場合は320x180、640x360
の場合にRenderScriptよりも速くなっています。
libyuvは圧倒的。4KでもRenderScriptの2〜3倍です。

libyuvのように最適化されたライブラリがあるならば、その使用を検討すべきだと思います。
自分で書くのであれば、RenderScriptにはある程度、オーバーヘッドがあるということを認識して、
対象画像のサイズによって、どの方法で実装するかなどを検討する必要がありそうです。

なお、今回の平均値の算出方法では出てきませんが、
RenderScriptの初回実行時はコンパイル処理が走るため、
320x180でも 300〜500 msec程度かかりました。

ソース

それぞれの変換処理ソースは以下の通り。

PureJavaによる変換処理
public void rgbToYuv(byte[] rgb, int width, int height, byte[] yuv) {
  int rgbIndex = 0;
  int yIndex = 0;
  int uvIndex = width * height;
  for (int j = 0; j < height; ++j) {
    for (int i = 0; i < width; ++i) {
      final int r = rgb[rgbIndex] & 0xFF;
      final int g = rgb[rgbIndex + 1] & 0xFF;
      final int b = rgb[rgbIndex + 2] & 0xFF;

      final int y = (int) (0.257 * r + 0.504 * g + 0.098 * b + 16);
      final int u = (int) (-0.148 * r - 0.291 * g + 0.439 * b + 128);
      final int v = (int) (0.439 * r - 0.368 * g - 0.071 * b + 128);

      yuv[yIndex++] = (byte) Math.max(0, Math.min(255, y));
      if ((i & 0x01) == 0 && (j & 0x01) == 0) {
        yuv[uvIndex++] = (byte) Math.max(0, Math.min(255, v));
        yuv[uvIndex++] = (byte) Math.max(0, Math.min(255, u));
      }

      rgbIndex += 4;
    }
  }
}
JNIによる変換処理
void rgbToYuv(JNIEnv *env, jobject, jbyteArray rgbArray, jint width, jint height, jbyteArray yuvArray) {
  jbyte *rgb = env->GetByteArrayElements(rgbArray, NULL);
  jbyte *yuv = env->GetByteArrayElements(yuvArray, NULL);

  int rgbIndex = 0;
  int yIndex = 0;
  int uvIndex = width * height;
  for (int j = 0; j < height; ++j) {
    for (int i = 0; i < width; ++i) {
      int r = rgb[rgbIndex] & 0xFF;
      int g = rgb[rgbIndex + 1] & 0xFF;
      int b = rgb[rgbIndex + 2] & 0xFF;

      int y = (int) (0.257 * r + 0.504 * g + 0.098 * b + 16);
      int u = (int) (-0.148 * r - 0.291 * g + 0.439 * b + 128);
      int v = (int) (0.439 * r - 0.368 * g - 0.071 * b + 128);

      yuv[yIndex++] = (jbyte) (y < 0 ? 0 : y > 255 ? 255 : y);
      if ((i & 0x01) == 0 && (j & 0x01) == 0) {
        yuv[uvIndex++] = (jbyte) (v < 0 ? 0 : v > 255 ? 255 : v);
        yuv[uvIndex++] = (jbyte) (u < 0 ? 0 : u > 255 ? 255 : u);
      }

      rgbIndex += 4;
    }
  }

  env->ReleaseByteArrayElements(yuvArray, yuv, 0);
  env->ReleaseByteArrayElements(rgbArray, rgb, 0);
}
libyuvによる変換処理
void rgbToBgr(JNIEnv *env, jobject, jbyteArray rgbArray, jint width, jint height, jbyteArray bgrArray) {
  jbyte *rgb = env->GetByteArrayElements(rgbArray, NULL);
  jbyte *bgr = env->GetByteArrayElements(bgrArray, NULL);

  ABGRToARGB((uint8*) rgb, width << 2, (uint8*) bgr, width << 2, width, height);

  env->ReleaseByteArrayElements(bgrArray, bgr, 0);
  env->ReleaseByteArrayElements(rgbArray, rgb, 0);
}

void bgrToYuv(JNIEnv *env, jobject, jbyteArray bgrArray, jint width, jint height, jbyteArray yuvArray) {
  jbyte *bgr = env->GetByteArrayElements(bgrArray, NULL);
  jbyte *yuv = env->GetByteArrayElements(yuvArray, NULL);

  ARGBToNV21((uint8*) bgr, width << 2, (uint8*) yuv, width, (uint8*) &yuv[width * height], width, width, height);

  env->ReleaseByteArrayElements(yuvArray, yuv, 0);
  env->ReleaseByteArrayElements(bgrArray, bgr, 0);
}
RenderScriptによる変換処理
rs_allocation gOut;
int width;
int height;
int frameSize;

void RS_KERNEL convert(uchar4 in, uint32_t x, uint32_t y) {
  uchar r = in.r;
  uchar g = in.g;
  uchar b = in.b;

  int yInt = (int) (0.257f * r + 0.504f * g + 0.098f * b) + 16;
  int uInt = (int) (-0.148f * r - 0.291f * g + 0.439f * b) + 128;
  int vInt = (int) (0.439f * r - 0.368f * g - 0.071f * b) + 128;

  uchar yChar = (uchar) (yInt < 0 ? 0 : yInt > 255 ? 255 : yInt);
  uchar uChar = (uchar) (uInt < 0 ? 0 : uInt > 255 ? 255 : uInt);
  uchar vChar = (uchar) (vInt < 0 ? 0 : vInt > 255 ? 255 : vInt);

  rsSetElementAt_uchar(gOut, yChar, width * y + x);

  if ((x & 0x01) == 0 && (y & 0x01) == 0) {
    uint32_t offset = frameSize + width * (y >> 1) + x;
    rsSetElementAt_uchar(gOut, vChar, offset);
    rsSetElementAt_uchar(gOut, uChar, offset + 1);
  }
}
RenderScriptによる変換処理の呼び出し元
public void rgbToYuv(byte[] rgb, int width, int height, byte[] yuv) {
  final Type.Builder inType = new Type.Builder(mRenderScript, Element.RGBA_8888(mRenderScript))
      .setX(width)
      .setY(height);

  final Type.Builder outType = new Type.Builder(mRenderScript, Element.U8(mRenderScript))
      .setX(width * height * 3 / 2);

  final Allocation inAllocation = Allocation.createTyped(mRenderScript, inType.create(), Allocation.USAGE_SCRIPT);
  final Allocation outAllocation = Allocation.createTyped(mRenderScript, outType.create(), Allocation.USAGE_SCRIPT);

  inAllocation.copyFrom(rgb);

  final ScriptC_rgb2yuv script = new ScriptC_rgb2yuv(mRenderScript);
  script.set_gOut(outAllocation);
  script.set_width(width);
  script.set_height(height);
  script.set_frameSize(width * height);
  script.forEach_convert(inAllocation);

  outAllocation.copyTo(yuv);
}

すべてのソースは GitHub にあります。