4. HDR 合成进阶:权重图生成策略、融合算法与色调映射

HDR 合成做到这一步,其实已经过了「能看」的阶段了。接下来要解决的,是怎么让合成结果「好看」、自然、没有鬼影和光晕。我刚开始做多曝光融合时,踩过不少坑——权重图稍微调不好,天空就出现一圈亮边,或者暗部细节死活拉不出来。今天咱们就把这些核心问题拆开揉碎了讲。

4.1 权重图生成策略:不只是简单加权

权重图决定了每个像素在最终结果里的贡献比例。最简单的做法是根据像素亮度给权重——中间亮度的像素权重高,过曝和欠曝的权重低。但实际场景远没这么简单。

4.1.1 基于质量的权重

我常用的权重因子有三个维度:

权重类型 计算依据 典型场景
对比度权重 拉普拉斯响应值 纹理丰富区域
饱和度权重 颜色通道标准差 色彩鲜艳区域
良好曝光权重 高斯钟形曲线 中间调区域

我个人习惯把这三个权重乘起来,再归一化。举个例子:

def compute_weight(img, mu=0.5, sigma=0.2):
    # 良好曝光权重:高斯钟形
    luminance = 0.299 * img[:,:,0] + 0.587 * img[:,:,1] + 0.114 * img[:,:,2]
    well_exposed = np.exp(-0.5 * ((luminance - mu) / sigma) ** 2)
    
    # 对比度权重:拉普拉斯值
    laplacian = cv2.Laplacian(luminance, cv2.CV_32F)
    contrast = np.abs(laplacian)
    
    # 饱和度权重:通道标准差
    std = np.std(img, axis=2)
    saturation = std / np.max(std)
    
    # 组合
    weight = well_exposed * contrast * saturation
    weight = weight / np.sum(weight)
    return weight
我的经验:sigma 取 0.2 到 0.3 之间效果最好。太小了权重图会变成二值化,过渡不自然;太大了又起不到筛选作用。

4.1.2 边缘感知权重

直接乘出来的权重图有个问题——在物体边缘容易产生光晕。我曾经在拍树叶场景时,合成结果边缘总有一圈亮边,后来发现是权重在边缘处突变导致的。

解决办法:对权重图做引导滤波或双边滤波,让权重在边缘处保持锐利,在平坦区域平滑过渡。

# 引导滤波平滑权重图
weight_smooth = cv2.ximgproc.guidedFilter(
    guide=img, 
    src=weight, 
    radius=8, 
    eps=1e-6
)

4.2 融合算法:拉普拉斯金字塔 vs 梯度域

权重图生成好了,怎么融合?直接加权平均?不行,那样会在边缘产生鬼影和接缝。业界主流方案有两种。

4.2.1 拉普拉斯金字塔融合

这个方法的思路很简单:把每张图分解成不同频率的带通图像,然后对每个频率层分别加权融合,最后重建。

为什么这样做有效?因为人眼对不同频率的敏感度不同——低频(大块颜色)需要平滑过渡,高频(细节纹理)需要保持锐利。金字塔正好能分开处理。

def laplacian_blend(img_list, weight_list, levels=5):
    # 对每张图构建拉普拉斯金字塔
    pyramids = []
    for img in img_list:
        gauss = img.copy()
        lap_pyramid = []
        for i in range(levels):
            next_gauss = cv2.pyrDown(gauss)
            gauss_up = cv2.pyrUp(next_gauss, dstsize=(gauss.shape[1], gauss.shape[0]))
            lap = gauss - gauss_up
            lap_pyramid.append(lap)
            gauss = next_gauss
        lap_pyramid.append(gauss)  # 最底层是高斯残差
        pyramids.append(lap_pyramid)
    
    # 对每层加权融合
    blended = []
    for level in range(levels + 1):
        fused = np.zeros_like(pyramids[0][level])
        for i in range(len(img_list)):
            fused += weight_list[i] * pyramids[i][level]
        blended.append(fused)
    
    # 重建
    result = blended[-1]
    for level in range(levels - 1, -1, -1):
        result = cv2.pyrUp(result, dstsize=(blended[level].shape[1], blended[level].shape[0]))
        result += blended[level]
    return result
关键点:金字塔层数一般取 4-6 层。层数太少,融合痕迹明显;层数太多,计算量大且可能引入低频伪影。

4.2.2 梯度域融合

拉普拉斯金字塔虽然好用,但有个缺点——它本质上还是线性加权,遇到大范围过曝/欠曝区域时,细节恢复能力有限。

梯度域融合的思路更直接:我们不直接融合像素值,而是融合梯度(即像素变化率),然后通过求解泊松方程重建图像。说白了,就是让融合结果在梯度上与源图一致,这样细节保留得最好。

我记得在做一个室内 HDR 项目时,窗外阳光强烈,室内很暗。用金字塔融合,窗外总有点发灰;换成梯度域融合,窗外细节一下就鲜明了。

# 梯度域融合核心步骤(伪代码)
# 1. 计算每张图的梯度场
grad_x = cv2.Sobel(img, cv2.CV_32F, 1, 0)
grad_y = cv2.Sobel(img, cv2.CV_32F, 0, 1)

# 2. 根据权重图选择最大梯度
max_grad_x = np.max([w * gx for w, gx in zip(weights, grad_x_list)], axis=0)
max_grad_y = np.max([w * gy for w, gy in zip(weights, grad_y_list)], axis=0)

# 3. 求解泊松方程重建图像
# 散度 = div(grad_x, grad_y)
divergence = np.gradient(max_grad_x, axis=0) + np.gradient(max_grad_y, axis=1)
# 用迭代法或FFT求解拉普拉斯方程
result = solve_poisson(divergence)
注意:梯度域融合计算量大,实时应用慎用。另外,如果梯度场不一致(比如有运动物体),重建结果可能出现伪影。我一般只在静态场景或后期处理时用。

4.3 色调映射:全局 vs 局部

融合完的图像还是高动态范围的线性数据,要显示到普通屏幕上,必须做色调映射。这一步做不好,前面所有努力都白费。

4.3.1 全局色调映射

全局映射用一个函数把整个图像的亮度范围压缩到 [0,1]。最简单的就是 Reinhard 全局算子:

def reinhard_global(hdr, key=0.18, burn=0.0):
    luminance = 0.2126 * hdr[:,:,0] + 0.7152 * hdr[:,:,1] + 0.0722 * hdr[:,:,2]
    log_avg = np.exp(np.mean(np.log(luminance + 1e-6)))
    scaled = (key / log_avg) * luminance
    mapped = scaled / (1.0 + scaled)
    # 处理颜色
    result = np.zeros_like(hdr)
    for c in range(3):
        result[:,:,c] = (hdr[:,:,c] / luminance) * mapped
    return result

全局映射的优点是快、稳定。缺点是暗部和亮部细节不能兼顾——你想想看,一个函数怎么可能同时压住高光又提亮阴影?

4.3.2 局部色调映射

局部映射会考虑像素周围的亮度分布。比如基于视网膜模型的映射、基于双边滤波的分层映射等。

我常用的方法是:把图像分解成基础层(低频)和细节层(高频),对基础层做全局映射,细节层保留不变,最后合并。这样既压缩了动态范围,又保住了纹理细节。

def local_tone_map(hdr, sigma_s=50, sigma_r=0.4):
    luminance = rgb_to_luminance(hdr)
    # 双边滤波提取基础层
    base = cv2.bilateralFilter(luminance, d=0, sigmaColor=sigma_r, sigmaSpace=sigma_s)
    detail = luminance / (base + 1e-6)
    # 对基础层做全局映射
    base_mapped = reinhard_global(base)
    # 合并
    result_lum = base_mapped * detail
    # 恢复颜色
    result = apply_luminance(hdr, result_lum)
    return result
我的建议:局部映射参数 tuning 很麻烦。sigma_s 控制空间范围,一般取图像宽度的 1/10;sigma_r 控制亮度范围,0.3-0.5 比较安全。调参时先固定一个,调另一个,别同时动。

4.4 本章知识结构

下面这张图把 HDR 合成进阶的流程串起来了:

HDR 合成进阶核心流程 多曝光图像序列 权重图生成 对比度 | 饱和度 | 良好曝光 | 边缘感知 拉普拉斯金字塔 多频带加权融合 梯度域融合 泊松方程重建 直接加权平均 简单但易有鬼影 HDR 融合结果 色调映射 全局映射 | 局部映射(基础层+细节层) LDR 显示图像

4.5 避坑指南

最后分享几个我踩过的坑:

  • 权重图归一化顺序:先乘后归一,别先归一后乘。否则权重图会失去相对关系。
  • 金字塔层数选择:不是越多越好。我曾经为了追求完美,叠了 10 层金字塔,结果重建出来全是低频伪影。一般 5 层足够。
  • 色调映射参数:全局映射的 key 值(平均亮度映射目标)取 0.18 是经典值,但实际场景可以微调。暗调场景取 0.1,亮调场景取 0.3,效果更好。
  • 颜色恢复:色调映射后一定要做颜色校正。我习惯在映射后加一个简单的饱和度增强:result = (result - 0.5) * 1.2 + 0.5。

嗯,这一章的内容就到这儿。权重图、融合算法、色调映射,这三块是 HDR 合成的核心。你把这些吃透了,大部分场景都能搞定。