DJI ドローンライブ映像のYUV画像をRGB画像に変換

本記事では、DJI ドローンの映像ストリーミングから取得したYUV画像をRGBコンバートについてご紹介します。

# DJI ドローンライブ映像のYUV画像取得
[ストリーミングからYUV画像取得](https://qiita.com/k-koh/items/e572f377e16bd79e0812)を参考してDJI Mobile SDKでドローンの映像ストリーミングからYUV画像取得します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/c64d3fe5-ac5e-b3a4-4dd0-bc2e19b62da2.png)

```java
    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とは](https://ja.wikipedia.org/wiki/YUV)、輝度信号Yと、2つの色差信号を使って表現される色空間。
 

# 何故YUVフォーマット
人間の目は明るさの変化には敏感だが, 色の変化には鈍感であるというわけで，色度を抑え、輝度により広い帯域やビット数を割くことにより、少ない損失で効率の良い伝送や圧縮を実現するフォーマット。

# RGB と YCbCr
RGB方式では、Red, Green, Blueの３つの原色を混ぜ合わせることで 色を表現する手法である。
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は暗く感じることから、このような変換式になっている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/39dc1826-b158-a233-3e87-ec9b9e0b7474.png)



# YUV形式
ビデオ業界全体で、さまざまな YUV 形式が定義されています。
[MSDN](https://docs.microsoft.com/ja-jp/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering?redirectedfrom=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 の水平ダウンサンプリングを意味します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/98af0b64-7afb-1292-ace5-e31b020a8bef.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/c6d6acdb-5547-1497-f68c-1a7bdf85fbac.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/a30e41f3-d54a-c69b-1427-c8b8a08598eb.png)


```
    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つの連続するバイトとしてエンコードされた、アルファの値が含まれます。最高クォリティの画質です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/1bdd0d63-1537-acfe-5d91-07ba3b2f22a9.png)

#### 4:2:2 形式、16ビット/ピクセル
各マクロピクセルは、4つの連続するバイトとしてエンコードされた2ピクセルです。 この結果、彩度の水平ダウンサンプリングが2倍になります。

##### YUY2
データを符号なしの char 値の配列として処理できます。最初のバイトには最初の y サンプル、2番目のバイトには最初の U (Cb) サンプル、3番目のバイトには第2の y (Cr) のサンプルが含まれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/11788606-b276-ecd1-2254-612b29fce8f3.png)

##### UYVY
YUY2 形式と同じですが、バイト順が反転される点が異なっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/eadf35d9-3622-4ecc-e944-f8b372089273.png)

#### 4:2:0 形式、ピクセルあたり 16 ビット
2 つの 4:2:0 16 ビット/ピクセル (bpp) 形式、 水色チャネルは、水平方向と垂直方向の両方の次元で 2 の係数でサブサンプリングされます。

##### IMC1
すべての Y サンプルは、符号なし char 値の配列としてメモリ内で最初に 表示 されます。 その後に、すべての V (Cr) サンプル、およびすべての U (Cb) サンプルが続きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/cfa58a5f-5404-a91f-459f-f9d11bb5e14c.png)

Strideは、画面の幅 (バイト単位) です。

```
BYTE* pV = pY + (((Height + 15) & ~15) * Stride);
BYTE* pU = pY + (((((Height * 3) / 2) + 15) & ~15) * Stride);
```

##### IMC3
この形式は IMC1 と同じですが、UとVのプレーンがスワップされる点が異なっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/bece5e25-47da-3ee2-b1d9-867611f48d7d.png)

#### 4:2:0 形式、ピクセルあたり 12 ビット
4 つの 4:2:0 12-bpp 形式、チャネルチャネルは水平方向と垂直方向の両方の次元で 2 の係数でサブサンプリングされます。

##### IMC2
この形式は、次の違いを除いて IMC1 と同じです。V (Cr) と U (Cb) の行は、半ストライド境界でインターリーブされます。IMC1 よりもアドレス空間を効率的に使用できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/c2b47b3a-e722-bfee-c185-56b22efb1147.png)

##### IMC4
 IMC2 と同じですが、U (Cb) と V (Cr) の行がスワップされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/54b14332-eacd-f5c7-16e6-638bf21b3a58.png)

##### YV12
 V 平面のストライドは、Y 平面のストライドの半分です。V プレーンには、Y 平面の半分の数の線が含まれている。 V プレーンの直後には、V 平面と同じストライドと線数を持つすべての U (Cb) サンプルが続きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/91975364-7edf-322d-786c-367cb85daa71.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/7253a747-b57d-674f-0e11-7a08726af684.png)


##### NV12
Y 平面の直後には、パックされた U (Cb) サンプルと V (Cr) サンプルを含む符号なし char 値の配列が続きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/26bc3ff8-242b-11dd-93c9-3c245bc1eb38.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/e2943c4a-fb11-d3ef-643c-3d7840bdfe0e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/08991bb9-a341-80ff-f683-cb4376077b07.png)

[YUVフォーマットの詳細な説明](https://www.codetd.com/ja/article/12328621)


####　変換部分のコード

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