曝光融合基础:多曝光图像序列与Mertens算法

做HDR相关项目时,我经常被问到同一个问题:“拍了好几张不同亮度的照片,怎么才能合成一张完美的?”

这个问题,说白了就是曝光融合要解决的核心。今天我们就来聊聊这个基础话题。

多曝光图像序列:你拍到的“光”并不完整

先说说什么是多曝光图像序列。这个概念其实很简单——对同一个场景,用不同的曝光时间拍一组照片。

举个例子。你站在一个窗户前,窗外是蓝天白云,室内是暗调的家具。单张照片要么窗外过曝一片白,要么室内欠曝一片黑。为什么?因为相机传感器的动态范围有限,大概10-12档,而真实场景可能超过20档。

所以我们需要拍一组照片:

  • 欠曝:保留高光细节(天空的云彩)
  • 正常曝光:保留中间调
  • 过曝:保留暗部细节(室内的纹理)

我习惯用EV(曝光值)步长来控制。一般拍3-5张,EV步长设为1或2。比如-2EV、0EV、+2EV。场景动态范围越大,需要的张数越多。

我的经验:拍夜景时,我建议至少拍5张。暗部噪点多,多几张过曝的能有效降噪。我曾经在拍城市天际线时只拍了3张,结果暗部全是噪点,后期根本救不回来。

曝光融合的目标:不是简单的“取平均”

很多人以为曝光融合就是把几张照片叠在一起取平均。嗯,如果你真这么干,结果会非常糟糕——重影、模糊、细节丢失,样样都来。

真正的目标是:从每张输入图像中提取“最好”的部分,然后无缝拼接成一张图。

什么叫“最好”?有三个标准:

标准 含义 为什么重要
对比度 局部纹理的清晰程度 高对比度区域通常细节丰富
饱和度 颜色的鲜艳程度 过曝/欠曝区域饱和度低,质量差
良好曝光度 像素值接近中间调(0.5左右) 极端亮或暗的像素信息量少

说白了,我们要找的是“信息量最大”的像素。天空部分从欠曝图里取,暗部从过曝图里取,中间调从正常曝光图里取。

经典Mertens融合算法:一个优雅的解决方案

2007年,Mertens等人提出了一种方法,至今仍是曝光融合的标杆。我第一次读到这篇时,心里想的是:“原来可以这么简单又有效?”

算法的核心思路就三步:

  1. 计算质量权重图:对每张输入图像,计算每个像素的对比度、饱和度和良好曝光度,组合成一个权重图。
  2. 多分辨率分解:把图像和权重图都分解成拉普拉斯金字塔和高斯金字塔。
  3. 逐层融合:在金字塔的每一层,用权重图加权融合,最后重建出结果图。

为什么要用金字塔?直接加权不行吗?

我踩过这个坑。直接加权会出现明显的拼接痕迹——天空和暗部的交界处有一条线,特别难看。金字塔融合能平滑过渡,因为它在不同尺度上做融合,低频部分平滑,高频部分保留细节。

核心公式:融合后的金字塔系数 = Σ(权重金字塔系数 × 拉普拉斯金字塔系数)

下面这张图展示了整个流程:

Mertens曝光融合算法流程 输入图像 1 输入图像 2 输入图像 N 对比度 + 饱和度 + 良好曝光度 对比度 + 饱和度 + 良好曝光度 对比度 + 饱和度 + 良好曝光度 权重图 1 权重图 2 权重图 N 拉普拉斯金字塔 拉普拉斯金字塔 拉普拉斯金字塔 高斯金字塔(权重) 高斯金字塔(权重) 高斯金字塔(权重) 逐层加权融合 融合结果

权重计算:三个质量指标的融合

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。否则融合结果会偏亮或偏暗。我曾经忘记归一化,结果图像亮得像曝光过度,排查了半天才发现问题。

多分辨率融合:为什么金字塔是必须的

直接加权融合的问题在于权重边界处的突变。想象一下,天空部分主要来自欠曝图,地面部分主要来自过曝图。在交界处,权重从1突然变成0,融合结果就会出现一条明显的接缝。

金字塔融合解决了这个问题:

  • 高斯金字塔:对权重图做低通滤波,让权重平滑过渡
  • 拉普拉斯金字塔:保留图像的细节信息
  • 逐层融合:在每一层,用平滑后的权重去融合细节

说白了,大尺度上的过渡是平滑的,小尺度上的细节是保留的。这就是Mertens算法优雅的地方。

一句话总结: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)
使用建议:实际项目中,我推荐用OpenCV的createMergeMertens函数。它经过了高度优化,处理4K图像也很快。但理解上面的代码能帮你深入掌握原理。

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

  • 输入图像要对齐:手持拍摄时,一定要做图像配准。否则会出现重影。我建议用三脚架,或者后期用光流法对齐。
  • 权重归一化不能省:少归一化一次,结果就偏色。我吃过这个亏。
  • 金字塔层数要够:层数太少,融合痕迹明显。一般取log2(min(H, W))层。
  • 注意数据类型:输入要是float [0,1],uint8会丢失精度。