简介:这份毕业设计资源基于Python深度学习实现图像隐写分析与隐写去除,面向计算机、通信、人工智能、自动化等专业的学生与从业者,可直接用于期末课程设计、大作业或毕业设计参考。压缩包共168个文件,容量7.71MB,以Python源码(47个py)为核心,配合40个pgm测试图像、30个pyc编译文件、24个xml配置、4个ui界面文件及PPT论文演示,覆盖模型实现、界面交互与实验说明等完整环节。项目中包含SRNet、HUGO等隐写分析/隐写工具相关模块及测试输出,可帮助学习者理解深度学习在图像隐写检测与清除中的应用流程。代码已经调试运行,作者答辩评分98分,具备较高借鉴价值;基础较好的读者还可在此基础上调整功能,进一步拓展研究。目前已有160人学习下载,适合作为入门进阶的实战项目。
1. 图像隐写分析不只是找茬:判断“有没有”之后,还要解决“怎么去掉”
图像隐写分析(Steganalysis)在很多毕设里被做成一道“二分类题”:输入一张图,神经网络输出 0 或 1,判断里面有没有藏东西。但这个项目把任务往前推了一步——先用 Python 深度学习模型判断图像是否被隐写,再通过一个残差回归网络把嵌入痕迹尽量抹掉,还原出接近原始载体的画面。主干用的是 SRNet 结构,把“检测”和“去除”接在同一条流水线上,还配了 GUI 做交互演示。适合正在找毕设题目、或者想把图像隐写分析从“分类问题”做成“图像恢复问题”的从业者。复现时你很快会发现,真正卡人的不是网络跑不起来,而是训练数据怎么生成、模型学的到底是一张图还是图里的一层噪声。
2. 先造数据:HUGO 嵌入模拟与 pair 样本生成
2.1 为什么训练集不是现成的,而是要自己“嵌”出来
图像隐写分析没有 ImageNet 那种现成数据集,核心原因是:stego(隐写图)必须和 cover(原始载体)成对出现,模型才知道“同一张图,被嵌之前长什么样,被嵌之后长什么样”。公开数据集里只有一堆干净图片,隐写对要自己用嵌入算法现场造。
这个项目选的是 HUGO,全称 Highly Undetectable Stego,一种内容自适应隐写算法。它不像早期 LSB 替换那样均匀地改像素,而是先算一个失真函数,把嵌入改动尽可能集中到图像纹理复杂、人眼不敏感的区域,因此更难被传统统计特征发现。训练时用 HUGO 在 BOSSBase 这类灰度载体上以不同嵌入率生成 stego 图,网络才能看到“嵌入痕迹在不同纹理区域的分布规律”。
嵌入率 bpp(bits per pixel)是这里最关键的参数。bpp = 0.4 意味着每个像素平均要嵌 0.4 比特,换算下来一张 512×512 的图要嵌入大约 13 KB 秘密信息。bpp 越高,嵌入改动越明显,SRNet 学起来越容易,但也越偏离真实隐蔽场景;bpp 越低,痕迹越弱,模型很容易学到“什么都不干,输出原图”这种偷懒解。所以训练数据里混着 0.1、0.2、0.4 三档 bpp 是常见做法。
2.2 生成 pair 的 Python 骨架与嵌入率参数
真实 HUGO 的嵌入过程包含四个方向的高通滤波器失真计算和 STC(Syndrome-Trellis Codes)编码,短代码里不好完整实现。我一般先用一个简化骨架把数据 pipeline 跑通,验证目录、格式和批次逻辑没问题后,再把失真计算换成官方的 HUGO 实现。
import numpy as np from PIL import Image def prepare_train_pair(cover_path, bpp=0.4, seed=42): cover = np.asarray(Image.open(cover_path).convert("L"), dtype=np.uint8) rng = np.random.default_rng(seed) h, w = cover.shape # 按嵌入率算总共要嵌多少比特 payload_bits = int(h * w * bpp / 8) * 8 stego = cover.copy() # 示意:真实 HUGO 会按失真函数选嵌入位置,这里用随机掩膜代替 # 失真值越低的像素越可能被改,纹理区失真大,反而改得少 mask = rng.random((h, w)) flat_idx = np.argpartition(mask.ravel(), -payload_bits)[-payload_bits:] row_idx, col_idx = np.unravel_index(flat_idx, cover.shape) # 把选中像素的最低比特位替换成随机秘密比特 secret_bits = rng.integers(0, 2, payload_bits) stego[row_idx, col_idx] = (stego[row_idx, col_idx] & 0xFE) | secret_bits return cover, stego cover, stego = prepare_train_pair("p1.jpg", bpp=0.4, seed=7) print(cover.shape, stego.shape, np.mean(cover != stego))这个脚本的输入是单张灰度图,输出是 cover 和 stego 两个形状相同的数组。参数 seed 必须固定,否则同一个载体每次生成的嵌入位置不一样,复现实验时会很痛苦。np.argpartition 那行代码做的是局部排序:选出差值最大的前 payload_bits 个像素索引,这样避免对整张图做全量排序,大图批量生成时能省不少时间。
替换 LSB 只是示例。真正用 HUGO 时,这里要换成“按失真函数计算每个像素的嵌入代价,再用 STC 编码选择改动位置和改动方向”,失真函数的权重直接影响最终嵌入分布。训练集建议按 50% 的 0.2 bpp、30% 的 0.4 bpp、20% 的 0.1 bpp 混合,这样模型不会只盯着最强痕迹学。
2.3 目录线索怎么读:HUGO_like 与 encoder 输出
项目文件里有个 HUGO_like 目录,还有 encoder_HUGO_test.out 和 encoder_test_8.out 这两个输出文件。从命名习惯看,前者应该是一份接近 HUGO 的嵌入器实现,后者像是用两个不同配置跑完嵌入测试后保存的结果文件。遇到这种目录结构,我建议先别急着读代码,直接看 README.md 里写了哪些入口脚本,再去翻 out 文件的前几行,看它是日志文本、图片数组还是编码参数。
这类 out 文件通常是调试留下的“后悔药”:训练中途发现嵌入器行为不对,可以把当前输入图、嵌入参数、输出图都 dump 出来对比。自己复现时也建议保持这个习惯,每次生成 pair 都顺手存一张差值图(cover 减 stego 的绝对值放大 10 倍),肉眼看一遍再进网络。很多嵌入器“看起来能用、实际上把整张图都改了”的问题,靠打印指标发现不了,靠差值图一眼就能看出来。
3. SRNet 网络与训练参数:从双通道输入到残差回归
3.1 为什么这个项目选 SRNet 做隐写去除
SRNet 最早是为隐写检测设计的网络,全称 Structured Steganalysis Residual Network,论文发表在 IEEE TIFS 上。它跟普通分类网络最大的区别是:输入不是单张图,而是原始图和经过高速滤波后的“残差图”叠成的双通道。隐写嵌入是加性改动,改动幅度通常只有 1 到 2 个灰度级,直接让网络看图很难区分,但把它转成残差后,嵌入痕迹会从平滑的背景噪声里凸出来。
这个项目做“隐写去除”时借用了同一套残差思想,只是把末端分类头换成了回归头。网络不再输出“含密/不含密”,而是输出一张与输入同尺寸的残差图。训练目标是让网络拟合 cover 减 stego 的差值,推理时用 stego 减去预测残差得到“去隐写后的图”。为什么学残差而不直接学干净图?因为干净图和隐写图 99% 的像素是相同的,直接回归整张图会让网络倾向于输出模糊的平均脸;而残差图里大部分区域是 0,只有嵌入位置有微小值,网络把注意力集中在真正要修的地方,收敛快很多。
3.2 SRNet 骨架的 PyTorch 复现与参数说明
项目源码里如果用的是 TensorFlow,结构也能对应上;我习惯用 PyTorch 复现,骨架长这样:
import torch.nn as nn class SRNetRemove(nn.Module): def __init__(self): super().__init__() # 预处理块:把 2 通道输入升到 64 通道,不降分辨率 self.prep = nn.Sequential( nn.Conv2d(2, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), ) # 特征提取:4 层卷积,逐步降采样 self.features = nn.Sequential( nn.Conv2d(64, 16, 3, stride=2, padding=1), nn.BatchNorm2d(16), nn.ReLU(inplace=True), nn.Conv2d(16, 16, 3, stride=2, padding=1), nn.BatchNorm2d(16), nn.ReLU(inplace=True), ) # 残差块组:保持 16 通道,继续增强感受野 self.res_blocks = nn.Sequential( *[ResBlock(16) for _ in range(4)], ) # 回归头:输出单通道残差图 self.head = nn.Sequential( nn.Conv2d(16, 64, 1), nn.ReLU(inplace=True), nn.Conv2d(64, 1, 1), ) def forward(self, cover, stego): # 双通道输入:原图 + 隐写图 x = torch.cat([cover, stego], dim=1) x = self.prep(x) x = self.features(x) x = self.res_blocks(x) residual = self.head(x) # 特意学习小残差,数值范围需要稳定 return residualforward 里拼接 cover 和 stego 用的是 torch.cat,维度要确保都是 [N, 1, H, W],否则拼接后通道数不是 2,后面卷积层直接报错。特征提取部分每层 stride=2,原始 512×512 输入到残差块前已经缩到 128×128,既降计算量,又强制网络学习更大范围的纹理上下文。回归头最后没有接 Sigmoid 或 Tanh,因为残差值是有正有负的浮点数,直接线性输出反而好训练。
ResBlock 建议用两个 3×3 卷积加跳跃连接的经典结构。这个项目既然叫“隐写分析以及隐写去除”,网络头部也可以顺便分出一个二分类分支,后面我会讲这个进阶玩法。
3.3 训练策略:batch、学习率、epoch 与评估指标
训练配置我一般这样起步:AdamW 优化器,初始学习率 1e-3,跑 10 个 epoch 后降到 1e-4,total epochs 60。batch size 在 32GB 显存上可以开到 16,输入裁剪 256×256,超过就随机裁剪,低于就 pad。loss 用简单粗暴的 MSE,因为残差图大部分是 0,MSE 能保证网络不过度修正边缘纹理。
评估别只看 training loss。每个 epoch 结束要在固定测试集上算三样东西:PSNR、SSIM,以及“残留检测率”。用另一个预训练好的隐写分析分类器判断去除后的图还能不能被识别为 stego——这个指标比 PSNR 更接近真实目标。PSNR 只衡量像素差异,SSIM 衡量结构相似度,两个都高才是“肉眼看起来干净”。
记录训练曲线时,events.out.tfevents 文件会自动被 TensorBoard 识别,命令是 tensorboard --logdir logs。我习惯同时记录原图残差和预测残差的 L1 距离,如果训练中这个值开始回升,说明学习率太大,模型在震荡。
4. 从骨架到可运行:训练脚本、TensorBoard 日志与 GUI 串联
4.1 从项目文件反推复现顺序
拿到压缩包先别急着运行。快速扫一遍文件清单,里面几个文件能直接告诉我复现顺序:README.md 是入口文档;events.out.tfevents 说明训练过程有 TensorBoard 日志,环境里需要装 tensorboard;SRNet.iml、GUI.iml 是 IntelliJ 工程文件,IDE 能直接打开,但和训练无关;HUGO_like 和 encoder 开头的一批 out 文件,是数据生成和嵌入测试相关。.iml 文件不是源码,别浪费时间读它们,直接跳过。
复现顺序建议是:先把 HUGO_like 嵌入器跑通,生成一对 cover/stego;再用这对样本把 SRNet 前向跑一遍,确认双通道输入和残差输出维度对上;然后启动训练脚本,盯 TensorBoard 曲线;最后打开 GUI,加载最好的一版 checkpoint 做交互演示。跳步是最容易翻车的,有人直接跑 GUI,结果模型权重路径写死,报错后还以为代码有问题。
4.2 训练主循环与断点恢复逻辑
训练脚本里最值得抄的是断点恢复部分。深度学习训练经常跑到一半被机房断电或者显存溢出打断,没有断点恢复就得从头再来,非常伤。一个合格的训练循环必须具备“启动时检查 checkpoint,存在就继续训练”的能力。
def train_loop(model, train_loader, val_loader, args): optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr) start_epoch = 0 best_psnr = 0.0 if args.resume and os.path.exists(args.ckpt_path): ckpt = torch.load(args.ckpt_path) model.load_state_dict(ckpt["model"]) optimizer.load_state_dict(ckpt["optimizer"]) start_epoch = ckpt["epoch"] + 1 best_psnr = ckpt.get("best_psnr", 0.0) print(f"resume from epoch {start_epoch}, best PSNR {best_psnr:.2f}") for epoch in range(start_epoch, args.epochs): model.train() for batch_idx, (cover, stego) in enumerate(train_loader): cover = cover.to(args.device) stego = stego.to(args.device) residual = model(cover, stego) loss = nn.functional.mse_loss(residual, cover - stego) optimizer.zero_grad() loss.backward() optimizer.step() # 每个 epoch 结束跑一次验证 val_psnr = validate(model, val_loader, args) if val_psnr > best_psnr: best_psnr = val_psnr torch.save({ "model": model.state_dict(), "optimizer": optimizer.state_dict(), "epoch": epoch, "best_psnr": best_psnr, }, args.ckpt_path)这段代码里有几个细节值得注意。启动时先检查 resume 路径,加载模型的同时必须加载 optimizer,否则续训时学习率状态会重置,前几个 epoch 可能出现 loss 突然飙升。torch.save 里保存的“epoch + optimizer + best_psnr”是为了恢复后能继续按最优模型挑 checkpoint——验证 PSNR 超过历史最好值才保存,防止最后几个 epoch 过拟合把好权重覆盖掉。
学习率调度我习惯用 torch.optim.lr_scheduler.StepLR,每 20 个 epoch 乘 0.5。别用 ReduceLROnPlateau,验证 PSNR 一旦停滞不动,它会把学习率一口气降到很低,后面再怎么调也上不去。
4.3 GUI 串联:加载权重、单图预测与差值图展示
项目里出现 GUI.iml 文件,说明作者有图形界面方案。毕设答辩时 GUI 是加分项,因为评委可以现场上传一张图,看系统判断是否含密、输出去除结果,直观程度远高于命令行。我把这类界面拆成三个区域:左侧是图片上传和显示区,中间是分析按钮和结果 label,右侧是去除前后的对比图以及 PSNR/SSIM 数值。
后台逻辑就是训练好的模型推理,核心代码如下:
def analyze_and_remove(image_path, model, device): img = load_image(image_path) # 转成 [1, 1, H, W] 的 float32 with torch.no_grad(): residual = model(img, img) # 先用自身当近似输入 removed = img - residual return removed, residual注意一个细节:GUI 里没有“原始 cover”,输入的就是 stego 图,所以模型输入的两个通道都不是干净图。常见做法是用 stego 自身同时作为 cover 和 stego 通道传入,SRNet 在训练时见多了这种近似,推理时结果不会差太多。但也因为这样,GUI 显示的 PSNR 是和 stego 比,不是和真实 cover 比,答辩时不要标错数值含义。
5. 复现避坑:五条典型翻车记录与排查路径
5.1 训练集与验证集数据泄漏
现象:训练 PSNR 一路走高,验证 PSNR 也非常高,看起来模型完美收敛,可一到 GUI 里换一张没见过的图就一脸懵,去除后全是纹理涂抹。
原因:生成 pair 时没有按“载体文件”划分布。同一张载体图既出现在训练集,又出现在验证集,只是嵌入位置和秘密比特不同,模型相当于记住了图本身的结构,而不是学会修隐写痕迹。
解决:先把载体文件按 8:2 分成训练载体和验证载体,再分别做嵌入。验证集只能来自验证载体,训练集永远碰不到这些图。检查方法很简单:把训练集和验证集里各抽一张 cover,算一下两张图的像素级距离,如果几乎为 0,说明泄漏了。
5.2 训练 bpp 与测试 bpp 失配
现象:模型在 0.4 bpp 数据上训练得不错,改成 0.1 bpp 后去除能力大幅下降,PSNR 掉好几个点。
原因:不同嵌入率产生的残差幅度和分布密度差别很大。0.4 bpp 的嵌入痕迹明显,模型学到的是“找大改动并抹平”;0.1 bpp 时改动稀疏且微弱,模型把大量平坦区域当成了残差去修,反而引入噪声。
解决:训练时按比例混合三档嵌入率,验证时可以按不同 bpp 分别统计 PSNR,单独记录 low/medium/high 三列曲线。正常现象是 bpp 越低 PSNR 越高,但 SSIM 可能不高,因为网络对微弱改动“不敢动”,保留了一些原始嵌入痕迹。
5.3 MSE 在降,PSNR 在涨,但纹理区出现明显伪影
现象:loss 很漂亮,PSNR 从 32 涨到 38,但挑一张纹理复杂的图放大看,树丛和草地边缘出现条状伪影,像水印没擦干净。
原因:MSE 是全局平均指标,它容忍“大部分像素修得很好,少数纹理区修坏了”的解。SRNet 的残差学习会把高频纹理边缘误判为嵌入噪声,用力过猛导致过平滑。
解决:把损失改成 MSE 加 0.1 倍 SSIM 损失的组合。或者训练后做一次后处理:用原 stego 的梯度幅度生成一个信任掩膜,在纹理较强的区域削弱网络输出,保留更多原始细节。这个方法在传统图像去噪里叫 guided filter,套到隐写去除上同样管用。
5.4 训练曲线陡降后进入平台期,再也上不去
现象:loss 前 5 个 epoch 降得飞快,之后 30 个 epoch 纹丝不动,PSNR 卡在 34 左右,换网络结构也没效果。
原因:残差图大部分是 0,网络收敛到“输出接近 0 残差”的比较优解后,梯度变得非常稀疏。这不是网络结构问题,而是学习率太大导致优化过程在平台期震荡,跳不出局部最优。
解决:初始学习率降到 3e-4 试试;或者在 loss 里对非零残差像素加权。我一般会计算一个残差掩膜,让有嵌入改动的像素比无改动像素多贡献 5 倍的梯度,这样模型不会轻易满足于“全输出 0”的偷懒解。
5.5 显存爆掉,batch size 只能开到 2
现象:512×512 输入配上双通道和 64 通道初始卷积,32GB 显存只跑得动 batch 4,训练慢得离谱。
原因:SRNet 本身不是轻量网络,双通道输入让第一层卷积计算量翻倍,又加上分支网络,显存自然吃紧。
解决:batch size 降到 4,用梯度累积模拟更大的 batch;把输入图随机裁剪到 256×256 训练,验证时才用全尺寸;再用 torch.cuda.amp 混合精度,给 16 通道卷积单独做 FP16。这三个手段叠加后显存占用能降一半还多,训练速度明显提升。
6. 进阶:换嵌入器、联合分析与一个目检习惯
6.1 从 HUGO 换到 S-UNIWARD 和 WOW:只改一个接口就行
毕设做完 HUGO 版,想体现工作量,最简单有效的是把嵌入器换成 S-UNIWARD 或 WOW。这两个算法和 HUGO 一样是内容自适应,但失真函数定义不同,嵌入痕迹的空间分布会差很多。模型训练时换一个嵌入器,等于一次完整的新实验,答辩有得讲。
embedders = { "hugo": HUGOEmbedder(verbose=False), "uniward": UNIWARDEmbedder(verbose=False), "wow": WOWEmbedder(verbose=False), } def get_pair(path, algo, bpp, seed): cover = load_gray_image(path) stego = embedders[algo].embed(cover, bpp, seed=seed) return cover, stego训练时可以用多嵌入器混合数据,也可以每个嵌入器单独训一个模型,然后对比它们在“跨嵌入器泛化”上的表现。实测下来,HUGO 训的模型拿到 S-UNIWARD 上,去除效果通常会掉一截,这是正常现象,说明网络学到的是特定嵌入算法的痕迹模式,不是通用的隐写噪声。如果能做一个“多嵌入器联合训练、跨算法测试”,这个实验完整度已经超过大多数毕设。
6.2 联合分析分支与一个验证习惯
项目标题里“隐写分析”和“隐写去除”两个功能,可以在同一个 SRNet 上实现。做法是在 res_blocks 之后分两条头:一条保持原有回归头输出残差,另一条接全局平均池化和一个二分类头,输出含密概率。训练时两个损失相加,分析分支用交叉熵,去除分支用 MSE。这个结构下两个任务共享特征提取层,去除任务学到的残差先验能帮助分析任务判断薄弱痕迹,分析任务提供的类别信号也能指导去除分支更精准地定位改动区域。
从那以后,我每次训完一版模型,都会强制自己走一遍目检流程:从测试集里挑 5 张原图,按固定 bpp 嵌入,跑去除,然后把“原图、隐写图、去除图、差值放大图”拼成一张四联图扫一眼。数值指标再高,差值图上如果还看得见人脸的轮廓,说明模型把内容信息也当成残差抹掉了,这种问题靠 PSNR 发现不了。这个习惯让我避免了好几次“指标完美、实际翻车”的答辩事故,希望帮到你。
本文还有配套的精品资源,点击获取