3、光源估计方法(上):灰度世界算法(Gray World)原理与实现
说到白平衡,最基础也最经典的方法,就是灰度世界算法。我刚开始接触AWB时,第一个上手的就是它。说白了,这个算法的核心假设就一句话:一张色彩丰富的照片,所有像素的R、G、B平均值应该趋于相等。
为什么会这样?你想想看,如果场景里的颜色足够多、足够杂,红绿蓝三通道的统计均值就应该差不多。如果照片偏黄,那蓝色通道的平均值肯定偏低,红色和绿色偏高。我们只要算出偏差,反向补偿回去就行了。
3.1 算法原理
灰度世界算法的数学表达其实很简单。假设输入图像为 \(I\),尺寸为 \(H \times W\),我们分别计算三个通道的均值:
\[ \bar{R} = \frac{1}{HW}\sum_{i=1}^{H}\sum_{j=1}^{W} I_R(i,j) \] \[ \bar{G} = \frac{1}{HW}\sum_{i=1}^{H}\sum_{j=1}^{W} I_G(i,j) \] \[ \bar{B} = \frac{1}{HW}\sum_{i=1}^{H}\sum_{j=1}^{W} I_B(i,j) \]然后,我们计算一个“理想灰度值”,通常取三个通道均值的平均值:
\[ K = \frac{\bar{R} + \bar{G} + \bar{B}}{3} \]最后,每个通道乘以一个增益系数,把均值拉到K:
\[ G_R = \frac{K}{\bar{R}}, \quad G_G = \frac{K}{\bar{G}}, \quad G_B = \frac{K}{\bar{B}} \] \[ I'_R = I_R \cdot G_R, \quad I'_G = I_G \cdot G_G, \quad I'_B = I_B \cdot G_B \]嗯,这里要注意:增益系数不能无限大。如果某个通道均值接近0,增益会爆炸。我遇到过这种情况,拍了一张纯蓝色物体的特写,蓝色通道均值很高,红色和绿色几乎为0,结果校正后图像全是噪点。
核心要点:灰度世界算法假设场景中颜色分布足够均匀。如果场景被大面积单色主导(比如蓝天、绿草地、红墙),这个假设就会失效。
3.2 算法流程
整个处理流程,我习惯分成下面几步:
- 读取图像:加载RGB图像,注意数据类型。我一般用浮点型,避免整数运算精度丢失。
- 计算通道均值:分别统计R、G、B三个通道的像素平均值。
- 计算灰度参考值K:取三个通道均值的平均值,或者取最大均值(不同变体)。
- 计算增益系数:K除以各通道均值,得到三个增益。
- 应用增益:每个像素乘以对应通道的增益系数。
- 裁剪与输出:将像素值裁剪到[0, 255]范围,转换回整数类型。
下面这张图,是我自己画的一个流程结构,方便你理解数据是怎么流动的:
3.3 代码实现
我习惯用Python + OpenCV做原型验证。下面是一个完整的实现,包含了上面说的所有步骤:
import cv2
import numpy as np
def gray_world_awb(img):
"""
灰度世界白平衡校正
:param img: 输入BGR图像 (uint8)
:return: 校正后的BGR图像 (uint8)
"""
# 转浮点,避免精度丢失
img_float = img.astype(np.float32)
# 计算各通道均值
mean_b = np.mean(img_float[:, :, 0])
mean_g = np.mean(img_float[:, :, 1])
mean_r = np.mean(img_float[:, :, 2])
# 计算灰度参考值
k = (mean_r + mean_g + mean_b) / 3.0
# 计算增益,防止除零
eps = 1e-6
gain_r = k / (mean_r + eps)
gain_g = k / (mean_g + eps)
gain_b = k / (mean_b + eps)
# 限制增益范围,避免过曝
max_gain = 10.0
gain_r = min(gain_r, max_gain)
gain_g = min(gain_g, max_gain)
gain_b = min(gain_b, max_gain)
# 应用增益
img_float[:, :, 0] *= gain_b
img_float[:, :, 1] *= gain_g
img_float[:, :, 2] *= gain_r
# 裁剪并转回uint8
img_out = np.clip(img_float, 0, 255).astype(np.uint8)
return img_out
我的经验:增益上限设10倍就够了。我曾经试过不设上限,结果一张暗光照片的蓝色通道增益飙到50多倍,整张图全是蓝色噪点。另外,如果图像里有大面积的过曝区域(纯白),建议先用掩膜排除掉,否则均值会被拉偏。
3.4 算法变体
灰度世界算法有不少变体,我在不同项目里试过几种,各有适用场景:
| 变体名称 | 灰度参考值K的计算方式 | 适用场景 |
|---|---|---|
| 标准灰度世界 | 三通道均值的平均值 | 颜色分布均匀的通用场景 |
| 最大灰度世界 | 取三通道均值中的最大值 | 偏暗场景,保留更多亮度 |
| 中值灰度世界 | 用中位数代替均值 | 存在少量极端颜色时更鲁棒 |
| 加权灰度世界 | 按亮度或饱和度加权平均 | 需要保留特定色彩倾向时 |
我个人最常用的是标准版和中值版。中值版在遇到画面里有小面积强色块时,比均值版稳定很多。举个例子,拍一张室内照片,角落里有个红色灭火器,均值版会把整张图往青色拉,中值版受影响就小得多。
3.5 优缺点分析
灰度世界算法不是万能的,但它简单、快速、容易理解。我整理了一下它的优缺点:
优点:
- 计算量极小,适合实时处理
- 实现简单,十几行代码就能跑起来
- 对颜色丰富的场景效果不错
- 可以作为更复杂算法的初始化步骤
缺点:
- 大面积单色场景会失效(蓝天、草地、红墙)
- 对暗部噪声敏感,增益放大噪声
- 无法处理多光源混合场景
- 假设过于理想化,真实场景常不满足
避坑指南:我曾经在一个无人机航拍项目里直接用灰度世界算法做白平衡,结果飞过一片油菜花田时,整段视频都偏蓝紫色。后来加了场景检测,检测到黄色占比过高时切换到其他算法,问题才解决。所以,灰度世界算法适合做预处理或兜底方案,不建议作为唯一白平衡策略。
3.6 适用场景总结
灰度世界算法最适合的场景,说白了就是“颜色够杂”的地方。比如:
- 街景、城市风光(颜色丰富)
- 室内多人合影(肤色、衣服颜色多样)
- 商品拍摄(背景和物体颜色混合)
- 视频直播的快速预处理(计算量低)
不适合的场景:
- 纯色背景的证件照
- 水下摄影(整体偏蓝绿)
- 日落、日出(大面积暖色)
- 医疗内窥镜(单一色调组织)