经典算法:灰度世界、完美反射与色温估计
说到自动白平衡,我最早接触的就是这三板斧。灰度世界、完美反射、基于色温估计——它们构成了整个AWB算法的基础。说实话,直到今天,很多量产产品里的核心逻辑依然脱胎于它们。
这一章,我们就把这三个经典算法掰开揉碎。我会结合自己踩过的坑,告诉你每个算法到底怎么用、什么时候会翻车。
灰度世界算法(Gray World)
这个算法的假设很简单:一张色彩丰富的自然图像,所有像素的R、G、B均值应该趋于相等。说白了,就是认为世界平均是灰色的。
为什么能这么假设?因为自然场景中各种颜色混合后,统计上确实接近中性灰。我当年第一次验证这个假设时,对着几百张户外照片算均值,发现R、G、B通道均值偏差基本在5%以内——嗯,确实靠谱。
算法流程
- 计算图像R、G、B三个通道的均值:R_avg、G_avg、B_avg
- 计算灰色参考值:Gray = (R_avg + G_avg + B_avg) / 3
- 计算各通道增益:Kr = Gray / R_avg, Kg = Gray / G_avg, Kb = Gray / B_avg
- 对每个像素应用增益: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。你想想看,如果场景里有一块白纸、一面白墙,那它反射的光线就代表了光源本身的颜色。
我曾经在一个手机摄像头项目里用过这个算法。当时拍室内白墙效果极好,但一到户外拍树叶,算法就崩了——因为最亮的点是天空,而天空是蓝色的,算法硬把蓝色通道压下去,整张图变得偏黄。
算法流程
- 统计图像中每个像素的亮度值(通常取RGB最大值)
- 选取亮度最高的前0.1%~1%的像素作为"参考白点"
- 计算这些白点的R、G、B均值
- 以G通道为基准,计算R和B的增益
- 对全图应用增益
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随色温变化的曲线。那段时间我做梦都在看色温曲线图。
核心思路
色温估计算法通常分两步:
- 色温估计:从图像中提取色温特征,映射到色温值
- 增益计算:根据色温值查表或公式计算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张以上照片。标定数据越密集,插值精度越高。另外,不同传感器的色温响应曲线差异很大,换传感器必须重新标定。
三种算法对比
| 维度 | 灰度世界 | 完美反射 | 色温估计 |
|---|---|---|---|
| 假设条件 | 世界平均为灰 | 最亮点为白 | 色温连续变化 |
| 计算复杂度 | 低 | 中 | 高 |
| 标定需求 | 无 | 无 | 需要标定 |
| 大色块场景 | 差 | 中 | 好 |
| 混合光源 | 差 | 差 | 中 |
| 实现难度 | 简单 | 简单 | 中等 |
知识结构图
实际工程中的选择建议
这三种算法没有的好坏。我个人的经验是:
- 快速原型验证:用灰度世界,三行代码搞定
- 有标准光源的场景:用完美反射,比如扫描仪、复印机
- 消费级摄像头:用色温估计,精度高但需要标定
- 高端产品:三种算法融合,根据场景置信度动态切换
我的建议:初学者先吃透灰度世界和完美反射。这两个算法虽然简单,但能帮你建立对白平衡问题的直觉。等你遇到真实场景的偏色问题时,再回头研究色温估计——那时候你才会真正理解标定数据为什么重要。