4. 完美反射法:从“找白点”到白平衡

完美反射法,名字听着挺玄乎,其实核心思想很简单——“场景里最亮的那块区域,理论上应该是白的”。你想想看,一张白纸在阳光下是白的,在灯光下它还是白的,只是被光源染了色。如果我们能找到这个“本该是白色”的区域,那光源的颜色不就暴露了吗?

我在刚入行那会儿,第一次接触自动白平衡,用的就是这个方法。当时觉得它太理想化了,但实际调起来,发现它比想象中要皮实得多。

4.1 算法原理:找“白点”的数学逻辑

完美反射法的假设是:图像中RGB三个通道各自的最大值,对应了场景中“完美反射体”的响应。这个完美反射体,说白了就是一个能把所有波长的光都等比例反射回去的东西——理想中的白纸。

具体步骤分三步走:

  1. 找各通道最大值:分别统计R、G、B三个通道的像素值,找出每个通道的最大值,记为 R_max、G_max、B_max。
  2. 计算增益系数:以G通道为基准(因为人眼对绿色最敏感),计算R和B的增益:
    • gain_R = G_max / R_max
    • gain_B = G_max / B_max
  3. 应用增益:对每个像素的R和B通道乘以对应的增益系数,G通道保持不变。

嗯,这里要注意。实际工程中,我们很少直接用全局最大值。为什么?因为图像里可能有高光溢出、镜面反射点,这些点的值已经饱和了(比如255),用它算增益会出问题。我一般会取前0.1%最亮像素的平均值,或者用直方图统计,取99.9%分位点的值。

核心公式:

R_out = R_in × (G_max / R_max)

B_out = B_in × (G_max / B_max)

G_out = G_in

4.2 Python代码实现:从理论到跑通

下面是我写的一个基础实现。代码不长,但包含了几个工程上的小技巧。我建议你直接跑一下,看看效果。

import numpy as np
import cv2

def perfect_reflector_awb(img, percentile=99.9):
    """
    完美反射法自动白平衡
    :param img: 输入BGR图像,uint8类型
    :param percentile: 取前百分之多少的像素作为“白点”候选
    :return: 白平衡校正后的图像
    """
    # 分离通道
    b, g, r = cv2.split(img.astype(np.float32))
    
    # 计算各通道的阈值(取前percentile%的像素)
    r_thresh = np.percentile(r, percentile)
    g_thresh = np.percentile(g, percentile)
    b_thresh = np.percentile(b, percentile)
    
    # 取各通道最大值(这里用阈值代替全局最大,更鲁棒)
    r_max = np.mean(r[r >= r_thresh])
    g_max = np.mean(g[g >= g_thresh])
    b_max = np.mean(b[b >= b_thresh])
    
    # 计算增益,以G通道为基准
    gain_r = g_max / r_max
    gain_b = g_max / b_max
    
    # 应用增益
    r_balanced = np.clip(r * gain_r, 0, 255)
    b_balanced = np.clip(b * gain_b, 0, 255)
    g_balanced = g  # G通道不变
    
    # 合并通道并转回uint8
    result = cv2.merge([b_balanced.astype(np.uint8), 
                        g_balanced.astype(np.uint8), 
                        r_balanced.astype(np.uint8)])
    return result

# 使用示例
if __name__ == "__main__":
    img = cv2.imread("test_raw.jpg")
    balanced = perfect_reflector_awb(img)
    cv2.imwrite("balanced.jpg", balanced)

我的经验:percentile这个参数很关键。在室内暖光下,我一般设99.5%;在户外强阳光下,设99.9%更稳。你可以做个滑动条,实时调参看看效果。

4.3 优缺点分析:它不完美,但够用

任何算法都有它的脾气。完美反射法也不例外。我把它拆成表格,方便你对比。

维度 优点 缺点
计算速度 极快。只需要一次直方图统计和几个除法,适合嵌入式实时处理 如果图像分辨率高,直方图统计也有开销,但总体可控
实现难度 代码量少,逻辑直观,新手也能快速上手 调参(percentile)需要经验,不同场景差异大
适用场景 场景中有明显白色/灰色物体时效果很好,比如文档扫描、室内白墙 场景中完全没有白色物体时(比如纯色风景),结果会偏色严重
鲁棒性 对噪声不敏感,因为取的是统计值而非单个像素 对高光溢出敏感,如果白色区域过曝,增益会偏小

我曾经在一个监控项目里用过它。场景是夜晚的停车场,只有路灯和车灯。结果呢?路灯是暖色的,车灯是冷色的,完美反射法直接懵了——它把路灯下的地面当成了“白点”,结果整张图偏蓝得一塌糊涂。后来我加了区域权重,只取图像中央区域的统计值,才勉强压住。

避坑指南:千万不要在RAW域直接套用这个算法!RAW数据是线性的,而完美反射法假设的是“亮度”而非“光强”。如果你在RAW域用,必须先做gamma校正或转到线性空间。我曾经吃过这个亏,调了一整天发现颜色怎么都拉不回来。

4.4 知识结构图:完美反射法的完整流程

下面这张图把整个流程串起来了。从输入图像到输出校正结果,每一步都标清楚了。

完美反射法流程图 完美反射法自动白平衡流程 输入BGR图像 分离R/G/B通道 统计各通道前percentile%像素 计算R/B通道增益 输出校正后图像 关键参数 percentile: 99.5%~99.9% 基准通道: G 增益上限: 通常≤3.0

4.5 什么时候该用它?什么时候该换?

我个人的经验是:完美反射法最适合做“预处理”。什么意思呢?就是在你上更复杂的算法(比如灰度世界法、色温估计法)之前,先用它把明显的色偏拉回来。它速度快,不会成为瓶颈。

但如果你遇到下面这些情况,我建议你换个思路:

  • 场景里没有白色物体:比如森林、日落、夜景。这时候完美反射法会强行把某个颜色当成白色,结果就是偏色。
  • 光源混合:比如室内既有日光灯又有台灯。完美反射法只能估计一个全局色温,处理不了混合光源。
  • RAW域处理:前面说了,RAW数据是线性的,直接套用会出问题。如果你非要用,记得先做线性化。

一句话总结:完美反射法是个好工具,但它不是万能的。理解它的假设条件,你就能用好它。