曝光融合基础:多曝光图像序列与Mertens算法
做HDR相关项目时,我经常被问到同一个问题:“拍了好几张不同亮度的照片,怎么才能合成一张完美的?”
这个问题,说白了就是曝光融合要解决的核心。今天我们就来聊聊这个基础话题。
多曝光图像序列:你拍到的“光”并不完整
先说说什么是多曝光图像序列。这个概念其实很简单——对同一个场景,用不同的曝光时间拍一组照片。
举个例子。你站在一个窗户前,窗外是蓝天白云,室内是暗调的家具。单张照片要么窗外过曝一片白,要么室内欠曝一片黑。为什么?因为相机传感器的动态范围有限,大概10-12档,而真实场景可能超过20档。
所以我们需要拍一组照片:
- 欠曝:保留高光细节(天空的云彩)
- 正常曝光:保留中间调
- 过曝:保留暗部细节(室内的纹理)
我习惯用EV(曝光值)步长来控制。一般拍3-5张,EV步长设为1或2。比如-2EV、0EV、+2EV。场景动态范围越大,需要的张数越多。
曝光融合的目标:不是简单的“取平均”
很多人以为曝光融合就是把几张照片叠在一起取平均。嗯,如果你真这么干,结果会非常糟糕——重影、模糊、细节丢失,样样都来。
真正的目标是:从每张输入图像中提取“最好”的部分,然后无缝拼接成一张图。
什么叫“最好”?有三个标准:
| 标准 | 含义 | 为什么重要 |
|---|---|---|
| 对比度 | 局部纹理的清晰程度 | 高对比度区域通常细节丰富 |
| 饱和度 | 颜色的鲜艳程度 | 过曝/欠曝区域饱和度低,质量差 |
| 良好曝光度 | 像素值接近中间调(0.5左右) | 极端亮或暗的像素信息量少 |
说白了,我们要找的是“信息量最大”的像素。天空部分从欠曝图里取,暗部从过曝图里取,中间调从正常曝光图里取。
经典Mertens融合算法:一个优雅的解决方案
2007年,Mertens等人提出了一种方法,至今仍是曝光融合的标杆。我第一次读到这篇时,心里想的是:“原来可以这么简单又有效?”
算法的核心思路就三步:
- 计算质量权重图:对每张输入图像,计算每个像素的对比度、饱和度和良好曝光度,组合成一个权重图。
- 多分辨率分解:把图像和权重图都分解成拉普拉斯金字塔和高斯金字塔。
- 逐层融合:在金字塔的每一层,用权重图加权融合,最后重建出结果图。
为什么要用金字塔?直接加权不行吗?
我踩过这个坑。直接加权会出现明显的拼接痕迹——天空和暗部的交界处有一条线,特别难看。金字塔融合能平滑过渡,因为它在不同尺度上做融合,低频部分平滑,高频部分保留细节。
下面这张图展示了整个流程:
权重计算:三个质量指标的融合
Mertens算法的关键就在于权重计算。我拆开来讲:
1. 对比度(Contrast)
用拉普拉斯滤波器的响应值来衡量。纹理越丰富,对比度越高。这个指标能帮我们保留边缘和细节。
2. 饱和度(Saturation)
计算RGB三个通道的标准差。颜色越鲜艳,标准差越大。过曝区域颜色发白,欠曝区域颜色发暗,饱和度都低。
3. 良好曝光度(Well-exposedness)
用高斯曲线来衡量。像素值越接近0.5,得分越高。公式是:exp(-(pixel - 0.5)² / (2σ²))。σ一般取0.2。
最终权重 = 对比度^w_c × 饱和度^w_s × 良好曝光度^w_e。三个指数通常都设为1,但你可以调整。
多分辨率融合:为什么金字塔是必须的
直接加权融合的问题在于权重边界处的突变。想象一下,天空部分主要来自欠曝图,地面部分主要来自过曝图。在交界处,权重从1突然变成0,融合结果就会出现一条明显的接缝。
金字塔融合解决了这个问题:
- 高斯金字塔:对权重图做低通滤波,让权重平滑过渡
- 拉普拉斯金字塔:保留图像的细节信息
- 逐层融合:在每一层,用平滑后的权重去融合细节
说白了,大尺度上的过渡是平滑的,小尺度上的细节是保留的。这就是Mertens算法优雅的地方。
代码实现:一个最小可运行版本
下面是我写的一个简化版实现。不依赖OpenCV,只用NumPy和SciPy。适合你理解核心逻辑。
import numpy as np
from scipy.ndimage import gaussian_filter
from skimage.transform import pyramid_laplacian, pyramid_gaussian
def mertens_fusion(images, sigma=0.2, w_c=1.0, w_s=1.0, w_e=1.0, levels=5):
"""
Mertens曝光融合算法实现
参数:
images: 输入图像列表,每个图像形状为 (H, W, 3),值范围 [0, 1]
sigma: 良好曝光度高斯曲线的标准差
w_c, w_s, w_e: 对比度、饱和度、良好曝光度的权重指数
levels: 金字塔层数
返回:
融合后的图像,形状 (H, W, 3)
"""
n = len(images)
H, W = images[0].shape[:2]
# 1. 计算每张图像的权重图
weights = []
for img in images:
# 对比度:拉普拉斯响应
lap = np.abs(gaussian_filter(img, sigma=1.0) - img)
contrast = np.mean(lap, axis=2)
# 饱和度:RGB标准差
mean_rgb = np.mean(img, axis=2, keepdims=True)
saturation = np.std(img - mean_rgb, axis=2)
# 良好曝光度:高斯曲线
well_exp = np.exp(-(img - 0.5)**2 / (2 * sigma**2))
well_exp = np.mean(well_exp, axis=2)
# 组合权重
w = (contrast ** w_c) * (saturation ** w_s) * (well_exp ** w_e)
weights.append(w + 1e-12) # 防止除零
# 归一化权重
weight_sum = sum(weights)
weights = [w / weight_sum for w in weights]
# 2. 多分辨率融合
# 构建每张图像的拉普拉斯金字塔
lap_pyramids = []
for img in images:
lap_pyr = list(pyramid_laplacian(img, levels=levels))
lap_pyramids.append(lap_pyr)
# 构建权重图的高斯金字塔
gauss_pyramids = []
for w in weights:
gauss_pyr = list(pyramid_gaussian(w, levels=levels))
gauss_pyramids.append(gauss_pyr)
# 3. 逐层融合
fused_pyr = []
for level in range(levels):
fused_level = np.zeros_like(lap_pyramids[0][level])
for i in range(n):
# 扩展权重维度以匹配图像通道
w_expanded = np.expand_dims(gauss_pyramids[i][level], axis=2)
fused_level += w_expanded * lap_pyramids[i][level]
fused_pyr.append(fused_level)
# 4. 重建图像(从金字塔最顶层开始)
result = fused_pyr[-1]
for level in range(levels - 2, -1, -1):
# 上采样并叠加
result = gaussian_filter(result, sigma=1.0)
result = result[:fused_pyr[level].shape[0], :fused_pyr[level].shape[1]]
result += fused_pyr[level]
return np.clip(result, 0, 1)
createMergeMertens函数。它经过了高度优化,处理4K图像也很快。但理解上面的代码能帮你深入掌握原理。
避坑指南:我踩过的几个坑
- 输入图像要对齐:手持拍摄时,一定要做图像配准。否则会出现重影。我建议用三脚架,或者后期用光流法对齐。
- 权重归一化不能省:少归一化一次,结果就偏色。我吃过这个亏。
- 金字塔层数要够:层数太少,融合痕迹明显。一般取
log2(min(H, W))层。 - 注意数据类型:输入要是float [0,1],uint8会丢失精度。