资讯动态

Harr小波图像融合实战:OpenCV实现多聚焦图像清晰合成

发布时间:2026/10/15 8:00:01 来源:尧图企业网站定制
1. 从一张拍不清的照片说起为什么需要图像融合前阵子帮朋友处理一组室内场景的照片同一机位拍了两张一张对焦在近处的摆件上细节锐利但背景虚化另一张对焦在远处的墙面上背景清晰但前景糊了。朋友问我能不能把两张“拼”成一张全都清楚的图。这个需求听起来像是修图软件的活儿但本质上它属于图像融合的范畴——把同一场景的多幅图像信息整合成一幅信息更完整的图像。图像融合在工业检测、医学影像、遥感观测里用得非常多。比如红外与可见光图像的融合红外能穿透烟雾看到热源可见光能提供纹理细节两者一合既能看到目标又能看清轮廓。再比如多聚焦图像融合就是我朋友遇到的那种情况不同焦距各拍一张融合后得到全焦图像。而实现融合的方法有很多种从最简单的加权平均到复杂的深度学习模型都有但如果你想要一个原理清晰、代码量可控、效果稳定的方案基于Harr小波分解的融合方法是一个非常值得上手的起点。这篇文章面向的是有一定OpenCV基础、想动手实现图像融合的开发者。我会把Harr小波分解的原理讲透把融合规则的设计逻辑说清楚然后给出完整的OpenCV实现代码最后分享我在调试过程中踩过的坑和总结的参数调节经验。你不需要有信号处理的背景只要会写Python、装过OpenCV就能跟着做出来。2. 为什么选Harr小波方案选型背后的逻辑2.1 图像融合的常见技术路线对比在动手写代码之前先搞清楚市面上有哪些融合方法以及为什么Harr小波值得选。我把常见的几类方法列出来做个对比方法类别代表算法优点缺点适用场景空间域方法加权平均、主成分分析实现简单、速度快容易产生鬼影、对比度下降对融合质量要求不高的快速预览金字塔方法拉普拉斯金字塔多尺度融合、效果较好分解层数受限、重构有误差多聚焦图像融合小波变换方法Harr小波、Daubechies小波多尺度多方向、信息保留好对位移敏感、计算量中等多聚焦、红外可见光融合深度学习方法CNN、GAN效果最好需要大量训练数据、模型大有标注数据的专业场景从表里能看出来小波变换方法处在一个“甜点区”效果比简单的空间域方法好一大截实现难度又比深度学习低得多。而Harr小波又是所有小波里最简单的一种它的滤波器系数只有四个值计算过程就是加减法非常适合用来理解小波分解的本质。2.2 Harr小波分解到底在做什么很多人一听到“小波”就觉得玄乎其实Harr小波的核心思想用一句话就能概括把信号拆成低频的“概貌”和高频的“细节”。拿一维信号举例假设有四个像素值[10, 12, 8, 6]。Harr小波分解的第一步是两两配对求平均和差值平均值(1012)/2 11(86)/2 7差值(10-12)/2 -1(8-6)/2 1这样原来的四个数就变成了[11, 7, -1, 1]。前两个是低频分量概貌后两个是高频分量细节。如果继续对低频分量做同样的操作就得到了多级分解。放到二维图像上情况稍微复杂一点。图像是二维信号所以要做行方向和列方向各一次分解一共得到四个子带LL低频-低频图像的近似概貌包含了大部分能量LH低频-高频水平方向的细节对应垂直边缘HL高频-低频垂直方向的细节对应水平边缘HH高频-高频对角线方向的细节注意不同教材对LH和HL的命名可能相反这里约定第一个字母代表行方向处理结果第二个字母代表列方向。实际写代码时不用纠结名字看子带对应的物理意义就行。这个分解过程可以递归进行。对LL子带再做一次二维Harr分解就得到二级分解结果。分解级数越多频率划分越细但计算量也越大。对于一般的图像融合任务2到3级分解就够用了。2.3 融合规则的设计思路分解完之后关键问题来了怎么把两张图的子带合并成一张图的子带低频子带包含的是图像的亮度和整体结构高频子带包含的是边缘和纹理。这两类信息的重要性不同所以要用不同的融合策略低频子带常用加权平均。因为低频信息差异不大简单平均就能得到平滑的结果。但纯平均会降低对比度所以有时候会用基于局部能量的加权让能量大的区域占更大权重。高频子带常用绝对值取大。高频系数绝对值越大说明该位置的边缘越强、细节越丰富所以取绝对值大的那个系数能最大程度保留清晰区域的细节。这个“低频平均高频取大”的组合是Harr小波融合里最经典也最稳定的策略。我试过其他变体比如低频也取大、高频用阈值收缩效果都不如这个组合稳定。3. 核心细节拆解从原理到可运行的代码3.1 环境准备与依赖确认动手之前先把环境理清楚。这个项目对环境的依赖非常轻量pip install opencv-python numpy matplotlibopencv-python负责图像的读取、显示和基础处理numpy负责矩阵运算小波分解的加减法全靠它matplotlib可选用来对比显示融合前后的图像版本方面没有特殊要求OpenCV 4.x 和 NumPy 1.20 以上都能跑。我实测在 Python 3.8 到 3.11 上都正常。如果你用的是 Anaconda这些库基本都自带了直接跳过安装步骤。提示不要用pip install cv2这个包名是错的正确的包名是opencv-python。我见过不少新手在这里卡住。3.2 Harr小波分解的手动实现OpenCV 本身没有提供小波变换的函数所以这部分需要自己写。好在Harr小波的实现非常直接核心就是切片和加减法。先看一维分解的实现思路。假设输入是一个长度为N的一维数组我们要把它变成N/2个低频系数和N/2个高频系数import numpy as np def harr_1d_decompose(signal): 一维Harr小波分解 signal np.asarray(signal, dtypenp.float32) n len(signal) # 保证长度为偶数 if n % 2 ! 0: signal np.append(signal, signal[-1]) n 1 # 偶数索引和奇数索引配对 even signal[0::2] odd signal[1::2] # 低频平均值高频差值的一半 low (even odd) / 2.0 high (even - odd) / 2.0 return low, high这里有个细节值得说明高频系数为什么除以2因为Harr小波的规范正交基要求能量守恒除以2或者说除以根号2的平方是为了让分解后的总能量和原信号一致。如果你不关心能量守恒只做融合其实不除也能用但重构的时候要对应处理。二维分解就是在一维的基础上先对每一行做分解再对结果的每一列做分解def harr_2d_decompose(img): 二维Harr小波分解返回LL, LH, HL, HH四个子带 img np.asarray(img, dtypenp.float32) h, w img.shape # 处理奇数尺寸 if h % 2 ! 0: img np.vstack([img, img[-1:, :]]) h 1 if w % 2 ! 0: img np.hstack([img, img[:, -1:]]) w 1 # 行方向分解 row_low np.zeros((h, w // 2), dtypenp.float32) row_high np.zeros((h, w // 2), dtypenp.float32) for i in range(h): low, high harr_1d_decompose(img[i, :]) row_low[i, :] low row_high[i, :] high # 列方向分解 LL np.zeros((h // 2, w // 2), dtypenp.float32) LH np.zeros((h // 2, w // 2), dtypenp.float32) HL np.zeros((h // 2, w // 2), dtypenp.float32) HH np.zeros((h // 2, w // 2), dtypenp.float32) for j in range(w // 2): low, high harr_1d_decompose(row_low[:, j]) LL[:, j] low LH[:, j] high low, high harr_1d_decompose(row_high[:, j]) HL[:, j] low HH[:, j] high return LL, LH, HL, HH这段代码里用循环逐行逐列处理逻辑清晰但速度一般。如果你要处理大图或者视频流可以用NumPy的向量化操作重写把循环去掉。不过对于单张图像的融合任务这个速度完全够用一张1080p的图分解一次大概几十毫秒。3.3 融合规则的具体实现分解完成后对两张图的子带分别应用融合规则def fuse_coefficients(LL1, LL2, LH1, LH2, HL1, HL2, HH1, HH2): 低频平均高频绝对值取大 # 低频加权平均 LL_fused (LL1 LL2) / 2.0 # 高频绝对值取大 LH_fused np.where(np.abs(LH1) np.abs(LH2), LH1, LH2) HL_fused np.where(np.abs(HL1) np.abs(HL2), HL1, HL2) HH_fused np.where(np.abs(HH1) np.abs(HH2), HH1, HH2) return LL_fused, LH_fused, HL_fused, HH_fused低频用平均是最稳妥的选择。我试过用最大值、用局部方差加权结果要么对比度异常要么出现块状伪影。平均虽然简单但胜在稳定融合后的图像过渡自然。高频用绝对值取大背后的逻辑是高频系数的绝对值代表该位置边缘的强度。两张图在同一位置一张清晰一张模糊清晰那张的边缘强度一定更大所以取绝对值大的系数就相当于自动选择了清晰区域的信息。3.4 小波重构把子带还原成图像融合完子带后需要做逆变换把系数还原成像素图像。重构是分解的逆过程def harr_1d_reconstruct(low, high): 一维Harr小波重构 n len(low) signal np.zeros(2 * n, dtypenp.float32) signal[0::2] low high signal[1::2] low - high return signal def harr_2d_reconstruct(LL, LH, HL, HH): 二维Harr小波重构 h, w LL.shape # 列方向重构 row_low np.zeros((2 * h, w), dtypenp.float32) row_high np.zeros((2 * h, w), dtypenp.float32) for j in range(w): row_low[:, j] harr_1d_reconstruct(LL[:, j], LH[:, j]) row_high[:, j] harr_1d_reconstruct(HL[:, j], HH[:, j]) # 行方向重构 img np.zeros((2 * h, 2 * w), dtypenp.float32) for i in range(2 * h): img[i, :] harr_1d_reconstruct(row_low[i, :], row_high[i, :]) return img重构的公式推导很简单分解时low (even odd) / 2high (even - odd) / 2那么反解出来就是even low highodd low - high。这就是为什么重构代码里是加和减。注意如果分解时对奇数尺寸做了填充重构后图像会比原图大一行或一列需要裁剪回原始尺寸。这个细节很容易被忽略导致融合结果尺寸对不上。4. 完整实操流程从读图到保存结果4.1 主流程代码组织把前面的函数串起来形成一个完整的融合流程import cv2 import numpy as np def wavelet_fuse(img1_path, img2_path, output_path, levels2): 基于Harr小波分解的图像融合主函数 # 读取图像并转为灰度图 img1 cv2.imread(img1_path, cv2.IMREAD_GRAYSCALE) img2 cv2.imread(img2_path, cv2.IMREAD_GRAYSCALE) if img1 is None or img2 is None: raise ValueError(图像读取失败请检查路径) # 统一尺寸 h min(img1.shape[0], img2.shape[0]) w min(img1.shape[1], img2.shape[1]) img1 cv2.resize(img1, (w, h)).astype(np.float32) img2 cv2.resize(img2, (w, h)).astype(np.float32) # 多级分解 coeffs1 decompose_multilevel(img1, levels) coeffs2 decompose_multilevel(img2, levels) # 逐级融合 fused_coeffs [] for (LL1, LH1, HL1, HH1), (LL2, LH2, HL2, HH2) in zip(coeffs1, coeffs2): fused_coeffs.append(fuse_coefficients(LL1, LL2, LH1, LH2, HL1, HL2, HH1, HH2)) # 多级重构 result reconstruct_multilevel(fused_coeffs) # 裁剪回原始尺寸并保存 result result[:h, :w] result np.clip(result, 0, 255).astype(np.uint8) cv2.imwrite(output_path, result) return result多级分解和重构的辅助函数def decompose_multilevel(img, levels): 多级二维Harr小波分解 coeffs [] current img for _ in range(levels): LL, LH, HL, HH harr_2d_decompose(current) coeffs.append((LL, LH, HL, HH)) current LL # 对低频子带继续分解 return coeffs def reconstruct_multilevel(coeffs): 多级二维Harr小波重构 # 从最深层开始逐级重构 LL, LH, HL, HH coeffs[-1] current harr_2d_reconstruct(LL, LH, HL, HH) for i in range(len(coeffs) - 2, -1, -1): _, LH, HL, HH coeffs[i] current harr_2d_reconstruct(current, LH, HL, HH) return current这里有个容易搞混的地方多级分解时每一级只对上一级的LL子带继续分解但融合和重构时需要把每一级的LH、HL、HH都保留下来。重构时从最深层往上走用当前重构结果作为下一级的LL配合该级保存的LH、HL、HH继续重构。4.2 参数选择与效果验证分解级数levels是唯一需要手动调的参数。我的经验是1级分解融合效果最基础适合差异不大的图像速度快2级分解最常用的选择在效果和速度之间取得平衡3级分解适合大尺寸图像2000像素以上能捕捉更粗粒度的结构信息4级以上收益递减明显而且容易在图像边缘产生振铃效应验证融合效果时我习惯把原图1、原图2、融合结果并排显示import matplotlib.pyplot as plt fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(cv2.imread(img1.jpg, 0), cmapgray) axes[0].set_title(Source 1) axes[1].imshow(cv2.imread(img2.jpg, 0), cmapgray) axes[1].set_title(Source 2) axes[2].imshow(result, cmapgray) axes[2].set_title(Fused) for ax in axes: ax.axis(off) plt.tight_layout() plt.show()看融合结果时重点关注两个地方一是原本模糊的区域是否变清晰了二是清晰区域和模糊区域的交界处有没有明显的拼接痕迹。如果交界处有突兀的亮线或暗线说明融合规则在该区域选择不一致需要调整策略。4.3 彩色图像的处理方式前面的代码都是针对灰度图的。彩色图像的处理有两种思路思路一分通道处理。把BGR三个通道拆开每个通道单独做小波融合最后合并。这种做法实现简单但可能引入颜色失真因为三个通道的融合决策可能不一致。思路二转换到YCrCb空间。只对Y通道亮度做小波融合Cr和Cb通道色度用简单的平均或取第一张图的色度。这种做法能更好地保持颜色一致性是我更推荐的方式。def wavelet_fuse_color(img1_path, img2_path, output_path, levels2): 彩色图像融合Y通道小波融合色度通道平均 img1 cv2.imread(img1_path) img2 cv2.imread(img2_path) h min(img1.shape[0], img2.shape[0]) w min(img1.shape[1], img2.shape[1]) img1 cv2.resize(img1, (w, h)) img2 cv2.resize(img2, (w, h)) # 转换到YCrCb ycbcr1 cv2.cvtColor(img1, cv2.COLOR_BGR2YCrCb).astype(np.float32) ycbcr2 cv2.cvtColor(img2, cv2.COLOR_BGR2YCrCb).astype(np.float32) # Y通道小波融合 y1, cr1, cb1 ycbcr1[:,:,0], ycbcr1[:,:,1], ycbcr1[:,:,2] y2, cr2, cb2 ycbcr2[:,:,0], ycbcr2[:,:,1], ycbcr2[:,:,2] coeffs1 decompose_multilevel(y1, levels) coeffs2 decompose_multilevel(y2, levels) fused_coeffs [] for c1, c2 in zip(coeffs1, coeffs2): fused_coeffs.append(fuse_coefficients(*c1, *c2)) y_fused reconstruct_multilevel(fused_coeffs)[:h, :w] # 色度通道平均 cr_fused (cr1 cr2) / 2.0 cb_fused (cb1 cb2) / 2.0 # 合并并转回BGR ycbcr_fused np.stack([y_fused, cr_fused, cb_fused], axis2) ycbcr_fused np.clip(ycbcr_fused, 0, 255).astype(np.uint8) result cv2.cvtColor(ycbcr_fused, cv2.COLOR_YCrCb2BGR) cv2.imwrite(output_path, result) return result色度通道用平均而不是取大是因为人眼对色度细节不敏感色度上的微小差异不会影响观感但色度不一致反而会让人察觉出颜色异常。5. 常见问题与排查技巧实录5.1 融合结果出现块状伪影这是最常见的问题表现为融合图像上出现规则的方块状纹理。根本原因是高频系数选择不一致在某个局部区域有些位置选了图1的系数有些位置选了图2的系数导致重构后出现不连续的跳变。解决办法有两个。一是改用基于窗口的融合规则不逐像素比较绝对值而是比较以该像素为中心的3x3或5x5窗口内的能量和def fuse_high_freq_window(H1, H2, window_size3): 基于局部窗口能量的高频融合 kernel np.ones((window_size, window_size), dtypenp.float32) energy1 cv2.filter2D(np.abs(H1), -1, kernel) energy2 cv2.filter2D(np.abs(H2), -1, kernel) return np.where(energy1 energy2, H1, H2)二是对融合后的高频系数做一次一致性验证如果某位置的系数来自图1但周围大多数系数来自图2就把它改成图2的系数。这个操作叫“一致性滤波”能有效消除孤立的错误选择。5.2 融合图像整体偏暗或偏亮如果融合结果比原图明显偏暗通常是低频融合策略的问题。平均操作会拉低高亮区域的亮度因为另一张图在对应位置可能是暗的。改进方法是基于局部能量的加权平均def fuse_low_freq_weighted(LL1, LL2, window_size3): 基于局部能量的低频加权融合 kernel np.ones((window_size, window_size), dtypenp.float32) energy1 cv2.filter2D(LL1 ** 2, -1, kernel) energy2 cv2.filter2D(LL2 ** 2, -1, kernel) total energy1 energy2 1e-6 w1 energy1 / total w2 energy2 / total return w1 * LL1 w2 * LL2这样能量大的区域通常也是更清晰的区域会获得更大的权重融合结果的亮度更接近清晰图像。5.3 图像尺寸不一致导致报错两张输入图像尺寸不同时直接做小波分解会在矩阵运算时报错。我的处理方式是统一缩放到较小尺寸而不是填充到较大尺寸。因为填充会引入人工边界在边界处产生虚假的高频响应影响融合质量。如果两张图的长宽比差异很大缩放会导致明显变形这时候应该先做中心裁剪裁成相同的长宽比再统一尺寸。5.4 常见问题速查表问题现象可能原因排查方向解决方法融合结果有块状伪影高频系数选择不一致检查高频融合规则改用窗口能量比较一致性滤波整体偏暗/偏亮低频平均拉低对比度检查低频融合策略改用局部能量加权平均边缘出现亮线/暗线分解重构边界处理不当检查奇数尺寸填充逻辑统一尺寸后再分解重构后裁剪颜色失真三通道独立融合检查是否分通道处理转YCrCb空间只融合Y通道运行速度慢逐像素循环检查是否用了Python循环改用NumPy向量化操作重构后尺寸不对奇数尺寸填充未裁剪检查重构后裁剪逻辑记录原始尺寸重构后裁剪5.5 实操心得参数调节的优先级调参的时候不要一上来就改分解级数。我的经验是先调融合规则再调窗口大小最后才动分解级数。融合规则决定了信息选择的基本逻辑如果规则本身不合理调多少级分解都救不回来。窗口大小影响融合的平滑程度窗口越大越平滑但细节保留越少一般3x3或5x5就够了。分解级数影响的是频率划分的粒度对最终效果的影响反而是最小的。还有一个容易被忽略的点输入图像的质量。如果两张源图本身就很模糊或者曝光差异极大再好的融合算法也出不来好结果。融合的前提是源图之间有互补信息如果两张图的信息高度重叠融合的意义就不大了。6. 性能优化与扩展方向6.1 向量化加速前面展示的分解和重构代码用了Python循环逻辑清晰但速度受限。如果要把这个方案用到批量处理或实时场景需要做向量化改造。核心思路是用NumPy的切片和广播操作替代循环def harr_2d_decompose_fast(img): 向量化版本的二维Harr小波分解 img np.asarray(img, dtypenp.float32) h, w img.shape if h % 2 ! 0: img np.vstack([img, img[-1:, :]]) if w % 2 ! 0: img np.hstack([img, img[:, -1:]]) # 行方向利用切片一次性处理所有行 even_cols img[:, 0::2] odd_cols img[:, 1::2] row_low (even_cols odd_cols) / 2.0 row_high (even_cols - odd_cols) / 2.0 # 列方向同样用切片 even_rows_low row_low[0::2, :] odd_rows_low row_low[1::2, :] LL (even_rows_low odd_rows_low) / 2.0 LH (even_rows_low - odd_rows_low) / 2.0 even_rows_high row_high[0::2, :] odd_rows_high row_high[1::2, :] HL (even_rows_high odd_rows_high) / 2.0 HH (even_rows_high - odd_rows_high) / 2.0 return LL, LH, HL, HH向量化版本的速度比循环版本快10倍以上处理一张4K图像只需要几毫秒。重构函数也可以用同样的方式改造。6.2 扩展到其他小波基Harr小波的优点是简单缺点是频率划分不够精细在图像平滑区域容易产生“方块效应”。如果对融合质量有更高要求可以换成Daubechies小波db2、db4或Symlet小波。这些正交小波的滤波器系数更长分解效果更好但实现复杂度也更高。一个折中的方案是使用双正交小波比如Cohen-Daubechies-Feauveau小波它的分解和重构滤波器不同但具有线性相位特性能避免重构时的相位失真。OpenCV虽然没有内置小波函数但可以用cv2.filter2D配合自定义的滤波器核来实现。6.3 与深度学习方法结合如果手头有标注数据可以考虑用浅层CNN来学习融合规则用小波分解做预处理。具体做法是先对源图做小波分解把LL、LH、HL、HH四个子带作为CNN的输入通道让网络学习如何组合这些子带。这样既保留了小波变换的多尺度优势又利用了深度学习的拟合能力。不过这种方案需要大量的训练数据而且模型的可解释性不如纯小波方法。对于大多数应用场景纯小波融合已经足够好了。6.4 批量处理与自动化实际项目中往往需要处理大量图像对。我通常会写一个批处理脚本自动遍历文件夹里的图像对按命名规则配对然后调用融合函数import os import glob def batch_fuse(input_dir, output_dir, levels2): 批量融合按文件名前缀配对 os.makedirs(output_dir, exist_okTrue) img1_list sorted(glob.glob(os.path.join(input_dir, *_A.*))) for path1 in img1_list: path2 path1.replace(_A., _B.) if not os.path.exists(path2): print(f跳过找不到配对图像 {path2}) continue name os.path.basename(path1).replace(_A., _fused.) output_path os.path.join(output_dir, name) try: wavelet_fuse(path1, path2, output_path, levels) print(f完成{name}) except Exception as e: print(f失败{name}原因{e})批处理时要注意异常处理单张图像失败不应该中断整个批次。另外建议加一个日志记录方便回溯哪些图像处理失败了。7. 一些踩坑之后的个人体会这个方案我从第一次写到现在前后改了好几版。最开始用的是逐像素循环一张图跑好几秒后来改成向量化才把速度提上来。融合规则也试过很多种低频取大、高频平均、基于梯度的选择最后发现还是“低频平均高频绝对值取大”这个最朴素的组合最稳。有一个坑我印象很深早期版本没有处理奇数尺寸结果遇到一张1921x1081的图分解重构后尺寸变成了1922x1082保存的时候和原图对不上排查了半天才发现是填充逻辑的问题。后来我在分解函数里统一做了偶数化处理重构后再裁剪回原始尺寸这个问题就再也没出现过。还有一个经验是关于分解级数的。我一开始觉得级数越多效果越好直接上了4级结果融合图像边缘出现了明显的振铃而且处理时间翻了好几倍。后来降到2级效果反而更自然。所以参数这个东西不是越大越好够用就行。如果你打算把这个方案用到实际项目里我的建议是先把灰度图的流程跑通确认融合效果符合预期再扩展到彩色图像。彩色图像的处理虽然只多了几行代码但颜色空间转换和通道合并的细节很容易出错先把基础打牢再往上加功能会省很多调试时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑