树莓派相机自动白平衡详解(二)

目录

[一、阶段 1 粗搜索 + 阶段 2 横向求精・完整详解 + 数值实例](#一、阶段 1 粗搜索 + 阶段 2 横向求精・完整详解 + 数值实例)

前置准备(示例参数)

[① CT‑Curve 色温折线控制点(4 个采样光源点)](#① CT‑Curve 色温折线控制点(4 个采样光源点))

[② 图像网格采样](#② 图像网格采样)

[③ 算法参数](#③ 算法参数)

[二、阶段 1:Coarse Search 粗搜索](#二、阶段 1:Coarse Search 粗搜索)

[遍历候选光源(8575K,r=0.42, b=0.80)](#遍历候选光源(8575K,r=0.42, b=0.80))

遍历候选光源 (5716K,r=0.58, b=0.70)

遍历候选光源 (3627K,r=0.75, b=0.54)

遍历候选光源 (2498K,r=0.92, b=0.36)

粗搜索全部结果汇总

[三、阶段 2:Fine‑Search 精细 Transverse(横向)求精](#三、阶段 2:Fine‑Search 精细 Transverse(横向)求精)

[步骤 2‑1:求峰值点所在线段、切线向量](#步骤 2‑1:求峰值点所在线段、切线向量)

[步骤 2‑2:顺时针旋转 90°,得到 Transverse 横向法线向量](#步骤 2‑2:顺时针旋转 90°,得到 Transverse 横向法线向量)

[步骤 2‑3:生成横向偏移候选白点](#步骤 2‑3:生成横向偏移候选白点)

[步骤 2‑4:对 3 个偏移白点重新打分(打分公式和粗搜索完全一样!)](#步骤 2‑4:对 3 个偏移白点重新打分(打分公式和粗搜索完全一样!))

[候选白点 P0(无偏移,峰值点)](#候选白点 P0(无偏移,峰值点))

[阶段 1 与阶段 2 对比](#阶段 1 与阶段 2 对比)

四、代码实现

五、核心误区再次强调


一、阶段 1 粗搜索 + 阶段 2 横向求精・完整详解 + 数值实例

前置准备(示例参数)

① CT‑Curve 色温折线控制点(4 个采样光源点)

索引 色温 \(r=R/G\) \(b=B/G\)
8575K 0.42 0.80
5716K 0.58 0.70
3627K 0.75 0.54
2498K 0.92 0.36

折线顺序:

② 图像网格采样

为简化案例,我们不用完整 16×12=192 块,取3 块网格色块

色块编号 色块色度
色块 A
色块 B
色块 C

物理场景:室内荧光灯照明。 真实白点不在 CT 曲线上面,后面精细搜索就会看到效果。

③ 算法参数

  1. 高斯标准差:,代价函数:

相等时常数分母可以约去,直接比较残差平方和; 2. 先验代价:,代价越小代表光源出现概率越高

光源
先验代价 8.0 4.0 1.0 3.0
  1. 总代价公式(负对数似然,最小化):

规则:候选白点严格限制在 CT‑Curve 折线上,不能离开曲线 ;沿着曲线大步遍历所有控制点;对每一个候选光源,全部网格重新计算一遍残差打分

遍历候选光源(8575K,r=0.42, b=0.80)

  1. 白平衡增益
  1. 逐个色块计算残差
  • 色块 A:
  • 色块 B:
  • 色块 C:

网格总代价 = 0.687+0.771+0.786 = 2.244

总代价 = 网格代价 + 先验代价 = 2.244 + 8.0 = 10.244

遍历候选光源 (5716K,r=0.58, b=0.70)

色块残差累加网格代价≈1.112 总代价 =1.112+4.0= 5.112

遍历候选光源 (3627K,r=0.75, b=0.54)

  • 色块 A
  • 色块 B
  • 色块 C

网格总代价 = 0.0005+0.0005+0.0030=0.004 总代价 = 0.004 + 1.0 = 1.004

遍历候选光源 (2498K,r=0.92, b=0.36)

网格代价≈1.431 总代价 = 1.431+3.0=4.431

粗搜索全部结果汇总

光源 总代价
10.244
5.112
1.004 (最小值,峰值)
4.431

👉粗搜索输出峰值点:

关键点:

  1. 一定落在 CT‑Curve 折线之上
  2. 全程没有色块向 CT 曲线投影运算;每换一个光源白点,全部色块残差重新计算一遍

三、阶段 2:Fine‑Search 精细 Transverse(横向)求精

文档原文:fine search is allowed to wander transversely off the CT Curve 核心:移动候选光源白点,垂直离开 CT 曲线;色块网格数据完全固定不变,不做任何修改

步骤 2‑1:求峰值点所在线段、切线向量

的线段上线段向量(切线方向,沿着 CT 曲线色温降低):

切线向量沿着曲线走 →色温变化方向

步骤 2‑2:顺时针旋转 90°,得到 Transverse 横向法线向量

二维向量顺时针旋转 90° 公式:

= 横向方向(垂直 CT 曲线切线) ,也就是文档中 transverse 方向。 沿着这个方向移动白点,色温几乎不变,白点偏离 CT 曲线。

归一化,得到单位横向法线向量: 向量模长:

步骤 2‑3:生成横向偏移候选白点

设置最大横向偏移步长 我们采样 3 个候选白点:

  1. 中心点(无偏移,粗搜索峰值)

  2. 正向横向偏移

  3. 负向横向偏移

注意:两个白点不在原始 CT 曲线折线上,完成了 wander transversely off the CT Curve。

步骤 2‑4:对 3 个偏移白点重新打分(打分公式和粗搜索完全一样!)

色块数据 A/B/C 保持不变,仅更换候选白点。

候选白点 P0(无偏移,峰值点)

总代价 = 1.004,前面粗搜索已经算出。

候选白点 P1 (r=0.7209, b=0.5125)

逐个色块算残差平方求和,网格代价≈0.0012 先验代价依然取 1.0 总代价 = 0.0012 + 1.0 = 1.0012

候选白点 P2 (r=0.7791,b=0.5675) 网格代价≈0.027,总代价 = 1.027

精细搜索打分汇总

候选白点 是否位于 CT 曲线 总代价
P0(峰值原点) ✅是 1.004
P1(横向偏移‑法线方向) ❌离开曲线 1.0012 (最小代价最优解)
P2(反向横向偏移) ❌离开曲线 1.027

👉最终输出白点:

实例解读: 当前场景光源是荧光灯,它的白点并不落在黑体 CT 曲线上; 粗搜索只能在曲线上找点,找到一个近似解; 精细搜索沿着横向法线跳出曲线,找到了代价更低、白平衡效果更好的白点。完美对应官方文档中「适配荧光灯」的设计目标。


#几何方向文本示意图(r‑b 色度平面)

复制代码
b ↑
  |
  |        I1
  |          \
  |            \    I2(Ppeak) ● ← CT‑Curve折线
  |               \     ↘
  |                  \ ●P1 (横向偏移最优白点,跳出曲线)
  |                     \
  |                        I3
  |
  ‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑> r
                    
      → τ:切线方向(沿着曲线色温降低)
      ↘ n:Transverse横向法线方向(白点偏移方向,垂直切线)

阶段 1 与阶段 2 对比

项目 粗搜索 Coarse Search 精细横向求精 Fine‑Transverse Search
候选白点运动范围 严格被限制在 CT‑Curve 折线之上 沿着法线方向可以跳出 CT 曲线
色块网格数据 固定不变 固定不变,全程不会改动色块
移动对象 光源白点,沿曲线切线方向 光源白点,法线横向方向为主,切线微调为辅
打分计算逻辑 \(gain_r=1/r_k,\Delta r=gain_r*r_i-1\) 打分公式完全一模一样,没有任何变化
目标 找到似然峰值所在色温 峰值附近微调白点色度,适配非黑体光源(LED、荧光灯)

四、代码实现

cpp 复制代码
#include <stdio.h>
#include <math.h>
#include <stdbool.h>

//==================== 可调配置参数 ====================
#define NUM_CT_POINTS        4
#define MAX_REGIONS          192         // 真实ISP: 16 x 12 = 192块
#define MAX_TRANS_OFFSET     0.04f       // Transverse横向最大偏移
#define MAX_TANGENT_OFFSET   0.02f       // 切线(色温)方向微调范围
#define STEP_TRANS_SAMPLES   5           // 横向采样点数
#define STEP_TANGENT_SAMPLES 3           // 切线色温方向采样点数
#define RESIDUAL_CLAMP_LIMIT 0.15f       // 残差截断阈值,抑制彩色物体
#define MIN_WHITE_VAL        0.01f       // 白点最小值,防止除零
#define DEBUG_PRINT          1

//==================== 数据结构体 ====================
typedef struct
{
    float temp_k;
    float r;        // r = R/G
    float b;        // b = B/G
    float cost_prior;
}CtPoint;

// ISP网格色块: 加const保证只读,全程冻结不可修改
typedef struct
{
    float r;
    float b;
}GridRegion;

// 二维白点坐标
typedef struct
{
    float r;
    float b;
}WhitePoint;

//==================== 标定数据 (与示例数值保持一致) ====================
const CtPoint ct_curve[NUM_CT_POINTS] =
{
    {8575.0f,  0.42f, 0.80f, 8.0f},
    {5716.0f,  0.58f, 0.70f, 4.0f},
    {3627.0f,  0.75f, 0.54f, 1.0f},
    {2498.0f,  0.92f, 0.36f, 3.0f}
};

// 演示色块数组;正式工程替换为MAX_REGIONS=192
const GridRegion grid[] =
{
    {0.74f, 0.55f},
    {0.76f, 0.53f},
    {0.77f, 0.56f}
};
const int num_regions = sizeof(grid)/sizeof(grid[0]);

//==================== 核心打分函数 (粗搜/精搜共用) ====================
/**
 * @brief  计算候选白点总负对数代价
 * @param  wp         候选白点
 * @param  dist_curve 白点距离CT‑Curve的垂直距离,用于先验衰减
 * @return total cost
 */
float calculate_total_cost(const WhitePoint wp, float dist_curve)
{
    // 除零安全保护
    float r_white = fmaxf(wp.r, MIN_WHITE_VAL);
    float b_white = fmaxf(wp.b, MIN_WHITE_VAL);

    float gain_r = 1.0f / r_white;
    float gain_b = 1.0f / b_white;

    float sum_region_cost = 0.0f;
    for(int i = 0; i < num_regions; i++)
    {
        float ri = grid[i].r;
        float bi = grid[i].b;

        float dr = gain_r * ri - 1.0f;
        float db = gain_b * bi - 1.0f;

        // 【新增残差截断Clamp】抑制高饱和彩色色块
        dr = fmaxf(-RESIDUAL_CLAMP_LIMIT, fminf(dr, RESIDUAL_CLAMP_LIMIT));
        db = fmaxf(-RESIDUAL_CLAMP_LIMIT, fminf(db, RESIDUAL_CLAMP_LIMIT));

        sum_region_cost += dr*dr + db*db;
    }

    // 先验代价衰减补偿:白点越远离CT曲线,惩罚越大
    int near_idx = 2; //示例峰值邻近控制点
    float base_prior = ct_curve[near_idx].cost_prior;
    float prior_cost = base_prior + 20.0f * dist_curve * dist_curve;

    float total_cost = sum_region_cost + prior_cost;
    return total_cost;
}

/**
 * @brief 线段上两点之间线性插值
 */
WhitePoint lerp_point(const WhitePoint p0, const WhitePoint p1, float t)
{
    WhitePoint res;
    res.r = p0.r + t*(p1.r - p0.r);
    res.b = p0.b + t*(p1.b - p0.b);
    return res;
}

/**
 * @brief 计算点到线段的垂直距离
 */
float point_segment_dist(WhitePoint pt, WhitePoint seg0, WhitePoint seg1)
{
    float dx = seg1.r - seg0.r;
    float dy = seg1.b - seg0.b;
    float t;

    if(fabsf(dx)<1e-6f && fabsf(dy)<1e-6f)
    {
        dx = pt.r - seg0.r;
        dy = pt.b - seg0.b;
        return sqrtf(dx*dx+dy*dy);
    }

    t = ((pt.r - seg0.r)*dx + (pt.b - seg0.b)*dy)/(dx*dx + dy*dy);
    t = fmaxf(0.0f, fminf(1.0f, t));

    WhitePoint proj;
    proj.r = seg0.r + t*dx;
    proj.b = seg0.b + t*dy;

    float dr = pt.r - proj.r;
    float db = pt.b - proj.b;
    return sqrtf(dr*dr + db*db);
}

//==================== 阶段 1:粗搜索 Coarse Search ====================
void coarse_search(WhitePoint *wp_peak, int *out_peak_seg_idx)
{
    float min_cost = 1e9f;
    int best_seg = 0;
    WhitePoint best_wp;

#if DEBUG_PRINT
    printf("\n========== 阶段 1: Coarse 粗搜索 ==========\n");
#endif

    // 遍历所有折线线段,在每一段内部采样寻找峰值
    for(int seg = 0; seg < NUM_CT_POINTS - 1; seg++)
    {
        WhitePoint p0 = {ct_curve[seg].r, ct_curve[seg].b};
        WhitePoint p1 = {ct_curve[seg+1].r, ct_curve[seg+1].b};

        // 线段内多点采样,寻找线段内最优插值点
        for(int s = 0; s <= 4; s++)
        {
            float t = s / 4.0f;
            WhitePoint wp = lerp_point(p0, p1, t);
            float dist = 0.0f; //插值点就在曲线上,距离=0
            float cost = calculate_total_cost(wp, dist);

#if DEBUG_PRINT
            printf("Seg%d t=%.2f | r=%.3f b=%.3f | Cost=%.4f\n",
                    seg, t, wp.r, wp.b, cost);
#endif
            if(cost < min_cost)
            {
                min_cost = cost;
                best_wp = wp;
                best_seg = seg;
            }
        }
    }

    *wp_peak = best_wp;
    *out_peak_seg_idx = best_seg;

#if DEBUG_PRINT
    printf("\n>>粗搜索峰值白点: r_peak=%.3f, b_peak=%.3f\n",wp_peak->r,wp_peak->b);
#endif
}

//==================== 阶段 2:二维精细搜索 (切线色温 + Transverse横向法线) ====================
void fine_transverse_search(const WhitePoint wp_peak, int seg_idx, WhitePoint *wp_final)
{
#if DEBUG_PRINT
    printf("\n========== 阶段 2: Fine‑Search 二维求精 ==========\n");
#endif

    WhitePoint p0 = {ct_curve[seg_idx].r, ct_curve[seg_idx].b};
    WhitePoint p1 = {ct_curve[seg_idx+1].r, ct_curve[seg_idx+1].b};

    // 切线向量 τ (色温变化方向)
    float tau_x = p1.r - p0.r;
    float tau_y = p1.b - p0.b;

    // 顺时针旋转90° 得到Transverse横向法线向量 n_raw
    // (x,y)顺时针90° → (y, -x)
    float n_x_raw = tau_y;
    float n_y_raw = -tau_x;

    //归一化切线向量
    float len_tau = sqrtf(tau_x*tau_x + tau_y*tau_y);
    float tau_unit_x = tau_x / len_tau;
    float tau_unit_y = tau_y / len_tau;

    //归一化横向法线向量
    float len_n = sqrtf(n_x_raw*n_x_raw + n_y_raw*n_y_raw);
    float n_unit_x = n_x_raw / len_n;
    float n_unit_y = n_y_raw / len_n;

#if DEBUG_PRINT
    printf("切线 τ 单位向量   = (%.3f , %.3f)\n",tau_unit_x,tau_unit_y);
    printf("Transverse法线n̂ = (%.3f , %.3f)\n",n_unit_x,n_unit_y);
#endif

    //二维栅格搜索:切线方向 + 横向法线方向
    float min_fine_cost = 1e9f;
    WhitePoint best_wp = wp_peak;

    //生成切线方向偏移序列
    for(int itan = 0; itan < STEP_TANGENT_SAMPLES; itan++)
    {
        float t_offset = -MAX_TANGENT_OFFSET
                        + itan*(2.0f*MAX_TANGENT_OFFSET)/(STEP_TANGENT_SAMPLES-1);

        //生成横向法线方向偏移序列
        for(int itr = 0; itr < STEP_TRANS_SAMPLES; itr++)
        {
            float trans_offset = -MAX_TRANS_OFFSET
                                + itr*(2.0f*MAX_TRANS_OFFSET)/(STEP_TRANS_SAMPLES-1);

            WhitePoint candidate;
            candidate.r = wp_peak.r + t_offset*tau_unit_x + trans_offset*n_unit_x;
            candidate.b = wp_peak.b + t_offset*tau_unit_y + trans_offset*n_unit_y;

            //计算白点到CT‑Curve垂直距离,用于先验代价惩罚
            float dist = point_segment_dist(candidate, p0, p1);
            float cost = calculate_total_cost(candidate, dist);

#if DEBUG_PRINT
            printf("TanOff=%.4f TransOff=%.4f | r=%.4f b=%.4f | Cost=%.4f\n",
                    t_offset, trans_offset, candidate.r, candidate.b, cost);
#endif

            if(cost < min_fine_cost)
            {
                min_fine_cost = cost;
                best_wp = candidate;
            }
        }
    }

    *wp_final = best_wp;
#if DEBUG_PRINT
    printf("\n>>求精后最终白点 r_final=%.4f , b_final=%.4f\n",best_wp.r,best_wp.b);
#endif
}

//==================== 主入口 ====================
int main(void)
{
    WhitePoint wp_peak;
    WhitePoint wp_final;
    int peak_seg_index;

    coarse_search(&wp_peak, &peak_seg_index);
    fine_transverse_search(wp_peak, peak_seg_index, &wp_final);

    float gain_r = 1.0f / fmaxf(wp_final.r, MIN_WHITE_VAL);
    float gain_b = 1.0f / fmaxf(wp_final.b, MIN_WHITE_VAL);

    printf("\n========== AWB 算法完成 ==========\n");
    printf("粗搜峰值白点    r=%.3f , b=%.3f\n",wp_peak.r,wp_peak.b);
    printf("求精后最终白点  r=%.4f , b=%.4f\n",wp_final.r,wp_final.b);
    printf("白平衡增益 gain_r=%.4f , gain_b=%.4f\n",gain_r,gain_b);

    return 0;
}

五、核心误区再次强调

  1. Transverse 横向偏移,是光源白点的偏移量,不是色块到曲线的垂直距离
  2. 色块永远不会投影到 CT 色温曲线,粗搜索打分完全不需要色块‑曲线距离;
  3. 精细搜索只更换候选白点,色块采样数据从 ISP 读取之后全程冻结。
相关推荐
Scabbards_1 小时前
面试Leetcode - 算法合集
算法·leetcode·面试
chuan.bai1 小时前
Java RAG 实战附录:qwen3 与 bge-m3 模型切换指南
java·人工智能·算法
wuyk5551 小时前
7.AVL 树:第一个自平衡二叉搜索树
开发语言·stm32·单片机·算法
rannn_1112 小时前
【力扣hot100】二叉树专题+总结
java·算法·leetcode·二叉树
啊嘞嘞?2 小时前
力扣(岛屿数量)
算法·leetcode
zander2582 小时前
LeetCode 198. 打家劫舍
算法·leetcode·深度优先
吃着火锅x唱着歌2 小时前
LeetCode 648.单词替换
算法·leetcode·职场和发展
一只积极向上的小咸鱼2 小时前
分词器tokenizer
算法·llm
Elsa️7462 小时前
leetcode 14.最长公共前缀
算法·leetcode·职场和发展