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
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.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
4.4 本章知识结构
下面这张图把 HDR 合成进阶的流程串起来了:
4.5 避坑指南
最后分享几个我踩过的坑:
- 权重图归一化顺序:先乘后归一,别先归一后乘。否则权重图会失去相对关系。
- 金字塔层数选择:不是越多越好。我曾经为了追求完美,叠了 10 层金字塔,结果重建出来全是低频伪影。一般 5 层足够。
- 色调映射参数:全局映射的 key 值(平均亮度映射目标)取 0.18 是经典值,但实际场景可以微调。暗调场景取 0.1,亮调场景取 0.3,效果更好。
- 颜色恢复:色调映射后一定要做颜色校正。我习惯在映射后加一个简单的饱和度增强:
result = (result - 0.5) * 1.2 + 0.5。
嗯,这一章的内容就到这儿。权重图、融合算法、色调映射,这三块是 HDR 合成的核心。你把这些吃透了,大部分场景都能搞定。