(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程度かかりました。
ソース
それぞれの変換処理ソースは以下の通り。
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;
}
}
}
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);
}
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);
}
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);
}
}
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 にあります。