目录
一、阶段 1 粗搜索 + 阶段 2 横向求精・完整详解 + 数值实例
前置准备(示例参数)
① CT‑Curve 色温折线控制点(4 个采样光源点)
② 图像网格采样
③ 算法参数
二、阶段 1:Coarse Search 粗搜索
遍历候选光源(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‑1:求峰值点所在线段、切线向量
步骤 2‑2:顺时针旋转 90°,得到 Transverse 横向法线向量
步骤 2‑3:生成横向偏移候选白点
步骤 2‑4:对 3 个偏移白点重新打分(打分公式和粗搜索完全一样!)
候选白点 P0(无偏移,峰值点)
阶段 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)
- 白平衡增益
2. 逐个色块计算残差
- 色块 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 个候选白点:
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、荧光灯) |
四、代码实现
#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 读取之后全程冻结。