Edited at

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 にあります。