色彩空间基础:从RGB到色温的桥梁

做AWB算法,说白了就是在不同色彩空间里来回折腾。你想想看,摄像头拍到的原始数据是RGB,但我们要估计的色温却跟XYZ、Lab这些空间关系更密切。这一章,我就带你把这些空间之间的路给打通。

RGB与sRGB:别被名字骗了

RGB大家都熟,但sRGB跟它可不是一回事。我刚开始做图像处理时,就吃过这个亏——直接把sRGB当RGB算,结果颜色偏得一塌糊涂。

简单说,RGB是个大框架,sRGB是它下面一个具体的标准化版本。sRGB规定了三件事:

  • 白点:D65标准光源(色温6500K)
  • 伽马:约2.2的伽马校正曲线
  • 色域:覆盖CIE 1931色度图中约35%的区域

大多数消费级显示器、网络图片都默认用sRGB。你在做AWB时,如果输入图像标称sRGB,那就得按它的规矩来。

关键点:sRGB的伽马校正不是简单的幂函数,它在暗部有一段线性区。公式如下:

// sRGB 伽马解码(线性化)
float srgb_to_linear(float c) {
    if (c <= 0.04045f)
        return c / 12.92f;
    else
        return pow((c + 0.055f) / 1.055f, 2.4f);
}

// sRGB 伽马编码(非线性化)
float linear_to_srgb(float c) {
    if (c <= 0.0031308f)
        return c * 12.92f;
    else
        return 1.055f * pow(c, 1.0f / 2.4f) - 0.055f;
}

XYZ色彩空间:色度学的通用语

XYZ空间是CIE在1931年搞出来的,它跟设备无关。什么意思?就是不管你是用iPhone拍还是用单反拍,只要知道XYZ值,就能唯一确定一个颜色。

从sRGB到XYZ的转换,需要一个3x3矩阵。这个矩阵跟sRGB的白点和色域有关:

// sRGB (D65) 到 XYZ 的转换矩阵
const float M_srgb_to_xyz[3][3] = {
    {0.4124564f, 0.3575761f, 0.1804375f},
    {0.2126729f, 0.7151522f, 0.0721750f},
    {0.0193339f, 0.1191920f, 0.9503041f}
};

// 转换步骤
// 1. 先做伽马解码(线性化)
// 2. 再乘矩阵
void srgb_to_xyz(float r, float g, float b,
                 float &x, float &y, float &z) {
    float rl = srgb_to_linear(r);
    float gl = srgb_to_linear(g);
    float bl = srgb_to_linear(b);
    
    x = M_srgb_to_xyz[0][0] * rl + M_srgb_to_xyz[0][1] * gl + M_srgb_to_xyz[0][2] * bl;
    y = M_srgb_to_xyz[1][0] * rl + M_srgb_to_xyz[1][1] * gl + M_srgb_to_xyz[1][2] * bl;
    z = M_srgb_to_xyz[2][0] * rl + M_srgb_to_xyz[2][1] * gl + M_srgb_to_xyz[2][2] * bl;
}

我的习惯:在实际项目中,我会把矩阵乘法写成SIMD版本,一次处理4个像素。对于1080p图像,能省下好几毫秒。

色度图与色温:一张图看懂所有

XYZ空间有个副产品——CIE xy色度图。把XYZ归一化:

x = X / (X + Y + Z)
y = Y / (X + Y + Z)
z = Z / (X + Y + Z)  // z = 1 - x - y,所以只用x和y就够了

所有可见光的颜色,都落在那个马蹄形区域内。而色温,就是黑体辐射体在不同温度下的颜色轨迹——普朗克轨迹。

CIE 1931 色度图与普朗克轨迹 10000K 6500K (D65) 5000K 3200K 2800K 色温与颜色对应 • 低色温(暖):偏红/橙 • 中色温(中性):偏白 • 高色温(冷):偏蓝 • AWB目标:将场景映射到 D65附近 x y

你看,色温越低(比如2800K的钨丝灯),颜色越偏红橙;色温越高(比如10000K的阴天),颜色越偏蓝。AWB要做的,就是把不同色温下的白色物体,都校正回D65标准白。

Lab色彩空间:把颜色差异变成距离

XYZ虽然跟设备无关,但它有个毛病——人眼对颜色的感知不是线性的。比如,在绿色区域差0.1,人眼可能觉得差别很大;在蓝色区域差0.1,人眼可能觉得差不多。

Lab空间就是为解决这个问题而生的。它把颜色映射到一个近似均匀的感知空间。L是亮度,a是红绿轴,b是黄蓝轴。

从XYZ到Lab的转换:

// 参考白点(D65)
const float Xn = 0.95047f;
const float Yn = 1.00000f;
const float Zn = 1.08883f;

float f(float t) {
    if (t > 0.008856f)
        return pow(t, 1.0f / 3.0f);
    else
        return (7.787f * t) + (16.0f / 116.0f);
}

void xyz_to_lab(float X, float Y, float Z,
                float &L, float &a, float &b) {
    float fx = f(X / Xn);
    float fy = f(Y / Yn);
    float fz = f(Z / Zn);
    
    L = 116.0f * fy - 16.0f;
    a = 500.0f * (fx - fy);
    b = 200.0f * (fy - fz);
}

我曾经踩过的坑:在计算Lab时,如果Y/Yn小于0.008856,直接开立方会出问题。必须用线性近似公式。另外,L的范围是0~100,a和b的范围大约是-128~127,但实际值可能超出这个范围,做量化时要小心。

色彩空间转换在AWB中的角色

说了这么多,这些空间跟AWB到底什么关系?我整理了一张表:

色彩空间 在AWB中的用途 为什么用它
sRGB 输入图像格式,伽马校正 大多数相机/图像默认输出
线性RGB 色温估计的原始数据 去掉伽马,颜色混合才是线性的
XYZ 计算色度坐标(x,y) 设备无关,可映射到色温
Lab 灰点检测、白平衡评估 感知均匀,a/b轴直接反映色偏

我个人习惯的AWB流程是这样的:

  1. 输入sRGB → 做伽马解码 → 得到线性RGB
  2. 线性RGB → 乘矩阵 → 得到XYZ
  3. XYZ → 归一化 → 得到(x,y)色度坐标
  4. (x,y) → 查表或计算 → 估计色温
  5. 同时:XYZ → Lab → 在ab平面上找灰点
  6. 综合色温估计和灰点信息 → 计算增益 → 校正

核心思路:色温估计靠的是色度图上的位置关系,灰点检测靠的是Lab空间中的中性轴。两者结合,才能做出鲁棒的AWB。

色温与色度坐标的对应关系

实际项目中,我们不会每次都从XYZ算色温。更高效的做法是查表。下面是一份常用的色温-色度对照表:

色温 (K) x y 典型场景
2000 0.5267 0.4133 烛光、日落
2800 0.4512 0.4059 钨丝灯
3200 0.4235 0.3990 卤素灯、演播室
5000 0.3457 0.3585 日光、闪光灯
6500 0.3127 0.3290 D65标准、阴天
10000 0.2800 0.2880 蓝天阴影

实用技巧:在嵌入式平台上,我会把这张表做成LUT(查找表),用线性插值估计任意色温的(x,y)。查表比实时计算普朗克公式快两个数量级。

小结