你是否遇到过这样的场景?在暖色灯光下拍摄的产品照片整体泛黄,或是阴天拍摄的风景照蒙着一层蓝色调。这种色偏问题不仅影响观感,更可能误导后续的图像分析。今天我们就用Python和OpenCV,从零实现经典的灰度世界白平衡算法,让你的图像色彩回归真实。
当我们谈论白平衡时,本质上是在讨论如何消除光源色温对物体真实颜色的影响。想象一下,在咖啡馆暖黄色灯光下,一张白纸在照片中会呈现黄色调;而在阴天的蓝光环境下,同样的白纸又会显得发蓝。人眼能自动适应这些变化,但相机传感器需要算法辅助才能达到类似效果。
灰度世界法(Gray World Algorithm)是最基础也最直观的自动白平衡方法之一。它的核心假设非常简洁:
自然场景中所有颜色的平均反射率趋近于中性灰(R=G=B)
基于这个假设,我们可以计算出整个图像在R、G、B三个通道的平均值,然后以绿色通道为基准,调整红蓝通道的增益,使三个通道的平均值相等。这种方法计算量小,在多数自然场景下效果不错,特别适合作为初学者的第一个白平衡实现项目。
主要优点:
局限性:
在开始编码前,我们需要准备好Python环境。推荐使用Anaconda创建虚拟环境:
bash复制conda create -n awb python=3.8
conda activate awb
pip install opencv-python numpy matplotlib
基础版的灰度世界法实现仅需不到20行代码。让我们先看一个最简实现:
python复制import cv2
import numpy as np
def gray_world_balance(img):
# 计算各通道平均值
avg_b = np.mean(img[:,:,0])
avg_g = np.mean(img[:,:,1])
avg_r = np.mean(img[:,:,2])
# 计算增益系数(以G通道为基准)
gain_b = avg_g / avg_b
gain_r = avg_g / avg_r
# 应用增益调整
img[:,:,0] = np.clip(img[:,:,0] * gain_b, 0, 255)
img[:,:,2] = np.clip(img[:,:,2] * gain_r, 0, 255)
return img
# 使用示例
image = cv2.imread('input.jpg')
balanced = gray_world_balance(image)
cv2.imwrite('output.jpg', balanced)
这个基础版本已经能处理大多数简单场景。让我们拆解关键步骤:
基础实现虽然简单,但在实际应用中可能会遇到各种问题。下面我们逐步添加优化策略。
原始算法对图像中的极端值(过亮/过暗区域)和噪声非常敏感。我们可以通过以下改进增强鲁棒性:
python复制def robust_gray_world(img, percentile=5):
# 计算各通道的百分位数(排除极端值)
low = percentile / 100.0
high = 1 - low
# 使用百分位数替代全局均值
avg_b = np.percentile(img[:,:,0], 50) # 中位数替代均值
avg_g = np.percentile(img[:,:,1], 50)
avg_r = np.percentile(img[:,:,2], 50)
# 添加平滑处理
gain_b = avg_g / (avg_b + 1e-6) # 避免除零
gain_r = avg_g / (avg_r + 1e-6)
# 应用增益调整
balanced = img.copy()
balanced[:,:,0] = np.clip(balanced[:,:,0] * gain_b, 0, 255)
balanced[:,:,2] = np.clip(balanced[:,:,2] * gain_r, 0, 255)
return balanced
优化点说明:
对于高分辨率图像,我们可以采用多尺度处理策略提升性能:
python复制def multi_scale_balance(img, min_size=256):
# 构建金字塔
pyramid = [img]
while min(pyramid[-1].shape[:2]) > min_size:
pyramid.append(cv2.pyrDown(pyramid[-1]))
# 从最上层开始处理
balanced = pyramid[-1]
for i in range(len(pyramid)-2, -1, -1):
# 应用灰度世界法
balanced = gray_world_balance(balanced)
# 上采样并应用到下一层
balanced = cv2.resize(balanced, (pyramid[i].shape[1], pyramid[i].shape[0]))
return balanced
白平衡调整有时会导致图像亮度变化或饱和度降低。我们可以添加补偿:
python复制def brightness_preserving_balance(img):
# 转换到HSV色彩空间
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# 保存原始亮度信息
original_v = hsv[:,:,2].copy()
# 应用灰度世界法
balanced = gray_world_balance(img)
# 转换平衡后的图像到HSV
balanced_hsv = cv2.cvtColor(balanced, cv2.COLOR_BGR2HSV)
# 恢复原始亮度
balanced_hsv[:,:,2] = original_v
# 转换回BGR
result = cv2.cvtColor(balanced_hsv, cv2.COLOR_HSV2BGR)
return result
实现算法后,我们需要客观评估其效果。以下是几种常用的评估方法:
最简单直接的方法是通过人眼观察校正前后的差异。我们可以创建一个对比展示函数:
python复制def compare_results(original, balanced, title='Comparison'):
# 水平拼接图像
comparison = np.hstack((original, balanced))
# 添加文字标注
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(comparison, 'Original', (10,30), font, 1, (255,255,255), 2)
cv2.putText(comparison, 'Balanced', (original.shape[1]+10,30), font, 1, (255,255,255), 2)
cv2.imshow(title, comparison)
cv2.waitKey(0)
cv2.destroyAllWindows()
除了主观评估,我们还可以计算一些客观指标:
| 指标名称 | 计算公式 | 理想值 | 说明 |
|---|---|---|---|
| 色偏指数 | ΔE = √[(R-128)² + (G-128)² + (B-128)²] | 越小越好 | 衡量整体色偏程度 |
| 通道差异 | max(avg_r,avg_g,avg_b)/min(avg_r,avg_g,avg_b) | 接近1 | 三通道平衡度 |
| 自然度评分 | 基于色彩统计模型 | 0-1 | 越高越自然 |
实现色偏指数计算的Python代码:
python复制def color_cast_index(img):
# 转换为LAB色彩空间
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
# 计算a、b通道均值
avg_a = np.mean(lab[:,:,1]) - 128
avg_b = np.mean(lab[:,:,2]) - 128
# 计算色偏指数
delta_e = np.sqrt(avg_a**2 + avg_b**2)
return delta_e
让我们看几个典型场景的处理效果:
案例1:室内暖光照片
案例2:阴天风景照
案例3:日落逆光人像
将上述优化整合,我们得到最终的实现版本:
python复制import cv2
import numpy as np
class GrayWorldAWB:
def __init__(self, percentile=2, preserve_brightness=True):
self.percentile = percentile
self.preserve_brightness = preserve_brightness
def apply(self, img):
if self.preserve_brightness:
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
original_v = hsv[:,:,2].copy()
# 使用百分位数计算
avg_b = np.percentile(img[:,:,0], 50)
avg_g = np.percentile(img[:,:,1], 50)
avg_r = np.percentile(img[:,:,2], 50)
# 计算增益
gain_b = avg_g / (avg_b + 1e-6)
gain_r = avg_g / (avg_r + 1e-6)
# 应用增益
balanced = img.copy()
balanced[:,:,0] = np.clip(balanced[:,:,0] * gain_b, 0, 255)
balanced[:,:,2] = np.clip(balanced[:,:,2] * gain_r, 0, 255)
if self.preserve_brightness:
balanced_hsv = cv2.cvtColor(balanced, cv2.COLOR_BGR2HSV)
balanced_hsv[:,:,2] = original_v
balanced = cv2.cvtColor(balanced_hsv, cv2.COLOR_HSV2BGR)
return balanced.astype(np.uint8)
# 使用示例
awb = GrayWorldAWB(percentile=5, preserve_brightness=True)
image = cv2.imread('input.jpg')
balanced = awb.apply(image)
cv2.imwrite('output.jpg', balanced)
工程化建议:
掌握了基础实现后,你可以进一步探索以下方向:
将算法应用于视频流只需稍作修改:
python复制def process_video(input_path, output_path):
cap = cv2.VideoCapture(input_path)
fps = cap.get(cv2.CAP_PROP_FPS)
size = (int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter(output_path, fourcc, fps, size)
awb = GrayWorldAWB()
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
balanced = awb.apply(frame)
out.write(balanced)
cap.release()
out.release()
虽然传统方法有效,但结合深度学习可以获得更好效果:
python复制# 伪代码示例 - 实际需要训练模型
class HybridAWB:
def __init__(self, model_path):
self.model = load_keras_model(model_path)
self.gray_world = GrayWorldAWB()
def apply(self, img):
# 使用模型预测场景类型
scene_type = self.model.predict(preprocess(img))
if scene_type == 'normal':
return self.gray_world.apply(img)
else:
return specialized_balance(img, scene_type)
对于资源受限设备,可以考虑以下优化:
cpp复制// 示例C++实现片段
void applyGrayWorldAWB(Mat &img) {
// 计算通道均值
Scalar means = mean(img);
double avg_b = means[0], avg_g = means[1], avg_r = means[2];
// 计算并应用增益
for(int i=0; i<img.rows; ++i) {
for(int j=0; j<img.cols; ++j) {
img.at<Vec3b>(i,j)[0] = saturate_cast<uchar>(img.at<Vec3b>(i,j)[0] * (avg_g/avg_b));
img.at<Vec3b>(i,j)[2] = saturate_cast<uchar>(img.at<Vec3b>(i,j)[2] * (avg_g/avg_r));
}
}
}