3. AE核心算法:直方图统计与亮度评估、目标亮度设定、AE收敛策略
自动曝光(AE)是ISP 3A算法里最贴近人眼感受的一环。说白了,就是让摄像头在不同光照下都能拍出亮度合适的画面。我做了这么多年ISP,发现很多工程师把AE想得太简单——以为就是算个平均亮度然后调增益。其实,真正的AE算法要解决三个核心问题:怎么评估当前亮度、目标亮度定多少、怎么又快又稳地达到目标。
这一章,我们就把这三点拆开来讲。
3.1 直方图统计与亮度评估
你想想看,一张图像亮不亮,最直观的方法是什么?很多人第一反应是算平均亮度。但我在项目中踩过坑——单纯的平均亮度很容易被误导。比如一张夜景照片,大部分区域是暗的,但路灯区域很亮,平均下来可能觉得画面还行,实际上暗部细节全丢了。
所以,专业的AE评估必须依赖直方图统计。
3.1.1 直方图怎么算
直方图就是把图像的亮度值从0到255分成若干个bin,统计每个bin里有多少像素。我习惯用256个bin,也就是每个灰度级一个bin,这样精度最高。
// 伪代码:直方图统计
uint32_t hist[256] = {0};
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
uint8_t luminance = get_luminance(frame, x, y);
hist[luminance]++;
}
}
嗯,这里要注意——不要直接用RGB的某个通道。我见过有人直接用G通道当亮度,结果在纯色场景下偏差很大。正确的做法是用亮度公式:Y = 0.299*R + 0.587*G + 0.114*B。
3.1.2 从直方图提取亮度特征
有了直方图,我们能提取出比平均值更有价值的特征:
| 特征名称 | 计算方法 | 物理含义 |
|---|---|---|
| 平均亮度 | ∑(i * hist[i]) / total_pixels | 整体亮度水平 |
| 中值亮度 | 累计像素达到50%时的灰度值 | 抗噪点能力强,比平均更鲁棒 |
| 暗区比例 | ∑(hist[0..31]) / total_pixels | 画面欠曝程度 |
| 亮区比例 | ∑(hist[224..255]) / total_pixels | 画面过曝程度 |
| 直方图宽度 | max(i|hist[i]>0) - min(i|hist[i]>0) | 画面动态范围 |
我个人习惯用中值亮度作为主评估指标。为什么?因为平均亮度对极端值太敏感。有一次我在调试室内场景,画面里有一盏很亮的台灯,平均亮度被拉高了很多,导致AE把整体压暗,人脸反而黑了。换成中值亮度后,这个问题就解决了。
3.1.3 分区加权评估
直方图统计还有一个进阶用法——分区加权。你想想看,拍照时最重要的区域通常是画面中央或者人脸。所以我们会把图像分成多个区域,给不同区域不同的权重。
典型分区权重配置(5x5网格):
中央3x3区域权重 = 2.0
边缘区域权重 = 0.5
最外围权重 = 0.2
这样计算出来的加权亮度,更符合人眼关注的重点。
3.2 目标亮度设定
评估出当前亮度后,下一步就是确定——我们要把画面调到多亮?这个目标亮度不是随便定的,它直接影响最终画质。
3.2.1 固定目标亮度的局限
很多入门级的方案直接把目标亮度定在128(8bit图像的中值)。但我在实际项目中吃过亏——白天室外场景,目标亮度128拍出来很舒服;到了夜晚,同样128拍出来的画面噪点满天飞。
为什么会这样?因为人眼的亮度感知是对数关系的。同样的128亮度值,在白天和夜晚给人的感受完全不同。
3.2.2 自适应目标亮度策略
我建议的做法是:根据环境照度动态调整目标亮度。
| 环境照度 | 目标亮度范围 | 策略说明 |
|---|---|---|
| > 1000 lux(晴天室外) | 120 ~ 140 | 保留高光细节,避免过曝 |
| 100 ~ 1000 lux(室内) | 100 ~ 120 | 平衡亮暗,保证主体清晰 |
| 10 ~ 100 lux(暗光) | 80 ~ 100 | 适当降低目标,抑制噪声 |
| < 10 lux(极暗) | 60 ~ 80 | 优先保证可见度,接受一定噪声 |
我的经验:在暗光场景下,目标亮度每降低10,信噪比能提升约3dB。所以别死磕128,该降就降。
3.2.3 场景识别的辅助作用
更智能的做法是结合场景识别。比如检测到人脸时,把目标亮度设定在人脸区域;检测到背光时,适当提高目标亮度来补偿。
我曾经调试过一个安防摄像头项目,白天室外场景总是过曝。后来发现是目标亮度设得太高,加上传感器动态范围不够。最后我们把目标亮度从128降到110,同时启用了分区加权,效果立竿见影。
3.3 AE收敛策略:步进与PID控制
评估出当前亮度,设定了目标亮度,接下来就是怎么调过去。这一步做不好,画面就会忽明忽暗,或者来回震荡。
3.3.1 步进控制(Step Control)
步进控制是最简单的方法——每次调整一个固定步长,直到达到目标。
// 步进控制伪代码
int current_luma = get_current_luminance();
int target_luma = get_target_luminance();
int step = 8; // 每次调整8个灰度级
if (current_luma < target_luma - step) {
increase_exposure(step);
} else if (current_luma > target_luma + step) {
decrease_exposure(step);
} else {
// 在目标范围内,保持不动
hold_exposure();
}
步进控制的优点是简单、稳定,不会震荡。但缺点也很明显——收敛速度慢。如果场景从室内走到室外,亮度差了几十个灰度级,步进控制可能要好几秒才能跟上。
注意:步长不能设得太大。我曾经试过步长设为32,结果画面在亮暗之间来回跳,用户体验极差。步长一般建议在4~16之间,根据帧率动态调整。
3.3.2 PID控制
PID控制是工业界的经典方案,用在AE上效果也很好。它有三个分量:
- P(比例):根据当前误差大小调整,误差大就调得多
- I(积分):累积历史误差,消除稳态偏差
- D(微分):根据误差变化趋势调整,抑制震荡
// PID控制伪代码
static float integral = 0;
static float prev_error = 0;
float error = target_luma - current_luma;
integral += error * dt;
float derivative = (error - prev_error) / dt;
float output = Kp * error + Ki * integral + Kd * derivative;
prev_error = error;
// output 映射到曝光参数调整量
adjust_exposure(output);
PID控制的优势是收敛快、精度高。但参数调起来很头疼——Kp、Ki、Kd三个系数,调不好就会震荡。
3.3.3 我的实战调参经验
我调试PID参数时有个习惯:先调P,再加I,最后加D。
- 先调P:把Ki和Kd设为0,只调Kp。让画面能快速接近目标,但允许有稳态误差。
- 再加I:加入积分项,消除稳态误差。注意I不能太大,否则会过冲。
- 最后加D:如果画面有震荡,加入微分项抑制。D太大会引入噪声。
典型参数范围(参考):
Kp: 0.5 ~ 2.0
Ki: 0.01 ~ 0.1
Kd: 0.1 ~ 0.5
具体值取决于传感器特性和帧率,需要实际调试。
3.3.4 步进与PID的混合策略
在实际项目中,我更喜欢把两者结合起来:
- 大误差时用步进:当误差大于20个灰度级时,用大步进快速逼近
- 小误差时切PID:当误差小于20个灰度级时,切换到PID精细调节
这样做的好处是:既保证了快速响应,又保证了稳态精度。我曾经在一个手机项目上用了这个方案,AE收敛时间从原来的2秒缩短到0.5秒,而且画面几乎没有闪烁。
3.4 本章知识体系
下面这张图总结了AE核心算法的整体流程和关键节点:
3.5 避坑指南
最后,分享几个我踩过的坑:
我曾经犯过的错误:
- 直方图bin数太少:用32个bin做统计,结果亮度评估精度不够,画面总是偏暗或偏亮。后来改成256个bin,问题解决。
- PID积分饱和:场景切换时,积分项累积了很大的值,导致画面过冲严重。加了积分限幅和抗饱和机制后才稳定。
- 步进控制死区太小:死区设成2个灰度级,结果画面在目标附近来回抖动。把死区扩大到5个灰度级后,画面就稳定了。