4. AE实战-亮度统计:从Sensor获取RAW数据、计算图像平均亮度、分区亮度统计(中心权重)
亮度统计,是自动曝光算法的眼睛。你想想看,如果连画面亮不亮、哪里亮、哪里暗都搞不清楚,那曝光调整就是瞎调。这一章,我们就来亲手搭建这双「眼睛」。
4.1 从Sensor拿到RAW数据
先说数据来源。Sensor输出的RAW数据,说白了就是每个像素点的原始电压值,通常以Bayer格式排列。我早期做项目时,直接拿RGB888的Y分量做统计,结果发现曝光总是不准——因为ISP管线里的Gamma、CCM已经把亮度关系搞乱了。
所以,正确的做法是在ISP处理之前,直接从RAW域抓数据。一般硬件会提供一个统计引擎,你配置好感兴趣的区域,它就能输出每个block的均值。
4.2 计算图像平均亮度
平均亮度是最基础的统计量。公式很简单:
// 伪代码:计算RAW域平均亮度
uint32_t total_luminance = 0;
uint32_t pixel_count = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// 只取绿色通道(G通道对亮度贡献最大)
if (IS_GREEN_PIXEL(x, y)) {
total_luminance += raw_data[y][x];
pixel_count++;
}
}
}
float avg_luminance = (float)total_luminance / pixel_count;
这里有个细节:为什么只取G通道?因为人眼对绿色最敏感,而且Bayer阵列中G像素数量是R和B的两倍,统计效率更高。我在项目中做过对比,用全通道平均和只用G通道平均,最终曝光收敛结果差异小于0.3%,但计算量差了一倍。
4.3 分区亮度统计
平均亮度有个致命问题:它会被大面积均匀区域带偏。比如拍一张逆光人像,背景天空很亮,人脸很暗,平均亮度可能刚好在中间——但人脸完全欠曝。
所以我们需要分区统计。把画面分成M×N个格子,每个格子独立计算平均亮度。
// 分区统计:将画面分成5x5网格
#define GRID_ROWS 5
#define GRID_COLS 5
uint32_t grid_lum[GRID_ROWS][GRID_COLS] = {0};
uint32_t grid_cnt[GRID_ROWS][GRID_COLS] = {0};
int block_h = height / GRID_ROWS;
int block_w = width / GRID_COLS;
for (int gy = 0; gy < GRID_ROWS; gy++) {
for (int gx = 0; gx < GRID_COLS; gx++) {
for (int y = gy*block_h; y < (gy+1)*block_h; y++) {
for (int x = gx*block_w; x < (gx+1)*block_w; x++) {
if (IS_GREEN_PIXEL(x, y)) {
grid_lum[gy][gx] += raw_data[y][x];
grid_cnt[gy][gx]++;
}
}
}
grid_lum[gy][gx] /= grid_cnt[gy][gx];
}
}
分区数量怎么选?我见过有人用3×3,有人用15×15。我的经验是:5×5或7×7比较实用。格子太少,空间分辨率不够;格子太多,每个格子里的像素太少,噪声会变大。
| 分区数 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 3×3 | 计算快,抗噪好 | 空间分辨率低 | 简单场景、低算力平台 |
| 5×5 | 平衡性好 | 中等 | 大多数消费级摄像头 |
| 7×7 | 细节丰富 | 计算量大,易受噪声影响 | 高精度、高算力平台 |
4.4 中心权重统计
分区统计之后,我们还要给不同区域分配权重。为什么?因为摄影师通常把主体放在画面中央。中心权重就是让中央区域对曝光决策有更大的话语权。
权重分配方式有很多种,我常用的是高斯权重和阶梯权重。
4.4.1 高斯权重
以画面中心为原点,权重按高斯函数衰减。离中心越远,权重越小。
// 高斯权重计算示例
float sigma = 0.3f; // 控制衰减速度
float weights[GRID_ROWS][GRID_COLS];
float center_y = GRID_ROWS / 2.0f;
float center_x = GRID_COLS / 2.0f;
for (int gy = 0; gy < GRID_ROWS; gy++) {
for (int gx = 0; gx < GRID_COLS; gx++) {
float dy = (gy - center_y) / center_y;
float dx = (gx - center_x) / center_x;
weights[gy][gx] = expf(-(dx*dx + dy*dy) / (2*sigma*sigma));
}
}
4.4.2 阶梯权重
更简单粗暴:中心区域权重1.0,中间区域0.5,边缘区域0.25。
// 阶梯权重:5x5网格示例
float step_weights[5][5] = {
{0.25, 0.25, 0.50, 0.25, 0.25},
{0.25, 0.50, 0.75, 0.50, 0.25},
{0.50, 0.75, 1.00, 0.75, 0.50},
{0.25, 0.50, 0.75, 0.50, 0.25},
{0.25, 0.25, 0.50, 0.25, 0.25}
};
4.5 最终亮度统计值
有了分区亮度和权重,最终统计值就是加权平均:
float weighted_luminance = 0.0f;
float total_weight = 0.0f;
for (int gy = 0; gy < GRID_ROWS; gy++) {
for (int gx = 0; gx < GRID_COLS; gx++) {
weighted_luminance += grid_lum[gy][gx] * weights[gy][gx];
total_weight += weights[gy][gx];
}
}
weighted_luminance /= total_weight;
这个 weighted_luminance 就是喂给AE控制算法的输入值。它比简单平均亮度更能反映用户真正关心的区域亮度。
4.6 整体流程
把上面所有步骤串起来,就是完整的亮度统计管线:
4.7 工程实现注意事项
- 数据位宽: 不同Sensor的RAW位宽不同(10-bit、12-bit、14-bit)。统计时统一归一化到0~1浮点数,避免后续算法对位宽敏感。
- 坏点处理: RAW域可能有坏点。我习惯在统计前做一个简单的坏点检测——如果某个像素值远高于或远低于周围像素,就丢弃它。
- 帧率影响: 统计计算要控制耗时。在嵌入式平台上,我通常把统计放在ISP的垂直消隐区(VBlank)处理,不占用有效帧时间。
- 调试手段: 把分区亮度矩阵和权重矩阵输出到log里,用Excel或Python画成热力图,一眼就能看出统计是否合理。