3. AE核心算法:统计信息提取与曝光控制
自动曝光(AE)是ISP 3A算法里最基础、也最考验工程经验的一环。说白了,就是让摄像头在不同光照下都能输出亮度合适的图像。你想想看,如果曝光不准,后面再好的降噪、再强的HDR都白搭。
我个人习惯把AE算法拆成两大部分:统计信息的提取和曝光量的闭环控制。前者告诉你当前画面长什么样,后者决定下一步该怎么调。今天我们就把这套流程掰开揉碎了讲。
3.1 统计信息提取:看懂画面
AE的第一步,是从RAW域或YUV域的图像数据里提取统计信息。硬件ISP模块通常会直接输出这些统计值,我们只需要配置好窗口和权重就行。
3.1.1 亮度直方图
亮度直方图是最直观的工具。它统计了每个亮度级别的像素个数。我一般会看三个关键区域:
- 暗部(0~64):如果像素堆积在这里,画面欠曝。
- 中间调(64~192):主体细节所在,希望尽量平坦。
- 亮部(192~255):如果出现尖峰,说明过曝了。
实战经验:我在项目中遇到过一种情况——直方图看起来分布很均匀,但画面实际很暗。后来发现是传感器输出的是线性数据,而直方图统计的是Gamma校正后的数据。所以,一定要确认统计模块工作在哪个域。
直方图还能用来做场景分类。比如,低对比度场景的直方图集中在中间窄带,高对比度场景则两端都有分布。这个信息对后续的曝光策略选择很有帮助。
3.1.2 分区统计
全局直方图有个问题——它不知道亮暗区域在画面中的位置。比如逆光人像,人脸暗、背景亮,全局直方图可能显示正常,但人脸已经黑成一片了。
分区统计就是解决这个问题的。我们把画面分成M×N个格子(常见的是15×15或13×17),每个格子独立计算平均亮度和直方图。
| 分区数 | 优点 | 缺点 |
|---|---|---|
| 4×4 | 计算量小,适合低端平台 | 空间分辨率低,容易漏掉小目标 |
| 15×15 | 主流选择,平衡精度和性能 | 需要一定的DMA带宽 |
| 32×32 | 精细控制,适合人脸跟踪 | 存储和计算开销大 |
每个分区还可以配置权重。比如,画面中央区域权重设为2,边缘设为0.5。这样AE会优先保证中央区域的曝光准确。
避坑指南:我曾经在调试一个安防摄像头时,发现AE总是对天空过曝。后来检查发现,分区权重没有排除天空区域。我的做法是:根据场景语义(比如天空检测)动态调整分区权重,效果立竿见影。
3.2 曝光表设计与查表法
有了统计信息,下一步就是确定目标亮度。曝光表(Exposure Table)就是干这个的。
3.2.1 曝光表的结构
曝光表本质上是一个查找表(LUT),输入是场景亮度估计值,输出是曝光参数组合(曝光时间、模拟增益、数字增益)。
// 曝光表示例(简化版)
typedef struct {
uint16_t scene_lux; // 场景照度估计值
uint16_t exposure_time; // 曝光时间(us)
uint16_t analog_gain; // 模拟增益(Q8格式)
uint16_t digital_gain; // 数字增益(Q8格式)
} exposure_entry_t;
exposure_entry_t exp_table[] = {
{10, 30000, 256, 256}, // 暗光:长曝光+高增益
{50, 20000, 128, 256}, // 室内:中等曝光
{200, 10000, 64, 256}, // 室外阴天
{1000, 5000, 32, 256}, // 晴天
{5000, 1000, 16, 256}, // 强光
};
曝光表的设计有几个关键点:
- 覆盖范围:从0.1 lux到10000 lux都要有对应的条目。
- 平滑过渡:相邻条目之间的参数变化不能太大,否则画面会闪烁。
- 优先级约束:先调曝光时间,再调模拟增益,最后调数字增益。因为数字增益会放大噪声。
3.2.2 查表与插值
实际场景的照度不会刚好落在表里的某个点上。所以我们需要做线性插值。
// 查表插值函数
void lookup_exposure(uint16_t target_lux, exposure_entry_t *out) {
// 找到相邻的两个表项
int idx_low = 0, idx_high = TABLE_SIZE - 1;
for (int i = 0; i < TABLE_SIZE - 1; i++) {
if (target_lux >= exp_table[i].lux && target_lux <= exp_table[i+1].lux) {
idx_low = i;
idx_high = i + 1;
break;
}
}
// 计算插值系数
float ratio = (float)(target_lux - exp_table[idx_low].lux) /
(exp_table[idx_high].lux - exp_table[idx_low].lux);
// 线性插值
out->exposure_time = exp_table[idx_low].exposure_time +
ratio * (exp_table[idx_high].exposure_time - exp_table[idx_low].exposure_time);
out->analog_gain = exp_table[idx_low].analog_gain +
ratio * (exp_table[idx_high].analog_gain - exp_table[idx_low].analog_gain);
// ... 数字增益同理
}
注意:曝光时间和增益的插值要分开做。因为曝光时间受帧率限制(比如最大33ms对应30fps),而增益受噪声限制。我曾经见过一个方案,把曝光时间和增益绑在一起插值,结果在过渡区出现了帧率跳变。
3.3 PID控制器在AE中的应用
查表法给出的是开环控制。但实际场景中,传感器的响应是非线性的,而且环境光会变化。所以我们需要闭环控制——PID控制器。
3.3.1 误差计算
PID的输入是亮度误差:
error = target_brightness - current_brightness;
其中target_brightness是目标亮度(比如直方图均值128),current_brightness是当前帧的统计亮度。
3.3.2 位置式PID
我习惯用位置式PID,直接输出曝光参数的变化量:
float pid_update(float error) {
// 比例项
float p_term = Kp * error;
// 积分项(带限幅)
integral += error;
integral = clamp(integral, -INTEGRAL_MAX, INTEGRAL_MAX);
float i_term = Ki * integral;
// 微分项
float d_term = Kd * (error - last_error);
last_error = error;
// 总输出
float output = p_term + i_term + d_term;
return clamp(output, -OUTPUT_MAX, OUTPUT_MAX);
}
参数调优的经验值:
- Kp:0.3~0.8,太大容易振荡。
- Ki:0.01~0.05,主要用于消除稳态误差。
- Kd:0.1~0.3,抑制快速变化,但容易引入噪声。
我的习惯:先把Kp调到画面不振荡,然后加一点点Ki让亮度稳定在目标值,最后加Kd抑制过冲。记住,积分项一定要限幅,否则从暗室走到阳光下,积分项会积累到很大,导致画面长时间过曝。
3.4 步长与死区控制
PID输出的是连续值,但实际硬件参数是离散的。而且,如果每次微小的亮度变化都去调整曝光,画面会一直闪烁。这就是步长和死区要解决的问题。
3.4.1 步长控制
步长控制限制了每次调整的最大幅度。比如,曝光时间每次最多变化500us,增益每次最多变化16(Q8格式)。
// 步长限幅
int16_t delta_exp = clamp(pid_output, -MAX_STEP_EXP, MAX_STEP_EXP);
int16_t delta_gain = clamp(pid_output, -MAX_STEP_GAIN, MAX_STEP_GAIN);
步长太大,画面会跳变;步长太小,收敛慢。我一般根据场景动态调整步长:
- 大误差(|error| > 30):大步长,快速收敛。
- 小误差(|error| < 10):小步长,精细调整。
3.4.2 死区控制
死区就是设置一个误差阈值,在这个范围内不调整曝光。比如,亮度误差在±3以内,我们认为画面已经足够好了,不需要调整。
// 死区判断
if (abs(error) < DEAD_ZONE) {
return; // 不调整
}
死区太小,画面会频繁微调,看起来像在呼吸;死区太大,亮度偏差会一直存在。我常用的死区值是±2~±5(8位亮度范围)。
避坑指南:我曾经在一个项目中,死区设成了±1,结果画面在暗光下一直闪烁。后来发现是传感器噪声导致亮度统计值在±1之间跳动。我的解决方案是:对统计值做时域滤波,比如用IIR滤波器平滑一下,再送入PID。
3.5 知识体系总览
下面这张图总结了AE核心算法的完整流程:
从图中可以看到,AE是一个典型的测量-计算-控制-反馈闭环。统计信息提取是基础,曝光表提供初始值,PID做精细调节,步长和死区保证稳定性。
嗯,这套流程我用了很多年,从手机摄像头到安防监控,基本框架都没变过。区别只在于参数调优和场景适配。你只要把今天讲的这几个模块吃透,AE这块就算入门了。