4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-23

概要

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参照。

YUV420は水平・垂直2×2ピクセルのうち、Cb信号を上2ピクセルから1ピクセル取り、Cr信号を下2ピクセルから1ピクセル取る方式である。これらを画像イメージで見ると以下になる(英語版Wikipedia参照)。
image.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の影響であると思われる。

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

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

あとがき

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

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?