第3章:自动白平衡原理:灰度世界、完美反射与色温估计
各位同学,欢迎来到自动白平衡的核心章节。
说实话,我刚开始接触ISP(图像信号处理)时,觉得白平衡不就是调个色温嘛,有什么难的?直到第一次在混合光源场景下翻车——拍出来的照片一半偏蓝一半偏黄,怎么调都救不回来。嗯,从那以后我才老老实实把几个经典假设模型啃透。
这一章,我们就来拆解自动白平衡(AWB)的三大基石:灰度世界假设、完美反射假设,以及色温估计方法。理解了它们,你才算真正入了AWB的门。
3.1 为什么要做白平衡?
人眼有色彩恒常性——不管在日光灯下还是烛光下,我们看白纸还是白的。但CMOS传感器没这个本事,它老老实实记录光谱能量分布。所以,一张在钨丝灯下拍的白纸,如果不做处理,就是黄澄澄的。
白平衡的本质,就是把图像中原本是中性色(灰/白)的像素,映射回R=G=B的状态。说白了,就是找到场景中的“白”,然后算出一个校正增益,把偏色拉回来。
校正后像素 = 原始像素 × 增益矩阵
其中增益矩阵通常是对角阵:
R_gain = G / R_avg, B_gain = G / B_avg
3.2 灰度世界假设(Gray World)
这是最经典、也最暴力的方法。它的假设很简单:一幅色彩丰富的自然图像,所有像素的R、G、B通道平均值应该相等。如果不等,说明有色偏,那就把偏高的通道压下来,偏低的通道拉上去。
3.2.1 算法流程
- 计算全图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
import numpy as np
def gray_world_awb(img):
"""灰度世界白平衡"""
# img: HxWx3, dtype=np.uint8
img_float = img.astype(np.float32)
R_avg = np.mean(img_float[:,:,0])
G_avg = np.mean(img_float[:,:,1])
B_avg = np.mean(img_float[:,:,2])
R_gain = G_avg / R_avg
B_gain = G_avg / B_avg
img_float[:,:,0] *= R_gain
img_float[:,:,2] *= B_gain
return np.clip(img_float, 0, 255).astype(np.uint8)
3.3 完美反射假设(Perfect Reflector / White Patch)
这个方法换个思路:它假设图像中最亮的像素应该是纯白的(R=G=B=255)。如果最亮像素偏色,那就用它的偏色程度来校正整幅图。
3.3.1 算法流程
- 找到图像中亮度最高的像素(或前1%最亮像素的平均值)
- 计算该像素的R、G、B值:R_max, G_max, B_max
- 计算增益:R_gain = 255 / R_max, B_gain = 255 / B_max
- 应用增益到全图
def perfect_reflector_awb(img, percentile=99):
"""完美反射白平衡"""
img_float = img.astype(np.float32)
# 取前percentile%最亮像素的平均值
R_max = np.percentile(img_float[:,:,0], percentile)
G_max = np.percentile(img_float[:,:,1], percentile)
B_max = np.percentile(img_float[:,:,2], percentile)
R_gain = 255.0 / R_max
B_gain = 255.0 / B_max
img_float[:,:,0] *= R_gain
img_float[:,:,2] *= B_gain
return np.clip(img_float, 0, 255).astype(np.uint8)
3.4 两种假设的对比
| 特性 | 灰度世界 | 完美反射 |
|---|---|---|
| 假设基础 | 全图均值相等 | 最亮像素为白 |
| 适用场景 | 色彩丰富的自然场景 | 有明确白色参考的场景 |
| 失效场景 | 大面积单色 | 高光过曝、光源直射 |
| 计算复杂度 | 低(一次全图遍历) | 低(需排序或分位数) |
| 鲁棒性 | 中等 | 较差(对异常值敏感) |
3.5 色温估计方法
前面两种方法都是直接算增益,但实际工程中,我们往往需要先估计场景的色温,再查表或插值得到增益。为什么?因为不同色温下,白点的R/G、B/G比值是固定的,我们可以提前标定好。
3.5.1 基于色温曲线的估计
标准做法是:在D65(6500K)、D50(5000K)、A光源(2856K)等标准色温下,拍摄灰卡,记录R/G和B/G比值。然后拟合出一条色温-比值曲线。实际使用时,从图像中提取灰区像素的R/G、B/G,反查色温。
# 伪代码:色温查表
calibration_table = {
6500: (1.0, 1.0), # R_gain, B_gain
5000: (1.1, 0.9),
2856: (1.5, 0.6),
}
def estimate_color_temp(rg_ratio, bg_ratio):
"""根据R/G和B/G比值估计色温"""
# 实际项目中用插值,这里简化
for ct, (rg_ref, bg_ref) in calibration_table.items():
if abs(rg_ratio - rg_ref) < 0.05 and abs(bg_ratio - bg_ref) < 0.05:
return ct
return None
3.5.2 灰区检测
色温估计的关键是找到图像中的“灰区”——那些原本应该是中性色的像素。怎么找?我常用的方法是:在RGB空间中,筛选出R/G和B/G比值在一定范围内的像素。这些像素大概率是灰色或白色。
|R/G - 1| < threshold 且 |B/G - 1| < threshold
阈值通常取0.1~0.3,视噪声水平调整。
3.6 知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以看到,灰度世界和完美反射是两种独立的假设,而色温估计则是更工程化的方法——它依赖标定数据,但鲁棒性更好。
3.7 实战中的避坑指南
- 不要迷信单一假设:灰度世界和完美反射都有硬伤。我做过一个项目,在黄昏时分拍海面,灰度世界把整张图拉成了紫色——因为海面大面积蓝色,均值严重偏离。
- 灰区检测要加亮度约束:太暗的像素噪声大,R/G比值不稳定。我一般只取亮度在[30, 230]范围内的像素做灰区检测。
- 色温表要覆盖全范围:从2500K到7500K至少标定5个点,否则插值误差会很大。我曾经只标了3个点,结果在荧光灯场景下色温估计偏差了800K,校正后肤色像僵尸。
- 别忘了G通道:很多初学者只调R和B,但G通道的增益其实也可以微调。虽然G通常作为参考通道,但在某些传感器中,G通道的响应曲线并不平坦。