本記事では、DJI ドローンの映像ストリーミングから取得したYUV画像をRGBコンバートについてご紹介します。
DJI ドローンライブ映像のYUV画像取得
ストリーミングからYUV画像取得を参考してDJI Mobile SDKでドローンの映像ストリーミングからYUV画像取得します。
public void onYuvDataReceived(MediaFormat format,
final ByteBuffer yuvFrame,
int dataSize,
final int width,
final int height) {
// The obtained data of format is MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar
// and the width and height must be even.
}
YUVとは
YUVやYCbCrやYPbPrとは、輝度信号Yと、2つの色差信号を使って表現される色空間。
何故YUVフォーマット
人間の目は明るさの変化には敏感だが, 色の変化には鈍感であるというわけで,色度を抑え、輝度により広い帯域やビット数を割くことにより、少ない損失で効率の良い伝送や圧縮を実現するフォーマット。
RGB と YCbCr
RGB方式では、Red, Green, Blueの3つの原色を混ぜ合わせることで 色を表現する手法である。
YCbCr方式では、輝度成分のYと青から輝度を引いた青色差Cb(B-Y)、赤から輝度を引いた赤色差Cr(R-Y)と3種類で色を表現する。
RGB と YCbCr は 以下の計算式で相互に変換が可能である
RGB to YUV
Y = 0.299R + 0.587G + 0.114B
U = -0.169R - 0.331G + 0.500B
V = 0.500R - 0.419G - 0.081B
YUV to RGB
R = 1.000Y + 1.402V
G = 1.000Y - 0.344U - 0.714V
B = 1.000Y + 1.772U
輝度Y には G の成分が多く含まれており、B の成分が少ないことがわかる。 人間の目には緑色G は明るく感じ、青色Bは暗く感じることから、このような変換式になっている。
YUV形式
ビデオ業界全体で、さまざまな YUV 形式が定義されています。
MSDNから各形式について説明があります。
クロマチャネル(色差Cb/Cr)は、知覚品質が大幅に失われることなく、ルミナンスチャネル(輝度Y)よりも低いサンプリングレートを持つことができます。
ルミナンス(輝度Y)のサンプルはクロスで表され、
クロマチャネル(色差Cb/Cr)のサンプルは円で表されます。
- 4:4:4 は、クロマチャネルのダウンサンプリングがないことを意味します。
- 4:2:2 は、垂直ダウンサンプリングを使用せずに、2:1 の水平ダウンサンプリングを意味します。
- 4:2:0 は、2:1 垂直ダウンサンプリングを使用して、2:1 の水平ダウンサンプリングを意味します。
- 4:1:1 は、垂直ダウンサンプリングを使用せずに、4:1 の水平ダウンサンプリングを意味します。
4:4:4 (32 bpp)
AYUV
4:2:2 (16 bpp)
YUY2
UYVY
4:2:0 (16 bpp)
IMC1
IMC3
4:2:0 (12 bpp)
IMC2
IMC4
YV12
NV12
4:4:4 形式、32ビット/ピクセル
AYUV
各ピクセルが4つの連続するバイトとしてエンコードされた、アルファの値が含まれます。最高クォリティの画質です。
4:2:2 形式、16ビット/ピクセル
各マクロピクセルは、4つの連続するバイトとしてエンコードされた2ピクセルです。 この結果、彩度の水平ダウンサンプリングが2倍になります。
YUY2
データを符号なしの char 値の配列として処理できます。最初のバイトには最初の y サンプル、2番目のバイトには最初の U (Cb) サンプル、3番目のバイトには第2の y (Cr) のサンプルが含まれます。
UYVY
YUY2 形式と同じですが、バイト順が反転される点が異なっています。
4:2:0 形式、ピクセルあたり 16 ビット
2 つの 4:2:0 16 ビット/ピクセル (bpp) 形式、 水色チャネルは、水平方向と垂直方向の両方の次元で 2 の係数でサブサンプリングされます。
IMC1
すべての Y サンプルは、符号なし char 値の配列としてメモリ内で最初に 表示 されます。 その後に、すべての V (Cr) サンプル、およびすべての U (Cb) サンプルが続きます。
Strideは、画面の幅 (バイト単位) です。
BYTE* pV = pY + (((Height + 15) & ~15) * Stride);
BYTE* pU = pY + (((((Height * 3) / 2) + 15) & ~15) * Stride);
IMC3
この形式は IMC1 と同じですが、UとVのプレーンがスワップされる点が異なっています。
4:2:0 形式、ピクセルあたり 12 ビット
4 つの 4:2:0 12-bpp 形式、チャネルチャネルは水平方向と垂直方向の両方の次元で 2 の係数でサブサンプリングされます。
IMC2
この形式は、次の違いを除いて IMC1 と同じです。V (Cr) と U (Cb) の行は、半ストライド境界でインターリーブされます。IMC1 よりもアドレス空間を効率的に使用できます。
IMC4
IMC2 と同じですが、U (Cb) と V (Cr) の行がスワップされます。
YV12
V 平面のストライドは、Y 平面のストライドの半分です。V プレーンには、Y 平面の半分の数の線が含まれている。 V プレーンの直後には、V 平面と同じストライドと線数を持つすべての U (Cb) サンプルが続きます。
NV12
Y 平面の直後には、パックされた U (Cb) サンプルと V (Cr) サンプルを含む符号なし char 値の配列が続きます。
変換部分のコード
# define CLIP(x) do{if(x < 0){x = 0;} else if(x > 255){x = 255;}} while(0)
# define CONVERT_R(Y, V) ((298 * (Y - 16) + 409 * (V - 128) + 128) >> 8)
# define CONVERT_G(Y, U, V) ((298 * (Y - 16) - 100 * (U - 128) - 208 * (V - 128) + 128) >> 8)
# define CONVERT_B(Y, U) ((298 * (Y - 16) + 516 * (U - 128) + 128) >> 8)
void NV12ToRGB(u8* rgbBuffer, u8* yuvBuffer, int width, int height)
{
u8* uvStart = yuvBuffer + width * height;
u8 y[2] = { 0, 0 };
u8 u = 0;
u8 v = 0;
int r = 0;
int g = 0;
int b = 0;
for (int rowCnt = 0; rowCnt < height; rowCnt++)
{
for (int colCnt = 0; colCnt < width; colCnt += 2)
{
u = *(uvStart + colCnt + 0);
v = *(uvStart + colCnt + 1);
for (int cnt = 0; cnt < 2; cnt++)
{
y[cnt] = yuvBuffer[rowCnt * width + colCnt + cnt];
r = CONVERT_R(y[cnt], v);
CLIP(r);
g = CONVERT_G(y[cnt], u, v);
CLIP(g);
b = CONVERT_B(y[cnt], u);
CLIP(b);
rgbBuffer[(rowCnt * width + colCnt + cnt) * 3 + 0] = (u8)r;
rgbBuffer[(rowCnt * width + colCnt + cnt) * 3 + 1] = (u8)g;
rgbBuffer[(rowCnt * width + colCnt + cnt) * 3 + 2] = (u8)b;
}
}
uvStart += width * (rowCnt % 2);
}
}