
拍过微距、拍过远近同框的场景你就会明白多聚焦图像融合Multi-Focus Images Fusion是个很实在的需求。普通镜头受景深限制对焦在近处时远处糊对焦在远处时近处糊。多聚焦图像融合要做的就是把同一场景下聚焦不同位置的多张图合在一起生成一张全图都清晰的图。这个事听起来简单但真正落地时涉及清晰度评价、多尺度分解、权重图估计等一系列问题踩坑极多。这篇内容是我把几种主流融合算法从理论到代码过了一遍后的复盘会按“原理—实现—评估—排坑”的顺序来讲想自己写融合Demo或者入门图像融合的同学可以直接参考。1. 多聚焦图像融合到底在解决什么问题1.1 从“全清晰照片”的需求说起先聊一个生活场景。你用手机拍一杯咖啡杯子上的拉花很清楚但背景里的窗户已经虚成一片光斑再拉一下对焦窗户清楚了杯子又模糊了。这不是手机不行而是光学系统的物理限制不管光圈怎么收镜头对焦的工作距离总是有限的超过景深范围的物体会产生离焦模糊。多聚焦图像融合Multi-Focus Images Fusion的典型应用场景就是这么冒出来的对同一相机位置连续拍摄多张聚焦在不同距离的照片然后把每张图里“聚焦良好”的区域也就是最锐利、高频信息最丰富的区域摘出来重新拼合成一张所有目标都清晰的图。注意这里说的是“多聚焦”不是多曝光HDR。HDR处理的是亮度动态范围多聚焦处理的是清晰度覆盖范围两个问题的数学模型完全不同。在工业视觉领域这个技术尤其重要。比如PCB板检测板上元件高度不同一颗望远镜头的景深不够覆盖整块板单帧成像就会让某些焊点发虚。人工调焦确认成本高产线上又要求连续看板于是多聚焦融合就成为了前端图像预处理的常见手段。显微成像、文档翻拍、监控取证也经常用到背后诉求一致去掉离焦模糊让信息尽可能完整。1.2 一篇博文能帮你搞定的事我接触图像融合的时间不算短但真正动手写融合代码时发现网上资料要么只给了概念图要么就贴一段调用现成库的“黑盒代码”完全不清楚内部发生了什么。这篇文章打算做一次完完整整的复盘从最经典的拉普拉斯金字塔讲起再说小波变换最后聊CNN式融合思路同时把融合之前容易忽视的“配准”“亮度一致性”问题拿出来讲再给出一套客观评估指标方便你和别人谈效果时有统一标准。适合的读者包括正在做毕业设计选的图像融合方向的同学、需要给视觉检测工具链加预处理的工程师以及纯粹想把OpenCV用得更明白的爱好者。我会尽量用“为什么这么做”去解释每一个关键选择而不是只给一份能跑但看不懂的代码。2. 融合算法的核心思路与选型2.1 融合的本质是“选取重构”把多聚焦融合拆开看本质就是两步先判断每个像素或区域来自哪张图更合适这是“选取”再把这些被选中的清晰内容拼接成一个无缝整体这是“重构”。听起来简单但难点就在如何判断“哪张图更合适”以及怎么消除拼接痕迹。判断聚焦质量最直接的做法是看梯度。清晰图像中物体边缘的灰度变化剧烈梯度幅值大离焦模糊相当于对图像做了低通滤波边缘被抹平梯度自然变小。所以经典算法中局部方差、拉普拉斯能量、梯度能量都是常用的聚焦测度。但仅仅在像素级做一个“谁梯度大选谁”的硬决策会有非常明显的边界效应。我试过用手工构造的权重图做直接加权结果物体边缘附近会出现大量“撕裂”和“重影”。原因很简单像素之间没有空间相关性噪声会让相邻像素被分配到来图不同的两张源图。所以现代算法倾向于做软决策把二值选择变成连续权重并在多尺度空间里重构这个思路贯穿整个多聚焦融合的发展史。2.2 主流方案对比空间域、变换域、深度学习多聚焦融合的算法谱系大致能分三类空间域、变换域、基于深度学习。我拿一个简单表格给你列出来方法类别核心思路优点典型局限空间域直接在像素或图像块上计算清晰度并加权平均计算快实现直观容易产生块效应对齐要求高变换域先做金字塔或小波分解在不同尺度/频率上融合多尺度天然符合视觉系统细节保留好计算量偏高需要设计分解层数深度学习用卷积网络自动学习聚焦区域和融合权重特征提取能力强复杂场景效果好需要训练数据部署成本高我的经验是入门阶段最好把拉普拉斯金字塔吃透因为它已经把变换域方法的核心思想展示得明明白白——低频层决定了整体结构和亮度高频层决定了边缘和纹理融合规则可以针对不同频率单独设计。金字塔结构还是理解小波变换的一个完美跳板搞懂尺度分解后再学小波你会觉得思路是连续的。如果你只是临时处理少量图片用现成库里的cv2.merge配合简单权重图也能跑但效果上限很低。真正让融合效果拉开差距的是对“聚焦测度”和“重构方式”这两件事的把握。下面我把三种我能跑通的实现细节都给你讲一遍。3. 经典算法拆解与Python实现示例3.1 拉普拉斯金字塔融合拉普拉斯金字塔的底层逻辑是用“多尺度减法”把图像的细节层和基础层分开。先构造高斯金字塔每一层都是上一层做高斯模糊后下采样拉普拉斯金字塔每一层则是原图与上采样后的上一层之间的残差。这个残差里主要存储边缘、纹理等高频信息而顶层残差里是一张很小的低频近似图。融合的流程可以这样描述对两张源图分别建立拉普拉斯金字塔。创建一张融合权重图通常用“清晰度图”或边缘能量图做平滑后得到。每一层都只用同一个权重图去加权融合拉普拉斯系数。最后从顶层开始逐层上采样、累加得到融合结果。代码可以写得很短我贴一段比较接近工程实践的版本import cv2 import numpy as np def build_laplacian_pyramid(img, levels): gaussian_pyramid [img] for _ in range(levels): img cv2.pyrDown(img) gaussian_pyramid.append(img) laplacian_pyramid [] for i in range(levels): size (gaussian_pyramid[i].shape[1], gaussian_pyramid[i].shape[0]) up cv2.pyrUp(gaussian_pyramid[i 1], dstsizesize) lap cv2.subtract(gaussian_pyramid[i], up) laplacian_pyramid.append(lap) laplacian_pyramid.append(gaussian_pyramid[-1]) return laplacian_pyramid def reconstruct_from_laplacian(pyramid): img pyramid[-1] for i in range(len(pyramid) - 2, -1, -1): size (pyramid[i].shape[1], pyramid[i].shape[0]) img cv2.pyrUp(img, dstsizesize) img cv2.add(img, pyramid[i]) return img def focus_measure(img): gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) lap cv2.Laplacian(gray, cv2.CV_64F) return cv2.GaussianBlur(cv2.convertScaleAbs(lap), (5, 5), 0) img_a cv2.imread(focus_front.jpg) img_b cv2.imread(focus_back.jpg) # 使用拉普拉斯响应作为聚焦测度并归一化为权重图 weight_a focus_measure(img_a).astype(np.float32) weight_b focus_measure(img_b).astype(np.float32) total weight_a weight_b 1e-6 weight_a / total weight_b / total levels 4 lap_a build_laplacian_pyramid(img_a, levels) lap_b build_laplacian_pyramid(img_b, levels) fusion [] for la, lb in zip(lap_a, lap_b): wa np.expand_dims(weight_a, axis2) if len(la.shape) 3 else weight_a wb np.expand_dims(weight_b, axis2) if len(lb.shape) 3 else weight_b fusion.append(la * wa lb * wb) result reconstruct_from_laplacian(fusion) cv2.imwrite(fusion_result.jpg, result)这段代码里有个关键细节权重图是在原图分辨率上计算的但在和小尺寸金字塔层融合时我并没有额外做下采样归一化。因为拉普拉斯金字塔每一层的尺寸不同直接使用原始分辨率权重会造成权重和系数尺寸不匹配。实际操作中更稳妥的做法是给每一层单独生成权重图或者直接用同一权重做“下采样”操作来匹配尺寸。工程里我建议按层生成也就是对原始权重图做cv2.resize到当前层尺寸。拉普拉斯金字塔的好处是保留了空间位置的对应关系不会像傅里叶变换那样把局部信息打散。缺点是如果层数太少全局亮度差异容易串到细节层上层数太多又会有计算压力。实际应用中四到五层是比较均衡的选择。3.2 小波变换融合小波变换可以看成拉普拉斯金字塔的“高级版”。它把图像分解成低频近似分量和水平、垂直、对角三个方向的高频细节分量。融合规则可以做得更精细低频分量控制整体结构适合用加权平均或能量选择高频分量包含边缘方向信息通常直接取两个源图中绝对值更大的一方因为大的系数意味着该位置存在明显纹理。用PyWavelets实现非常快import pywt import numpy as np def fuse_wavelet(img_a, img_b, level3, waveletdb2): coeff_a pywt.wavedec2(img_a, wavelet, levellevel) coeff_b pywt.wavedec2(img_b, wavelet, levellevel) fused list(coeff_a) # 近似分量取平均。细节分量取绝对值大的 fused[0] (coeff_a[0] coeff_b[0]) / 2.0 for i in range(1, len(coeff_a)): fused[i] [] for c_a, c_b in zip(coeff_a[i], coeff_b[i]): fused[i].append(np.where(np.abs(c_a) np.abs(c_b), c_a, c_b)) return pywt.waverec2(fused, wavelet)这段代码我用过多次有两点特别提醒。第一彩色图像处理时要小心pywt.wavedec2只支持二维数组。常见做法是分别在BGR三通道上融合最后再合并或者转成YUV后只对Y通道做小波UV通道直接取一张源图或做简单平均。后者运行速度更快因为人眼对亮度细节最敏感颜色信息少一点损失不容易被发现。第二小波基函数的选取会影响边缘。db2短小波对局部细节响应好haar最简单但会产生块状痕迹sym8更平滑但速度慢一些。我在显微图像上对比过db2到db4通常都能获得不错的效果如果你不确定可以先跑一遍自动网格搜索用亮度熵和梯度指标做初筛。小波融合对配准误差的容忍度比空间域加权平均高不少因为它把一个像素的能量分散到了多个尺度上。但要注意如果两张图之间存在拍摄时刻的微小位移高频细节直接取绝对值大反而可能引入“鬼影”。所以无论用金字塔还是小波第一步永远是保证输入图像严格对齐。3.3 更进一步的CNN融合框架简述深度学习出现后多聚焦融合的主流思路从“人工设计特征”变成了“让网络学特征”。常见框架包括用卷积网络做二分类判断每个像素或每个块来自哪张源图或者让网络直接回归出融合权重图再或者用生成对抗思路把融合结果和源图放到一个域里做约束。我实际测试过一个很轻量的CNN方案把两张图叠成一个双通道输入灰度情况下送入一个只有四层卷积的网络输出一张单通道权重图然后用这个权重图加权原图。网络本身可以用一个简单的无监督损失比如让融合结果的梯度能量尽量大同时和两张源图的色差尽量小。效果确实比纯手工权重图细腻尤其在精细毛发、树叶这类边缘场景中能明显看到伪影减少。不过深度学习并非万能。训练数据不足时网络很容易把“离焦模糊”误判为“平滑区域”因为两者在低频域表现相似。我建议初学者不要跳过经典算法直接上CNN否则连网络为什么错误都不知道。先把金字塔和小波的特性吃透再去看深度学习你会更清楚网络到底在拟合什么。4. 图像配准与预处理融合前容易被忽略的坑4.1 为什么要配准很多教程里默认两张源图已经严格对齐但实际拍摄中手抖、快门延迟、镜头畸变都会带来亚像素到几个像素的偏差。不配准就直接融合结果往往是物体边缘出现双重轮廓也就是俗称的“鬼影”。这个鬼影和小波抖动的鬼影本质相同但来源不同一个是空间错位一个是融合规则对错位放大。配准不是多聚焦融合独有的问题但多聚焦场景有个特殊点源图中的清晰区域可能是互补的。如果一张图前景清晰、后景模糊另一张刚好相反那么特征匹配在模糊区域可能提不到足够特征点。所以配准要在“比较清晰”的区域做或者先用高通滤波增强边缘再提取特征。4.2 基于特征的配准流程我常用OpenCV的SIFT或ORB来做配准。SIFT鲁棒性更好ORB更快。整体流程是提取两图的特征点和描述子。用暴力匹配或FLANN匹配找到对应关系。用RANSAC剔除错误匹配并估计单应矩阵。对其中一张图做透视变换使两图坐标系一致。下面是精简代码import cv2 import numpy as np def align_images(img_1, img_2, max_features1000): gray_1 cv2.cvtColor(img_1, cv2.COLOR_BGR2GRAY) gray_2 cv2.cvtColor(img_2, cv2.COLOR_BGR2GRAY) sift cv2.SIFT_create(max_features) kp1, des1 sift.detectAndCompute(gray_1, None) kp2, des2 sift.detectAndCompute(gray_2, None) matcher cv2.FlannBasedMatcher(dict(algorithm1, trees5), {}) matches matcher.knnMatch(des1, des2, k2) good [] for m, n in matches: if m.distance 0.75 * n.distance: good.append(m) src_pts np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2) dst_pts np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2) matrix, mask cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) h, w img_1.shape[:2] aligned cv2.warpPerspective(img_1, matrix, (w, h)) return aligned注意这里的返回是把img_1对齐到img_2的坐标系。如果两张图视角差太大单应矩阵只适用于平面场景立体重叠区域还是会有视差。多聚焦拍摄通常可近似看作纯抖动单应足够。还有一个容易踩的坑如果源图是彩色图且光照差异大直接在BGR上提取SIFT表现不稳定。我习惯先做灰度化后再加一次CLAHE均衡再提取特征匹配数量会明显提升。4.3 亮度一致性处理即便配准成功两张源图如果曝光参数不一致融合后会在拼接区域产生明显的亮度跳变。尤其是自动曝光下焦点位置改变时测光区域不同画面亮度会跟着变。这个问题的常规解法是做直方图匹配。具体操作是选取一张源图作为参考图把另一张图的直方图映射到参考图的直方图分布上。OpenCV里可以用cv2.calcHist和cv2.LUT自己实现也可以用现成库简单调用。在YUV空间只匹配Y通道能减少颜色漂移。另一种思路是放弃对全局亮度做齐一化而是让融合规则里带一个低频亮度校正在金字塔融合的顶层低频分量上用一个局部均值比去修正待融合层。这样能避免把全局匹配做得过度导致某些区域色彩失真。我更推荐先做简单的全局直方图匹配如果效果不自然再引入局部修正。5. 评估指标怎么判断“融合得好”5.1 主观评价的局限性融合效果的好坏第一眼当然是看人眼是否舒服。但做工程时你不可能把所有融合结果都打印出来让人打分尤其在批量调参阶段需要一个能量化差异的客观标准。主观评价的另一个问题是不稳定同样一张图不同评测者可能给出截然相反的排序。所以学术界和工程界都倾向于把主观评价和客观指标结合起来。主观评价可以看伪影、清晰度、颜色一致性客观指标则用一张表来量化和对比。注意没有哪个单一指标是完美的我最常做的事是同时算三个指标看它们是否一致地支持同一结论。5.2 常用客观指标与计算这里列几个我在融合实验中几乎必看的指标指标名称含义计算思路备注信息熵图像信息量的多少统计灰度直方图计算熵值越高说明细节越多但噪声也会拉高平均梯度图像边缘强度水平和垂直梯度的平均幅值越高越清晰但要防噪声标准差灰度分布离散程度像素灰度标准差反映对比度不是越高越好互信息融合图与源图的信息关联联合熵与边缘熵组合计算高说明保留了更多源图信息边缘保持度融合图保留源图边缘的程度对源图和融合图做梯度比较更贴近多聚焦融合需求写代码时信息熵和平均梯度都很好算。比如平均梯度def average_gradient(img): gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gx cv2.Sobel(gray, cv2.CV_32F, 1, 0) gy cv2.Sobel(gray, cv2.CV_32F, 0, 1) return np.mean(np.sqrt(gx**2 gy**2))互信息可以直接用sklearn中的mutual_info_score处理灰度向量但计算量偏大。实际评估时我会把图像先降采样再计算互信息趋势基本能保持一致。需要注意的是如果你用源图直接做指标对比可能会出现“融合图平均梯度低于某一个源图”的情况这未必是坏事。因为融合图是两张图清晰区域取交集的折中不可能所有边缘都优于每一张源图。只有当你发现融合图的边缘保持度明显低于两个源图的平均值时才说明融合规则有问题。6. 常见问题与排查技巧实录6.1 融合结果出现光晕和伪影光晕通常出现在高反差边缘附近因为融合权重图在边缘两侧发生了剧烈变化。拿强光源来说一张图里灯亮、背景暗清晰的灯心会在权重图上产生一个尖锐的高斯峰另一张图里灯可能对焦不准权重较低。如果权重图没有做足够的平滑加权融合会生成一圈半透明区域看着就像物体在发光。解决办法我建议按顺序排查先确认输入是否已配准再检查权重图是否用了足够大的高斯核比如(15, 15)甚至更大然后看金字塔层数是否过少层数太少会导致低频信息直接叠加把两层图亮度冲突暴露出来。多数光晕问题在前三步就能解决。6.2 聚焦区域误判导致细节丢失如果融合结果里某个本该清晰的细节变得比源图还模糊那基本可以认定是聚焦测度判断错了。比如低纹理白色墙壁离焦和聚焦状态下梯度变化不大算法很容易把它划分到错误的权重区域。另一个常见情况是图像含有较多传感器噪声噪声梯度幅值很大可能比真正的边缘还要大导致算法把噪声区域当清晰区域。我试过比较有效的做法是采用多测度融合不只算拉普拉斯梯度还把局部方差加进去同时对源图像先做一次轻度的中值滤波去噪再计算聚焦测度。另一种思路是改成滑窗块级别的判断而不是像素级让空间邻域的有效像素参与投票能显著降低误判。6.3 颜色失真颜色失真大多来自融合规则不分通道。在RGB三通道上做独立加权平均时同一位置三通道系数不同颜色自然就会偏掉。尤其是小波变换里高频细节系数取绝对值较大的一方时如果三个通道的“较大”来自不同源图融合结果会出现彩色边缘。稳妥的策略是在亮度/色度分离空间处理。把RGB转成YUVY通道承载亮度信息做完整的融合算法U和V通道只做简单的加权平均或者干脆选择其中一张清晰度高的作为来源。这样既保留了色彩一致性又显著减少了计算量。我自己的融合项目里基本默认用这个流程比纯RGB融合的错误颜色少了至少一半。6.4 性能优化从Python到实时Python版本的融合算法跑一张1080p图像拉普拉斯金字塔大概需要几百毫秒到一秒小波更快一些CNN则要看模型复杂度。如果只是离线处理这个速度够用但如果要接检测产线就得多花点心思。常见的优化手段包括先把图像缩到合适尺寸做融合再对感兴趣区域做细节增强把权重图计算从每个源图的每个像素改成隔行隔列采样再做插值使用多线程同时计算金字塔各层。更进一步可以把核心融合函数用Cython或Numba加速或者把整个流程固化到OpenCV的UMat中做GPU处理。我实测过Numba对for密集的权重计算能提升三到五倍而金字塔层的cv2函数本身已经很快瓶颈往往出在Python循环和数据类型转换上。最后说一点实际操作中的体会我把金字塔、小波、CNN三种方案都跑过之后最大的感受是多聚焦图像融合的难点不在于会用某种算法而在于知道在什么场景下选择什么策略。对焦差异明显的场景一个简单的拉普拉斯金字塔就能做得很好边缘复杂、要求更严的场景小波变换的多方向分解优势就会体现出来如果你有足够的训练数据和算力深度学习才有可能带来真正的精度提升。另外无论你选哪条路线配准和权重平滑都是绕不开的预处理。很多帖子喜欢把融合算法包装得花团锦簇但实际工程里我遇到最多的伪影、鬼影几乎都来自输入没有对齐或权重不够平滑。先把这些基本功打好再研究高深算法路会顺很多。最后分享一个我常用的小技巧跑融合实验时不要只看最终图像把中间权重图单独保存下来看一眼。权重图能直观告诉你算法认为哪里清晰、哪里不清晰。如果权重图本身看起来都支离破碎那么最终结果一定好不到哪去。学会读权重图比调十个参数都有用。