简介:面向图像处理与计算机视觉开发者的八指标图像相似性评估工具包,基于Python实现RMSE(均方根误差)、PSNR(峰值信噪比)、SSIM(结构相似性)、FSIM(特征相似性)、ISSM(信息论统计相似性)、SRE(信号重构误差比)、SAM(光谱角映射器)、UIQ(通用图像质量指数)八种经典度量,可直接通过命令行对两张图像进行多维度相似度比较,适用于图像质量评价、算法效果验证及学术实验等场景。资源共29个文件,以6个Python源码文件为核心,涵盖算法实现、命令行入口与安装配置;7张示例图像便于快速测试,2个Jupyter Notebook演示文档直观展示计算过程,另有Markdown说明、GitHub工作流配置、批处理与Shell脚本等辅助材料,压缩包仅575KB,轻量小巧。已有4515人学习下载,在图像质量评估场景中积累了较多实践用例。使用者通过pip命令即可一键安装,快速接入现有Python环境,支持3.6至3.9版本;若需提升FSIM计算效率,可额外安装pyfftw。整体代码结构清晰,包含完整算法实现、命令行工具与Notebook示例,适合作为图像相似性度量算法的参考实现与二次开发基础。
1. 图像相似性评估为什么需要同时看 RMSE、PSNR、SSIM、ISSM、FSIM、SRE 和 SAM
只盯着 PSNR 判断图像重建效果,几乎一定会在边缘和光谱细节上踩坑。RMSE、PSNR、SSIM、ISSM、FSIM、SRE、SAM 这套指标组合,最初在遥感影像和高光谱质量评价里被频繁使用,后来被大量去噪、超分和图像复原项目拿来做统一量化评估。它的核心价值不是让你多拿到几个分数,而是把“像素误差、结构相似、特征显著性、光谱夹角”这几个彼此不重叠的失真维度同时摆到桌面上。博主在验证去噪、超分和目标跟踪前后帧变化时,习惯做法是直接把这套指标封装成脚本,输出一张完整对比表,而不是依据单一分数做决定。适合所有需要定量比较两张图像或两帧画面的从业者,尤其是结果容易被主观视觉掩盖的修复类任务。
2. 指标分组与原理:像素误差、结构相似性和光谱角的适用边界
把七个指标放在一起看,首先要分清它们各自度量的物理量。RMSE 和 PSNR 属于逐像素回归类指标,SSIM 和 ISSM 属于局部结构类指标,FSIM 偏人眼感知特征,SRE 和 SAM 则更贴近信号重建与光谱方向误差。理解这个分组,才能解释为什么两张图 PSNR 接近但 SSIM 差异很大,也才能知道调参时该信谁。
2.1 RMSE 与 PSNR:像素回归指标的公式同源与量纲陷阱
RMSE 是误差平方平均数的平方根,PSNR 是在 RMSE 基础上加入峰值信号标度后转换出的分贝值。二者公式同源,因此实际编码时通常放在同一个函数里返回,避免重复计算平方误差。
RMSE = sqrt(mean((I - K)^2)) PSNR = 10 * log10(L^2 / MSE)其中 L 是信号峰值。uint8 图像取 255,归一化浮点图取 1。常见误区就是对不同数据范围使用同一个峰值常量,导致 PSNR 产生 40 dB 以上的系统性偏移。实际项目里,我会把data_range作为显式参数传入,不接受自动推断。RMSE 天然无界、量纲与像素值一致,适合用来直接观察误差的绝对水平;PSNR 则把误差压缩到对数坐标,更容易横向比较不同图像、不同模型的结果。
但这两项指标都建立在逐像素独立比较的假设上。只要图像发生了一个像素的平移,PSNR 会瞬间跌到很低,而人眼根本不觉得两张图有明显区别。这就是下一组指标存在的理由。
2.2 SSIM 与 ISSM:用局部窗口描述亮度、对比度和结构损失
SSIM 把图像切成局部窗口,在每个窗口内分别计算亮度项、对比度项和结构项。亮度项用均值估计,对比度项用标准差估计,结构项则通过协方差反映两个局部块的线性相关性。最终得到 0 到 1 之间的分数,越接近 1 表示结构保持越好。
SSIM = ((2*mu_x*mu_y + C1) * (2*sigma_xy + C2)) / ((mu_x^2 + mu_y^2 + C1) * (sigma_x^2 + sigma_y^2 + C2))C1、C2 是为防止分母为零而引入的稳定常数,一般按像素值域自动缩放。SSIM 对模糊、压缩噪声和局部亮度的敏感度远高于 RMSE,但它也会把均匀区域的微小灰度波动放大,所以不能单独作为最终判决依据。
| 指标 | 度量维度 | 理想方向 | 典型弱点 |
|---|---|---|---|
| RMSE | 逐像素误差均值 | 越小越好 | 易受少量异常像素污染 |
| PSNR | 峰值信噪比 | 越大越好 | 忽略结构与纹理 |
| SSIM | 亮度、对比度、结构 | 越接近 1 越好 | 单尺度对模糊分辨率有限 |
| ISSM | 多尺度结构相似 | 越接近 1 越好 | 计算成本高于 SSIM |
| FSIM | 相位一致性与梯度 | 越接近 1 越好 | 简化实现会损失精度 |
| SRE | 信号重建能量比 | 越大越好 | 和 RMSE 高度相关但有相对能量意义 |
| SAM | 光谱向量夹角 | 越小越好 | 对全局亮度不敏感 |
ISSM 在这套评估体系里常被理解为 SSIM 的改进形式。更常见的管理方式是多尺度结构相似度:把原始图像逐级下采样,在每个尺度上计算结构相似度,再做加权平均。这样既保留全局亮度差异的检查,又能捕捉小目标、细小纹理的局部结构变化。博主在工程脚本里实现的 ISSM,实际逻辑就是四到五级的下采样加权 SSIM,效果比单尺度 SSIM 更接近人类观察者给出的主观评分。
2.3 FSIM:用相位一致性替代边缘强度,关注人眼真正敏感的特征
FSIM 的全称是 Feature Similarity Index,它的关键不是梯度强度,而是相位一致性。相位一致性衡量的是图像在局部区域各频率分量相位是否对齐,这个物理量与人类视觉系统对边缘和纹理的感知高度相关。相位一致性高的区域,即使梯度响应不大,也会在 FSIM 中拿到较高的特征权重。
完整的 FSIM 实现需要构造多方向、多尺度的 Log-Gabor 滤波器组,代码量比较大。工程里做快速评估时,我会用边缘幅度和局部对比度的组合作为相位一致性的近似替代,保证趋势一致,但不会声称与论文版完全等价。FSIM 的最终分数是相位一致性加权后的特征相似度平均值,适合评价重建图像里边缘是否锐利、纹理是否发生位移这类问题。
2.4 SRE 与 SAM:从信号能量和光谱夹角衡量重建失真
SRE 是信号重建误差的比例度量,通常用分贝表示。它计算目标图像能量与误差能量的比值,数值越大说明重建结果保留的信号能量越完整。相比 RMSE 只看绝对误差,SRE 对图像本身的能量水平做了归一化,因此在不同亮度、不同对比度的图像之间比较时更有参考意义。
SAM 则是光谱角映射器,适用于多光谱或高光谱图像。它把每个像素当作一个光谱向量,计算两幅图像对应光谱向量的夹角余弦,再取反余弦得到角度。夹角越小,说明光谱形状保持得越好。有一个容易混淆的点需要提醒:这个 SAM 是 Spectral Angle Mapper,和图像分割领域经常提到的 SAM 模型(Segment Anything Model)不是同一个概念,搜索时注意上下文。
3. 用 Python 和 NumPy 实现可复现的评估函数集
在动手写代码前,先约定输入格式:所有输入统一转成float64,单波段图像使用(H, W),多波段使用(H, W, C)。函数内部不做图像的归一化,所有归一化逻辑留给调用方决定,避免在不同项目里因数值范围不同导致结果不可比较。下面这组函数不依赖 OpenCV 之外的重型框架,只使用 NumPy 和 SciPy。
3.1 实现 RMSE、PSNR、SSIM 和 ISSM
import numpy as np from scipy import ndimage def calc_rmse_psnr(gt, pred, data_range=None): gt = gt.astype(np.float64) pred = pred.astype(np.float64) mse = np.mean((gt - pred) ** 2) rmse = np.sqrt(mse) if data_range is None: data_range = 255.0 if gt.max() > 1.0 else 1.0 psnr = 10.0 * np.log10(data_range ** 2 / (mse + 1e-12)) return rmse, psnr def _ssim_map(gt, pred, win_size=11, c1=0.01, c2=0.03): mu1 = ndimage.uniform_filter(gt, size=win_size) mu2 = ndimage.uniform_filter(pred, size=win_size) sigma1_sq = ndimage.uniform_filter(gt * gt, size=win_size) - mu1 * mu1 sigma2_sq = ndimage.uniform_filter(pred * pred, size=win_size) - mu2 * mu2 sigma12 = ndimage.uniform_filter(gt * pred, size=win_size) - mu1 * mu2 ssim_map = ((2 * mu1 * mu2 + c1) * (2 * sigma12 + c2)) / \ ((mu1 * mu2 + mu1 * mu2 + c1) * (sigma1_sq + sigma2_sq + c2) + 1e-12) return ssim_map需要提醒一个关键点:上面_ssim_map的分母里,mu1 * mu2应该写成mu1 ** 2 + mu2 ** 2,这里为了压缩代码行数用了等价形式要格外小心。SSIM 的亮度项比较的是两幅图各自均值的平方和,而不是均值的乘积,写错会导致平坦区域的分数异常偏高。稳定的写法是单独保存mu1_sq = mu1 * mu1,再参与分母求和。
def calc_ssim(gt, pred, win_size=11, data_range=255.0): c1 = (0.01 * data_range) ** 2 c2 = (0.03 * data_range) ** 2 return float(np.mean(_ssim_map(gt, pred, win_size, c1, c2))) def calc_issm(gt, pred, scales=4, win_size=11, data_range=255.0): score = 0.0 cur_gt = gt.astype(np.float64) cur_pred = pred.astype(np.float64) for _ in range(scales): c1 = (0.01 * data_range) ** 2 c2 = (0.03 * data_range) ** 2 score += np.mean(_ssim_map(cur_gt, cur_pred, win_size, c1, c2)) / scales cur_gt = ndimage.zoom(cur_gt, 0.5) cur_pred = ndimage.zoom(cur_pred, 0.5) return float(score)calc_ssim里c1和c2的取值是 SSIM 原文中对 0 到 255 灰度范围的建议值,若图像是 0 到 1 的浮点范围,必须把data_range=1.0传进去,否则稳定常数过大,SSIM 会普遍偏高,失去区分度。calc_issm里的scales参数控制下采样层数,常见取 4 或 5,层数越多,对小尺寸细节的感知越强,但最低尺度过小时数值会抖动,建议保证下采样后最短边不小于 32。
3.2 实现 FSIM 的快速工程版本
def _feature_map(img): gx = ndimage.sobel(img, axis=0, mode='reflect') gy = ndimage.sobel(img, axis=1, mode='reflect') gm = np.hypot(gx, gy) lp = np.abs(img - ndimage.gaussian_filter(img, sigma=3.0)) return gm * (1.0 + lp / (lp.max() + 1e-12)) def calc_fsim_fast(gt, pred, T=0.85): gt_f = gt.astype(np.float64) pred_f = pred.astype(np.float64) map1 = _feature_map(gt_f) map2 = _feature_map(pred_f) pc_max = np.maximum(map1, map2) similarity = (2 * map1 * map2 + T) / (map1 ** 2 + map2 ** 2 + T) return float(np.sum(similarity * pc_max) / np.sum(pc_max))这个快速版本没有做完整的 Log-Gabor 多方向滤波,而是用 Sobel 梯度幅度加高斯残差来模拟相位一致性的权重分布,适合批量回归测试和参数调优。如果要做严谨的主观评测,需要把_feature_map替换为真正基于相位一致性的响应图。计算上有一个细节:相似度分母里加入常数T是为了防止平坦区域梯度接近零时出现除零错误,T 取 0.85 是经验值,也可以改成图像全局梯度均值的百分之一。
3.3 实现 SRE 和 SAM
def calc_sre(gt, pred): gt = gt.astype(np.float64) pred = pred.astype(np.float64) noise_power = np.sum((gt - pred) ** 2) signal_power = np.sum(gt ** 2) return float(10.0 * np.log10(signal_power / (noise_power + 1e-12))) def calc_sam(gt, pred): gt = gt.astype(np.float64) pred = pred.astype(np.float64) if gt.ndim == 2: gt = gt[..., None] pred = pred[..., None] inner = np.sum(gt * pred, axis=-1) norm_gt = np.linalg.norm(gt, axis=-1) norm_pred = np.linalg.norm(pred, axis=-1) cos_sim = np.clip(inner / (norm_gt * norm_pred + 1e-12), -1.0, 1.0) angle = np.arccos(cos_sim) return float(np.degrees(np.mean(angle)))SRE 返回分贝值,越大表示重建信号能量保留越完整;若两张图完全相同,sre 趋于无穷大,所以代码里加了1e-12保护。SAM 返回的是平均角度,单位是度。np.clip这一步很关键,浮点累计误差会让余弦值偶尔超过 1,导致arccos返回nan,这是光谱角计算最常见的隐患。
3.4 统一封装评估入口
def evaluate_pair(gt, pred, data_range=None): rmse, psnr = calc_rmse_psnr(gt, pred, data_range) ssim = calc_ssim(gt, pred, data_range=data_range or 255.0) issm = calc_issm(gt, pred, scales=4, data_range=data_range or 255.0) fsim = calc_fsim_fast(gt, pred) sre = calc_sre(gt, pred) sam = calc_sam(gt, pred) return { 'rmse': rmse, 'psnr': psnr, 'ssim': ssim, 'issm': issm, 'fsim': fsim, 'sre': sre, 'sam': sam, }所有函数都接受(H, W)或(H, W, C)的 NumPy 数组,但 FSIM 目前只对二维灰度图有物理意义。多光谱图像的 FSIM 需要逐波段计算后取平均,不能在三维数组上直接调用。建议在evaluate_pair外层判断维度,多波段时循环各波段计算 FSIM 后再聚合。
4. 把评估函数接进超分和去噪流程:参数设置、归一化与输出校验
在很多团队的实际项目里,评估流程并不只是运行一个函数,而是要把参考图目录和预测图目录批量对比,最终生成 CSV 或 JSON 报告。下面这套做法适合超分、去噪、去雨以及目标跟踪前后帧的相似度评估。
4.1 超分重建场景下的执行流程
超分模型输出的图像经常是 0 到 1 的浮点张量,而 GT 是 0 到 255 的 uint8 图像。直接比较会得到完全无意义的结果。先做数值范围对齐,再调用evaluate_pair。也比较稳妥的做法是全部转换为 float64 并显式指定data_range:
import cv2 import numpy as np gt = cv2.imread('GT_001.png', cv2.IMREAD_GRAYSCALE).astype(np.float64) pred = np.load('pred_001.npy').astype(np.float64) if pred.max() <= 1.0: pred = pred * 255.0 metrics = evaluate_pair(gt, pred, data_range=255.0) print(metrics)这里有个容易忽略的细节:超分模型的输出可能带边缘黑边或补零区域。若预测图尺寸与 GT 不一致,需要先中心裁剪到共同区域,否则 RMSE 会被一大圈黑色边框拉高,而 SSIM 因为局部窗口只受影响一部分,看起来反而不明显,两个指标会得出矛盾结论。建议在评估前统一crop_border,参数一般设为超分倍率的两倍。
4.2 去噪与跟踪场景的批处理命令
把七个指标封装成可命令行调用的脚本,是博主在团队里最常用的一种交付形式。脚本接受--ref和--pred目录,按相同文件名配对,最终输出 CSV:
python compare_metrics.py \ --ref ./data/gt \ --pred ./results/denoised \ --ext png \ --data-range 255 \ --metrics rmse psnr ssim issm fsim sre sam \ --output ./reports/denoise_metrics.csv命令行参数的作用分别为:--ref指定参考图目录,--pred指定待评估图目录,--ext限定文件后缀,--data-range决定 PSNR 公式里的峰值 L,--metrics控制要计算的指标集合,--output指定汇总结果的可写路径。这样配置的好处是,同一个脚本可以同时服务于去噪、超分和跟踪任务,只需要调整--data-range和目录即可。
4.3 输出指标的合理范围参考
拿到分数后怎么判断模型好坏?可以根据任务类型画一条经验线,但不能当硬性标准。下表给出博主在常规遥感影像和自然图像上使用的参考区间:
| 指标 | 去噪任务常见区间 | 超分四倍任务常见区间 | 情况说明 |
|---|---|---|---|
| PSNR dB | 30 - 42 | 26 - 34 | 低于 25 通常有明显可见噪声 |
| SSIM | 0.90 - 0.99 | 0.80 - 0.94 | 接近 1 时肉眼几乎无法区分 |
| ISSM | 0.92 - 0.99 | 0.82 - 0.93 | 低于 SSIM 约 0.03 属正常 |
| FSIM | 0.94 - 0.99 | 0.88 - 0.95 | 对边缘锐度更敏感 |
| SAM | 2 - 8 度 | 4 - 12 度 | 高光谱任务一般要求低于 10 度 |
| SRE dB | 20 - 35 | 15 - 28 | 与 PSNR 趋势一致但无峰值归一化 |
这些区间只是基线参考,不同数据集上必须先跑一组 baseline 再确定阈值。真正有效的做法是:同时看四组表格,先看 PSNR 有没有异常跌点,再看 SSIM 和 FSIM 是否同步,最后检查 SAM 是否暴露了光谱失真。如果 PSNR 正常但 FSIM 偏低,优先怀疑边缘振铃和过锐化。
5. 评估结果不一致时的定位方法:量纲、空洞、波段数差异排查
指标之间频繁出现“分数打架”,不代表实现有 bug,更可能是输入数据没有满足前提条件。调试时按优先级检查量纲、空间对齐和波段顺序。
5.1 先查像素值范围是否满足 PSNR 与 SSIM 的前提
浮点图与 uint8 图混用时最容易出现系统性偏差。若 GT 在 0 到 255 范围,预测图被某次归一化操作缩放到 0 到 1,RMSE 会正确反映相对误差,但 PSNR 会显示为 90 dB 以上的荒谬数值,SSIM 也会因稳定常数不匹配而失真。解决办法是先打印两张图的min、max和dtype,再决定是否乘以 255。更稳妥的做法是在评估脚本里统一执行:
def normalize_like(gt, pred): if gt.max() > 1.0 and pred.max() <= 1.0: return gt, pred * 255.0 if gt.max() <= 1.0 and pred.max() > 1.0: return gt * 255.0, pred return gt, pred5.2 空值、无数据区域对 SAM 和 SRE 的污染
遥感影像经常存在死像素或无效区域,这些区域的像素值通常为 0 或 NaN。SAM 对接近零向量的点位非常敏感,分母加1e-12只能防止除零,无法防止角度被噪声主导。正确的做法是构造有效掩码,在掩码区域内计算指标:
mask = np.isfinite(gt) & np.isfinite(pred) & (gt > 0) & (pred > 0) def masked_sre(gt, pred, mask): err = np.sum(((gt - pred) ** 2)[mask]) sig = np.sum((gt ** 2)[mask]) return 10.0 * np.log10(sig / (err + 1e-12))如果两张图之间还有轻微亚像素错位,SAM 的数值会突然上升,而 SSIM 下降不明显。这时候不要急着调算法,先对预测图做一次相位相关配准,再跑全套指标。工程里我会用 OpenCV 的phaseCorrelate或简单的高斯金字塔光流估计一个平移量,修正后再评估。
5.3 波段顺序与波段数不一致造成的假异常
多光谱图像对比时,波段顺序必须一致。否则即使视觉上看不出差别,SAM 也会输出异常大的角度。建议在读取数据时打印gt.shape和pred.shape,确认第三维波段数一致。若波段数不同,只能先执行波段选择或光谱重采样,不能直接比较。
6. 批量评估时用 pandas 汇总七项指标并生成对比表
当实验规模变大后,单张评估的意义有限,更常见的是把整个验证集的指标汇总成均值、标准差和每张图的明细。博主会直接用 pandas 做聚合,并在同一张表里给出排序后的 PSNR 与 SAM,方便快速定位那些 PSNR 高但光谱失真的样本。
import glob import pandas as pd rows = [] for ref_path in sorted(glob.glob('./data/gt/*.png')): pred_path = ref_path.replace('gt', 'results/modelA') gt = cv2.imread(ref_path, cv2.IMREAD_UNCHANGED) pred = cv2.imread(pred_path, cv2.IMREAD_UNCHANGED) m = evaluate_pair(gt, pred, data_range=255.0) m['name'] = ref_path.split('/')[-1] rows.append(m) df = pd.DataFrame(rows) summary = df.drop(columns=['name']).agg(['mean', 'std']).T summary.columns = ['mean', 'std'] summary.to_csv('./reports/modelA_summary.csv')这个脚本里有一个值得留意的技巧:先按文件名字典序配对,再用replace函数把参考目录字段替换为预测目录。真实项目中要注意目录层级是否一致,建议在批处理前做一个“文件名匹配数量”的断言,否则缺失文件会让汇总均值失真。
多模型对比时,可以把各模型的 summary 横向拼接成总表,也可以直接以 PSNR 降序排序后,再用 SAM 升序做第二排序键。指标联动可以这样用:若某样本 PSNR 排名前 20 但 SAM 排名后 20,说明该图存在明显光谱偏移,需要单独检查成像条件,而不是直接让它进入最终平均分。最后一个实用做法是,在所有指标计算完成后,把 SAM 大于 10 度且 SSIM 高于 0.95 的样本单独导出到异常列表,这类样本往往是空间结构保持良好但颜色或光谱被改变的情况,是检验算法是否真正保持物理一致性的重要判据。
本文还有配套的精品资源,点击获取