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的流程:

  1. 网格划分:将图像分成MxN个网格,每个网格统计平均亮度。
  2. 权重分配:根据人脸检测结果,给包含人脸的网格分配高权重(比如10x),背景网格分配低权重(1x)。
  3. 加权平均:计算所有网格的加权平均亮度。
  4. 曝光调整:基于加权平均亮度调整曝光参数。
// 区域权重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的流程图,帮你理清整体逻辑:

区域权重AE流程图 原始图像帧 MxN网格划分与亮度统计 人脸检测与区域定位 动态权重分配(人脸优先) 加权平均亮度计算 曝光参数调整 说明 人脸区域权重通常设为 背景区域的5~10倍 权重需做时间平滑 常见网格配置 5x5 / 7x5 / 9x7 网格越多,计算量越大

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在大多数场景下“感觉不到”在调整,但在场景剧烈变化时又能快速跟上。