061、自动白平衡AWB算法详解灰度世界、完美反射与色温估计的融合一、一个让我失眠三天的AWB问题2018年某款旗舰手机在印度市场上市后用户投诉如潮——室内暖光灯下拍出的照片偏蓝像蒙了一层冰。我带着团队在班加罗尔蹲了整整一周白天测室外晚上泡在商场里。最后发现问题出在AWB模块对混合光源的处理上商场天花板的荧光灯色温约4000K和货架上的钨丝灯约2800K同时存在灰度世界算法直接崩溃把整个画面往冷色调拉。这个案例让我意识到教科书上那些漂亮的AWB公式在实际场景中往往不堪一击。今天这篇笔记我就把当年踩过的坑、补过的课以及最终沉淀下来的融合策略原原本本写出来。二、灰度世界最朴素的假设最脆弱的防线灰度世界算法的核心假设很简单一张色彩丰富的照片所有颜色的平均值应该是灰色。用数学表达就是R、G、B三个通道的均值应该相等。// 灰度世界实现——别在生产环境直接这么写后面会告诉你为什么voidawb_gray_world(uint16_t*r_gain,uint16_t*g_gain,uint16_t*b_gain,uint32_t*raw_buffer,intwidth,intheight){uint64_tsum_r0,sum_g0,sum_b0;inttotal_pixelswidth*height;// 这里踩过坑直接遍历全图遇到大面积纯色场景会死得很惨for(inti0;itotal_pixels;i){sum_rraw_buffer[i*30];sum_graw_buffer[i*31];sum_braw_buffer[i*32];}floatavg_r(float)sum_r/total_pixels;floatavg_g(float)sum_g/total_pixels;floatavg_b(float)sum_b/total_pixels;// 以G通道为基准计算R和B的增益*r_gain(uint16_t)(avg_g/avg_r*256);// 别这样写整数除法精度不够*g_gain256;// G通道通常不做增益或者只做微调*b_gain(uint16_t)(avg_g/avg_b*256);}这段代码看起来逻辑清晰但实际调试中你会发现当画面被大面积绿色草坪占据时整张照片会偏紫当画面是蓝色天空时照片会偏黄。灰度世界假设在自然界中几乎不成立——天空、草地、海洋这些大面积单色场景恰恰是用户最常拍摄的。我在某安防项目中就吃过这个亏监控摄像头对着停车场画面里全是灰色水泥地灰度世界算法反而把灰色调成了偏绿。后来加了饱和度检测只有画面色彩丰富度超过阈值才启用灰度世界。三、完美反射寻找画面中最亮的那个点完美反射算法的思路和灰度世界完全相反它假设画面中最亮的区域是纯白色通过这个参考点来校正其他颜色。// 完美反射实现——注意看那个阈值怎么设这里全是坑voidawb_perfect_reflector(uint16_t*r_gain,uint16_t*g_gain,uint16_t*b_gain,uint32_t*raw_buffer,intwidth,intheight){// 先找到亮度最高的像素uint32_tmax_luma0;intmax_x0,max_y0;for(inty0;yheight;y){for(intx0;xwidth;x){uint32_tidx(y*widthx)*3;// 这里用简单亮度公式实际应该用YUV转换uint32_tluma(raw_buffer[idx]raw_buffer[idx1]raw_buffer[idx2])/3;if(lumamax_luma){max_lumaluma;max_xx;max_yy;}}}// 取最亮点的RGB值作为参考白点uint32_tref_rraw_buffer[(max_y*widthmax_x)*30];uint32_tref_graw_buffer[(max_y*widthmax_x)*31];uint32_tref_braw_buffer[(max_y*widthmax_x)*32];// 计算增益——这里踩过坑如果最亮点是过曝的参考值就废了*r_gain(uint16_t)((float)ref_g/ref_r*256);*g_gain256;*b_gain(uint16_t)((float)ref_g/ref_b*256);}这个算法的致命问题是如果画面中最亮的点不是白色比如红色车尾灯整个白平衡就崩了。我在车载项目中遇到过夜间行车前车刹车灯亮起完美反射算法把整个画面往青色拉因为把红色尾灯当成了参考白点。后来我们加了一个筛选条件只有亮度在95%分位以上、且色度在某个范围内的像素才参与计算。这个阈值调了两个月不同场景下表现差异巨大。四、色温估计从物理模型到工程妥协色温估计是更物理的方法。黑体辐射理论告诉我们不同温度的黑体会发出不同颜色的光。通过分析图像中R/G和B/G的比值可以反推出光源色温。// 色温估计——这个查表法是我从某日本传感器厂商的参考设计里扒出来的// 注意不同sensor的光谱响应不同这个表不能通用typedefstruct{uint16_tcolor_temp;// 色温单位Kfloatratio_rg;// R/G比值floatratio_bg;// B/G比值}ColorTempTable;// 这里踩过坑查表法在色温跳变时会有抖动需要加平滑staticColorTempTable ct_table[]{{2500,1.85,0.45},// 钨丝灯{3200,1.65,0.55},// 卤素灯{4000,1.45,0.65},// 荧光灯{5000,1.25,0.80},// 日光灯{6500,1.00,1.00},// 日光D65标准{8000,0.85,1.20},// 阴天{10000,0.70,1.45},// 蓝天阴影};voidawb_color_temp_estimate(uint16_t*r_gain,uint16_t*g_gain,uint16_t*b_gain,floatavg_rg,floatavg_bg){// 计算当前画面的R/G和B/G比值floatcurrent_rgavg_rg;floatcurrent_bgavg_bg;// 在表中查找最近的色温点floatmin_dist999.0f;intbest_idx0;for(inti0;isizeof(ct_table)/sizeof(ct_table[0]);i){floatdistsqrt(pow(current_rg-ct_table[i].ratio_rg,2)pow(current_bg-ct_table[i].ratio_bg,2));if(distmin_dist){min_distdist;best_idxi;}}// 根据查到的色温计算增益——别这样写插值比查表更平滑// 这里应该用双线性插值但为了演示先简化*r_gain(uint16_t)(1.0f/ct_table[best_idx].ratio_rg*256);*g_gain256;*b_gain(uint16_t)(1.0f/ct_table[best_idx].ratio_bg*256);}色温估计的精度取决于两个因素一是查表数据的准确性二是sensor光谱响应的一致性。同一个色温下不同sensor拍出来的R/G比值可能差20%。所以这个表必须针对具体sensor标定不能通用。我在某手机项目中吃过这个亏直接用了参考设计里的色温表结果在室内LED灯下偏色严重。后来发现LED灯的光谱不是连续的黑体辐射有多个峰值色温估计完全不准。最后不得不针对LED灯单独建了一个表。五、融合策略三个臭皮匠顶个诸葛亮单一算法都有致命缺陷但把它们融合起来就能互相弥补。我在实际项目中用的融合策略是这样的// 融合AWB算法——这个版本经过了三个项目的验证但每个项目都要调权重typedefstruct{floatgray_world_weight;// 灰度世界权重floatperfect_ref_weight;// 完美反射权重floatcolor_temp_weight;// 色温估计权重floatconfidence;// 整体置信度}AWBWeights;voidawb_fusion(uint16_t*r_gain,uint16_t*g_gain,uint16_t*b_gain,uint32_t*raw_buffer,intwidth,intheight){uint16_tr_gw,g_gw,b_gw;// 灰度世界结果uint16_tr_pr,g_pr,b_pr;// 完美反射结果uint16_tr_ct,g_ct,b_ct;// 色温估计结果// 分别计算三个算法的增益awb_gray_world(r_gw,g_gw,b_gw,raw_buffer,width,height);awb_perfect_reflector(r_pr,g_pr,b_pr,raw_buffer,width,height);awb_color_temp_estimate(r_ct,g_ct,b_ct,calculate_avg_rg(raw_buffer,width,height),calculate_avg_bg(raw_buffer,width,height));// 计算每个算法的置信度——这里踩过坑置信度计算比算法本身还复杂floatconf_gwcalculate_gray_world_confidence(raw_buffer,width,height);floatconf_prcalculate_perfect_reflector_confidence(raw_buffer,width,height);floatconf_ctcalculate_color_temp_confidence(raw_buffer,width,height);// 归一化权重floattotal_confconf_gwconf_prconf_ct;floatw_gwconf_gw/total_conf;floatw_prconf_pr/total_conf;floatw_ctconf_ct/total_conf;// 加权融合——别这样写直接加权平均会有过冲// 应该先对增益做对数域融合再转回线性域*r_gain(uint16_t)(r_gw*w_gwr_pr*w_prr_ct*w_ct);*g_gain256;*b_gain(uint16_t)(b_gw*w_gwb_pr*w_prb_ct*w_ct);}这个融合策略的关键在于置信度计算。灰度世界的置信度取决于画面色彩丰富度——如果画面中颜色种类多灰度世界就可靠如果画面是单色置信度就要降低。完美反射的置信度取决于最亮区域的色度——如果最亮区域接近白色置信度就高如果偏色严重置信度就低。色温估计的置信度取决于画面中是否有中性色区域——如果有灰色或白色物体色温估计就准。我在某车载项目中把这三个置信度做成一个三维查表根据场景自动切换。比如在隧道里单一色温、低照度色温估计权重最高在户外风景色彩丰富灰度世界权重最高在夜间城市有大量灯光完美反射权重最高。六、实战中的那些坑统计区域的选择不要用全图统计。我在手机项目中把画面分成9宫格只取中间5个格子排除边缘的暗角和过曝区域。这个策略在80%的场景下有效但遇到逆光人像时人脸在边缘反而被排除掉了。后来加了人脸检测检测到人脸时人脸区域权重提高10倍。时间平滑AWB增益不能突变。我在某安防项目中摄像头在室内外切换时画面会突然变色持续好几帧。后来加了IIR滤波器时间常数设为0.8即当前帧占80%历史帧占20%切换过程变得平滑。但注意这个时间常数不能太大否则在快速移动场景下会有拖影。增益限制R和B的增益不能无限大。我在某手机项目中在极低照度下1 luxR增益被算到8倍导致红色通道噪声爆炸。后来加了增益上限R和B增益最大不超过4倍。同时当增益超过2倍时自动降低图像亮度避免噪声放大。混合光源处理这是AWB最难的问题。我在某商场项目中天花板是荧光灯4000K柜台是射灯3000K画面中同时存在两种色温。单一算法无法处理最后用了区域AWB把画面分成多个区域每个区域独立计算色温然后根据区域位置做渐变过渡。这个方案计算量大了3倍但效果确实好。七、个人经验性建议做了十几年AWB调试我最大的感悟是没有万能的算法只有合适的场景。灰度世界、完美反射、色温估计这三个算法就像三个工具每个都有适用场景。关键是要知道什么时候用哪个以及怎么组合。如果你刚开始做AWB我建议从灰度世界入手因为它最简单、最容易理解。然后逐步加入完美反射和色温估计。不要一开始就搞复杂的融合策略先把单一算法调好再考虑融合。另外调试AWB最好的工具不是示波器而是你的眼睛。我每次调试AWB都会准备一组标准色卡Macbeth色卡、X-Rite色卡在不同光源下拍摄然后对比色卡的还原度。这个习惯保持了十年比任何自动测试工具都可靠。最后如果你在项目中遇到AWB问题不要急着改算法先搞清楚场景。是混合光源是单色场景是低照度还是sensor本身的光谱响应有问题把问题定位清楚解决方案自然就有了。下一篇文章我会讲AWB和AE的联动——这两个模块在实战中经常打架怎么让它们和谐共处是个有意思的话题。