经典算法:灰度世界与完美反射
这一章,我们来聊聊白平衡领域里两个最经典的算法——灰度世界和完美反射。说实话,这两个算法虽然“年纪”不小了,但直到今天,很多工业级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)
完美反射算法(Perfect Reflector)
完美反射算法,也叫“镜面法”或“白点法”。它的思路和灰度世界完全相反——它认为图像中最亮的那个点应该是白色的。你想想,如果一张图里有一个纯白物体,那它反射的光就是光源的真实颜色。找到这个点,就能知道光源的色温了。
算法原理
算法步骤如下:
- 将图像从RGB转到YUV或计算每个像素的亮度值(比如取RGB最大值)。
- 找出亮度最高的前N%的像素(通常取1%~5%)。
- 计算这些“候选白点”的RGB平均值。
- 用这个平均值作为光源颜色估计,计算增益系数。
代码实现
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)
两种算法的对比分析
这两个算法各有千秋,我整理了一个对比表格,方便你快速理解:
| 对比维度 | 灰度世界算法 | 完美反射算法 |
|---|---|---|
| 核心假设 | 图像平均色为灰色 | 最亮像素为白色 |
| 适用场景 | 色彩丰富、无明显主色调 | 存在白色/高光区域 |
| 失效场景 | 大面积单色背景 | 无白色物体、暗光环境 |
| 计算复杂度 | 低(只需一次均值计算) | 中等(需要排序或百分位计算) |
| 鲁棒性 | 对噪声敏感 | 对高光噪声敏感 |
| 实现难度 | 简单 | 中等 |
嗯,这里我要多说一句。在实际项目中,我很少单独使用其中一个算法。更常见的做法是:先用灰度世界做粗调,再用完美反射做精调,或者根据场景自适应切换。比如在手机相机里,检测到画面中有大面积人脸时,会优先用完美反射(因为人脸高光区域接近白色);而拍摄风景时,则更依赖灰度世界。
最后,我用一张流程图来总结这两个算法的核心逻辑:
从这张图可以看得很清楚:两个算法走的是完全不同的路径,但最终都殊途同归。灰度世界从全局统计出发,完美反射从局部极值出发。理解了这个本质区别,你就能根据实际场景灵活选择了。