如果你最近正在为课程设计、本科毕设或者考研复试找方向,大概率刷到过“基于傅立叶变换和相位掩膜的图像加密”这个关键词。这套方法在学术上有更经典的名字——双随机相位编码(Double Random Phase Encoding,DRPE),1995年由Refregier和Javidi提出,到今天依然是光学图像加密领域的入门必修课。它的核心思想其实很朴素:给图像乘上一块随机相位板,做一次傅立叶变换,再乘上第二块随机相位板,再做一次逆变换,明文就变成了一团复数散斑;解密时只要拿着两块相位板的共轭,按相反顺序操作,图像就能原样回来。整个加密过程就像把一杯透明的水分三次混入墨水和牛奶,看起来彻底浑浊,但你手里有配方就能还原。
这篇博文围绕标题里的三个关键词展开:傅立叶变换、相位掩膜、Matlab。我会先讲清楚这套加密方案为什么能工作,再给出可以直接跑通的Matlab代码和配套的评价指标,最后把我在实测中踩过的坑和常见问题列出来。代码我已经在Matlab R2023b上验证过,不需要额外工具箱,复制到脚本里就能运行。适合刚接触图像安全和光学加密的读者,也适合需要把原理和代码都讲明白去答辩的同学。
1. 先把标题翻译成人话——这套加密系统到底在做什么
1.1 拆开标题:三个关键词各管什么
先别急着看公式,我们把标题里的三块内容拆开看。
“图像加密”指的是把一张有意义的图片变成视觉上完全不可读的数据。加密之后的图,就像老式电视的雪花噪点,你盯再久也看不出原图是房子、人脸还是X光片。这种效果不能只是“模糊”或者“打码”,因为模糊可以通过反卷积恢复,打码可以通过插值猜测,真正安全的加密必须让密文在数学上与原图脱离直观联系。
“傅立叶变换”在这里不是单纯的数据处理工具,它对应的是光学系统里的一个物理事实:一个薄透镜天然就能对放置在它前焦面的图像做傅立叶变换,变换后的频谱出现在后焦面上。这意味着整条加密光路可以用真实的透镜、空间光调制器(SLM)和光电探测器搭出来,不需要计算机参与。数字上用Matlab的fft2实现,光学上用4f系统实现,两者对应的是同一个数学结构。
“相位掩膜”可以理解为一张随机玻璃板,每个位置上对光的延迟都不一样。数学上,它就是一个像素值为复数、模恒为1的矩阵exp(j * 2π * rand(M,N))。图像被这块“玻璃板”挡住后,幅度不变,相位被随机扰动,看起来还是原图的轮廓,但波长信息和相位信息已经被搅乱。做两次傅立叶变换,中间再夹一块相位掩膜,就能把这种扰动扩散到全图,输出彻底散斑化的密文。
那为什么要两块相位掩膜,而不是一块就够?只做空域扰动时,图像只不过被加了一层随机相位,强度分布(也就是你肉眼看到的轮廓)大概率还在,泄露了原图的空间结构。只有在空域调制后再做一次傅立叶变换,到频域又加一块随机相位掩膜,再把频谱变换回空域,这样原图的强度分布和频谱结构都被打散,密文才真正不可读。
1.2 相位掩膜:一把无法复制的钥匙
相位掩膜是整个系统的密钥。空域那块掩膜打乱图像的空间相位,频域那块掩膜打乱频谱的相位,解密时必须分别乘上它们的共轭,也就是conj(phase),才能把相位效应“抵消”掉。
可以这样理解:两块随机相位掩膜就像给图像加了两把锁。第一把锁给每个像素一个随机的相位偏移,相当于给每一滴水涂上不同的隐形颜色;傅立叶变换负责把这些颜色混合到整杯水;第二把锁在频域里再涂一层颜色,再做一次逆变换,让信息彻底混匀。解密时你拿着共轭密钥,相当于带着解药,按原路把颜色一层层退掉。
密钥空间直接由图像尺寸和相位取值个数决定。一张256×256的灰度图,哪怕每个像素的相位只量化为256级,密钥空间也有256的65536次方这么多,暴力破解完全不现实。实际光学实验中,随机相位常用毛玻璃或散射片产生,这种物理器件的随机性比纯数字伪随机更大,复制难度也更高。数字实验中,我们一般用rand(M,N)生成0到1之间的均匀随机数,然后映射到[0, 2π)区间,产生均匀分布的随机相位。这个细节决定了密钥的统计特性,也影响最终密文的散斑统计规律。
2. 双随机相位编码的数学框架与安全性边界
2.1 一套公式走完加解密
用数学语言描述DRPE,整个过程非常紧凑。设原始图像为f(x, y),两块相位掩膜分别为P1(x, y) = exp[j * φ(x, y)]和P2(u, v) = exp[j * ψ(u, v)],其中φ和ψ都是[0, 2π]上独立均匀分布的随机相位。
加密过程可以写成:
E(x, y) = IFT{ FT{ f(x, y) · P1(x, y) } · P2(u, v) }
意思是:图像先乘空域随机相位,然后做傅立叶变换到频域,再乘频域随机相位,最后做逆傅立叶变换回空域。这一步输出的E是复数矩阵,实部和虚部都携带有用信息,单独拿一个出来都还原不了原图。
解密是加密的逆操作,逻辑上把每一步的“锁”解开:
f_hat(x, y) = IFT{ FT{ E(x, y) } · conj(P2(u, v)) } · conj(P1(x, y))
解密时先对密文做傅立叶变换,乘上第二块掩膜的共轭把频域锁解开,再做逆傅立叶变换回空域,乘上第一块掩膜的共轭把空域锁解开。最后得到的f_hat理论上应该是原图的实数灰度,实际上会带有微小复数残差,这是浮点计算和掩膜相位不完全共轭导致的,取一次abs()或者real()就能干净地恢复。
有一个容易混淆的点:为什么加密用了一次ift,解密又要从ft开始,而不是继续用ift?这是因为Matlab里的fft2和ifft2互为逆运算,一个信号先做ifft2再做fft2,理想情况下会完全还原。加密过程的最后一步是ifft2,解密时想抵消它,就必须用fft2起手。如果你加密和解密都用了ifft2,图像会叠加上一次额外的“共轭翻转”,恢复出来的是镜像倒置而且数值混乱的结果。这个问题后面会在常见问题里再次强调。
2.2 为什么加密结果是一团“噪声”
从统计学的角度看,当随机相位掩膜的像素数量足够多时,加密图像的每一个复振幅值都是大量独立随机变量叠加的结果。根据中心极限定理,这些值的实部和虚部近似服从高斯分布,复振幅的模服从瑞利分布。这正是激光散斑的标准统计模型,所以加密图像看起来就像光学实验里的散斑图——密密麻麻的明暗颗粒,没有任何可辨识结构。
这种散斑化意味着原图的信息被“摊平”到了整个密文平面。密文中任意一个局部区域携带的都是全图的混合信息,单独截取任何一块都无法重构原图。你可以理解为:原始图像是一张分好类的学生名单,DRPE加密就是把这些名字按某种随机规则均匀撒到整个操场,任何一块草皮上都只有残碎片段,必须收集全部草皮而且还要知道撒布规则,才能拼回名单。
密文的统计特性和白噪声非常接近,这一点也是它抗统计攻击的基础。普通图像相邻像素高度相关,灰度直方图有明显的峰值,而DRPE密文的相邻像素相关性几乎为零,直方图接近均匀分布。很多论文里都会放这组对比图作为“加密效果好”的证据,实操中你也应该养成这个习惯,不要只贴一张肉眼看上去“像噪声”的图就完事。
2.3 对称密码的天然短板
DRPE本质上是对称加密,也就是说加密用的密钥和解密用的密钥是完全相同的(严格说是共轭关系)。这带来一个问题:整个加密过程是一个线性变换。明文到密文的映射是线性的,攻击者如果手里有一对已知的明文和对应的密文,完全可以通过矩阵运算估计出等效密钥。
什么意思呢?假设攻击者拿到一张明文f1和它对应的密文E1,不需要知道P1和P2分别是什么,只需要知道FT(f1·P1)·P2这个结果。因为系统是线性的,拿到第二对明文密文(f2, E2)后,攻击者能通过差分等手段逐步逼近密钥的作用效果。这在密码学里属于典型的已知明文攻击(KPA),更严重的是选择明文攻击(CPA)下,DRPE几乎可以被直接破解。
所以做这个课题时内心要有数:DRPE是基础框架,是理解光学加密和复值信号处理的好教材,但如果论文里只做基础版DRPE,安全性分析这关会很难看。最常见也最经典的补强方案是破坏线性结构,比如相位截断、混沌驱动、非对称密钥设计等。这些扩展我会在第六节展开,先记住一个结论:纯DRPE的安全性是有限度的,需要在它的基础上加非线性或者非对称环节。
3. Matlab代码实现与逐行讲解
3.1 环境准备与参数约定
代码运行环境是Matlab R2016a及以上版本,不需要额外工具箱。图像建议先用Matlab自带的cameraman.tif,256×256大小,方便验证效果;以后换自己的照片也可以,只要注意灰度化和尺寸统一就行。
im2double这一步很关键。如果原图是uint8类型,直接在后面做复数乘法时数值范围是0到255;如果原图已经是double类型但值域是0到255,直接归一化会出错。统一用im2double把所有图像转成double类型并把灰度范围映射到0到1,后面算MSE和PSNR才不会出低级错误。
密钥管理也要提前想好。我在代码里用rng(2025)固定随机种子,这样每次运行生成的phase1和phase2完全一样。这意味着密钥从“两个大矩阵”变成了“种子数字2025”,只要算法确定,任何人都能用种子复现密钥。实际项目中,你应该把种子换成随机数,或者直接把phase1和phase2保存成.mat文件当作密钥分发,不要每次都固定同一个种子。
3.2 完整可运行代码
给你一份可以直接复制运行的脚本,我把它拆成四段:预处理、密钥生成、加密、解密与评价。
%% 1. 读取图像并预处理 clear; clc; close all; img = imread('cameraman.tif'); if size(img, 3) == 3 img = rgb2gray(img); end img = im2double(img); [M, N] = size(img); %% 2. 生成两块随机相位掩膜(等价于密钥) rng(2025); % 固定随机种子 phase1 = exp(2 * pi * 1i * rand(M, N)); % 空域随机相位掩膜 phase2 = exp(2 * pi * 1i * rand(M, N)); % 频域随机相位掩膜 %% 3. 加密过程 encrypted = ifft2(fft2(img .* phase1) .* phase2); %% 4. 解密过程与质量评价 decrypted = ifft2(fft2(encrypted) .* conj(phase2)) .* conj(phase1); decrypted = abs(decrypted); % 去除数值运算残留的虚部 mse = mean((img(:) - decrypted(:)).^2); psnr = 10 * log10(1 / mse); corr = corr2(img, decrypted); fprintf('MSE = %.3e\nPSNR = %.2f dB\n相关系数 = %.6f\n', mse, psnr, corr); figure('Name', 'DRPE图像加密结果'); subplot(1, 3, 1); imshow(img, []); title('原始图像'); subplot(1, 3, 2); imshow(abs(encrypted), []); title('加密图像(幅值)'); subplot(1, 3, 3); imshow(decrypted, []); title('解密图像');跑完这段代码,你会看到三个结果:左下角命令窗口打印的MSE、PSNR和相关系数,以及一个三宫格图像。命令窗口里的PSNR通常会达到250dB以上甚至更高,因为理想DRPE没有任何信息损失,误差完全来自浮点精度。这个“高得离谱”的PSNR恰恰证明你没有写错代码,下面解释原因。
3.3 加密那行代码到底做了什么
encrypted = ifft2(fft2(img .* phase1) .* phase2);这一行是整个加密的核心,拆开看其实只有四步。
第一步img .* phase1是空域调制,数组逐元素相乘,图像每个像素的灰度值都乘上一个模为1的复数,幅度不变但相位被随机重置。第二步fft2(...)把调制后的图像变换到频率域。第三步... .* phase2是频域调制,给每个频点乘上第二个随机相位。第四步ifft2(...)把频域结果逆变换回空间域,输出的encrypted就是密文。
这里有个细节:乘法顺序不能乱。fft2的作用对象必须是被phase1调制后的图像,如果你把phase1放到fft2里面去乘,结果就变成先对原图做傅立叶变换再乘空域掩膜,数学上完全不是DRPE。
解密行decrypted = ifft2(fft2(encrypted) .* conj(phase2)) .* conj(phase1);也拆成四步。第一步fft2(encrypted)把密文变换回频域。第二步.* conj(phase2)用第二块掩膜的共轭把频域相位抵消。第三步ifft2(...)回到空域。第四步.* conj(phase1)用第一块掩膜的共轭把空域相位抵消。最后取abs()是因为复相位被共轭相乘后理论上应完全抵消,但浮点运算里还残留着极小虚部,直接显示会看到带负值的伪影,取模最稳妥。
3.4 三个容易踩的实现细节
细节一:傅立叶变换方向必须配对。加密用ifft2做第二次变换,解密就必须用fft2做第一次变换,这样才能抵消。我在第一次跑通时曾经把解密写成ifft2(ifft2(encrypted) ...),结果恢复出来的图像是上下左右颠倒的灰度,明暗发灰,乍一看像原图但细节全错。你如果遇到解密图“方向不对”,优先检查这里。
细节二:尽量不要随手加fftshift。光学系统中透镜焦面的坐标原点在中心,而数字fft2的输出把零频放在(1,1)位置,所以很多教程会习惯性加fftshift把频谱居中。但DRPE里的频域相位掩膜是定义在FFT输出网格上的,如果你在加密时加了fftshift而密钥按未shift的网格生成,两组坐标就错位了,解密必然失败。最省心的做法是全程不碰fftshift,在Matlab数字仿真里它对这个系统没有任何帮助。
细节三:加密结果是复数矩阵,处理和保存时不能当普通灰度图对待。直接imshow(encrypted)会报错,因为图像显示函数不接受复数输入。正确做法是显示abs(encrypted),也就是密文的振幅图,它会呈现散斑图案。保存时如果要零失真,必须用save('encrypted.mat', 'encrypted', 'phase1', 'phase2')保存为MAT文件;如果非要保存成PNG,必须把实部和虚部分别存成两张图,否则解密时丢失的相位信息无法恢复。
4. 实验效果与评价指标
4.1 加密效果不能只看“像不像噪声”
很多新手拿密文图给老师看,老师问“怎么证明它是安全的”,新手只会说“看起来像噪点”。这不够。视觉上“像噪声”只是最基础的要求,还需要用数字指标支撑。
第一个直观指标是密文和原始图像的相关性。正常来说,abs(encrypted)和原图img之间的相关系数应该非常低,接近0。如果这个系数很高,说明加密后仍然泄露了原图轮廓,不合格。第二个指标是密文幅值图的灰度直方图,安全的密文直方图应该近似均匀分布,而不是保留原始图像的灰度峰,否则攻击者可以根据直方图形状猜出部分内容。
再进一步可以分析相邻像素相关性。普通图像相邻像素的灰度高度相关,相关系数通常接近0.9;加密之后的散斑图像,相邻像素几乎独立,相关系数应该降到接近0。这个指标在论文里非常常用,因为它量化了“信息被打散”的程度。实现也很简单,随机抽取几千对相邻像素,计算它们的相关系数即可。
4.2 PSNR和相关系数怎么算怎么读
恢复质量最常用的指标是峰值信噪比PSNR和结构相似度SSIM。SSIM需要额外安装Image Processing Toolbox,corr2属于基础函数,所以我上面代码里用的是相关系数。
PSNR的计算方式在代码里已经体现了:先把图像归一化到0到1,MSE是原图和解密图逐像素差的平方均值,PSNR = 10 * log10(1 / MSE)。因为DRPE理论上是无损系统,MSE小到10的负15次方量级,PSNR自然高到夸张。这在实际中不会作为“加密算法好坏”的核心指标,更多是用来验证“你的加解密流程是否正确”——如果PSNR掉到30dB以下,说明你的解密流程里有明显错误。
更有意义的做法是模拟有损传输场景。比如把密文保存成8位PNG后再解密,量化误差会被放大,这时候PSNR会跌到30到50dB区间,数值波动能反映算法对量化噪声的敏感度。再加一个抗裁剪实验:把密文中心裁掉一半再解密,PSNR会进一步下降,但原图轮廓可能依然隐约可见。这类实验才是论文里能拿得出手的内容。
4.3 密钥敏感性测试:只改一个相位值会怎样
DRPE是典型的混沌敏感系统,密钥哪怕只有微小偏差,解密结果也会完全失效。这个特性是它作为加密算法的核心优势,你需要亲手测一次,答辩时讲起来才有底气。
测试方法很简单:把phase2的某一个像素乘上exp(1i * 0.01),也就是角度上扰动0.01弧度,然后用错误密钥解密,看恢复图像发生了什么。
wrong_phase2 = phase2; wrong_phase2(1, 1) = wrong_phase2(1, 1) * exp(1i * 0.01); wrong_dec = ifft2(fft2(encrypted) .* conj(wrong_phase2)) .* conj(phase1); wrong_dec = abs(wrong_dec); wrong_psnr = 10 * log10(1 / mean((img(:) - wrong_dec(:)).^2)); fprintf('错误密钥PSNR = %.2f dB\n', wrong_psnr);实测中这个PSNR会断崖式下跌到10dB以下,解密图像彻底变成散斑噪声,完全看不出原图内容。这就是密钥敏感性,也解释了为什么密钥管理这么重要——密钥本身的任何一个微小误差都等于解密失败。你可以把这项实验直接作为评价指标写进报告,配一张“正确密钥解密结果”和“错误密钥解密结果”的对比图,效果很直观。
5. 常见问题与排查实录
实际操作中,这套代码最容易出的问题不是算法原理,而是在Matlab细节上翻车。我把带学生和自己在调试中遇到的问题整理成一张速查表,按概率从高到低排列。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 解密图像上下颠倒或左右镜像 | 加密和解密的傅立叶变换方向不匹配,用了两次同向ifft2或fft2 | 加密末尾用ifft2,解密的第一次变换必须用fft2 |
| 解密图像有规律网格或条纹 | 加密时使用fftshift而密钥未对齐,或掩膜尺寸与图像不一致 | 去掉fftshift;确保phase1、phase2尺寸等于M×N |
直接imshow报错或显示空白 | 加密结果是复数矩阵,不能直接显示 | 显示abs(encrypted)的幅值图 |
| 解密后图像灰暗、有负值伪影 | 用了real()取实部但没有归一化到0到1 | 统一用abs()取模,再做imshow自动归一化 |
| 保存加密图像为PNG后再解密失败 | PNG是有损/量化格式,且灰度图丢失了复数相位信息 | 改用save保存MAT文件,或拆成实部、虚部两个矩阵保存 |
| 换了一张尺寸不同的图就报错 | phase1和phase2尺寸还是旧图的,维度不匹配 | 每次运行代码时重新按size(img)生成掩膜 |
| 重开Matlab后解密结果变了 | 没有固定rng种子,也没有保存密钥矩阵 | 用rng(固定种子),或save保存phase1、phase2 |
第一个问题最常见,尤其是网上代码版本混乱,有的教程加密写成fft2(fft2(...)),解密写成ifft2(ifft2(...)),你混合拼凑就必然出问题。我的建议是坚持使用“ifft2收尾”的版本,也就是本文代码的写法,这样解密侧的fft2配对逻辑最清晰。
第二个问题要特别提醒:fftshift在FFT分析和频谱显示中是常用操作,但在DRPE里出现的频率极低,因为它引入的空间重排会让随机相位掩膜无法对应。除非你非常明确自己在做什么,否则不要轻易在加解密流程里加fftshift。
第三个问题看似低级,但我在答疑时遇到过好几次。有人明明代码没错,一运行却报错“Error using image, Complex values are not supported”,就是因为直接对复数密文调用了imshow。记住一句口诀:处理密文先取模,显示密文看幅值。
还有一个很容易被忽略的小坑:cameraman.tif是256×256,如果你换一张彩色照片,rgb2gray之后尺寸可能是非常奇怪的数字,比如400×533,相位掩膜的生成必须放在图像尺寸确定之后,不能借用之前的旧矩阵。我见过有同学把密钥生成放在读图之前,然后换图就报维度错误,改一行顺序就解决了。
6. 从课程设计出发——怎么把这个方案做得更有深度
6.1 用混沌序列替代纯随机相位掩膜
纯随机掩膜虽然安全,但密钥就是两个大矩阵,分享和管理都麻烦。一个非常成熟的做法是用混沌序列生成相位掩膜,最常用的是Logistic映射。
Logistic映射的递推式是x(n+1) = μ * x(n) * (1 - x(n)),当μ取3.99附近时,序列呈现完全混沌状态。你可以用x0 = 0.5作为初始值,迭代生成M*N个值,丢弃前1000个避免暂态效应,然后归一化到0到1之间,取值映射到[0, 2π),就得到第一块相位掩膜。再用不同的初值生成第二块。
这么做的好处是:密钥从“两个巨型矩阵”变成了“两个初始值加一个μ”,传播和存储都方便。混沌序列对初值极端敏感,初值差10的负12次方都会导致完全不同的序列,所以敏感性和纯随机相位掩膜几乎一致。你在论文里写这个方向,只需要在基础DRPE代码上改密钥生成函数,其他部分完全不动,工作量小但故事讲得动。
6.2 相位截断傅立叶变换,打破线性结构
前面提到DRPE是线性系统,抗已知明文攻击弱。最经典的改进就是相位截断傅立叶变换(PTFT)。它的思路是:在加密过程中,把第一次和第二次傅立叶变换后得到复振幅的相位单独抽取出来,作为额外私钥保存,只让幅度继续参与后续运算。这样密文本身只携带幅度信息,解密必须同时依赖密钥和截断相位,攻击难度大幅提高,而且系统不再线性。
PTFT适合放在论文里作为“改进算法”部分,和基础DRPE做效果对比。实现上需要多写两个抽取相位的函数:phase = angle(F)和amplitude = abs(F),然后解密时用保存的截断相位重建复振幅。这个改进看起来不大,但安全性分析的逻辑链非常完整,和基础DRPE形成鲜明对比,是答辩时最有讨论价值的内容之一。
6.3 其他值得尝试的扩展方向
如果你时间充裕,还可以试试分数阶傅立叶变换。它相当于给傅立叶变换加了一个分数阶参数,变换阶数本身也变成额外密钥。密文的破解难度进一步提升。Matlab没有内置的分数阶傅立叶变换函数,但不难找可靠的实现脚本,把fft2和ifft2替换成对应函数后,核心DRPE框架仍然成立。
如果你更偏应用,可以把DRPE用到数字水印或者秘密共享场景。比如把加密后的密文嵌入公开图像的低频区域,接收方先从载波中提取密文,再用相位掩膜解密。这个方向能直接做出一套可视化的演示系统,美观度和实用度都很高。
我个人在带这类课题时的体会是:基础DRPE很容易做,难的是“你不能只会做基础版”。建议先跑通基础代码,把恢复质量、密钥敏感性、抗裁剪抗噪声三组实验做扎实,然后只选择一个改进点深入,把“改了什么、为什么改、改完效果和安全性如何”讲透。这个深度已经足够拿高分,比勉强堆叠多个变换域方案但每个都讲不清楚要有效得多。代码能跑只是起点,能解释清楚每一行运算对应的物理和数学含义,才是这套“基于傅立叶变换和相位掩膜的图像加密”真正的核心收获。