色彩空间基础:从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就够了
所有可见光的颜色,都落在那个马蹄形区域内。而色温,就是黑体辐射体在不同温度下的颜色轨迹——普朗克轨迹。
你看,色温越低(比如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流程是这样的:
- 输入sRGB → 做伽马解码 → 得到线性RGB
- 线性RGB → 乘矩阵 → 得到XYZ
- XYZ → 归一化 → 得到(x,y)色度坐标
- (x,y) → 查表或计算 → 估计色温
- 同时:XYZ → Lab → 在ab平面上找灰点
- 综合色温估计和灰点信息 → 计算增益 → 校正
核心思路:色温估计靠的是色度图上的位置关系,灰点检测靠的是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)。查表比实时计算普朗克公式快两个数量级。