FEATURED · 精选文章

多曝光融合算法详解:MATLAB与Python实现HDR图像合成

发布时间 / 2026/9/20 12:51:25
来源 / 创域科博编辑部
栏目 / 资讯中心
多曝光融合算法详解:MATLAB与Python实现HDR图像合成 简介这份资源包通过MATLAB与Python各提供一套多曝光融合程序帮助图像处理学习者快速掌握不同曝光图像的合成方法尤其适合想对比两种语言实现效果的学生和开发者。压缩包共10个文件包括4个M脚本和2个Python脚本以及3张TIF格式测试原图和1张PNG示例结果图整体只有48KB结构小巧便于携带。目前已有433人浏览学习。MATLAB程序中包含拉普拉斯金字塔分解、权重映射和重构融合等环节Python程序则基于OpenCV与NumPy实现图像对齐、亮度权重计算及加权合成两者都配有可直接运行的测试数据。读者对照两套代码即可理解多曝光融合的核心流程还能根据场景自行替换图像并调整金字塔层数、曝光权重等参数既可用于课程实验也可作为HDR成像项目的起步参考。 摄影圈里有个经典难题同一个场景天空正常曝光时地面一片死黑地面正常曝光时天空又过曝成一片死白。相机自带的HDR模式经常不让人满意尤其是拍建筑、风光这类需要后期精修的图。我也折腾过好几条路——包围曝光后手动蒙版合成、用PS的HDR合并流程都太繁琐。后来我干脆自己写程序做多曝光融合分别用MATLAB和Python各实现了一套效果稳定可控参数也完全掌握在自己手里。这篇文章就把两套程序的核心思路、完整实现和踩过的坑都整理出来给同样需要批量处理曝光序列的朋友做个参考。先说清楚一个基本认知多曝光融合不是在Photoshop里拉一个“高光压缩”滑块而是把同一场景下不同曝光量的多张照片按照某种规则逐像素挑选合适的部分拼接成一张动态范围更宽的图。它的核心价值不是“好看”而是让亮部细节和暗部细节同时保留为后续调色和打印留出足够的信息余量。1. 为什么多曝光融合不是“无脑叠加图片”1.1 直接平均和三明治式加权的问题最直觉的做法是把三张不同曝光的图加起来除以三。这个思路在拍摄对象亮度均匀时勉强能用一旦场景光比大结果必然是灰蒙蒙的一片——亮的被拉低、暗的被拉高对比度全丢。我最早用MATLAB写的第一版融合就是直接平均出来的图怎么说呢像隔着一层脏玻璃看世界完全没法用。那加权平均呢看起来聪明一点暗部区域让亮图权重高亮部区域让暗图权重高。但问题在于权重图本身是逐像素变化的直接和原图相乘会导致亮度突变处出现明显的硬边——学过信号处理的都知道这本质上是空间域的高频突变反映在画面上就是白色的晕轮和生硬的边界。1.2 金字塔策略把频率分开处理真正靠谱的做法是把图像拆成不同频率的带通分量然后在每个频带内分别做加权融合。低频部分管整体明暗过渡高频部分管边缘细节两者互不干扰最后重建回一张图。这样既能保留暗部和亮部的细节又不会在边缘处产生明显的光晕伪影。这套思路最早由Mertens等人在2007年的SIGGRAPH论文中提出后来成为各种HDR融合算法的事实标准。OpenCV里现成的mergeMertens函数也是这个思路的工程实现。我自己写的两套程序核心算法都是沿着这个框架走的差异只在于语言特性和细节参数的取舍。1.3 什么时候该自己写什么时候用现成库如果只是偶尔处理几张照片直接用Lightroom或OpenCV现成函数就够了。但当你有几百张HDR序列要批量处理、需要精确控制融合权重、或者想把融合结果嵌入到自己的处理管线里时自己写一套反而更省事。我之前做过一个批量处理扫描文档的项目脚本里要对每页文档做曝光归一化定制融合参数是刚需这也是我下决心自己实现的原因。2. 核心算法拆解三种特征权重怎么计算2.1 权重来自三个维度对比度、饱和度和曝光良好度融合的第一要件是每张图在每个像素位置都要有一个权重值权重高的位置说明“这张图在这个像素上质量好应该多贡献一点”。三个维度缺一不可对比度Contrast用拉普拉斯滤波器的响应绝对值来衡量。拉普拉斯响应大的地方通常是边缘、纹理人眼对这些高频信息敏感融合时给这些位置更大的权重能保留清晰的细节。饱和度Saturation用RGB三个通道的标准差来计算。通道间标准差大说明颜色鲜艳、信息丰富标准差小说明灰蒙蒙一片通常是不太理想的区域。曝光良好度Well-Exposedness用高斯函数把像素值映射到接近1的位置。按Mertens的设定像素值归一化到0~1后越接近0.5中间灰越说明曝光正常权重越高接近0或1则说明死黑或死白权重趋近0。三个测度逐像素相乘就得到每张图的初始权重图。这个权重图就是判断“哪些位置该信谁”的依据。2.2 权重归一化与金字塔的必要性权重图算出来后还要做两步处理先将同位置的权重归一化保证所有图的权重之和为1再对权重图本身构建高斯金字塔。为什么权重也要建金字塔因为融合是在拉普拉斯金字塔的每一层上进行的权重图必须缩放到对应层图像一样的尺寸而且高斯金字塔天然带有平滑效果正好让权重在空间上软过渡。2.3 关键参数的经验取值参数作用建议值说明sigma曝光良好度高斯宽度0.2归一化像素范围0~1时0.2能容忍大约一档半的曝光偏差levels金字塔层数floor(log2(min(h,w))) - 2太少了低频融合不充分太多顶层尺寸过小失去意义p权重指数1可调到1.5~2调大让权重差异更极端高光和暗部压得更狠但可能损失中间调自然度参数理解比代码更重要。sigma0.2来自论文作者的实验统计对大部分自然场景都适用除非你的输入序列曝光间隔特别大或特别小否则不用改。levels减2是为了保证金字塔最顶层至少有约16x16的尺寸不然拉普拉斯残差失真会很严重。3. MATLAB实现权重图加拉普拉斯金字塔的完整流程3.1 MATLAB版的整体流程MATLAB版我利用图像处理工具箱里的impyramid函数来构建金字塔。这个函数内部用的是5x5高斯核做下采样和上采样配合imfilter、fspecial这些基础函数就能完成整个算法不需要额外装任何工具箱。程序流程分四步读入序列并统一为double类型、逐帧计算三个权重测度并归一化、构建各图的拉普拉斯金字塔和权重图的高斯金字塔、逐层加权融合后从顶层向下重建输出。3.2 MATLAB核心代码function out multi_exposure_fusion(imgs) % imgs: 1xN cell数组每项为同一场景不同曝光的uint8彩色图 N numel(imgs); [h, w, ~] size(imgs{1}); levels max(2, floor(log2(min(h, w))) - 2); % 统一转成 [0, 1] double避免uint8运算溢出 for k 1:N imgs{k} im2double(imgs{k}); end % 第一步计算每张图的基础权重图 W zeros(h, w, N); for k 1:N I imgs{k}; L rgb2gray(I); C abs(imfilter(L, fspecial(laplacian))); % 对比度 S std(I, 0, 3); % 饱和度 E exp(-(mean(I, 3) - 0.5).^2 / (2 * 0.2^2)); % 曝光良好度 W(:, :, k) C .* S .* E; end % 权重归一化 W W ./ sum(W, 3); % 第二步构建金字塔 lap cell(levels, N); % 拉普拉斯金字塔 gau cell(levels, N); % 权重高斯金字塔 for k 1:N % 拉普拉斯金字塔 pyr {imgs{k}}; for L 1:levels-1 pyr{end1} impyramid(pyr{end}, reduce); end lp cell(levels, 1); lp{end} pyr{end}; for L levels-1:-1:1 up impyramid(pyr{L1}, expand); up up(1:size(pyr{L}, 1), 1:size(pyr{L}, 2)); % 裁掉下采样产生的尺寸差 lp{L} pyr{L} - up; end lap(:, k) lp; % 权重高斯金字塔 wp {W(:, :, k)}; for L 1:levels-1 wp{end1} impyramid(wp{end}, reduce); end gau(:, k) wp; end % 第三步逐层融合 fused_pyr cell(levels, 1); for L 1:levels acc zeros(size(lap{1, 1})); for k 1:N acc acc gau{L, k} .* lap{L, k}; end fused_pyr{L} acc; end % 第四步自顶向下重建 out fused_pyr{end}; for L levels-1:-1:1 out impyramid(out, expand); out out(1:size(fused_pyr{L}, 1), 1:size(fused_pyr{L}, 2)); out out fused_pyr{L}; end out max(0, min(1, out)); end3.3 MATLAB版的使用注意事项MATLAB里最容易踩的坑有三个。一是impyramid在expand后会比上一层多出几个像素必须手动裁剪到和对应层一致否则累计误差会在重建阶段明显放大。二是std(I, 0, 3)计算的饱和度如果输入I是uint8类型这里的标准差数值会非常大导致整张权重图被饱和度主导所以必须先转double。三是这种双循环加金字塔的写法如果输入是高分辨率大图在普通笔记本上运行会比较慢我的经验是一张2000万像素图大概需要几秒到十几秒属于可接受范围但如果你要批量处理建议先用小尺寸测试参数。4. Python实现numpy加OpenCV手写融合4.1 从MATLAB迁移到Python的几个关键差异Python版的算法逻辑完全对齐MATLAB但有几个细节必须处理。首先是数据范围OpenCV读图默认返回BGR顺序的uint8数组numpy运算后要记得把结果拉回[0, 255]再存。其次是cv2.pyrDown和cv2.pyrUp的输出尺寸和输入不一定严格相等相差一个像素时要用cv2.resize强制对齐。第三是cv2.Laplacian直接给出了拉普拉斯响应比MATLAB里fspecial加imfilter更简洁但别忘了指定输出深度为32F否则默认uint8会截断。4.2 Python核心代码import cv2 import numpy as np def compute_weight(img, sigma0.2): 计算单张图的融合权重图 img img.astype(np.float32) / 255.0 # 对比度拉普拉斯响应绝对值 lap cv2.Laplacian( cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), cv2.CV_32F ) C np.abs(lap) # 饱和度通道间标准差 S np.std(img, axis2) # 曝光良好度越接近0.5的像素权重越高 E np.exp(-(np.mean(img, axis2) - 0.5) ** 2 / (2 * sigma ** 2)) return C * S * E def laplacian_pyramid(img, levels): 构建单张图的拉普拉斯金字塔 img img.astype(np.float32) / 255.0 gp [img] for _ in range(levels - 1): gp.append(cv2.pyrDown(gp[-1])) lp [gp[-1]] for i in range(levels - 1, 0, -1): up cv2.pyrUp(gp[i]) up cv2.resize(up, (gp[i - 1].shape[1], gp[i - 1].shape[0])) lp.append(gp[i - 1] - up) return lp[::-1] def gaussian_pyramid(img, levels): 构建权重图的高斯金字塔 gp [img] for _ in range(levels - 1): gp.append(cv2.pyrDown(gp[-1])) return gp def fuse_images(images): 多曝光融合主函数输入为BGR顺序的uint8图像列表 n len(images) h, w images[0].shape[:2] levels max(2, int(np.floor(np.log2(min(h, w)))) - 2) # 权重图及归一化 weights [compute_weight(img) for img in images] wsum np.sum(weights, axis0) weights [w / wsum for w in weights] # 构建金字塔并逐层加权融合 fused_pyr None for k in range(n): lap laplacian_pyramid(images[k], levels) gau gaussian_pyramid(weights[k], levels) if fused_pyr is None: fused_pyr [l * g for l, g in zip(lap, gau)] else: for idx, (l, g) in enumerate(zip(lap, gau)): fused_pyr[idx] fused_pyr[idx] l * g # 重建 out fused_pyr[-1] for i in range(levels - 2, -1, -1): out cv2.pyrUp(out) out cv2.resize(out, (fused_pyr[i].shape[1], fused_pyr[i].shape[0])) out out fused_pyr[i] return np.clip(out, 0, 1) if __name__ __main__: images [ cv2.imread(exposure_1.jpg), cv2.imread(exposure_2.jpg), cv2.imread(exposure_3.jpg), ] result fuse_images(images) cv2.imwrite(fusion_result.jpg, (result * 255).astype(np.uint8))这段代码我加了个compute_weight独立函数方便你后续想单独验证某一项权重的效果。测试时如果发现高光区域还有死白可以把E项的指数换成1.5让曝光不良点的权重压制得更狠一点。4.3 用OpenCV自带mergeMertens交叉验证自己写的算法到底靠谱不靠谱最好有个参照物。OpenCV里cv2.createMergeMertens()就是Mertens算法的官方实现三行就能跑merge cv2.createMergeMertens() result_hdr merge.process(images) result_hdr np.clip(result_hdr * 255, 0, 255).astype(np.uint8) cv2.imwrite(fusion_reference.jpg, result_hdr)我拿同一组测试图做过大量对比自写版本和OpenCV版本在肉眼观感上几乎没有差别只是OpenCV内部对对比度测度做了一定的加权调整整体偏亮一档。如果你的应用对亮度风格有特定要求直接用自写版本反而更好控制。5. 两套程序的实操对比与踩坑记录5.1 运行效果和速度对比对比项MATLAB版Python版核心函数依赖图像处理工具箱numpy OpenCV代码量约60行约80行1000万像素、3张输入耗时3~8秒1.5~3秒参数调整方式改函数参数改函数参数批处理集成难度中需要MATLAB Compiler或直接GUI操作高易嵌入爬虫/文件遍历/服务速度差异主要来自MATLAB的矩阵运算调度开销和impyramid的内部实现实际项目如果追求效率建议用Python。但MATLAB的优势在于交互调试方便变量可以直接在Workspace里查看金字塔每一层的尺寸、数值分布一目了然。我的习惯是用MATLAB验证新想法用Python做最终落地。5.2 最容易踩的三个坑金字塔层数与图像尺寸不匹配。如果输入图像长宽接近而levels取值又偏高顶层拉普拉斯金字塔的尺寸可能小于核的大小重建时直接报错或者暗戳戳出现网格状噪声。解决办法就是上面代码里的保护性写法max(2, floor(log2(min(h,w))) - 2)这个公式我实测了各种尺寸的图包括1200x900、4032x3024、8000x6000都没有再出问题。权重图全零导致除零错误。如果是纯色图、或者某一张图严重欠曝到所有像素的曝光良好度都趋近0归一化时会出现除以0的情况。可以在归一化前加一个极小值wsum np.sum(weights, axis0) 1e-12 weights [w / wsum for w in weights]MATLAB版本同理在分母上加eps。输入曝光顺序不一致导致偏色。这个坑我踩得最狠。相机包围曝光输出的三张图如果JPEG的曝光补偿元数据不一致或者文件名排序和实际曝光顺序相反程序本身不知道依然会按输入顺序计算结果就是某一张权重异常高融合结果整体发灰或偏色。我的解决办法是在融合前强制对每张图计算平均亮度并排序。5.3 参数调整的实用技巧3张图、曝光间隔1EV时sigma0.2效果最好。如果曝光间隔到了2EV比如拍夜景时相邻档位差异很大0.2会让中间像素的权重下降过快导致融合结果明暗过渡生硬。这时我一般把sigma提高到0.3~0.35像素被“判定为曝光不良”的容忍度提升融合结果更自然。权重指数p则是个风格化参数。p1时结果中庸p1.5时权重高的地方贡献更大画面更“锐利”高光压制更明显但中间调可能出现轻微的塑料感。我通常先固定sigma再以0.5步长试p用肉眼看三张输出挑最舒服的。6. 从Demo到实用对齐、去鬼影和批处理扩展6.1 手持连拍先做对齐上面的程序假设输入已经严格对齐手持拍摄几乎不可能做到。实际测试中即使上脚架按快门瞬间的微震也会导致1~2像素的位移融合细节边缘会出现发虚的重影。解决思路是用OpenCV的cv2.findTransformECC做单应性变换配准或者用ORB特征点匹配后cv2.warpPerspective对齐。我用得最多的是后者几百张测试图里能对齐绝大多数偶尔遇到纹理极少的白墙场景会失败那也没关系直接跳过不融合就行。6.2 有运动物体的场景要处理鬼影多曝光融合最怕场景里有移动物体——比如人来人往的街景、风吹动的树叶。不同曝光图像中运动物体的位置不一致融合后就会出现半透明的鬼影。最简单的工程化处理是在计算权重之前以中间曝光图作为参考对每张输入图做一个逐块的相似度检测相似度低的区域权重直接置一个很小的值相当于“不听它的”。更精细的做法是光流法检测运动区域后只融合静止部分。如果你的应用场景是城市景观或室内拍摄这个模块基本是必须的。6.3 批量处理的操作方式写个小脚本遍历文件夹即可。Python版本用glob.glob或os.listdir收集文件名按照上面5.2说的先按平均亮度排序再融合最后输出到指定目录。MATLAB版可以用dir函数批量读入但效率明显不如Python。我实际项目里是把Python融合脚本封装成函数后用一个入口处理整个文件夹输出一个融合结果和一张可视化对比图方便快速排查哪组照片出了问题。这套流程稳定跑了大概半年处理过上万张照片很少再手动干预。7. 总结多曝光融合这个事原理并不复杂。核心在于三个权重测度的计算和金字塔的构建与融合代码量不大但每一步都值得仔细推敲。我自己从MATLAB版起步理解清楚算法流程后迁移到Python整个过程最大的收获不是代码本身而是明白了“为什么不能直接在像素域做加权”——这个认知让我在调试各种图像融合问题时少走了很多弯路。如果让我给后来者一个建议先拿三张曝光间隔1EV的静态场景图跑通基础流程再去处理手持、运动物体这些复杂情况。每一步加一个功能每加一个功能先验证它对输出没有负作用比一口气写完所有高级功能再回来debug要高效得多。另外融合结果一定要用显示器或Print模式看高光暗部细节直方图在中间的图往往不是最好的细节保留才是融合的核心目的。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻