传统白平衡算法:灰度世界法、完美反射法、动态阈值法、色温估计法
说到白平衡,很多刚入行的朋友会觉得——不就是让白色变白吗?其实没那么简单。我做了这么多年图像处理,踩过的坑不少,今天就把四种最经典的传统方法掰开揉碎了讲给你听。
这四种方法,说白了就是四种不同的「猜色温」策略。它们各有脾气,也各有擅长场景。我们一个个来看。
灰度世界法(Gray World)
这个算法的假设很朴素:一张色彩丰富的照片,所有像素的RGB平均值应该趋近于灰色。换句话说,R、G、B三个通道的均值应该相等。
为什么能这么假设?你想想看,自然场景里红、绿、蓝物体出现的概率差不多,平均下来就是灰的。嗯,这个假设在大多数户外场景下都挺靠谱。
具体怎么做?我直接上代码:
def gray_world(img):
# 计算每个通道的均值
mean_r = np.mean(img[:,:,0])
mean_g = np.mean(img[:,:,1])
mean_b = np.mean(img[:,:,2])
# 计算全局均值
mean_gray = (mean_r + mean_g + mean_b) / 3
# 计算增益并校正
gain_r = mean_gray / mean_r
gain_g = mean_gray / mean_g
gain_b = mean_gray / mean_b
result = img.copy()
result[:,:,0] = np.clip(img[:,:,0] * gain_r, 0, 255)
result[:,:,1] = np.clip(img[:,:,1] * gain_g, 0, 255)
result[:,:,2] = np.clip(img[:,:,2] * gain_b, 0, 255)
return result.astype(np.uint8)
我的经验:灰度世界法对大面积单色场景会失效。比如你拍一片蓝天,均值偏蓝,校正后反而偏黄。我曾在无人机航拍项目里吃过这个亏——拍海面时画面直接变黄了。后来加了场景判断才解决。
完美反射法(Perfect Reflector)
这个方法换个角度想:图像中最亮的像素点,应该就是白色光源的反射。找到这些「最亮点」,用它们的RGB值来估计色温。
实现思路是这样的:
- 把图像转成灰度,找到最亮的前1%像素
- 提取这些像素的原始RGB值
- 计算每个通道的最大值作为参考白点
- 用参考白点校正整张图
def perfect_reflector(img, percentile=99):
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
threshold = np.percentile(gray, percentile)
mask = gray >= threshold
max_r = np.max(img[:,:,0][mask])
max_g = np.max(img[:,:,1][mask])
max_b = np.max(img[:,:,2][mask])
# 以最亮点为参考
result = img.copy()
result[:,:,0] = np.clip(img[:,:,0] * (255/max_r), 0, 255)
result[:,:,1] = np.clip(img[:,:,1] * (255/max_g), 0, 255)
result[:,:,2] = np.clip(img[:,:,2] * (255/max_b), 0, 255)
return result.astype(np.uint8)
注意:如果场景里本身就有高光(比如太阳、灯泡),完美反射法会把这些高光误判为白色参考点,导致校正过度。我曾经在室内灯光场景里遇到过,画面直接偏蓝了。
动态阈值法
这个方法比前两个聪明一些。它不依赖全局假设,而是动态地找白色区域。核心思路是:在YCrCb色彩空间里,找到色度分量(Cr、Cb)接近零的区域,这些区域就是白色。
具体步骤:
- 把图像从RGB转到YCrCb
- 计算Cr和Cb的均值和标准差
- 设定阈值:|Cr - mean_Cr| < k*std_Cr 且 |Cb - mean_Cb| < k*std_Cb
- 满足条件的像素视为白色参考点
- 用这些参考点计算增益
def dynamic_threshold(img, k=1.5):
ycrcb = cv2.cvtColor(img, cv2.COLOR_RGB2YCrCb)
y, cr, cb = cv2.split(ycrcb)
mean_cr, std_cr = np.mean(cr), np.std(cr)
mean_cb, std_cb = np.mean(cb), np.std(cb)
# 动态阈值筛选白色点
mask = (np.abs(cr - mean_cr) < k*std_cr) & \
(np.abs(cb - mean_cb) < k*std_cb)
if np.sum(mask) == 0:
return img # 没找到白色点就原图返回
# 用白色点计算增益
mean_r = np.mean(img[:,:,0][mask])
mean_g = np.mean(img[:,:,1][mask])
mean_b = np.mean(img[:,:,2][mask])
mean_gray = (mean_r + mean_g + mean_b) / 3
gain_r = mean_gray / mean_r
gain_g = mean_gray / mean_g
gain_b = mean_gray / mean_b
result = img.copy()
result[:,:,0] = np.clip(img[:,:,0] * gain_r, 0, 255)
result[:,:,1] = np.clip(img[:,:,1] * gain_g, 0, 255)
result[:,:,2] = np.clip(img[:,:,2] * gain_b, 0, 255)
return result.astype(np.uint8)
关键参数:k值控制白色区域的筛选严格程度。k越小,筛选越严格,白色点越少但越准。我一般用1.5~2.0之间,具体看场景。
色温估计法
这个方法最直接——既然白平衡的本质是估计色温,那就直接算色温。利用普朗克黑体辐射曲线,建立色温与RGB比值的关系。
实际应用中,我们通常用查表法:
| 色温(K) | R/G比值 | B/G比值 | 典型场景 |
|---|---|---|---|
| 2500 | 2.10 | 0.45 | 烛光、日落 |
| 4000 | 1.40 | 0.75 | 暖白荧光灯 |
| 5500 | 1.00 | 1.00 | 正午日光 |
| 6500 | 0.85 | 1.20 | 阴天、阴影 |
| 8000 | 0.70 | 1.50 | 蓝天下的阴影 |
算法流程:
- 计算图像的平均R/G和B/G比值
- 在色温表中找到最接近的色温值
- 用该色温对应的增益系数校正
我的建议:色温估计法最适合有先验知识的场景。比如你知道拍摄环境是室内荧光灯,直接查表用4000K的系数,效果往往比自适应算法好。我在做医疗内窥镜项目时就这么干的——因为光源色温是固定的。
四种方法对比总结
| 算法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 灰度世界法 | 简单快速,户外效果好 | 大面积单色场景失效 | 自然风景、街拍 |
| 完美反射法 | 对高光场景鲁棒 | 易受高光干扰 | 室内、有白色物体场景 |
| 动态阈值法 | 自适应强,准确度高 | 计算量大,参数调优麻烦 | 复杂光照场景 |
| 色温估计法 | 物理意义明确,可预测 | 需要先验知识 | 固定光源场景 |
说实话,没有哪个算法是万能的。我个人的习惯是:先用动态阈值法做主力,如果场景特殊就切换到色温估计法。灰度世界法和完美反射法虽然简单,但在很多移动端设备上依然在用——毕竟算力有限。
嗯,这里要提醒你:实际工程中,往往需要组合使用。比如先灰度世界法粗调,再用动态阈值法精调。我曾经在一个安防监控项目里就是这么干的,效果比单独用任何一种都好。
最后说一句:这些传统方法虽然看起来「老」,但它们是理解AI白平衡的基础。你想想看,深度学习模型做的无非就是更复杂的色温估计和增益计算。把传统方法吃透了,后面学AI方法会轻松很多。