4. AE实战:基于统计信息的AE代码实现与场景策略
自动曝光(AE)这东西,说白了就是让摄像头拍出来的画面亮度刚刚好。太亮过曝,太暗欠曝,都不行。我刚开始做ISP的时候,以为AE就是调个增益完事,后来发现根本不是这么回事——不同场景下,曝光策略天差地别。
这一章,咱们直接上手写代码。我会用Python实现一个基于统计信息的AE引擎,然后聊聊逆光、夜景、高动态这三种典型场景该怎么处理。
4.1 AE的核心:统计信息从哪来?
AE算法要干活,首先得知道当前画面亮不亮。这个信息从哪来?从图像传感器输出的统计信息来。
典型的ISP硬件会提供以下几种统计信息:
| 统计类型 | 含义 | 用途 |
|---|---|---|
| 全局亮度均值 | 整帧像素亮度的平均值 | 基础曝光判断 |
| 分区亮度直方图 | 将画面分成MxN块,每块统计亮度分布 | 局部曝光调整、权重计算 |
| 亮度直方图 | 整帧的亮度分布(0~255各有多少像素) | 过曝/欠曝检测 |
| 饱和度统计 | 高亮区域像素占比 | 防止过曝 |
嗯,这里要注意:分区统计才是AE的灵魂。全局均值太粗糙了,逆光场景下背景亮、人脸暗,均值可能刚好在128,但人脸早就黑成一片了。
核心思路:AE不是让整帧亮度都到目标值,而是让感兴趣区域的亮度到目标值。分区统计就是为了找到这个“感兴趣区域”。
4.2 基于统计信息的AE代码实现
咱们直接上代码。我会实现一个简化版的AE引擎,包含三个核心模块:统计信息采集、亮度评估、曝光参数调整。
import numpy as np
from typing import List, Tuple
class SimpleAE:
"""基于统计信息的自动曝光引擎"""
def __init__(self, target_luminance: float = 120.0,
rows: int = 15, cols: int = 15):
"""
初始化AE引擎
Args:
target_luminance: 目标亮度值(0~255)
rows, cols: 分区统计的行列数
"""
self.target_luminance = target_luminance
self.rows = rows
self.cols = cols
# 曝光参数
self.exposure_time = 30.0 # 毫秒
self.analog_gain = 1.0
self.digital_gain = 1.0
# 权重矩阵:默认中心区域权重高
self.weight_map = self._create_weight_map()
# 历史记录,用于平滑
self.history = []
def _create_weight_map(self) -> np.ndarray:
"""创建分区权重矩阵,中心区域权重更高"""
w = np.ones((self.rows, self.cols))
center_r, center_c = self.rows // 2, self.cols // 2
for r in range(self.rows):
for c in range(self.cols):
dist = np.sqrt((r - center_r)**2 + (c - center_c)**2)
# 高斯权重,中心区域权重高
w[r, c] = np.exp(-dist / (self.rows * 0.3))
return w / w.sum() # 归一化
def collect_statistics(self, frame: np.ndarray) -> dict:
"""
采集统计信息
Args:
frame: 输入图像,shape=(H, W),灰度图
Returns:
统计信息字典
"""
H, W = frame.shape
block_h, block_w = H // self.rows, W // self.cols
# 分区亮度矩阵
zone_luminance = np.zeros((self.rows, self.cols))
for r in range(self.rows):
for c in range(self.cols):
block = frame[r*block_h:(r+1)*block_h,
c*block_w:(c+1)*block_w]
zone_luminance[r, c] = block.mean()
# 全局直方图
hist, _ = np.histogram(frame.flatten(), bins=256, range=(0, 256))
# 过曝/欠曝比例
over_exposed = (frame > 240).mean()
under_exposed = (frame < 16).mean()
return {
'zone_luminance': zone_luminance,
'global_mean': frame.mean(),
'histogram': hist,
'over_exposed_ratio': over_exposed,
'under_exposed_ratio': under_exposed
}
def evaluate_brightness(self, stats: dict) -> float:
"""
评估当前亮度,返回加权亮度值
使用分区权重计算感兴趣区域的亮度
"""
weighted_lum = (stats['zone_luminance'] * self.weight_map).sum()
return weighted_lum
def adjust_exposure(self, stats: dict) -> Tuple[float, float, float]:
"""
根据统计信息调整曝光参数
Returns:
(exposure_time, analog_gain, digital_gain)
"""
current_lum = self.evaluate_brightness(stats)
# 计算需要的亮度调整比例
ratio = self.target_luminance / (current_lum + 1e-6)
# 限制调整幅度,防止震荡
ratio = np.clip(ratio, 0.5, 2.0)
# 曝光策略:先调曝光时间,再调增益
if ratio < 1.0:
# 画面太亮,优先降低曝光时间
new_time = self.exposure_time * ratio
new_time = max(new_time, 1.0) # 最小1ms
new_gain = self.analog_gain
else:
# 画面太暗,先增加曝光时间
new_time = self.exposure_time * ratio
if new_time > 60.0: # 超过60ms可能产生运动模糊
new_time = 60.0
# 剩余亮度靠增益补偿
remaining = ratio / (new_time / self.exposure_time)
new_gain = self.analog_gain * remaining
new_gain = min(new_gain, 16.0) # 最大增益16x
else:
new_gain = self.analog_gain
# 更新参数(带平滑)
alpha = 0.3 # 平滑系数,防止突变
self.exposure_time = (1 - alpha) * self.exposure_time + alpha * new_time
self.analog_gain = (1 - alpha) * self.analog_gain + alpha * new_gain
return self.exposure_time, self.analog_gain, self.digital_gain
def process_frame(self, frame: np.ndarray) -> dict:
"""
处理一帧图像,返回曝光参数
这是AE的主入口函数
"""
# 1. 采集统计信息
stats = self.collect_statistics(frame)
# 2. 评估亮度
current_lum = self.evaluate_brightness(stats)
# 3. 调整曝光
exp_time, ag, dg = self.adjust_exposure(stats)
# 4. 记录历史
self.history.append({
'frame_mean': stats['global_mean'],
'weighted_lum': current_lum,
'exposure_time': exp_time,
'analog_gain': ag
})
return {
'exposure_time': exp_time,
'analog_gain': ag,
'digital_gain': dg,
'weighted_luminance': current_lum,
'over_exposed': stats['over_exposed_ratio'],
'under_exposed': stats['under_exposed_ratio']
}
代码要点:
- 权重矩阵:默认中心权重高,适合人脸在中间的场景。逆光场景需要调整权重。
- 平滑系数alpha:0.3是我常用的值,太快会闪烁,太慢跟不上场景变化。
- 曝光时间上限60ms:这是我个人的经验值,超过这个值手持拍摄容易糊。
4.3 不同场景下的曝光策略
同一个AE算法,在不同场景下表现天差地别。我做过一个项目,室内效果完美,拿到室外逆光场景直接翻车。后来我总结出三种典型场景的应对策略。
4.3.1 逆光场景
逆光时,背景很亮,主体(通常是脸)很暗。全局均值可能正常,但人脸已经欠曝了。
策略:人脸优先曝光
- 检测人脸区域:通过人脸检测模块获取人脸框位置。
- 修改权重矩阵:将人脸区域的权重提高,背景区域权重降低。
- 目标亮度适当提高:人脸区域的目标亮度可以设到140~160,比正常场景高一些。
def set_face_weight(self, face_rects: List[Tuple[int, int, int, int]]):
"""
根据人脸区域设置权重
Args:
face_rects: 人脸框列表,每个元素为(x, y, w, h)
"""
# 重置权重为均匀分布
new_weight = np.ones((self.rows, self.cols))
for (fx, fy, fw, fh) in face_rects:
# 将人脸框映射到分区坐标
r_start = int(fy / self.frame_height * self.rows)
r_end = int((fy + fh) / self.frame_height * self.rows)
c_start = int(fx / self.frame_width * self.cols)
c_end = int((fx + fw) / self.frame_width * self.cols)
# 人脸区域权重设为5倍
new_weight[r_start:r_end, c_start:c_end] *= 5.0
self.weight_map = new_weight / new_weight.sum()
避坑指南:我曾经在逆光场景下把权重全压在人脸上,结果背景过曝到惨白。后来加了过曝保护——如果背景区域过曝比例超过10%,就适当降低人脸权重,保证背景不过曝。
4.3.2 夜景场景
夜景的难点是亮度不够,而且噪声大。单纯提高增益会引入大量噪声。
策略:长曝光 + 低增益
| 参数 | 白天 | 夜景 |
|---|---|---|
| 曝光时间 | 1~30 ms | 30~200 ms |
| 模拟增益 | 1x~4x | 1x~2x(优先用长曝光) |
| 数字增益 | 1x | 1x~2x(尽量不用) |
| 目标亮度 | 100~130 | 80~100(暗一点反而噪声少) |
我个人的习惯是:夜景下先拉长曝光时间到极限(比如200ms),如果还不够亮,再慢慢加模拟增益。数字增益能不用就不用,那玩意儿放大噪声太明显。
def night_mode_adjust(self, stats: dict) -> Tuple[float, float, float]:
"""夜景模式下的曝光调整"""
current_lum = self.evaluate_brightness(stats)
ratio = self.target_luminance / (current_lum + 1e-6)
# 夜景策略:优先增加曝光时间
new_time = self.exposure_time * ratio
# 曝光时间上限放宽到200ms
if new_time <= 200.0:
new_gain = self.analog_gain
else:
new_time = 200.0
# 剩余部分用增益补偿,但限制增益不超过4x
remaining = ratio / (200.0 / self.exposure_time)
new_gain = min(self.analog_gain * remaining, 4.0)
# 平滑更新
alpha = 0.2 # 夜景下平滑更慢,防止闪烁
self.exposure_time = (1 - alpha) * self.exposure_time + alpha * new_time
self.analog_gain = (1 - alpha) * self.analog_gain + alpha * new_gain
return self.exposure_time, self.analog_gain, self.digital_gain
4.3.3 高动态场景
高动态场景(HDR)是最头疼的——画面里既有很亮的窗户,又有很暗的角落。单帧曝光根本搞不定。
策略:多帧融合 + 局部曝光
这里我讲两种主流做法:
- 多帧HDR:拍三帧(短曝光、中曝光、长曝光),然后融合。短帧保留高亮细节,长帧保留暗部细节。
- 局部自适应曝光:不同区域用不同曝光参数。这个需要硬件支持,但效果更好。
对于多帧HDR,AE要做的是:
- 根据场景动态范围,决定短帧和长帧的曝光时间。
- 短帧目标亮度:60~80(保证高亮区域不过曝)
- 长帧目标亮度:160~200(保证暗部提亮)
- 中帧:正常曝光,作为融合的参考帧。
def hdr_exposure_plan(self, stats: dict) -> dict:
"""
生成HDR多帧曝光方案
根据直方图判断动态范围,决定短帧和长帧的曝光时间
"""
hist = stats['histogram']
# 计算暗部和亮部像素比例
dark_ratio = hist[:64].sum() / hist.sum()
bright_ratio = hist[192:].sum() / hist.sum()
# 判断是否需要HDR
if dark_ratio > 0.3 and bright_ratio > 0.3:
# 高动态场景,启用HDR
normal_time = self.exposure_time
# 短帧:正常曝光的 1/4
short_time = normal_time * 0.25
# 长帧:正常曝光的 4倍
long_time = min(normal_time * 4.0, 200.0)
return {
'mode': 'hdr',
'short_exposure': short_time,
'normal_exposure': normal_time,
'long_exposure': long_time,
'short_gain': 1.0,
'long_gain': 1.0
}
else:
# 动态范围正常,用单帧
return {
'mode': 'normal',
'exposure_time': self.exposure_time,
'analog_gain': self.analog_gain
}
4.4 AE策略选择流程图
下面这张图总结了不同场景下AE策略的选择逻辑。我在项目中就是按这个流程走的,基本能覆盖90%以上的场景。
4.5 实战中的避坑指南
代码写完了,策略也定了,但实际跑起来还是会遇到各种问题。我把自己踩过的坑列出来,你们能少走弯路。
坑1:曝光震荡
我曾经遇到一个场景,AE在亮暗之间来回跳,画面一闪一闪的。原因是平滑系数太小,或者目标亮度设得太死。解决方案:
- 增加平滑系数(alpha调到0.1~0.2)
- 设置死区(亮度在目标值±5%范围内不调整)
- 限制单次调整幅度(ratio限制在0.8~1.2)
坑2:场景切换响应慢
从室内走到室外,AE半天反应不过来。这是因为平滑系数太大,或者曝光时间范围限制太死。解决方案:
- 检测亮度突变(连续两帧亮度变化超过30%),临时增大alpha
- 放宽曝光时间范围,让AE能快速调整
坑3:人脸检测不准导致曝光异常
有一次人脸检测模块把背景里的海报误检成人脸,结果AE对着海报优化曝光,真正的人脸反而黑了。解决方案:
- 对人脸检测结果做置信度过滤
- 结合运动检测,静止区域权重降低
- 保留一部分背景权重,防止完全依赖人脸
4.6 小结
这一章我们做了三件事:
- 写了AE代码:基于分区统计的AE引擎,包含权重矩阵、平滑策略、曝光参数调整。
- 讲了三种场景策略:逆光(人脸优先)、夜景(长曝光低增益)、高动态(多帧融合)。
- 总结了实战经验:曝光震荡、场景切换慢、人脸误检,这些坑我都踩过。
AE这东西,理论不难,难的是调参和场景适配。我建议你们拿着代码去跑几个真实场景,看看不同参数下效果差多少。只有亲手调过,才能真正理解AE的脾气。