做图像加密课题的人,十有八九会在某个晚上对着“双随机相位结构结合菲涅尔变换图像加密”这种标题发呆:到底是怎么个双随机法?菲涅尔变换明明是光学衍射里的东西,为什么会跑到加密领域来?我第一次拿到这个题目的时候也是一脸懵。后来自己动手把Matlab代码跑通,又把每个参数翻来覆去地改,才把整条链路彻底捋明白。
这篇就从原理讲到代码,再讲到我实际调试中踩过的坑。不管你是课程设计、毕业课题,还是单纯对光学信息安全感兴趣,只要想搞懂“双随机相位+菲涅尔变换”这套图像加密方案,跟着走一遍基本就够了。网上很多代码站会把这类仿真打包成“4324期”这种编号发布,打开一看其实核心就两个函数加一个主脚本,结构不复杂,但真正麻烦的是跑通之后改参数、做分析、写报告这些事。这篇权当一份参考答案。
1. 双随机相位编码的底层逻辑
1.1 一个随机相位掩模做不到的事
先说第一个随机相位掩模(RPM1)到底做了什么。假设你有一张灰度图像,数值上可以看成一个实值矩阵。给它乘上一个复相位因子 exp(i·2π·rand(M,N)),其中 rand 生成 0 到 1 之间的均匀随机数,这样每个像素的相位都被改成了随机值,但振幅完全不变。
这里有个非常容易误解的点:乘上相位掩模之后,在空域直接看强度图,图像基本还是原来的样子。因为强度等于振幅平方,相位变化不改变振幅。很多人刚接触DRPE时会困惑“这不还是能看见原图吗?哪来的加密?”答案是:加密效果不是在这一步产生的,而是在衍射传播之后才显现出来的。
相位被打乱之后,光场在自由空间中传播,不同位置的子波相互干涉,原本有序的空间结构被彻底破坏,观察面上得到的强度分布才会变成类似白噪声的图案。这就是为什么DRPE至少需要一次衍射过程,而不是简单地把图像和随机相位相乘就完事。
1.2 第二个随机相位掩模让信息彻底“溶解”
如果只有一个随机相位掩模一次衍射,严格来说系统还不够安全。原因在于:图像的空域信息和相位扰动之间存在某种可预测的关系,如果攻击者知道系统参数,单掩模方案是可以被反推的。
所以经典的双随机相位编码(DRPE)在第一次衍射之后,在变换域(菲涅尔域)再乘一个独立的随机相位掩模(RPM2),然后再做一次衍射得到密文。这相当于对信号做了两次独立的随机调制,中间还穿插了两次空间扩散。第一块掩模把图像的相位信息打乱,第二块掩模把衍射场的复振幅再次打乱。两道关卡叠加之后,输出面上的强度分布和原始图像之间几乎找不到线性痕迹。
用生活化的比喻来说:图像是一封写好的信,第一块随机相位是搅碎纸张的碎纸机,第一次衍射是把碎片撒到空中,第二块随机相位是再给每个碎片喷上乱码涂料,第二次衍射则是让这些碎片飘得到处都是。想恢复原信,必须知道碎纸机、涂料配方、飘散距离,一个都不能错。
1.3 为什么是菲涅尔变换而不是傅里叶变换
很自然会有一个疑问:教科书里的DRPE经典框架用的是4f系统,也就是两次傅里叶变换,为什么这个题目偏要用菲涅尔变换?
关键在于菲涅尔衍射多出了两个自由密钥维度:衍射距离和波长。
4f系统里,透镜的焦平面正好是输入面的频谱面,系统结构固定后,变换关系是确定的,你只能在两个随机相位掩模上做文章。菲涅尔变换不一样,它是光在自由空间中传播的自然模型,衍射距离 z 是可以任意取的,波长 λ 也是可以变的。加密方把 z1、z2、λ 作为额外密钥,解密方如果不知道这些参数,即便拿到了两个随机相位掩模,也无法正确重建图像。
另外从物理实现角度看,菲涅尔域DRPE不需要严格的透镜共轭关系,光路结构更简单,装配误差的容错性也好一些。在Matlab数值仿真中,这种差异体现在:菲涅尔变换只需要用FFT实现衍射计算,不需要构造严格满足傅里叶变换关系的共轭掩模对,随机相位掩模R1、R2可以独立生成,自由度更高。
2. 菲涅尔离散化:两种数值实现与参数选择
2.1 单次FFT和角谱法的本质区别
Matlab里模拟菲涅尔衍射,主流有两种写法:单次FFT法(S-FFT)和角谱法(也叫传递函数法TF)。
单次FFT法是从菲涅尔衍射积分的二次相位近似出发,把衍射过程化简成一次傅里叶变换。它的特点是:输出面的采样间隔会随距离变化,公式为 Δx2 = λ·z / (M·Δx1)。也就是说,传播距离越大,输出图像的物理尺寸越大,像素间隔变得越粗。
角谱法则是用频域传递函数 H(fx,fy) = exp(i·k·z) · exp(-i·π·λ·z·(fx²+fy²)) 直接作用于输入频谱,再做逆FFT得到输出。它需要两次FFT,但最大优势是输出阵列尺寸和采样间隔始终保持不变。
如果你只是模拟一次衍射,两种方法都行。但在加密解密这种往返传播场景里,使用角谱法最省心:解密时直接传入 -z,输出尺寸和加密端完全对齐,不会出现坐标错位。单次FFT由于输出尺度随距离改变,加解密之间还要做插值对齐,非常麻烦。所以我这边的经验是:DRPE仿真一律用角谱法做核心传播函数。
| 维度 | 单次FFT(S-FFT) | 角谱法(TF) |
|---|---|---|
| FFT次数 | 1次 | 2次 |
| 输出采样间隔 | 随z变化 | 不变 |
| 输出面尺寸 | 随z变化 | 不变 |
| 加密解密往返 | 需坐标对齐,麻烦 | 天然对齐 |
| 适用场景 | 单次衍射演示 | DRPE加解密、需要逆向传播的场景 |
2.2 采样条件:算不好解密必崩
角谱法听起来无脑好用,但它有一个非常关键的采样限制,很多人第一次跑出来噪声图就是栽在这里。
频域传递函数 exp(-i·π·λ·z·(fx²+fy²)) 是一个相位快速振荡的函数。离散网格能正确表示一个信号,需要满足奈奎斯特条件:相邻频域采样点之间,传递函数的相位变化不能超过 π,保守一点应该小于 π/2。
频域采样间隔是 Δf = 1/(M·Δx),频域最大频率是 f_max = 1/(2Δx)。我们可以估算最大相位变化率在频谱边缘处最激烈,具体推导不展开,最后会得到一个很实用的判据:
z ≤ M·Δx² / λ,保守取 z ≤ M·Δx² / (4λ)
打个比方。图像尺寸 M = 256,像素间隔 Δx = 15.6μm(常见空间光调制器的像素大小),波长 λ = 632.8nm。M·Δx² / λ = 256 × (15.6e-6)² / 632.8e-9 ≈ 0.096m。也就是说在保守估计下,衍射距离最好不要超过 9.6cm。如果硬要跑更大的距离,解密结果会因传递函数欠采样而严重失真。
这个公式做课程设计或实验参数预判非常有用,建议直接抄走。另一种缓解欠采样的思路是增大图像分辨率M或者增大像素尺寸Δx,但都不如直接把距离控制在安全范围内来得干脆。
2.3 两个核心Matlab函数
有了上面的理论基础,函数代码其实很短。先写角谱法主函数:
function Uout = fresnelTF(Uin, lambda, z, L) % fresnelTF - 角谱法菲涅尔衍射 % 输入: % Uin - 输入复振幅矩阵 % lambda- 波长(与L单位一致) % z - 衍射距离,可正可负,负值表示逆向传播 % L - 输入平面物理宽度 % 输出: % Uout - 衍射后的复振幅 [M, N] = size(Uin); if M ~= N error('建议使用正方形输入,方便频域网格对齐'); end dx = L / M; k = 2 * pi / lambda; fx = (-N/2 : N/2-1) / L; fy = (-M/2 : M/2-1) / L; [FX, FY] = meshgrid(fx, fy); H = exp(1i * k * z) .* exp(-1i * pi * lambda * z * (FX.^2 + FY.^2)); H = ifftshift(H); % 频域对齐,关键步骤 Uout = ifft2(fft2(Uin) .* H); end这里ifftshift(H)是经典陷阱。fft2输出的零频分量在矩阵左上角,而我们定义的FX/FY频域网格是从负频率到正频率排列,零频在矩阵中心。如果不把传递函数H用ifftshift移到左上角,频域相乘时坐标就对不齐,解密结果会是一团乱麻。
再给一个单次FFT版本作为对比:
function [Uout, x2, y2] = fresnelSFFT(Uin, lambda, z, L1) % fresnelSFFT - 单次FFT菲涅尔衍射 % 注意:输出面采样间隔会随z变化 [M, N] = size(Uin); if M ~= N error('建议使用正方形输入'); end dx1 = L1 / M; k = 2 * pi / lambda; L2 = lambda * z * M / L1; dx2 = L2 / M; x1 = (-M/2 : M/2-1) * dx1; y1 = (-M/2 : M/2-1) * dx1; [X1, Y1] = meshgrid(x1, y1); x2 = (-M/2 : M/2-1) * dx2; y2 = (-M/2 : M/2-1) * dx2; [X2, Y2] = meshgrid(x2, y2); Uout = exp(1i * k * z) / (1i * lambda * z) ... .* exp(1i * k/(2*z) * (X2.^2 + Y2.^2)) ... .* fftshift(fft2(Uin .* exp(1i * k/(2*z) * (X1.^2 + Y1.^2)))) * dx1^2; end注意fresnelSFFT里我用fftshift(fft2(...))来对齐输出坐标。两个函数都假设正方形输入,实际使用前把图像imresize成 M×M 即可。
3. 加密与解密全流程拆解
3.1 加密端三步走
加密流程看起来只有几个步骤,但每一步都有物理意义。完整链路如下:
第一步:输入图像 f(x,y) 与第一块随机相位掩模 R1 相乘,得到调制后的复振幅 U1 = f · R1。这一步的作用是把图像的振幅信息“托付”给一个复振幅场,相位已经被随机化。
第二步:U1 经过距离 z1 的菲涅尔衍射,得到 U2。这个衍射过程把空域信息扩散到整个观察平面,相当于第一次打散。
第三步:U2 与第二块随机相位掩模 R2 相乘,得到 U3 = U2 · R2,再经过距离 z2 的二次衍射,得到最终密文 cipher。
这里有两个细节要注意。第一,两块随机相位掩模必须是独立生成的,不能由同一个随机种子产生,否则相关性会被利用。第二,加密结果是一个复数矩阵,不是普通图像。要完整保存密文,最好直接存.mat文件,或者把强度和相位分别存成两个浮点矩阵。如果硬要转成uint8图片,相位信息就丢了,解密必然全毁。
3.2 解密端的关键细节
解密是加密的逆过程,但初学者经常在三个地方翻车:方向、共轭、取模。
方向:解密时第一次传播必须用 -z2,也就是fresnelTF(cipher, lambda, -z2, L),这是光的逆向传播。角谱法的好处在这里体现得淋漓尽致,直接传负距离即可,复用同一个函数。
共轭:第一块掩模、第二块掩模在解密时都要换成复共轭,即conj(R1)、conj(R2)。因为加密时乘的是 exp(i·φ),解密时要乘 exp(-i·φ) 才能把相位扰动抵消。
取模:原始图像是实值强度图,所以解密到最后一步用abs()取振幅即可。如果原始图像本身是复振幅,那就不能取模,需要记录完整复数场。这里以最常见的灰度图像为例,取abs()是正确操作。
3.3 完整可运行脚本
把上面所有东西串起来,一份完整的Matlab脚本长这样:
clear; clc; close all; %% 参数设置 lambda = 632.8e-9; % 波长 632.8nm L = 4e-3; % 平面宽度 4mm M = 256; % 图像像素数 z1 = 0.02; % 第一次衍射距离 2cm z2 = 0.02; % 第二次衍射距离 2cm %% 读取并预处理图像 img = imread('cameraman.tif'); img = im2double(imresize(img, [M, M])); %% 生成两个随机相位掩模 rng('shuffle'); % 每次运行都不同 R1 = exp(1i * 2 * pi * rand(M, M)); R2 = exp(1i * 2 * pi * rand(M, M)); %% 加密 U1 = img .* R1; % 第一块随机相位调制 U2 = fresnelTF(U1, lambda, z1, L); % 第一次菲涅尔衍射 U3 = U2 .* R2; % 第二块随机相位调制 cipher = fresnelTF(U3, lambda, z2, L); % 第二次衍射得到密文 %% 解密 V1 = fresnelTF(cipher, lambda, -z2, L); % 反向传播 V2 = V1 .* conj(R2); % 抵消第二块掩模 V3 = fresnelTF(V2, lambda, -z1, L); % 反向传播 decrypted = abs(V3 .* conj(R1)); % 抵消第一块掩模并取模 %% 显示 figure; subplot(1,3,1); imshow(img, []); title('原始图像'); subplot(1,3,2); imshow(abs(cipher).^2, []); title('加密结果(强度)'); subplot(1,3,3); imshow(decrypted, []); title('解密结果');跑完之后你会看到:原始图像清晰可见,加密结果是一幅完全看不出内容的噪声图,解密结果和原图基本一致。加密结果我用的是强度图abs(cipher).^2,因为直接显示复振幅的实部或虚部意义不大,而强度是探测器实际能记录的量,但要注意:仿真中的密文是复数矩阵,含有相位信息,模拟的是“全息记录”的理想情况,不是强度探测。
4. 密钥敏感性与安全性分析
4.1 相位掩模错了会怎样
DRPE的密钥安全性主要来自两块随机相位掩模的随机性。理论上每个像素的相位有无限多种取值可能,两块掩模联合起来的密钥空间极大,暴力搜索完全不现实。
我在实验中做过最直观的测试:把解密端的R1换成一个错误掩模(重新生成一个随机矩阵),其余参数全部保持不变。解密结果立刻从清晰图像变成完全无规律的噪声。用相关系数corr2(img, decrypted)来量化,正确解密时相关系数接近1,错误掩模解密时相关系数通常不到0.05。
这就是随机相位掩模的“雪崩效应”:相位掩模哪怕是差一个像素、差一个随机种子,整个解密空间都会被彻底改变。所以实际工程中,务必把种子固定下来或者直接保存掩模矩阵,而不是每次重新生成。
4.2 距离和波长也是密钥
菲涅尔域DRPE相比传统4f系统的一个显著优势,就是距离参数参与了加密。我把 z1 从 20mm 改成 19.9mm,解密结果肉眼已经看不清内容;偏差超过 0.5mm 时,解密图基本就变成噪声了。这个灵敏度和系统参数有关,但总体趋势非常明显。
波长也是类似。把 λ 从 632.8nm 改成 633.8nm,解密效果同样大打折扣。在实际光学系统中,波长可以精确调控,距离可以精确测量,这两者作为额外密钥几乎不增加成本,却让攻击者多两个需要猜测的维度。
我个人的建议是:在做性能分析时,把“距离偏差-解密质量”曲线画出来,通常是一根急剧下降的曲线,这既是加密系统安全性的直观证据,也是答辩或报告里很加分的一张图。
4.3 这套方案的真实短板与加固思路
DRPE虽然经典,但安全性不是无懈可击的。最重要的问题是:它本质上是线性系统。整个加密过程可以表示成矩阵乘法,输入与输出之间存在线性映射关系。如果攻击者能拿到一组或多组明文-密文对,理论上可以估计出系统传输矩阵,从而在其他密文上做近似逆运算。对安全性要求极高的场景,纯DRPE是不够的。
常见的加固方向有三个:
- 在DRPE之前先做像素置乱,比如混沌映射置乱、Arnold变换,把明文的空间相关性提前打散,破坏线性估计的前提。
- 把明文相关的信息注入相位掩模生成过程,让掩模和具体图像绑定,避免通用密钥被一次性破解。
- 在光学系统中放弃直接记录复振幅,改为只记录强度,解密时使用相位恢复算法。这样加密系统从线性变成非线性,抗已知明文攻击的能力显著增强,但代价是解密复杂度大幅上升。
5. 实测问题与经验速查
5.1 解密全噪声的六个检查点
我在调试过程中遇到过各种花式翻车,最常见的表现就是解密结果一片噪声。如果你也遇到这种情况,按顺序检查下面六个地方:
第一,检查解密时是否用了负距离。正向传播是加z,逆向传播一定是减z,也就是传入 -z2 和 -z1。方向错了,一切白搭。
第二,检查相位掩模是否取了共轭。解密端要用conj(R1)和conj(R2),不是原封不动再用一次R1、R2。
第三,检查密文的存储和读取方式。如果中间保存成jpg或uint8,相位信息已经永久丢失,Matlab再努力也救不回来。仿真演示请直接用.mat保存复数矩阵。
第四,检查显示前是否取了abs。解密得到的复振幅可能包含一个全局相位因子,直接imshow(real(decrypted))或imshow(decrypted, [])会看到斑驳的条纹甚至负数区域,取abs()才是正确的恢复结果。
第五,检查传递函数是否做了ifftshift。这是最隐蔽的坑,代码看着没问题,但频域坐标没对齐,解密出来一定是乱的。建议先用 z=0 自检:fresnelTF(U, lambda, 0, L)应该返回U本身。如果这一步都不成立,说明传递函数实现有bug。
第六,检查衍射距离是否超出奈奎斯特限制。前面给的公式,如果 z 太大,传递函数欠采样,加解密往返就会引入误差。这个情况的表现通常是:解密图像不是完全噪声,而是模糊、重影、边缘有异常纹路。
5.2 边缘振铃与有限孔径问题
角谱法本质上是带限运算,因为离散网格只能表示有限带宽。加上FFT隐含的周期性,如果图像内容靠近边缘,衍射计算会引入边缘振铃,解密结果边缘会出现亮的边框或条纹。
缓解办法很实用:加密前给图像做边缘padding,比如padarray(img, [M/2, M/2], 'symmetric'),让有效信息远离边界,完成整个加解密后再裁掉padding区域。我实测下来,对称填充的效果比零填充更好,因为零填充会在边界引入突变的振幅跳变,反而制造额外的高频分量。
5.3 参数调优建议
做实验时,参数不要随便拍脑袋。下面这套组合是我实测比较稳的起点:
lambda = 632.8e-9; L = 4e-3; M = 256; z1 = 0.02; z2 = 0.02;在Matlab里,z1=z2=0.02m用上面的参数,采样条件安全,加密结果噪声特性好,解密重建质量高。如果是演示需要加密效果更明显,可以稍增大距离但注意别越过奈奎斯特边界。图像分辨率方面,M不要低于128,否则随机相位掩模的熵不够高,加密变得“不够乱”;M取512时效果更好,但内存占用和速度会差一些,主流机器跑起来没问题。
5.4 踩坑速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 解密全噪声 | 掩模未共轭/距离方向错/频域未对齐 | 按5.1逐项排查 |
| 解密图有清晰轮廓但偏暗 | 显示时用了实部而不是模值 | 改用 abs() 显示 |
| 边缘出现亮框或条纹 | FFT边界效应 | 加padarray对称填充 |
| 加密结果隐约能看到原图轮廓 | 衍射距离太短,扩散不充分 | 增大z1、z2 |
| 解密图模糊重影 | 距离超出采样条件 | 减小z或增大M |
| 彩色图加密后颜色异常 | 没做灰度转换 | 先rgb2gray再加密 |
| 保存再读解密失败 | 转成了uint8或jpg | 改用.mat保存复数矩阵 |
这最后一列的解决方案,基本就是我在项目里反复用过、确认有效的办法。每一次看到解密失败,不要急着怀疑算法,先对照这张表做减法,通常能省下大量时间。
最后分享一个容易被忽略但很出效果的小技巧:展示加密结果时,直接imshow(abs(cipher).^2, [])可能因为动态范围太大而显得很暗。你可以用imshow(mat2gray(log(1 + abs(cipher).^2)))做一次对数变换,把噪声图案的细节拉出来,演示效果会好很多。另外,做密钥敏感性分析时,建议把错误距离、错误掩模、错误波长三组解密结果并排放在一张图里对比,视觉冲击力远比一长串数据强。这套方案虽然不是什么新东西,但作为光学信息安全的大门入口,依然值得亲手实现一遍——尤其当你自己把反向传播、共轭、坐标对齐这三件事全部捋顺之后,再回头看任何DRPE变体方案,都会觉得通透得多。