经典算法:灰度世界、完美反射与色温估计

说到自动白平衡,我最早接触的就是这三板斧。灰度世界、完美反射、基于色温估计——它们构成了整个AWB算法的基础。说实话,直到今天,很多量产产品里的核心逻辑依然脱胎于它们。

这一章,我们就把这三个经典算法掰开揉碎。我会结合自己踩过的坑,告诉你每个算法到底怎么用、什么时候会翻车。

灰度世界算法(Gray World)

这个算法的假设很简单:一张色彩丰富的自然图像,所有像素的R、G、B均值应该趋于相等。说白了,就是认为世界平均是灰色的。

为什么能这么假设?因为自然场景中各种颜色混合后,统计上确实接近中性灰。我当年第一次验证这个假设时,对着几百张户外照片算均值,发现R、G、B通道均值偏差基本在5%以内——嗯,确实靠谱。

算法流程

  1. 计算图像R、G、B三个通道的均值:R_avg、G_avg、B_avg
  2. 计算灰色参考值:Gray = (R_avg + G_avg + B_avg) / 3
  3. 计算各通道增益:Kr = Gray / R_avg, Kg = Gray / G_avg, Kb = Gray / B_avg
  4. 对每个像素应用增益:R' = R * Kr, G' = G * Kg, B' = B * Kb
def gray_world(img):
    """灰度世界白平衡"""
    # 计算各通道均值
    avg_r = np.mean(img[:, :, 0])
    avg_g = np.mean(img[:, :, 1])
    avg_b = np.mean(img[:, :, 2])
    
    # 灰色参考值
    gray = (avg_r + avg_g + avg_b) / 3.0
    
    # 计算增益
    kr = gray / avg_r
    kg = gray / avg_g
    kb = gray / avg_b
    
    # 应用增益
    result = img.copy()
    result[:, :, 0] = np.clip(img[:, :, 0] * kr, 0, 255)
    result[:, :, 1] = np.clip(img[:, :, 1] * kg, 0, 255)
    result[:, :, 2] = np.clip(img[:, :, 2] * kb, 0, 255)
    
    return result.astype(np.uint8)

我的经验:灰度世界算法对大色块场景特别敏感。比如一张全是蓝天的照片,蓝色通道均值偏高,算法会强行压低蓝色,结果天空变成青灰色。我建议在应用前先判断图像色彩丰富度,如果主色占比超过60%,最好换算法。

适用场景与局限

场景类型 效果 说明
自然风景 优秀 色彩丰富,满足灰度假设
室内混合光源 一般 局部色温差异大,均值偏差
单色物体特写 差 严重违反假设,偏色明显
夜景灯光 较差 暗区噪声干扰均值计算

完美反射算法(Perfect Reflector)

这个算法的思路完全不同。它假设图像中最亮的像素点应该是纯白色的——也就是R=G=B=255。你想想看,如果场景里有一块白纸、一面白墙,那它反射的光线就代表了光源本身的颜色。

我曾经在一个手机摄像头项目里用过这个算法。当时拍室内白墙效果极好,但一到户外拍树叶,算法就崩了——因为最亮的点是天空,而天空是蓝色的,算法硬把蓝色通道压下去,整张图变得偏黄。

算法流程

  1. 统计图像中每个像素的亮度值(通常取RGB最大值)
  2. 选取亮度最高的前0.1%~1%的像素作为"参考白点"
  3. 计算这些白点的R、G、B均值
  4. 以G通道为基准,计算R和B的增益
  5. 对全图应用增益
def perfect_reflector(img, percentile=99.9):
    """完美反射白平衡"""
    # 计算每个像素的亮度(取最大值)
    brightness = np.max(img, axis=2)
    
    # 找到亮度阈值
    threshold = np.percentile(brightness, percentile)
    
    # 提取参考白点
    mask = brightness >= threshold
    white_pixels = img[mask]
    
    if len(white_pixels) == 0:
        return img
    
    # 计算白点均值
    avg_r = np.mean(white_pixels[:, 0])
    avg_g = np.mean(white_pixels[:, 1])
    avg_b = np.mean(white_pixels[:, 2])
    
    # 以G通道为基准计算增益
    kr = avg_g / avg_r
    kb = avg_g / avg_b
    
    # 应用增益
    result = img.copy()
    result[:, :, 0] = np.clip(img[:, :, 0] * kr, 0, 255)
    result[:, :, 2] = np.clip(img[:, :, 2] * kb, 0, 255)
    
    return result.astype(np.uint8)

避坑指南:我曾经在夜景场景里用过完美反射,结果惨不忍睹。因为暗区噪声会产生大量"伪亮点",算法把这些噪声点当成了参考白点。我的建议是:先做降噪,再统计白点。另外,百分位阈值要根据场景动态调整——高光场景用99.9%,普通场景用99.5%就够了。

基于色温估计的算法

这个算法更接近人眼的感知方式。它不直接操作像素,而是先估计场景的色温,然后根据色温查表得到校正参数。

我记得第一次做这个算法时,花了两周时间标定色温曲线。对着标准色卡在不同色温下拍了几百张照片,然后拟合出R/G和B/G随色温变化的曲线。那段时间我做梦都在看色温曲线图。

核心思路

色温估计算法通常分两步:

  1. 色温估计:从图像中提取色温特征,映射到色温值
  2. 增益计算:根据色温值查表或公式计算R、B通道增益

常用的色温估计方法有两种:

  • 基于色度空间的方法:将RGB转换到色度空间,用色度坐标估计色温
  • 基于统计的方法:用R/G和B/G的比值分布来估计色温
def color_temperature_awb(img, temp_curve):
    """基于色温估计的白平衡"""
    # 1. 计算R/G和B/G比值
    r_g_ratio = np.mean(img[:, :, 0]) / np.mean(img[:, :, 1])
    b_g_ratio = np.mean(img[:, :, 2]) / np.mean(img[:, :, 1])
    
    # 2. 根据比值估计色温(查表或插值)
    color_temp = estimate_temp_from_ratio(r_g_ratio, b_g_ratio, temp_curve)
    
    # 3. 根据色温查增益表
    kr, kb = lookup_gain_from_temp(color_temp, temp_curve)
    
    # 4. 应用增益
    result = img.copy()
    result[:, :, 0] = np.clip(img[:, :, 0] * kr, 0, 255)
    result[:, :, 2] = np.clip(img[:, :, 2] * kb, 0, 255)
    
    return result.astype(np.uint8)

关键点:色温估计算法的精度取决于标定数据的质量。我建议至少采集10个色温点(2500K~7500K),每个点拍20张以上照片。标定数据越密集,插值精度越高。另外,不同传感器的色温响应曲线差异很大,换传感器必须重新标定。

三种算法对比

维度 灰度世界 完美反射 色温估计
假设条件 世界平均为灰 最亮点为白 色温连续变化
计算复杂度 低 中 高
标定需求 无 无 需要标定
大色块场景 差 中 好
混合光源 差 差 中
实现难度 简单 简单 中等

知识结构图

经典白平衡算法知识结构 经典白平衡算法 灰度世界算法 完美反射算法 色温估计算法 均值假设 通道增益 白点检测 亮度阈值 色温标定 增益查表 实际工程中的选择策略 色彩丰富场景 → 灰度世界 | 有明确白点 → 完美反射 | 需要高精度 → 色温估计 多数产品采用混合策略:先判断场景类型,再选择或融合不同算法

实际工程中的选择建议

这三种算法没有的好坏。我个人的经验是:

  • 快速原型验证:用灰度世界,三行代码搞定
  • 有标准光源的场景:用完美反射,比如扫描仪、复印机
  • 消费级摄像头:用色温估计,精度高但需要标定
  • 高端产品:三种算法融合,根据场景置信度动态切换

我的建议:初学者先吃透灰度世界和完美反射。这两个算法虽然简单,但能帮你建立对白平衡问题的直觉。等你遇到真实场景的偏色问题时,再回头研究色温估计——那时候你才会真正理解标定数据为什么重要。