☰
基于CNN的LSB隐写分析:Python实现与优化
2026/10/12 16:26:39 网站建设 项目流程

简介:这份资源面向信息安全、数字水印与隐写分析方向的学生和研究者,尤其适合做课程设计或毕业设计时参考。内容围绕LSB信息隐藏算法展开,覆盖空域与变换域两类思路,并延伸到JPEG、PDF等载体上的信息隐藏实现,同时提供基于卷积神经网络的隐写分析方案,以及Matlab下的SRM、SCA隐写分析代码,形成从隐藏到检测的完整链路。压缩包共137个文件,约304.81MB,包含24个Python源码、32个Matlab脚本、30篇PDF文献、12篇caj论文,以及ipynb实验笔记、docx设计报告、prototxt网络配置和mat数据等,兼顾代码复现与理论查阅。目前已有1135人学习下载。读者可据此获得可运行的隐写与隐写分析工程、CNN模型训练流程、经典文献合集及设计报告框架,便于快速搭建实验环境并理解算法原理。

1. 从一张被改过的截图说起:LSB 隐写与 CNN 隐写分析到底在解决什么

你手里有一张 PNG 截图,肉眼看不出任何异常,但它的像素最低位可能已经被替换成了另一段数据——这就是 LSB 隐写。把秘密写进图像最低有效位,视觉上几乎无损,却足以让原图携带额外信息。问题在于:写进去容易,查出来难。传统 RS 分析、卡方检验在低嵌入率下误报率飙升,而卷积神经网络(CNN)恰好擅长从像素级噪声里找统计痕迹。这个标题讲的就是用 Python 把 CNN 训练成一个隐写分析器,输入是可疑图像,输出是“干净”还是“被 LSB 改过”。适合两类人:做图像取证、数据防泄漏的工程师,以及想找一个能跑通、有明确评价指标的深度学习练手项目的人。下面从数据构造讲到模型调参,每一步都给出可复现的命令和参数。

2. 用 Python 构造 LSB 隐写数据集:从载体图到标签

2.1 为什么不能直接拿 ImageNet 当载体

隐写分析模型学的是像素最低位的统计异常,不是物体类别。用 ImageNet 这类语义丰富的图,模型很容易走捷径去记纹理和边缘,而不是真正关注 LSB 平面。常见做法是选 BOSSbase 或 BOWS2 这类专用载体库,它们本身就是为隐写分析准备的,图像内容偏自然场景,尺寸统一为 512×512。如果没有这些库,用 DIV2K 或自己拍的自然风景图也行,但必须满足两个条件:一是 PNG 无损格式,二是图像内容不要有大面积纯色区域——纯色区域的 LSB 平面本身就很规律,嵌入后反而看不出差异。

我一般会先做一次筛选:把图像转成灰度,计算 LSB 平面的熵,熵值低于 6.5 的直接剔除。这一步能过滤掉大量“死气沉沉”的图,让后续训练少很多噪声。

2.2 用 Python 实现 LSB 嵌入与标签生成

下面这段代码完成三件事:读取载体图、按指定嵌入率写入随机比特、保存为 stego 图并生成标签文件。嵌入率用 bpp(bit per pixel)表示,0.4 bpp 意味着每像素平均改 0.4 个最低位。

import os import numpy as np from PIL import Image def lsb_embed(cover_path, payload_bits, output_path): """ cover_path: 载体图路径 payload_bits: 要嵌入的比特数组,长度 <= H*W output_path: 输出 stego 图路径 """ img = Image.open(cover_path).convert('L') # 转灰度,简化处理 arr = np.array(img, dtype=np.uint8) h, w = arr.shape flat = arr.flatten() n = len(payload_bits) if n > h * w: raise ValueError('payload too large') # 只改最低位,其余位保持不变 flat[:n] = (flat[:n] & 0xFE) | payload_bits stego = flat.reshape(h, w) Image.fromarray(stego).save(output_path) def generate_dataset(cover_dir, stego_dir, bpp=0.4, seed=42): os.makedirs(stego_dir, exist_ok=True) rng = np.random.default_rng(seed) labels = [] for fname in os.listdir(cover_dir): if not fname.lower().endswith('.png'): continue cover_path = os.path.join(cover_dir, fname) img = Image.open(cover_path).convert('L') h, w = img.size[1], img.size[0] total_bits = h * w n_bits = int(total_bits * bpp) payload = rng.integers(0, 2, size=n_bits, dtype=np.uint8) stego_path = os.path.join(stego_dir, fname) lsb_embed(cover_path, payload, stego_path) labels.append((cover_path, 0)) labels.append((stego_path, 1)) return labels

逻辑说明:flat[:n] & 0xFE把最低位清零,再或上 payload 比特,保证只动 LSB。rng.integers(0, 2, ...)生成 0/1 随机序列,模拟加密后的载荷。参数bpp控制嵌入强度,常用 0.1、0.2、0.4 三档做对比实验;seed固定后每次生成的数据集完全一致,方便复现。

生成完数据后,按 8:1:1 划分训练、验证、测试集。注意:同一张载体图生成的 cover 和 stego 必须落在同一个集合里,否则模型会学到“这张图我见过”的捷径,测试准确率虚高。

2.3 数据增强的边界:哪些操作会破坏 LSB 信号

旋转、缩放、JPEG 重压缩都会破坏 LSB 平面,尤其是 JPEG 压缩,直接把最低位抹掉。所以隐写分析的增强只能用无损操作:随机水平翻转、垂直翻转、90 度旋转。这些操作不改变像素值,只改变位置,LSB 统计特征完整保留。我见过有人用 ColorJitter 做增强,结果模型在验证集上准确率 99%,一到测试集就掉到 55%——因为颜色抖动引入了新的像素值,把 LSB 信号淹没了。

提示:数据增强前先确认操作是否改变像素值。任何涉及插值、量化、颜色空间转换的操作都不能用。

3. 搭一个能跑通的 CNN 隐写分析模型:结构选择与训练参数

3.1 为什么选 SRNet 的简化版而不是 ResNet

隐写分析模型和普通图像分类模型的目标不同。ResNet 靠池化层快速降维,但池化会丢失高频噪声——而 LSB 信号恰恰藏在高频里。SRNet 的设计思路是前几层不用池化,用步长卷积代替,保留更多底层细节。完整 SRNet 有 12 层,参数量大,训练慢。我一般用它的简化版:前 4 层步长卷积 + 6 层残差块 + 全局平均池化 + 全连接。这个结构在 0.4 bpp 下能到 85% 以上的准确率,训练时间控制在单卡 2 小时以内。

具体结构参数:第一层卷积核 3×3,输出通道 64,步长 1;第二层 3×3,通道 64,步长 2;第三层 3×3,通道 128,步长 2;第四层 3×3,通道 128,步长 2。之后接 6 个残差块,每个块两层 3×3 卷积,通道 128。最后全局平均池化到 128 维,接一个 2 类全连接。

3.2 用 PyTorch 定义模型与训练循环

import torch import torch.nn as nn class SRNetLite(nn.Module): def __init__(self, num_classes=2): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 64, 3, stride=1, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.Conv2d(64, 64, 3, stride=2, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.Conv2d(64, 128, 3, stride=2, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.Conv2d(128, 128, 3, stride=2, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), ) self.res_blocks = nn.Sequential(*[ nn.Sequential( nn.Conv2d(128, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.Conv2d(128, 128, 3, padding=1), nn.BatchNorm2d(128), ) for _ in range(6) ]) self.gap = nn.AdaptiveAvgPool2d(1) self.fc = nn.Linear(128, num_classes) def forward(self, x): x = self.features(x) x = self.res_blocks(x) + x # 残差连接 x = self.gap(x).flatten(1) return self.fc(x) # 训练参数 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = SRNetLite().to(device) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.5) criterion = nn.CrossEntropyLoss()

逻辑说明:features前四层逐步降维,但步长卷积比最大池化保留更多高频信息。res_blocks里的残差连接缓解梯度消失,6 个块是精度和速度的折中。StepLR每 20 个 epoch 把学习率减半,防止后期震荡。lr=1e-3是 Adam 的常用起点,如果 loss 在前 5 个 epoch 不降,降到 5e-4 再试。

训练时 batch size 设 32,输入裁剪成 256×256 的随机块。为什么不用整图 512×512?因为显存吃紧,而且随机裁剪本身就是一种增强。每个 epoch 从每张图里随机裁一个块,相当于无限数据。训练 60 个 epoch 通常足够收敛,验证集准确率曲线会在 40 epoch 后趋于平缓。

3.3 评价指标:准确率之外必须看的两条曲线

隐写分析不能只看准确率。低嵌入率下,模型容易把所有样本判为 cover,准确率也有 50%,但毫无意义。必须同时看 ROC 曲线下的 AUC 和等错误率 EER。AUC 越接近 1 越好,EER 越低越好。我一般在验证集上每 5 个 epoch 算一次 AUC,如果 AUC 连续 10 个 epoch 不涨,就提前停。

另外,测试集必须包含不同 bpp 的样本。只在 0.4 bpp 上训练,模型到 0.1 bpp 上可能直接失效。常见做法是混合 0.1、0.2、0.4 三档一起训练,让模型学会适应不同嵌入强度。混合训练后,0.1 bpp 的准确率能从 55% 提到 72% 左右。

4. 避坑与排查:LSB 隐写分析训练中最容易翻车的 5 个点

4.1 现象:训练 loss 正常下降,验证准确率始终 50%

原因:数据划分时把同一张载体的 cover 和 stego 分到了不同集合。模型在训练集见过这张图的 cover,验证时遇到它的 stego,直接靠记忆判断,而不是学 LSB 特征。解决:按载体图划分,同一张图的所有版本必须进同一个集合。写个检查脚本,确认训练集和验证集的文件名无交集。

4.2 现象:模型在 PNG 上表现好,换 JPG 直接崩

原因:JPEG 压缩会量化 DCT 系数,LSB 平面被破坏。如果训练集全是 PNG,模型没学过压缩后的残差特征。解决:训练时加入 JPEG 重压缩增强,质量因子随机选 70 到 95。注意:增强后的图要重新生成标签,因为压缩后 LSB 信号已经变了。更稳妥的做法是单独训练一个针对 JPEG 的模型,输入用 DCT 残差而不是空域像素。

4.3 现象:GPU 显存溢出,batch size 降到 8 还是报错

原因:输入尺寸 512×512 太大,SRNetLite 前几层通道数 128,中间特征图占用显存高。解决:训练时随机裁剪 256×256,测试时用整图但分块推理再投票。分块推理的代码很简单:把 512×512 切成 4 个 256×256,分别预测,取平均概率。这样显存占用降到 1/4,准确率损失不到 1%。

4.4 现象:AUC 很高但 EER 也很高

原因:类别不平衡。如果 stego 样本远多于 cover,模型偏向判 stego,ROC 曲线整体上移,但 EER 点对应的阈值偏离 0.5。解决:训练时用 WeightedRandomSampler 做平衡采样,或者损失函数加 class weight。我一般直接保证 cover 和 stego 数量 1:1,生成数据时就配对生成,不额外采样。

4.5 现象:换一台机器跑,准确率掉 10 个点

原因:PyTorch 版本或 CUDA 版本不同,导致随机种子行为不一致,或者卷积算法选了不同实现。解决:固定所有随机种子,包括 Python、NumPy、PyTorch、CUDA。在代码开头加:

import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False

deterministic=True会牺牲一点速度,但保证每次结果一致。如果追求速度,可以设benchmark=True,但复现性就没了。做实验阶段建议开 deterministic。

5. 进阶技巧:用残差学习把低嵌入率准确率再提一截

5.1 为什么直接预测像素不如预测残差

LSB 嵌入只改变最低位,对像素值的影响是 ±1。如果模型直接看像素,大部分信息是图像内容本身,LSB 信号被淹没。残差学习的思路是:先用一个固定高通滤波器(比如 KV 核)卷积图像,得到残差图,再送进 CNN。KV 核是 5×5 的卷积核,专门增强高频噪声。这一步不需要训练,是固定的预处理。

我试过在 SRNetLite 前面加一层 KV 卷积,0.1 bpp 下的准确率从 72% 提到 78%,AUC 从 0.81 提到 0.87。代价是训练时间增加约 15%,因为输入通道从 1 变成 1(KV 核输出还是单通道),但计算量略增。

5.2 用 SRM 滤波器组做多尺度残差

KV 核只有一种尺度,SRM(Spatial Rich Model)提供了 30 个高通滤波器,覆盖不同方向和频率。全部用上计算量太大,我一般选其中 5 个:一阶水平、一阶垂直、二阶水平、二阶垂直、3×3 拉普拉斯。把这 5 个滤波器的输出拼成 5 通道输入,再送进 CNN。这样模型能同时看到多个尺度的残差,对 0.05 bpp 这种极低嵌入率也有一定检测能力。

import torch.nn.functional as F # 定义 5 个 SRM 滤波器 srm_kernels = torch.tensor([ [[0, 0, 0], [0, -1, 1], [0, 0, 0]], # 一阶水平 [[0, 0, 0], [0, -1, 0], [0, 1, 0]], # 一阶垂直 [[0, 0, 0], [1, -2, 1], [0, 0, 0]], # 二阶水平 [[0, 1, 0], [0, -2, 0], [0, 1, 0]], # 二阶垂直 [[0, 1, 0], [1, -4, 1], [0, 1, 0]], # 拉普拉斯 ], dtype=torch.float32).unsqueeze(1) # shape: (5,1,3,3) def srm_residual(x): # x: (B,1,H,W) return F.conv2d(x, srm_kernels, padding=1) # 输出 (B,5,H,W)

逻辑说明:srm_kernels的 shape 是 (5,1,3,3),表示 5 个输出通道,每个通道一个 3×3 核。F.conv2d不训练这些核,只做固定卷积。输出 5 通道残差图直接作为 CNN 的输入。注意:残差图的值域可能超出 [0,255],训练前做一次标准化,减均值除标准差。

5.3 验证方法:用已知嵌入率的图做盲测

训练完模型,别只看测试集准确率。找 20 张模型没见过的图,用 0.05、0.1、0.2、0.4 四个 bpp 分别生成 stego,然后让模型盲测。记录每个 bpp 下的准确率和 AUC。如果 0.05 bpp 下 AUC 低于 0.65,说明模型对极低嵌入率不敏感,需要加 SRM 残差或增加训练数据。这个盲测流程我每次都会跑一遍,比看 loss 曲线直观得多。

最后说个习惯:我每次训完模型,会把权重、训练参数、数据生成种子写进一个experiment_log.txt,和模型文件放一起。过两个月回头看,没有这个日志根本记不清当时用的什么 bpp 和 lr。隐写分析这行,复现性比什么都重要。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询