4. AWB 算法实现:灰度世界、白点检测与色温曲线
好,咱们进入正题。自动白平衡(AWB)说白了,就是让相机在各种光线下都能把白色拍成白色。你想想看,在钨丝灯下拍的白纸偏黄,在阴天拍的白纸偏蓝,这肯定不行。AWB 要做的,就是把这些偏色给拉回来。
这一章,我带你手撸三种最经典的 AWB 算法。它们分别是:灰度世界、白点检测、色温曲线拟合。这三种思路完全不同,但实际工程中经常组合使用。我个人习惯先跑灰度世界做粗调,再用白点检测做精调,最后用色温曲线兜底。
4.1 灰度世界算法(Gray World)
灰度世界是最简单、最直觉的 AWB 方法。它的假设是:一张色彩丰富的照片,所有像素的 R、G、B 平均值应该相等,也就是趋向于灰色。
嗯,这个假设在大多数自然场景下是成立的。但我在项目中遇到过一个问题——如果场景里大面积都是同一种颜色,比如一片蓝天或一片绿草地,灰度世界就会翻车。它会强行把蓝色或绿色拉成灰色,结果整个画面都偏色。
算法步骤其实就三步:
- 计算整张图像的 R、G、B 通道均值:
R_avg、G_avg、B_avg - 计算三个通道的增益:
R_gain = G_avg / R_avg、B_gain = G_avg / B_avg(G 通道保持不变) - 对每个像素应用增益:
R_out = R_in * R_gain、B_out = B_in * B_gain
代码实现也很直接:
import numpy as np
def gray_world_awb(img):
"""
灰度世界白平衡
img: HxWx3, dtype=np.uint8, BGR顺序
"""
# 转成float防止溢出
img_float = img.astype(np.float32)
# 计算各通道均值
b_avg = np.mean(img_float[:, :, 0])
g_avg = np.mean(img_float[:, :, 1])
r_avg = np.mean(img_float[:, :, 2])
# 计算增益
r_gain = g_avg / r_avg
b_gain = g_avg / b_avg
# 应用增益
img_float[:, :, 2] *= r_gain # R通道
img_float[:, :, 0] *= b_gain # B通道
# 裁剪到[0, 255]并转回uint8
img_out = np.clip(img_float, 0, 255).astype(np.uint8)
return img_out
4.2 白点检测算法(White Point Detection)
灰度世界假设所有颜色平均后是灰色,但白点检测的思路完全不同。它认为:图像里总有一些像素是真正的白色或灰色,只要找到这些白点,用它们的颜色信息就能算出正确的白平衡增益。
怎么找白点?我常用的方法是:在 YCbCr 色彩空间里,找那些色度分量(Cb、Cr)接近 0 的像素。说白了,就是找那些看起来不带颜色的像素点。
我曾经踩过一个坑:直接用 RGB 空间的阈值来检测白点,结果经常把高光过曝区域误判成白点。后来我改用 YCbCr 空间,效果好了很多。因为 YCbCr 把亮度和色度分开了,更容易控制检测条件。
白点检测的流程如下:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | RGB → YCbCr | 转换色彩空间,分离亮度与色度 |
| 2 | 亮度筛选 | 只保留 Y 在 [Y_low, Y_high] 区间的像素 |
| 3 | 色度筛选 | 只保留 |Cb| < T_cb 且 |Cr| < T_cr 的像素 |
| 4 | 计算白点均值 | 对筛选出的像素计算 R、G、B 均值 |
| 5 | 计算增益 | 与灰度世界类似,用白点均值计算增益 |
代码实现:
def white_point_awb(img, y_low=50, y_high=240, cb_thresh=15, cr_thresh=15):
"""
白点检测白平衡
"""
# 转换到YCbCr
img_ycbcr = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb)
y, cb, cr = cv2.split(img_ycbcr)
# 创建白点掩码
mask = (y > y_low) & (y < y_high) & \
(np.abs(cb - 128) < cb_thresh) & \
(np.abs(cr - 128) < cr_thresh)
# 如果没有检测到白点,回退到灰度世界
if np.sum(mask) < 100:
return gray_world_awb(img)
# 提取白点像素
white_pixels = img[mask]
# 计算白点均值
b_mean = np.mean(white_pixels[:, 0])
g_mean = np.mean(white_pixels[:, 1])
r_mean = np.mean(white_pixels[:, 2])
# 计算增益
r_gain = g_mean / r_mean
b_gain = g_mean / b_mean
# 应用增益
img_float = img.astype(np.float32)
img_float[:, :, 2] *= r_gain
img_float[:, :, 0] *= b_gain
return np.clip(img_float, 0, 255).astype(np.uint8)
4.3 色温曲线拟合(Color Temperature Curve Fitting)
前面两种方法都是基于图像内容的,但色温曲线拟合走的是另一条路——它基于先验知识。说白了,就是提前测量好各种光源下 R/G 和 B/G 的比值,然后拟合出一条曲线。实际使用时,根据图像算出的 R/G、B/G 比值,在曲线上找到对应的色温,再用这个色温去查表得到增益。
我当年在手机 ISP 团队时,就负责搭建这套标定系统。我们会在暗室里用 D65(6500K)、D50(5000K)、A 光源(2856K)等标准光源拍摄灰卡,记录下不同色温下的 R/G、B/G 比值。然后把这些数据点拟合成一条平滑曲线。
色温曲线拟合的典型数据长这样:
| 色温 (K) | R/G | B/G | 光源类型 |
|---|---|---|---|
| 2800 | 1.85 | 0.65 | 白炽灯 |
| 4000 | 1.45 | 0.85 | 荧光灯 |
| 5500 | 1.20 | 1.05 | 日光 |
| 6500 | 1.10 | 1.15 | 阴天 |
| 8000 | 1.00 | 1.30 | 阴影 |
拟合曲线后,算法流程就变成了:
- 从图像中提取 R/G、B/G 比值(可以用灰度世界或白点检测的结果)
- 在曲线上找到最接近的色温点
- 用该色温对应的增益值做白平衡校正
下面这张 SVG 图展示了三种 AWB 算法的核心逻辑和它们之间的关系:
4.4 三种算法的对比与选型建议
讲到这里,你可能要问了:这三种算法到底该用哪个?我的答案是:看场景。
我整理了一个对比表,方便你快速决策:
| 维度 | 灰度世界 | 白点检测 | 色温曲线 |
|---|---|---|---|
| 计算量 | 极低 | 中等 | 低(查表) |
| 适用场景 | 色彩丰富的自然场景 | 有白色/灰色参照物 | 已知光源类型 |
| 失败场景 | 单色大面积场景 | 无白点/低照度 | 混合光源 |
| 鲁棒性 | 一般 | 较好 | 依赖标定质量 |
| 实现难度 | 简单 | 中等 | 较复杂(需标定) |