ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

图像相似性度量指标详解:PSNR、SSIM、SAM等七种算法与Python实现

图像相似性度量指标详解:PSNR、SSIM、SAM等七种算法与Python实现 简介这是一份基于Python的图像相似度评估工具包实现了RMSE、PSNR、SSIM、FSIM、ISSM、SRE、SAM与UIQ八种常用指标用于量化两幅图像间的差异。适用于图像去噪、超分辨率重建、压缩质量评估等场景也便于遥感影像分析、计算机视觉研究者在算法对比与实验验证时使用。压缩包共29个文件包含Python源码、示例图片、Jupyter Notebook演示、Markdown说明文档与配置文件等体积约575KB。其中py为核心指标实现png为测试示例ipynb与md提供可运行演示和详细说明方便快速理解与集成。已有4515人学习下载可借助该工具快速完成图像相似度定量比较。资源附带清晰目录与README指南支持pip安装和命令行调用示例与文档有助于理解各指标的计算原理与适用差异读者可直接将评估方法嵌入自己的图像处理流程结合测试图像验证算法效果。1. 图像相似性度量不是“像不像”而是可复现的数值决策在图像超分辨率、压缩算法选型、医学影像重建和生成模型评测里最常被问到的问题就是“这两张图到底像不像”。主观判断经不起复现换一个测试样本结论就会漂移所以需要一组可计算的标量来替人做决定。RMSE、PSNR、SSIM、ISSM、FSIM、SRE、SAM 这七个指标恰好覆盖了像素差、结构失真、边缘特征和光谱偏移四个维度它们不需要训练数据适合作为算法回归测试和模型选型的基础设施。这套体系并不新鲜但我在实际项目里见过太多人只跑一个 PSNR 或者只抄一个 SSIM 的默认参数结果阈值设了等于没设。这篇文章会把每个指标的公式边界、可运行的 Python 实现和参数陷阱一次讲完末尾再给一个校准阈值的通用技巧。2. 指标分工与数学定义像素差、结构感知、光谱角各管一段2.1 像素级指标RMSE 与 PSNR 的互补关系RMSE均方根误差是最直接的亮度差异度量对两张图逐像素做差平方后取均值开根号RMSE sqrt( mean( (A - B)^2 ) )RMSE 越小表示像素数值越接近但它对整体亮度偏移和随机噪声的反应是混淆的。举例来说一张图整体加 10 亮度和另一张图加上相同能量的随机噪声RMSE 可能完全相同但肉眼对前者的感知远好于后者。这说明像素差指标只回答“数值差多少”不回答“失真类型”。PSNR 是 RMSE 的对数变换计算的是峰值信号与均方误差的比值PSNR 10 * log10( max_val^2 / mean((A - B)^2) )max_val 必须和图像动态范围一致8-bit 图像取 255浮点 0-1 图像取 1。PSNR 越大越好单位是 dB一般来说超过 40dB 人眼很难区分差异30dB 到 40dB 之间属于可接受低于 25dB 通常意味着明显失真。由于 PSNR 和 RMSE 信息等价我一般只保留 PSNR另外加一个 SRE 来刻画重建任务中的信噪特性。2.2 结构感知SSIM 与 ISSM 的分工SSIM结构相似性不是逐像素比较而是把图像切分成局部窗口分别比较亮度、对比度和结构。标准公式由三项组成L (2μa μb C1) / (μa^2 μb^2 C1)C (2σa σb C2) / (σa^2 σb^2 C2)S (σab C3) / (σa σb C3)SSIM L * C * Sμ、σ、σab 是窗口内的均值、方差和协方差C1、C2、C3 是防止除零的稳定性常数。结果是 0 到 1越接近 1 越好。SSIM 对局部模糊、压缩块效应比 PSNR 敏感得多这也是它被广泛用于图像质量评估的原因。ISSM 在这里按我在项目里的惯例指基于梯度幅值的结构相似性度量。实现方式是把原始像素替换为梯度幅值图再套 SSIM 的公式。由于梯度幅值突出了边缘位置ISSM 会比 SSIM 更快地反映出边缘模糊、振铃效应等高频失真。这个指标没有唯一的行业标准你在技术报告里必须写明自己的定义否则别人无法复现你的数值。2.3 频域特征FSIM 基于相位一致性FSIM特征相似性是更深一层的感知模型核心假设是人眼对图像特征的感知主要取决于相位一致性Phase Congruency而不是像素强度。相位一致性高的点对应边缘和角点FSIM 先计算两幅图的相位一致性图 PC_a、PC_b 和梯度幅值图 GM_a、GM_b然后得到局部相似度S_L(x) (PC 相似度) * (GM 相似度)最终的 FSIM 是对 S_L 做加权平均权重使用两幅图 PC 的较大值FSIM sum(S_L * max(PC_a, PC_b)) / sum(max(PC_a, PC_b))这个指标的优点是与主观评分的相关性通常优于 SSIM缺点是相位一致性需要多尺度多方向的滤波器组纯手写非常耗时。实际工程中我建议直接使用 piq 这类图像质量评估库避免自己从零实现 Log-Gabor 滤波器。2.4 光谱与重建误差SAM 与 SRE 的适用场景SAM光谱角制图把图像中的每个像素看作是 C 维光谱向量两个像素之间的相似度用向量夹角来表示SAM arccos( dot(A_pixel, B_pixel) / (||A_pixel|| * ||B_pixel||) )这里的 C 是通道数RGB 图像 C3多光谱 C几十高光谱 C数百。SAM 的单位是弧度常用角度表示。它最大的特点是对整体亮度缩放不敏感因为向量方向不变只是模长变化。这个特性让 SAM 在多光谱和高光谱影像分类、变化检测中成为标配指标。SRE信号重建误差比是信号能量与误差能量的比值SRE 10 * log10( sum(A^2) / sum((A - B)^2) )它和 PSNR 的区别在于分母基准不同PSNR 用峰值信号SRE 用实际信号能量。在超分辨率、去噪、压缩重建任务中SRE 更能反映重建结果对原始信息能量的保留程度。如果两张图完全相同SRE 趋近无穷大实际代码里要对分母加极小值处理。下表汇总了这七个指标的关键属性指标类别取值范围最优值典型使用场景RMSE像素差0 到 ∞越小越好数值差异诊断PSNR像素差/能量0 到 ∞ dB越大越好压缩、去噪、重建SSIM结构感知0 到 1越大越好感知质量评估ISSM结构感知/梯度0 到 1越大越好边缘失真检测FSIM特征感知0 到 1越大越好主观相关性验证SAM光谱角0 到 π rad越小越好多光谱/高光谱影像SRE信号能量0 到 ∞ dB越大越好重建误差评估3. Python 实现一条命令算出全部相似性指标3.1 准备测试图像与统一的图像输入接口先准备好两张尺寸一致的图像。如果尺寸不一致我会强制先对齐但要注意 resize 本身会引入插值误差在正式评估前最好先做配准而不是直接 resize。下面的函数统一读图、转 float64、可选统一尺寸import numpy as np from skimage import io, transform def load_and_prepare(path_a, path_b, sizeNone): a io.imread(path_a).astype(np.float64) b io.imread(path_b).astype(np.float64) if size is not None: a transform.resize(a, size, anti_aliasingTrue) b transform.resize(b, size, anti_aliasingTrue) return a, b这段代码返回两个 float64 数组数组形状完全一致才能继续计算。anti_aliasingTrue可以降低缩小时的高频混叠但前提是你接受 resize 造成的插值误差。正式实验里我通常只在调试阶段使用这个参数最终结果一定是在原始分辨率上计算。3.2 RMSE / PSNR / SRE 三个像素能量指标这三个指标可以放在一起实现因为它们本质都在计算像素差平方项def rmse(a, b): return float(np.sqrt(np.mean((a - b) ** 2))) def psnr(a, b, max_val255.0): mse_val np.mean((a - b) ** 2) if mse_val 0: return float(inf) return 10.0 * np.log10(max_val ** 2 / mse_val) def sre(a, b, eps1e-10): signal np.sum(a ** 2) error np.sum((a - b) ** 2) if error eps: return float(inf) return 10.0 * np.log10((signal eps) / (error eps))psnr函数里的max_val必须由调用方根据图像实际动态范围传入否则 0-1 图像用了 255 会让 PSNR 凭空多出约 48dB。sre用eps防止分子分母同时为 0 的情况全黑图的 SRE 没有物理意义建议在脚本外做判断。这三个函数返回值越大表示质量越好只有 RMSE 是越小越好。3.3 SSIM / ISSM 的计算与窗口参数SSIM 直接用 scikit-image 的现成实现关键是设置data_range和channel_axis。ISSM 则用梯度幅值替代原始像素from skimage.metrics import structural_similarity as ssim_metric def ssim_score(a, b, data_range255.0, multichannelTrue): return ssim_metric(a, b, data_rangedata_range, channel_axis-1 if multichannel else None) def issm_score(a, b, data_rangeNone): # 计算 x 方向和 y 方向的梯度合成复梯度 ga np.gradient(a, axis0) 1j * np.gradient(a, axis1) gb np.gradient(b, axis0) 1j * np.gradient(b, axis1) gmag_a np.abs(ga) gmag_b np.abs(gb) if data_range is None: data_range max(gmag_a.max(), gmag_b.max()) - 0.0 return ssim_metric(gmag_a, gmag_b, data_rangedata_range, channel_axis-1)SSIM 默认使用 7x7 的高斯窗口窗口大小和 sigma 改不了的话就用官方默认。ISSM 把梯度幅值当作一张新的单通道或多通道图像再做结构相似度比较。这里复梯度的写法相当于把 x 方向和 y 方向梯度合成一个幅值能更容易捕捉到任意方向的边缘。注意梯度幅值图的范围和原图完全不同所以data_range要单独算否则 SSIM 内部归一化会出现偏差。3.4 FSIM 的实用计算路径用 piq 而不是手写滤波器FSIM 的完整实现涉及多尺度 Log-Gabor 滤波器组手写几百行且容易出错我一般用 piq 库import torch import piq def fsim_score(a, b): # piq 要求输入为 float32 的 torch tensor形状是 (N, C, H, W) ta torch.from_numpy(a.astype(np.float32)).unsqueeze(0) tb torch.from_numpy(b.astype(np.float32)).unsqueeze(0) # 归一化到 0-1避免亮度范围影响 ta ta / ta.max() if ta.max() 0 else ta tb tb / tb.max() if tb.max() 0 else tb return piq.fsim(ta, tb).item()这段代码把 numpy 数组转成(1, C, H, W)的张量然后归一化到 0-1。piq.fsim对灰度图和 RGB 图都支持但如果你的图像是无符号整型读取的必须先转 float否则torch.from_numpy会保留整型类型梯度计算时精度不够。piq 返回的是均值标量直接.item()取出。如果你的环境不能装 piq最低成本的方法是退回到用 SSIM 代替 FSIM但那样就会丢掉相位一致性信息。3.5 SAM 光谱角的向量化实现SAM 的实现难点在于批量计算向量夹角。把图像 reshape 成像素列表每个像素一行通道列然后向量化计算def sam_score(a, b, in_degreesTrue, eps1e-10): if a.ndim 2: a a[..., np.newaxis] b b[..., np.newaxis] original_shape a.shape a_flat a.reshape(-1, original_shape[-1]) b_flat b.reshape(-1, original_shape[-1]) dot np.sum(a_flat * b_flat, axis1) norm_a np.linalg.norm(a_flat, axis1) norm_b np.linalg.norm(b_flat, axis1) cos_theta np.clip(dot / (norm_a * norm_b eps), -1.0, 1.0) angles np.arccos(cos_theta) mean_angle float(np.mean(angles)) if in_degrees: return np.degrees(mean_angle) return mean_anglenp.clip这一步不能漏浮点误差会让cos_theta偶尔变成 1.0000001直接送进arccos就会产生 NaN。mean 取的是整张图像中所有像素光谱角的平均值如果只需要全局相似度这个平均足够如果你还想看局部区域的光谱偏差可以先计算每个像素的角度再按窗口聚合。3.6 汇总输出结构化结果有了上面五个子函数可以统一封装成evaluate_similarity返回字典def evaluate_similarity(a, b, max_valNone): if max_val is None: max_val max(a.max(), b.max()) return { RMSE: rmse(a, b), PSNR: psnr(a, b, max_valmax_val), SSIM: ssim_score(a, b, data_rangemax_val), ISSM: issm_score(a, b, data_rangemax_val), FSIM: fsim_score(a, b), SAM: sam_score(a, b), SRE: sre(a, b) }这样调用方只需要传两张图像数组就能得到统一字段名的结果字典。优先把这些结果转成 pandas DataFrame方便保存 CSV 或者对接后续的阈值判断逻辑。max_val必须在入口处统一避免 PSNR 用 255、SSIM 用 1 这种低级错误。4. 参数设置与边界条件data_range、通道轴与数值稳定性4.1 data_range 设置错误会让 PSNR 和 SSIM 结果完全不可比这是我在代码评审里最容易发现的问题。很多脚本从 skimage.io 读出图像后不区分 dtypeuint8 图像直接输入 PSNR 时 max_val 用 255float 图像也照样抄 255结果 PSNR 凭空偏高。SSIM 的参数data_range同样是这个作用它决定了亮度比较项 C1、C2 的基准。正确做法是从图像 dtype 或者元数据读取动态范围def infer_max_val(arr): if arr.dtype np.uint8: return 255.0 if arr.dtype np.uint16: return 65535.0 return 1.0 if arr.max() 1.0 else arr.max()如果两张图的 dtype 不一致先统一转成 float64 再传到一个公共的max_val。否则 PSNR 40dB 的结果毫无意义因为基准都不是同一个。4.2 多通道图像RGB、多光谱与高光谱的处理差异RMSE、PSNR、SRE 对通道没有特殊要求直接展平所有通道一起算即可。SSIM 必须指定channel_axis-1否则会默认把最后三个通道当成普通空间维结果会飘。ISSM 同样要指定通道轴。FSIM 在 piq 中支持灰度或 RGB但如果是多光谱影像piq 没有对任意通道数做适配。常见的做法是逐波段计算 FSIM 后取平均也就是把每一层通道拆开单独作为灰度图计算。SAM 则完全不同通道数越多光谱分量越丰富所以必须保留全部波段参与向量夹角计算绝不能逐通道分开算 SAM 再平均那会丢失光谱之间的相关关系。4.3 图像对齐与边界填充对结果的影响哪怕只有 1 个像素的平移RMSE 和 SSIM 都会断崖式下降。这个判断不夸张因为像素级比较默认的是“同一个位置应该对应真实世界的同一个点”。在图像超分和重建评估中原始高分辨率图和重建结果通常已经在同一个网格上不需要配准。但如果你的输入来自两个传感器或者同一相机的不同时刻必须先做配准。常见做法是用 OpenCV 的相位相关或 ECC 迭代算法先把 B 图对齐到 A 图再计算指标。千万别假想两个图是天然对齐的很多指标异常波动都来自对齐问题而不是算法质量问题。另一个边界是图像边缘SSIM 在边界处会做 padding默认的反射填充会引入外部像素如果图像很小或边界存在硬边建议先裁掉边缘区域再计算。4.4 数值稳定性除零、acos 越界和 NaN整型读图后直接做减法可能溢出所以第一步转 float64。PSNR 遇到两张图完全一致时 MSE 为 0返回inf合情合理SRE 同样要捕捉inf。SAM 的arccos输入必须用clip限制在 [-1, 1] 内否则一个浮点误差就让结果变成 NaN。FSIM 如果图像全黑归一化时max_val为 0python 侧会碰到除零务必加判断if ta.max() 0。另外注意 uint8 图像的像素差平方如果用整型计算会直接溢出成错误值所以所有指标函数的输入都应当是 float 数组这是最低要求。5. 进阶技巧用合成退化实验校准各指标的灵敏度阈值拿到七个指标之后最容易被忽略的是“阈值从哪来”。不要拍脑袋定 PSNR 30、SSIM 0.90这些数字未必符合你的数据分布。我一般用一个合成退化扫描脚本取一张参考图按照不同强度加噪声、模糊、亮度偏移再计算每个指标的变化曲线从这张表里反推合理的质量门限。import pandas as pd from skimage import filters ref, _ load_and_prepare(ref.png, ref.png) rows [] for sigma in [0.0, 0.5, 1.0, 2.0, 4.0, 8.0]: if sigma 0: degraded ref.copy() else: degraded filters.gaussian(ref, sigmasigma, channel_axis-1) res evaluate_similarity(ref, degraded, max_val255.0) res[sigma] sigma rows.append(res) df pd.DataFrame(rows) print(df.to_string(indexFalse))这个脚本的输出是一张 6 行 7 列的表格每一行对应一个高斯模糊强度。你可以从表格里看出sigma 从 0 升到 1 时RMSE 变化可能不大但 FSIM 已经掉了不少sigma 从 2 升到 4 时SSIM 会快速下降。这时候再决定你的“可接受相似线”如果你做的是超分算法调优通常以 SSIM 下降不超过 0.02 作为回归底线如果做多光谱判读SAM 超过 2 度就应当报警。校准完成后把阈值写进项目的质量门禁配置文件例如quality_gate.json{ min_psnr: 32.0, min_ssim: 0.92, min_fsim: 0.95, max_sam_degree: 1.5, min_sre: 20.0 }每次跑完新实验先用evaluate_similarity算出全部指标再对照这份配置输出 pass/fail。这样既不用把七个指标全部人工盯一遍也能让后续迭代过程中“看起来变好了”和“数值真的变好了”这两个判断彻底分开。本文还有配套的精品资源点击获取
返回列表