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]++;
    }
}
我的经验:实际项目中不会逐帧全图统计,太慢了。我一般用下采样后的缩略图(比如1/16分辨率)来做统计,精度损失很小,但速度能快十几倍。

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);
关键洞察:标准差低(<30)说明画面“平”,可能是雾天、过曝或欠曝。标准差高(>70)说明画面有层次感,但也可能意味着动态范围过大。

我个人习惯把均值和标准差结合起来,形成一个亮度状态向量:

均值范围 标准差范围 状态判断 AE动作
80-140 40-70 正常 保持
<80 <30 欠曝且平 大幅提亮
<80 >50 暗部有细节 适度提亮
>180 <30 过曝且平 大幅压暗
>180 >50 高光有细节 适度压暗

4.4 基于统计的曝光决策逻辑

好了,现在我们有了直方图、区域权重、均值和标准差。怎么用这些数据来做曝光决策?

我把它总结成一个三步流程:

  1. 计算当前亮度指标:加权均值、标准差、直方图分布
  2. 与目标亮度比较:目标亮度通常设在100-120(8位数据),具体看场景
  3. 计算曝光调整量:用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);
避坑指南:我曾经在项目中直接用全量PID,结果画面像“呼吸”一样来回震荡。后来加了死区(dead zone)和输出限幅,才稳定下来。记住——AE不是要精确到1个亮度单位,而是要“看起来舒服”。

更完整的决策逻辑还会考虑:

  • 直方图溢出检测:如果亮部或暗部有大量像素溢出,优先处理溢出
  • 场景切换检测:如果亮度突然大幅变化,可能是场景切换,需要快速响应
  • 时间平滑:对调整量做指数平滑,避免画面闪烁

4.5 本章知识体系总览

下面这张图总结了亮度统计与曝光决策的完整流程:

亮度统计与曝光决策流程 原始图像帧 ① 直方图统计(256级亮度分布) ② 区域权重划分(中心/人脸权重) ③ 亮度指标计算 加权均值 | 标准差 | 直方图分布特征 ④ 曝光决策逻辑 死区判断 | PID控制 | 步长限幅 | 时间平滑 曝光参数调整量 关键参数 • 目标亮度:100-120 • 死区阈值:±5 • 最大步长:±15 避坑要点 • 避免全量PID震荡 • 场景切换快速响应 • 人脸消失防跳变

嗯,到这里,亮度统计的核心内容就讲完了。直方图给了你全局分布,权重划分让你聚焦重点区域,均值和标准差帮你判断画面状态,最后决策逻辑把这些信息转化成具体的曝光调整。