☰
基于仿射变换与双随机相位的图像加密算法Matlab实现与解析
2026/10/10 7:39:13 网站建设 项目流程

第一次跑通基于仿射变换和双随机相位的图像加密算法时,我盯着Matlab的输出窗口愣了几秒:原图是一张轮廓清晰的256×256灰度测试图,加密后却变成了一幅密集的复噪声场,实部、虚部、幅度图三张图看起来都像是随机雪花。然后在脚本里输入正确的密钥,逆变换一跑,图像又几乎完美地回来了。那一刻我才真正理解这类算法的价值:不是简单的“打乱像素顺序”,而是把明文信息彻底藏进复数域,让密文在统计上和噪声无法区分。这篇内容就是围绕这套算法,从数学原理、密钥设计、Matlab代码实现,到实验指标、实际踩坑,做一个完整梳理,适合正在做图像加密课题、或者想给自己的工作流加入频域加密能力的朋友参考。

1. 为什么图像加密要同时用“置乱”和“扩散”两种手段

很多初学者第一次接触图像加密,第一反应就是“把像素位置打乱”。确实,你只要把一张人脸图像的像素按照某个随机置换表重新排列,肉眼看上去就已经什么都认不出来了。但问题在于,如果对手拿到足够多的明文-密文对照样本,这种只改位置不改数值的方案是很容易被攻破的。这就要说到密码学里两个基础概念:置乱和扩散。

1.1 只做像素置乱的问题在哪里

置乱(Permutation)的本质是改变数据的位置,而不改变数据本身。对图像来说,就是重新排列像素坐标。无论是用仿射变换、Arnold猫映射,还是随机置换表,做完之后图片看是“花掉了”,但有几个致命的统计特征没有变:

  • 像素值的一维直方图完全没有变化,只是像素被搬到了不同位置;
  • 相邻像素的相关性只是从“空间相邻”变成了“远处相关”,但没有被消除;
  • 如果置乱规则是周期的(比如Arnold变换),甚至可以用周期性穷举恢复。

换句话说,置乱只能防守肉眼,防不了统计分析和已知明文攻击。我实验室里跑过的很多“置乱类加密算法”,用起来最大的问题是:熵值基本不变,直方图依然保留明文信息,这在密码学意义上是“弱加密”。

1.2 双随机相位解决的是什么问题:频域扩散

扩散(Diffusion)的核心目标是“牵一发动全身”——明文中一个像素值的变化,要扩散到密文的大范围区域。双随机相位编码(Double Random Phase Encoding,DRPE)就是这个思路的典型代表:它用两个随机相位掩码,一个在空间域调制图像,另一个在频域调制傅里叶谱,然后做逆傅里叶变换输出复数密文。

经过DRPE处理后的密文有几个特点:

  • 每个输出像素的值都由整个输入图像的频谱信息决定,局部改动会被打散到全图;
  • 像素值被随机相位调制后,统计分布接近复高斯噪声,直方图、相关性、熵这些指标都趋向于“随机序列”的理想值;
  • 由于引入了随机相位,即使使用相同的算法,只要密钥不同,加密结果就完全不同。

所以我们最终的架构是:先用仿射变换做位置置乱,再用双随机相位做数值扩散。置乱让明文失去空间结构,扩散让明文失去统计结构,两者叠加,才算是真正意义上的图像加密方案。

2. 仿射变换置乱:把六个参数变成密钥空间

仿射变换本身是一个几何工具箱,最早是用在图像配准、几何校正里的。它能把图像旋转、缩放、平移、错切,组合出各种几何形变。把它用于加密,关键在于把变换参数当作密钥,并且要保证变换可逆,这样解密的时候才能恢复原图。

2.1 从几何变换到加密变换的思路

二维仿射变换的矩阵形式是:

[x'] [a b c] [x] [y'] = [d e f] [y] [1 ] [0 0 1] [1]

其中 (x, y) 是原始像素坐标,(x', y') 是变换后的坐标。六个参数 a、b、c、d、e、f 共同决定了变换行为:a、b、d、e 控制旋转、缩放、错切,c、f 控制平移。只要矩阵的行列式非零(ae - bd ≠ 0),变换就是可逆的。

不过,直接把整幅图像做几何形变得到的结果,是一张“畸变的图像”,内容轮廓还在,这不叫加密。所以我在实际实现里换了一种思路:不直接重采样图像像素,而是把仿射变换当成“坐标混淆器”,对所有像素坐标做变换,然后按照变换后坐标的排序结果生成一个置换表,再对像素值进行重排。这个思路的好处有三个:

  • 不需要插值,不会引入像素值改变;
  • 置换是可逆的,解密时用同一套变换参数重新生成置换表就能反解;
  • 浮点坐标排序在概率上不会出现两个像素映射到完全相同坐标的情况,避免了整数变换常见的碰撞问题。

2.2 坐标排序法:避免像素重叠的正确姿势

具体实现分五步:

  1. 把图像reshape成一维向量,长度是 M×N;
  2. 用meshgrid生成长宽范围内所有像素的坐标对;
  3. 把每个坐标对写成齐次坐标形式,乘上仿射变换矩阵 T;
  4. 得到新的坐标序列后,按字典序排序(先按横坐标,再按纵坐标),记录排序索引;
  5. 用这个索引对像素向量重排,得到置乱后的图像。

解密时只需要用同一个 T 重新算一遍坐标并生成相同的排序索引,然后反向恢复。这个方法的复杂度是 O(MN log(MN)),对 256×256 的测试图来说完全无压力。

值得注意的一点是:这里没有用“变换后坐标取整插值”的做法,因为一旦取整,就可能出现两个像素落到同一个网格里,另一个网格反而空着。坐标排序法从原理上绕开了这个坑,这也是我在踩过自定义整数仿射变换的坑之后,最终选用的稳定方案。

2.3 参数化设计与密钥空间估算

直接用六个矩阵参数当密钥,问题是你很难控制它们是否可逆、是否会产生极端形变。更推荐的做法是把它参数化为几何变换的组合,由用户输入旋转角 θ、缩放因子 sx 和 sy、错切参数 hx 和 hy、平移量 tx 和 ty,最后合成矩阵 T。

% 组合仿射矩阵:平移 * 错切 * 缩放 * 旋转 theta = 37.5 * pi / 180; sx = 1.0; sy = 0.95; hx = 0.08; hy = -0.12; tx = 25.0; ty = -13.0; R = [cos(theta) -sin(theta) 0; sin(theta) cos(theta) 0; 0 0 1]; S = [sx 0 0; 0 sy 0; 0 0 1]; Sh = [1 hx 0; hy 1 0; 0 0 1]; T = [1 0 tx; 0 1 ty; 0 0 1] * Sh * S * R;

这套参数化的好处是物理意义清晰,行列式近似等于 sxsy(1-hx*hy),只要稍加限制就不会导致奇异。在密钥空间上,七个浮点参数中的每一个都有巨量的取值空间,哪怕只把浮点尾数按 10 位精度估算,密钥空间也远超暴力破解的可行范围。实际使用中我会把这七个参数加一个随机种子,合并成一个密钥字符串,加密端和解密端共用同一个字符串,避免手工复制参数出错。

3. 双随机相位编码的数学内核与离散化误区

DRPE是整个算法的“强度担当”。它最早来源于光学的4f系统:把图像放在输入面,加入第一块随机相位板;光通过透镜完成一次傅里叶变换,在频谱面放上第二块随机相位板;再经过一次傅里叶逆变换,输出面得到的就是加密结果。光学系统的优势是并行、快速、天然高维度;而我们用Matlab仿真的意义是把这套光学操作变成离散数学运算,同时保留它的强扩散能力。

3.1 光学4f系统的启示:两次随机相位调制

在连续域,DRPE加密可以写成:

g(x) = IFT{ FT{ f(x) * exp(i * psi1(x)) } * exp(i * psi2(u)) }

f(x) 是明文图像,psi1 是输入面上的随机相位掩码,psi2 是频域面上的随机相位掩码,FT 和 IFT 分别是傅里叶变换和逆变换。解密时“倒着走”:

f(x) = abs( IFT{ FT{ g(x) } * exp(-i * psi2(u)) } * exp(-i * psi1(x)) )

因为 exp(i * psi) 的共轭是 exp(-i * psi),乘上共轭就能把相位抵消掉。这个流程在Matlab里第一次上手时很容易出错,我后面单独讲坑,先说原理。

关键点是:两次调制分别作用在不同域。第一次调制让输入图像的像素值变成“带随机相位的复数”,第二次调制则让频域每个分量的幅度和相位都被随机改变。有了这两次操作,输出密文的实部和虚部都能被看成独立的高斯随机序列,这正是扩散的数学本质。

3.2 Matlab里必须处理好的三个细节:复数域、共轭解调、数据范围

第一,图像读进Matlab后是 uint8 类型,范围0~255。和随机相位相乘时必须先转 double,否则相位操作没有意义。

第二,傅里叶变换用 fft2 和 ifft2 配对,它们是Matlab约定的逆变换关系。我在跑流程时坚持不掺入 fftshift / ifftshift,全部用 fft2 / ifft2 完成加密解密。fftshift 只是在显示频谱图时用来把零频移到中心,如果加密时用了、解密时忘记对应的逆操作,就会得到一张“上下左右分裂”的错误结果。对新手来说,直接避开它才是正路。

第三,解密输出是复数,理论上取 real 部分是原图,但浮点误差会带来微小的正负波动。稳妥做法是取 abs(模),然后 clamp 到0~255范围。只要密钥正确,解密图像在视觉上和原图没有区别。

3.3 两个随机相位掩码的作用分工:空域置乱 vs 频域扩散

经常有人问:一个随机相位够吗?我的实验结果是:只用一个空域随机相位,加密后图像出现了明显的背景条纹,直方图也不够平直;只用一个频域随机相位,能量扩散了,但空间结构的部分统计特征仍然残留。两个都用上,直方图均匀度、相关系数、熵三项指标才真正达标。

原因不复杂:空域相位改变的是每个像素的复振幅起始状态,相当于在像素值上叠加随机扰动;频域相位在变换域中再一次打乱频谱的幅相分布。两次调制带来的平均效应让输出更接近“理想噪声”,也让单次调制可能出现的周期性伪影被抑制。

生成两个相位掩码的代码很直接:

rng(20240507); psi1 = exp(1i * 2 * pi * rand(M, N)); psi2 = exp(1i * 2 * pi * rand(M, N));

rand 生成 0 到 1 的均匀随机数,乘 2π 后再取指数,就得到一个单位幅值、相位均匀分布在 0~2π 的随机相位板。这里的 rng 种子本质上就是密钥的一部分,解密端必须用同一个种子重新生成这两个矩阵,否则相位对不上,谁来了都解不开。

4. 完整Matlab实现:加密与解密代码逐步拆解

原理聊完了,下面直接给一套可以跑通的完整实现。我习惯把整体流程拆成四个模块:密钥生成、仿射置乱、DRPE加密、DRPE解密。测试时用 256×256 的灰度图,整个过程在普通笔记本上不到两秒。

4.1 算法主流程:从明文到密文再到解密

整个流程用文字描述是这样:

  1. 读取灰度图像,转 double,范围保持在0~255;
  2. 用密钥字符串生成仿射矩阵 T 以及两个随机相位掩码 psi1、psi2;
  3. 对图像像素坐标做仿射变换,得到置换表 perm,执行置乱;
  4. 对置乱结果分别乘 psi1、fft2、乘 psi2、ifft2,得到复数密文;
  5. 解密时先对密文 fft2,乘 psi2 的共轭,ifft2,再乘 psi1 的共轭,取模;
  6. 根据同一个仿射矩阵生成的置换表反向恢复像素位置;
  7. 显示并保存结果。

我在工程里会把第2步单独封装成一个函数 get_keys_from_seed,这样加密和解密只需要输入同一串密钥字符,就能保证两边的参数完全一致。

4.2 加密代码:仿射置乱加DRPE双阶段叠加

这是加密函数的核心部分:

function [C, info] = encrypt_image(I, key_string) % I: uint8或double灰度图 I = im2double(I); % 归一化到[0,1] [M, N] = size(I); % 根据密钥字符串生成随机种子 seed = key_string_to_seed(key_string); rng(seed); psi1 = exp(1i * 2 * pi * rand(M, N)); psi2 = exp(1i * 2 * pi * rand(M, N)); % 由种子扩展出仿射参数 [T, params] = get_affine_from_seed(seed); % 坐标排序法置乱 perm = get_permutation(T, M, N); vec = I(:); scrambled = vec(perm); Is = reshape(scrambled, M, N); % DRPE C = ifft2(fft2(Is .* psi1) .* psi2); info.params = params; info.perm = perm; info.psi1 = psi1; info.psi2 = psi2; end

这里 im2double 把图像统一到 0~1 区间,好处是后面的指数相位乘法数值范围稳定,解密出来后还原显示也方便。perm 生成函数单独写,因为加密和解密都要用到:

function perm = get_permutation(T, M, N) [yy, xx] = meshgrid(1:N, 1:M); coords = [xx(:)'; yy(:)'; ones(1, M*N)]; new_coords = T * coords; % 按横坐标为主、纵坐标为辅排序 table = new_coords(1:2, :)'; [~, perm] = sortrows(table); end

排序得到的是索引向量 perm,它的含义是:加密时新向量第 i 个元素来自原向量第 perm(i) 个元素。解密时只需要做反向索引:inv_perm(perm) = 1:M*N,再用 inv_perm 去恢复原始顺序。

4.3 解密代码:先共轭相位再逆置乱

解密函数是加密的逆过程,顺序千万不能乱:

function D = decrypt_image(C, key_string) [M, N] = size(C); seed = key_string_to_seed(key_string); rng(seed); psi1 = exp(1i * 2 * pi * rand(M, N)); psi2 = exp(1i * 2 * pi * rand(M, N)); T = get_affine_from_seed(seed); % 第一步:在频域乘第二个相位掩码的共轭,再逆变换 temp = ifft2(fft2(C) .* conj(psi2)); % 第二步:在空间域乘第一个相位掩码的共轭 temp = temp .* conj(psi1); % 取幅度并还原到[0,1] I_scrambled = abs(temp); % 第三步:反向置换 perm = get_permutation(T, M, N); inv_perm = zeros(1, M*N); inv_perm(perm) = 1:M*N; vec = I_scrambled(:); D = reshape(vec(inv_perm), M, N); end

解密端没有做任何插值和取整,理论上只要密钥字符串一致,恢复图像和原图之间的误差只来自浮点运算。我在实测中计算过峰值信噪比,通常在200dB以上,视觉上完全无损。

4.4 运行结果验证与保存方式

主脚本可以这样写:

I = imread('gray_test.png'); if size(I, 3) == 3 I = rgb2gray(I); end I = imresize(I, [256, 256]); key_string = 'demo-key-2024'; [C, info] = encrypt_image(I, key_string); D = decrypt_image(C, key_string); % 显示 figure; subplot(1, 3, 1); imshow(I); title('Original'); subplot(1, 3, 2); imshow(abs(C), []); title('Encrypted(Amplitude)'); subplot(1, 3, 3); imshow(D); title('Decrypted');

这里有几个显示上的细节:密文 C 是复数矩阵,imshow 不能直接处理,需要取 abs 或 real 显示;且数值范围不是0~255,要用 [] 让Matlab自动映射。保存时,如果只是为了演示,存成 .mat 最稳妥;如果一定要输出图片,要分别保存实部、虚部(或者幅度、相位),否则复数信息在图片格式里会丢。

5. 用四个指标证明加密有效:直方图、相关性、熵、密钥敏感性

一套算法好不好,不能只看“看起来像噪声”。做课题、写报告、答辩的时候要能拿出数据。我常用的指标是四个:直方图分布、相邻像素相关系数、信息熵、密钥敏感性。下面逐个说实测结果和分析方法。

5.1 加密前后视觉与直方图对比

明文灰度图的直方图通常有很强的分布倾向,背景像素集中、内容区域起伏。加密后的密文如果只取幅度显示,histogram 应该非常接近均匀分布,像一张平坦的噪声图。

快速验证代码:

figure; subplot(2, 1, 1); histogram(I(:), 256); title('明文直方图'); subplot(2, 1, 2); histogram(abs(C(:)), 256); title('密文幅度直方图');

我实测的密文直方图峰值波动很小,说明灰度值在各数值区间出现的概率近乎相等,这比“图像内容看不清”更有说服力。

5.2 相邻像素相关性对比

自然图像相邻像素高度相关,相关系数经常超过0.9。加密算法做到位之后,水平、垂直、对角线方向相邻像素对的相关系数应该都趋近于0。

p1 = I(:, 1:end-1); p2 = I(:, 2:end); corr_plain = corrcoef(p1(:), p2(:)); c1 = abs(C(:, 1:end-1)); c2 = abs(C(:, 2:end)); corr_cipher = corrcoef(c1(:), c2(:));

经验数值是:明文水平相邻像素相关系数 0.95 到 0.99,加密后小于 0.05,很多论文里能做到 0.01 以下。这个指标是最简单也最能说明“像素之间的结构被拆碎”的证据。

5.3 信息熵与密钥敏感性

8位灰度图的理想信息熵是8,表示所有256个灰度级等概率出现。明文图像熵值一般只有7到7.9,加密后的密文在幅度统计下应该非常接近8。

计算方式:

p = imhist(I) / numel(I); H = -sum(p(p>0) .* log2(p(p>0)));

我常用 save 或 sprintf 做一个表格,把明文和密文的熵、相关系数放在一起对比。这两项一摆,算法强度的基本盘就立住了。

密钥敏感性的测试更有意思:用原始密钥解密成功后,把密钥字符串随便改一个字符,哪怕只是末尾数字加一,解密结果就完全变成噪声。这说明密钥的每一位都参与到了相位掩码和仿射参数生成中,不存在“近似密钥可解密”的风险。我还会再做一个量化测试:改一个字符后,计算解密图像与原图的PSNR和SSIM。实测PSNR直接降到10dB以下、SSIM接近0,和随机图没有区别。

5.4 抗噪声和抗裁剪能力测试

这一项虽然不属于硬性评价指标,但实用价值很高。双随机相位属于“全域加密”,信息含量被铺开到整个密文平面,因此对局部裁剪有一定鲁棒性。我做过两个测试:

  • 给密文加上高斯噪声(均值为0,方差0.01),再解密。噪声被逆变换摊平,恢复图有轻微颗粒感,但主体轮廓仍然可辨;
  • 把密文中间约四分之一区域置零,再解密,损失的是频谱低中频信息的大部分,图像会变模糊,但大体内容还在。

测试结果为我的结论提供了直接证据:这套算法在密文传输中信噪比下降或部分数据丢失的情况下,仍然可以以“可视质量”恢复明文,这在很多需要容错的应用场景里很关键。

6. 调试过程中的几个大坑和实用建议

最后写几个我实际踩过的坑。这些坑都很隐蔽,但只要理解了背后的原因,处理起来也就几行代码的事。我特意把它们总结出来,是希望其他人能少走弯路。

6.1 复数密文的存储:想存成图片?先搞清楚实部和虚部

我最初图省事,加密完直接把C矩阵用 imwrite 存成PNG。结果Matlab提示“imaginary parts discarded”,存下来的图片只有实部,解密端恢复出来一团模糊。这是因为图像格式不支持复数。正确做法是存成 .mat 文件,或者分别保存实部、虚部为两份浮点数据。显然,如果这是一套要交付的Demo,我会建议用 .mat 保存密文矩阵,并把密钥字符串单独存储,两者同时具备才能解密。

6.2 fftshift 位置不一致导致的“假破解失败”

我在调试中曾经为了显示漂亮的频谱图,在加密流程里加了一次 fftshift,解密时却忘了把数据做 ifftshift,结果无论密钥多正确,恢复图都是错开的四块。后来我把约定写死:fft2 / ifft2 只管计算,绝不掺入 fftshift;频域图要中心化显示时,另建一个变量单独处理。这样就把“计算流程”和“可视化流程”彻底隔离,再没出过类似问题。

6.3 别让随机数种子变成“隐性风险”

初版代码里我是直接把 rand 出的相位矩阵作为密钥的,加密后把 psi1、psi2 一并存进 .mat。这样做在Demo里可行,但防止有人不小心把密文和相位一起发给别人,如果在传输过程中密文泄露,相位也泄露,整个加密就形同虚设。现在我就用一段固定的字符串作为主密钥,所有伪随机序列从这条字符串派生出来。哪怕是同一张图片,只要密钥字符串不同,密文就完全不同。这才是科学的密钥管理方式。

6.4 参数选型与后续扩展思路

从我的使用经验来看,默认参数选择的关键是:仿射参数不要让图像置乱后产生明显的块状纹理;旋转角建议取 20° 到 90° 的任意值,错切系数建议控制在一个比较小的范围,避免坐标变化太大导致排序结果偏向某一方向。相位掩码用 rand 生成即可,因为两个相位本身已经是均匀随机,不需要额外密码学随机源也能达到足够强度。

参数几何意义建议范围
θ旋转角度(弧度制)0.2 ~ 2.5
sx, sy缩放因子0.5 ~ 2.0
hx, hy错切系数-0.3 ~ 0.3
tx, ty平移量(像素)-50 ~ 50

后续如果要增强安全性,可以做的扩展方向很多。比如用混沌序列替代 rand 来生成相位掩码,让密钥对初值更敏感;或者把仿射参数本身也放入混沌轨迹中,让每个像素块的变换参数都不相同。这些我都试过,效果差异不大但分析复杂度高了不少。如果只是做入门级算法Demo或课程设计,本文的这套架构已经足够,先把指标跑出来,再考虑进阶方向。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询