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 算法流程

整个处理流程,我习惯分成下面几步:

  1. 读取图像:加载RGB图像,注意数据类型。我一般用浮点型,避免整数运算精度丢失。
  2. 计算通道均值:分别统计R、G、B三个通道的像素平均值。
  3. 计算灰度参考值K:取三个通道均值的平均值,或者取最大均值(不同变体)。
  4. 计算增益系数:K除以各通道均值,得到三个增益。
  5. 应用增益:每个像素乘以对应通道的增益系数。
  6. 裁剪与输出:将像素值裁剪到[0, 255]范围,转换回整数类型。

下面这张图,是我自己画的一个流程结构,方便你理解数据是怎么流动的:

灰度世界算法流程图 输入RGB图像 计算R/G/B三通道均值 (R̄, Ḡ, B̄) 计算灰度参考值 K = (R̄+Ḡ+B̄)/3 计算增益系数 G_R, G_G, G_B 输出校正后图像 注意事项 • 输入图像建议用浮点型 • 增益系数需设上限防溢出 • 输出前必须裁剪到[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 适用场景总结

灰度世界算法最适合的场景,说白了就是“颜色够杂”的地方。比如:

  • 街景、城市风光(颜色丰富)
  • 室内多人合影(肤色、衣服颜色多样)
  • 商品拍摄(背景和物体颜色混合)
  • 视频直播的快速预处理(计算量低)

不适合的场景:

  • 纯色背景的证件照
  • 水下摄影(整体偏蓝绿)
  • 日落、日出(大面积暖色)
  • 医疗内窥镜(单一色调组织)