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 色温查找表构建

有了色温曲线,下一步就是建查找表。为什么要建表?因为实时计算色温曲线太慢了,查表快得多。

构建查找表的步骤,我一般这么干:

  1. 采样色温点:从2000K到10000K,每隔100K采一个点,共81个点。
  2. 计算对应色度坐标:用黑体辐射公式算出每个色温的(x, y)。
  3. 计算R/G、B/G比值:根据传感器光谱响应,把(x, y)转成R/G和B/G。
  4. 存储为表格:色温值作为索引,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 色温估计的整体流程

把上面这些串起来,色温估计的完整流程就是:

  1. 从图像中统计R、G、B通道的均值或灰度世界假设值。
  2. 计算R/G和B/G比值。
  3. 在色温查找表中搜索最接近的(R/G, B/G)组合。
  4. 用插值算法算出精确色温值。
  5. 根据色温值确定白平衡增益。

下面这张图展示了整个流程:

色温估计流程图 色温估计完整流程 图像输入 统计R/G/B通道均值 计算R/G和B/G比值 色温查找表搜索 色温插值计算 输出色温值 说明 查找表预先构建 插值算法可选线性/样条 输出色温用于白平衡增益

3.5 实际项目中的注意事项

做色温估计,有几个坑我踩过,分享给你:

  • 光源不唯一:场景中可能有多个光源,比如室内混合光。这时候色温估计会失效,需要做区域分割。
  • 传感器差异:不同传感器的光谱响应不同,色温查找表必须针对具体传感器标定。
  • 温度漂移:传感器温度变化会影响R/G、B/G比值,高精度场景需要做温度补偿。
  • 低照度噪声:暗光下R/G、B/G比值噪声大,色温估计会抖动。我一般会加时域滤波。

总结一下:色温估计的核心就是“查表+插值”。表建得好,插值选得对,色温估计就稳了。剩下的就是针对具体场景调参和做鲁棒性处理。