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真正需要的。

实际项目中,我还会对直方图做加权处理。比如人脸区域给更高权重,天空区域适当降低权重。这样AE会更关注主体,而不是被背景牵着鼻子走。

3.2 目标亮度设定:AE要“追”什么?

有了当前亮度,还得知道目标亮度是多少。这个目标亮度不是拍脑袋定的,它取决于场景和用户需求。

我一般把目标亮度设定分成三个层次:

  1. 全局目标亮度:整个画面的平均亮度目标。典型值在100~120(8bit),偏亮场景可以设到130~140。
  2. 区域目标亮度:对特定区域(人脸、车牌)单独设目标。人脸我习惯设到140~160,比全局亮一点,这样肤色好看。
  3. 动态目标亮度:根据场景自动调整。比如夜景降低目标亮度,避免噪声被放大;强光下提高目标亮度,保留更多暗部细节。

核心公式:

target_luminance = base_target + scene_offset + roi_offset

其中base_target是基础值(比如110),scene_offset是场景补偿(夜景-20,逆光+15),roi_offset是感兴趣区域补偿(人脸+20)。

我曾经在一个安防项目中踩过坑:把目标亮度设得太高,结果夜晚画面全是噪声。后来加了场景自适应,根据环境照度动态调整目标亮度,问题才解决。

3.3 AE收敛过程:从“乱跳”到“稳定”

AE不是一步到位的。它是个迭代过程:先测当前亮度,算偏差,调参数,再测,再调……直到亮度偏差小于某个阈值。

收敛过程我一般画成这个流程图:

AE收敛过程流程图 AE收敛过程流程图 获取原始图像 计算亮度直方图 计算当前平均亮度 偏差 < 阈值? 是 收敛完成 否 调整参数 收敛速度取决于步长和场景变化速度。步长太大容易震荡,步长太小收敛慢。

收敛速度是个关键指标。太快了画面会闪烁,太慢了用户觉得卡顿。我一般把收敛时间控制在5~15帧之间,具体看场景。

注意: 场景切换时(比如从室内走到室外),AE会重新收敛。这时候如果步长设得太大,画面会先过曝再拉回来,出现“闪光”效果。我建议在场景切换检测到后,先用大步长快速接近目标,再用小步长精细调整。

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还回不来。加了限幅之后,即使长时间处于大偏差状态,积分项也不会失控。

调参经验: 我一般先调Kp,让AE能快速响应;再加一点点Ki消除稳态误差(Ki通常是Kp的1/10~1/5);最后加Kd抑制震荡(Kd通常是Kp的1/20~1/10)。如果画面出现来回跳,说明Kd太大了。

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更平滑,积分限幅和参数优先级是关键