4. AE算法核心——亮度统计:直方图统计、区域权重划分、亮度均值与标准差计算、基于统计的曝光决策逻辑
各位好,欢迎来到第四章。
前面我们聊了AE的整体框架,也看了曝光时间、增益、光圈这些硬件参数怎么配合。但有个核心问题一直没解决——相机怎么知道当前画面是亮还是暗?
你可能会说:“这还不简单?算个平均亮度不就行了?”
嗯,我当年刚入行时也是这么想的。直到我在一个项目中做逆光场景的调试,发现平均亮度明明很“正常”,但人脸却黑成一团。从那以后我才明白——亮度统计,绝不是算个平均值那么简单。
今天我们就来拆解AE的“眼睛”:直方图统计、区域权重划分、亮度均值与标准差,以及最终的曝光决策逻辑。
4.1 直方图统计——画面的“像素分布地图”
直方图是什么?说白了,就是统计画面里每个亮度级别有多少像素。
假设我们把亮度从0(纯黑)到255(纯白)分成256个区间。直方图就是告诉你:亮度为0的像素有多少个,亮度为1的有多少个……一直到255。
H[k] = count(Pixel(x,y) == k),其中 k ∈ [0, 255]
我在项目中习惯把直方图分成三个区域:
- 暗部(0-85):阴影、暗角、黑色物体
- 中间调(86-170):主体、人脸、纹理
- 亮部(171-255):高光、天空、白色物体
为什么要分区域?因为不同场景的“理想直方图”完全不一样。
| 场景类型 | 直方图特征 | AE策略倾向 |
|---|---|---|
| 正常光照 | 中间调集中,两侧平滑衰减 | 保持当前曝光 |
| 逆光 | 暗部堆积,亮部有小峰 | 增加曝光补偿 |
| 过曝 | 亮部溢出(堆积在255附近) | 降低曝光 |
| 夜景 | 暗部堆积,亮部稀疏 | 增加增益/曝光时间 |
直方图统计的代码实现其实不复杂:
// 伪代码:直方图统计
uint32_t histogram[256] = {0};
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
uint8_t brightness = getPixelBrightness(frame, x, y);
histogram[brightness]++;
}
}
4.2 区域权重划分——凭什么中心区域更重要?
你想想看,一张照片里,边角区域和中心区域的重要性一样吗?
显然不一样。绝大多数用户关注的是画面中心的主体——人脸、产品、标志物。如果边角过曝或欠曝,用户可能不太在意;但中心区域一旦出问题,这张照片就废了。
所以,我们需要区域权重划分。
常见的权重划分方式有两种:
4.2.1 中心权重矩阵
给画面划分成 M×N 个网格,每个网格分配一个权重值。中心区域的权重高,边缘的权重低。
// 5x5 中心权重矩阵示例
float weight[5][5] = {
{0.2, 0.4, 0.5, 0.4, 0.2},
{0.4, 0.8, 1.0, 0.8, 0.4},
{0.5, 1.0, 1.5, 1.0, 0.5},
{0.4, 0.8, 1.0, 0.8, 0.4},
{0.2, 0.4, 0.5, 0.4, 0.2}
};
4.2.2 人脸/主体检测权重
更高级的做法——先做人脸检测或主体检测,然后给检测到的区域分配更高的权重。
加权平均亮度的计算公式:
// 加权平均亮度
float weighted_mean = 0.0f;
float total_weight = 0.0f;
for (int y = 0; y < grid_h; y++) {
for (int x = 0; x < grid_w; x++) {
float block_brightness = getBlockMean(frame, x, y);
weighted_mean += block_brightness * weight[y][x];
total_weight += weight[y][x];
}
}
weighted_mean /= total_weight;
4.3 亮度均值与标准差——不只是“平均”那么简单
均值告诉你画面整体是亮是暗,但标准差告诉你画面的对比度。
举个例子:
- 一张纯灰卡的照片:均值=128,标准差≈0
- 一张明暗分明的风景照:均值=128,标准差≈60
你看,均值完全一样,但两张照片的“感觉”天差地别。
标准差的计算公式:
// 计算亮度标准差
float mean = getWeightedMean(frame, weight_matrix);
float variance = 0.0f;
int total_pixels = width * height;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
float diff = getPixelBrightness(frame, x, y) - mean;
variance += diff * diff;
}
}
variance /= total_pixels;
float stddev = sqrt(variance);
我个人习惯把均值和标准差结合起来,形成一个亮度状态向量:
| 均值范围 | 标准差范围 | 状态判断 | AE动作 |
|---|---|---|---|
| 80-140 | 40-70 | 正常 | 保持 |
| <80 | <30 | 欠曝且平 | 大幅提亮 |
| <80 | >50 | 暗部有细节 | 适度提亮 |
| >180 | <30 | 过曝且平 | 大幅压暗 |
| >180 | >50 | 高光有细节 | 适度压暗 |
4.4 基于统计的曝光决策逻辑
好了,现在我们有了直方图、区域权重、均值和标准差。怎么用这些数据来做曝光决策?
我把它总结成一个三步流程:
- 计算当前亮度指标:加权均值、标准差、直方图分布
- 与目标亮度比较:目标亮度通常设在100-120(8位数据),具体看场景
- 计算曝光调整量:用PID控制器或查表法决定调整方向和幅度
下面是一个简化的决策逻辑:
// 曝光决策逻辑(简化版)
float target_brightness = 110.0f; // 目标亮度
float current_brightness = getWeightedMean(frame, weight);
float error = target_brightness - current_brightness;
// 死区处理:误差小于阈值就不调整
if (fabs(error) < 5.0f) {
return; // 保持当前曝光
}
// 计算曝光调整量(P控制器)
float kp = 0.8f; // 比例系数
float adjustment = error * kp;
// 限制调整步长,防止画面闪烁
if (adjustment > 15.0f) adjustment = 15.0f;
if (adjustment < -15.0f) adjustment = -15.0f;
// 应用调整到曝光参数
applyExposureAdjustment(adjustment);
更完整的决策逻辑还会考虑:
- 直方图溢出检测:如果亮部或暗部有大量像素溢出,优先处理溢出
- 场景切换检测:如果亮度突然大幅变化,可能是场景切换,需要快速响应
- 时间平滑:对调整量做指数平滑,避免画面闪烁
4.5 本章知识体系总览
下面这张图总结了亮度统计与曝光决策的完整流程:
嗯,到这里,亮度统计的核心内容就讲完了。直方图给了你全局分布,权重划分让你聚焦重点区域,均值和标准差帮你判断画面状态,最后决策逻辑把这些信息转化成具体的曝光调整。