4、HDR合成算法(一):基于曝光融合的Mertens算法、拉普拉斯金字塔分解、权重图计算
好,咱们进入HDR合成的核心环节。前面几章我们讲了如何采集多帧不同曝光的图像,也聊了如何对齐。现在,终于到了「融合」这一步。
市面上HDR合成算法很多,但Mertens算法是我个人最常用的一种。为什么?因为它简单、高效,而且效果非常自然。说白了,它不需要计算复杂的相机响应曲线,也不需要生成真正的HDR图像再做色调映射。它直接在LDR域就把事情办了。
核心思想: 对每一帧输入图像,计算三个质量指标——对比度、饱和度、曝光度。然后把这些指标组合成权重图,指导多帧图像的融合。权重高的像素保留更多细节,权重低的像素被抑制。
4.1 Mertens算法总览
Mertens算法在2007年提出,全称是「Exposure Fusion」。它的流程非常清晰:
- 输入: 多帧不同曝光的LDR图像(已对齐)
- 权重计算: 对每一帧,逐像素计算对比度、饱和度、曝光度三个指标
- 权重归一化: 将三指标组合成最终权重,并做归一化
- 金字塔分解: 对每一帧图像和对应的权重图,分别构建拉普拉斯金字塔和高斯金字塔
- 逐层融合: 在金字塔的每一层,用权重图的高斯金字塔加权融合图像的金字塔
- 重建: 从融合后的金字塔重建最终HDR结果
嗯,这里要注意:为什么需要金字塔?直接加权平均不行吗?我刚开始做的时候也这么想,结果发现直接加权会在边缘产生明显的拼接痕迹。金字塔分解能有效避免这个问题。
4.2 权重图计算:三个核心指标
权重图是Mertens算法的灵魂。每一帧图像,我们都要计算三个指标,然后组合成一个权重值。我习惯用下面的公式:
W(i,j) = C(i,j)^wc * S(i,j)^ws * E(i,j)^we
其中wc、ws、we是指数权重,控制每个指标的影响力。默认值我一般设为1、1、1,但具体场景可以调。
4.2.1 对比度(Contrast)
对比度衡量的是图像局部区域的纹理丰富程度。高对比度的区域通常包含更多细节,应该赋予更高权重。
计算方法很简单:对灰度图应用拉普拉斯滤波器,取值。
// 对比度计算示例(伪代码)
float computeContrast(Image img, int i, int j) {
// 拉普拉斯算子 [-1 -1 -1; -1 8 -1; -1 -1 -1]
float lap = 8 * gray(i,j)
- gray(i-1,j-1) - gray(i-1,j) - gray(i-1,j+1)
- gray(i,j-1) - gray(i,j+1)
- gray(i+1,j-1) - gray(i+1,j) - gray(i+1,j+1);
return abs(lap);
}
我的经验: 对比度权重对纹理丰富的场景(如树叶、布料)特别有效。但要注意,拉普拉斯算子对噪声敏感。如果输入图像有噪点,建议先做轻度高斯滤波。
4.2.2 饱和度(Saturation)
饱和度衡量颜色的鲜艳程度。高饱和度的像素通常更「可信」,因为过曝或欠曝区域往往颜色发白或发黑,饱和度很低。
我常用的计算方式:
// 饱和度计算
float computeSaturation(Image img, int i, int j) {
float R = img.r(i,j), G = img.g(i,j), B = img.b(i,j);
float mean = (R + G + B) / 3.0f;
// 标准差作为饱和度指标
float std = sqrt((R-mean)^2 + (G-mean)^2 + (B-mean)^2);
return std;
}
你想想看,一张过曝的天空照片,R、G、B都接近255,标准差几乎为0,饱和度权重自然就低。这正好符合我们的需求。
4.2.3 曝光度(Well-Exposedness)
曝光度指标用来惩罚过曝和欠曝的像素。我们希望保留那些曝光适中的区域。
我习惯用高斯函数来建模:
// 曝光度计算
float computeExposure(float value, float sigma) {
// value 是归一化到[0,1]的亮度值
// sigma 控制容忍范围,我常用0.2
return exp(-0.5 * pow((value - 0.5) / sigma, 2));
}
这个函数在value=0.5时取最大值1,向两端衰减。sigma越小,对曝光偏差越敏感。
| 指标 | 物理含义 | 计算方式 | 典型权重指数 |
|---|---|---|---|
| 对比度 C | 局部纹理丰富度 | 拉普拉斯滤波取值 | wc = 1.0 |
| 饱和度 S | 颜色鲜艳程度 | RGB标准差 | ws = 1.0 |
| 曝光度 E | 亮度适中程度 | 高斯函数(μ=0.5, σ=0.2) | we = 1.0 |
4.3 权重归一化与组合
计算出三个指标后,需要组合成最终的权重图。注意,每个像素的权重在所有帧之间必须归一化,保证和为1。
// 权重组合与归一化
for each pixel (i,j) {
float totalWeight = 0;
for each frame k {
float C = contrast[k][i][j];
float S = saturation[k][i][j];
float E = exposure[k][i][j];
W[k][i][j] = pow(C, wc) * pow(S, ws) * pow(E, we);
totalWeight += W[k][i][j];
}
// 归一化
for each frame k {
W[k][i][j] /= totalWeight;
}
}
我曾经踩过的坑: 如果某一帧某个像素的三个指标都接近0,归一化后会出现除零问题。一定要加一个极小值epsilon(比如1e-6)做保护。另外,权重图直接做加权平均会产生边缘伪影,这就是为什么我们需要金字塔分解。
4.4 拉普拉斯金字塔分解
好,现在权重图有了,图像也有了。为什么不直接加权平均?
直接加权平均的问题在于:权重图在边缘处变化剧烈,而图像内容在边缘处也有高频变化。两者相乘会产生「振铃效应」和「拼接痕迹」。
拉普拉斯金字塔的思路是:把图像分解成不同频段的子带,然后在每个频段分别融合。低频部分用平滑的权重,高频部分用锐利的权重——但金字塔结构天然保证了过渡自然。
4.4.1 高斯金字塔构建
先对图像做高斯滤波和下采样,得到一系列分辨率递减的图像。每一层是上一层的1/4大小。
// 高斯金字塔构建
vector<Image> buildGaussianPyramid(Image img, int levels) {
vector<Image> pyramid;
pyramid.push_back(img);
for (int l = 1; l < levels; l++) {
Image blurred = gaussianBlur(pyramid[l-1], 5x5 kernel);
Image downsampled = downsample(blurred, 2);
pyramid.push_back(downsampled);
}
return pyramid;
}
4.4.2 拉普拉斯金字塔构建
拉普拉斯金字塔存储的是「细节层」——每一层是高斯金字塔当前层与上一层的上采样版本之差。
// 拉普拉斯金字塔构建
vector<Image> buildLaplacianPyramid(Image img, int levels) {
vector<Image> gaussPyramid = buildGaussianPyramid(img, levels);
vector<Image> laplacianPyramid;
for (int l = 0; l < levels-1; l++) {
Image upsampled = upsample(gaussPyramid[l+1], 2);
Image detail = gaussPyramid[l] - upsampled;
laplacianPyramid.push_back(detail);
}
// 最后一层保留高斯金字塔的最顶层(低频残差)
laplacianPyramid.push_back(gaussPyramid[levels-1]);
return laplacianPyramid;
}
4.4.3 权重图的高斯金字塔
注意:权重图不需要拉普拉斯金字塔,只需要高斯金字塔。因为权重图是平滑的,高频信息对它没有意义。
// 对权重图构建高斯金字塔
vector<Image> weightGaussPyramid = buildGaussianPyramid(weightMap, levels);
4.5 逐层融合与重建
现在,我们有了每一帧的拉普拉斯金字塔,以及每一帧权重图的高斯金字塔。融合过程非常简单:
// 逐层融合
for each level l {
for each pixel (i,j) {
fusedLaplacian[l][i][j] = 0;
for each frame k {
fusedLaplacian[l][i][j] +=
weightGaussPyramid[k][l][i][j] * laplacianPyramid[k][l][i][j];
}
}
}
// 重建
Image result = fusedLaplacian[levels-1]; // 从最顶层开始
for (int l = levels-2; l >= 0; l--) {
result = upsample(result, 2) + fusedLaplacian[l];
}
重建过程就是从顶层到底层,逐层上采样并加上细节层。最终得到的就是融合后的HDR结果。
为什么金字塔融合效果好? 因为权重图的高斯金字塔在每一层都是平滑的,而图像的拉普拉斯金字塔在每一层保留了该频段的细节。两者相乘,相当于用平滑的权重去调制不同频段的细节,不会产生突变。这就是多分辨率融合的精髓。
4.6 实践中的参数调优
Mertens算法有几个关键参数可以调。我根据自己的项目经验,整理了一个参考表:
| 参数 | 默认值 | 调优方向 | 我的建议 |
|---|---|---|---|
| 对比度指数 wc | 1.0 | 增大→纹理更突出 | 风景照可以提到1.2 |
| 饱和度指数 ws | 1.0 | 增大→色彩更鲜艳 | 人像建议降到0.8,避免肤色过饱和 |
| 曝光度指数 we | 1.0 | 增大→更偏好中间曝光 | 夜景可以降到0.5,保留更多暗部 |
| 金字塔层数 | log2(min(H,W)) | 层数越多,融合越平滑 | 一般5-8层足够,太多会模糊 |
| 曝光度sigma | 0.2 | 增大→容忍度更高 | 高动态场景用0.25 |
我的调试习惯: 先固定金字塔层数为6,三个指数都设为1,跑一遍看看效果。然后根据结果微调。如果天空出现光晕,降低曝光度指数;如果地面纹理模糊,提高对比度指数。每次只调一个参数,方便观察变化。