概要
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参照)。
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倍の差があった。
※厳密に統計を取って比較したわけではなく、何回かプログラムを実行しておおよその比較である。
あとがき
C, Rustと比較してPythonが一番シンプルに実装できる。但し速度も100倍くらい遅い。
多分Pythonに詳しい人ならもっとシンプルに実装できると思う。
YUVからRGBへの変換の理屈については怪しい部分もあると思う。