4. 白平衡与色彩校正:灰度世界假设、色温估计、AWB算法实现、色彩校正矩阵(CCM)
白平衡和色彩校正,说白了就是让相机「看懂」颜色。人眼有很强的色彩恒常性——一张白纸在日光灯下是白的,在烛光下你看着还是白的。但传感器没这个本事,它老老实实记录光谱能量分布,结果就是偏色。我入行时调的第一块 sensor,室内拍出来跟泡了酱油似的,从那以后我就对 AWB 格外上心。
4.1 色温与偏色原理
色温用开尔文(K)表示。低色温偏红黄,高色温偏蓝青。典型场景:
| 光源 | 色温范围(K) | 偏色倾向 |
|---|---|---|
| 烛光 | 1500 - 2000 | 深红 |
| 白炽灯 | 2500 - 3000 | 橙黄 |
| 日光灯 | 4000 - 5000 | 中性偏绿 |
| 正午日光 | 5000 - 6500 | 中性 |
| 阴天 | 6500 - 8000 | 偏蓝 |
| 蓝天阴影 | 8000 - 12000 | 深蓝 |
传感器 R/G/B 通道对不同色温的响应曲线不一样。低色温下 R 通道增益要压低,B 通道要拉高;高色温反过来。这就是 AWB 要干的事——找到合适的 R/G/B 增益,把中性色拉回中性。
4.2 灰度世界假设
这是最经典的 AWB 方法。假设场景中所有颜色的平均反射率是中性灰,即 R、G、B 三个通道的均值相等。
算法步骤很简单:
- 计算整帧图像 R、G、B 通道的均值:
R_avg、G_avg、B_avg - 计算各通道增益:
R_gain = G_avg / R_avg,B_gain = G_avg / B_avg - 对每个像素应用增益:
R_out = R_in * R_gain,B_out = B_in * B_gain
核心公式:
R_gain = G_avg / R_avg
B_gain = G_avg / B_avg
G_gain = 1.0 (通常固定)
我在项目中遇到过一个问题:当场景中大面积是纯色(比如绿草地、红墙),灰度世界假设就失效了。整帧偏绿时,G_avg 本来就高,算出来的 R_gain 和 B_gain 会偏大,结果反而过校正。所以实际工程里很少直接用全局灰度世界,而是结合其他方法。
4.3 色温估计方法
更靠谱的做法是先估计色温,再查表或插值得到增益。常见方法:
- 基于 R/G 和 B/G 的色温曲线:在不同色温下拍摄灰卡,记录 R/G 和 B/G 比值,拟合出一条色温曲线。运行时根据当前 R/G、B/G 反推色温。
- 普朗克轨迹法:利用黑体辐射轨迹在色度图上的投影,把 R/G、B/G 映射到色温。
- 学习型方法:用大量标定数据训练一个回归模型,输入 R/G、B/G 统计量,输出色温。
经验之谈: 我建议在 lab 里至少标定 5 个色温点(2300K、2850K、4000K、5000K、6500K),然后做线性插值。色温曲线不是完全线性的,但分段线性在工程上够用。我曾经试过只用 3 个点,结果在混合光源场景下偏色严重,后来加到 7 个点才稳定。
4.4 AWB 算法实现
一个实用的 AWB 流程通常包含以下步骤:
- 统计信息提取:将图像分成 N×N 块,每块计算 R/G、B/G 均值。
- 有效区域筛选:去掉过暗(噪声大)、过亮(饱和)、饱和度太高的块。只保留接近中性色的区域。
- 色温估计:用筛选后的块计算全局 R/G、B/G,映射到色温。
- 增益计算:根据色温查表得到 R_gain、B_gain。
- 增益平滑:对帧间增益做低通滤波,避免闪烁。
- 像素级校正:对每个像素应用增益。
注意: 增益平滑的时间常数要小心。太大会导致 AWB 响应慢,用户从室内走到室外,画面要好几秒才变正常;太小则增益抖动,画面会一闪一闪的。我一般用 0.8~0.95 的 IIR 系数,具体看帧率。
下面是一个简化的 AWB 实现片段:
// 简化版 AWB 核心逻辑
void awb_process(uint16_t *r, uint16_t *g, uint16_t *b, int width, int height) {
// 1. 分块统计
int blocks = 16;
float rg_avg = 0, bg_avg = 0;
int valid_count = 0;
for (int by = 0; by < blocks; by++) {
for (int bx = 0; bx < blocks; bx++) {
float block_r = 0, block_g = 0, block_b = 0;
int pixels = 0;
for (int y = by * height/blocks; y < (by+1) * height/blocks; y++) {
for (int x = bx * width/blocks; x < (bx+1) * width/blocks; x++) {
int idx = y * width + x;
block_r += r[idx];
block_g += g[idx];
block_b += b[idx];
pixels++;
}
}
block_r /= pixels;
block_g /= pixels;
block_b /= pixels;
// 2. 有效区域筛选:亮度适中,饱和度低
float rg = block_r / block_g;
float bg = block_b / block_g;
float sat = fabs(rg - 1.0) + fabs(bg - 1.0);
if (sat < 0.3 && block_g > 32 && block_g < 224) {
rg_avg += rg;
bg_avg += bg;
valid_count++;
}
}
}
if (valid_count == 0) return;
rg_avg /= valid_count;
bg_avg /= valid_count;
// 3. 查表得到增益(简化:直接按灰度世界)
float r_gain = 1.0 / rg_avg;
float b_gain = 1.0 / bg_avg;
// 4. 应用增益
for (int i = 0; i < width * height; i++) {
r[i] = clamp(r[i] * r_gain, 0, 255);
b[i] = clamp(b[i] * b_gain, 0, 255);
}
}
4.5 色彩校正矩阵(CCM)
AWB 做完后,颜色基本不偏了,但还不够准。因为 sensor 的滤色片和人的视觉系统响应不同,需要用一个 3×3 矩阵把 sensor RGB 映射到标准色彩空间(比如 sRGB)。这就是 CCM。
CCM 的形式:
[ R_out ] [ a11 a12 a13 ] [ R_in ]
[ G_out ] = [ a21 a22 a23 ] * [ G_in ]
[ B_out ] [ a31 a32 a33 ] [ B_in ]
矩阵系数通过拍摄标准色卡(比如 X-Rite ColorChecker)来标定。最小二乘法拟合,让输出色块和标准值的色差最小。
标定流程:
- 在标准光源下拍摄色卡
- 提取每个色块的 sensor RGB 值
- 获取色卡的标准 sRGB 值
- 用线性回归求解 3×3 矩阵
- 在验证集上评估 ΔE 色差
我踩过一个坑:CCM 系数对光源很敏感。在 D65 下标定的矩阵,拿到 A 光源下用,颜色会偏。所以量产时通常要准备多组 CCM,根据 AWB 估计的色温做插值。嗯,这里要注意,CCM 插值不能直接插系数,要在色度空间做,否则会出现负值或奇异矩阵。
4.6 AWB + CCM 联合流程
整个白平衡与色彩校正的 pipeline 是这样的:
这个流程里,AWB 和 CCM 是耦合的。AWB 先把白点拉正,CCM 再把整体色域映射准确。顺序不能颠倒——如果先做 CCM 再做 AWB,CCM 会把偏色也一起映射进去,后面 AWB 就很难拉回来。
工程建议: 调试时先固定 AWB 增益,单独调 CCM;CCM 收敛后再放开 AWB 一起调。分开调能更快定位问题。我一般会在 ISP tuning 工具里同时显示校正前后的色卡色块和 ΔE 值,一眼就能看出哪个环节有问题。
4.7 常见问题与避坑
- 混合光源场景:室内有日光灯又有窗边日光,AWB 会无所适从。我建议在这种场景下做区域 AWB,不同区域用不同增益,但边缘会有色差,需要做平滑过渡。
- 低照度噪声放大:暗光下 R/B 通道增益很大,噪声也被放大。我曾经在 10 lux 下拍出来全是彩色噪点。解决办法是限制最大增益,或者结合降噪模块一起做。
- CCM 负值问题:标定出来的 CCM 系数可能有负值,导致某些像素溢出或变负。需要做 clamp 和防溢出处理。
- 肤色保护:AWB 和 CCM 调得太「准」时,肤色可能会偏黄或偏青。很多 ISP 会在 AWB 里加一个肤色区域保护逻辑,不让增益把肤色拉偏。
避坑指南: 我曾经在一个项目里直接用灰度世界假设做 AWB,结果用户拍了一面红墙,画面直接变青。后来我加了有效区域筛选,只保留中性色块参与统计,问题就解决了。记住:AWB 不是让整帧平均变灰,而是让场景中的中性色变灰。
白平衡和色彩校正,说到底是让相机「学会」人眼的色彩恒常性。没有完美的算法,只有适合场景的工程取舍。调好 AWB 和 CCM,你的图像就成功了一半。