3. AE算法核心:亮度统计直方图、目标亮度设定、AE收敛过程、AE的反馈控制模型
AE(自动曝光)说白了,就是帮摄像头找到那个“不亮不暗刚刚好”的曝光参数。我做了这么多年ISP,见过太多因为AE没调好导致画面翻车的案例——要么人脸死黑,要么天空一片惨白。这一章,咱们就把AE的四个核心模块拆开揉碎了讲。
3.1 亮度统计直方图:AE的“眼睛”
AE要工作,首先得知道当前画面到底有多亮。这个任务就交给亮度统计直方图。它不是直接拿RGB三个通道算平均,而是先把图像转成亮度分量Y,再统计每个亮度级别的像素个数。
我个人习惯用这个公式算亮度:
Y = 0.299 * R + 0.587 * G + 0.114 * B
嗯,就是标准的ITU-R BT.601权重。为什么绿色权重最高?因为人眼对绿色最敏感。你想想看,如果给蓝色和红色一样的权重,那统计出来的亮度就跟人眼感受对不上了。
直方图的横轴是亮度等级(0~255,8bit数据),纵轴是像素个数。我一般会把它分成三个区:
| 区域 | 亮度范围 | 含义 |
|---|---|---|
| 暗区 | 0 ~ 85 | 阴影、暗部细节 |
| 中间区 | 86 ~ 170 | 主体、中间调 |
| 亮区 | 171 ~ 255 | 高光、过曝风险区 |
实际项目中,我还会对直方图做加权处理。比如人脸区域给更高权重,天空区域适当降低权重。这样AE会更关注主体,而不是被背景牵着鼻子走。
3.2 目标亮度设定:AE要“追”什么?
有了当前亮度,还得知道目标亮度是多少。这个目标亮度不是拍脑袋定的,它取决于场景和用户需求。
我一般把目标亮度设定分成三个层次:
- 全局目标亮度:整个画面的平均亮度目标。典型值在100~120(8bit),偏亮场景可以设到130~140。
- 区域目标亮度:对特定区域(人脸、车牌)单独设目标。人脸我习惯设到140~160,比全局亮一点,这样肤色好看。
- 动态目标亮度:根据场景自动调整。比如夜景降低目标亮度,避免噪声被放大;强光下提高目标亮度,保留更多暗部细节。
核心公式:
target_luminance = base_target + scene_offset + roi_offset
其中base_target是基础值(比如110),scene_offset是场景补偿(夜景-20,逆光+15),roi_offset是感兴趣区域补偿(人脸+20)。
我曾经在一个安防项目中踩过坑:把目标亮度设得太高,结果夜晚画面全是噪声。后来加了场景自适应,根据环境照度动态调整目标亮度,问题才解决。
3.3 AE收敛过程:从“乱跳”到“稳定”
AE不是一步到位的。它是个迭代过程:先测当前亮度,算偏差,调参数,再测,再调……直到亮度偏差小于某个阈值。
收敛过程我一般画成这个流程图:
收敛速度是个关键指标。太快了画面会闪烁,太慢了用户觉得卡顿。我一般把收敛时间控制在5~15帧之间,具体看场景。
3.4 AE的反馈控制模型:PID在曝光中的应用
AE本质上是个反馈控制系统。我最早做AE时用的是简单的比例控制,效果嘛……只能说能用,但不够好。后来引入了PID控制,收敛质量和稳定性都上了一个台阶。
PID控制器的三个分量:
| 分量 | 作用 | AE中的含义 |
|---|---|---|
| 比例(P) | 根据当前偏差调整 | 当前亮度与目标亮度的差值 |
| 积分(I) | 消除稳态误差 | 历史偏差的累积 |
| 微分(D) | 抑制震荡 | 亮度变化的速度 |
代码实现大概是这样的:
// PID控制器结构体
typedef struct {
float Kp; // 比例系数
float Ki; // 积分系数
float Kd; // 微分系数
float integral; // 积分累积
float prev_error; // 上一次偏差
} PIDController;
// AE的PID更新函数
float ae_pid_update(PIDController *pid, float target, float current) {
float error = target - current;
// 比例项
float p_out = pid->Kp * error;
// 积分项(带限幅)
pid->integral += error;
if (pid->integral > INTEGRAL_MAX) pid->integral = INTEGRAL_MAX;
if (pid->integral < -INTEGRAL_MAX) pid->integral = -INTEGRAL_MAX;
float i_out = pid->Ki * pid->integral;
// 微分项
float d_out = pid->Kd * (error - pid->prev_error);
pid->prev_error = error;
return p_out + i_out + d_out;
}
嗯,这里要注意积分限幅。我曾经在一个项目中没加限幅,结果积分项越积越大,画面直接过曝到255还回不来。加了限幅之后,即使长时间处于大偏差状态,积分项也不会失控。
PID的输出最终要映射到曝光参数上。曝光参数有三个自由度:
- 曝光时间:影响运动模糊和帧率
- 模拟增益:影响噪声
- 数字增益:影响量化精度
我一般优先调曝光时间,不够再加增益。为什么?因为增益会放大噪声,而曝光时间只是让传感器多收集一会儿光。当然,如果场景在运动,曝光时间太长会导致模糊,这时候就得权衡了。
映射关系可以这样写:
// 将PID输出映射到曝光参数
void ae_apply_exposure(float pid_output) {
// 先调曝光时间
float new_exposure = base_exposure * (1.0 + pid_output);
if (new_exposure < MAX_EXPOSURE) {
set_exposure_time(new_exposure);
set_analog_gain(1.0);
set_digital_gain(1.0);
return;
}
// 曝光时间到上限了,开始调增益
new_exposure = MAX_EXPOSURE;
float gain_needed = (base_exposure * (1.0 + pid_output)) / MAX_EXPOSURE;
// 先调模拟增益,再调数字增益
float analog = min(gain_needed, MAX_ANALOG_GAIN);
float digital = gain_needed / analog;
set_exposure_time(new_exposure);
set_analog_gain(analog);
set_digital_gain(digital);
}
这个优先级策略我用了很多年,效果一直不错。说白了就是:能用曝光时间解决的问题,就别用增益。
本章核心要点:
- 亮度直方图是AE的输入,要关注分布而非均值
- 目标亮度要分全局、区域、动态三层设定
- 收敛过程是迭代反馈,步长决定速度和稳定性
- PID控制让AE更平滑,积分限幅和参数优先级是关键