树莓派相机自动白平衡详解(一)
树莓派相机自动白平衡详解(二)
树莓派相机自动白平衡详解(三)
以上说明的树莓派自动白平衡的一些基本思路。但是之前没有找到源码,因此有些理解还是存在一些误差。现在根据源码修正并详细说明。
目录
一、整体架构与调用流
二、核心函数逐条解析
三、上层后处理
四、代码实现
五、原版 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; - 换算增益:
6. 结果存入异步线程缓存。
三、上层后处理
- 主线程拷贝异步计算结果;
- IIR 一阶帧间平滑,抑制帧与帧之间增益跳变;
- gain 做硬件寄存器允许范围的 clamp;
- 输出给 ISP 硬件,上报 metadata 色温。
四、代码实现
#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 只用来实现白点偏离普朗克曲线,不是用来微调色温。