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算法的完整统计信息获取链路:
4.5 避坑指南
最后分享几个我在项目中踩过的坑:
- 直方图溢出:有些ISP的直方图bin是16-bit计数器,但像素数可能超过65535。我曾经遇到过直方图统计值溢出导致AE误判的情况。解决办法是定期检查总像素数,如果发现溢出就做一次归一化重置。
- 分区统计的边界效应:分区边界上的像素可能被重复统计或漏统计。我建议在ISP配置里开启「边界像素排除」选项,或者自己在软件层做边界裁剪。
- 统计信息的时间对齐:直方图和分区统计可能来自不同帧。我记得有一次调试,发现直方图和分区统计的亮度值对不上,查了半天才发现是驱动返回的统计信息不是同一帧的。后来我强制要求驱动必须返回同一帧的统计信息。
总结:AE算法的起点就是准确获取图像的亮度统计信息。直方图告诉你「整体亮不亮」,分区统计告诉你「哪里亮哪里暗」。把这两个基础统计用好,AE算法就成功了一半。