3. 经典色温估计方法:灰度世界、白点假设与完美反射
好,我们进入正题。色温估计是 AWB 的核心,说白了就是让相机知道「当前场景下,什么颜色才算白」。
我刚开始做 ISP 时,觉得这问题很简单——找个白色物体不就行了?后来发现,真实场景哪有那么多纯白物体等你拍。所以前辈们总结了几套经典假设,今天咱们一个一个拆开看。
3.1 灰度世界假设(Gray World)
这个假设很直观:一张色彩丰富的自然图像,所有像素的 RGB 平均值应该趋近于灰色。换句话说,R、G、B 三个通道的均值应该相等。
为什么能这么假设?因为自然界中各种颜色混合在一起,统计上会抵消成中性灰。你想想看,蓝天、绿树、红墙……平均下来确实接近灰色。
3.1.1 算法流程
- 计算图像 R、G、B 三个通道的均值:
R_avg、G_avg、B_avg - 计算灰色参考值:
Gray = (R_avg + G_avg + B_avg) / 3 - 计算各通道增益:
R_gain = Gray / R_avg,G_gain = Gray / G_avg,B_gain = Gray / B_avg - 用增益校正每个像素
// 灰度世界算法核心实现
void grayWorldAWB(uint8_t* img, int width, int height) {
double sumR = 0, sumG = 0, sumB = 0;
int total = width * height;
// 第一步:统计各通道总和
for (int i = 0; i < total; i++) {
sumR += img[i * 3 + 0];
sumG += img[i * 3 + 1];
sumB += img[i * 3 + 2];
}
// 第二步:计算均值
double avgR = sumR / total;
double avgG = sumG / total;
double avgB = sumB / total;
// 第三步:计算灰色参考和增益
double gray = (avgR + avgG + avgB) / 3.0;
double gainR = gray / avgR;
double gainG = gray / avgG;
double gainB = gray / avgB;
// 第四步:应用增益
for (int i = 0; i < total; i++) {
img[i * 3 + 0] = clamp(img[i * 3 + 0] * gainR);
img[i * 3 + 1] = clamp(img[i * 3 + 1] * gainG);
img[i * 3 + 2] = clamp(img[i * 3 + 2] * gainB);
}
}
3.1.2 适用场景与局限
| 场景类型 | 效果 | 原因 |
|---|---|---|
| 色彩丰富的户外 | 优秀 | 颜色分布均匀,假设成立 |
| 单一主色调场景 | 较差 | 均值偏离灰色,校正过度 |
| 低光照环境 | 一般 | 噪声干扰均值计算 |
| 室内混合光源 | 中等 | 局部色温不一致,全局假设失效 |
3.2 白点假设(White Patch)
这个方法和灰度世界正好相反。灰度世界看「平均」,白点假设看「最亮」。
它的核心思想是:图像中最亮的像素点,应该对应场景中的白色区域。找到这些白点,用它们来估计光源色温。
我个人习惯把 White Patch 叫做「找最亮的仔」——你找到图像里最亮的那几个像素,假设它们就是白色,然后算增益。
3.2.1 算法实现
- 遍历图像,找到 R、G、B 各通道的最大值:
R_max、G_max、B_max - 计算各通道增益:
R_gain = 255 / R_max,以此类推 - 用增益校正全图
3.2.2 改进版本:多白点假设
基础版 White Patch 太脆弱了。我在项目中常用的是「多白点假设」——不是只找一个最亮点,而是找一批候选白点。
// 多白点假设算法
void multiWhitePatchAWB(uint8_t* img, int width, int height) {
int total = width * height;
double sumR = 0, sumG = 0, sumB = 0;
int count = 0;
// 计算亮度阈值(取前 5% 亮度)
// 实际项目中会用直方图统计
int threshold = 200; // 简化示例
for (int i = 0; i < total; i++) {
uint8_t r = img[i * 3 + 0];
uint8_t g = img[i * 3 + 1];
uint8_t b = img[i * 3 + 2];
// 亮度足够高,且接近灰色(防止彩色高光干扰)
int luminance = (r + g + b) / 3;
int maxDiff = max(abs(r - g), max(abs(r - b), abs(g - b)));
if (luminance > threshold && maxDiff < 30) {
sumR += r;
sumG += g;
sumB += b;
count++;
}
}
if (count == 0) return; // 没找到白点,跳过
double avgR = sumR / count;
double avgG = sumG / count;
double avgB = sumB / count;
double gainR = 255.0 / avgR;
double gainG = 255.0 / avgG;
double gainB = 255.0 / avgB;
// 应用增益...
}
3.3 完美反射理论(Perfect Reflector)
这个名字听起来很唬人,其实它和白点假设是一家人。完美反射理论认为:场景中存在一个完美反射面,它能完全反射入射光的所有波长。这个反射面的颜色,就是光源的真实颜色。
说白了,完美反射就是 White Patch 的「理论升级版」。白点假设找的是「最亮像素」,完美反射找的是「最接近理想白体的像素」。
3.3.1 与 White Patch 的区别
| 对比维度 | White Patch | Perfect Reflector |
|---|---|---|
| 理论基础 | 经验假设 | 物理光学模型 |
| 白点判定 | 亮度最高 | 反射率最高(接近 1) |
| 抗噪能力 | 弱 | 中等(可结合反射率估计) |
| 实现复杂度 | 低 | 中 |
3.3.2 实际应用中的折中
我在实际项目中很少严格按完美反射理论去算反射率——那需要知道场景的几何信息和光源分布,太复杂了。通常的做法是:
- 用 White Patch 的思路找候选白点
- 加一个「色度一致性」约束:候选点的 R/G、B/G 比值要接近
- 再加一个「空间分布」约束:白点不能全集中在某个角落
这样既保留了完美反射的物理合理性,又保持了 White Patch 的计算效率。
3.4 三种方法的对比与选择
这三种方法没有的好坏,关键看场景。我一般这样选:
- 灰度世界:色彩丰富的户外场景,首选
- 白点假设:室内有人造光源、有白色参照物的场景
- 完美反射:需要高精度色温估计的专业场景(比如医疗影像、工业检测)
但说实话,现代 ISP 很少只用一种方法。我习惯的做法是:用灰度世界做基础校正,用白点假设做局部修正,再用完美反射做异常检测。三个方法互相兜底,效果才稳。
3.5 本章小结
三种经典方法,各有各的脾气。灰度世界稳但怕单色,白点假设灵但怕没白,完美反射准但怕复杂。我建议你从灰度世界入手,先跑通基础流程,再逐步叠加其他方法。