3. AE核心算法:直方图统计与亮度评估、目标亮度设定、PID控制与步进搜索策略
自动曝光(AE)说白了,就是让摄像头拍出来的画面不黑不白,亮度刚刚好。我做了这么多年ISP,见过太多因为AE没调好导致产品翻车的案例。今天咱们就把AE的核心算法掰开揉碎讲清楚。
3.1 直方图统计与亮度评估
先问个问题:怎么判断一张图是太亮还是太暗?
你可能会说「看平均亮度呗」。嗯,平均亮度确实能反映整体明暗,但有个坑——它会被大面积亮区或暗区带偏。举个例子,夜景里有一盏路灯,背景全黑,平均亮度可能刚好,但人脸早就黑成一片了。
所以,我习惯用直方图来做亮度评估。直方图统计的是每个亮度级别的像素数量,横轴0~255代表亮度,纵轴代表像素个数。
核心思路:直方图能告诉你画面的亮度分布细节,而不仅仅是平均值。
具体怎么做?分三步:
- 统计直方图:遍历每个像素,按亮度值累加计数。
- 分区加权:把画面分成多个区域,中央区域权重高,边缘权重低。
- 计算亮度指标:用加权后的直方图算出平均亮度、中位数亮度、暗区占比等。
我给你们看一段实际项目中用过的代码片段:
// 直方图统计与亮度评估
void calc_histogram(uint8_t *frame, int width, int height, uint32_t *hist) {
memset(hist, 0, 256 * sizeof(uint32_t));
for (int i = 0; i < width * height; i++) {
hist[frame[i]]++;
}
}
float calc_weighted_luminance(uint32_t *hist, int total_pixels) {
// 中央区域权重更高,这里简化处理
float weighted_sum = 0.0f;
for (int i = 0; i < 256; i++) {
weighted_sum += i * hist[i];
}
return weighted_sum / total_pixels;
}
我的经验:直方图统计时别忘了做「暗区保护」和「亮区保护」。我曾经遇到一个场景,画面里有一小块高亮区域,平均亮度被拉得很高,结果AE把整体压暗,暗部细节全丢了。后来加了暗区占比阈值,才解决这个问题。
3.2 目标亮度设定
有了当前亮度,还得知道「目标亮度」是多少。目标亮度不是拍脑袋定的,它取决于场景和用户需求。
我一般把目标亮度设定分成两类:
| 场景类型 | 目标亮度范围 | 说明 |
|---|---|---|
| 室内正常光照 | 100~140 | 人脸清晰,背景不过曝 |
| 室外强光 | 120~160 | 保留高光细节,避免全白 |
| 夜景/低照度 | 40~80 | 优先保证暗部可见,允许少量噪点 |
| 背光场景 | 80~120 | 人脸区域优先,背景可过曝 |
你可能会问:「这些值怎么来的?」
说白了,这是调出来的。我刚开始做AE时,拿着示波器对着灰阶卡一帧一帧调,调了整整一周才找到感觉。后来我把经验总结成一套规则:
- 人脸优先:如果检测到人脸,目标亮度以人脸区域为准。
- 场景自适应:根据环境照度传感器或直方图分布,动态调整目标值。
- 用户可调:给用户留一个曝光补偿(EV)接口,±2EV范围内可调。
避坑指南:我曾经在一个项目中,目标亮度设得太高,导致画面整体偏亮,高光区域全部过曝。后来加了「高光溢出保护」——如果直方图中亮度250以上的像素占比超过5%,就自动降低目标亮度。
3.3 PID控制
有了当前亮度和目标亮度,怎么让摄像头「听话」地调整到目标值?
PID控制是工业界的经典方案。P是比例,I是积分,D是微分。我简单解释一下:
- P(比例):当前偏差越大,调整幅度越大。但纯P控制会有稳态误差。
- I(积分):累积过去的偏差,消除稳态误差。但I太大会导致超调。
- D(微分):预测偏差的变化趋势,抑制超调。但D对噪声敏感。
在AE中,PID的输入是亮度偏差,输出是曝光时间或增益的调整量。
// PID控制器结构体
typedef struct {
float Kp, Ki, Kd; // PID参数
float integral; // 积分项
float prev_error; // 上一次偏差
float output_min, output_max; // 输出限幅
} PID_Controller;
float pid_update(PID_Controller *pid, float target, float current) {
float error = target - current;
pid->integral += error;
// 积分限幅,防止积分饱和
if (pid->integral > 100.0f) pid->integral = 100.0f;
if (pid->integral < -100.0f) pid->integral = -100.0f;
float derivative = error - pid->prev_error;
pid->prev_error = error;
float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative;
// 输出限幅
if (output > pid->output_max) output = pid->output_max;
if (output < pid->output_min) output = pid->output_min;
return output;
}
我的调参经验:先调P,让系统能快速响应;再加I,消除稳态误差;最后加D,抑制超调。我习惯在调试时把PID输出打印出来,观察波形——如果输出震荡,说明P太大或D太小;如果响应太慢,说明P太小或I太大。
3.4 步进搜索策略
PID控制虽然好用,但有个前提——系统是线性的。实际上,曝光时间和增益对亮度的影响并不是完全线性的。这时候,步进搜索策略就派上用场了。
步进搜索说白了就是「试一步,看效果,再决定下一步」。我常用的策略有两种:
| 策略名称 | 原理 | 适用场景 |
|---|---|---|
| 二分搜索 | 每次取中间值,快速逼近目标 | 初始曝光设定、大范围亮度跳变 |
| 梯度下降 | 根据亮度变化方向,逐步调整 | 微调、平滑过渡 |
二分搜索的流程是这样的:
- 设定曝光时间的上下限(比如1ms~30ms)。
- 取中间值作为当前曝光时间。
- 计算当前亮度与目标亮度的偏差。
- 如果偏差为正(太暗),把下限设为当前值;反之把上限设为当前值。
- 重复直到偏差小于阈值。
梯度下降则更平滑:
// 梯度下降步进搜索
float step_search(float current_lum, float target_lum, float current_exp) {
float step_size = 0.1f; // 步长
float diff = target_lum - current_lum;
float adjustment = step_size * diff;
// 限制调整幅度,防止突变
if (adjustment > 0.5f) adjustment = 0.5f;
if (adjustment < -0.5f) adjustment = -0.5f;
return current_exp + adjustment;
}
我曾经踩过的坑:步进搜索的步长不能固定。在亮度接近目标时,大步长会导致来回震荡;在亮度偏差大时,小步长又太慢。我后来用了「自适应步长」——偏差大时步长大,偏差小时步长小,效果好了很多。
3.5 核心逻辑流程图
下面这张图把AE核心算法的流程串起来了,你一看就明白:
3.6 实战中的取舍
最后聊点实际的。在嵌入式平台上跑AE算法,资源是有限的。我给你们几个建议:
- 直方图统计用硬件加速:很多ISP芯片自带直方图统计模块,别用CPU去算,太慢。
- PID参数存多组:不同场景(室内、室外、夜景)用不同的PID参数,效果比一组参数通吃好得多。
- 步进搜索和PID结合用:大范围跳变用步进搜索,小范围微调用PID,两者互补。
总结一下:AE的核心就三件事——知道当前多亮(直方图统计),知道想要多亮(目标亮度设定),知道怎么调过去(PID+步进搜索)。这三件事做好了,AE就稳了八成。剩下的两成,靠的是场景识别和参数调优,那是经验活。