3. AE核心算法:直方图统计与亮度评估、目标亮度设定、PID控制与步进搜索策略

自动曝光(AE)说白了,就是让摄像头拍出来的画面不黑不白,亮度刚刚好。我做了这么多年ISP,见过太多因为AE没调好导致产品翻车的案例。今天咱们就把AE的核心算法掰开揉碎讲清楚。

3.1 直方图统计与亮度评估

先问个问题:怎么判断一张图是太亮还是太暗?

你可能会说「看平均亮度呗」。嗯,平均亮度确实能反映整体明暗,但有个坑——它会被大面积亮区或暗区带偏。举个例子,夜景里有一盏路灯,背景全黑,平均亮度可能刚好,但人脸早就黑成一片了。

所以,我习惯用直方图来做亮度评估。直方图统计的是每个亮度级别的像素数量,横轴0~255代表亮度,纵轴代表像素个数。

核心思路:直方图能告诉你画面的亮度分布细节,而不仅仅是平均值。

具体怎么做?分三步:

  1. 统计直方图:遍历每个像素,按亮度值累加计数。
  2. 分区加权:把画面分成多个区域,中央区域权重高,边缘权重低。
  3. 计算亮度指标:用加权后的直方图算出平均亮度、中位数亮度、暗区占比等。

我给你们看一段实际项目中用过的代码片段:

// 直方图统计与亮度评估
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控制虽然好用,但有个前提——系统是线性的。实际上,曝光时间和增益对亮度的影响并不是完全线性的。这时候,步进搜索策略就派上用场了。

步进搜索说白了就是「试一步,看效果,再决定下一步」。我常用的策略有两种:

策略名称 原理 适用场景
二分搜索 每次取中间值,快速逼近目标 初始曝光设定、大范围亮度跳变
梯度下降 根据亮度变化方向,逐步调整 微调、平滑过渡

二分搜索的流程是这样的:

  1. 设定曝光时间的上下限(比如1ms~30ms)。
  2. 取中间值作为当前曝光时间。
  3. 计算当前亮度与目标亮度的偏差。
  4. 如果偏差为正(太暗),把下限设为当前值;反之把上限设为当前值。
  5. 重复直到偏差小于阈值。

梯度下降则更平滑:

// 梯度下降步进搜索
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核心算法的流程串起来了,你一看就明白:

AE核心算法流程图 原始图像帧 直方图统计与亮度评估 计算亮度偏差 偏差<阈值? 保持当前参数 PID控制 / 步进搜索 更新曝光时间/增益 关键点 • 直方图分区加权 • 目标亮度自适应 • PID参数调优

3.6 实战中的取舍

最后聊点实际的。在嵌入式平台上跑AE算法,资源是有限的。我给你们几个建议:

  • 直方图统计用硬件加速:很多ISP芯片自带直方图统计模块,别用CPU去算,太慢。
  • PID参数存多组:不同场景(室内、室外、夜景)用不同的PID参数,效果比一组参数通吃好得多。
  • 步进搜索和PID结合用:大范围跳变用步进搜索,小范围微调用PID,两者互补。

总结一下:AE的核心就三件事——知道当前多亮(直方图统计),知道想要多亮(目标亮度设定),知道怎么调过去(PID+步进搜索)。这三件事做好了,AE就稳了八成。剩下的两成,靠的是场景识别和参数调优,那是经验活。