色温估计基础:从假设到实战

色温估计,说白了就是让相机知道「现在是什么光」。我刚开始做ISP时,觉得这步很简单——不就是算个平均值吗?后来被各种奇葩场景教做人,才明白这里面的门道有多深。

这一章我们聊三种最经典的估计方法。它们虽然简单,但直到今天,很多商用方案里依然能看到它们的影子。

灰度世界假设

这个假设很直观:一张色彩丰富的照片,所有颜色的平均值应该是灰色。你想想看,如果场景里红绿蓝分布均匀,那R、G、B三个通道的均值应该差不多。

具体做法分三步:

  1. 计算整张图R、G、B通道的均值:R_avg, G_avg, B_avg
  2. 计算全局灰度值:Gray = (R_avg + G_avg + B_avg) / 3
  3. 计算各通道增益:R_gain = Gray / R_avg, G_gain = Gray / G_avg, B_gain = Gray / B_avg

代码实现也很直接:

// 灰度世界假设的C实现片段
void gray_world_awb(uint16_t* raw_r, uint16_t* raw_g, uint16_t* raw_b, 
                    int width, int height, float* r_gain, float* b_gain) {
    uint64_t sum_r = 0, sum_g = 0, sum_b = 0;
    int total = width * height;
    
    for (int i = 0; i < total; i++) {
        sum_r += raw_r[i];
        sum_g += raw_g[i];
        sum_b += raw_b[i];
    }
    
    float avg_r = (float)sum_r / total;
    float avg_g = (float)sum_g / total;
    float avg_b = (float)sum_b / total;
    
    float gray = (avg_r + avg_g + avg_b) / 3.0f;
    
    *r_gain = gray / avg_r;
    *b_gain = gray / avg_b;
    // G通道增益通常固定为1.0
}
踩坑提醒: 我曾经在一个全是蓝色包装的工厂流水线项目里用灰度世界,结果白平衡完全崩了。场景里90%都是蓝色,平均值根本不灰。这个假设的致命弱点就是——场景必须色彩丰富。

完美反射假设

这个方法换个思路:图像中最亮的像素点,应该是纯白色。换句话说,场景里总有一些区域是白色或高光,它们反射了光源的真实颜色。

我习惯这么理解:你拿一张白纸放在不同灯光下,白纸看起来会偏色。但如果你知道白纸本该是白的,就能反推出光源颜色。

实现步骤:

  1. 找到图像中最亮的N%像素(通常取前1%-5%)
  2. 计算这些像素的R、G、B均值
  3. 用这些均值计算增益,和灰度世界类似

两种方法的对比:

方法 核心假设 适用场景 弱点
灰度世界 场景平均色为灰 色彩丰富的自然场景 单色场景失效
完美反射 最亮像素为白 有高光/白色物体的场景 无高光时失效
我的经验: 实际项目中,我经常把两种方法做个加权融合。比如先算灰度世界的结果,再算完美反射的结果,然后根据场景的「色彩丰富度」和「高光占比」动态调权重。效果比单独用任何一种都稳。

基于色温曲线的估计方法

前面两种方法都是「盲猜」,没有利用任何先验知识。而色温曲线法,说白了就是查表+插值。

我们先在实验室标定好:在不同色温的光源下(比如D65、D50、A光源),拍一张灰卡,记录下R/G和B/G的比值。把这些点连成一条曲线,就是色温曲线。

实际估计时:

  1. 计算当前图像的R/G和B/G
  2. 在色温曲线上找到最近的点
  3. 用插值算出对应的色温和增益

嗯,这里要注意:色温曲线不是一条直线,它通常是一条平滑的弧线。因为不同传感器的色彩响应不同,每条曲线都是针对特定传感器标定的。

// 色温曲线插值示例(伪代码)
typedef struct {
    float ct;      // 色温
    float rg_ratio; // R/G
    float bg_ratio; // B/G
} ColorTempPoint;

float estimate_ct(float rg, float bg, ColorTempPoint* curve, int n) {
    // 找到最近的两个标定点
    for (int i = 0; i < n-1; i++) {
        if (rg >= curve[i].rg_ratio && rg <= curve[i+1].rg_ratio) {
            float t = (rg - curve[i].rg_ratio) / 
                      (curve[i+1].rg_ratio - curve[i].rg_ratio);
            return curve[i].ct + t * (curve[i+1].ct - curve[i].ct);
        }
    }
    return -1; // 超出范围
}
核心要点: 色温曲线法比前两种更准,因为它用了传感器的真实响应数据。但代价是——你得先做标定。我见过不少团队偷懒直接用公开的曲线,结果换了个传感器就全废了。

三种方法的对比与选择

我做项目时一般这么选:

  • 快速原型:灰度世界,三行代码搞定
  • 有高光场景:完美反射,比如户外阳光
  • 量产产品:色温曲线法,精度最高

当然,现在主流方案都是多种方法融合。比如先用灰度世界做粗估计,再用色温曲线做细调。或者用深度学习直接回归色温——那是后面章节的内容了。

色温估计方法对比 灰度世界假设 完美反射假设 色温曲线法 特性 • 假设场景平均色为灰 • 实现最简单 • 单色场景失效 特性 • 假设最亮像素为白 • 需要高光区域 • 无高光时失效 特性 • 基于传感器标定数据 • 精度最高 • 需要前期标定工作 实际项目:多方法融合使用 灰度世界做粗估计 → 色温曲线做细调 → 完美反射做校验
重要提醒: 不管你用哪种方法,RAW域处理一定要在去马赛克之前做。我见过有人先做了demosaic再调白平衡,结果颜色完全不对。因为插值后的RGB已经混入了相邻像素的信息,这时候再调增益,误差会被放大。