プログラミング
記事内に商品プロモーションを含む場合があります

色空間変換色メモ(いろいろな色空間の変換)

Aru

色空間変換を調査したのでメモとしてまとめました。わかる人向けに細かな説明はしていませんが、参考になれば幸いです。

輝度定数の比較

輝度・色差変換(YPbPr)の係数は規格ごとに異なる。$Y = K_R R + K_G G + K_B B$ の $K_R, K_G, K_B$ を下表にまとめる。

規格用途$K_R$$K_G$$K_B$
ITU-R BT.601SDTV(NTSC/PAL)0.29900.58700.1140
ITU-R BT.709HDTV / sRGB0.21260.71520.0722
ITU-R BT.2020UHDTV(4K/8K)/ HDR0.26270.67800.0593
Display P3(D65)Apple 静止画0.2289750.6917390.079287
補足

KR/KG/KB は原色(R・G・B の色度座標)と基準白で決まる。本表の 4 規格はすべて D65 白点を共有しており、係数の差は原色の違いによる(白点が同じでも原色が動くと、D65 を作る混色比率が変わる)。

sRGB の線形化(EOTF / OETF)

EOTF / OETF とは

ガンマを数式化した「転送関数」の呼び名。EOTF(Electro-Optical Transfer Function)は電気信号 → 光の変換で、表示側の応答。保存されたコード値(非線形)を、ディスプレイが実際に出力する光強度(線形)へ戻す関数です。OETF(Opto-Electronic Transfer Function)は光 → 電気信号の変換で、撮像側の符号化。カメラが捉えた光(線形)をコード値(非線形)へ変換する関数です。

処理の方向でいうと、線形化は表示側の EOTF、ガンマ再付加は撮像側の OETFに相当します。つまり「保存された sRGB の値」を「光強度に比例する線形値」に直すのが EOTF、その逆で「線形値を sRGB のコード値に戻す」のが OETF です。

値域は 0-255 フルスケール。EOTF(線形化)。

$$C_{lin} = \begin{cases} \dfrac{C’_{sRGB}}{12.92}, & C’_{sRGB} \le 0.04045 \\ \left( \dfrac{C’_{sRGB} + 0.055}{1.055} \right)^{2.4}, & C’_{sRGB} > 0.04045 \end{cases}$$

OETF(ガンマ再付加)。

$$C’_{sRGB} = \begin{cases} 12.92 imes C_{lin}, & C_{lin} \le 0.0031308 \\ 1.055 imes C_{lin}^{\frac{1}{2.4}} – 0.055, & C_{lin} > 0.0031308 \end{cases}$$

どちらも単一の累乗($\gamma = 2.2$)ではなく、暗部を線形のまま($12.92$ 倍)にして黒付近の量子化誤差を抑え、それ以上を指数領域($2.4$ 乗)にする 2 段階の関数です。

def srgb_to_linear(c):
    c /= 255.0
    return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4

def linear_to_srgb(c):
    v = 12.92 * c if c <= 0.0031308 else 1.055 * c ** (1 / 2.4) - 0.055
    return round(v * 255)

YPbPr の一般式

線形 RGB を輝度 $Y$ と色差 $P_b, P_r$ に分離する。色差は $[-0.5, +0.5]$ に正規化した輝度との差。

$$Y = K_R R + K_G G + K_B B$$

$$P_b = \frac{0.5}{1 – K_B}(B – Y), \quad P_r = \frac{0.5}{1 – K_R}(R – Y)$$

各規格の順・逆変換行列

行列を 0-255 の RGB に直接掛けると $Y \in [0, 255]$、$P_b, P_r \in [-128, 128]$ のフルスケール値になる。

BT.601

RGB ➡︎ YPbPr

$$\begin{bmatrix} Y \\ P_b \\ P_r \end{bmatrix} = \begin{bmatrix} 0.299000 & 0.587000 & 0.114000 \\ -0.168736 & -0.331264 & 0.500000 \\ 0.500000 & -0.418688 & -0.081312 \end{bmatrix} \begin{bmatrix} R \\ G \\ B \end{bmatrix}$$

YPbPr ➡︎ RGB

$$\begin{bmatrix} R \\ G \\ B \end{bmatrix} = \begin{bmatrix} 1.000000 & 0.000000 & 1.402000 \\ 1.000000 & -0.344136 & -0.714136 \\ 1.000000 & 1.772000 & 0.000000 \end{bmatrix} \begin{bmatrix} Y \\ P_b \\ P_r \end{bmatrix}$$

BT601_F = [[0.299000, 0.587000, 0.114000],
           [-0.168736, -0.331264, 0.500000],
           [0.500000, -0.418688, -0.081312]]
BT601_R = [[1.000000, 0.000000, 1.402000],
           [1.000000, -0.344136, -0.714136],
           [1.000000, 1.772000, 0.000000]]

BT.709

RGB ➡︎ YPbPr

$$\begin{bmatrix} Y \\ P_b \\ P_r \end{bmatrix} = \begin{bmatrix} 0.212600 & 0.715200 & 0.072200 \\ -0.114572 & -0.385428 & 0.500000 \\ 0.500000 & -0.454153 & -0.045847 \end{bmatrix} \begin{bmatrix} R \\ G \\ B \end{bmatrix}$$

YPbPr ➡︎ RGB

$$\begin{bmatrix} R \\ G \\ B \end{bmatrix} = \begin{bmatrix} 1.000000 & 0.000000 & 1.574800 \\ 1.000000 & -0.187324 & -0.468124 \\ 1.000000 & 1.855600 & 0.000000 \end{bmatrix} \begin{bmatrix} Y \\ P_b \\ P_r \end{bmatrix}$$

BT709_F = [[0.212600, 0.715200, 0.072200],
           [-0.114572, -0.385428, 0.500000],
           [0.500000, -0.454153, -0.045847]]
BT709_R = [[1.000000, 0.000000, 1.574800],
           [1.000000, -0.187324, -0.468124],
           [1.000000, 1.855600, 0.000000]]

BT.2020

RGB ➡︎ YPbPr

$$\begin{bmatrix} Y \\ P_b \\ P_r \end{bmatrix} = \begin{bmatrix} 0.262700 & 0.678000 & 0.059300 \\ -0.139600 & -0.360400 & 0.500000 \\ 0.500000 & -0.459800 & -0.040200 \end{bmatrix} \begin{bmatrix} R \\ G \\ B \end{bmatrix}$$

YPbPr ➡︎ RGB

$$\begin{bmatrix} R \\ G \\ B \end{bmatrix} = \begin{bmatrix} 1.000000 & 0.000000 & 1.474600 \\ 1.000000 & -0.164600 & -0.571400 \\ 1.000000 & 1.881400 & 0.000000 \end{bmatrix} \begin{bmatrix} Y \\ P_b \\ P_r \end{bmatrix}$$

BT2020_F = [[0.262700, 0.678000, 0.059300],
            [-0.139600, -0.360400, 0.500000],
            [0.500000, -0.459800, -0.040200]]
BT2020_R = [[1.000000, 0.000000, 1.474600],
            [1.000000, -0.164600, -0.571400],
            [1.000000, 1.881400, 0.000000]]

Display P3

RGB ➡︎ YPbPr

$$\begin{bmatrix} Y \\ P_b \\ P_r \end{bmatrix} = \begin{bmatrix} 0.228975 & 0.691739 & 0.079287 \\ -0.124346 & -0.375654 & 0.500000 \\ 0.500000 & -0.448583 & -0.051417 \end{bmatrix} \begin{bmatrix} R \\ G \\ B \end{bmatrix}$$

YPbPr ➡︎ RGB

$$\begin{bmatrix} R \\ G \\ B \end{bmatrix} = \begin{bmatrix} 1.000000 & 0.000000 & 1.542051 \\ 1.000000 & -0.211064 & -0.510439 \\ 1.000000 & 1.841426 & 0.000000 \end{bmatrix} \begin{bmatrix} Y \\ P_b \\ P_r \end{bmatrix}$$

P3_F = [[0.228975, 0.691739, 0.079287],
        [-0.124346, -0.375654, 0.500000],
        [0.500000, -0.448583, -0.051417]]
P3_R = [[1.000000, 0.000000, 1.542051],
        [1.000000, -0.211064, -0.510439],
        [1.000000, 1.841426, 0.000000]]

mul3 の使い方

順変換・逆変換はどれも 3×3 行列の積なので、mul3 という共通関数 1 つで済ませる。行列の各行と入力タプルの各要素を掛けて合計した 3 要素を返す。

def mul3(matrix, v):
    a, b, c = v
    return [row[0]*a + row[1]*b + row[2]*c for row in matrix]

使い方。行列は各規格の節の「Python 係数」にある。_F が順変換(RGB → YPbPr)、_R が逆変換(YPbPr → RGB)。

rgb = (255, 0, 51)
ypbpr = mul3(BT709_F, rgb)  # Y, Pb, Pr(Y: 0-255 / Pb, Pr: -128..128)
rgb2 = mul3(BT709_R, ypbpr) # 逆変換で元に戻る

YPbPr → 輝度・色相・彩度変換

色差平面 $(P_b, P_r)$ を極座標とみなして、彩度 $S$(色差ベクトルの長さ)と色相 $\theta$(角度)を取り出す。輝度 $Y$ はそのまま使う。

$$S = \sqrt{P_b^2 + P_r^2}, \quad \theta = \operatorname{atan2}(P_r, P_b)$$

import math

def ypbpr_to_hs(y, pb, pr):
    hue = math.atan2(pr, pb)
    sat = math.hypot(pb, pr)
    return y, hue, sat

輝度・色相・彩度変換 → YPbPr

逆に、輝度 $Y$、彩度 $S$、色相 $\theta$ から色差 $P_b, P_r$ を復元する。

$$P_b = S\cos\theta, \quad P_r = S\sin\theta$$

def hs_to_ypbpr(y, hue, sat):
    return y, sat * math.cos(hue), sat * math.sin(hue)

彩度を $s$ 倍・色相を $\Delta\theta$ 回転したければ、変換した $(Y, \theta, S)$ を書き換えて($S’ = S \cdot s$、$\theta’ = \theta + \Delta\theta$)から戻すだけです。

RGB ↔ HSV

色相 $H$ は RGB の最大値・最小値から決まり、HSV は最大値 $V$ を明度軸に使う。

$$V = \max(R,G,B), \quad S = \frac{\max – \min}{\max}$$

$$H = \begin{cases} 60\left(\dfrac{G-B}{\max-\min} \bmod 6\right), & \max = R \\ 60\left(\dfrac{B-R}{\max-\min} + 2\right), & \max = G \\ 60\left(\dfrac{R-G}{\max-\min} + 4\right), & \max = B \end{cases}$$

def rgb_to_hsv(rgb):
    r, g, b = [c / 255.0 for c in rgb]
    mx, mn = max(r, g, b), min(r, g, b)
    d = mx - mn
    if d == 0:
        return 0.0, 0.0, mx
    h = (60 * (((g - b) / d) % 6)) if mx == r else \
        (60 * ((b - r) / d + 2)) if mx == g else \
        (60 * ((r - g) / d + 4))
    s = 0.0 if mx == 0 else d / mx
    return h, s, mx

def hsv_to_rgb(hsv):
    h, s, v = hsv
    c = v * s
    x = c * (1 - abs((h / 60) % 2 - 1))
    m = v - c
    if h < 60:   r, g, b = c, x, 0.0
    elif h < 120: r, g, b = x, c, 0.0
    elif h < 180: r, g, b = 0.0, c, x
    elif h < 240: r, g, b = 0.0, x, c
    elif h < 300: r, g, b = x, 0.0, c
    else:        r, g, b = c, 0.0, x
    return round((r + m) * 255), round((g + m) * 255), round((b + m) * 255)

RGB ↔ HLS

HLS は明度軸に最大値と最小値の平均 $L$ を使う。色相 $H$ は HSV と同じ式。

$$L = \frac{\max + \min}{2}$$

$$S = \begin{cases} \dfrac{\max – \min}{\max + \min}, & L \le 0.5 \\ \dfrac{\max – \min}{2 – \max – \min}, & L > 0.5 \end{cases}$$

def rgb_to_hls(rgb):
    r, g, b = [c / 255.0 for c in rgb]
    mx, mn = max(r, g, b), min(r, g, b)
    d = mx - mn
    l = (mx + mn) / 2
    if d == 0:
        return 0.0, l, 0.0
    h = (60 * (((g - b) / d) % 6)) if mx == r else \
        (60 * ((b - r) / d + 2)) if mx == g else \
        (60 * ((r - g) / d + 4))
    s = d / (mx + mn) if l <= 0.5 else d / (2 - mx - mn)
    return h, l, s

def _h2c(p, q, t):
    if t < 0: t += 1
    if t > 1: t -= 1
    if t < 1/6: return p + (q - p) * 6 * t
    if t < 1/2: return q
    if t < 2/3: return p + (q - p) * (2/3 - t) * 6
    return p

def hls_to_rgb(hls):
    h, l, s = hls
    if s == 0:
        v = round(l * 255)
        return v, v, v
    q = l * (1 + s) if l < 0.5 else l + s - l * s
    p = 2 * l - q
    h /= 360
    return (round(_h2c(p, q, h + 1/3) * 255),
            round(_h2c(p, q, h) * 255),
            round(_h2c(p, q, h - 1/3) * 255))

sRGB ↔ Display P3(線形 RGB 間)

線形 RGB 同士の変換。sRGB と Display P3 は原色が異なるため、CIE XYZ 経由で導かれる行列を掛ける。

sRGB → Display P3。

$$\begin{bmatrix} R_{P3} \\ G_{P3} \\ B_{P3} \end{bmatrix} = \begin{bmatrix} 0.822462 & 0.177538 & 0.000000 \\ 0.033194 & 0.966806 & 0.000000 \\ 0.017083 & 0.072397 & 0.910520 \end{bmatrix} \begin{bmatrix} R_{sRGB} \\ G_{sRGB} \\ B_{sRGB} \end{bmatrix}$$

Display P3 → sRGB。

$$\begin{bmatrix} R_{sRGB} \\ G_{sRGB} \\ B_{sRGB} \end{bmatrix} = \begin{bmatrix} 1.224940 & -0.224940 & 0.000000 \\ -0.042057 & 1.042057 & 0.000000 \\ -0.019638 & -0.078636 & 1.098274 \end{bmatrix} \begin{bmatrix} R_{P3} \\ G_{P3} \\ B_{P3} \end{bmatrix}$$

変換関数。mul3 は「mul3 の使い方」の節で定義した共通関数を再利用する。

SRGB_TO_P3 = [[0.822462, 0.177538, 0.000000],
              [0.033194, 0.966806, 0.000000],
              [0.017083, 0.072397, 0.910520]]
P3_TO_SRGB = [[1.224940, -0.224940, 0.000000],
              [-0.042057, 1.042057, 0.000000],
              [-0.019638, -0.078636, 1.098274]]

linear_srgb = [srgb_to_linear(c) for c in rgb]     # 0-255 → 線形 0-1
linear_p3 = mul3(SRGB_TO_P3, linear_srgb)           # → 線形 Display P3

注意点

Gamut外(色域外)

大きく補正すると逆変換後の RGB が $[0, 255]$ を超える。特に OETF は負値の累乗で NaN になるため、逆変換直後にクリップする。

演算精度

8bit 整数のまま処理すると丸め誤差やトーンジャンプ(バンディング)が起きる。内部は floatで処理し、最後に 0-255 へ丸める。

def clamp(v, lo=0.0, hi=255.0):
    return lo if v < lo else hi if v > hi else v

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です

ABOUT ME
ある/Aru
ある/Aru
IT&機械学習エンジニア/ファイナンシャルプランナー(CFP®)
専門分野は並列処理・画像処理・機械学習・ディープラーニング。プログラミング言語はC, C++, Go, Pythonを中心として色々利用。現在は、Kaggle, 競プロなどをしながら悠々自適に活動中 保有資格:CFP, マンション管理士、管理業務主任、宅建士など
記事URLをコピーしました