3、AE算法核心——测光与统计:测光模式与RAW域亮度统计
各位好,欢迎来到第三章。
上一章我们聊了AE的整体框架,说白了就是“看多亮→算多亮→调多亮”。但这里有个关键问题——“看多亮”到底怎么看?
你拿一个相机对着同一个场景,拍出来的亮度可能完全不同。为什么?因为测光的方式不一样。我早年调试一个安防摄像头时,就遇到过这种情况:画面里有一扇窗户,室内正常,窗外过曝。客户说“你们算法不行啊”。其实不是算法不行,是测光模式没选对。
这一章,我们就来拆解AE的“眼睛”——测光与统计。
3.1 测光模式:三种主流策略
测光模式,本质上就是“如何给画面不同区域分配权重”。ISP里常见的就三种:全局测光、中心加权测光、点测光。我按实际工程中的使用频率来聊。
3.1.1 全局测光(Average / Global Metering)
这个最简单。把整帧图像的所有像素亮度加起来,取个平均值。每个像素权重都一样。
优点:计算量极小,硬件实现简单。适合光照均匀的场景,比如阴天、室内均匀照明。
缺点:一旦场景里有大面积亮区或暗区,结果就偏。比如逆光人像,背景天空很亮,全局平均下来人脸就黑了。
我记得有一次做车载环视项目,四个摄像头拼接。用全局测光,结果车左侧是阴影、右侧是阳光,拼接缝处亮度跳变严重。后来不得不换成区域加权。
3.1.2 中心加权测光(Center-Weighted Metering)
这个模式认为:“画面中心最重要”。给中心区域分配高权重,边缘区域低权重。权重分布通常是一个高斯函数或者圆形渐变。
为什么这么设计?因为大多数摄影场景,主体都在画面中央。人像、物体特写,都适用。
我个人的习惯是:在手机摄像头默认模式下,中心加权测光是最稳妥的选择。它比全局测光更智能,又比点测光更宽容。
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)。它做三件事:
- 分块(Gridding):把整帧图像分成M×N个块。比如15×15、32×32。每个块就是一个统计单元。
- 亮度计算:对每个块内的像素,计算亮度值。对于Bayer数据,常用的亮度公式是:
Y = 0.299*R + 0.587*G + 0.114*B。但注意,每个像素只有一个颜色通道,所以需要插值或者用相邻像素补全。 - 直方图统计:对每个块,统计亮度直方图(比如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 测光与统计的完整流程
我把整个流程画成一张图,方便你理解数据流向。
3.5 避坑指南:我踩过的几个坑
最后,分享几个实际项目中遇到的坑。希望能帮你省点调试时间。
- 坑一:Bayer模式搞错。 我曾经把BGGR当成RGGB,结果R和B通道互换,亮度计算偏色严重。后来加了Bayer模式检测,才解决。
- 坑二:分块大小不合适。 块太大,细节丢失;块太小,噪声大。我一般建议16×16或32×32,具体看传感器分辨率和AE响应速度要求。
- 坑三:忽略黑电平。 RAW域数据有黑电平偏移。如果不减去黑电平,暗区的亮度统计会偏高,导致AE欠曝。记得先做黑电平校正,再做统计。
- 坑四:测光模式切换时的亮度跳变。 从全局切到点测光,如果目标区域亮度突变,画面会闪一下。我通常加一个时间域滤波,让目标亮度平滑过渡。