3、高级算法:基于色温估计的算法、基于边缘信息的白平衡、融合策略与自适应选择
好,我们进入第三章。前两章我们聊了基础方法和一些经典套路。但说实话,真实场景远比实验室复杂。你想想看,一张照片里可能有暖色的灯光、冷色的阴影,还有各种材质的反光。这时候,单一算法往往顾此失彼。
所以这一章,我们来点硬核的。我会带你看看三种高级思路:基于色温估计、基于边缘信息,以及融合与自适应策略。这些方法,是我在实际项目中反复打磨过的。
3.1 基于色温估计的算法
先说说色温估计。这玩意儿听起来玄乎,其实核心就一句话:找到光源的颜色,然后把它掰正。
我个人习惯把这类方法分成两步走。第一步,估计场景中的光源色温。第二步,根据色温值计算增益矩阵。嗯,这里要注意,色温不是随便猜的,它得符合物理规律。
3.1.1 普朗克轨迹与色温查找
黑体辐射的色温轨迹,在色度图上是一条曲线。我们叫它普朗克轨迹。不同光源,比如白炽灯(~2800K)、日光(~5500K)、阴天(~6500K),都落在这条曲线上。
算法怎么做?简单说,就是统计图像中所有像素的色度分布,然后找到一条最匹配的普朗克轨迹曲线。我在项目中遇到过一个问题:如果场景里全是单一颜色(比如一片蓝天),色温估计会偏得离谱。后来我加了一个约束——只统计中高亮度的像素,效果好了很多。
关键点:色温估计的精度,很大程度上取决于你如何筛选参与统计的像素。暗部噪声大,高光容易饱和,中间调才是好数据。
3.1.2 基于色温的增益计算
有了色温值,接下来就是算增益。假设我们估计出色温是 5000K,那么 R、G、B 三个通道的增益可以查表得到。这个表是事先标定好的,对应不同色温下的白点坐标。
代码实现其实不复杂。我贴一段核心逻辑:
// 伪代码:基于色温的白平衡增益计算
float estimateColorTemp(const Image& img) {
// 1. 提取中高亮度像素
// 2. 计算色度直方图
// 3. 匹配普朗克轨迹,返回色温值
}
void applyWhiteBalance(Image& img, float colorTemp) {
// 查表得到 R、G、B 增益
float gainR = lookupGainR(colorTemp);
float gainG = lookupGainG(colorTemp);
float gainB = lookupGainB(colorTemp);
// 应用增益
for each pixel (r, g, b) {
r *= gainR;
g *= gainG;
b *= gainB;
}
}
我的经验:查表法虽然快,但精度受限于标定数据。如果你追求极致效果,可以用多项式拟合代替查表。我曾经在一个手机项目里这么干过,色温估计的连续性好了不少。
3.2 基于边缘信息的白平衡
好,接下来聊个有意思的思路——利用边缘信息。你可能会问,白平衡跟边缘有什么关系?
其实道理很简单。图像中真正的边缘,往往对应物体的轮廓。这些地方的颜色变化,通常不包含光源信息。换句话说,边缘像素更能反映物体本身的颜色。
3.2.1 为什么边缘信息有效?
我举个例子。你拍一张白墙前面放个红苹果的照片。白墙受光源影响大,但苹果边缘的红绿过渡,基本不受光源颜色干扰。如果我们只拿边缘附近的像素来做白平衡统计,就能避开大面积的均匀色块(比如墙壁),从而得到更准确的估计。
说白了,边缘信息相当于一个天然的「颜色恒常性」滤波器。
3.2.2 算法实现步骤
具体怎么做?我一般分三步:
- 计算梯度图:用 Sobel 或 Canny 检测边缘。
- 筛选边缘像素:只保留梯度幅值大于阈值的像素。
- 在这些像素上做白平衡统计:比如用灰度世界法或完美反射法。
这里有个坑。我曾经在低光照场景下试过,边缘检测出来的全是噪声,根本没法用。后来我加了一个预处理——先做高斯模糊,再算梯度。噪声被抑制了,边缘也更干净。
注意:边缘阈值的选择很关键。阈值太高,边缘太少,统计样本不足;阈值太低,噪声混入,结果反而变差。我建议用 Otsu 方法自动计算阈值,或者保留梯度幅值前 20% 的像素。
3.2.3 代码示例
// 伪代码:基于边缘信息的白平衡
Image edgeBasedWhiteBalance(const Image& img) {
// 1. 高斯模糊
Image blurred = gaussianBlur(img, 3);
// 2. 计算梯度
Image gradX = sobelX(blurred);
Image gradY = sobelY(blurred);
Image gradMag = sqrt(gradX^2 + gradY^2);
// 3. 筛选边缘像素
float threshold = otsuThreshold(gradMag);
std::vector<Pixel> edgePixels;
for each pixel (x, y) {
if (gradMag(x, y) > threshold) {
edgePixels.push_back(img(x, y));
}
}
// 4. 在边缘像素上应用灰度世界法
float avgR = mean(edgePixels, 'R');
float avgG = mean(edgePixels, 'G');
float avgB = mean(edgePixels, 'B');
float gainR = avgG / avgR;
float gainB = avgG / avgB;
// 5. 应用到全图
return applyGain(img, gainR, 1.0, gainB);
}
3.3 融合策略与自适应选择
讲到这里,你可能会问:既然每种方法都有优缺点,能不能把它们结合起来?
当然可以。这也是我目前在项目中主要使用的思路。融合不是简单的平均,而是根据场景特征动态选择。
3.3.1 加权融合
最简单的融合方式就是加权平均。比如,色温估计法在均匀光照下表现好,边缘法在纹理丰富的场景中更准。我们可以根据图像的纹理复杂度,动态调整权重。
我常用的一个公式是这样的:
// 加权融合
float textureScore = computeTextureScore(img); // 0~1
float weightColorTemp = 1.0 - textureScore;
float weightEdge = textureScore;
Image result = weightColorTemp * colorTempResult + weightEdge * edgeResult;
纹理复杂度怎么算?可以用梯度幅值的均值,也可以用拉普拉斯算子的响应。我个人偏好用拉普拉斯,因为它对边缘的响应更集中。
3.3.2 自适应选择
加权融合虽然简单,但有时候会两头不讨好。比如,一张既有大面积纯色又有丰富纹理的照片,加权平均的结果可能既不如色温法也不如边缘法。
这时候,我建议用自适应选择。思路是:先对图像做区域分割,然后对每个区域单独选择最优算法。
举个例子:
- 天空区域(低纹理)→ 用色温估计法
- 草地边缘(高纹理)→ 用边缘法
- 人物皮肤(中等纹理)→ 用两种方法的加权
这样做的好处是,每个区域都能得到最适合它的处理。代价是计算量大了不少。不过现在硬件性能上来了,这点开销可以接受。
我的建议:如果你做的是实时系统(比如手机相机预览),用加权融合就够了。如果是离线处理(比如修图软件),可以上自适应选择,效果更精细。
3.3.3 避坑指南
最后,分享几个我踩过的坑:
- 不要在所有场景都用融合。有些场景单一算法就够用了,融合反而引入不必要的复杂度。
- 注意权重平滑。如果权重在帧与帧之间跳变,视频会出现闪烁。我一般会加一个低通滤波器。
- 边缘法在低光照下慎用。我曾经在一个夜景项目里吃过亏,边缘全是噪声,结果白平衡完全乱掉。后来我加了一个光照强度检测,光照低于某个阈值就自动切回色温估计法。