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

(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程度かかりました。

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

```java: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;
    }
  }
}
```

```c: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);
}
```

```c: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);
}
```

```c: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);
  }
}
```

```java: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](https://github.com/alzybaad/RGB2YUV) にあります。