简介面向图像融合算法研究的Python评估指标工具包实现了EN、SF、SD、PSNR、MSE、MI、VIF、AG、CC、SCD、Qabf、SSIM、MS-SSIM、Nabf等十余种主流融合质量指标支持单幅图像、单算法全部结果批量评估也可一键计算所有对比算法并写入Excel大幅提升实验对比与论文数据产出效率。资源共540个文件zip压缩包约123.55MB其中504张png为测试图与融合结果图9个py脚本是核心指标实现另有README说明文档与配置文件结构清晰便于直接运行或二次开发。已有5389人学习下载适用于计算机视觉、图像处理方向的学生与科研人员。直接复用指标计算逻辑可省去自行实现评价指标的重复工作快速获得论文所需的客观量化结果。1. 图像融合评估指标只有在两个条件下才可信参考图选型与参数对齐做多聚焦融合、红外可见光融合这类实验融合图对比书和项目汇报往往绕不开一组量化指标——信息熵、标准差、平均梯度、空间频率、PSNR、SSIM、互信息。这些图像融合评估指标在论文里出现频率极高但真正用 Python 把它们算对并让结果能与别人的表格横向对比不是把公式敲进去就完事。这套资源解决的就是这个从零实现一套可运行的指标计算代码同时讲清每个指标在什么条件下可信、什么条件下会骗人。适合正在做融合算法复现、需要出指标对比表或者项目验收时想拿量化数据说明结果的你。图像融合没有统一的金标准指标只能在对比中讲故事。以下内容先给出无参考指标和有参考指标的 Python 实现再讲封装与批量评估最后落到我最常踩的五个坑。全程代码可以直接复制进项目里改。2. 无参考指标先行信息熵、标准差、平均梯度与空间频率的实现2.1 为什么先算信息熵和平均梯度无参考指标是指不需要标准参考图、直接对融合图本身做统计的指标适合在多数真实融合场景里做首轮质量判断。信息熵衡量灰度分布的信息量熵高通常说明细节丰富但要注意噪声和高频伪影同样会拉高熵所以它只能作为“信息活跃度”的证据。平均梯度和空间频率衡量边缘强度与高频活跃程度融合图越清晰、边缘越锐利这两项数值一般越高。标准差反映图像整体灰度离散程度对比度强的融合图标准差偏大。这四个指标计算量小、不依赖参考图适合在批量对比时当第一道筛选。我在实际评估时通常先跑这四个若有较大差异再上带参考的指标避免一开始就被有参考指标的具体数值带偏。2.2 四个指标的 numpy 实现与边界处理下面的代码把公共的灰度转换逻辑抽成一个函数避免四个指标各自处理彩色图与归一化。代码中的注释标了关键行为import numpy as np def _as_gray(image): 统一输入格式单通道 float64范围 0~255。 彩色图按亮度方程转灰度0~1 范围的图自动放大到 0~255。 image np.squeeze(image) image image.astype(np.float64) if image.ndim 3: image 0.299 * image[..., 0] 0.587 * image[..., 1] 0.114 * image[..., 2] if image.max() 1.0: image image * 255.0 return image def information_entropy(image, bins256): 信息熵默认按 256 个灰度级统计直方图。 img _as_gray(image) hist, _ np.histogram(img.ravel(), binsbins, range(0, 255)) p hist / hist.sum() p p[p 0] return -np.sum(p * np.log2(p)) def standard_deviation(image): 标准差反映灰度离散程度。 img _as_gray(image) return np.std(img) def average_gradient(image): 平均梯度用中心差分近似水平/垂直梯度再求幅值均值。 img _as_gray(image) gx np.gradient(img, axis1) gy np.gradient(img, axis0) return np.mean(np.sqrt(gx ** 2 gy ** 2)) def spatial_frequency(image): 空间频率行频率与列频率的均方根。 img _as_gray(image) rf np.mean(np.diff(img, axis1) ** 2) cf np.mean(np.diff(img, axis0) ** 2) return np.sqrt(rf cf)四个函数在逻辑上有一个共同约定所有指标都通过_as_gray转成单通道、0~255 范围的float64。这样做的好处是不管你传入的是cv2.imread读出的 uint8 图、imageio读出的浮点图还是形状为(H, W, 1)的单通道图指标计算都能对齐。参数上要注意三点。information_entropy的bins256对应 8bit 灰度图如果融合图本身是 12bit 或 16bit 医学影像256 个 bin 会丢失高灰度级分布需要把range同步改成实际最大值。average_gradient用np.gradient而不是np.diff因为np.diff会让输出尺寸缩减边缘像素被丢弃而np.gradient在边界用单边差分补齐结果更稳定。spatial_frequency里np.diff虽然也让尺寸变小但由于np.mean是对所有元素做归一化行频率与列频率之间不存在维度失配问题平方后再开方就是常见定义。2.3 无参考指标联合判读什么时候该信、什么时候该怀疑无参考指标的绝对值意义有限更可靠的是横向对比。同一个数据集下算法 A 的熵比算法 B 高 0.2平均梯度高 3.5空间频率高 8.2这时可以判断 A 在细节保留上更有优势。反过来如果某个融合图熵特别高但标准差也不低别急着下结论熵和标准差同时升高可能是噪声被放大需要结合融合图的灰度直方图看是否有长尾。我一般还会加一个限定条件无参考指标适合初筛不适合终极验收。信息熵不感知空间结构一张随机噪声图也能得到很高的熵平均梯度只衡量梯度幅值不判断梯度方向是否合理。要最终证明融合质量还是需要有参考指标或人工判读。把这四个指标当成“融合图是否足够活跃”的探测器而不是“融合是否正确”的证据会比较稳。3. 有参考指标选型与实现PSNR、SSIM 和互信息的关键参数3.1 没有参考图像时怎么建立评估基准有参考指标需要一个前提存在一张“标准答案”参考图。多聚焦融合数据集通常配套全聚焦参考图红外与可见光融合如果有显式真值模型也能拿到参考。但很多真实场景并没有参考图这时常见做法是用源图中清晰区域拼出一张近似参考图或者用公认效果稳定的融合算法结果当作代理参考。我一般更倾向于用前者因为代理参考会天然偏向某类算法导致指标比较失真。参考图确定后接下来所有有参考指标都围绕“融合图与参考图之间的差异”展开。数据范围是这组指标的第一约定参考图若是 uint8data_range 传 255若是 0~1 浮点图data_range 必须传 1。这个参数错一个数量级PSNR 会直接差出十几 dB。3.2 PSNR 实现data_range 是数值能否对比的命门PSNR 公式本身很简单但工程实现里最容易被坑的是 MSE 的计算范围。看下面的实现def psnr(fused, ref, data_range255): f _as_gray(fused) r _as_gray(ref) mse np.mean((f - r) ** 2) if mse 0: return float(inf) return 10 * np.log10(data_range ** 2 / mse)两个输入都经过_as_gray转成 0~255 的 float64所以data_range255才成立。如果你读入的图是 0~1 浮点图_as_gray已经把它放大到 0~255这里仍然传 255 即可但如果你的代码里没有做这个转换直接拿 0~1 图算 MSE此时 data_range 应该传 1。我建议在模块里统一约定_as_gray负责把范围对齐到 0~255所有指标默认data_range255。这样项目里只有一处需要确认数据来源而非每个指标各自猜。MSE 为 0 时返回float(inf)是必要的。两张图完全一致时PSNR 定义上是无穷大不能直接参与后续平均否则排序会出错。实际批量评估里如果出现 inf说明融合图与参考图完全相同这种情况要么数据集有问题要么你拿错了图建议单独排查。3.3 SSIM调库与手写的差别在哪SSIM 比较的是局部窗口内的亮度、对比度和结构相似性窗口大小、高斯权重、边界填充都会影响结果。工程上最稳的是直接调 scikit-imagefrom skimage.metrics import structural_similarity as ssim_fn def ssim(fused, ref, win_size7, data_range255): score, _ ssim_fn( _as_gray(fused), _as_gray(ref), win_sizewin_size, data_rangedata_range, fullTrue, ) return float(score)fullTrue会返回一个像素级的ssim_map第 6 章做热力图诊断时还要用它。注意返回值顺序skimage 里开启fullTrue时返回(score, map)如果同时开启gradientTrue返回结构会变成三元组。我踩过一次忘记解包顺序的坑建议调用时显式写成score, _ ...。手写 SSIM 也不复杂常用的做法是均值滤波代替高斯滤波from scipy.ndimage import uniform_filter def ssim_numpy(fused, ref, win_size11, data_range255): f _as_gray(fused) r _as_gray(ref) C1 (0.01 * data_range) ** 2 C2 (0.03 * data_range) ** 2 mu1 uniform_filter(f, sizewin_size) mu2 uniform_filter(r, sizewin_size) v1 uniform_filter(f * f, sizewin_size) - mu1 ** 2 v2 uniform_filter(r * r, sizewin_size) - mu2 ** 2 cov uniform_filter(f * r, sizewin_size) - mu1 * mu2 ssim_map ((2 * mu1 * mu2 C1) * (2 * cov C2)) / ( (mu1 ** 2 mu2 ** 2 C1) * (v1 v2 C2)) return float(ssim_map.mean())手写版的uniform_filter默认边界填充是 reflect与 skimage 内部的边界处理并不完全一致所以手写值和调库值一定会有小数级差异。这不是代码写错而是实现细节不同。如果你只是要出对比表我建议直接调 skimage省得审稿人或验收方问一句“你的 SSIM 是怎么算的”时还要解释窗口权重。如果要在嵌入式或纯 numpy 环境里跑再用手写版。3.4 互信息联合直方图估计与 bins 选择互信息衡量融合图与参考图之间的统计依赖程度依赖联合直方图估计。实现短但结果对参数非常敏感def mutual_information(fused, ref, bins64): f _as_gray(fused) r _as_gray(ref) hist2d, _, _ np.histogram2d( f.ravel(), r.ravel(), binsbins, range[[0, 255], [0, 255]], ) pxy hist2d / np.sum(hist2d) px np.sum(pxy, axis1, keepdimsTrue) py np.sum(pxy, axis0, keepdimsTrue) return float(np.sum(pxy * np.log2(pxy / (px * py 1e-12))))bins64是经验折中bin 太少联合分布太粗互信息会偏低bin 太细单个 bin 内样本稀疏概率估计噪声变大互信息又会漂移。和论文结果对比时一定要先确认对方的 bins 和 range否则同一个融合图可能算出 0.3 与 1.2 的差距。代码里加的1e-12是防止某个联合概率出现在边缘情况下除零它会让极端稀疏处的对数结果偏大但正常图像下不影响有效数字。4. 把八个指标封装成一个评估模块批量 CSV 输出与参数约定4.1 模块接口设计统一输入、统一输出手动逐个调用上一章的指标函数来回转换图和改参数很容易顾此失彼。常见的做法是封装一个评估器类把所有参数集中在__init__里所有指标统一输入格式和输出字典。下面是一个可运行的骨架class FusionEvaluator: def __init__(self, data_range255, mi_bins64, ssim_win_size7): self.data_range data_range self.mi_bins mi_bins self.ssim_win_size ssim_win_size def evaluate(self, fused, refNone): f _as_gray(fused) result { entropy: information_entropy(f), std: standard_deviation(f), average_gradient: average_gradient(f), spatial_frequency: spatial_frequency(f), } if ref is not None: ref _as_gray(ref) result[psnr] psnr(f, ref, data_rangeself.data_range) result[ssim] ssim(f, ref, win_sizeself.ssim_win_size, data_rangeself.data_range) result[mi] mutual_information(f, ref, binsself.mi_bins) return result这个接口的要点是refNone时只算无参考指标适合没有真值的场景有参考图时自动补齐有参考指标。data_range、mi_bins、ssim_win_size作为类参数暴露出来批量对比时只改一处即可统一全部指标口径。如果你在实验里还要算相关系数和 RMSE可以继续在ref分支里加但不要把这些值混进无参考结果里否则 CSV 表格的表头会变得难以解释。4.2 批量导出 CSV对比表与排序的常见写法单张图评估只是热身融合实验通常要跑一组算法。下面这个批量函数把结果写进 CSV并可按 SSIM 降序排序import csv def batch_evaluate(fused_paths, ref_path, output_csvmetrics.csv): ev FusionEvaluator() header [name, entropy, std, average_gradient, spatial_frequency, psnr, ssim, mi] rows [] for path in fused_paths: fused _load_image(path) ref _load_image(ref_path) res ev.evaluate(fused, ref) name path.split(/)[-1] rows.append([name] [res[k] for k in header[1:]]) rows.sort(keylambda r: r[header.index(ssim)], reverseTrue) with open(output_csv, w, newline, encodingutf-8) as fp: writer csv.writer(fp) writer.writerow(header) writer.writerows(rows)_load_image里我一般会固定用cv2.imread(path, cv2.IMREAD_UNCHANGED)或np.load读取灰度数据并现场确认通道顺序。排序放在写文件之前这样输出的 CSV 第一行永远是综合表现最好的算法。newline是防止 Linux 与 Windows 下 CSV 出现空行这个细节在交付给别人跑的时候特别容易忽略。4.3 参数约定表一份可以写进项目注释的口径下面这张表是我在做指标对比前会先固定在代码注释里的口径避免每轮实验参数漂移指标参考依赖关键参数口径约定信息熵无bins256灰度级固定 0~255标准差无无单通道全局统计平均梯度无np.gradient边界用单边差分空间频率无np.diff尺寸差异由 mean 抵消PSNR有data_range255输入统一放大到 0~255SSIM有win_size7调 skimagefullTrue互信息有bins64range 固定 0~255这张表写到项目文档或代码注释里以后每轮实验只需要对表确认参数有没有被改动。我习惯把这份口径固定在评估脚本的 docstring 中防止两周后回来看结果时忘了 SSIM 窗口是 7 还是 11。5. 避坑指南融合指标计算中常见的五个翻车现场5.1 PSNR 比论文低十几 dB先查 data_range 而不是查公式现象同一个融合图按网上公式算出的 PSNR 和论文结果差开 10~20 dB方程怎么检查都找不出问题。原因绝大多数情况是数据范围没对齐。uint8 图的像素值范围是 0~255浮点图是 0~1公式里的MAX^2若用了错误的 255 或 1结果会直接差出约 48 dB。我也见过有人把读入的 float64 图直接当 0~255 用导致 MSE 虚高。解决在评估模块入口统一用_as_gray把范围强制对齐到 0~255data_range全部固定传 255。任何从外部进来的图都过这一层不要再在 PSNR 内部单独猜一次。5.2 SSIM 报错、返回 NaN 或窗口尺寸异常现象输入小尺寸融合图比如 64×64时skimage 直接抛窗口尺寸错误某些平坦区域较多的图SSIM 结果出现 NaN。原因win_size不能大于图像宽高而纯色区域局部方差为 0公式分母趋近 0导致数值不稳定。解决调用前取win_size min(7, h, w)并强制为奇数对全是常数的图像区域SSIM 定义上应该接近 1可以在局部方差小于某个极小阈值时直接赋 1。实际批量评估里我给 skimage 的win_size设成 7但传入前先对尺寸做一次下限保护。5.3 互信息忽高忽低复现不出论文结果现象同一个融合图用不同脚本算互信息一次是 0.3一次是 1.2谁也不是明显的代码 bug。原因联合直方图的bins和range不一致。有人用 64有人用 256有人没有显式传range导致直方图覆盖范围不同概率估计完全变样。解决固定bins64、range[[0,255],[0,255]]并在所有实验记录中备注这两个值。想和某篇论文的结果对比第一件事是翻它的方法段或者代码确认它用的直方图配置。互信息是一个相对度量不是绝对真理参数不同就不可比。5.4 彩色融合图直接计算三通道被拍平导致熵和 MI 失真现象彩色融合图用np.loadtxt或 PIL 读入后形状是(H, W, 3)。直接传给信息熵函数结果异常高且多张图之间没有明显区分度。原因ravel()会把 RGB 三通道顺序拼接成一个长向量直方图统计的是三通道混合分布而不是灰度分布。只要通道顺序一变BGR 与 RGB结果就跟着变。解决所有指标入口统一走_as_gray把彩色图按 0.299R 0.587G 0.114B 转成单通道。如果项目的融合结果本身就是彩色图建议评估时固定使用同一个灰度转换函数不要一会儿用np.mean(axis2)一会儿用亮度方程因为这两种灰度化方式会产生系统性差异。5.5 梯度类指标随边界处理方式小范围抖动现象平均梯度和空间频率的值在不同机器上跑出微小差异换一个差分实现后差异变大复现实验时数据对不上。原因np.diff会让输出尺寸变小边缘像素被丢弃np.gradient在边界用单边差分np.pad模式不同也会影响边界取值。一张 512×512 的图边缘几百个像素在梯度类指标里占比不大但足以让小数的最后两位变化。解决统一用np.gradient(img, axis1)与np.gradient(img, axis0)计算平均梯度空间频率则固定用np.diff后再做均值。关键是别混用同一个项目里所有梯度类指标只选一种差分实现并在代码注释里写明。边界引起的差异虽然小但对比表格里小数点后两位对不上解释起来很麻烦。6. 用 SSIM 局部热力图定位融合失真区域一个可复现的验证技巧全局指标只能告诉你“融合质量平均是好是坏”但融合结果往往在某些区域明显失真、在某些区域几乎无损。只看平均分你甚至不知道问题出在边缘还是平坦区域。我的习惯是跑完指标对比后再输出一张 SSIM 局部热力图用像素级质量分布指导下一步调参。import matplotlib.pyplot as plt def ssim_heatmap(fused, ref, win_size7, data_range255): fused _as_gray(fused) ref _as_gray(ref) score, ssim_map ssim_fn( fused, ref, win_sizewin_size, data_rangedata_range, fullTrue, ) plt.imshow(ssim_map, cmapjet, vmin0, vmax1) plt.colorbar() plt.title(fSSIM map (global SSIM{score:.4f})) plt.show()这段代码的关键是fullTrue返回的ssim_map与输入图尺寸一致每个像素映射到局部窗口的 SSIM 值。观察热力图时我会按颜色分布做三类判断低 SSIM 集中在边缘说明融合图与参考图存在边缘位置偏移或重影低 SSIM 集中在平坦区域多半是色偏或分块痕迹低 SSIM 呈随机散点很可能来自噪声。这个技巧对多聚焦融合尤其有效。多聚焦融合最容易出问题的是分界线附近热力图里能看到一条明显的低 SSIM 条带这比全局指标更能说明融合规则在边界处的决策是否合理。对红外与可见光融合热力图还能暴露目标区域对比度不足的位置比单看 PSNR 直观得多。从那以后我每次提交融合评估结果前都会强制走一遍完整流程先固定 data_range、bins、win_size 这套口径再跑批量指标对比表最后看一眼 SSIM 热力图确认空间分布没有意外。如果热力图暴露了局部失真我会回头查融合权重图或分块参数而不是直接改指标公式。这套流程把“图像融合评估指标”从黑匣子变成了可定位问题的诊断工具希望帮到你。本文还有配套的精品资源点击获取