3、AE核心算法:直方图统计与权重映射、基于平均亮度的AE、基于区域权重的AE(人脸优先)、AE的收敛速度与稳定性控制
自动曝光(AE)是ISP 3A算法里最贴近用户体验的一环。你想想看,用户拿起手机拍照,第一眼看到的就是画面亮暗是否合适。如果曝光不准,再好的降噪、再准的白平衡都白搭。
这一章我们深入AE的核心算法。我会从最基础的直方图统计讲起,逐步深入到基于平均亮度、区域权重,再到人脸优先的AE策略。最后,我会重点聊聊收敛速度和稳定性控制——这部分在实际工程中踩坑最多。
3.1 直方图统计与权重映射
AE的第一步,是知道当前画面到底有多亮。硬件ISP通常会输出一帧图像的亮度直方图,而不是全图每个像素的亮度值。为什么?因为直方图数据量小,硬件实现简单,而且足够做决策。
直方图统计的核心思路:将图像亮度从0到255分成若干个bin(通常是256个),统计每个亮度级别下的像素个数。
关键点:直方图不是最终目标,而是AE决策的输入特征。我们需要从直方图中提取出“当前场景的平均亮度”或“感兴趣区域的亮度”。
权重映射是怎么回事?说白了,就是给不同亮度区域分配不同的关注度。比如,暗部细节我们可能不太在意,但人脸区域的亮度必须准确。
我习惯的做法是:先统计全图直方图,然后根据场景模式(如夜景、逆光、人像)应用不同的权重曲线。举个例子,夜景模式下我会给暗部区域更高的权重,避免整体画面过暗。
// 伪代码:直方图权重映射
uint32_t hist[256]; // 硬件统计的直方图
float weight[256]; // 权重表,根据场景模式预计算
float weighted_sum = 0.0f;
float total_weight = 0.0f;
for (int i = 0; i < 256; i++) {
weighted_sum += hist[i] * weight[i] * i;
total_weight += hist[i] * weight[i];
}
float target_luminance = weighted_sum / total_weight;
工程经验:权重表不要写死在代码里。我建议做成可配置的查找表(LUT),通过ISP tuning工具在线调整。不同sensor的响应曲线差异很大,固定权重往往翻车。
3.2 基于平均亮度的AE
这是最基础的AE算法,也是很多入门教程里讲的方法。它的逻辑很简单:计算全图的平均亮度,然后调整曝光时间或增益,让平均亮度逼近目标值。
公式长这样:
target_luminance = 120; // 目标平均亮度(8bit图像)
current_luminance = get_average_luminance();
// 计算曝光补偿系数
float compensation = target_luminance / current_luminance;
// 调整曝光时间或增益
new_exposure_time = current_exposure_time * compensation;
new_gain = current_gain * compensation;
听起来很完美对吧?但实际用起来问题不少。我在项目中遇到过这样的场景:一个画面里左边是明亮的窗户,右边是暗角。平均亮度算出来可能刚好在目标值附近,但窗户过曝、暗角欠曝,用户体验极差。
避坑指南:纯平均亮度AE在均匀光照场景下表现尚可,但遇到逆光、大光比场景,基本就是灾难。我曾经在一个安防项目中只用了平均亮度AE,结果客户投诉说“白天看不清人脸”。后来不得不加上区域权重。
3.3 基于区域权重的AE(人脸优先)
为了解决平均亮度AE的缺陷,业界普遍采用区域权重AE。核心思想:把画面分成多个区域(比如5x5或9x7的网格),每个区域分配一个权重。人脸区域权重最高,背景区域权重低。
区域权重AE的流程:
- 网格划分:将图像分成MxN个网格,每个网格统计平均亮度。
- 权重分配:根据人脸检测结果,给包含人脸的网格分配高权重(比如10x),背景网格分配低权重(1x)。
- 加权平均:计算所有网格的加权平均亮度。
- 曝光调整:基于加权平均亮度调整曝光参数。
// 区域权重AE伪代码
#define GRID_ROWS 9
#define GRID_COLS 7
float grid_luminance[GRID_ROWS][GRID_COLS];
float grid_weight[GRID_ROWS][GRID_COLS];
// 初始化权重:默认全1.0
for (int r = 0; r < GRID_ROWS; r++)
for (int c = 0; c < GRID_COLS; c++)
grid_weight[r][c] = 1.0f;
// 人脸区域权重提升
for (auto &face : face_rects) {
int grid_r = face.center_y * GRID_ROWS / image_height;
int grid_c = face.center_x * GRID_COLS / image_width;
// 人脸区域权重设为10倍
grid_weight[grid_r][grid_c] = 10.0f;
// 相邻区域也适当提升
// ...
}
// 计算加权平均亮度
float weighted_sum = 0.0f, total_weight = 0.0f;
for (int r = 0; r < GRID_ROWS; r++) {
for (int c = 0; c < GRID_COLS; c++) {
weighted_sum += grid_luminance[r][c] * grid_weight[r][c];
total_weight += grid_weight[r][c];
}
}
float target_luminance = weighted_sum / total_weight;
关键洞察:人脸优先AE的核心不是算法多复杂,而是人脸检测的稳定性和权重分配的平滑性。如果人脸检测一帧有、一帧无,AE会剧烈抖动。我通常会在人脸检测结果上做时间滤波,确保权重变化是渐变的。
这里我画了一张区域权重AE的流程图,帮你理清整体逻辑:
3.4 AE的收敛速度与稳定性控制
这是AE算法里最容易被忽视,但实际工程中问题最多的部分。收敛速度太快,画面会闪烁;太慢,用户觉得反应迟钝。稳定性控制不好,在明暗交替的场景下,画面会来回震荡。
我个人习惯把AE控制看作一个PID调节器。比例项(P)决定响应速度,积分项(I)消除稳态误差,微分项(D)抑制震荡。但在ISP里,我们通常只用PI控制,D项容易引入噪声。
| 控制参数 | 作用 | 调大后的影响 | 调小后的影响 |
|---|---|---|---|
| 比例系数 Kp | 决定对亮度误差的响应速度 | 收敛快,但可能过冲 | 收敛慢,画面变化平滑 |
| 积分系数 Ki | 消除稳态亮度偏差 | 稳态精度高,但可能震荡 | 稳态有静差,亮度不准 |
| 积分限幅 | 防止积分饱和 | 抗饱和能力强 | 积分可能过大,恢复慢 |
| 死区阈值 | 亮度误差小于阈值时不调整 | 稳定性好,但精度略降 | 频繁调整,画面闪烁 |
我曾经踩过的坑:在一个手机项目中,AE收敛速度调得太快,导致从室内走到室外时,画面先过曝再迅速变暗,用户看到的就是“闪一下”。后来我把Kp降低了40%,并加入了亮度误差的死区阈值(±3个灰度级),问题才解决。
收敛速度控制的几个实用技巧:
- 分阶段调整:亮度误差大时用快速收敛,误差小时用慢速微调。我通常设两个阈值:误差>30时Kp=0.8,误差<10时Kp=0.2。
- 曝光时间与增益的协调:优先调整曝光时间,增益作为补充。因为增益会引入噪声,能不动尽量不动。
- 帧间平滑:对最终输出的曝光参数做一阶低通滤波。公式:
output = alpha * new_value + (1-alpha) * old_value,alpha通常取0.3~0.6。 - 场景切换检测:当亮度变化超过阈值时,重置积分项,避免旧场景的积分值影响新场景。
// AE PI控制器实现示例
typedef struct {
float Kp; // 比例系数
float Ki; // 积分系数
float integral; // 积分累计值
float integral_max; // 积分限幅
float dead_zone; // 死区阈值
float alpha; // 平滑系数
float last_output; // 上一帧输出
} AEController;
float ae_pi_control(AEController *ctrl, float target, float current) {
float error = target - current;
// 死区处理
if (fabs(error) < ctrl->dead_zone) {
return ctrl->last_output;
}
// 比例项
float p_term = ctrl->Kp * error;
// 积分项
ctrl->integral += ctrl->Ki * error;
// 积分限幅
if (ctrl->integral > ctrl->integral_max)
ctrl->integral = ctrl->integral_max;
if (ctrl->integral < -ctrl->integral_max)
ctrl->integral = -ctrl->integral_max;
float output = p_term + ctrl->integral;
// 帧间平滑
output = ctrl->alpha * output + (1 - ctrl->alpha) * ctrl->last_output;
ctrl->last_output = output;
return output;
}
调参建议:不要一上来就调Ki。先把Kp调到画面不闪烁、响应可接受的程度,再慢慢加Ki消除稳态误差。我一般从Kp=0.5、Ki=0.05开始,然后根据实际效果微调。
最后说一句,AE的收敛速度和稳定性是一对矛盾。追求快速响应,就难免有超调;追求稳定,响应就会迟钝。实际工程中,我通常会在两者之间找一个平衡点——让AE在大多数场景下“感觉不到”在调整,但在场景剧烈变化时又能快速跟上。