4. AE算法基础与统计信息获取:目标亮度与统计直方图

AE(自动曝光)算法,说白了就是帮摄像头找到「最舒服的亮度」。你想想看,一张照片太亮或太暗,细节全丢了,那还谈什么画质?我做了这么多年ISP,见过太多因为曝光翻车的案例——人脸过曝成白板,夜景暗部死黑一片。嗯,这章我们就从最基础的东西讲起。

4.1 AE的核心目标:目标亮度

AE算法的终极目标只有一个:让图像的平均亮度逼近一个预设的目标值。这个目标值通常用目标亮度(Target Luminance)来表示,单位可以是8-bit的灰度值(0-255),也可以是归一化的0-1。

我个人习惯把目标亮度分成三类场景:

场景类型 目标亮度范围(8-bit) 典型应用
标准场景 100 - 130 日常拍照、视频通话
高动态场景 80 - 100 逆光、HDR模式
低照度场景 40 - 70 夜景、暗光环境

关键点:目标亮度不是固定死的。我在项目中遇到过,同一个传感器在不同色温下,同样的目标亮度拍出来的人脸肤色差异很大。所以实际工程中,目标亮度往往需要结合AWB(自动白平衡)的结果做动态调整。

4.2 从ISP获取亮度统计信息

知道了目标亮度,下一步就是获取当前图像的亮度统计。ISP硬件通常会提供两种最基础的统计信息:亮度直方图(Histogram)和分区统计(Zone)。

4.2.1 亮度直方图(Histogram)

直方图统计的是图像中每个亮度级别的像素数量。举个例子,8-bit图像有256个亮度级别(0-255),直方图就是一个长度为256的数组,每个元素代表对应亮度值的像素个数。

ISP硬件一般会提供两种直方图模式:

  • 全局直方图:统计整帧图像的亮度分布。适合做全局曝光调整。
  • ROI直方图:只统计感兴趣区域(比如人脸框内)的亮度分布。适合做局部曝光优化。

代码层面,从ISP驱动获取直方图通常长这样:

// 假设ISP驱动返回的直方图数据结构
typedef struct {
    uint32_t histogram[256];  // 256个bin,每个bin统计像素个数
    uint32_t total_pixels;    // 总像素数,用于归一化
    uint16_t roi_x;           // ROI起始x坐标(如果是ROI模式)
    uint16_t roi_y;           // ROI起始y坐标
    uint16_t roi_width;       // ROI宽度
    uint16_t roi_height;      // ROI高度
} isp_histogram_t;

// 获取直方图示例
isp_histogram_t hist;
isp_get_histogram(&hist);  // 调用ISP驱动接口

// 计算平均亮度
float avg_luminance = 0.0f;
for (int i = 0; i < 256; i++) {
    avg_luminance += i * hist.histogram[i];
}
avg_luminance /= hist.total_pixels;

我的经验:直方图bin的数量不一定是256。有些低端ISP只给64个bin,甚至32个。遇到这种情况,我建议在AE算法内部做线性插值,把稀疏的直方图重采样到256个bin,这样后续的统计计算会更平滑。

4.2.2 分区统计(Zone)

分区统计,就是把图像分成M×N个网格,每个网格独立统计亮度信息。常见的分区有5×5、7×7、15×15等。分区统计的好处是能感知图像不同区域的亮度差异,这对逆光场景特别重要。

每个分区通常包含以下统计量:

  • 平均亮度:该分区内所有像素亮度的平均值。
  • 最大/最小亮度:该分区内的亮度极值。
  • 亮度方差:反映该分区内的纹理丰富程度。

ISP驱动获取分区统计的典型接口:

// 分区统计数据结构(以5x5分区为例)
#define ZONE_ROWS 5
#define ZONE_COLS 5

typedef struct {
    uint8_t avg_luminance[ZONE_ROWS][ZONE_COLS];  // 每个分区的平均亮度
    uint8_t max_luminance[ZONE_ROWS][ZONE_COLS];  // 每个分区的最大亮度
    uint8_t min_luminance[ZONE_ROWS][ZONE_COLS];  // 每个分区的最小亮度
    uint16_t variance[ZONE_ROWS][ZONE_COLS];      // 每个分区的亮度方差
} isp_zone_stats_t;

// 获取分区统计
isp_zone_stats_t zone_stats;
isp_get_zone_stats(&zone_stats);

// 计算中心区域的平均亮度(用于人脸优先场景)
float center_avg = 0.0f;
for (int r = 1; r < ZONE_ROWS - 1; r++) {
    for (int c = 1; c < ZONE_COLS - 1; c++) {
        center_avg += zone_stats.avg_luminance[r][c];
    }
}
center_avg /= 9;  // 3x3中心区域

注意:分区统计的精度取决于分区数量。分区太少(比如3×3),逆光场景下容易误判;分区太多(比如15×15),计算量暴增,而且噪声会被放大。我个人建议从7×7起步,根据实际效果再调整。

4.3 直方图与分区统计的融合使用

在实际工程中,我很少只用直方图或只用分区统计。两者各有优劣:

统计方式 优势 劣势
全局直方图 计算简单,能反映整体亮度分布 无法感知局部亮度差异
分区统计 能感知空间亮度变化,适合逆光/人脸场景 计算量大,分区边界可能引入伪影

我的做法是:先用分区统计判断场景类型(是否逆光、是否有人脸),然后根据场景类型决定直方图的权重分配。比如逆光场景,我会给暗部区域的直方图bin更高的权重,让AE算法优先提亮暗部。

4.4 知识体系:AE统计信息获取流程

下面这张图展示了从ISP硬件到AE算法的完整统计信息获取链路:

AE统计信息获取流程 ISP硬件 直方图统计 分区统计 驱动接口获取 驱动接口获取 AE算法融合计算 说明:直方图提供全局亮度分布,分区统计提供空间亮度信息,两者在AE算法中融合使用 驱动接口负责从ISP硬件读取原始统计值,并传递给上层AE算法

4.5 避坑指南

最后分享几个我在项目中踩过的坑:

  • 直方图溢出:有些ISP的直方图bin是16-bit计数器,但像素数可能超过65535。我曾经遇到过直方图统计值溢出导致AE误判的情况。解决办法是定期检查总像素数,如果发现溢出就做一次归一化重置。
  • 分区统计的边界效应:分区边界上的像素可能被重复统计或漏统计。我建议在ISP配置里开启「边界像素排除」选项,或者自己在软件层做边界裁剪。
  • 统计信息的时间对齐:直方图和分区统计可能来自不同帧。我记得有一次调试,发现直方图和分区统计的亮度值对不上,查了半天才发现是驱动返回的统计信息不是同一帧的。后来我强制要求驱动必须返回同一帧的统计信息。

总结:AE算法的起点就是准确获取图像的亮度统计信息。直方图告诉你「整体亮不亮」,分区统计告诉你「哪里亮哪里暗」。把这两个基础统计用好,AE算法就成功了一半。