说起来你可能不信,光学图像加密、随机相位加密、深度学习,这三个词放在一起,乍看像是实验室里搭满透镜和激光器才会碰到的高冷课题。但等我真把整套流程跑完之后,最大的感受是:它本质上是一门“用一个光学映射把图像藏起来,再让神经网络想办法把它找回来”的工程手艺,离日常代码并没有那么远。如果你正在做信息安全、图像隐私保护,或者准备拿光学加密方向做毕设/课程设计,下面这些内容应该能帮你省掉不少弯路。
这里说的“深度学习随机相位加密光学图像加密方法”,并不是要求你真的把4f光学平台搬进机房。它的核心思路是:用数值仿真模拟经典的双随机相位编码过程,把明文图像调制成一幅像噪声一样看不出内容的密文,再用深度神经网络替代传统解析逆系统,从密文里恢复明文。整个源码的核心就三块:随机相位掩码生成、加密仿真、解密网络。链路跑通后,你得到的密文是一团毫无结构的噪声图,而恢复出来的明文在视觉细节上依然能保持很高的相似度。
1. 这套方法到底在解决什么问题?
1.1 先看清经典双随机相位加密的全貌
传统光学图像加密的代表实现叫双随机相位编码,英文简称DRPE。它的思路很优雅:在4f系统的物面放一块随机相位掩码,让明文图像的相位被打乱;在频谱面再放第二块随机相位掩码,把频域信息也涂上随机相位。光经过两次调制之后,最终输出的是一幅平稳的、统计特性接近白噪声的复值密文。
稍微量化一点,加密过程可以写成:
c(x,y) = IFT{ FT{ I(x,y) * M1(x,y) } * M2(u,v) }
其中 I(x,y) 是明文,M1 和 M2 是两个独立生成的随机相位掩码,FT 和 IFT 表示光学傅里叶变换。解密时要用两块掩码的共轭,按相反顺序做两次傅里叶变换:
I_est(x,y) = IFT{ FT{ c(x,y) * M2*(u,v) } * M1*(x,y) }
这里的关键在于:只有精确知道 M1、M2,才能还原明文;否则恢复出来的结果就是噪声。因为每个相位掩码的每个像素都可以在 0 到 2π 之间连续取值,密钥空间的规模是天文数字。举个直观例子,64x64 尺寸的灰度图,两块掩码合计 8192 个像素,就算每个像素相位只量化到 256 级,密钥空间也有 256^8192 量级,暴力搜索完全不现实。
但经典 DRPE 有个工程痛点:它依赖光学系统精确对准,掩码相位值会受器件加工误差、温度、振动影响;密文是复数,普通相机只能记录强度,要记录完整的复振幅还得上相移干涉或者全息装置。这些偏差累积下来,传统逆解法的重建质量会明显下降。这也是后来大家想让深度学习介入的直接契机:能不能让网络在一个存在噪声和误差的真实环境下,依然学到可用的逆映射。
1.2 深度学习在这里究竟扮演什么角色
我把相关思路和实际代码跑下来,发现深度学习在这个问题里可以有三个角色,最容易落地的是解密端。
第一个角色是替代码里的“数字逆系统”。输入密文的幅度和相位(或者实部虚部),输出明文。传统逆解法是由掩码共轭精确反推,深度学习则是用一种可学习的非线性映射去逼近这个逆过程。好处是即使掩码有一点点不匹配、密文里混入噪声,网络仍然可以恢复出大致轮廓,这在鲁棒性上明显优于纯解析反演。
第二个角色是安全性验证工具。研究者经常故意训练一个“不带密钥”的模型,让它从密文直接猜明文,用恢复质量来评估系统抵抗统计攻击和已知密文攻击的能力。注意这不是鼓励任何人去干非法的事,而是在自己的系统上做安全测试,属于安全工程的常规动作。
第三个角色是端到端联合设计。把两块相位掩码当成可学习参数,让网络同时优化加密器和解密器。这样得到的“学习型光学加密”在某些场景下比纯随机掩码更容易在真实器件上落地。不过它偏离了“随机相位”的原意,更像自动设计掩码,实现复杂度也更高。
本博文侧重第一个场景:固定光学仿真链路加深度学习解密。这也是最好复现、最适合入门的路线。
2. 核心原理与参数:不搞清楚,后面全白干
2.1 随机相位掩码与光学坐标约定
随机相位掩码最简单的数学形式是两个独立生成的相位函数:
M1(x,y) = exp(j * θ1(x,y)) M2(u,v) = exp(j * θ2(u,v))
其中 θ1、θ2 都在 [0, 2π) 上均匀随机采样。注意 M2 是在频域坐标上采样,尺寸必须与频谱一模一样,不要和空域混着用。离散实现时可以直接用 PyTorch 生成:
import torch import numpy as np def random_phase_mask(shape, seed=None): if seed is not None: torch.manual_seed(seed) theta = 2 * np.pi * torch.rand(shape, dtype=torch.float32) return torch.exp(1j * theta)这里有一个容易踩的坑:离散 FFT 和光学傅里叶变换的坐标约定不一致。用torch.fft.fft2算完之后,零频位于左上角,而光学透镜在频谱面上默认中心是零频。所以仿真中的频谱一般要配合torch.fft.fftshift使用,而且加密和解密全程要保持完全一致的约定。
如果你偷懒得过且过,有些情况下居然也能还原——那多半是共轭操作把某种不一致抵消了。但一旦引入噪声、裁剪或掩码失配,问题就会立刻爆发,恢复结果直接从清晰图像变成雪花屏。
2.2 复数密文怎么喂给神经网络
CNN 没法直接吃复数张量。密文在代码里是一个 complex64 张量,常见有三种输入组织方式:
- 振幅+相位:取
.abs()和.angle(),拼接成两个通道。 - 实部+虚部:取
.real和.imag,两个通道。 - 四通道:把振幅、相位、实部、虚部都放进去。
我的实测结论是:在同样的网络结构和训练轮数下,振幅+相位的效果最稳。幅度刻画整体强度,相位携带高频细节,这两种特征符合图像先验;实虚部虽然在数学上完备,但看过去就是两幅没有太多语义特征的二维场,网络收敛慢很多。
同时,输入前要做归一化。相位的范围是 [-π, π],直接除以 π 缩放到 [-1,1] 就好。振幅分布通常跨度很大,直接用原始值会让损失被大值样本主导。建议先统计训练集振幅的 99% 分位数,把振幅截断到 [0, max_amp] 再除以 max_amp;或者用torch.log1p(amplitude)做压缩。这一步不做,后面训练大概率卡在 loss 不降的困境里。
还有另一种极端情况:有些研究者为了模拟实际 CCD 记录,只保留密文的强度而丢掉相位。这种情况下信息损失是本质性的,解密难度会高好几个台阶。如果刚开始复现,建议先做包含完整复振幅的仿真版本,跑通后再往强度域挑战。
2.3 解密网络与损失函数怎么定
网络结构我用了一个轻量 UNet,不需要在这里贴全部代码,结构就是编码器-瓶颈-解码器。输入通道是 2(振幅+相位),输出通道是 1,对应灰度明文。如果你要处理彩色图像,可以把明文换成 3 通道,但加密仿真要对每个通道分别做 FFT,计算量直接翻三倍,一般不划算。
损失函数用了 L1 + SSIM 的组合:
loss = alpha * L1(pred, target) + (1 - alpha) * (1 - SSIM(pred, target))
alpha 取 0.3 左右。纯 L2 会让边缘发糊,纯 SSIM 在高频上容易不稳定。L1 对细节恢复更好,SSIM 负责约束整体结构。我对比过三四种组合,L1+SSIM 是效果和稳定性最平衡的。
评价指标除了 PSNR、SSIM,最好附上相关系数 CC。这是老文献里常见的参数,便于与 DRPE 原文对比。CC 的计算很简单,就是两个图像矩阵的 Pearson 相关系数,很多图像质量评估库都自带。给论文或课程报告准备结果时,PSNR/SSIM/CC 三件套一起放,比较完整。
3. 源码实现与实操全流程
3.1 环境准备与数据集构建
环境方面,我用的是 Python 3.9 + PyTorch 2.0,OpenCV 读图,显存 4GB 以上就够跑 64x64 的小图训练。如果显存只有 2GB,把 batch size 降到 8 并开混合精度也能离线训练,只是速度慢一些。
数据集就用公开图像库里的自然图像子集,取一个小的就行。预处理流程:
- 转灰度图。
- resize 到 64x64 或 128x128。
- 像素归一化到 [0,1]。
- 保存为 npy 或 h5,方便后面缓存密文。
这里有一个非常关键的设计决策:固定掩码还是动态掩码。如果你想复现“固定密钥”场景,必须在准备数据的时候就确定掩码种子并固定,否则每个 step 重新生成掩码,模型面对的是一个不断变化的映射目标,很难收敛。如果目标是“任意随机密钥”,那模型需要额外接收掩码信息作为条件输入,难度更大,建议放在后面对比实验里再上。
我习惯的代码结构是这样:
drpe_denoise/ ├── config.py # 超参数,含图像尺寸、掩码种子、训练轮数 ├── dataset.py # 明文 dataset 与密文缓存构建 ├── drpe.py # 随机相位掩码生成 + DRPE 加密仿真 ├── model.py # 解密UNet ├── train.py # 训练循环 └── evaluate.py # PSNR/SSIM/CC 评估与可视化3.2 加密与解密核心代码
加密函数的核心逻辑:
def drpe_encrypt(image, mask1, mask2): # image: (B,1,H,W) float tensor,值域 [0,1] field = image * mask1 spectrum = torch.fft.fftshift(torch.fft.fft2(field), dim=(-2, -1)) field_masked = spectrum * mask2 cipher = torch.fft.ifft2(torch.fft.ifftshift(field_masked, dim=(-2, -1))) return cipher传统逆解密(用于对照实验):
def drpe_decrypt(cipher, mask1, mask2): spec = torch.fft.fftshift(torch.fft.fft2(cipher), dim=(-2, -1)) field = spec * torch.conj(mask2) field = torch.fft.ifft2(torch.fft.ifftshift(field, dim=(-2, -1))) out = field * torch.conj(mask1) return out.abs()这里有一个小细节:mask2 在加密时是在频域直接乘上去的,所以解密时先回到频谱面,乘共轭,再逆变换回物面,最后去掉 mask1 的相位。很多初学者会把顺序搞反,导致输出仍然是一团噪声。
训练时,如果模型要学固定密钥下的解密,网络输入就是密文的幅度+相位,输出归一化后的明文。如果用动态密钥,建议把两个掩码的幅度相位也拼到输入里,或者通过一个小分支把掩码编码成条件向量,否则模型面对的是多解问题,很难收敛。
3.3 训练策略与时长控制
固定密钥+小数据集是最容易跑通的起点。以 64x64 分辨率、500 张训练图、UNet 参数量几百万级别为例,单卡训练 200 个 epoch,20 分钟左右就能看到一个能用的结果,前提是开启 AMP 混合精度和数据增强。
动态密钥模式更难:每个 batch 的密钥都不同,模型需要额外理解“掩码输入”,才能学到跨密钥泛化。建议采用课程学习:前 50 个 epoch 用固定掩码,让网络先学会基本图像重建;后 150 个 epoch 再切换为随机掩码并把掩码拼进输入。这个技巧能明显降低一开始就上动态密钥带来的发散风险。
训练循环里还有两个常被忽略的配置:梯度裁剪和 EMA。梯度裁剪我一般 clamp 到 5.0,光学图像重建任务里梯度偶尔会出现尖峰,裁剪之后稳定很多。EMA 是每轮把模型参数做指数滑动平均,虽然代码多几行,但能白嫖提升 0.2~0.5dB 的 PSNR。很多开源实现不会主动提这一点,实际用下来收益很实在。
4. 实验效果与安全性权衡
4.1 几组关键对照实验
跑实验时,有两类对比必须做,否则结论站不住脚。一类是理想条件下的传统逆 DRPE 解密,用来确认加密仿真代码正确;另一类是带扰动条件下的对比,用来体现深度模型的鲁棒性。下面是我在类似数据规模和参数下跑出来的参考区间:
| 场景 | 方法 | PSNR参考 | SSIM参考 | 说明 |
|---|---|---|---|---|
| 理想密钥 | 传统DRPE逆解 | 60+ | 0.9999 | 近似精确重建,纯数值仿真 |
| 掩码失配1% | 传统DRPE逆解 | 18~25 | 0.7~0.85 | 对误差非常敏感 |
| 固定密钥 | 深度学习解密 | 30~35 | 0.92~0.96 | 需要训练固定掩码集合 |
| 动态密钥 | 深度学习+条件输入 | 28~32 | 0.90~0.94 | 能泛化到同分布新密钥 |
| 无密钥盲恢复 | 深度学习直接猜 | 14~18 | 0.4~0.6 | 只能恢复出模糊轮廓 |
这些区间会随数据集和网络结构浮动,但趋势很明确:只要已知密钥约束,深度模型不输传统逆解;一旦出现扰动,深度模型更抗造。
传统逆解在掩码失配 1% 时的退化很夸张,原因是 DRPE 把信息均匀摊到了整个频域,任何一个相位像素错误都会扩散到全图。就算只有 1% 的掩码像素出错,输出也基本看不到原图轮廓。而深度模型因为见过大量“退化到正常”的样本,反而学会了在这种局部误差下提取可用信息。
4.2 聊聊模型即密钥的风险
固定掩码模式下,训练好的解密模型本质上是把那组随机掩码的逆过程压缩进了网络权重。也就是说,模型文件本身已经成为一枚“隐式密钥”。如果只把密文和模型公开,而掩码种子不公开,等于攻击者拿到了一个可用的解密器。
所以这里要明确安全边界:
- 源码仓库里只提供训练流程和示例掩码,实际部署时掩码种子与模型分开保管。
- 不要把掩码种子直接写在配置文件里随模型一起分发。
- 如果要分享 demo,用随机生成的临时掩码,而不是正式密钥。
我特意做过一个极端实验:固定密钥模式训练好的网络,换到另一组随机密钥下评估,PSNR 直接掉到 15 以下。这说明网络确实“记住”了某个密钥空间的逆映射,而不是凭空学会了任意解密。所以“模型即密钥”不是危言耸听,是设计系统时必须留意的边界。
5. 常见问题与排查技巧实录
5.1 现象速查表
实际操作中,我把遇到过的典型问题整理成了表,方便你对照排查:
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 训练loss不下降 | 振幅输入未归一化,损失被大值样本主导 | 按99%分位数截断或log压缩 |
| 输出全灰/全黑 | 网络输出范围与标签不一致 | 确认输出层没有不恰当的激活函数,标签范围一致 |
| 加密后还原出现偏移 | fftshift/ifftshift使用不一致 | 统一加解密约定的shift顺序 |
| 固定掩码训练却无法收敛 | 数据加载时每次重新生成掩码 | 在数据集初始化时生成一次并固定种子 |
| 动态密钥训练效果差 | 模型没收到掩码条件信息 | 将掩码幅度/相位拼接到输入,使用条件解密网络 |
| 显存不足 | 输入尺寸或batch偏大 | 改小输入尺寸、batch降到8,开混合精度 |
| 图像隐约倒置 | 傅里叶变换与光学坐标对应关系反了 | 检查前向/逆向的镜像关系,必要时翻转掩码 |
5.2 几个影响结果质量的细节
第一,密文的振幅分布一定要先看直方图再做归一化。我第一版直接除以最大值,结果大部分像素被压到 0 附近,网络学到的是“输出一个平均值”,PSNR 只有 12。后来改成 99% 分位数截断,效果立刻起来。这个操作看似简单,却是整个实验从“跑得动”到“跑得好”的分水岭。
第二,如果密文需要保存到磁盘,建议不要只存幅值图。只用幅值会让相位信息永久丢失,解密任务就从“复值逆变换”变成更难的“相位恢复”问题。正确做法是把复数实虚部分开,用 float16 双通道存储;或者直接存 npy 复数数组。
第三,不要迷信网络结构。光学的随机相位加密与解密本质上是一个线性变换加随机相位调制,只要输入预处理靠谱,一个简单的 UNet 就够用。我试过更大的 Transformer 类模型,结果提升不到 1dB,训练成本却翻了几倍。在这个问题上,数据组织的收益通常远大于模型结构的收益。
第四,评估时除了单张可视化,最好计算整个测试集上的 PSNR 均值。单张图选得好,不代表模型整体稳健。安全评测报告里一定要看分布,不要只贴一张效果最好的图。
5.3 建议的执行路线
如果从零开始复现,我会按这个顺序走:
- 先写 drpe.py,用同一张图、同一组掩码加密再解密,验证逆解代码没 bug。
- 用固定种子构造 50 张图的迷你训练集,跑一个极小的 UNet,确认 loss 能下降。
- 观察生成的图像:如果隐约出现明文轮廓,说明输入特征有效。
- 再把数据集放大到 500 张,训练 200 个 epoch,得到可发布的结果。
- 最后上动态密钥+条件输入,面向“任意随机相位掩码”的泛化场景。
这个路线能避免一上来就被各种高级问题劝退,尤其是动态密钥和对抗攻击,等到基础链路稳定之后再碰会顺手很多。
我个人在实际操作中最受益的一个改动,是把加密后的密文一次性全部预计算并缓存到磁盘,而不是每个 epoch 重新跑 FFT。这样一来,训练轮数增加的成本只是 GPU 读数据,几乎零额外开销。我甚至会把密文的幅度和相位分别存成两个单通道文件,配合标准数据加载器直接读取。这个“离线密文缓存”看起来不起眼,却让整个实验迭代速度快了好几倍。
踩过几次坑之后,我现在的体会是:先把逆解闭环跑通,再谈模型泛化,最后才谈攻击与防御,这条路线对新手最友好。深度学习随机相位加密看起来神秘,实际上就是“用数据驱动的逆映射替代解析逆系统”的一次漂亮实践,值得花时间把每个环节都吃透。