经典算法:灰度世界与完美反射

这一章,我们来聊聊白平衡领域里两个最经典的算法——灰度世界和完美反射。说实话,这两个算法虽然“年纪”不小了,但直到今天,很多工业级ISP流水线里依然能看到它们的身影。我个人习惯把它们比作“白平衡界的倚天剑和屠龙刀”,各有各的绝活,也各有各的脾气。

灰度世界算法(Gray World)

灰度世界算法的核心思想,说白了就一句话:一张色彩丰富的照片,所有像素颜色的平均值应该是灰色的。你想想看,如果一张图整体偏蓝,那它的RGB平均值肯定也是偏蓝的。我们只要算出这个偏差,然后反向拉回来就行了。

算法原理

假设我们有一张图像,其R、G、B三个通道的平均值分别为 R_avg、G_avg、B_avg。灰度世界认为,这个平均值应该等于一个“灰色值” K。通常K取三个通道平均值的均值,或者取最大通道值的一半。

然后,我们为每个通道计算一个增益系数:

Kr = K / R_avg
Kg = K / G_avg
Kb = K / B_avg

最后,用这些系数去乘每个像素的对应通道值,就完成了白平衡校正。

代码实现(Python + OpenCV)

下面是我在实际项目中常用的实现方式。注意,我习惯用浮点数运算来避免精度损失。

import cv2
import numpy as np

def gray_world_balance(img):
    """
    灰度世界白平衡
    :param img: BGR图像
    :return: 校正后的BGR图像
    """
    # 计算每个通道的平均值
    b_avg = np.mean(img[:, :, 0])
    g_avg = np.mean(img[:, :, 1])
    r_avg = np.mean(img[:, :, 2])

    # 计算灰色值K(这里取三个通道的均值)
    k = (b_avg + g_avg + r_avg) / 3

    # 计算增益
    kb = k / b_avg
    kg = k / g_avg
    kr = k / r_avg

    # 应用增益,并做数值裁剪
    balanced = img.copy().astype(np.float32)
    balanced[:, :, 0] = np.clip(balanced[:, :, 0] * kb, 0, 255)
    balanced[:, :, 1] = np.clip(balanced[:, :, 1] * kg, 0, 255)
    balanced[:, :, 2] = np.clip(balanced[:, :, 2] * kr, 0, 255)

    return balanced.astype(np.uint8)
小技巧: 如果图像中有大面积纯色背景(比如蓝天、绿草地),灰度世界算法会失效。我曾经在一个无人机航拍项目里吃过这个亏——天空占了画面70%,结果算法把蓝天硬拉成了灰色,整个画面变得惨不忍睹。

完美反射算法(Perfect Reflector)

完美反射算法,也叫“镜面法”或“白点法”。它的思路和灰度世界完全相反——它认为图像中最亮的那个点应该是白色的。你想想,如果一张图里有一个纯白物体,那它反射的光就是光源的真实颜色。找到这个点,就能知道光源的色温了。

算法原理

算法步骤如下:

  1. 将图像从RGB转到YUV或计算每个像素的亮度值(比如取RGB最大值)。
  2. 找出亮度最高的前N%的像素(通常取1%~5%)。
  3. 计算这些“候选白点”的RGB平均值。
  4. 用这个平均值作为光源颜色估计,计算增益系数。

代码实现

def perfect_reflector_balance(img, percentile=95):
    """
    完美反射白平衡
    :param img: BGR图像
    :param percentile: 亮度百分位阈值(默认95%)
    :return: 校正后的BGR图像
    """
    # 计算每个像素的亮度(取RGB最大值)
    gray = np.max(img, axis=2).astype(np.float32)

    # 找到亮度阈值
    threshold = np.percentile(gray, percentile)

    # 生成掩码:只保留亮度高于阈值的像素
    mask = gray >= threshold

    # 如果掩码全为False,则降低阈值
    if np.sum(mask) == 0:
        mask = gray >= np.max(gray) * 0.9

    # 计算候选白点的RGB平均值
    b_avg = np.mean(img[:, :, 0][mask])
    g_avg = np.mean(img[:, :, 1][mask])
    r_avg = np.mean(img[:, :, 2][mask])

    # 计算增益(目标为255)
    kb = 255.0 / b_avg
    kg = 255.0 / g_avg
    kr = 255.0 / r_avg

    # 应用增益
    balanced = img.copy().astype(np.float32)
    balanced[:, :, 0] = np.clip(balanced[:, :, 0] * kb, 0, 255)
    balanced[:, :, 1] = np.clip(balanced[:, :, 1] * kg, 0, 255)
    balanced[:, :, 2] = np.clip(balanced[:, :, 2] * kr, 0, 255)

    return balanced.astype(np.uint8)
注意: 如果图像中根本没有白色物体(比如夜景、暗调照片),完美反射算法会出大问题。我曾经在一个室内暗光场景测试中,算法把台灯灯泡当作白点,结果整张图变得偏蓝发紫,非常诡异。

两种算法的对比分析

这两个算法各有千秋,我整理了一个对比表格,方便你快速理解:

对比维度 灰度世界算法 完美反射算法
核心假设 图像平均色为灰色 最亮像素为白色
适用场景 色彩丰富、无明显主色调 存在白色/高光区域
失效场景 大面积单色背景 无白色物体、暗光环境
计算复杂度 低(只需一次均值计算) 中等(需要排序或百分位计算)
鲁棒性 对噪声敏感 对高光噪声敏感
实现难度 简单 中等

嗯,这里我要多说一句。在实际项目中,我很少单独使用其中一个算法。更常见的做法是:先用灰度世界做粗调,再用完美反射做精调,或者根据场景自适应切换。比如在手机相机里,检测到画面中有大面积人脸时,会优先用完美反射(因为人脸高光区域接近白色);而拍摄风景时,则更依赖灰度世界。

我的经验: 如果你刚开始接触白平衡,我建议先从灰度世界入手。它简单、直观,而且80%的场景下效果都还不错。等你把灰度世界吃透了,再去看完美反射,会发现很多思路是相通的。

最后,我用一张流程图来总结这两个算法的核心逻辑:

白平衡经典算法流程对比 输入图像 (BGR) 灰度世界算法 完美反射算法 计算RGB三通道均值 计算灰色值 K 计算各通道增益系数 计算像素亮度(取最大值) 筛选前N%最亮像素 计算候选白点RGB均值 校正后图像 可根据场景自适应切换算法

从这张图可以看得很清楚:两个算法走的是完全不同的路径,但最终都殊途同归。灰度世界从全局统计出发,完美反射从局部极值出发。理解了这个本质区别,你就能根据实际场景灵活选择了。