第三章:AE核心算法——从统计到策略的实战之路
自动曝光(AE)这东西,说白了就是让摄像头学会「看人下菜碟」。光线暗了提亮度,太亮了压曝光,最终目的就一个——让图像亮部不过曝、暗部有细节。我在做手机摄像头调试那几年,光是AE收敛抖动的坑就踩了不下十次。今天咱们就把AE的底裤扒干净,从统计信息提取到控制策略,再到收敛速度和稳定性,一条线讲透。
3.1 统计信息提取:AE的「眼睛」
AE要决策,首先得知道当前画面长什么样。这个「知道」的过程,就是统计信息提取。我个人习惯把这一步分成三个层次:全局亮度、分区统计、直方图分布。
3.1.1 亮度直方图
直方图不是新鲜概念,但AE里用的直方图和你在Photoshop里看到的不太一样。AE更关心的是有效动态范围——也就是像素值集中在哪一段。
// 一个简化的亮度直方图统计示例
#define HIST_BINS 256
uint32_t histogram[HIST_BINS] = {0};
void compute_luminance_histogram(uint8_t* y_plane, int width, int height) {
memset(histogram, 0, sizeof(histogram));
int total_pixels = width * height;
for (int i = 0; i < total_pixels; i++) {
uint8_t val = y_plane[i];
histogram[val]++;
}
}
嗯,这里要注意:直方图不能只看峰值。我曾经遇到过一个场景——室内灯光下拍白墙,直方图峰值在180附近,看着挺正常。但画面边缘有个窗户,实际已经过曝了。峰值骗了你,因为过曝区域占比小,在直方图上只是尾巴上一个小凸起。
3.1.2 分区统计
全局统计太粗糙,分区统计才是AE的杀手锏。把画面分成M×N个格子,每个格子独立算平均亮度。这样你就能知道——哦,画面左边暗右边亮,或者中间亮四周暗。
| 分区方式 | 典型网格 | 适用场景 | 我踩过的坑 |
|---|---|---|---|
| 均匀网格 | 5×5, 7×7 | 通用场景 | 网格太粗会漏掉小物体 |
| 中心加权 | 3×3 + 中心权重×2 | 人像、主体居中 | 主体偏移时容易翻车 |
| 人脸区域 | 动态ROI | 人脸检测联动 | 多人脸时权重分配要小心 |
分区统计的代码实现其实不复杂,但有个细节——边界处理。图像宽高不一定能被网格数整除,余数像素怎么处理?我一般直接丢弃,或者均匀分配到边缘格子。千万别做插值,那会引入额外的计算延迟。
3.2 AE控制策略:全局、局部与背光补偿
统计信息拿到手了,接下来就是怎么调曝光参数。AE控制策略分三大流派,各有各的脾气。
3.2.1 全局AE
全局AE最简单——整个画面算一个平均亮度,然后调曝光时间、增益、光圈。适合光照均匀的场景,比如阴天户外、室内均匀照明。
但全局AE有个致命弱点:容易被大面积背景带偏。举个例子,你拍一张逆光人像,背景是天空,全局平均亮度很高,AE就会拼命压曝光,结果人脸黑成一片。我在项目里见过这种翻车,用户直接投诉「这手机拍人像还不如五年前的老款」。
3.2.2 局部AE(区域加权)
局部AE就是给不同区域分配不同权重。比如中心区域权重0.6,四周各0.1。这样即使背景很亮,只要主体在中心,AE就会优先保证中心区域的曝光。
// 区域加权亮度计算
float zone_weights[5][5] = {
{0.02, 0.03, 0.05, 0.03, 0.02},
{0.03, 0.08, 0.12, 0.08, 0.03},
{0.05, 0.12, 0.20, 0.12, 0.05},
{0.03, 0.08, 0.12, 0.08, 0.03},
{0.02, 0.03, 0.05, 0.03, 0.02}
};
float compute_weighted_luminance(uint8_t** zone_avg, int rows, int cols) {
float weighted_sum = 0.0f;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
weighted_sum += zone_avg[r][c] * zone_weights[r][c];
}
}
return weighted_sum;
}
3.2.3 背光补偿
背光补偿是局部AE的进阶版。它专门处理逆光场景——背景亮、主体暗。策略是:检测到背光场景后,主动提高暗区权重,甚至直接以暗区为目标亮度。
怎么检测背光?我常用的方法是:计算中心区域与四周区域的亮度差。如果中心比四周暗超过30%,就判定为背光场景。这时候AE目标亮度要上调,比如从128调到160,同时限制最大曝光时间防止背景过曝太严重。
背光补偿的坑在于——误触发。有一次我在调试夜景模式,路灯下的雕塑被误判为背光,AE疯狂提亮,结果雕塑倒是亮了,背景天空噪点炸裂。后来我加了一个「全局亮度阈值」:只有全局平均亮度高于某个值(比如80)才启用背光补偿,夜景下直接禁用。
3.3 AE收敛速度与稳定性
AE不是一步到位的,它需要一个收敛过程。收敛太快容易震荡,收敛太慢用户觉得卡顿。这里有个经典的矛盾——速度 vs 稳定性。
3.3.1 收敛速度的影响因素
- 步长:每次调整曝光参数的幅度。步长越大,收敛越快,但容易过冲。
- 帧间隔:每几帧调整一次。1帧调整最快,但3帧调整更稳定。
- 传感器响应延迟:曝光参数改变后,传感器需要几帧才能输出稳定图像。这个延迟如果被忽略,AE就会「睁眼瞎」——以为没变化,继续调,结果调过头。
3.3.2 稳定性优化:死区与滞回
稳定性优化的核心就两个词:死区(Dead Zone)和滞回(Hysteresis)。
死区:当前亮度与目标亮度的差值小于某个阈值时,不调整参数。比如目标亮度128,当前亮度在125-131之间,AE就「装死」。这样可以避免微小波动导致的频繁调整。
滞回:进入和退出某个状态的阈值不同。比如背光补偿的触发阈值是30%亮度差,但退出阈值设为20%。这样画面在背光边缘来回切换时,AE不会反复横跳。
3.3.3 防震荡策略
AE震荡是最让人头疼的问题。画面忽明忽暗,像呼吸灯一样。我遇到过最严重的一次——在荧光灯下拍白墙,AE以2Hz的频率来回震荡,怎么调参数都没用。后来发现是荧光灯的100Hz频闪干扰了亮度统计。
解决方案:多帧平滑。统计信息不要只用当前帧,而是取最近N帧的加权平均。权重可以指数衰减——当前帧权重0.5,上一帧0.25,再上一帧0.125,以此类推。
// 指数移动平均(EMA)平滑亮度值
float smooth_luminance = 0.0f;
float alpha = 0.5f; // 当前帧权重
void update_smooth_luminance(float current_lum) {
smooth_luminance = alpha * current_lum + (1 - alpha) * smooth_luminance;
}
alpha值越大,响应越快但越容易震荡;alpha值越小,越稳定但响应迟钝。我一般取0.3-0.5,具体看场景。运动场景用0.5,静止场景用0.3。
3.4 实战中的AE调优清单
最后,我把自己这些年调AE的经验整理成一份清单。你调试的时候可以逐条对照。
| 检查项 | 典型问题 | 我的建议 |
|---|---|---|
| 直方图尾部占比 | 过曝/欠曝区域被忽略 | 统计亮度>240和<16的像素占比 |
| 分区权重归一化 | 权重和不等于1导致亮度偏移 | 每次修改权重后做归一化校验 |
| 背光补偿误触发 | 夜景下错误提亮 | 加全局亮度阈值,暗光禁用 |
| 收敛震荡 | 画面忽明忽暗 | 加EMA平滑,alpha取0.3-0.5 |
| 传感器延迟 | 参数调整后统计滞后 | 至少等待2帧再读取新统计 |
| 死区设置 | 微小波动导致频繁调整 | 死区取目标亮度的3%-5% |
AE这东西,理论说破天也就这些。但真正调好,靠的是对场景的理解和大量的实拍验证。别指望一套参数打天下——室内、室外、夜景、逆光,每个场景都得单独调。嗯,今天就到这儿,你先把统计信息提取的代码跑通,咱们下次再聊更细的调优技巧。