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 算法实现步骤

具体怎么做?我一般分三步:

  1. 计算梯度图:用 Sobel 或 Canny 检测边缘。
  2. 筛选边缘像素:只保留梯度幅值大于阈值的像素。
  3. 在这些像素上做白平衡统计:比如用灰度世界法或完美反射法。

这里有个坑。我曾经在低光照场景下试过,边缘检测出来的全是噪声,根本没法用。后来我加了一个预处理——先做高斯模糊,再算梯度。噪声被抑制了,边缘也更干净。

注意:边缘阈值的选择很关键。阈值太高,边缘太少,统计样本不足;阈值太低,噪声混入,结果反而变差。我建议用 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 避坑指南

最后,分享几个我踩过的坑:

  • 不要在所有场景都用融合。有些场景单一算法就够用了,融合反而引入不必要的复杂度。
  • 注意权重平滑。如果权重在帧与帧之间跳变,视频会出现闪烁。我一般会加一个低通滤波器。
  • 边缘法在低光照下慎用。我曾经在一个夜景项目里吃过亏,边缘全是噪声,结果白平衡完全乱掉。后来我加了一个光照强度检测,光照低于某个阈值就自动切回色温估计法。