YUV420をRGB888に変換するプログラムをpythonで作成

# 概要
YUV420の画像をどのようにRGB888に変換し、それをプログラミングするかを解説する。

# 目的
画像処理とpythonの勉強を行いたかったため。

# 作業環境
* Windows10 Home Insider Preview
  * バージョン:2004
  * OSビルドバージョン:19620
* WSL2 Ubuntu18.04
  * Remote-WSL(VSCode)
* python 3.6.9

# YUVイメージ
## YUVイメージ
YUVというよりもYCbCrからRGBへの変換であり、U, Vの値域を[-0.5, 0.5]に変換するとCb, Crになる。
これらについては[日本語版Wikipedia](https://jp.wikipedia.org/wiki/YUV)参照。

YUV420は水平・垂直2×2ピクセルのうち、Cb信号を上2ピクセルから1ピクセル取り、Cr信号を下2ピクセルから1ピクセル取る方式である。これらを画像イメージで見ると以下になる（[英語版Wikipedia](https://en.wikipedia.org/wiki/YUV)参照）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325874/a99af6be-9f08-f4cc-103e-3e4193ee9d41.png)

## YUVからRGBへの変換式
参考にした記事は以下。
http://koujinz.cocolog-nifty.com/blog/2009/03/rgbycbcr-a4a5.html
http://klabgames.tech.blog.jp.klab.com/archives/1054828175.html

日本語版WikipediaのITU-R BT.601の行列式からRGBからYUVへの以下の計算式となる。

```
Y = 0.299R + 0.587G + 0.114B
Cb = -0.169R - 0.331G + 0.500B
Cr = 0.500R - 0.419G - 0.081B
```

逆にYUVからRGBへの変換式は以下の計算式となる。

```
R = 1.000Y' + 1.402Cr'
G = 1.000Y' - 0.344Cb' - 0.714Cr'
B = 1.000Y' + 1.772Cb'
```

Y', Cr', Cb'はRangeとオフセットを加味してそれぞれ以下となる。

```
Y' = (Y - 16.0)×255.0 / 219.0
Cb' = (Cb - 128.0)×255.0 / 224.0
Cr' = (Cr - 128.0)×255.0 / 224.0
```

BT.601やBT.709はビデオ信号の規格なので、ブランキング区間などの関係でFull Rangeは使えない。その為、8bitのFull Range(0 - 255の256)ではなく以下のように縮小する必要がある。

```
Y : 16 - 235 (219)
Cb,Cr : 16 - 240 (224)
```

255/219, 255/224, とYの「-16」については上記のRangeとオフセットの影響で、(Cb - 128.0), (Cr - 128.0), は上述したUVをCb, Crに変換した際の[-0.5, 0.5](128/255)の影響であると思われる。

これらを纏めると以下になる。

```
R = (Y - 16.0)×255.0 / 219.0 + 1.402 * (Cr - 128.0)×255.0 / 224.0
G = (Y - 16.0)×255.0 / 219.0 - 0.344 * (Cb - 128.0)×255.0 / 224.0 - 0.714 * (Cr - 128.0)×255.0 / 224.0
B = (Y - 16.0)×255.0 / 219.0 + 1.772 * (Cb - 128.0)×255.0 / 224.0
```

さらにこれを固定少数に変換すると以下になる。

```
R = ((Y - 16.0)×298 + 408 * (Cr - 128.0)) >> 8
G = ((Y - 16.0)×298 - 100 * (Cb - 128.0) - 208 * (Cr - 128.0)) >> 8
B = ((Y - 16.0)×298 + 516 * (Cb - 128.0)) >> 8
```

※pythonで演算する際は別に固定少数で行う必要は無いが、最初はC言語でプログラムを組んだので固定少数で演算できるようにした。

上記演算で[0 - 255]の範囲を超える場合は0 or 255に丸める。

```
R = clip(((Y - 16.0)×298 + 408 * (Cr - 128.0)) >> 8)
G = clip(((Y - 16.0)×298 - 100 * (Cb - 128.0) - 208 * (Cr - 128.0)) >> 8)
B = clip(((Y - 16.0)×298 + 516 * (Cb - 128.0)) >> 8)
```

これで変換式が算出できた。

# プログラム
前述の変換式を使用してプログラムを組む。

## ソースコード
参考にした記事は以下。
https://shrex999.wordpress.com/2013/07/31/yuv-to-rgb-python-imaging-library/

作成したソースコードを以下に記載する。

```
import sys
import numpy as np
import time


class YUVData:
    def __init__(self):
        self.Y = 0
        self.U = 0
        self.V = 0


def clip(pix):
    if pix < 0:
        pix = 0
    elif pix > 255:
        pix = 255

    return pix


def set_rgbbuf(yuv):
    R = clip((298 * (yuv.Y - 16) + 409 * (yuv.V - 128) + 128) >> 8)
    G = clip(
        (298 * (yuv.Y - 16) - (100 * (yuv.U - 128) + 208 * (yuv.V - 128) - 128)) >> 8
    )
    B = clip((298 * (yuv.Y - 16) + 516 * (yuv.U - 128)) >> 8)

    return R, G, B


def main():
    start = time.clock()
    yuv_image = sys.argv[1]  # YUV420 file
    rgb_image = sys.argv[2]  # RGB888 file
    width = int(sys.argv[3])  # width
    height = int(sys.argv[4])  # height

    # Expand the yuv file to an array
    yuv_array = np.fromfile(yuv_image, np.uint8)

    # Setting uv offset
    u_data = width * height
    v_data = (width * height) + ((width * height) >> 2)
    
    # Initialize RGB buffer
    rgb_buf = [None] * (width * height * 3)
    id_1 = 0
    id_2 = width * 3

    yuv = YUVData()

    for row_cnt in range(0, height, 2):
        for col_cnt in range(0, width, 2):
            yuv.U = int(yuv_array[u_data])
            yuv.V = int(yuv_array[v_data])
            u_data += 1
            v_data += 1

            for cnt in range(0, 2):
                yuv.Y = int(yuv_array[row_cnt * width + col_cnt + cnt])
                rgb_buf[id_1], rgb_buf[id_1 + 1], rgb_buf[id_1 + 2] = set_rgbbuf(yuv)
                id_1 += 3
                yuv.Y = int(yuv_array[(row_cnt + 1) * width + col_cnt + cnt])
                rgb_buf[id_2], rgb_buf[id_2 + 1], rgb_buf[id_2 + 2] = set_rgbbuf(yuv)
                id_2 += 3

        id_1 += 3 * width
        id_2 += 3 * width

    with open(rgb_image, "wb") as f_rgb:
        for i in range(0, (width * height * 3)):
            f_rgb.write(rgb_buf[i].to_bytes(1, "little"))

    end = time.clock()
    print(end - start)


if __name__ == "__main__":
    main()
```

## プログラムの説明
プログラムの説明をしていく。

### 使用ライブラリ
以下をインポートして使用している。

```
import sys
import numpy as np
import time
```
sysはコマンドライン引数を使用する為（sys.arg)。
numpyはbinaryファイルを読み込むためのfromfileを使用する為に必要。
timeは処理時間を測定する為です。

### YUVファイルの読み込み
binaryファイルを読み込み、YUV用の配列に1byteずつ格納するため、numpyのfromfileを使用する。
```
yuv_array = np.fromfile(yuv_image, np.uint8)
```
open, read, seekでもできるが、これが一番すっきりしていてプログラムを組みやすかったので。
以下のページを参考にした。
https://code.i-harness.com/ja-jp/docs/numpy~1.14/generated/numpy.fromfile
https://sites.google.com/site/muxiayangpingnomemozhikichang/home/yan-jiu-yongmemo/python/binary-fileworead-guan-shude-yi-dingbaitozutsu-dumi-yumu

### RGB用の配列の初期化
```
rgb_buf = [None] * (width * height * 3)
```

1ピクセル辺りの情報量が24bit(RGB888)なので、配列のサイズは[画像サイズ * 3]となる。
配列には1byteのデータを格納する。

### Y成分用indexの初期化
```
id_1 = 0
id_2 = width * 3

```

YUVイメージにあるように[Y1,...], [Y7,...]、の2列ずつ変換を行うので、2列分のindexの初期値を算出しておく。


### YUV420からRGB888への変換処理1
```
    for row_cnt in range(0, height, 2):
        for col_cnt in range(0, width, 2):
            yuv.U = int(yuv_array[u_data])
            yuv.V = int(yuv_array[v_data])
            # print("u_data = %d u_data = %d" % (yuv.U, yuv.V))
            u_data += 1
            v_data += 1

            for cnt in range(0, 2):
                yuv.Y = int(yuv_array[row_cnt * width + col_cnt + cnt])
                rgb_buf[id_1], rgb_buf[id_1 + 1], rgb_buf[id_1 + 2] = set_rgbbuf(yuv)
                id_1 += 3
                yuv.Y = int(yuv_array[(row_cnt + 1) * width + col_cnt + cnt])
                rgb_buf[id_2], rgb_buf[id_2 + 1], rgb_buf[id_2 + 2] = set_rgbbuf(yuv)
                id_2 += 3

        id_1 += 3 * width
        id_2 += 3 * width
```

YUVイメージにあるように[Y1, Y2, Y7, Y8]と[U1]と[V1]でRGB888の1ピクセル分（3byte）のデータを作成する。
Y成分が4byte使用されるごとにU, V成分が1byte使用され、R, G, Bが算出されて3byte分の配列に格納される。
yuv_array[i]はnumpy.uint8型であり演算はintで実施する為、int型でキャストする。
1列分の算出が終わり、オフセットを調整する。2列分の算出を行っているため、id_1, id_2, を以下のように1列分スキップする必要がある。

```
id_1 += 3 * width
id_2 += 3 * width
```

### YUV420からRGB888への変換処理2
```
def clip(pix):
    if pix < 0:
        pix = 0
    elif pix > 255:
        pix = 255

    return pix


def set_rgbbuf(yuv):
    R = clip((298 * (yuv.Y - 16) + 409 * (yuv.V - 128) + 128) >> 8)
    G = clip(
        (298 * (yuv.Y - 16) - (100 * (yuv.U - 128) + 208 * (yuv.V - 128) - 128)) >> 8
    )
    B = clip((298 * (yuv.Y - 16) + 516 * (yuv.U - 128)) >> 8)

    return R, G, B
```

この演算は[YUVからRGBへの変換式]で示した通り。

### ファイルへの出力
```
    with open(rgb_image, "wb") as f_rgb:
        for i in range(0, (width * height * 3)):
            f_rgb.write(rgb_buf[i].to_bytes(1, "little"))
```

バイナリ形式でopenして(width * height * 3)分をwriteする。
int型をbytes型に変換して書き込みを実施。byteorderはlittleにしているが、8bitデータなのでどちらでも変わらない。

**プログラムの説明は以上**

## 処理速度
今回の変換処理と関係ないが、一応C, Rust, でも実装をしたので、Pythonを含めた3つの処理速度を測定した。
VGAサイズ(640x480)のYUV420の以下の画像を使用したところCとRustが10ms程度、Pythonが1000ms程度、でありおおよそ100倍の差があった。
※厳密に統計を取って比較したわけではなく、何回かプログラムを実行しておおよその比較である。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325874/57965146-7f9b-1dfe-621f-607d42ac7600.png)

# あとがき
C, Rustと比較してPythonが一番シンプルに実装できる。但し速度も100倍くらい遅い。
多分Pythonに詳しい人ならもっとシンプルに実装できると思う。
YUVからRGBへの変換の理屈については怪しい部分もあると思う。