公式
我们获取的图片通常属于sRGB色彩空间,其中典型的图片格式如JPEG和PNG均属于此。所以我们通常所讲的RGB其实指的是sRGB,所以所谓的RGB与Lab的转换,更严格一点讲应该是sRGB与Lab的转换。
sRGB不能直接转换为Lab,需要用XYZ过渡:sRGB -> XYZ -> Lab
。
反变换也一样需要XYZ过渡:Lab -> XYZ -> sRGB
。
sRGB转Lab
sRGB转XYZ
分两步:
- sRGB通过gamma变换转为RGB
- 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.0193340.3575800.7151600.1191930.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
[XYZ]=[RGB]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_whitey=Y/Yref_whitez=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_whiteY=y⋅Yref_whiteZ=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}
[RGB]=[XYZ](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