3. AE算法架构:从统计信息到曝光控制

AE算法,说白了就是一套「看天吃饭」的决策系统。摄像头看到什么场景,算法就根据场景亮度、目标亮度、用户偏好,算出一组曝光参数——快门、增益、光圈。我做了这么多年AE,最大的感受是:架构搭不好,后面调参全是补窟窿。

这一章,我们就把AE Pipeline从头到尾拆一遍。你跟着我走一遍流程,就知道每一块在干什么、为什么这么干。

3.1 AE Pipeline整体流程

先看整体。AE Pipeline通常跑在ISP的帧同步信号里,每来一帧,就跑一轮。我习惯把它分成5个阶段:

  1. 统计信息采集 —— 从RAW图里捞亮度数据
  2. 亮度计算 —— 把统计值换算成场景亮度
  3. 曝光策略 —— 根据场景类型选目标亮度和收敛方向
  4. 增益与快门控制 —— 算出具体的曝光参数
  5. 参数下发 —— 写入sensor寄存器,等下一帧生效

下面这张图,就是AE Pipeline的完整数据流。我建议你把它存下来,后面每一节都会对应到图上的某个模块。

AE Pipeline 整体数据流 RAW图输入 统计信息采集 亮度计算 曝光策略 增益与快门控制 参数下发 下一帧RAW图 关键说明: • 每帧触发一次AE计算,与VSYNC同步 • 统计信息采集在ISP RAW域完成 • 参数下发后,下一帧才生效

3.2 统计信息采集

统计信息是AE的「眼睛」。眼睛瞎了,后面算得再准也没用。

采集通常在ISP的RAW域做,也就是在去马赛克、白平衡之前。为什么?因为RAW数据最原始,没被任何处理「污染」过。我见过有人拿YUV域的亮度做AE,结果白平衡一变,亮度也跟着跳,收敛抖得不行。

常用的统计信息包括:

统计项 含义 典型用途
亮度直方图 256 bins,统计每个亮度级的像素数 判断过曝/欠曝、场景动态范围
分区平均亮度 将画面分成M×N块,每块算平均亮度 权重计算、背光检测
最大/最小亮度 全图最亮和最暗的像素值 防饱和、防黑切
亮度统计矩 均值、方差、偏度 场景分类(顺光/逆光/夜景)
我的习惯:分区数不要太多。16×12就够用了。分区太多,统计噪声大,反而影响收敛稳定性。我曾经试过32×24,结果每帧都在抖,最后发现是分区太小,每个区只有几十个像素,统计值方差太大。

3.3 亮度计算

有了统计信息,下一步就是算场景亮度。这里有个坑:统计值通常是0-1023的RAW域值,不是物理亮度。你需要一个映射关系。

我常用的亮度计算公式:

// 加权平均亮度
float calcSceneLuminance(uint16_t *hist, int bins, float *weights) {
    float total = 0.0f;
    float sum = 0.0f;
    for (int i = 0; i < bins; i++) {
        sum += hist[i] * i * weights[i];
        total += hist[i];
    }
    return (total > 0) ? (sum / total) : 0.0f;
}

权重怎么设?这取决于你的目标。如果希望人脸亮,就把人脸区域的权重设高。如果希望整体均匀,就用平均权重。

注意:权重不能突变。我曾经在背光场景下,权重从0.5跳到2.0,结果亮度直接过曝了两帧才拉回来。权重变化要平滑,最好做一阶低通滤波。

3.4 曝光策略

曝光策略是AE的「大脑」。它决定了两件事:

  • 目标亮度 —— 当前场景应该多亮
  • 收敛方向 —— 应该增亮还是减暗

策略通常分几种:

策略类型 适用场景 目标亮度设定
全局平均策略 均匀光照、风景 固定值(如18%灰)
人脸优先策略 人像、自拍 人脸区域亮度优先
背光补偿策略 逆光、窗边 主体亮度提升,背景可过曝
夜景策略 低照度 降低目标亮度,允许更多噪声

策略切换要小心。我遇到过一个问题:从室内走到室外,策略从「夜景」切到「全局平均」,目标亮度突然跳了30%,画面闪了一下。后来加了策略切换的渐变过渡,才解决。

3.5 增益与快门控制

这是AE的「手脚」。策略决定了「要多少亮度」,增益和快门负责「怎么达到这个亮度」。

控制逻辑的核心公式:

// 曝光参数计算
float targetLuminance = 120.0f;  // 目标亮度
float currentLuminance = 80.0f;  // 当前亮度
float exposureRatio = targetLuminance / currentLuminance;  // 1.5

// 先调快门(优先保证帧率)
float newShutter = currentShutter * exposureRatio;
if (newShutter > maxShutter) {
    newShutter = maxShutter;
    // 剩余部分用增益补
    float remainingRatio = exposureRatio / (newShutter / currentShutter);
    float newGain = currentGain * remainingRatio;
}

为什么先调快门?因为快门影响运动模糊和帧率,增益影响噪声。我的优先级是:

  1. 先调快门 —— 在帧率允许范围内优先用快门
  2. 再调增益 —— 快门到上限后,用增益补
  3. 最后调光圈 —— 如果有光圈,最后动它
避坑指南:我曾经在低照度场景下,快门直接拉到1/30s,增益也推到6x,结果画面虽然亮了,但运动物体拖影严重,噪声也大。后来加了「快门-增益联合优化」:先评估场景是否有运动,如果有,快门不能低于1/60s,增益可以适当提高。

3.6 参数下发与帧同步

参数算好了,怎么给sensor?这里有个时序问题。

Sensor通常通过I2C/SPI写寄存器。写寄存器不是立即生效的,要等下一个VSYNC信号。所以:

  • 第N帧采集统计信息
  • 第N帧的VBLANK期间计算曝光参数
  • 第N+1帧开始时写入sensor
  • 第N+2帧才用新参数曝光

也就是说,从采集到生效,至少延迟2帧。如果帧率是30fps,就是66ms的延迟。这个延迟在快速变化场景下会出问题。

我的做法:加一个「预测补偿」。根据最近几帧的亮度变化趋势,预测下一帧的亮度,提前调整参数。虽然不能完全消除延迟,但能把过冲减少一半以上。

3.7 小结

AE Pipeline说复杂也复杂,说简单也简单。核心就是:

  • 统计信息采集 —— 把RAW图变成数字
  • 亮度计算 —— 把数字变成场景亮度
  • 曝光策略 —— 决定要多少亮度
  • 增益与快门控制 —— 决定怎么达到这个亮度
  • 参数下发 —— 让sensor执行