
简介在夜间或弱光环境中可见光图像往往缺乏目标细节红外图像则能捕捉热辐射信息将两者融合已成为图像增强的重要手段。面向计算机视觉课程设计与期末大作业场景这套Python源码提供了完整的红外与可见光图像融合实现帮助学习者从代码层面理解融合目的与处理流程。压缩包共3个文件均为Python脚本总大小7KB涵盖图像预处理、直方图均衡化、Otsu阈值分割等关键环节结构简洁便于逐步阅读和调试。所有代码均经过严格调试在常见Python环境下可直接运行适合快速搭建实验环境或作为二次开发基础。读者既可借助源码完成课程任务也能通过调整参数与替换模块深入掌握图像融合算法设计思路。目前已有775人学习下载具有较强的参考价值。1. 红外和可见光图像融合这个 Python 项目在解决什么问题“红外和可见光图像融合”这个标题拆开看是三件事一张红外图、一张可见光图、一个把两张图合成一张的 Python 实现。红外的成像机制决定了它对温度敏感能穿透烟雾和低照度环境但缺少纹理和色彩可见光正好反过来细节丰富却依赖环境光。融合的目的不是把两幅图做加权平均而是让输出图同时保留红外目标的高显著性和可见光的场景细节例如夜间监控里的人和路灯、自动驾驶里的行人与路沿。适合读者包括做目标检测前处理的算法工程师、要复现论文的硕博生以及想用 Python 快速验证融合效果的开发者。拿到源码压缩包之后直接跑通不难难的是理解金字塔分解、融合规则和参数含义这三件事决定融合图是“干净锐利”还是“灰蒙蒙一片”。2. 像素级融合与拉普拉斯金字塔先从原理上决定源码结构2.1 融合层级为什么像素级是红外和可见光图像融合的起点图像融合首先要回答在哪个层级融。像素级融合直接作用于图像灰度值把红外和可见光按规则逐像素组合优点是信息保留完整、实现简单适合要求“输出一张融合图像”的项目缺点是计算量大、对配准误差敏感。特征级融合先提取边缘、角点或显著性区域在特征图上加权合并能吸收一部分配准误差但特征之外的细节会丢失。决策级融合则是两种传感器各自先做检测识别再用投票或置信度融合结果适合目标识别任务输出不再是图像。这份源码面向的是“图像融合结果”所以像素级是稳妥的起点。像素级融合还有一个前提红外和可见光两幅图必须已经在空间上对齐。红外相机和可见光相机的安装位置、视野不同未经配准的图直接做像素级操作目标轮廓会出现重影。很多公开数据集里的图像对已经配准如果是自采数据需要先做 ECC 或 SIFT 粗配准这块常见做法放在源码里独立成一个align.py模块。2.2 高斯金字塔与拉普拉斯金字塔多尺度携带细节金字塔融合的基本逻辑是把图像按尺度拆开逐尺度决定“像素听谁的”再合并回去。高斯金字塔通过反复高斯模糊和隔行下采样得到一组分辨率递减的图像拉普拉斯金字塔保存的是相邻两个高斯层之间的差分因此每一层对应特定尺度下的边缘和纹理。融合时对拉普拉斯层逐层决策可以让小细节车牌字符、树叶轮廓和大结构人体、车辆整体各取所需。构造拉普拉斯金字塔时最容易踩的坑是使用cv2.subtract。cv2.subtract会把负数截断成 0而拉普拉斯层恰恰依赖边缘两侧的正负差值一旦截断重建后的图像会发闷、丢失高对比细节。常见做法是用普通减法并全程保持float32精度最后重建完再转回uint8。2.3 高频层融合规则取大、加权平均、局部能量融合规则决定拉普拉斯每一层怎么取舍。取大规则在高频层直接比较红外和可见光对应像素绝对值谁大取谁边缘锐利但噪声也容易被当成细节保留。局部能量规则用窗口内能量代替单像素比较窗口求和能摊薄孤立噪声点对棋盘伪影的抑制也更好。近似层包含大部分亮度与背景信息通常直接用加权平均。下表是三种常见规则。规则适用场景主要问题max 取大边缘锐利、目标轮廓明显红外噪声被放大容易出现颗粒感mean 平均背景平滑、无强噪声红外目标显著性被削弱local_energy 局部能量噪声与细节并存窗口大小影响效果参数需要调下面这段代码可以独立测试三种规则它就是 3.2 节融合脚本的核心部件。def fuse_level(li, lv, ruleenergy, win5): 对某一层拉普拉斯金字塔做融合决策。 if rule max: return np.maximum(li, lv) if rule mean: return 0.5 * li 0.5 * lv if rule energy: # 窗口越大邻域平均的范围越大对孤立噪声越不敏感 kernel np.ones((win, win), dtypenp.float32) / (win * win) ei cv2.filter2D(li.astype(np.float32), -1, kernel, borderTypecv2.BORDER_REFLECT) ev cv2.filter2D(lv.astype(np.float32), -1, kernel, borderTypecv2.BORDER_REFLECT) # 邻域能量大的中心像素胜出而不是只看单点 return np.where(ei ev, li, lv).astype(np.float32) raise ValueError(unknown rule: rule)这里的li和lv是红外、可见光各自某一层拉普拉斯金字塔大小完全一致。win取奇数常用 3、5、7窗口越大背景细节越容易被抹平但抗噪更强。cv2.filter2D的borderType用BORDER_REFLECT可以避免边界区域因为补零导致能量异常偏低。2.4 深度网络方案理解 DenseFuse、FusionGAN 与金字塔方法的边界近几年的融合项目里也常见深度学习实现典型代表有 DenseFuse、FusionGAN、NestFuse。这些网络的共同结构是编码器分别提取红外与可见光的深层特征在特征空间完成融合再由解码器重建图像。它们的优势是能自动学习保留语义重要的区域融合结果在视觉上更干净完整代价是依赖大量已配准的红外-可见光图像对训练需要 GPU推理速度也远低于金字塔方法。传统金字塔方法虽然不含语义信息但解释性强、依赖少、CPU 上一张图只要几十毫秒适合做项目基线。工程里常见的推进路径是先用金字塔方法跑通整个评估流程确定评价指标后再替换成深度模型。3. Python 实现红外和可见光图像融合环境、目录与整套代码3.1 最小依赖与目录建议这个标题下的源码本质上是一个图像处理工程不建议引入重量级框架。Python 3.9 以上即可核心依赖只有 NumPy 和 OpenCV。安装命令如下。python -m venv .venv source .venv/bin/activate pip install numpy opencv-pythonWindows 下激活命令是.venv\Scripts\activate。如果后面要计算指标常用做法是再加上scikit-image它提供 PSNR、SSIM 等现成接口不过无参考融合指标还是得自己写。目录结构保持扁平即可建议如下。project/ ├── data/ │ ├── ir/ 红外灰度图 │ └── vis/ 可见光彩色图 ├── out/ 融合结果输出 ├── pyramid_fusion.py 核心融合代码 └── eval_metrics.py 质量评估代码把输入输出路径固定下来后续批处理和调参都方便。data 里放已经配准好的图像对文件名建议统一规则例如scene01_ir.png和scene01_vis.png批处理时只用文件名前缀配对。3.2 完整融合脚本从读图到保存下面这个脚本是金字塔融合的一个最小可运行版本核心包括高斯金字塔、拉普拉斯金字塔、融合与重建四个环节。import numpy as np import cv2 def gaussian_pyramid(img, levels): 逐层下采样得到高斯金字塔。 gp [img] for _ in range(levels - 1): img cv2.pyrDown(img) gp.append(img) return gp def laplacian_pyramid(gp): 高斯金字塔相邻层差分得到拉普拉斯金字塔。 lp [] for i in range(len(gp) - 1, 0, -1): up cv2.pyrUp(gp[i], dstsize(gp[i-1].shape[1], gp[i-1].shape[0])) # 注意这里用普通减法不能用 cv2.subtract否则负数被截断 diff gp[i-1].astype(np.float32) - up.astype(np.float32) lp.append(diff) lp.append(gp[-1].astype(np.float32)) return lp[::-1] def fuse_level(li, lv, ruleenergy, win5): if li.shape ! lv.shape: raise ValueError(lap layer size mismatch) if rule max: return np.maximum(li, lv) if rule mean: return 0.5 * li 0.5 * lv if rule energy: kernel np.ones((win, win), dtypenp.float32) / (win * win) ei cv2.filter2D(li.astype(np.float32), -1, kernel, borderTypecv2.BORDER_REFLECT) ev cv2.filter2D(lv.astype(np.float32), -1, kernel, borderTypecv2.BORDER_REFLECT) return np.where(ei ev, li, lv).astype(np.float32) raise ValueError(unknown rule: rule) def reconstruct(lp): 从拉普拉斯金字塔逐层上采样相加重建图像。 out lp[-1].astype(np.float32) for layer in reversed(lp[:-1]): out cv2.pyrUp(out, dstsize(layer.shape[1], layer.shape[0])) layer return out def fuse_image(ir, vis_gray, levels4, ruleenergy, win5): gp_ir gaussian_pyramid(ir, levels) gp_vis gaussian_pyramid(vis_gray, levels) lp_ir laplacian_pyramid(gp_ir) lp_vis laplacian_pyramid(gp_vis) fused_lp [fuse_level(a, b, rule, win) for a, b in zip(lp_ir, lp_vis)] return np.clip(reconstruct(fused_lp), 0, 255).astype(np.uint8) def main(ir_path, vis_path, out_path): # 红外读成单通道灰度可见光读成三通道 ir cv2.imread(ir_path, cv2.IMREAD_GRAYSCALE) vis cv2.imread(vis_path, cv2.IMREAD_COLOR) if vis.shape[:2] ! ir.shape[:2]: vis cv2.resize(vis, (ir.shape[1], ir.shape[0]), interpolationcv2.INTER_LINEAR) # YUV 空间只替换 Y 亮度分量颜色由可见光的 UV 提供 yuv cv2.cvtColor(vis, cv2.COLOR_BGR2YUV) y, u, v cv2.split(yuv) fused_y fuse_image(ir, y, levels4, ruleenergy, win5) merged cv2.merge([fused_y, u, v]) cv2.imwrite(out_path, cv2.cvtColor(merged, cv2.COLOR_YUV2BGR)) if __name__ __main__: main(data/ir/scene01_ir.png, data/vis/scene01_vis.png, out/scene01_fused.png)laplacian_pyramid里对每一层执行cv2.pyrUp时必须用dstsize明确指定目标尺寸否则 OpenCV 按照size*2计算碰上奇数尺寸时会导致层与层之间大小对不齐。fuse_image中的levels默认 4对 1024×768 以内的图足够层数过多时最小近似层只有几个像素融合意义不大。main里的 YUV 转换是整段代码的关键可见光彩色图转成 YUV 后红外图只与 Y 分量融合UV 直接沿用可见光这样最终输出既保留了红外热目标也保留了可见光色彩不会出现整张图偏色。3.3 金字塔参数与红外权重怎么定levels、rule、win、红外权重是四个必调参数。层数决定多尺度分解粒度规则决定像素取舍逻辑窗口决定局部能量计算范围。如果希望红外目标更突出常见做法是在融合近似层时给红外一个大于 0.5 的权重修改fuse_image中最后一段fused_lp[-1] 0.7 * lp_ir[-1] 0.3 * lp_vis[-1]。近似层决定整体亮度和背景这个位置提高红外权重不会像高频层那样引入噪点。下表给出参数选择的经验范围。参数建议范围影响levels3~5层数太少多尺度优势不明显太多则最小层过小且耗时增加ruleenergy / maxenergy 抗噪max 边缘锐利但噪点多win3 / 5 / 7窗口越大越抗噪但细节被平均掉红外近似层权重0.5~0.7权重越高热目标越亮背景纹理越弱3.4 levels 设置与 16 位红外数据预处理一些长波红外相机输出的是 16 位 PNG直接当成uint8读会得到一张几乎全黑或全白的图。常见做法是读入后用cv2.normalize做最小最大归一化到 0~255再参与金字塔分解。if ir.dtype np.uint16: ir cv2.normalize(ir, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)这段处理要在imread之后立即执行。注意 16 位红外图的动态范围可能集中在某一小段归一化后对比度会提升但红外噪声同样会被放大。如果观察到融合结果里出现大量颗粒可以先用高斯滤波做轻量平滑再用local_energy规则融合。4. 融合效果评估指标计算、参数调优与常见排错4.1 无参考指标信息熵与平均梯度融合图像没有标准答案主观“看着还行”不可靠。常用做法是用无参考指标量化信息丰富程度和边缘锐利程度其中信息熵衡量灰度分布的随机性平均梯度衡量边缘强度。import cv2 import numpy as np def image_metrics(img_gray): 计算信息熵和平均梯度输入必须是灰度 uint8 图。 hist cv2.calcHist([img_gray], [0], None, [256], [0, 256]).ravel() hist hist / hist.sum() # 加 1e-12 防止 log2(0) entropy float(-np.sum(hist * np.log2(hist 1e-12))) gx cv2.Sobel(img_gray, cv2.CV_32F, 1, 0, ksize3) gy cv2.Sobel(img_gray, cv2.CV_32F, 0, 1, ksize3) avg_grad float(np.mean(np.sqrt(gx ** 2 gy ** 2))) return {entropy: round(entropy, 4), avg_grad: round(avg_grad, 4)}信息熵不是越高越好熵过高往往意味着噪声也参与进来了。平均梯度同理锐利和颗粒感在数值上都表现为梯度增大。实际评估时要同时看两个指标并用三窗格对比图确认边缘没有出现重复或发灰。若项目有参考融合图还可以用skimage.metrics.structural_similarity计算 SSIM但真实红外与可见光融合场景通常没有参考图无参考指标才是主要依据。4.2 参数选择对照表下面的对应关系适用于大多数路测和安防场景可以快速定位参数问题的方向。现象可能原因调整方式目标轮廓模糊levels 太小或融合规则平均占比大levels 改为 4rule 改为 max 或 energy背景细节丢失红外近似层权重过高把 0.7 调回 0.5 左右噪声颗粒明显rulemax 放大了红外噪声改 energywin 从 5 调到 7目标不够突出红外目标亮度与背景接近先对红外图做对比度拉伸再融合融合图发灰拉普拉斯层用了 cv2.subtract改成 float32 普通减法4.3 YUV 丢色与未配准等高频踩坑点第一把可见光彩色图和红外灰度图直接传入np.maximum形状不一致会报错即使强行处理也会失去所有颜色信息。正确做法是转 YUV 只融合亮度分量。第二输入图像尺寸不一致金字塔层数稍多就报错因此在main入口处做统一 resize。第三红外图动态范围没有归一化导致融合结果整体偏暗。第四两张图未配准融合后目标有重影这是数据问题算法救不回来需要先做配准。第五金字塔层数设到 8 以上内存占用和耗时都上升但融合效果并不提升。4.4 用红外掩码增强热目标如果希望热目标更突出可以在融合后加一步红外掩码增强。将红外图归一化到 0~1作为权重叠加到融合结果上。这里需要注意掩码增强会同时提高目标的对比度也可能让目标周围出现光晕因此增强系数不宜超过 0.3。mask cv2.normalize(ir, None, 0.0, 1.0, cv2.NORM_MINMAX).astype(np.float32) enhanced fused_y.astype(np.float32) * (1.0 0.3 * mask) fused_y np.clip(enhanced, 0, 255).astype(np.uint8)5. 批处理验证融合效果用消融实验筛出最稳参数5.1 三窗格批处理验证脚本单个样例看不出参数泛化能力常见做法是把一组红外-可见光图像对全部跑一遍输出三窗格对比图每张图由可见光、红外伪彩、融合结果并排组成。import glob import os import cv2 from pyramid_fusion import fuse_image LEVELS 4 RULE energy WIN 5 for vis_path in sorted(glob.glob(data/vis/*.png)): name os.path.basename(vis_path) ir_path os.path.join(data/ir, name) if not os.path.exists(ir_path): continue ir cv2.imread(ir_path, cv2.IMREAD_GRAYSCALE) vis cv2.imread(vis_path, cv2.IMREAD_COLOR) if vis.shape[:2] ! ir.shape[:2]: vis cv2.resize(vis, (ir.shape[1], ir.shape[0])) yuv cv2.cvtColor(vis, cv2.COLOR_BGR2YUV) fused fuse_image(ir, yuv[:, :, 0], levelsLEVELS, ruleRULE, winWIN) fused cv2.cvtColor(cv2.merge([fused, yuv[:, :, 1], yuv[:, :, 2]]), cv2.COLOR_YUV2BGR) ir_color cv2.applyColorMap(ir, cv2.COLORMAP_INFERNO) row np.hstack([vis, ir_color, fused]) cv2.imwrite(fout/{name}, row)applyColorMap把灰度红外映射成伪彩色能让肉眼快速定位热目标np.hstack要求三张图高度一致而融合前后尺寸不变所以并排不会出错。逐张扫完三窗格重点看两个位置热目标轮廓是否清晰可见光纹理如路面标线、树枝是否保留。5.2 消融实验写指标 CSV验证参数组合时把每组配置跑出的信息熵和平均梯度写入 CSV再做横向比较。import csv configs [ {rule: max, levels: 4}, {rule: energy, levels: 4, win: 5}, {rule: energy, levels: 4, win: 7}, {rule: mean, levels: 4}, ] with open(metrics.csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([rule, win, levels, entropy, avg_grad]) for cfg in configs: fused fuse_image(ir, yuv[:, :, 0], levelscfg[levels], rulecfg[rule], wincfg.get(win, 5)) m image_metrics(fused) writer.writerow([cfg[rule], cfg.get(win, -), cfg[levels], m[entropy], m[avg_grad]])筛选参数时优先看平均梯度和三窗格局部放大的平衡平均梯度最高的配置可能噪声也最多需要回到图片上确认边缘没有“毛刺”。最终选参标准应当是“目标一眼可见、背景纹理连续、边缘无重影”三者同时满足指标只作为辅助证据。本文还有配套的精品资源点击获取