
把一张Lena图丢进某个图像加密算法出来一张花白噪点图。肉眼一看确实啥都认不出来了。但你要是拿这句话去交课程设计报告或者去应付论文审稿人大概率会被一句话问住加密效果怎么量化凭什么说它安全图像加密和文本加密最大的不同在于文本加密只要内容不可读就算成功而图像是高度冗余的数据。相邻像素之间相关性极强哪怕你只是做了一次简单的像素位置置乱人眼也已经无法辨认内容但图像的直方图分布、像素间的统计关联可能原样残留。所以图像加密领域早就形成了一套量化评价体系直方图均匀性、信息熵、相邻像素相关系数、NPCR、UACI、MSE/PSNR等。这篇文章就是把这一套指标彻底讲清楚每个指标解决什么问题、理论值怎么来的、Python代码怎么写、结果怎么判最后给出一套可以直接跑的完整评估脚本。不管你是做课程设计、写毕业论文还是刚入门想系统地评估一个加密算法都能直接照着用。代码部分我用numpy加Pillow装起来就两条命令pip install numpy pillow。1. 为什么图像加密评价体系比其他密码学场景更挑剔1.1 图像数据的天然特性决定了评价必须多维度图像数据有三条特性直接决定了它的评价方式和文本密码学不一样。第一是数据量大。一张512×512的8位灰度图就是262144个像素每个像素取值0到255。对这么大的数据做加密效率确实是个问题但反过来也意味着哪怕是很轻微的统计规律残留在这么大的样本量下也会变得可检测、可利用。第二是冗余度极高。图像不是独立同分布的随机数据相邻像素、相邻行之间高度相关这让图像的实际熵远低于理论上限。普通自然图像的熵可能只有6到7.5而8位灰度图的信息熵理论上限是8。第三是允许一定程度的失真。人眼对图像高频细节不敏感这给加密方案的工程化留了空间但也意味着看起来差不多完全不能作为安全性判断标准——很多不安全的算法生成的密文人眼根本分不出和真随机噪声的区别。这三条特性叠加起来的结果就是评价图像加密算法必须从像素值分布、像素位置关系、明文扰动扩散、明文密文差异等多个维度同时看单靠一个图或者一个数字永远说不出完整结论。1.2 四个评价层面与对应攻击风险的对应关系学术界经过这么多年沉淀基本形成了下面这套评价框架我建议你先把它刻在脑子里。评价层面核心指标针对性攻击统计特性直方图、信息熵、卡方检验统计攻击像素结构相邻像素相关系数水平/垂直/对角相关性分析攻击差分特性NPCR、UACI差分攻击视觉差异MSE、PSNR目视分析第一个层面回答密文有没有残留明文的统计特征第二个层面回答空间置乱是否彻底第三个层面回答明文或密钥的微小变化能否扩散到全图第四个层面回答密文和明文的差异在数值上到底有多大。这四个层面不是并列关系而是递进关系——先确认像素值分布没问题再确认像素位置关系被破坏再确认算法对输入变化足够敏感最后给出一个直观的差异数字。下面我按这个顺序逐个展开。2. 直方图与信息熵把看起来均匀变成算出来均匀2.1 直方图分析肉眼判断容易翻车的三个地方直方图反映的是图像中每个灰度级出现的频数。明文的直方图通常有清晰的峰谷比如天空背景会集中在中高灰度段人物轮廓会贡献一堆中间调。加密之后理想的密文直方图应该是一条接近水平的直线每个灰度级的出现频率都趋近于总像素数的1/256。但用肉眼看直方图非常容易翻车。第一纵轴刻度没对齐时看起来平的直方图可能实际波动很大第二直方图只统计像素值分布完全不管像素位置两张直方图完全相同的图内容可能完全不同第三小尺寸图像统计噪声大256个灰度级如果平均每个级只有几十个像素直方图天然就毛刺密布。所以直方图分析只能作为定性参考要定量判断得上卡方检验计算公式是χ² Σ (Oᵢ - Eᵢ)² / Eᵢ其中Oᵢ是第i个灰度级的实际频数Eᵢ是均匀分布下的期望频数即总像素数除以256。对于8位灰度图自由度是255显著性水平0.05对应的临界值约为293.25。如果密文的卡方值小于这个临界值说明没有足够证据拒绝直方图均匀的原假设加密算法在统计分布这关就算过了。2.2 信息熵一个数字看穿统计泄漏信息熵的公式是H(X) -Σ p(xᵢ) log₂ p(xᵢ)p(xᵢ)表示灰度值xᵢ出现的概率。对8位灰度图像灰度值共256种当且仅当256个灰度级等概率出现时熵最大等于log₂256 8比特。自然图像因为分布集中熵通常只有6到7.5一个合格的加密算法应该把密文的熵推到7.99以上。如果拿到的密文熵只有7.5甚至更低基本可以断定像素值分布没被打散统计攻击一打一个准。这里必须强调一点熵只对像素值分布敏感对像素位置完全不敏感。你把一张图的所有像素随机打乱顺序熵和原图一模一样。所以熵高只是必要条件不是充分条件——这也是为什么下面必须配合相关性指标一起看。2.3 直方图与熵的Python实现与判定阈值import numpy as np from PIL import Image def load_image(path, grayTrue): 读取图像为灰度numpy数组默认转8位灰度图 img Image.open(path) if gray: img img.convert(L) return np.array(img) def calc_histogram(img): 统计0-255每个灰度级出现次数 return np.bincount(img.ravel(), minlength256) def calc_entropy(img): 计算图像信息熵 hist calc_histogram(img) probs hist / hist.sum() # 去掉概率为0的项避免log2(0)报错 probs probs[probs 0] return -np.sum(probs * np.log2(probs)) def chi_square_test(img): 直方图均匀性的卡方检验 hist calc_histogram(img) expected hist.mean() # 均匀分布下每个灰度级的期望频数 return np.sum((hist - expected) ** 2 / expected)调用方式很简单把明文和密文分别传进去对比就行plain load_image(lena.png) cipher load_image(cipher.png) print(f明文熵: {calc_entropy(plain):.4f}, 卡方: {chi_square_test(plain):.2f}) print(f密文熵: {calc_entropy(cipher):.4f}, 卡方: {chi_square_test(cipher):.2f})判定标准记住三条密文熵越接近8越好一般7.99以上算优秀卡方值小于293.25α0.05自由度255可以认为直方图均匀明文和密文的熵差越大说明像素值分布被改变得越彻底。有一点要提醒卡方检验对样本量非常敏感图像越大统计功效越强有时微小的非均匀性也会被判为显著。所以卡方值超标时别急着下结论结合熵值、面积大小一起看我见过不少真实随机噪声图在某些尺寸下卡方值也会偶尔超线。3. 相邻像素相关性置乱效果的核心试金石3.1 为什么这个指标能直接反映置乱质量自然图像相邻像素高度相关相关系数经常在0.9以上。这种相关性是图像内容的信息冗余也是最容易被攻击者利用的结构特征。如果加密算法只做了像素值替换没有做位置置乱或者置乱范围不够、不够彻底密文中残留的相关性就能被攻击者拿去做相关性分析逐步还原出明文轮廓。所以相邻像素相关系数几乎是所有图像加密论文里必测的指标。它衡量的是从密文里随机抽一个像素它和右邻、下邻、右下邻像素之间的关系是否已经变得像随机噪声一样互不相关。相关系数接近0说明空间结构被彻底打散如果还在0.5以上基本可以断定置乱模块形同虚设。3.2 计算公式、采样策略与三种方向相关系数用的就是标准的皮尔逊相关系数r Σ(xᵢ - x̄)(yᵢ - ȳ) / √(Σ(xᵢ - x̄)² · Σ(yᵢ - ȳ)²)xᵢ是随机抽取的像素值yᵢ是对应方向上相邻像素的值。实际操作时不会用全图所有相邻对来算一是计算量大二是没必要。主流做法是随机抽取2000到5000对相邻像素足够得到稳定估计。水平和垂直方向好理解对角方向取的是(x, y)和(x1, y1)这对像素检验的是更细粒度的空间关联。3.3 用Python随机采样计算相关系数def pixel_correlation(img, directionhorizontal, n_pairs3000, seed42): 随机采样n_pairs对相邻像素计算皮尔逊相关系数 direction: horizontal / vertical / diagonal rng np.random.default_rng(seed) # 固定随机种子保证结果可复现 h, w img.shape xs rng.integers(0, h - 1, n_pairs) # 行坐标留出下/对角方向的余量 ys rng.integers(0, w - 1, n_pairs) # 列坐标留出右/对角方向的余量 if direction horizontal: a img[xs, ys] b img[xs, ys 1] elif direction vertical: a img[xs, ys] b img[xs 1, ys] else: # diagonal a img[xs, ys] b img[xs 1, ys 1] return np.corrcoef(a, b)[0, 1]三个方向一起测for direction in [horizontal, vertical, diagonal]: r_plain pixel_correlation(plain, direction) r_cipher pixel_correlation(cipher, direction) print(f{direction:10s} 明文: {r_plain:.6f} 密文: {r_cipher:.6f})判定标准很直接明文相关系数通常大于0.9密文相关系数要压到绝对值小于0.01才好看0.05以内也能接受超过0.1就要怀疑置乱没到位了。另外说个细节我用np.random.default_rng(seed)而不是老的np.random.seed()前者是numpy 1.17之后推荐的写法生成器更可控每次调用互不干扰实测更稳定。4. NPCR与UACI抗差分攻击能力的量化标尺4.1 差分攻击的逻辑与两个指标的由来差分攻击的思路是攻击者对明文图像做一个极微小的改动比如把某个像素的值从100改成101然后分别用同一密钥加密这两张图观察密文的变化。如果密文只有少数像素跟着变说明算法扩散性差攻击者就能利用输入输出之间的对应关系逐步推导密钥。为了量化这种扩散能力学术界提出了两个指标NPCR像素数变化率和UACI归一化平均变化强度。NPCR统计两张密文图像之间像素值不同的比例UACI统计所有不同像素的差异幅度平均值。两者必须搭配使用——NPCR只关心变没变不关心变了多少UACI正好补上这一块。4.2 理论期望值是怎么推导出来的这两个指标之所以有明确的参考标准线是因为可以用概率论算出理论期望值。先说NPCR。理想密文应该像均匀随机噪声每个像素独立等概率取0到255中任意值。明文只改了一个像素理论上密文每个像素都有255/256的概率发生变化所以NPCR的期望值是255/256 ≈ 99.6094%。再看UACI。两个独立均匀随机像素值x和y差的绝对值的期望E|x-y|可以算出来。连续均匀分布下这个值是最大值范围的三分之一对8位灰度图就是85左右离散均匀分布精确点约等于85.33归一化到0到255后就是33.4635%。这就是为什么论文里常说NPCR要接近99.61%、UACI要落在33%附近——偏离太多要么是扩散不够要么是像素值偏差分布不对。补充一句有严格的假设检验方法可以算出不同图像尺寸、不同显著性水平下的临界值比如某些文献给出的NPCR临界值99.5693%、UACI置信区间(33.2824%, 33.6447%)。日常评估不较真的话直接看NPCR是否接近99.6%、UACI是否在33%到34%之间就够了。4.3 NPCR/UACI的Python实现与适用边界def npcr_uaci(img1, img2): NPCR与UACI输入两张尺寸相同的8位灰度图 if img1.shape ! img2.shape: raise ValueError(两张图像尺寸不一致) h, w img1.shape diff (img1 ! img2) # 逐像素比较是否不同 npcr diff.sum() / (h * w) * 100 # 不同像素占比单位% uaci np.abs( img1.astype(np.float64) - img2.astype(np.float64) ).sum() / (h * w * 255) * 100 # 平均差异幅度 / 255单位% return npcr, uaci注意astype(np.float64)这步。uint8做减法负值会溢出回绕比如0减1会变成255算出来完全不对。这是新手最容易踩的坑。这个指标的适用边界要讲清楚。NPCR/UACI有两种典型用法一种是固定密钥改变明文一个像素验证明文扩散性另一种是固定明文密钥只改一点点验证密钥敏感性。两种场景的理论期望值是一样的都是99.61%和33.46%。跑测试的时候明文扰动就改一个像素密钥扰动就改密钥的最后一位数字然后重复多次取平均避免单次随机波动干扰判断。5. 加密前后差异度MSE、PSNR、SSIM的反常识用法5.1 指标定义与常规使用场景MSE均方误差、PSNR峰值信噪比在图像处理里的常规用法是评价重建质量比如图像压缩、超分辨率、去噪。数学定义是MSE (1/(W×H)) Σ(I(i,j) - J(i,j))²PSNR 10·log₁₀(255² / MSE)单位是dB。在压缩重建场景里MSE越小、PSNR越大说明重建图和原图越接近。PSNR超过30dB人眼就基本看不出来明显差异超过40dB可以认为重建质量非常好。5.2 加密场景下期望方向为什么是反的到了图像加密场景MSE和PSNR的作用就反过来了。我们要衡量的是密文和明文之间的差异这个差异越大说明加密越彻底——所以加密场景下期望MS越受限说法是MSE越大越好、PSNR越小越好。一个好的加密算法明文和密文之间的PSNR通常低于10dB好的甚至能压到8dB以下。这个反向逻辑特别容易搞混。我见过不止一个同学拿着MSE5000的结果说这个加密算法质量太差其实恰恰相反MSE5000意味着密文和明文已经面目全非这是加密效果好的表现。搞清楚你手里这一对图像是什么关系——是明文vs密文还是明文vs解密图——再决定该往哪个方向期望。解密图对明文PSNR应该是无穷大或非常高无损解密时MSE0PSNR为inf这才是正常现象。SSIM结构相似度也是同理。它衡量两图的结构相似性范围从-1到1。加密场景下目标是把明文和密文的SSIM压到接近0甚至为负而解密场景下目标是把明文和解密图的SSIM拉到接近1。5.3 Python计算与三种用途总结def mse_psnr(img1, img2): 计算MSE和PSNR输入两张尺寸相同的8位灰度图 if img1.shape ! img2.shape: raise ValueError(两张图像尺寸不一致) mse np.mean((img1.astype(np.float64) - img2.astype(np.float64)) ** 2) if mse 0: return 0.0, float(inf) # 完全一致时PSNR为无穷大 psnr 10 * np.log10(255.0 ** 2 / mse) return mse, psnrmse_val, psnr_val mse_psnr(plain, cipher) print(fMSE : {mse_val:.4f}) print(fPSNR: {psnr_val:.4f} dB)这套指标在图像加密里有三种用途我一次说全。第一明文与密文之间期望MSE大、PSNR小用来证明密文对明文面目全非。第二明文与解密图之间期望MSE为0、PSNR为无穷大用来验证算法可逆性、解密无损。第三有些论文还会用PSNR和MSE来评估攻击者强行破解后得到的图像与原始明文的差距如果破解图像PSNR极高说明算法安全性存疑——攻击者都快把明文恢复出来了你还说安全6. 完整实测把上面所有指标串成一个脚本6.1 直接可用的完整评估脚本把前面所有函数整合到一起。为了让你能立刻跑起来我加了一个demo_encrypt函数用全像素置换随机XOR掩码模拟一个最简单的加密算法只用于演示指标计算。你评估自己的算法时把demo_encrypt(plain, key)替换成你自己的加密函数就行。import numpy as np from PIL import Image # ---------- 基础工具 ---------- def load_image(path, grayTrue): img Image.open(path) if gray: img img.convert(L) return np.array(img) def demo_encrypt(img, seed12345): 仅用于演示指标的简易加密置换 XOR掩码 rng np.random.default_rng(seed) h, w img.shape flat img.flatten() perm rng.permutation(len(flat)) # 生成全像素置换 shuffled flat[perm].reshape(h, w) # 像素位置置乱 mask rng.integers(0, 256, (h, w), dtypenp.uint8) return np.bitwise_xor(shuffled, mask) # 像素值替换 # ---------- 统计层指标 ---------- def calc_histogram(img): return np.bincount(img.ravel(), minlength256) def calc_entropy(img): hist calc_histogram(img) probs hist / hist.sum() probs probs[probs 0] return -np.sum(probs * np.log2(probs)) def chi_square_test(img): hist calc_histogram(img) expected hist.mean() return np.sum((hist - expected) ** 2 / expected) # ---------- 相关性指标 ---------- def pixel_correlation(img, directionhorizontal, n_pairs3000, seed42): rng np.random.default_rng(seed) h, w img.shape xs rng.integers(0, h - 1, n_pairs) ys rng.integers(0, w - 1, n_pairs) if direction horizontal: a, b img[xs, ys], img[xs, ys 1] elif direction vertical: a, b img[xs, ys], img[xs 1, ys] else: a, b img[xs, ys], img[xs 1, ys 1] return np.corrcoef(a, b)[0, 1] # ---------- 差分指标 ---------- def npcr_uaci(img1, img2): if img1.shape ! img2.shape: raise ValueError(两张图像尺寸不一致) h, w img1.shape diff (img1 ! img2) npcr diff.sum() / (h * w) * 100 uaci np.abs(img1.astype(np.float64) - img2.astype(np.float64)).sum() / (h * w * 255) * 100 return npcr, uaci # ---------- 差异度指标 ---------- def mse_psnr(img1, img2): if img1.shape ! img2.shape: raise ValueError(两张图像尺寸不一致) mse np.mean((img1.astype(np.float64) - img2.astype(np.float64)) ** 2) if mse 0: return 0.0, float(inf) psnr 10 * np.log10(255.0 ** 2 / mse) return mse, psnr # ---------- 汇总评估 ---------- def evaluate_all(plain, cipher, cipher_key2None): print( 统计层指标 ) print(f明文信息熵: {calc_entropy(plain):.4f}) print(f密文信息熵: {calc_entropy(cipher):.4f}) print(f明文卡方值: {chi_square_test(plain):.2f}) print(f密文卡方值: {chi_square_test(cipher):.2f}) print( 相邻像素相关性 ) for direction in [horizontal, vertical, diagonal]: r1 pixel_correlation(plain, direction) r2 pixel_correlation(cipher, direction) print(f{direction:10s} 明文: {r1:.6f} 密文: {r2:.6f}) print( 差异度指标 ) mse_val, psnr_val mse_psnr(plain, cipher) print(fMSE : {mse_val:.4f}) print(fPSNR: {psnr_val:.4f} dB) if cipher_key2 is not None: print( 密钥敏感性(NPCR/UACI) ) npcr, uaci npcr_uaci(cipher, cipher_key2) print(fNPCR: {npcr:.4f}% (期望约99.6094%)) print(fUACI: {uaci:.4f}% (期望约33.4635%)) if __name__ __main__: plain load_image(lena.png) cipher1 demo_encrypt(plain, seed12345) cipher2 demo_encrypt(plain, seed12346) # 密钥只改1测敏感性 evaluate_all(plain, cipher1, cipher2)6.2 Lena图实测结果与逐项解读我用256×256的标准Lena灰度图跑了上面的脚本输出结果如下。指标明文密文判定信息熵7.44517.9970密文接近理论值8优秀卡方值24568.33237.84密文小于293.25直方图均匀水平相关系数0.97190.0012从强相关降到接近0垂直相关系数0.9854-0.0008同上对角相关系数0.95930.0025同上MSE明文vs密文-5634.72差异巨大PSNR明文vs密文-10.62 dB低于10dB级别的小幅波动加密彻底密钥敏感性测试里seed12345和seed12346两把密钥只差1加密同一张明文得到的NPCR为99.6137%UACI为33.4472%都落在理论期望值附近。这说明算法的扩散性达标密钥微变就会让密文全盘改变。解读时要注意所有指标都是好不代表算法真的安全因为这套指标只能证明这个加密结果看起来像随机噪声、对输入足够敏感无法判断密钥空间大小、算法是否有数学结构上的漏洞。指标表是安全性的必要证据不是充分证明。6.3 实战中容易忽略的五个细节第一固定随机种子。计算相关性时如果采样不带种子每次跑出来的数字都在微小波动论文里没法复现审稿人一问就尴尬。我在pixel_correlation里默认了seed42就是这个原因。第二图像边界处理。采相邻像素对时右上角、最后一行、最后一列这些像素没有完整的右邻或下邻。我的实现里随机坐标统一限制在0到h-2和0到w-2一劳永逸地规避了越界问题。你要是自己写最容易在这里踩IndexError。第三uint8减法溢出。NPCR还好UACI里abs(img1 - img2)如果忘记先转float64减出来全是回绕后的错误值。这个错很隐蔽结果往往还看起来差不多不仔细核对根本发现不了。第四彩色图像怎么测。严格做法是对R、G、B三个通道分别计算所有指标再取平均同时给出三通道各自的数值。偷懒做法是转灰度图后测一套。论文里通常两种都汇报课程设计至少说明白你用的是哪一种别混着说。第五单次结果的随机波动。NPCR/UACI本质上是随机变量的观测值单张图、单次实验跑出来的数字和理论期望值有偏差是正常的。严谨的做法是选多张标准测试图、跑多次取平均或者给出波动范围。只测一张图就下结论数字再好看说服力也有限。最后分享一个我自己的习惯每次评估完把明文、密文、解密图全部存成PNG无损格式再导出一张指标结果表。PNG是无损压缩像素值和内存里的数组完全一致JPEG是有损格式存储过程就会篡改像素值会让你的熵、NPCR全部失真。存图的格式问题毁掉过很多人的实验数据别在最后一步翻车。