ARTICLE DETAIL

资讯详情

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

图像加密性能评价指标全解析:从PSNR到NPCR的Python实践

图像加密性能评价指标全解析:从PSNR到NPCR的Python实践 做图像加密评估时最容易发生的误解就是“看着够乱就成功了”。早几年我刚接触图像加密做成一个混沌映射加密算法后把原图和密文并排展示给导师看导师丢回来一句“你的评价指标呢”当时我愣住了心想这还不够乱吗后来在审稿意见里被要求补充性能评价指标才意识到主观视觉判断根本不够必须用客观指标去量化“乱”的程度。这篇文章就围绕图像加密性能评价指标来写重点讲清楚每个指标的含义、适用场景、期望值以及如何用Python快速实现。这篇文章适合几类人看正在做加密算法毕业设计的学生、需要对比不同加密方案的科研人员以及想建立一套完整评测脚本的研究者。我会把计算原理、代码实现、踩坑经验一起写出来方便你直接“抄作业”。1. 图像加密效果好不好到底看什么1.1 为什么不能只看一张“看起来乱”的图“看起来乱”是一种主观感知很容易骗人。有些算法加密后的密文图虽然人眼已经看不出原图轮廓但在局部区域仍可能残留边缘信息或周期性纹理。人眼对高频噪声不敏感注意力多半集中在大的轮廓上所以很难察觉局部相关性。客观指标则能把这种残留结构量化出来。比如原图像中相邻像素通常高度相关相关系数可能接近0.95以上。加密后理想情况下相关系数应该趋近于0如果在某方向上仍然出现0.5以上的相关系数说明算法没有完全打破像素之间的空间关系攻击者可能利用统计特征恢复部分信息。这种问题靠眼睛根本发现不了。另一个现实原因是学术论文和项目验收都要求“可量化”。无论是写学位论文还是投稿评审专家都会关注指标数据是否达到公认的安全标准。如果只放两张图说服力会大打折扣。所以客观评价指标不是可选项而是必选项。1.2 一套完整的图像加密评价指标应该包括哪些图像加密的目标可以拆成两类一是把原始信息彻底隐藏让密文看起来像噪声二是算法要能抵抗常见的攻击比如差分攻击、统计攻击。针对这些目标业界逐渐形成了一套常用指标我一般会分成四组重建质量类指标PSNR、SSIM。它们虽然主要用于衡量压缩或解密后的失真但在加密场景中用来衡量密文与明文的偏离程度数值越低越说明加密彻底。统计特性类指标信息熵、相邻像素相关系数。用来反映密文的随机性和像素分布的均匀性。差分攻击类指标NPCR、UACI。用来衡量明文微小改变时密文的变化程度反映抗差分攻击能力。密钥敏感性指标严格来说NPCR/UACI也能用于评估密钥敏感性但通常单独观察密钥微小变化后密文的差异。有些人还会看直方图方差、卡方检验、像素改变率等额外指标不过对大多数应用场景来说上面这些已经足够形成一套评价体系。实际评测时不要只看单个指标要综合判断。一张密文图可能熵接近8但相邻像素相关系数依然偏高这说明算法随机化不充分。2. 从PSNR到SSIM重建质量指标的原理与Python实现2.1 PSNR的数学公式和实现要点PSNRPeak Signal-to-Noise Ratio峰值信噪比是图像处理里最常用的失真指标定义非常直接。先计算原图与失真图的均方误差MSEMSE (1 / (H × W)) × Σ (I(i, j) − K(i, j))²然后取信号峰值的平方和MSE的比值再用对数表示PSNR 10 × log10(MAX² / MSE)其中MAX是像素最大值8位灰度图就是255。如果MSE等于0PSNR是无穷大说明两幅图完全一样。在加密评测中PSNR越低越好。原图和密文图差别越大PSNR越小。通常对于8位灰度图如果PSNR低于10dB说明密文和明文差异已经非常大。但如果出现20dB以上的情况就要警惕加密不彻底可能只做了亮度平移或矩阵置换而没有扩散。实现时有一个最容易踩的坑直接对uint8类型做减法。uint8是无符号8位整数0减1会变成255导致MSE计算完全错误。所以要先把图像转成float64再运算。代码不复杂import numpy as np def psnr(img1, img2): if img1.shape ! img2.shape: raise ValueError(两张图像尺寸不一致) img1 img1.astype(np.float64) img2 img2.astype(np.float64) mse np.mean((img1 - img2) ** 2) if mse 0: return float(inf) return 10 * np.log10(255.0 ** 2 / mse)这段代码兼容灰度图和彩色图因为np.mean会直接对所有通道对应像素取平均。要注意的是如果彩色图像通道数相同这没问题如果你用的是浮点图像像素范围在0~1之间要把MAX改为1.0否则结果会偏大。2.2 SSIM的局部结构比较和skimage用法SSIMStructural Similarity结构相似性比PSNR更接近人的视觉感知。它从亮度、对比度、结构三个维度比较两个图像块SSIM ((2μxμy C1) × (2σxy C2)) / ((μx² μy² C1) × (σx² σy² C2))其中μ是均值σ是方差σxy是协方差C1、C2是为了防止分母为0加的稳定常数。SSIM取值在-1到1之间越接近1表示两图越相似。在加密场景中密文和明文的结构相似度应当尽可能低一般低于0.2就说明结构信息已经被破坏得很严重。手动实现SSIM需要做滑动窗口、高斯加权、处理边界等情况代码量大且容易出错。我推荐直接使用scikit-image库pip install scikit-image调用方式如下from skimage.metrics import structural_similarity as ssim def calculate_ssim(img1, img2): # data_range必须明确指定不然不同像素范围会得到不同结果 # 新版skimage用channel_axis-1表示多通道彩色图 return ssim(img1, img2, channel_axis-1, data_range255)如果你用的是老版本skimage的参数名是multichannelTrue比如structural_similarity(img1, img2, multichannelTrue, data_range255)。遇到提示TypeError时看看版本文档把参数替换一下就好。2.3 手动实现与库函数的对比有人会担心用库函数不够“硬核”非要手写一遍。我觉得没必要。SSIM实现中窗口半径、高斯滤波器标准差、常数的取值都会影响结果手写时只要某一处细节和普遍默认值不一致算出来的数值就和其他论文不可比。与其制造这种偏差不如统一用skimage实现保证横向对比时公平。我自己早期手动实现过一版结果跟库函数差0.03左右排查了半天发现是高斯窗口的sigma设置不一致。从那以后凡是实验对比我都用库函数只在教学或需要嵌入到模块里时才考虑手写。PSNR因为公式简单自己写就行不用引入多余依赖。3. 乱到看不见结构信息熵与相邻像素相关性3.1 信息熵衡量图像的不可预测性信息熵由香农提出是衡量信息不确定性的经典指标。对于灰度图像每个像素值可以看作一个随机变量信息熵定义如下H(X) −Σ P(xi) × log2(P(xi))其中P(xi)是灰度值xi的出现概率。8位灰度图像的理论最大熵是8也就是256个灰度值等概率出现时的情况。加密后的图像信息熵越接近8说明像素分布越均匀越接近完全随机噪声。实测中自然图像的信息熵通常在7到7.5之间因为灰度分布不均衡。经过良好加密的密文图信息熵应当大于7.99甚至接近7.999。如果加密后熵仍然只有7.5说明算法可能只对像素位置做了置换没有改变灰度分布。需要注意基于置乱的算法天然存在这个缺陷因为直方图不变熵也不变。Python实现很简单不需要调skimage直接用numpy就好了def entropy(image): if image.ndim 3: # 彩色图可以逐通道计算后取平均也可以转成灰度 return np.mean([entropy(image[:, :, c]) for c in range(image.shape[2])]) image image.astype(np.uint8) hist np.bincount(image.ravel(), minlength256).astype(np.float64) hist hist[hist 0] prob hist / hist.sum() return -np.sum(prob * np.log2(prob))这里有个细节bincount处理像素值范围0~255没问题但如果图像是float类型直接传入bincount会报错或产生错误结果。所以要么先转uint8要么用np.histogram。加密实验通常在0~255范围操作转uint8即可。3.2 相邻像素相关性加密必须打破的正相关结构自然图像的每个像素与它周围的像素之间存在很强的相关关系。你在纯色区域随便找一个像素它的水平邻域、垂直邻域值几乎相同相关系数可能接近0.9以上。图像加密的一个重要目标就是消除这种空间相关性。通常需要计算三个方向水平、垂直、对角线。计算公式就是皮尔逊相关系数corr Σ((xi − x̄)(yi − ȳ)) / sqrt(Σ(xi − x̄)² × Σ(yi − ȳ)²)实际计算时可以从图像中随机抽取3000到5000对相邻像素降低计算量对结果影响很小。实现代码如下def adjacent_correlation(img, n_pairs3000, seed42): if img.ndim 3: img np.mean(img, axis2).astype(np.uint8) rng np.random.default_rng(seed) h, w img.shape directions {horizontal: (0, 1), vertical: (1, 0), diagonal: (1, 1)} result {} for name, (dh, dw) in directions.items(): max_y h - dh - 1 max_x w - dw - 1 if max_y 0 or max_x 0: continue y rng.integers(0, max_y 1, sizen_pairs) x rng.integers(0, max_x 1, sizen_pairs) a img[y, x] b img[y dh, x dw] corr np.corrcoef(a, b)[0, 1] result[name] corr return result对灰度图运行加密后理想情况下相关系数应当接近0。实践中绝对值小于0.1就算不错优秀算法能做到0.01以下。如果某个方向仍然有0.3甚至0.5的相关系数说明扩散性不足建议重新审视加密算法。彩色图处理时常见做法是分别计算R、G、B三个通道的相关性并取平均值。也可以转换为YCbCr等颜色空间但加密算法通常是分通道处理直接逐通道计算更符合实际。4. 抗差分攻击能力NPCR与UACI的实现与解读4.1 为什么会提到差分攻击差分攻击是图像加密中不可忽视的威胁。攻击者会修改明文中一个像素点然后观察密文变化。如果加密算法扩散性差改一个像素只引起局部密文变化攻击者就能通过对比两组密文找出规律最终推出密钥。因此测试时我们通常对原图做微小修改比如将任意位置一个像素值加1再用同一个密钥加密得到两张密文图。如果算法足够“敏感”这两张密文图应当有大约99.6%的像素不同平均灰度变化在33.4%左右。这说明任意明文变化都会雪崩式扩散到整幅密文。4.2 NPCR和UACI的计算公式与Python实现NPCRNumber of Pixel Change Rate像素变化率考察两幅密文的像素值有多少个不同NPCR (Σ D(i, j)) / N × 100%其中D(i, j)在像素相同时为0不同时为1N是像素总数。理论期望值为99.6094%。UACIUnified Average Changing Intensity统一平均变化强度考察不同像素的变化幅度UACI (1 / N) × Σ |C1(i, j) − C2(i, j)| / 255 × 100%理论期望值约为33.4635%。它和NPCR互相补充。如果NPCR接近99.6%但UACI只有10%说明密文虽然大量像素都变了但变化幅度很小扩散强度不够可能某些加密模块只做了位平面置换却没有做非线性替换。实现时我用int16和float64防止差值变成负数也防止除法精度不足def npcr_uaci(img1, img2): if img1.shape ! img2.shape: raise ValueError(两张图像尺寸不一致) img1 img1.astype(np.int16) img2 img2.astype(np.int16) diff img1 ! img2 npcr np.mean(diff) * 100.0 uaci np.mean(np.abs(img1.astype(np.float64) - img2.astype(np.float64)) / 255.0) * 100.0 return npcr, uaci彩色图像也适用因为shape包含通道维度。比如RGB图diff会统计所有通道所有像素。如果想按通道单独看就循环通道分别调用。4.3 密钥敏感性测试一个另类但同样重要的指标密钥敏感性本质上是把“差分攻击”的对象从明文换成密钥。做法是固定同一幅明文用原始密钥K加密得到密文C1再把K的某个比特或某一位翻转一下比如把密钥数组的第一个值加1用修改后的密钥加密得到C2。最后计算C1和C2的NPCR和UACI。好的加密算法应该让NPCR接近99.6094%UACI接近33.4635%。如果改变密钥后只有少数像素变化那说明密钥空间里相邻密钥生成的密文高度相似攻击者可以通过邻近搜索方式逼近真实密钥。这一点在混沌加密里尤其重要很多混沌系统对初值敏感但编程实现时如果使用浮点数比较或省去了伪随机序列生成的前若干次迭代可能导致密钥敏感性下降。我一般会在评测脚本里保留一个密钥敏感性测试函数直接复用npcr_uaci只需要额外写一个“密钥加1”的版本。这样论文需要密钥敏感性结果时直接跑一遍就行。5. 我把这些指标打包成一套评测脚本常见坑与排查实录5.1 数据类型与溢出问题这个坑几乎每个初学者都会踩。OpenCV读取图像默认是uint8如果你直接计算img1 - img2当img1像素值小于img2像素值时结果不是负数而是255加上这个负数对应的补码值。这会导致MSE和UACI完全失真。正确做法是在计算前转成float64或int16。特别是UACI中用了绝对值差如果原图是uint8abs(img1 - img2)计算时先做了uint8减法再取abs结果会大得离谱。我之前用uint8算UACI直接得出超过100%的值后来才发现是减法溢出的问题。所以建议所有指标计算都遵守一个原则分析前转换类型不用原始uint8做减法。另外信息熵计算时bincount返回的统计数也是整数如果不转float64直接做除法概率数组会变成整数0结果就是0。需要记住Python的整数除法在numpy中的行为整型数组相除仍为整型所以要先astype(np.float64)。5.2 灰度图和彩色图的处理方法灰度图和彩色图在很多指标里处理方式不太一样。PSNR函数用np.mean可以直接处理彩色但SSIM如果用旧参数multichannelTrue会对R、G、B三通道联合计算如果传灰度图且不设multichannel就会因为维度不匹配报错。我在实际项目中习惯先统一转成灰度图再跑全套指标但这种方法会丢失彩色通道信息有时候加密算法恰好隐藏了某个通道的结构转灰度后反而看不出来。更好的方案是分开处理单通道指标逐通道计算再平均结构相似性指标用多通道模式NPCR/UACI则对全通道一起统计。这样既不会漏掉细节又能和文献里的数据对齐。如果你用cv2.imread读图注意它读进来是BGR顺序不是RGB。做视觉展示时用cv2.cvtColor转成RGB做指标计算时一般不受影响因为像素值本身没变但如果严格需要颜色顺序保持一致就要提前转。5.3 指标算出来“出格”了怎么办有时候指标结果和理论预期差得很远我列几个高频场景PSNR很高但SSIM也很高说明加密没有破坏结构可能只是做了颜色取反或简单的亮度平移。建议检查算法是否只做了逐像素异或。信息熵接近8但相关系数仍然很大说明像素直方图被打散但空间位置关系没打乱。常见于只做灰度映射不改位置的算法需要加入置乱步骤。NPCR很高但UACI偏低说明很多像素发生了改变但改变幅度很小可能加密输出与明文只差几个灰度级。建议观察密文图像是否存在可见轮廓。SSIM出现负值并不意味着有问题密文和明文结构负相关时SSIM为负也是合理的。这恰好说明结构已经被破坏得比较彻底。同一算法在不同图像上指标波动很大这很常见评估算法应该用多张标准测试图Lena、Baboon、Pepper等取平均结果不要只拿一张图说事。遇到指标异常时最快排查方式是把中间结果可视化出来。比如打印直方图分布、画出相邻像素散布图。如果水平方向相邻像素的散布图是类似yx的直线说明相关性为零这个指标肯定没戏。散布图可视化能提供比数字更直观的判断。5.4 一个可直接使用的评测脚本思路我习惯把指标封装成一个统一的评估函数输入原图、密文图和两个密文图直接打印所有指标。这个脚本不涉及具体加密算法只负责评测因此可以复用在任意项目里。函数流程大概是读入原图和密文图检查尺寸一致性。调用psnr和ssim得到原始图与密文图之间的差异。计算密文信息熵和相邻像素相关系数。如果还有第二张密文图就计算NPCR和UACI。把所有指标拼接成一个字典或表格输出。这样每次实验只需要调用一次省下大量重复代码也让实验结果有统一标准。我自己的评测脚本里还会把结果保存成CSV方便不同算法对比。最后再分享一个小经验性能评价指标不是为了数字好看而是为了发现算法的薄弱环节。我见过有人把加密算法做到NPCR 99.7%、UACI 33.5%但密钥空间只有2的20次方这种算法依然不安全。指标能告诉你“加密充分不充分”但算法安全性最终还是要结合密钥设计、密码分析等多方面因素综合判断。你把这套指标跑熟之后再回头审视自己的加密算法很多隐藏问题会清晰很多。
返回列表