简介:在夜间或弱光环境中,可见光图像往往缺乏目标细节,红外图像则能捕捉热辐射信息;将两者融合已成为图像增强的重要手段。面向计算机视觉课程设计与期末大作业场景,这套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, rule="energy", win=5): """对某一层拉普拉斯金字塔做融合决策。""" 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), dtype=np.float32) / (win * win) ei = cv2.filter2D(li.astype(np.float32), -1, kernel, borderType=cv2.BORDER_REFLECT) ev = cv2.filter2D(lv.astype(np.float32), -1, kernel, borderType=cv2.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, rule="energy", win=5): 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), dtype=np.float32) / (win * win) ei = cv2.filter2D(li.astype(np.float32), -1, kernel, borderType=cv2.BORDER_REFLECT) ev = cv2.filter2D(lv.astype(np.float32), -1, kernel, borderType=cv2.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, levels=4, rule="energy", win=5): 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]), interpolation=cv2.INTER_LINEAR) # YUV 空间只替换 Y 亮度分量,颜色由可见光的 UV 提供 yuv = cv2.cvtColor(vis, cv2.COLOR_BGR2YUV) y, u, v = cv2.split(yuv) fused_y = fuse_image(ir, y, levels=4, rule="energy", win=5) 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]。近似层决定整体亮度和背景,这个位置提高红外权重,不会像高频层那样引入噪点。
下表给出参数选择的经验范围。
| 参数 | 建议范围 | 影响 |
|---|---|---|
| levels | 3~5 | 层数太少,多尺度优势不明显;太多则最小层过小且耗时增加 |
| rule | energy / max | energy 抗噪,max 边缘锐利但噪点多 |
| win | 3 / 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, ksize=3) gy = cv2.Sobel(img_gray, cv2.CV_32F, 0, 1, ksize=3) 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 改为 4,rule 改为 max 或 energy |
| 背景细节丢失 | 红外近似层权重过高 | 把 0.7 调回 0.5 左右 |
| 噪声颗粒明显 | rule=max 放大了红外噪声 | 改 energy,win 从 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], levels=LEVELS, rule=RULE, win=WIN) 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(f"out/{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="", encoding="utf-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], levels=cfg["levels"], rule=cfg["rule"], win=cfg.get("win", 5)) m = image_metrics(fused) writer.writerow([cfg["rule"], cfg.get("win", "-"), cfg["levels"], m["entropy"], m["avg_grad"]])筛选参数时优先看平均梯度和三窗格局部放大的平衡:平均梯度最高的配置可能噪声也最多,需要回到图片上确认边缘没有“毛刺”。最终选参标准应当是“目标一眼可见、背景纹理连续、边缘无重影”三者同时满足,指标只作为辅助证据。
本文还有配套的精品资源,点击获取