简介:面向图像融合研究与算法评测的Python实用工具包,针对信息熵、空间频率、标准差、峰值信噪比、均方误差、互信息、视觉保真度、平均梯度、相关系数、差异相关和、基于梯度的融合性能Qabf、结构相似度SSIM、多尺度结构相似度MS-SSIM、基于噪声评估的融合性能Nabf等十余种指标提供统一实现,可快速量化不同融合算法的效果,适合论文实验、横向对比或工程验收等场景。压缩包共540个文件、约123.55MB,其中504个PNG测试图像用于结果展示与可视化对比,9个Python脚本为核心评估实现,另有pyc编译文件、XML工程配置、README说明和.gitignore等辅助内容,目录组织便于直接调用。脚本支持单幅图像评估、单算法全量融合结果评估,以及一次性计算所有对比算法的指标,并可将结果写入Excel,省去手工整理数据的时间;同时兼容常见图像读取与处理流程,方便接入已有研究项目。已有5357人学习,适合图像融合、质量评价方向的研究者与开发者,是算法验证和横向对比的实用工具。 做图像融合相关的研究或工程落地,最让人头疼的往往不是算法本身,而是怎么向别人证明“这张融合图确实比那张好”。尤其是多聚焦图像融合、多模态医学图像融合、红外与可见光融合这类场景,没有标准答案,光靠肉眼去看很容易被主观感受带偏。我在实际项目里踩过不少次这个坑:自己辛辛苦苦调了一版融合算法,主观上看觉得细节好了不少,结果汇报的时候拿不出量化数据,别人一句“好在哪?”就把我问住了。后来我把图像融合评估指标整套在 Python 里落了地,才算把这块短板补上。
这篇内容把这些指标的具体含义、适用场景、计算公式和 Python 实现一次性讲清楚,同时附上我在实操过程中积累的注意事项和踩坑经验。无论你是刚接触图像融合的学生,还是已经在做算法评测的工程师,都可以直接照着这套思路搭建自己的评估脚本。
1. 评估需求拆解:先搞清楚你融合的是哪一类图
1.1 图像融合的三种典型场景
图像融合的本质是把多幅来自同一场景但信息互补的图像合并成一张信息更丰富的图像。按应用场景分,主流的有三类。
第一类是多聚焦融合。镜头景深有限,拍微距或者场景纵深很大的照片时,很难让所有物体都清晰。多聚焦融合就是把焦点在前景和焦点在背景的多张图合成一张全清晰的图。这类融合结果通常存在一张理想的“全清晰图”,只是现实中拍不到,所以我们只能用间接指标去评价。
第二类是多模态医学图像融合。比如把 MRI 的软组织信息和 CT 的骨骼信息融合到同一张图上,帮助医生做诊断。这类场景对组织结构和边缘纹理的保持要求极高,评估时更看重结构相似性和边缘保留程度。
第三类是红外与可见光融合。红外图能突出热目标但在背景纹理上很弱,可见光图纹理丰富但在夜间或者遮挡场景下目标不清晰。融合的目的是兼顾热目标检测和场景理解,常用于安防监控和自动驾驶感知。
1.2 指标选择的底层逻辑:有参考和无参考两条路
评估指标必须先分类,因为不同融合场景能拿到的“标准答案”是不一样的。
有参考评估是指存在一张理想的参考图像,或者可以通过实验构造出参考图。比如多聚焦融合中,可以用全清晰图像作为理想参考参与计算。有参考指标计算简单、解释性强,最常用的包括峰值信噪比(PSNR)、均方根误差(RMSE)、结构相似性指数(SSIM)等。
无参考评估则是没有任何参考图时走的路。实际工程中绝大多数情况都属于这一类,因为真正理想的融合图是不存在的。无参考指标通过计算融合图自身的信息量、梯度能量、边缘保持度等特性来间接衡量融合质量。
我的经验是,实际评估时绝不能只依赖某一个指标,应该把有参考和无参考的指标组合成一个评估矩阵,从多个维度同时考察融合算法的表现。这个思路在后面 Python 实现中会具体体现出来。
2. 核心评估指标详解:公式、意义与适用边界
2.1 像素级基础指标:RMSE、PSNR 与相关系数
均方根误差(RMSE)是最直观的参考指标,它计算融合图与参考图之间逐像素差异的均方根值。数值越小表示融合图越接近参考图。公式上就是先算两张图的差,求平方再求平均,最后开根号。
峰值信噪比(PSNR)本质上是对 RMSE 做了一次对数变换。PSNR 的单位是 dB,数值越高说明失真越小。它设定了一个像素值动态范围的上限(通常是 255),然后与 RMSE 做比值,最后取对数乘以 20。实际项目中 PSNR 高于 30dB 通常已经算可以接受,但这个阈值会随图像内容剧烈变化,不能盲目套用。
相关系数(CC,Correlation Coefficient)衡量的是融合图和参考图之间的线性相关程度,范围在 -1 到 1 之间。CC 接近 1 说明两张图的整体亮度变化趋势高度一致。CC 对轻微的像素值偏移不敏感,所以它可以和 PSNR 互补,一起揭示融合图的“整体调性”是否准确。
这三个指标计算成本低、可解释性强,适合做算法调优过程中的快速反馈。但它们的局限也很明显:对图像的空间结构感知能力很弱,两张视觉上差异巨大的图,可能算出几乎一样的 PSNR。所以在结构敏感的场景里,我会更看重 SSIM。
2.2 结构感知指标:SSIM 与多尺度结构相似性
SSIM(Structural Similarity Index)是图像质量评估领域使用频率最高的指标之一。它的核心思路很朴素:人眼对图像结构的感知,主要体现在亮度、对比度和结构三个方面的相似性上。SSIM 把这三点分别建模,然后做乘积融合。
用个生活化的类比:如果拿两张照片 P 图和原图对比,PSNR 只关心“每个像素的颜色差了多少”,而 SSIM 会更像人眼——先看整体明暗是否一致(亮度),再看对比是否强烈(对比度),最后看纹理结构是否吻合(结构)。所以 SSIM 往往比 PSNR 更符合主观感受。
MS-SSIM(Multi-Scale SSIM)是 SSIM 的增强版。它把图像降采样成多个尺度,在每个尺度上分别计算对比度和结构相似度,最后加权融合。为什么需要多尺度?因为人眼看图时,既会关注整体布局,又会关注局部细节。单尺度 SSIM 可能漏掉某些尺度上的失真。我在评估医学图像融合时倾向使用 MS-SSIM,因为组织结构往往跨尺度存在,多尺度评估更可靠。
2.3 融合场景专属的无参考指标:边缘保持度、互信息与空间频率
无参考指标是融合评估的重头戏,因为它们真正回答的是“没有参考图时,怎么判断融合质量”。
边缘保持度 QAB/F是融合领域一个非常经典的指标。它先把融合图和两张输入图都提取梯度,分别计算融合图对每张输入图边缘强度和方向的保留程度,最后加权得到总分。QAB/F 的取值在 0 到 1 之间,越接近 1 说明融合图越完整地保留了输入图的边缘细节。这个指标对多聚焦融合和红外可见光融合特别有效,因为这两类场景最核心的要求就是边缘不丢失。
互信息(MI)源自信息论,衡量的是融合图从两张输入图中“提取”到了多少信息。直观理解就是:融合图应该同时携带输入图 A 和输入图 B 的独特信息,如果某张输入图的信息在融合图里完全丢失,那么互信息就偏低。计算上需要估计图像的概率分布和联合概率分布,通常通过直方图统计来实现。
空间频率(SF,Spatial Frequency)衡量融合图在水平和垂直方向上的灰度变化频率。SF 越高,说明图像包含的高频细节越多,整体越清晰锐利。但它容易受到噪声干扰,所以单独看 SF 意义不大,需要配合其他指标一起评估。
我把这些指标的使用建议整理成了下表:
| 指标 | 类型 | 适用场景 | 参考方向 | 计算成本 |
|---|---|---|---|---|
| RMSE | 有参考 | 快速回归调优 | 越小越好 | 极低 |
| PSNR | 有参考 | 通用质量评估 | 越大越好 | 低 |
| SSIM | 有参考 | 结构敏感场景 | 越接近1越好 | 中 |
| MS-SSIM | 有参考 | 医学图像、跨尺度内容 | 越接近1越好 | 中 |
| 互信息 MI | 无参考 | 信息保留度评估 | 越大越好 | 中 |
| 边缘保持度 QAB/F | 无参考 | 多聚焦、红外可见光 | 越接近1越好 | 较高 |
| 空间频率 SF | 无参考 | 清晰度快速检测 | 越大越好 | 低 |
3. Python 环境搭建与工具库选型
3.1 依赖安装的注意事项
做图像融合评估,Python 生态里最常用的库是 numpy、scikit-image 和 opencv-python。numpy 负责数值计算,scikit-image 提供 SSIM、PSNR 等现成实现,opencv 负责图像读取和基础处理。
安装时有个顺序和建议。如果你用的是 Linux 系统,直接用 pip 安装最稳妥:
pip install numpy scikit-image opencv-python如果你用的是 Windows,我建议优先用 Anaconda 或 Miniconda 创建虚拟环境,再在虚拟环境里安装依赖,避免系统 Python 环境被弄乱。特别是同时装了多个 Python 版本的时候,一定要先激活目标环境再执行安装命令,否则容易出现包装到了别的解释器里,导入时报错找不到模块的情况。
关于 scikit-image 的版本,不同 API 变化比较大。以 SSIM 为例,0.16 之前用skimage.measure.compare_ssim,0.16 之后改名成skimage.metrics.structural_similarity。如果你的代码是从旧项目里抄的,注意检查版本兼容问题。我后来习惯了统一用skimage.metrics下的新接口,顺便减少一个踩坑点。
3.2 开发环境与调试建议
我建议使用 Jupyter Notebook 做指标计算的快速验证,因为图像融合评估会频繁做可视化对比,Notebook 的交互式输出非常适合。等代码验证稳定后,再整理成独立的 .py 脚本,方便批量处理数据。
Visual Studio Code 配合 Python 插件是写脚本阶段的好选择。配置好 Python 解释器和调试断点,处理大批量图像评估时会省大量时间。如果只写代码不调试,定位问题时只能靠 print 输出,效率会低很多。
4. 评估脚本的核心实现细节
4.1 图像读取与数据预处理
处理的第一步是统一图片读入格式。opencv 的imread默认读进来是 BGR 通道顺序,而 scikit-image 通常按 RGB 处理。如果混用,评估结果会被通道顺序莫名其妙地影响。我的习惯是统一用 opencv 读图,然后显式转换成 RGB:
import cv2 import numpy as np def load_image(path, size=None): # 按 BGR 读入后转 RGB,避免通道顺序混淆 img = cv2.imread(path) if img is None: raise ValueError(f"图像读取失败,请检查路径: {path}") img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) if size is not None: img = cv2.resize(img, size, interpolation=cv2.INTER_LINEAR) return img.astype(np.float64)注意最后转成 float64 类型。很多指标计算涉及平方、开方、对数等操作,uint8 类型下做运算容易溢出或丢失精度,先转 float 是避免这类问题最简单的手段。
4.2 有参考指标的 Python 实现
PSNR 的实现非常简单,可以直接手写,也可以直接用 scikit-image 的peak_signal_noise_ratio函数。这里给出手写版本,方便你理解它到底在算什么:
def calculate_psnr(img_true, img_test, dynamic_range=255.0): mse = np.mean((img_true - img_test) ** 2) if mse == 0: return float('inf') psnr = 10 * np.log10(dynamic_range ** 2 / mse) return psnrSSIM 直接用 scikit-image 的接口更可靠,因为它的实现经过了充分验证,还支持多通道和 Gaussian 窗口配置:
from skimage.metrics import structural_similarity as ssim def calculate_ssim(img_true, img_test): # multichannel 参数根据输入维度自动判断 win_size = 7 # 如果图像尺寸小于 win_size,需要动态调整 min_dim = min(img_true.shape[:2]) if min_dim < win_size: win_size = min_dim if min_dim % 2 == 1 else min_dim - 1 score, _ = ssim(img_true, img_test, win_size=win_size, full=True, channel_axis=-1) return score这里有一个容易被忽略的坑:SSIM 的窗口大小不能超过图像尺寸,小尺寸图像直接调用默认参数会报错。所以我加了一个动态调整逻辑,当图像过小时自动缩小窗口。
4.3 无参考指标的 Python 实现
互信息的计算需要先估计两张图像的联合概率分布。最简单粗暴的方式是计算二维直方图,然后归一化得到联合概率。下面是完整的实现:
from scipy import ndimage def calculate_mutual_information(img_a, img_b, bins=64): # 先离散量化到指定区间 a = (img_a - img_a.min()) / (img_a.max() - img_a.min() + 1e-10) b = (img_b - img_b.min()) / (img_b.max() - img_b.min() + 1e-10) a = (a * (bins - 1)).astype(np.uint8) b = (b * (bins - 1)).astype(np.uint8) # 二维直方图 hist_2d, _, _ = np.histogram2d(a.ravel(), b.ravel(), bins=bins) # 归一化得到联合概率 p_ab = hist_2d / hist_2d.sum() # 边缘概率 p_a = p_ab.sum(axis=1) p_b = p_ab.sum(axis=0) p_b = p_b[:, np.newaxis] p_a = p_a[np.newaxis, :] # 计算互信息,避免 log(0) p_ab_safe = np.clip(p_ab, 1e-12, None) p_a_safe = np.clip(p_a, 1e-12, None) p_b_safe = np.clip(p_b, 1e-12, None) mi = np.sum(p_ab * np.log(p_ab_safe / (p_a_safe @ p_b_safe))) return mi互信息计算里最需要留意的是零概率问题。图像灰度分布往往集中在有限区间,二维直方图会有很多格子计数为零,直接取对数会得到负无穷。加一个小的 epsilon 再 clip 是常规操作。
边缘保持度 QAB/F 的实现相对复杂,这里给出简化版本的核心逻辑:先用 Sobel 算子求两张输入图和融合图的梯度幅值,然后逐像素计算边缘强度保留度,最后加权平均。完整项目级实现建议参考网上开源的 qabf 实现,核心代码如下:
def edge_strength(img): # Sobel 梯度幅值 gx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3) gy = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3) return np.sqrt(gx ** 2 + gy ** 2) def edge_preservation(img_a, img_b, img_f): g_a = edge_strength(img_a) g_b = edge_strength(img_b) g_f = edge_strength(img_f) # 融合图相对输入图的边缘强度保持比例 ratio_af = np.minimum(g_f / (g_a + 1e-10), (g_a + 1e-10) / (g_f + 1e-10)) ratio_bf = np.minimum(g_f / (g_b + 1e-10), (g_b + 1e-10) / (g_f + 1e-10)) q_af = np.mean(ratio_af) q_bf = np.mean(ratio_bf) return np.mean([q_af, q_bf])这个简化版虽然不如完整版那么精细,但在算法调优时足够判断相对好坏。需要精确数值用于论文发表时,建议使用完整版实现。
4.4 把多个指标封装成统一评估脚本
实际的评估场景不会只算一个指标。我习惯把所有指标封装到一个类里,传进去两张输入图和融合图,一次性返回全部指标:
class FusionMetrics: def __init__(self, img_a, img_b, img_f, has_reference=None): self.img_a = img_a.astype(np.float64) self.img_b = img_b.astype(np.float64) self.img_f = img_f.astype(np.float64) self.has_reference = has_reference def compute_all(self): metrics = {} metrics['SF'] = self.spatial_frequency() # 融合图与输入 A 的互信息 metrics['MI_A'] = calculate_mutual_information(self.img_a, self.img_f) metrics['MI_B'] = calculate_mutual_information(self.img_b, self.img_f) metrics['MI_Sum'] = metrics['MI_A'] + metrics['MI_B'] metrics['Q_ABF'] = edge_preservation(self.img_a, self.img_b, self.img_f) # 如果有参考图,再算有参考指标 if self.has_reference is not None: metrics['PSNR'] = calculate_psnr(self.has_reference, self.img_f) metrics['SSIM'] = calculate_ssim(self.has_reference, self.img_f) return metrics def spatial_frequency(self): row_diff = np.diff(self.img_f, axis=0) col_diff = np.diff(self.img_f, axis=1) rf = np.mean(row_diff ** 2) cf = np.mean(col_diff ** 2) return np.sqrt(rf + cf)封装成类的好处是,后面要评估新的融合结果时,不管融合算法是自己写的还是复现的,只要传入相同的输入图和融合图,就能得到统一格式的指标报告,大大减少重复劳动。
5. 评估实操中的典型问题与避坑经验
5.1 指标结果互相矛盾怎么办
实际评估中经常会出现一个指标变好、另一个指标变差的情况。比如某次多聚焦融合实验中,QAB/F 从 0.68 提升到 0.72,但互信息反而从 1.83 降到了 1.65。最直接的归因是:融合算法加强了边缘纹理的保留,但牺牲了部分亮度分布的一致性,导致融合图与输入图之间的信息重叠部分减少。
遇到这种情况,不要急着否定算法,先检查几个技术细节。第一,确认两张输入图和融合图的尺寸完全一致,尺寸不一致会导致指标计算错位。第二,确认像素值范围一致,如果一张是 0 到 255,另一张是 0 到 1,计算出来的指标基本不可信。第三,确认三张图的文件名对应关系没有张冠李戴。
如果技术细节没问题还是矛盾,就需要结合实际应用场景做取舍。目标检测场景下的红外与可见光融合,优先看边缘保持度和互信息,因为这两个指标对目标轮廓和上下文信息更敏感。视觉效果展示场景,多参考 SSIM 和空间频率。我的原则是:指标服务于应用目标,不服务于算法“分数好看”。
5.2 小尺寸图像的评估陷阱
小尺寸图像是评估里非常容易翻车的地方。比如一张 32x32 的红外图像块,直接调用 SSIM 的默认 win_size=7 反而还正常,但如果下采样到 16x16,窗口就会超过图像尺寸。
另一个典型问题是梯度类指标在小尺寸图像上噪声巨大。图像只有几十个像素时,一两个异常像素对空间频率的影响会被放大到离谱的程度。所以遇到小尺寸测试图,我一般会做两件事:一是统一调整到合理尺寸后再评估,比如 256x256;二是在加载图像时做去噪处理,用高斯滤波把孤立噪声点先磨掉,再算指标。
5.3 批量评估时的数据管理
批量评估多组图像时,最怕的是结果和图像对应不上。我早期吃过一次亏:批量跑了 20 组实验,最后汇总数据时发现文件命名顺序不对,导致整批评估结果作废。后来改用字典结构管理路径和结果:
import os import json def batch_evaluate(root_dir): results = {} files = sorted(os.listdir(root_dir)) for name in files: if not name.endswith('.png'): continue # 解析文件名中的输入图A、输入图B、融合图 parts = name.split('_') if len(parts) < 3: continue img_a = load_image(os.path.join(root_dir, parts[0] + '_A.png')) img_b = load_image(os.path.join(root_dir, parts[0] + '_B.png')) img_f = load_image(os.path.join(root_dir, name)) metrics = FusionMetrics(img_a, img_b, img_f).compute_all() results[name] = metrics # 导出 JSON 存档 with open('eval_results.json', 'w', encoding='utf-8') as f: json.dump(results, f, indent=2, ensure_ascii=False) return results文件命名规则越规范,后面的返工成本越低。强烈建议在项目开始前就约定统一的命名格式,比如“exp01_A.png、exp01_B.png、exp01_fused.png”,然后在评估脚本里写清楚对应关系。
5.4 Python 代码运行时的常见报错
我整理了几个新手容易碰到的报错和解决思路。
AttributeError 报错多出现在 scikit-image 版本过旧或过新导致 API 变化。0.19 及以后版本统一推荐skimage.metrics下的函数,如果报错就检查一下版本,必要时升级到 0.19 以上。
MemoryError 通常出现在批量处理高分辨率图像时。2048x2048 的灰度图算二维直方图还好,但如果是多通道的 4K 图像同时计算多个指标,内存就可能撑不住。对策是把图像转成灰度图再评估,或者分块计算。
TypeError 经常由数据类型不一致引起。比如一张图是 float64,另一张是 int,numpy 在做广播运算时会要么报错,要么悄悄做截断转换。我的统一做法是:在加载图像后立刻转 float64,后续所有计算都在浮点域进行。
6. 评估工具链的下一步扩展方向
我把这套评估脚本在项目里跑顺之后,发现它不仅能用于算法验收,还能反过来指导算法改进。比如每次调参后跑一组指标,把 QAB/F、互信息、SSIM 的变化趋势画成折线图,就能非常直观地看出哪次改动有效、哪次改动只是在“原地打转”。
对于重复性高、需要频繁做对比实验的场景,把评估脚本接进批处理流程也很值得做。用 pandas 汇总指标结果,用 matplotlib 生成对比图,做成一份自动化的评估报告,不仅省时间,汇报时也更有说服力。
以后如果数据量变大,还可以考虑引入深度学习模型来评估感知质量,但底层的像素级和结构级指标计算,仍然会长期作为图像融合评估的基础设施存在。把这套 Python 工具维护好,无论研究还是工程,都能省下大量精力。
本文还有配套的精品资源,点击获取