4、RGB与Lab互转:通过XYZ中间空间的转换流程

说实话,RGB和Lab之间的转换,是很多图像处理新手的第一道坎。

我刚开始做色彩管理时,也在这上面栽过跟头。RGB转Lab,不能直接转。必须经过一个中间人——XYZ色彩空间。这个流程,说白了就是:RGB → XYZ → Lab,反过来也一样。

为什么非要经过XYZ?

你想想看,RGB是设备相关的。你的显示器、我的手机、他的相机,各自的RGB标准都不一样。而Lab是设备无关的,它描述的是人眼看到的颜色。

XYZ色彩空间,就是这两者之间的桥梁。它是一个标准的、数学上定义好的中间空间。所有颜色转换,都先统一到XYZ,再转到目标空间。

我在项目中遇到过一个问题:直接用RGB数值去匹配颜色,结果不同设备上显示完全不一样。后来才意识到,必须先把RGB转到Lab,才能做真正的颜色比较。

第一步:RGB转XYZ

这一步,核心是线性化。RGB值通常是经过伽马校正的,不能直接拿来用。

关键点:先做伽马校正的逆运算,把非线性RGB转成线性RGB。

以sRGB标准为例,转换公式如下:

# 1. 伽马校正逆运算(sRGB标准)
def srgb_to_linear(c):
    if c <= 0.04045:
        return c / 12.92
    else:
        return ((c + 0.055) / 1.055) ** 2.4

# 2. 线性RGB转XYZ(sRGB标准矩阵)
M = [
    [0.4124564, 0.3575761, 0.1804375],
    [0.2126729, 0.7151522, 0.0721750],
    [0.0193339, 0.1191920, 0.9503041]
]

def linear_rgb_to_xyz(r, g, b):
    x = M[0][0]*r + M[0][1]*g + M[0][2]*b
    y = M[1][0]*r + M[1][1]*g + M[1][2]*b
    z = M[2][0]*r + M[2][1]*g + M[2][2]*b
    return x, y, z

我的习惯:实际项目中,我会用NumPy做矩阵运算,比手写循环快得多。而且要注意,RGB值要归一化到[0,1]范围再计算。

第二步:XYZ转Lab

这一步,需要先定义白点。白点就是标准光源下的参考白色。最常用的是D65标准光源。

转换公式如下:

# 3. XYZ转Lab(D65白点)
Xn, Yn, Zn = 0.95047, 1.0, 1.08883  # D65白点

def xyz_to_lab(x, y, z):
    # 归一化
    fx = x / Xn
    fy = y / Yn
    fz = z / Zn
    
    # 非线性变换
    def f(t):
        if t > 0.008856:
            return t ** (1/3)
        else:
            return (7.787 * t) + (16/116)
    
    L = 116 * f(fy) - 16
    a = 500 * (f(fx) - f(fy))
    b = 200 * (f(fy) - f(fz))
    
    return L, a, b

我曾经踩过的坑:白点选错,结果全偏色。D65和D50的白点数值不一样,转换结果天差地别。做印刷相关项目时,一定要确认用哪个白点标准。

第三步:Lab转回RGB

反向转换,就是逆着来一遍:

# 4. Lab转XYZ
def lab_to_xyz(L, a, b):
    fy = (L + 16) / 116
    fx = a / 500 + fy
    fz = fy - b / 200
    
    def f_inv(t):
        if t > 0.206897:
            return t ** 3
        else:
            return (t - 16/116) / 7.787
    
    x = Xn * f_inv(fx)
    y = Yn * f_inv(fy)
    z = Zn * f_inv(fz)
    return x, y, z

# 5. XYZ转线性RGB(逆矩阵)
M_inv = [
    [ 3.2404542, -1.5371385, -0.4985314],
    [-0.9692660,  1.8760108,  0.0415560],
    [ 0.0556434, -0.2040259,  1.0572252]
]

def xyz_to_linear_rgb(x, y, z):
    r = M_inv[0][0]*x + M_inv[0][1]*y + M_inv[0][2]*z
    g = M_inv[1][0]*x + M_inv[1][1]*y + M_inv[1][2]*z
    b = M_inv[2][0]*x + M_inv[2][1]*y + M_inv[2][2]*z
    return r, g, b

# 6. 线性RGB转sRGB(伽马校正)
def linear_to_srgb(c):
    if c <= 0.0031308:
        return 12.92 * c
    else:
        return 1.055 * (c ** (1/2.4)) - 0.055

完整流程总结

方向 步骤 关键操作
RGB → Lab 3步 伽马逆校正 → 矩阵乘法 → 非线性变换
Lab → RGB 3步 非线性逆变换 → 逆矩阵乘法 → 伽马校正

核心要点:

  • RGB和Lab不能直接互转,必须经过XYZ
  • 转换前一定要确认RGB标准(sRGB、Adobe RGB等)
  • 白点选择影响最终结果,D65最常用
  • 伽马校正和逆校正不能搞反

嗯,这套流程看起来步骤多,但实际写代码时,封装成几个函数就清爽了。我一般会写一个ColorConverter类,把RGB、XYZ、Lab之间的转换都包进去,用的时候直接调方法就行。

你想想看,理解了这套流程,以后做颜色分析、图像增强、风格迁移,心里就有底了。Lab空间的L通道单独处理亮度,a和b通道处理颜色,很多图像预处理算法都是基于这个思路设计的。