3、AE算法核心——测光与统计:测光模式与RAW域亮度统计

各位好,欢迎来到第三章。

上一章我们聊了AE的整体框架,说白了就是“看多亮→算多亮→调多亮”。但这里有个关键问题——“看多亮”到底怎么看?

你拿一个相机对着同一个场景,拍出来的亮度可能完全不同。为什么?因为测光的方式不一样。我早年调试一个安防摄像头时,就遇到过这种情况:画面里有一扇窗户,室内正常,窗外过曝。客户说“你们算法不行啊”。其实不是算法不行,是测光模式没选对。

这一章,我们就来拆解AE的“眼睛”——测光与统计。

3.1 测光模式:三种主流策略

测光模式,本质上就是“如何给画面不同区域分配权重”。ISP里常见的就三种:全局测光、中心加权测光、点测光。我按实际工程中的使用频率来聊。

3.1.1 全局测光(Average / Global Metering)

这个最简单。把整帧图像的所有像素亮度加起来,取个平均值。每个像素权重都一样。

优点:计算量极小,硬件实现简单。适合光照均匀的场景,比如阴天、室内均匀照明。

缺点:一旦场景里有大面积亮区或暗区,结果就偏。比如逆光人像,背景天空很亮,全局平均下来人脸就黑了。

我记得有一次做车载环视项目,四个摄像头拼接。用全局测光,结果车左侧是阴影、右侧是阳光,拼接缝处亮度跳变严重。后来不得不换成区域加权。

3.1.2 中心加权测光(Center-Weighted Metering)

这个模式认为:“画面中心最重要”。给中心区域分配高权重,边缘区域低权重。权重分布通常是一个高斯函数或者圆形渐变。

为什么这么设计?因为大多数摄影场景,主体都在画面中央。人像、物体特写,都适用。

我个人的习惯是:在手机摄像头默认模式下,中心加权测光是最稳妥的选择。它比全局测光更智能,又比点测光更宽容。

工程经验:中心加权的权重半径通常设为画面短边的1/3到1/2。太窄了接近点测光,太宽了退化成全局测光。

3.1.3 点测光(Spot Metering)

点测光只取画面中一个很小的区域(通常1%~5%的面积)来计算亮度。这个区域可以固定,也可以跟随对焦点移动。

什么时候用?场景中明暗反差极大,你只关心某个特定区域的曝光。比如舞台追光下的演员、月亮特写。

我曾经调试过一款工业检测相机,需要拍摄电路板上的焊点。焊点很小,周围是深色基板。用全局测光,焊点总是过曝。换成点测光,对准焊点区域,问题立刻解决。

注意:点测光非常“敏感”。如果测光点选错了位置,或者目标移动了,画面亮度会剧烈抖动。在自动模式下,我建议配合人脸检测或主体跟踪一起用。

3.2 三种测光模式对比

我把它们放在一张表里,方便你对照选型。

特性 全局测光 中心加权测光 点测光
权重分布 均匀 中心高、边缘低 单点集中
计算复杂度 极低 低 低
适用场景 均匀光照 主体居中 高反差、小目标
抗干扰能力 弱 中 强(但易抖动)
硬件实现 累加器即可 需权重表 需坐标选择

3.3 从RAW域获取亮度统计信息

好了,测光模式选定了。下一步就是:亮度数据从哪来?

很多初学者会直接拿RGB图像算亮度。但我要告诉你——在ISP Pipeline里,亮度统计通常在RAW域完成。为什么?

  • 早:RAW域在Pipeline最前端,越早拿到统计,AE响应越快。
  • 准:RAW数据是线性的,没有经过Gamma、色调映射等非线性处理。线性数据算平均亮度,物理意义更明确。
  • 省:RAW域分辨率通常比RGB域低(Bayer模式),统计计算量更小。

嗯,这里要注意:RAW域的数据是Bayer格式的。每个像素只包含R/G/B中的一个通道。所以你不能直接把它当灰度图用。

3.3.1 亮度统计的硬件实现

ISP硬件里,通常有一个专门的统计引擎(Statistics Engine)。它做三件事:

  1. 分块(Gridding):把整帧图像分成M×N个块。比如15×15、32×32。每个块就是一个统计单元。
  2. 亮度计算:对每个块内的像素,计算亮度值。对于Bayer数据,常用的亮度公式是:Y = 0.299*R + 0.587*G + 0.114*B。但注意,每个像素只有一个颜色通道,所以需要插值或者用相邻像素补全。
  3. 直方图统计:对每个块,统计亮度直方图(比如256 bins)。同时计算平均亮度、最大/最小亮度。

我参与过一个项目,统计引擎输出的是每个块的平均亮度和权重。AE算法拿到这些数据后,再根据测光模式做加权平均。这样硬件和软件分工明确,效率很高。

3.3.2 代码示例:RAW域亮度统计(伪代码)

下面是一个简化的伪代码,展示如何从Bayer RAW数据中计算每个块的亮度统计。实际硬件会并行处理,这里只体现逻辑。

// 假设输入:Bayer RAW图像,格式为BGGR
// 输出:每个block的平均亮度

#define BLOCK_SIZE 16  // 16x16像素一个块
#define GRID_W (IMAGE_WIDTH / BLOCK_SIZE)
#define GRID_H (IMAGE_HEIGHT / BLOCK_SIZE)

float block_luma[GRID_H][GRID_W];

for (int by = 0; by < GRID_H; by++) {
    for (int bx = 0; bx < GRID_W; bx++) {
        uint32_t sum_r = 0, sum_g = 0, sum_b = 0;
        uint32_t cnt_r = 0, cnt_g = 0, cnt_b = 0;

        // 遍历block内的每个像素
        for (int y = by * BLOCK_SIZE; y < (by+1) * BLOCK_SIZE; y++) {
            for (int x = bx * BLOCK_SIZE; x < (bx+1) * BLOCK_SIZE; x++) {
                // 根据Bayer模式判断颜色通道
                if ((y % 2 == 0) && (x % 2 == 0)) { // B
                    sum_b += raw_data[y][x];
                    cnt_b++;
                } else if ((y % 2 == 0) && (x % 2 == 1)) { // G1
                    sum_g += raw_data[y][x];
                    cnt_g++;
                } else if ((y % 2 == 1) && (x % 2 == 0)) { // G2
                    sum_g += raw_data[y][x];
                    cnt_g++;
                } else { // R
                    sum_r += raw_data[y][x];
                    cnt_r++;
                }
            }
        }

        // 计算block内各通道平均值
        float avg_r = (float)sum_r / cnt_r;
        float avg_g = (float)sum_g / cnt_g;
        float avg_b = (float)sum_b / cnt_b;

        // 亮度公式:Y = 0.299*R + 0.587*G + 0.114*B
        block_luma[by][bx] = 0.299f * avg_r + 0.587f * avg_g + 0.114f * avg_b;
    }
}
小技巧:实际工程中,为了节省硬件乘法器,亮度公式常用整数近似:Y = (77*R + 150*G + 29*B) >> 8。精度损失很小,但硬件面积能省不少。

3.4 测光与统计的完整流程

我把整个流程画成一张图,方便你理解数据流向。

测光与统计流程图 测光与统计完整流程 Bayer RAW 输入 分块 (M×N Grid) 每块亮度计算 亮度统计输出 测光模式选择 全局 / 中心加权 / 点测光 加权平均 → 场景亮度 关键点 • RAW域线性数据 • Bayer格式需分通道 • 分块粒度影响精度

3.5 避坑指南:我踩过的几个坑

最后,分享几个实际项目中遇到的坑。希望能帮你省点调试时间。

  • 坑一:Bayer模式搞错。 我曾经把BGGR当成RGGB,结果R和B通道互换,亮度计算偏色严重。后来加了Bayer模式检测,才解决。
  • 坑二:分块大小不合适。 块太大,细节丢失;块太小,噪声大。我一般建议16×16或32×32,具体看传感器分辨率和AE响应速度要求。
  • 坑三:忽略黑电平。 RAW域数据有黑电平偏移。如果不减去黑电平,暗区的亮度统计会偏高,导致AE欠曝。记得先做黑电平校正,再做统计。
  • 坑四:测光模式切换时的亮度跳变。 从全局切到点测光,如果目标区域亮度突变,画面会闪一下。我通常加一个时间域滤波,让目标亮度平滑过渡。
重要提醒:测光模式不是越复杂越好。很多场景下,中心加权测光+合理的权重分布,效果已经足够好。不要为了炫技而过度设计。