天天看點

RGB與Lab轉換公式程式RGB轉Lab後的數值範圍

文章目錄

  • 公式
    • sRGB轉Lab
      • sRGB轉XYZ
      • XYZ轉Lab
    • Lab轉sRGB
      • Lab轉XYZ
      • XYZ轉sRGB
  • 程式
  • RGB轉Lab後的數值範圍

公式

我們擷取的圖檔通常屬于sRGB色彩空間,其中典型的圖檔格式如JPEG和PNG均屬于此。是以我們通常所講的RGB其實指的是sRGB,是以所謂的RGB與Lab的轉換,更嚴格一點講應該是sRGB與Lab的轉換。

sRGB不能直接轉換為Lab,需要用XYZ過渡:

sRGB -> XYZ -> Lab

反變換也一樣需要XYZ過渡:

Lab -> XYZ -> sRGB

sRGB轉Lab

sRGB轉XYZ

分兩步:

  1. sRGB通過gamma變換轉為RGB
  2. RGB通過線性映射轉為XYZ

sRGB通過gamma變換轉為RGB

做gamma變換有一個注意事項:必需先将資料變到[0, 1]範圍内。根據通常的認識,sRGB的資料範圍應當是[0, 255]。下面的公式中我們以小寫的rgb代表[0, 255]的數值範圍,大寫的RGB代表歸一化後的[0, 1]數值範圍。計算流程如下:

歸一化:

R = r / 255 G = g / 255 B = b / 255 R = r / 255 \\[2ex] G = g / 255 \\[2ex] B = b / 255 R=r/255G=g/255B=b/255

gamma變換(t 代表R, G, B):

t = { ( t + 0.055 1.055 ) 2.4 , i f : t > 0.04045 t 12.92 , e l s e t= \begin{cases} \big(\frac{t+ 0.055}{1.055}\big)^{2.4}, if: t>0.04045 \\[2ex] \frac {t} {12.92}, else \end{cases} t=⎩⎨⎧​(1.055t+0.055​)2.4,if:t>0.0404512.92t​,else​

根據 t > 0.04045 t > 0.04045 t>0.04045可得 [ ( t + 0.055 ) / 1.055 ] 2.4 > 0.0031308 [(t + 0.055)/1.055]^{2.4} > 0.0031308 [(t+0.055)/1.055]2.4>0.0031308,後者是反變換時的定義域,後面會用到。

線性變換:

X = 0.412453 ⋅ R + 0.357580 ⋅ G + 0.180423 ⋅ B Y = 0.212671 ⋅ R + 0.715160 ⋅ G + 0.072169 ⋅ B Z = 0.019334 ⋅ R + 0.119193 ⋅ G + 0.950227 ⋅ B X = 0.412453 \cdot R + 0.357580 \cdot G + 0.180423 \cdot B \\[2ex] Y = 0.212671 \cdot R + 0.715160 \cdot G + 0.072169 \cdot B \\[2ex] Z = 0.019334 \cdot R + 0.119193 \cdot G + 0.950227 \cdot B X=0.412453⋅R+0.357580⋅G+0.180423⋅BY=0.212671⋅R+0.715160⋅G+0.072169⋅BZ=0.019334⋅R+0.119193⋅G+0.950227⋅B

如果記線性變換矩陣為:

M R G B 2 X Y Z = [ 0.412453 0.357580 0.180423 0.212671 0.715160 0.072169 0.019334 0.119193 0.950227 ] M_{RGB2XYZ} = \begin{bmatrix} 0.412453 & 0.357580 & 0.180423 \\[2ex] 0.212671 & 0.715160 & 0.072169 \\[2ex] 0.019334 & 0.119193 & 0.950227 \end{bmatrix} MRGB2XYZ​=⎣⎢⎢⎢⎡​0.4124530.2126710.019334​0.3575800.7151600.119193​0.1804230.0721690.950227​⎦⎥⎥⎥⎤​

那麼線性變換可表示為如下式子,上标T表示轉置:

[ X Y Z ] = [ R G B ] M R G B 2 X Y Z T \begin{bmatrix} X & Y & Z \end{bmatrix} = \begin{bmatrix} R & G & B \end{bmatrix} M_{RGB2XYZ}^T [X​Y​Z​]=[R​G​B​]MRGB2XYZT​

上述線性變換矩陣在Observer. = 2°, Illuminant = D65條件下得到,該條件與白色參考點定義相關,更具體的内容可參考如下網站:

Understanding CIE Illuminants and Observers

XYZ轉Lab

下面用大寫的XYZ表示上述sRGB轉XYZ的結果,小寫xyz表示XYZ通過白色參考點歸一化後的結果,白色參考點使用Observer. = 2°, Illuminant = D65條件下的結果,是

xyz_ref_white = (0.95047, 1.0, 1.08883)

。那麼XYZ轉Lab的計算流程如下:

歸一化:

x = X / X r e f _ w h i t e y = Y / Y r e f _ w h i t e z = Z / Z r e f _ w h i t e x = X / X_{ref\_white} \\[2ex] y = Y / Y_{ref\_white} \\[2ex] z = Z / Z_{ref\_white} \\[2ex] x=X/Xref_white​y=Y/Yref_white​z=Z/Zref_white​

非線性變換(t 代表x, y, z):

t = { t 1 / 3 , i f : t > ( 6 29 ) 3 ( 1 3 ) ( 29 6 ) 2 ⋅ t + 16 116 , e l s e t = \begin{cases} t^{1/3}, if: t> (\frac {6}{29})^3 \\[2ex] (\frac {1}{3})(\frac {29}{6})^2 \cdot t + \frac {16}{116}, else \end{cases} t=⎩⎨⎧​t1/3,if:t>(296​)3(31​)(629​)2⋅t+11616​,else​

根據t的範圍 t > ( 6 / 29 ) 3 t > (6/29)^3 t>(6/29)3 可得 t 1 / 3 t^{1/3} t1/3 > 6/29,後者是反變換時的定義域,後面會用到。

(另外要噴一下,這個分段公式在交界處函數值不連續,也不知道是根據什麼道理設計出來的)

線性變換:

L = 116 ⋅ y − 16 a = 500 ⋅ ( x − y ) b = 200 ⋅ ( y − z ) L = 116 \cdot y - 16 \\[2ex] a = 500 \cdot (x - y) \\[2ex] b = 200 \cdot (y - z) L=116⋅y−16a=500⋅(x−y)b=200⋅(y−z)

Lab轉sRGB

反變換隻需把正變換的公式反着推一下就OK了,由于公式都比較簡單,此處省略推導過程,直接羅列計算公式:

Lab轉XYZ

線性變換:

y = ( L + 16 ) / 116 x = a / 500 + y z = y − b / 200 y = (L + 16) / 116 \\[2ex] x = a / 500 + y \\[2ex] z = y - b / 200 y=(L+16)/116x=a/500+yz=y−b/200

非線性變換(t 代表x, y, z):

t = { t 3 , i f : t > 6 / 29 ( t − 16 116 ) ⋅ 3 ⋅ ( 6 29 ) 2 , e l s e t = \begin{cases} t^3, if: t > 6/29 \\[2ex] (t- \frac {16}{116}) \cdot 3 \cdot (\frac {6}{29})^2, else \end{cases} t=⎩⎨⎧​t3,if:t>6/29(t−11616​)⋅3⋅(296​)2,else​

反歸一化:

X = x ⋅ X r e f _ w h i t e Y = y ⋅ Y r e f _ w h i t e Z = z ⋅ Z r e f _ w h i t e X = x \cdot X_{ref\_white} \\[2ex] Y = y \cdot Y_{ref\_white} \\[2ex] Z = z \cdot Z_{ref\_white} \\[2ex] X=x⋅Xref_white​Y=y⋅Yref_white​Z=z⋅Zref_white​

XYZ轉sRGB

線性變換:

[ R G B ] = [ X Y Z ] ( M R G B 2 X Y Z T ) − 1 \begin{bmatrix} R & G & B \end{bmatrix} = \begin{bmatrix} X & Y & Z \end{bmatrix} (M_{RGB2XYZ}^T)^{-1} [R​G​B​]=[X​Y​Z​](MRGB2XYZT​)−1

矩陣 M R G B 2 X Y Z M_{RGB2XYZ} MRGB2XYZ​見

sRGB轉XYZ

部分。

gamma變換(t 代表R, G, B):

t = { 1.055 ⋅ t 1 / 2.4 − 0.055 , i f : t > 0.0031308 12.92 ⋅ t , e l s e t = \begin{cases} 1.055 \cdot t^{1/2.4} - 0.055, if: t > 0.0031308 \\[2ex] 12.92 \cdot t, else \end{cases} t=⎩⎨⎧​1.055⋅t1/2.4−0.055,if:t>0.003130812.92⋅t,else​

裁減:

t = { 1 , i f : t > 1 0 , i f : t < 0 t , e l s e t = \begin{cases} 1, if: t >1 \\[2ex] 0, if: t<0 \\[2ex] t, else \end{cases} t=⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧​1,if:t>10,if:t<0t,else​

反歸一化:

r = R ⋅ 255 g = G ⋅ 255 b = B ⋅ 255 r = R \cdot 255 \\[2ex] g = G \cdot 255 \\[2ex] b = B \cdot 255 r=R⋅255g=G⋅255b=B⋅255

程式

程式大體上參考了skimage.color子產品的實作,但是這裡主要為了配合公式進行了解,是以并沒有廣泛考慮數值類型的處理。是以下面程式中如果輸入是RGB,那麼其數值類型應當是uint8,數值範圍是[0, 255]。

程式後面跟skimage.color子產品的計算結果進行了比較,來檢驗自己的實作有沒有出什麼詭異的問題。

# -*- coding: utf-8 -*-
import numpy as np
from skimage import color

MAT_RGB2XYZ = np.array([[0.412453, 0.357580, 0.180423],
                        [0.212671, 0.715160, 0.072169],
                        [0.019334, 0.119193, 0.950227]])

MAT_XYZ2RGB = np.linalg.inv(MAT_RGB2XYZ)

XYZ_REF_WHITE = np.array([0.95047, 1.0, 1.08883])


def rgb_to_lab(rgb):
    """
    Convert color space from rgb to lab

    Parameters:
    -----------
    rgb: numpy array, dtype = uint8
        3-dim array, shape is [H, W, C], C must be 3

    Returns:
    --------
    numpy array in lab color space, dtype = float
    """
    return xyz_to_lab(rgb_to_xyz(rgb))


def lab_to_rgb(lab):
    """
    Convert color space from lab to rgb

    Parameters:
    -----------
    lab: numpy array, dtype = float
        3-dim array, shape is [H, W, C], C must be 3

    Returns:
    --------
    numpy array in rgb color space, dtype = uint8
    """
    return xyz_to_rgb(lab_to_xyz(lab))


def rgb_to_xyz(rgb):
    """
    Convert color space from rgb to xyz

    Parameters:
    -----------
    rgb: numpy array, dtype = uint8
        3-dim array, shape is [H, W, C], C must be 3

    Returns:
    --------
    xyz: numpy array, dtype = float
        array in xyz color space
    """
    # convert dtype from uint8 to float
    xyz = rgb.astype(np.float64) / 255.0

    # gamma correction
    mask = xyz > 0.04045
    xyz[mask] = np.power((xyz[mask] + 0.055) / 1.055, 2.4)
    xyz[~mask] /= 12.92

    # linear transform
    xyz = xyz @ MAT_RGB2XYZ.T
    return xyz


def xyz_to_rgb(xyz):
    """
    Convert color space from xyz to rgb

    Parameters:
    -----------
    xyz: numpy array, dtype = float
        3-dim array, shape is [H, W, C], C must be 3

    Returns:
    --------
    rgb: numpy array, dtype = uint8
        array in rgb color space
    """
    # linear transform
    rgb = xyz @ MAT_XYZ2RGB.T

    # gamma correction
    mask = rgb > 0.0031308
    rgb[mask] = 1.055 * np.power(rgb[mask], 1.0 / 2.4) - 0.055
    rgb[~mask] *= 12.92

    # clip and convert dtype from float to uint8
    rgb = np.round(255.0 * np.clip(rgb, 0, 1)).astype(np.uint8)
    return rgb


def xyz_to_lab(xyz):
    """
    Convert color space from xyz to lab

    Parameters:
    -----------
    xyz: numpy array, dtype = float
        3-dim array, shape is [H, W, C], C must be 3

    Returns:
    --------
    lab: numpy array, dtype = float
        array in lab color space
    """
    # normalization
    xyz /= XYZ_REF_WHITE

    # nonlinear transform
    mask = xyz > 0.008856
    xyz[mask] = np.power(xyz[mask], 1.0 / 3.0)
    xyz[~mask] = 7.787 * xyz[~mask] + 16.0 / 116.0
    x, y, z = xyz[..., 0], xyz[..., 1], xyz[..., 2]

    # linear transform
    lab = np.empty(xyz.shape)
    lab[..., 0] = (116.0 * y) - 16.0  # L channel
    lab[..., 1] = 500.0 * (x - y)  # a channel
    lab[..., 2] = 200.0 * (y - z)  # b channel
    return lab


def lab_to_xyz(lab):
    """
    Convert color space from lab to xyz

    Parameters:
    -----------
    lab: numpy array, dtype = float
        3-dim array, shape is [H, W, C], C must be 3

    Returns:
    --------
    xyz: numpy array, dtype = float
        array in xyz color space
    """
    # linear transform
    l, a, b = lab[..., 0], lab[..., 1], lab[..., 2]
    xyz = np.empty(lab.shape)
    xyz[..., 1] = (l + 16.0) / 116.0
    xyz[..., 0] = a / 500.0 + xyz[..., 1]
    xyz[..., 2] = xyz[..., 1] - b / 200.0
    index = xyz[..., 2] < 0
    xyz[index, 2] = 0

    # nonlinear transform
    mask = xyz > 0.2068966
    xyz[mask] = np.power(xyz[mask], 3.0)
    xyz[~mask] = (xyz[~mask] - 16.0 / 116.0) / 7.787

    # de-normalization
    xyz *= XYZ_REF_WHITE
    return xyz


if __name__ == '__main__':
    rgb = np.array([[[150, 150, 0]]], dtype=np.uint8)
    xyz = rgb_to_xyz(rgb)
    lab = xyz_to_lab(xyz)
    xyz_ = lab_to_xyz(lab)
    rgb_ = xyz_to_rgb(xyz_)

    print('-' * 15, ' self defined function result ', '-' * 15)
    print('rgb:', rgb)
    print('xyz:', xyz)
    print('lab:', lab)
    print('xyz_inverse:', xyz_)
    print('rgb_inverse:', rgb_)

    xyz2 = color.rgb2xyz(rgb)
    lab2 = color.xyz2lab(xyz2)
    xyz2_ = color.lab2xyz(lab2)
    rgb2_ = color.xyz2rgb(xyz2_)
    rgb2_ = np.round(255.0 * np.clip(rgb2_, 0, 1)).astype(np.uint8)

    print('-' * 15, ' skimage result ', '-' * 15)
    print('rgb:', rgb)
    print('xyz:', xyz2)
    print('lab:', lab2)
    print('xyz_inverse:', xyz2_)
    print('rgb_inverse:', rgb2_)

           

RGB轉Lab後的數值範圍

前面的内容很容易就可以在網上搜到,但是Lab類型資料的取值範圍跟RGB類型的對應關系有點需要注意的地方:Lab的色域範圍比RGB寬廣,是以如果一個Lab的圖檔是由RGB轉換過來的,那麼它的色域無法覆寫Lab的定義範圍。

接下來我們仔細算一下。

Lab資料類型的取值範圍被定義為:

L:[0, 100]

a:[-128, 127]

b:[-128, 127]

下面我們寫一小段程式,周遊RGB所有數值并轉為Lab,然後看看轉換出來的Lab的數值範圍如何:

# -*- coding: utf-8 -*-
import numpy as np
from skimage import color

if __name__ == '__main__':
    rgb = np.zeros([1, 256 * 256 * 256, 3])
    index = 0
    for r in range(0, 256):
        print('\rr = %d' % r, end='')
        for g in range(0, 256):
            for b in range(0, 256):
                rgb[0, index, :] = np.array([r, g, b])
                index += 1
    print()
    rgb = np.uint8(rgb)
    lab = color.rgb2lab(rgb)

    print('L, min: %f, max: %f' % (np.min(lab[0, :, 0]), np.max(lab[0, :, 0])))
    print('a, min: %f, max: %f' % (np.min(lab[0, :, 1]), np.max(lab[0, :, 1])))
    print('b, min: %f, max: %f' % (np.min(lab[0, :, 2]), np.max(lab[0, :, 2])))
           

輸入結果如下:

L, min: 0.000000, max: 100.000000
a, min: -86.183030, max: 98.233054
b, min: -107.857300, max: 94.478122
           

容易發現,L通道可以完整覆寫Lab定義的數值範圍,但是a,b通道不行。

是以當我們需要對RGB轉換而來的Lab資料做歸一化時,a,b通道使用[-128, 127]的範圍不能真正讓數值歸一化到[0, 1]之間。

應當使用下式(含L通道):

L = L / 100.0 a = ( a + 86.183030 ) / 184.416084 b = ( b + 107.857300 ) / 202.335422 L = L / 100.0 \\[2ex] a = (a + 86.183030) / 184.416084 \\[2ex] b = (b + 107.857300) / 202.335422 L=L/100.0a=(a+86.183030)/184.416084b=(b+107.857300)/202.335422