ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python隐写分析实战:LSB、CNN与PDF/JPEG隐写检测

2026/9/23 15:43:17 拓冰建站 浏览量
Python隐写分析实战:LSB、CNN与PDF/JPEG隐写检测 简介这份资源面向信息安全、图像处理方向的本科生与研究生以及需要完成隐写分析课程设计或毕业设计的开发者系统整理了LSB信息隐藏与卷积神经网络隐写分析的完整实现方案。包内共137个文件约304.81MB以Python源码、Matlab脚本、PDF与caj文献、docx设计报告、ipynb实验笔记及png/pgm测试图像为主另含prototxt网络配置、mat/cu/mex等工程文件覆盖空域与变换域JPEG信息隐藏、PDF文档信息隐藏、SRM与SCA隐写分析等核心模块。已有1135人学习下载说明该方向具备较高的参考价值。读者可据此获得可直接运行的项目源码、完整设计报告、文献资料与实验数据快速理解CNN用于隐写检测的建模思路并对照代码复现特征提取、模型训练与检测评估流程适合作为课程设计、竞赛或入门研究的实操参考。1. 从一张被改过的电子发票说起这套 Python 隐写分析资源到底能干什么电子发票 PDF 被人动过手脚肉眼几乎看不出来但文件里多出来的那点像素、那几行被替换的文本流往往就是证据。这套资源干的事就是把「藏」和「找」两头都跑通用 Python 实现 LSB 空域隐写、JPEG 变换域隐写、PDF 文本隐写再用卷积神经网络做隐写分析把藏了东西的图揪出来。它适合正在做课程设计、毕设或者想从零搭一套隐写检测流程的人。资源里除了源码还有设计报告和一批 PDF 隐写方向的文献省去你到处翻论文的时间。下面我按「先跑通 LSB再上 CNN 检测最后处理 PDF 和 JPEG」的顺序拆一遍参数和坑都写清楚。2. LSB 空域隐写从位平面到可复现的嵌入脚本LSBLeast Significant Bit是隐写里最经典也最容易翻车的方案。原理不复杂把载体图像每个像素的最低一位替换成秘密信息的比特人眼对最低位的变化几乎无感。但真动手写问题全在细节里——用哪个通道、怎么处理边界、嵌入容量怎么算、提取时怎么对齐。这一章把空域 LSB 的完整链路跑通后面 CNN 检测才有数据来源。2.1 位平面与嵌入容量的计算逻辑一张 RGB 图像有三个通道每个通道 8 位。LSB 只动最低位所以每个像素最多能藏 3 比特。假设图像尺寸是 512×512总容量就是 512×512×3 786432 比特约 96KB。这个数字决定了你能藏多大的秘密文件。但实际用的时候不会把三个通道全占满原因有两个一是全占满后图像直方图会出现明显的「成对」现象检测器一眼就能看出来二是嵌入率越高视觉质量下降越明显。常见做法是只用一个通道或者按 0.4~0.5 的嵌入率随机选像素。随机选像素需要种子提取端用同一个种子还原位置否则对不上。容量计算还有一个容易忽略的点秘密信息本身要加长度头。我一般会在比特流前面加 32 位整数表示消息长度提取时先读这 32 位再按长度取后续比特。不加长度头提取端不知道什么时候停多读的比特全是噪声。2.2 嵌入与提取的完整代码实现下面这段代码用 OpenCV 读图把一段文本转成比特流嵌进蓝色通道的最低位。提取脚本单独写保证两边逻辑对称。import cv2 import numpy as np def text_to_bits(text): 把文本转成比特串前面加32位长度头 data text.encode(utf-8) length len(data) # 32位长度头 bits format(length, 032b) for byte in data: bits format(byte, 08b) return bits def embed_lsb(image_path, text, output_path): img cv2.imread(image_path) if img is None: raise FileNotFoundError(f读不到图像: {image_path}) bits text_to_bits(text) # 只操作蓝色通道容量 高 * 宽 channel img[:, :, 0].flatten() if len(bits) len(channel): raise ValueError(f容量不够: 需要{len(bits)}位, 只有{len(channel)}位) # 逐位替换最低位 for i, bit in enumerate(bits): channel[i] (channel[i] 0xFE) | int(bit) img[:, :, 0] channel.reshape(img.shape[0], img.shape[1]) cv2.imwrite(output_path, img) print(f嵌入完成共{len(bits)}位输出: {output_path}) def extract_lsb(image_path): img cv2.imread(image_path) channel img[:, :, 0].flatten() # 先读32位长度 length_bits .join(str(channel[i] 1) for i in range(32)) length int(length_bits, 2) # 再读正文 data_bits .join(str(channel[i] 1) for i in range(32, 32 length * 8)) data bytes(int(data_bits[i:i8], 2) for i in range(0, len(data_bits), 8)) return data.decode(utf-8) if __name__ __main__: embed_lsb(cover.png, secret message here, stego.png) print(extract_lsb(stego.png))逻辑说明text_to_bits先把文本 UTF-8 编码再拼上 32 位长度头。embed_lsb只取蓝色通道用 0xFE清掉最低位再用| int(bit)写入新比特。提取时反过来先读 32 位算长度再按长度读正文。参数说明0xFE是掩码二进制11111110作用是保留高 7 位、清零最低位。如果你要改到绿色或红色通道把img[:, :, 0]的索引改成 1 或 2 即可但嵌入和提取必须用同一个通道否则提取出来全是乱码。图像格式必须用 PNG 或 BMPJPEG 有损压缩会把最低位改掉这是新手最常踩的坑。2.3 嵌入率与视觉质量的平衡嵌入率是隐写里绕不开的取舍。全容量嵌入每个像素都用虽然藏得多但 PSNR 会掉到 50dB 以下直方图成对效应非常明显。我一般把嵌入率控制在 0.4 左右也就是每 10 个像素用 4 个PSNR 能保持在 55dB 以上肉眼和常规统计都难发现。随机选像素需要一个伪随机序列Python 里用random.seed(42)固定种子嵌入端和提取端用同一个种子生成同一组索引。种子本身可以约定也可以藏在图像某个固定位置。这里不展开课程设计里固定种子就够了。还有一个细节嵌入前最好把图像转成 uint8OpenCV 读进来默认就是 uint8但如果你中间做了浮点运算记得转回来否则写出去的图会出问题。3. 用 CNN 做隐写分析SRM 残差与网络结构怎么搭LSB 藏完下一步是检测。传统隐写分析靠手工特征比如 SRMSpatial Rich Model提取残差再喂给分类器。这套资源里同时给了 SRM、SCA 和 CNN 的方案CNN 是重点。这一章讲清楚 CNN 隐写分析的输入怎么来、网络怎么搭、训练时看什么指标。3.1 为什么 CNN 能检测 LSB 隐写LSB 嵌入改变的是像素最低位这种改动在空间域上表现为高频噪声。传统 SRM 用一堆高通滤波器提取残差本质上是把高频信号放大。CNN 的第一层卷积核如果初始化成高通滤波器就能自动学到类似的残差特征而且后面的层可以继续组合出更复杂的模式。和图像分类不同隐写分析的信号非常弱。一张 512×512 的图嵌入 0.4 嵌入率的 LSB改动像素占比不到一半每个像素只改 1 位。这种信噪比下网络不能太深否则梯度消失也容易过拟合。常见做法是 5~8 层卷积每层 64 或 128 个 3×3 卷积核配合 BatchNorm 和 ReLU。资源里的 CNN 方案我跑过一遍输入是 256×256 的灰度图块不是整图。整图太大显存吃不消而且局部块已经能反映嵌入痕迹。训练时正样本是嵌了秘密的图负样本是原图比例 1:1。3.2 SRM 残差作为网络输入的预处理直接把原图喂给 CNN 也能跑但收敛慢。更稳的做法是先做 SRM 残差把残差图当输入。SRM 有几十个滤波器课程设计里不用全上选几个代表性的高通核就够。import numpy as np import cv2 # 几个常用的SRM高通滤波器 SRM_KERNELS [ np.array([[0, 0, 0], [0, -1, 1], [0, 0, 0]], dtypenp.float32), np.array([[0, 0, 0], [1, -2, 1], [0, 0, 0]], dtypenp.float32), np.array([[-1, 2, -1], [2, -4, 2], [-1, 2, -1]], dtypenp.float32), ] def srm_residual(image_path): img cv2.imread(image_path, cv2.IMREAD_GRAYSCALE).astype(np.float32) residuals [] for kernel in SRM_KERNELS: res cv2.filter2D(img, -1, kernel) residuals.append(res) # 堆叠成多通道形状 (H, W, C) return np.stack(residuals, axis-1) if __name__ __main__: res srm_residual(stego.png) print(res.shape) # (H, W, 3)逻辑说明cv2.filter2D对灰度图做卷积每个核输出一张残差图最后np.stack堆成多通道。这样 CNN 的输入通道数就是滤波器个数。参数说明第一个核[[0,0,0],[0,-1,1],[0,0,0]]是水平一阶差分第二个是水平二阶第三个是拉普拉斯核。核的值不用改改的是选哪几个。选多了输入通道多第一层卷积参数量上去训练慢。课程设计选 3~5 个足够。注意残差图有正有负别做归一化到 0~1直接保留浮点值。BatchNorm 会处理量纲问题。3.3 CNN 网络结构与训练参数网络结构我按资源里的思路搭了一个简化版输入是 SRM 残差输出二分类。import torch import torch.nn as nn class StegNet(nn.Module): def __init__(self, in_channels3): super().__init__() self.features nn.Sequential( nn.Conv2d(in_channels, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.Conv2d(64, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(128, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(), nn.AdaptiveAvgPool2d(1) ) self.classifier nn.Linear(128, 2) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) return self.classifier(x) # 训练参数 EPOCHS 30 BATCH_SIZE 32 LR 1e-3逻辑说明features是卷积主干四层卷积加两次池化最后AdaptiveAvgPool2d(1)把特征图压成 1×1接全连接分类。in_channels要和 SRM 残差通道数一致。参数说明学习率1e-3配 Adam 优化器30 个 epoch 在几千张图上够收敛。BatchNorm 放在卷积后、ReLU 前这是标准顺序。如果显存不够把BATCH_SIZE降到 16或者把输入裁到 128×128。训练时重点看验证集准确率不是训练集。隐写分析容易过拟合训练集到 99% 而验证集卡在 70% 是常事。如果验证集不涨先降学习率到1e-4再考虑加 Dropout。4. PDF 与 JPEG 隐写变换域和文档载体的处理空域 LSB 只是入门真实场景里 PDF 和 JPEG 更常见。PDF 电子发票篡改检测就是典型需求JPEG 则因为 DCT 变换隐写方式完全不同。这一章讲这两类载体的处理思路和代码要点。4.1 JPEG 变换域隐写的 DCT 系数修改JPEG 压缩分块做 DCT量化后得到 DCT 系数。变换域隐写不动像素改的是量化后的 DCT 系数。常见做法是把系数的最低有效位替换或者按量化步长做奇偶调制。和空域 LSB 比DCT 域隐写更抗压缩因为改的是压缩后的系数重新保存 JPEG 时只要质量因子一致系数基本不变。但容量小很多一张图能藏的比特数取决于非零 DCT 系数个数。import cv2 import numpy as np def dct_embed(image_path, text, output_path, quality90): img cv2.imread(image_path, cv2.IMREAD_GRAYSCALE).astype(np.float32) h, w img.shape # 补齐到8的倍数 h_pad (8 - h % 8) % 8 w_pad (8 - w % 8) % 8 img np.pad(img, ((0, h_pad), (0, w_pad)), modeedge) bits .join(format(b, 08b) for b in text.encode(utf-8)) bit_idx 0 for i in range(0, img.shape[0], 8): for j in range(0, img.shape[1], 8): block img[i:i8, j:j8] dct cv2.dct(block) # 只改中频系数避开直流和最高频 for u in range(2, 6): for v in range(2, 6): if bit_idx len(bits): break coef int(round(dct[u, v])) coef (coef ~1) | int(bits[bit_idx]) dct[u, v] float(coef) bit_idx 1 img[i:i8, j:j8] cv2.idct(dct) cv2.imwrite(output_path, img, [cv2.IMWRITE_JPEG_QUALITY, quality]) print(f嵌入{bit_idx}位)逻辑说明按 8×8 分块做 DCT在每块的中频区域u、v 从 2 到 5改系数最低位再 IDCT 还原。中频区域是视觉敏感度和鲁棒性的折中低频改动容易被看到高频容易被量化掉。参数说明quality90是保存质量嵌入和提取要用同一个值。range(2, 6)控制用哪些系数范围越大容量越大但图像质量越差。提取时按同样顺序读系数最低位即可。注意这段代码没加长度头实际用要补上。另外 JPEG 保存本身有量化误差嵌入后重新读出来系数可能变所以提取端要用同样的 quality 重新解码。4.2 PDF 文本隐写的载体特性PDF 隐写和图像隐写是两套逻辑。PDF 是结构化文档文本、字体、对象流都可以做文章。常见手法有调整字间距、改字符编码、在对象流里插冗余数据、利用 PDF 的增量更新机制。资源里的文献《基于PDF文档作为掩体的信息隐写方法》讲的就是这类。电子发票篡改检测之所以难是因为 PDF 允许增量更新改完不重写整个文件旧内容还在但渲染时用新的。检测端要对比对象流和渲染结果找不一致的地方。课程设计里做 PDF 隐写建议从文本层入手把秘密信息编码成不可见字符零宽字符插到文本流里。零宽字符在 PDF 里不显示但提取时能读出来。缺点是容易被过滤优点是实现简单。# 零宽字符映射 ZERO_WIDTH {0: \u200b, 1: \u200c} def pdf_text_embed(text, secret): bits .join(format(b, 08b) for b in secret.encode(utf-8)) hidden .join(ZERO_WIDTH[b] for b in bits) return text hidden def pdf_text_extract(text): bits for ch in text: if ch \u200b: bits 0 elif ch \u200c: bits 1 data bytes(int(bits[i:i8], 2) for i in range(0, len(bits) - 7, 8)) return data.decode(utf-8, errorsignore)逻辑说明把秘密信息的每个比特映射成零宽字符追加到文本末尾。提取时扫描零宽字符还原比特流。参数说明\u200b是零宽空格\u200c是零宽非连接符两者在 PDF 渲染里都不可见。映射表可以换但嵌入和提取要一致。注意不是所有 PDF 阅读器都保留零宽字符有些会过滤。做课程设计演示够用真实场景要结合对象流操作。5. 避坑与排查隐写分析里最容易翻车的五个地方这套流程我前后跑过几轮下面五个坑是出现频率最高的每个都按「现象 → 原因 → 解决」写清楚。现象提取出来是乱码。原因嵌入和提取用的通道不一致或者图像被重新保存成了 JPEG。解决确认两边都用蓝色通道索引 0载体图必须存成 PNG中间不要用任何有损压缩工具打开再保存。现象CNN 训练准确率一直卡在 50%。原因正负样本标签反了或者 SRM 残差没做对输入全是零。解决先打印一批残差图的均值和方差正常应该有正有负、方差不为零。再检查 DataLoader 里标签和图像是否对应。现象嵌入后图像肉眼可见噪点。原因嵌入率太高或者改了多个通道。解决把嵌入率降到 0.3~0.4只用一个通道。如果还明显检查是不是把高位也改了掩码必须是0xFE。现象JPEG 隐写提取失败。原因保存 quality 和提取时解码 quality 不一致或者 DCT 系数取整方式不同。解决嵌入和提取用同一个 quality 值系数取整统一用round不要一个用int一个用round。现象PDF 零宽字符提取为空。原因PDF 生成工具过滤了零宽字符或者文本被重新排版。解决换一个保留原始文本流的生成方式或者改用对象流嵌入。演示时直接用 Python 写文本文件验证逻辑再套到 PDF 上。6. 进阶技巧用嵌入率扫描验证检测器灵敏度CNN 训练完怎么知道它到底行不行光看准确率不够因为正负样本比例、图像内容都会影响结果。我一般会做嵌入率扫描用同一批载体图分别按 0.1、0.2、0.3、0.4、0.5 的嵌入率生成隐写图再让检测器逐档测试。这样能看出检测器在低嵌入率下的灵敏度边界。具体做法是写一个循环每档生成一批隐写图跑推理记录准确率。下面这段代码是核心逻辑。import torch import numpy as np from sklearn.metrics import accuracy_score def scan_embedding_rate(model, cover_images, rates, devicecpu): model.eval() results {} for rate in rates: preds, labels [], [] for img in cover_images: stego embed_lsb_with_rate(img, rate) # 按嵌入率生成隐写图 for sample, label in [(img, 0), (stego, 1)]: res srm_residual_from_array(sample) tensor torch.from_numpy(res).permute(2, 0, 1).unsqueeze(0).float() with torch.no_grad(): out model(tensor.to(device)) pred out.argmax(dim1).item() preds.append(pred) labels.append(label) acc accuracy_score(labels, preds) results[rate] acc print(f嵌入率 {rate}: 准确率 {acc:.4f}) return results逻辑说明对每个嵌入率生成对应的隐写图和原图配对跑推理最后算准确率。embed_lsb_with_rate按比例随机选像素嵌入srm_residual_from_array是前面 SRM 函数的数组版本。参数说明rates一般取[0.1, 0.2, 0.3, 0.4, 0.5]。device有 GPU 就填cuda没有就cpu。准确率随嵌入率下降而下降是正常的关键是看 0.2 以下还能不能保持在 70% 以上。如果 0.3 就掉到随机水平说明网络没学到东西回去检查 SRM 输入和标签。这个扫描还有个用处对比不同网络结构。我试过把卷积层从 4 层加到 6 层高嵌入率下准确率差不多但低嵌入率反而降了原因是过拟合。所以不是越深越好隐写分析的信号太弱网络容量大了容易记住噪声。从那以后我每次训完检测器都强制走一遍嵌入率扫描不看单点准确率。希望帮到你。本文还有配套的精品资源点击获取