
目录
[一、阶段 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))
[三、阶段 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 曲线上面,后面精细搜索就会看到效果。
③ 算法参数
- 高斯标准差:
,代价函数:
相等时常数分母可以约去,直接比较残差平方和; 2. 先验代价:
,代价越小代表光源出现概率越高
| 光源 | ||||
|---|---|---|---|---|
| 先验代价 | 8.0 | 4.0 | 1.0 | 3.0 |
- 总代价公式(负对数似然,最小化):
二、阶段 1:Coarse Search 粗搜索
规则:候选白点严格限制在 CT‑Curve 折线上,不能离开曲线 ;沿着曲线大步遍历所有控制点;对每一个候选光源,全部网格重新计算一遍残差打分。
遍历候选光源
(8575K,r=0.42, b=0.80)
- 白平衡增益
- 逐个色块计算残差
- 色块 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 |
👉粗搜索输出峰值点:
关键点:
一定落在 CT‑Curve 折线之上
- 全程没有色块向 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 个候选白点:
-
中心点(无偏移,粗搜索峰值)
-
正向横向偏移
-
负向横向偏移
注意:
两个白点不在原始 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;
}
五、核心误区再次强调
- Transverse 横向偏移,是光源白点的偏移量,不是色块到曲线的垂直距离;
- 色块永远不会投影到 CT 色温曲线,粗搜索打分完全不需要色块‑曲线距离;
- 精细搜索只更换候选白点,色块采样数据从 ISP 读取之后全程冻结。