
以上说明的树莓派自动白平衡的一些基本思路。但是之前没有找到源码,因此有些理解还是存在一些误差。现在根据源码修正并详细说明。
目录
[五、原版 rpi‑controller vs libipa 移植版关键差异](#五、原版 rpi‑controller vs libipa 移植版关键差异)
一、整体架构与调用流
算法运行于独立异步子线程;主线程只负责调度、拷贝结果、帧间 IIR 平滑、硬件输出。
IPA主线程 process()
↓ 唤醒异步线程 asyncFunc()
↓ doAwb()(AwbBayes子类实现)
1. prepareStats() 统计预处理
└ generateStats()
· 过滤无效AWB统计网格region
· biasProportion:每个region叠加biasCT虚拟白点分量(无中性色块时兜底防发散)
· sensitivityR / sensitivityB 做传感器通道校正
2. 判断有效zone数量,如果小于minRegions,直接跳过本次计算,复用上一帧结果
3. awbBayes() 算法主体
① zone内部归一化:`R = R/(G+1)`,`B = B/(G+1)`,得到R/G、B/G色度
② interpolatePrior():依据当前lux照度,对多组yaml配置的prior做线性插值,生成当前帧prior(PWL:t→−logP(I))
③ prior整体乘以系数 `prior *= valid_zone_count / total_zone_count`;源码注释标注:属于经验调参,非严格贝叶斯推导
④ coarseSearch()【阶段1 粗搜索】:全部候选严格落在CT‑Curve普朗克轨迹上,输出`t_coarse`
⑤ fineSearch()【阶段2精细搜索】:允许白点离开CT‑Curve,适配LED、荧光等非黑体光源
⑥ 换算白平衡增益,乘以传感器校正系数,存入异步缓存`asyncResults_`
↓ fetchAsyncResults() 拷贝子线程结果
· 帧间一阶IIR平滑(不属于Bayes算法本体)
· gain硬件寄存器范围clamp限幅
· 输出增益给ISP,metadata上报色温
二、核心函数逐条解析
1. generateStats / prepareStats
- 过滤 region:
minPixels单 region 有效像素占比、minGG 通道最小值,不满足直接丢弃该 region。 - bias 兜底:不是新增 zone,在现有 region 的 R/B 累加虚拟白点分量,防止整张图无真实中性色块时算法完全失控。
- 乘以
sensitivityR/sensitivityB,把传感器原始色度映射到 canonical 标准色度空间。
2. computeDelta2Sum 似然代价函数
输入候选白平衡增益 gainR、gainB,中性点偏移whitepointR/whitepointB(通常 = 0)
deltaLimit截断保护 :delta2 = min(delta2, deltaLimit);单个网格残差超过阈值直接钳位,抑制大片红墙、绿草等高饱和彩色物体干扰总打分。 返回所有 zone 截断后的残差平方累加和delta2Sum。
总打分:finalLogLikelihood = delta2Sum − priorLogLikelihood;最小化该值等价最大化后验概率。
3. interpolatePrior
yaml 配置多组AwbPrior{lux, prior‑PWL}。
- lux 小于最小配置 lux,取最小 lux 对应的 prior;lux 大于最大配置 lux,取最大 lux 对应的 prior。
- lux 落在两组 lux 中间,对两条 prior 折线做线性插值合成当前帧 prior。
- awbBayes 内执行经验缩放:
prior *= valid_zone / total_zone。
4. coarseSearch 阶段 1:CT‑Curve 全局粗搜索
- 在 mode 限定色温区间
[ctLo, ctHi]遍历色温t;自适应步长:t += t/10 * coarseStep;色温越高步长越大。 - 每个
t查表 CT‑Curve 得到曲线上白点;计算增益
gainR=1/r,gainB=1/b。 - 调用
computeDelta2Sum()得到似然代价;查表 prior 得到priorLogLikelihood;计算finalLogLikelihood。 - 保存所有
(t, finalLogLikelihood);选出代价最小采样点;取该点左右相邻点,调用interpolateQuadatric()三点二次抛物线插值,输出粗搜索最优色温t_coarse。
所有候选点严格位于 CT‑Curve 上,不能横向偏移。
5. fineSearch 阶段 2 精细搜索【重点,严格对齐原始 patch】
原始源码注释:Mostly the estimated CT is already good enough, but the fine search allows us to wander transverely off the CT curve. 翻译:绝大多数场景粗搜索输出的色温已经足够好;fineSearch主要设计目标:让白点可以离开普朗克 CT‑Curve,适配非黑体光源 ;色温发生变化属于循环带来的附带副作用,不是设计目标。
执行步骤:
- 使用输入的粗搜索结果
tIn = t_coarse;取tIn ± nsteps*step两点,计算差分得到切线,构造transverse向量,归一化为单位向量。
⚠️该向量仅在
tIn位置几何上是 CT‑Curve 法线 ;向量一旦计算完成,全程固定不变 ;施加到其他tTest仅做简单矢量加法;代码不校验其他位置的几何法线关系。
-
如果向量模平方过小
length2() <1e‑6,直接 return,不做横向偏移,输出保持 CT‑Curve 上结果。 -
nsteps变量先用于计算 transverse 向量;之后执行nsteps += numDeltas,扩大外层循环的扫描范围;源码注释意图:横向方向采样点越多,顺带把 tTest 扫描范围放大。 -
外层 i 循环:生成一组测试色温
tTest = tIn + i*step- 每个
tTest查表 CT‑Curve,得到该色温对应的曲线上基准点;查表 prior 得到
priorLogLikelihood。 - 内层 j 循环 :dist 取值区间硬边界
[-transverseNeg, transversePos];dist 是固定 transverse 单位向量的缩放系数。
⚠️源码完全不计算候选白点到 CT‑Curve 的真实垂直几何距离 ;没有 K・d² 二次距离惩罚项;仅限制 dist 缩放系数的取值范围。
- 每个 dist,生成候选白点,计算 gainR/gainB,调用
computeDelta2Sum(),得到该点总代价。 - j 循环结束,对 j 方向采样点做
interpolateQuadatric()二次插值,得到该tTest的最优distOpt;必须重算插值点完整代价,不能复用采样数组内的代价。 - 和全局最优解对比更新
bestT、bestR、bestB。
- 每个
-
i 循环全部结束,把全局最优解写回输出参数
t、r、b。
输出
bestT有可能和输入t_coarse不一致,这是 i 循环带来的附带副作用,不是该函数的设计目标。
6. awbBayes 主入口
- zone 做 R、B 除以 G 归一化;
- interpolatePrior 得到插值后的 prior,做经验缩放;
- 调用
coarseSearch()得到t_coarse; - 调用
fineSearch()得到优化后的bestT bestR bestB; - 换算增益:
- 结果存入异步线程缓存。
三、上层后处理
- 主线程拷贝异步计算结果;
- IIR 一阶帧间平滑,抑制帧与帧之间增益跳变;
- gain 做硬件寄存器允许范围的 clamp;
- 输出给 ISP 硬件,上报 metadata 色温。
四、代码实现
cpp
#include <stdio.h>
#include <math.h>
// ====================== 基础工具、结构体 ======================
typedef struct
{
double x;
double y;
} Point2D;
/**
* 极简PWL分段折线模拟
* x数组:输入(色温K);y数组:输出;n:控制点数量
*/
typedef struct
{
double *x;
double *y;
int n;
} Pwl;
/** PWL求值,返回插值结果 */
double pwlEval(const Pwl *p, double t)
{
if (t <= p->x[0]) return p->y[0];
if (t >= p->x[p->n-1]) return p->y[p->n-1];
for(int i = 0; i < p->n-1; i++)
{
double x0 = p->x[i];
double x1 = p->x[i+1];
if(t >= x0 && t <= x1)
{
double alpha = (t - x0) / (x1 - x0);
return p->y[i] * (1.0 - alpha) + p->y[i+1] * alpha;
}
}
return 0.0;
}
/** PWL值域钳位 */
double pwlClamp(const Pwl *p, double t)
{
if(t < p->x[0]) return p->x[0];
if(t > p->x[p->n-1]) return p->x[p->n-1];
return t;
}
/** PWL整体乘以一个标量(原版prior *= scale) */
void pwlScale(Pwl *p, double scale)
{
for(int i = 0; i < p->n; i++)
p->y[i] *= scale;
}
/** 三点二次抛物线插值求极小点,对应interpolateQuadatric */
double interpolateQuadatric(Point2D a, Point2D b, Point2D c)
{
const double eps = 1e-3;
Point2D ca = {c.x - a.x, c.y - a.y};
Point2D ba = {b.x - a.x, b.y - a.y};
double denom = 2.0 * (ba.y * ca.x - ca.y * ba.x);
if(fabs(denom) < 1e-12)
return b.x;
double xmin = a.x - (ba.x*ba.x*(c.y-a.y) - ca.x*ca.x*(b.y-a.y)) / denom;
if(a.y < c.y - eps)
return a.x;
else if(c.y < a.y - eps)
return c.x;
return xmin;
}
/** 二维向量运算 */
Point2D pointAdd(Point2D a, Point2D b)
{
Point2D r = {a.x + b.x, a.y + b.y};
return r;
}
Point2D pointMul(Point2D a, double s)
{
Point2D r = {a.x * s, a.y * s};
return r;
}
double pointLen2(Point2D p)
{
return p.x*p.x + p.y*p.y;
}
double pointLen(Point2D p)
{
return sqrt(pointLen2(p));
}
// ===================== AWB数据结构 =====================
/** 每个统计zone,R/G B/G */
typedef struct
{
double R;
double B;
} AwbZone;
/** bayes配置,对应yaml */
typedef struct
{
double coarseStep;
double transversePos;
double transverseNeg;
double deltaLimit;
double whitepointR;
double whitepointB;
} AwbBayesConfig;
/**
* computeDelta2Sum:似然代价
* gainR/gainB: 候选增益(GainG=1)
* zones: 有效统计zone数组
* zoneCnt: 有效zone数量
* cfg: bayes配置
*/
double computeDelta2Sum(double gainR, double gainB,
const AwbZone *zones, int zoneCnt,
double wpR, double wpB,
const AwbBayesConfig *cfg)
{
double sum = 0.0;
for(int i = 0; i < zoneCnt; i++)
{
double dr = gainR * zones[i].R - 1.0 - wpR;
double db = gainB * zones[i].B - 1.0 - wpB;
double d2 = dr*dr + db*db;
if(d2 > cfg->deltaLimit)
d2 = cfg->deltaLimit;
sum += d2;
}
return sum;
}
/**
* coarseSearch 阶段1:粗搜索,全部候选落在CT‑Curve
* ctR: t → R/G; ctB: t → B/G
* prior: t → priorLogLikelihood(-logP(I))
* tLo tHi: 色温搜索上下限
* 返回:粗搜索最优色温 tCoarse
*/
double coarseSearch(const Pwl *ctR, const Pwl *ctB, const Pwl *prior,
double tLo, double tHi,
const AwbZone *zones, int zoneCnt,
const AwbBayesConfig *cfg)
{
Point2D points[256];
int pointCnt = 0;
int bestIdx = 0;
double t = tLo;
while(1)
{
double r = pwlEval(ctR, t);
double b = pwlEval(ctB, t);
double gainR = 1.0 / r;
double gainB = 1.0 / b;
double delta2Sum = computeDelta2Sum(gainR, gainB, zones, zoneCnt,
cfg->whitepointR, cfg->whitepointB, cfg);
double tClamp = pwlClamp(prior, t);
double priorVal = pwlEval(prior, tClamp);
double finalCost = delta2Sum - priorVal;
points[pointCnt].x = t;
points[pointCnt].y = finalCost;
if(points[pointCnt].y < points[bestIdx].y)
bestIdx = pointCnt;
pointCnt++;
if(fabs(t - tHi) < 1e-6)
break;
/* 自适应步长,原版 t += t/10 * coarseStep */
double step = t / 10.0 * cfg->coarseStep;
t += step;
if(t > tHi)
t = tHi;
}
double tBest = points[bestIdx].x;
if(pointCnt > 2 && bestIdx >= 1 && bestIdx < pointCnt-1)
{
Point2D a = points[bestIdx-1];
Point2D b = points[bestIdx];
Point2D c = points[bestIdx+1];
tBest = interpolateQuadatric(a,b,c);
}
return tBest;
}
/**
* fineSearch ------------【原版rpi‑controller核心】
* 输入:tIn = coarseSearch输出tCoarse;输入输出tOut rOut bOut
* 注意原版源码行为:
* 1、transverse向量仅使用tIn附近差分计算**一次,全程固定复用**;
* 2、i循环生成tTest是附带副作用;作者注释:fineSearch主要目的允许白点离开CT‑Curve,不是微调色温;
* 3、dist是向量缩放系数,**不计算点到曲线垂直几何距离,无K*d²惩罚;仅硬限制dist区间**;
* 4、若transverse向量退化,直接返回,不做横向偏移。
*/
void fineSearch(double tIn,
double *tOut, double *rOut, double *bOut,
const Pwl *ctR, const Pwl *ctB, const Pwl *prior,
const AwbZone *zones, int zoneCnt,
const AwbBayesConfig *cfg)
{
int spanR_dummy = -1;
int spanB_dummy = -1;
(void)spanR_dummy;
(void)spanB_dummy;
double step = tIn / 10.0 * cfg->coarseStep * 0.1;
int nsteps = 5;
/* 利用输入tIn,计算transverse向量,仅此一次,之后不再重算 */
double tPlus = tIn + nsteps * step;
double tMinus = tIn - nsteps * step;
double rP = pwlEval(ctR, tPlus);
double bP = pwlEval(ctB, tPlus);
double rM = pwlEval(ctR, tMinus);
double bM = pwlEval(ctB, tMinus);
double rDiff = rP - rM;
double bDiff = bP - bM;
Point2D transverse = {bDiff, -rDiff};
if(pointLen2(transverse) < 1e-6)
{
/* 向量退化,直接返回,不做横向求精 */
*tOut = tIn;
*rOut = pwlEval(ctR, tIn);
*bOut = pwlEval(ctB, tIn);
return;
}
/* 归一化为单位向量 */
double lenT = pointLen(transverse);
transverse.x /= lenT;
transverse.y /= lenT;
double bestCost = 1e20;
double bestT = tIn;
double bestR = pwlEval(ctR, tIn);
double bestB = pwlEval(ctB, tIn);
double transverseRange = cfg->transverseNeg + cfg->transversePos;
const int maxNumDeltas = 12;
int numDeltas = (int)(transverseRange * 100.0 + 0.5) + 1;
if(numDeltas < 3) numDeltas = 3;
if(numDeltas > maxNumDeltas) numDeltas = maxNumDeltas;
/* 源码:nsteps += numDeltas;横向采样越多,tTest扫描范围自动扩大(附带副作用) */
nsteps += numDeltas;
/* ======外层i循环:生成tTest;属于附带实现,不是设计主要目标 ====== */
for(int i = -nsteps; i <= nsteps; i++)
{
double tTest = tIn + i * step;
double tTestClamp = pwlClamp(prior, tTest);
double priorVal = pwlEval(prior, tTestClamp);
double rCurve = pwlEval(ctR, tTest);
double bCurve = pwlEval(ctB, tTest);
Point2D samp[12];
int bestJ = 0;
/* j内层循环:沿【固定transverse向量】做dist缩放采样 */
for(int j = 0; j < numDeltas; j++)
{
double dist = -cfg->transverseNeg + (transverseRange * j) / (numDeltas - 1);
Point2D curvePt = {rCurve, bCurve};
Point2D rbTest = pointAdd(curvePt, pointMul(transverse, dist));
double rTest = rbTest.x;
double bTest = rbTest.y;
double gainR = 1.0 / rTest;
double gainB = 1.0 / bTest;
double delta2Sum = computeDelta2Sum(gainR, gainB, zones, zoneCnt,
cfg->whitepointR, cfg->whitepointB, cfg);
double cost = delta2Sum - priorVal;
samp[j].x = dist;
samp[j].y = cost;
if(samp[j].y < samp[bestJ].y)
bestJ = j;
}
/* 边界保护,保证插值左右点存在 */
bestJ = (bestJ < 1) ? 1 : bestJ;
bestJ = (bestJ > numDeltas-2) ? (numDeltas-2) : bestJ;
/* 抛物线插值,求这条j采样线上最优dist */
double distOpt = interpolateQuadatric(samp[bestJ-1], samp[bestJ], samp[bestJ+1]);
/* 钳位dist,不能超出配置的hard‑limit */
if(distOpt < -cfg->transverseNeg) distOpt = -cfg->transverseNeg;
if(distOpt > cfg->transversePos) distOpt = cfg->transversePos;
Point2D curvePt = {rCurve, bCurve};
Point2D rbOpt = pointAdd(curvePt, pointMul(transverse, distOpt));
double rOpt = rbOpt.x;
double bOpt = rbOpt.y;
/* 【重要】插值点必须完整重算代价,不直接复用samp数组y值 */
double gainROpt = 1.0 / rOpt;
double gainBOpt = 1.0 / bOpt;
double deltaSumOpt = computeDelta2Sum(gainROpt, gainBOpt, zones, zoneCnt,
cfg->whitepointR, cfg->whitepointB, cfg);
double costOpt = deltaSumOpt - priorVal;
/* 更新全局最优 */
if(costOpt < bestCost)
{
bestCost = costOpt;
bestT = tTest;
bestR = rOpt;
bestB = bOpt;
}
}
*tOut = bestT;
*rOut = bestR;
*bOut = bestB;
}
/**
* awbBayes 主计算入口(剥离lux插值、bias统计预处理,只保留核心搜索)
* ctR ctB: CT‑Curve; priorScaled:已经lux插值+经验缩放后的prior PWL
* tLo tHi: 色温搜索区间
* zones zoneCnt:预处理完成的zone数组
* cfg: bayes配置
* out_gainR out_gainB out_t:输出结果
*/
void awbBayesCore(const Pwl *ctR, const Pwl *ctB, const Pwl *priorScaled,
double tLo, double tHi,
const AwbZone *zones, int zoneCnt,
const AwbBayesConfig *cfg,
double *out_gainR, double *out_gainB, double *out_t)
{
/* 阶段1:粗搜索 */
double tCoarse = coarseSearch(ctR, ctB, priorScaled, tLo, tHi, zones, zoneCnt, cfg);
/* 阶段2 fineSearch */
double tFine, rFine, bFine;
fineSearch(tCoarse, &tFine, &rFine, &bFine, ctR, ctB, priorScaled, zones, zoneCnt, cfg);
*out_gainR = 1.0 / rFine;
*out_gainB = 1.0 / bFine;
*out_t = tFine;
}
// ===================== 示例main调用演示 =====================
int main(void)
{
/* -------- 模拟CT‑Curve(普朗克轨迹,极简示例控制点) -------- */
double ctR_x[] = {2700.0, 3627.0, 5716.0, 8575.0};
double ctR_y[] = {0.90, 0.75, 0.58, 0.42};
double ctB_x[] = {2700.0, 3627.0, 5716.0, 8575.0};
double ctB_y[] = {0.40, 0.54, 0.70, 0.80};
Pwl ctR = {ctR_x, ctR_y, 4};
Pwl ctB = {ctB_x, ctB_y, 4};
/* 模拟prior先验PWL,示例 */
double priorX[] = {2700, 3627, 5716, 8575};
double priorY[] = {15.0, 5.0, 5.0, 20.0};
Pwl prior = {priorX, priorY, 4};
/* 原版经验缩放:prior *= (有效zone / total zone),这里示例:0.7 */
pwlScale(&prior, 0.7);
/* 模拟2个有效统计zone */
AwbZone zones[] = {
{0.59, 0.69},
{0.57, 0.71}
};
int zoneCnt = sizeof(zones)/sizeof(zones[0]);
AwbBayesConfig cfg = {
.coarseStep = 0.2,
.transversePos = 0.01,
.transverseNeg = 0.01,
.deltaLimit = 0.2,
.whitepointR = 0.0,
.whitepointB = 0.0
};
double gainR, gainB, tOut;
awbBayesCore(&ctR, &ctB, &prior,
2500.0, 10000.0,
zones, zoneCnt,
&cfg,
&gainR, &gainB, &tOut);
printf("==== awbBayesCore 输出结果 ====\n");
printf("estimated CCT(K): %.2f K\n", tOut);
printf("gainR=%.4f gainB=%.4f (gainG=1.0)\n", gainR, gainB);
return 0;
}
五、原版 rpi‑controller vs libipa 移植版关键差异
| 对比项 | rpi‑controller patch v5‑1 原版 | libipa 移植版 AwbBayes |
|---|---|---|
| fineSearch 设计目标 | 主要目标:允许白点跳出 CT‑Curve;色温变化是 i 循环附带副作用 | 删除 i 循环;t 完全固定等于 coarseSearch 输出,不再扫描色温 |
| transverse 向量 | 仅t_coarse位置计算一次,全程固定复用;其他 tTest 只做矢量相加 |
只使用粗搜点的局部法线,没有多组 tTest |
| dist/d | dist:向量缩放系数;不计算点‑曲线真实垂直距离;无 K・d² 惩罚 | d:真实几何垂直距离;代价函数增加 \(K\cdot d^2\)二次惩罚 |
| prior 经验缩放 | prior *= valid_zone / total_zone(源码注释存疑,经验调参) |
删除该 prior 缩放逻辑 |
| deltaLimit /bias 机制 | 全部保留 | 全部保留 |
提示:网络上很多资料描述的「固定 t、单条法线、\(K\cdot d^2\)距离惩罚」是libipa 移植版本 ,不能套用到这份原始 rpi‑controller 源码。
五、数值示例
场景条件
室内,lux=400lux;yaml 关键配置:
- coarseStep=0.2;
transversePos=0.01;transverseNeg=0.01;deltaLimit=0.2;biasProportion=0.05;biasCT=4500K; - auto 模式色温搜索区间:2500‑10000K;
- ISP 输出 24 个 AWB 网格 region;过滤后有效 zone=16,大于
minRegions。
- prepareStats :过滤无效 region;每个 region 叠加 biasCT 虚拟白点;做
sensitivityR/sensitivityB校正。 - interpolatePrior:lux=400,在两组 lux‑prior 之间插值;执行 prior 经验缩放。
- coarseSearch :2500‑10000K 自适应步长遍历 CT‑Curve;每个 t 计算似然 + 先验代价;三点插值输出粗搜索最优:
。
- fineSearch
- 以
t_coarse=3720K计算固定 transverse 单位向量; - i 循环生成一组
tTest(附带扫描,非设计目标); - 每个
tTest,j 循环沿固定 transverse 向量做 dist 采样,生成可跳出 CT‑Curve 的候选白点;j 方向插值求最优 dist,重算代价; - 在全部候选中选出全局最小代价;因 i 循环副作用输出
bestT=3660K,白点色度bestR=0.734,bestB=0.556,白点已经离开 CT‑Curve(本函数真正设计目标)。
- 以
- 增益换算:
,gainG=1.0。
- 主线程:IIR 帧间平滑、硬件限幅,输出 ISP 并上报 metadata 色温。
⚠️注意:
bestT=3660K偏离 3720K 属于附带副作用;原始作者设计意图:fineSearch 只用来实现白点偏离普朗克曲线,不是用来微调色温。