第三章: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附近,看着挺正常。但画面边缘有个窗户,实际已经过曝了。峰值骗了你,因为过曝区域占比小,在直方图上只是尾巴上一个小凸起。

实战技巧:AE直方图统计时,建议同时关注尾部占比。比如统计亮度值大于240的像素占比,超过5%就说明有过曝风险。这个阈值我一般取3%-8%,具体看传感器特性。

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;
}
注意:权重矩阵的归一化一定要做!我见过有人直接拿整数权重累加,结果亮度值直接溢出。权重和必须等于1.0,否则AE会朝错误方向收敛。

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%-5%。128的目标亮度,死区设为±4到±6比较合适。滞回量取触发阈值的30%-50%。触发阈值30%,滞回就设10%-15%。

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。

AE控制策略与收敛流程 传感器原始数据 统计信息提取 亮度直方图 分区统计 背光检测 AE控制策略选择 收敛速度与稳定性控制 核心闭环:统计 → 策略 → 收敛 → 反馈到传感器 → 下一帧统计

3.4 实战中的AE调优清单

最后,我把自己这些年调AE的经验整理成一份清单。你调试的时候可以逐条对照。

检查项 典型问题 我的建议
直方图尾部占比 过曝/欠曝区域被忽略 统计亮度>240和<16的像素占比
分区权重归一化 权重和不等于1导致亮度偏移 每次修改权重后做归一化校验
背光补偿误触发 夜景下错误提亮 加全局亮度阈值,暗光禁用
收敛震荡 画面忽明忽暗 加EMA平滑,alpha取0.3-0.5
传感器延迟 参数调整后统计滞后 至少等待2帧再读取新统计
死区设置 微小波动导致频繁调整 死区取目标亮度的3%-5%
一个小技巧:调试AE时,把亮度统计值和目标值打印到log里,用Excel画成曲线。一眼就能看出收敛过程是平滑还是震荡。我每次调新平台的AE,第一件事就是搭这个可视化工具。

AE这东西,理论说破天也就这些。但真正调好,靠的是对场景的理解和大量的实拍验证。别指望一套参数打天下——室内、室外、夜景、逆光,每个场景都得单独调。嗯,今天就到这儿,你先把统计信息提取的代码跑通,咱们下次再聊更细的调优技巧。