3. 色温估计
色温估计,说白了就是让相机猜出当前是什么光。你想想看,一张白纸在日光下是白的,在钨丝灯下发黄,在荧光灯下发绿。相机如果不做色温估计,那拍出来的颜色就全乱套了。
我个人习惯把色温估计分成三步走:先建立光源模型,再构建查找表,最后用插值算出精确值。咱们一步步来。
3.1 色温曲线:光源的数学模型
色温曲线不是随便画出来的。它描述的是黑体辐射体在不同温度下的色度轨迹。我刚开始做ISP时,总觉得这东西太理论,直到有一次在实验室里对着色温计发呆……嗯,后来才明白,这条曲线就是相机判断光源的“尺子”。
标准色温曲线在CIE 1931色度图上的走向是这样的:
| 色温 (K) | x 坐标 | y 坐标 | 典型光源 |
|---|---|---|---|
| 2000 | 0.5267 | 0.4133 | 烛光、日出日落 |
| 3200 | 0.4235 | 0.3990 | 钨丝灯 |
| 5000 | 0.3457 | 0.3585 | 日光灯、正午阳光 |
| 6500 | 0.3127 | 0.3290 | 阴天、D65标准光源 |
| 10000 | 0.2800 | 0.2880 | 蓝天阴影 |
这条曲线有个特点:低色温偏红黄,高色温偏蓝紫。我在项目中遇到过,有些算法直接把色温曲线拟合成多项式,但效果往往不好——因为真实光源并不完全在黑体轨迹上。
关键点:实际光源(比如荧光灯、LED)会偏离黑体轨迹。所以色温曲线只能作为参考,不能死板套用。
3.2 色温查找表构建
有了色温曲线,下一步就是建查找表。为什么要建表?因为实时计算色温曲线太慢了,查表快得多。
构建查找表的步骤,我一般这么干:
- 采样色温点:从2000K到10000K,每隔100K采一个点,共81个点。
- 计算对应色度坐标:用黑体辐射公式算出每个色温的(x, y)。
- 计算R/G、B/G比值:根据传感器光谱响应,把(x, y)转成R/G和B/G。
- 存储为表格:色温值作为索引,R/G和B/G作为数据。
代码实现大概是这样的:
// 色温查找表结构
typedef struct {
uint16_t color_temp; // 色温值 (K)
float rg_ratio; // R/G 比值
float bg_ratio; // B/G 比值
} AWB_LUT_Entry;
// 构建查找表
void build_awb_lut(AWB_LUT_Entry *lut, int num_entries) {
for (int i = 0; i < num_entries; i++) {
uint16_t ct = 2000 + i * 100; // 从2000K开始,步长100K
float x, y;
blackbody_chromaticity(ct, &x, &y); // 计算色度坐标
lut[i].color_temp = ct;
lut[i].rg_ratio = sensor_rg_from_xy(x, y); // 传感器相关转换
lut[i].bg_ratio = sensor_bg_from_xy(x, y);
}
}
我的经验:采样步长不要太大。我曾经用200K步长,结果在低色温区出现明显色偏。后来改成100K,效果就好多了。如果存储空间允许,50K步长更稳。
3.3 色温插值算法
查表只能给出离散点的值,但实际色温是连续的。这时候就需要插值。
常用的插值方法有三种:
| 方法 | 精度 | 计算量 | 适用场景 |
|---|---|---|---|
| 最近邻插值 | 低 | 极低 | 低端芯片、实时预览 |
| 线性插值 | 中 | 低 | 大多数场景,推荐使用 |
| 三次样条插值 | 高 | 中 | 高精度需求、后处理 |
我个人最常用的是线性插值。为什么?因为色温曲线在局部范围内近似线性,线性插值已经够用,而且计算量小。
线性插值的核心逻辑:
// 色温线性插值
float interpolate_rg(AWB_LUT_Entry *lut, int num_entries, uint16_t ct) {
// 边界处理
if (ct <= lut[0].color_temp) return lut[0].rg_ratio;
if (ct >= lut[num_entries-1].color_temp) return lut[num_entries-1].rg_ratio;
// 二分查找相邻点
int low = 0, high = num_entries - 1;
while (high - low > 1) {
int mid = (low + high) / 2;
if (lut[mid].color_temp <= ct) low = mid;
else high = mid;
}
// 线性插值
float t = (float)(ct - lut[low].color_temp) /
(lut[high].color_temp - lut[low].color_temp);
return lut[low].rg_ratio + t * (lut[high].rg_ratio - lut[low].rg_ratio);
}
避坑指南:我曾经在低色温区(2000K-3000K)直接用线性插值,结果出现了明显的颜色断层。后来发现,低色温区色温曲线弯曲度大,线性插值误差大。解决方案是在低色温区加密采样点,或者改用三次样条插值。
3.4 色温估计的整体流程
把上面这些串起来,色温估计的完整流程就是:
- 从图像中统计R、G、B通道的均值或灰度世界假设值。
- 计算R/G和B/G比值。
- 在色温查找表中搜索最接近的(R/G, B/G)组合。
- 用插值算法算出精确色温值。
- 根据色温值确定白平衡增益。
下面这张图展示了整个流程:
3.5 实际项目中的注意事项
做色温估计,有几个坑我踩过,分享给你:
- 光源不唯一:场景中可能有多个光源,比如室内混合光。这时候色温估计会失效,需要做区域分割。
- 传感器差异:不同传感器的光谱响应不同,色温查找表必须针对具体传感器标定。
- 温度漂移:传感器温度变化会影响R/G、B/G比值,高精度场景需要做温度补偿。
- 低照度噪声:暗光下R/G、B/G比值噪声大,色温估计会抖动。我一般会加时域滤波。
总结一下:色温估计的核心就是“查表+插值”。表建得好,插值选得对,色温估计就稳了。剩下的就是针对具体场景调参和做鲁棒性处理。