3、AE算法实现:统计信息提取、亮度权重映射、曝光补偿、AE Table设计与查表法
AE算法听起来玄乎,说白了就是让摄像头自己知道「现在该亮一点还是暗一点」。我做了这么多年ISP,发现很多工程师把AE想复杂了。其实核心就四步:先看画面有多亮,再决定哪里重要,然后算算要不要补偿,最后查个表把参数定下来。
嗯,咱们一步步拆开讲。
3.1 统计信息提取:你得知道画面在说什么
AE的第一步,是从RAW域拿到统计信息。这部分通常在ISP的统计引擎里硬件完成,但算法工程师得清楚它吐出来的是什么。
我个人习惯把统计信息分成三类:
- 亮度直方图:每个bin统计像素个数,一般是256个bin,覆盖0~1023的RAW值
- 分区平均亮度:把画面切成M×N块,每块算一个平均亮度值
- 过曝/欠曝像素占比:统计亮度超过阈值(比如饱和值95%)和低于阈值(比如暗电流+噪声门限)的像素比例
这里有个坑。我在项目中遇到过,有人直接用RAW域的10bit数据做直方图,结果发现暗部细节全被噪声淹没了。为什么?因为RAW域的响应是线性的,人眼对亮度的感知却是对数的。你想想看,暗部稍微有点噪声,在直方图上就占了很大一片。
代码实现上,统计模块通常长这样:
// 伪代码:RAW域AE统计信息提取
#define GRID_ROWS 15
#define GRID_COLS 15
#define HIST_BINS 256
typedef struct {
uint32_t hist[HIST_BINS]; // 亮度直方图
uint16_t grid_luma[GRID_ROWS][GRID_COLS]; // 分区平均亮度
float over_exposure_ratio; // 过曝比例
float under_exposure_ratio; // 欠曝比例
} AE_Stats;
void AE_ExtractStats(uint16_t *raw_buf, int width, int height, AE_Stats *stats) {
// 1. 清空统计缓冲区
memset(stats, 0, sizeof(AE_Stats));
// 2. 遍历每个像素
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
uint16_t val = raw_buf[y * width + x];
// 直方图统计(先做Gamma映射)
uint8_t bin = (uint8_t)(pow(val / 1023.0f, 1.0f/2.2f) * 255);
stats->hist[bin]++;
// 分区平均亮度
int grid_y = y * GRID_ROWS / height;
int grid_x = x * GRID_COLS / width;
stats->grid_luma[grid_y][grid_x] += val;
}
}
// 3. 归一化分区亮度
for (int i = 0; i < GRID_ROWS; i++)
for (int j = 0; j < GRID_COLS; j++)
stats->grid_luma[i][j] /= (width/GRID_COLS) * (height/GRID_ROWS);
}
3.2 亮度权重映射:不是所有区域都重要
拿到分区亮度之后,你不能简单取平均。为什么?因为画面中心通常比边缘重要,人脸区域比天空重要。这就是权重映射要做的事。
我常用的权重映射分两种:
| 权重类型 | 适用场景 | 权重分布特点 |
|---|---|---|
| 中心权重 | 普通拍照、风景 | 中心区域权重高,边缘递减 |
| 人脸权重 | 人像、自拍 | 人脸检测框内权重显著提高 |
中心权重的实现很简单,就是一个二维高斯分布:
// 中心权重表生成(15x15网格)
float weight[15][15];
float sigma = 3.0f; // 标准差,控制权重衰减速度
int cx = 7, cy = 7; // 中心坐标
for (int i = 0; i < 15; i++) {
for (int j = 0; j < 15; j++) {
float dx = (i - cx) / sigma;
float dy = (j - cy) / sigma;
weight[i][j] = expf(-0.5f * (dx*dx + dy*dy));
}
}
加权平均亮度的计算:
float weighted_luma = 0.0f;
float total_weight = 0.0f;
for (int i = 0; i < GRID_ROWS; i++) {
for (int j = 0; j < GRID_COLS; j++) {
weighted_luma += stats->grid_luma[i][j] * weight[i][j];
total_weight += weight[i][j];
}
}
weighted_luma /= total_weight;
3.3 曝光补偿:让算法学会「手下留情」
加权平均亮度算出来了,但直接拿它去调曝光参数,拍出来的照片往往偏暗。为什么?因为人眼喜欢亮一点的画面,而算法只追求「平均亮度等于目标值」。
曝光补偿就是干这个的——在算法算出来的基础上,人为加一点偏移。我一般分三步:
- 场景检测:判断当前是逆光、夜景、还是普通场景
- 补偿量计算:根据场景查一个补偿偏移量
- 限幅:补偿后的亮度不能超过传感器最大增益的物理限制
举个例子,逆光场景下,人脸区域亮度可能只有背景的1/4。如果不做补偿,算法会为了照顾背景而把人脸压得很暗。这时候我会加+1.0EV到+1.5EV的补偿。
// 曝光补偿计算
float AE_CalcCompensation(AE_Stats *stats, int scene_type) {
float comp = 0.0f;
switch (scene_type) {
case SCENE_BACKLIGHT:
comp = 1.2f; // +1.2EV
break;
case SCENE_NIGHT:
comp = 0.5f; // +0.5EV,夜景不能补太多,否则噪声爆炸
break;
case SCENE_NORMAL:
comp = 0.3f; // +0.3EV,稍微亮一点更讨喜
break;
}
// 限幅:补偿后亮度不能超过目标值的2倍
float max_comp = 1.0f; // 最大+1EV
return min(comp, max_comp);
}
3.4 AE Table设计与查表法:把计算变成查表
好了,现在我们有目标亮度了。但怎么把它变成曝光参数(曝光时间、模拟增益、数字增益)?
最直接的办法是公式计算,但公式计算有延迟,而且不同传感器响应曲线不一样。我更喜欢用查表法——提前标定好一张AE Table,把「目标亮度→曝光参数」的映射关系存进去。
AE Table的设计原则:
- 优先调曝光时间:曝光时间增加不引入噪声,但会降低帧率
- 其次调模拟增益:模拟增益比数字增益噪声小
- 最后调数字增益:数字增益会放大量化噪声,能不用就不用
一张典型的AE Table长这样:
| 亮度区间 | 曝光时间(ms) | 模拟增益(dB) | 数字增益(dB) | 说明 |
|---|---|---|---|---|
| 0 ~ 50 | 33 | 24 | 6 | 极暗场景,优先拉满曝光时间 |
| 50 ~ 100 | 33 | 18 | 3 | 暗光,降低模拟增益 |
| 100 ~ 150 | 33 | 12 | 0 | 室内,只用模拟增益 |
| 150 ~ 200 | 20 | 6 | 0 | 正常光照,缩短曝光时间 |
| 200 ~ 255 | 10 | 0 | 0 | 强光,只用曝光时间控制 |
查表法的实现:
// AE Table结构
typedef struct {
uint8_t luma_threshold; // 亮度阈值
float exposure_time; // 曝光时间(ms)
float analog_gain; // 模拟增益(dB)
float digital_gain; // 数字增益(dB)
} AE_Table_Entry;
AE_Table_Entry ae_table[] = {
{50, 33.0f, 24.0f, 6.0f},
{100, 33.0f, 18.0f, 3.0f},
{150, 33.0f, 12.0f, 0.0f},
{200, 20.0f, 6.0f, 0.0f},
{255, 10.0f, 0.0f, 0.0f},
};
void AE_LookupTable(uint8_t target_luma, float *exp_time, float *analog_gain, float *digital_gain) {
for (int i = 0; i < 5; i++) {
if (target_luma <= ae_table[i].luma_threshold) {
*exp_time = ae_table[i].exposure_time;
*analog_gain = ae_table[i].analog_gain;
*digital_gain = ae_table[i].digital_gain;
return;
}
}
// 超出范围取最后一项
*exp_time = ae_table[4].exposure_time;
*analog_gain = ae_table[4].analog_gain;
*digital_gain = ae_table[4].digital_gain;
}
3.5 整体流程:一张图看懂AE算法
说了这么多,咱们用一张SVG图把整个流程串起来。这张图我画了很多遍,每次给新人讲AE都用它。
这张图里有个细节你可能注意到了——反馈箭头。AE不是一锤子买卖,它是一个闭环。当前帧算出的曝光参数,会影响下一帧的RAW输入。所以AE算法必须做平滑处理,防止画面亮度来回跳变。
嗯,关于平滑处理,我习惯用IIR滤波器:
// 曝光参数平滑
float smooth_factor = 0.3f; // 平滑系数,越小越平滑
float current_exp_time = ae_table_entry.exposure_time;
float prev_exp_time = ae_state->last_exposure_time;
float final_exp_time = prev_exp_time * (1 - smooth_factor) + current_exp_time * smooth_factor;
好了,AE算法的核心实现就这些。统计信息提取是基础,权重映射让算法「有重点」,曝光补偿让画面更讨喜,AE Table把复杂的计算变成了简单的查表。这套流程我在三个不同的传感器平台上验证过,效果都还不错。你拿去用的时候,记得根据你的传感器动态范围和镜头F数微调一下Table里的阈值。