
简介面向计算机科学与技术专业学生的图像信息隐藏技术毕设/课设项目基于Python与Django实现图像秘密信息嵌入与提取功能。资源打包为zip格式大小38.02MB内含项目部署说明、说明文档及Django项目核心代码文件夹覆盖前后端界面、后端处理逻辑与数据库操作等模块。目前已有66人浏览学习。该项目核心采用PIL、OpenCV等图像处理库通过像素值微调实现信息不可见嵌入与提取可应用于版权保护、数据认证等场景同时利用Django框架构建Web应用结合ORM与SQL数据库存储用户图像及数据。配套说明文档详细阐述项目背景、系统架构、功能模块与使用方法部署说明则指导快速搭建本地环境适合毕设参考、课设复现及Python综合开发能力提升。1. 图像信息隐藏技术为什么值得选作Python毕业设计图像信息隐藏技术这个选题每年都有不少学生选但很多人做到最后只是在图上盖了个可见水印和密码学没什么关系。真正的图像信息隐藏是把一段秘密数据嵌进一张看起来正常的图像像素中让人的肉眼和常见统计工具都看不出异常。它和加密的区别在于加密让数据不可读隐写让数据不可见。作为 Python 毕业设计它既能覆盖图像处理、位运算、密码学基础又能做到让评委看到可量化的隐藏效果工作量适中论文也有清晰的指标可写。下面会以最常见的 LSB 算法作为主线从像素原理、Python 实现、加密增强到源码 zip 包的整理与验证完整走一遍。适合正在选这类题目的读者也适合已经把源码包下载回来却不知道从哪下手的同学。2. 图像信息隐藏基础与LSB算法在Python中的落地2.1 像素、位平面与LSB替换先算清楚代价一张 RGB 图像在 Python 里就是三维数组高、宽、三个通道。每个通道的值范围是 0 到 255写成二进制正好 8 位。这 8 位从低到高分别称为第 0 位到第 7 位越高位改动后对像素值影响越大。第 0 位是 0 或 1 时像素值只波动 1/255第 1 位波动 2第 2 位波动 4第 7 位则是 128。LSB 算法取的就是最低有效位Least Significant Bit把秘密数据的每个比特直接替换掉载体图像某些像素通道的第 0 位使图像失真控制在人眼几乎无法察觉的范围。为了直观理解这里把不同位平面改动后的效果列出。编程中经常会先思考“改这一位要付出多少可见度”这个表格就是最直接的成本表。位平面通道像素值波动视觉失真抗有损压缩能力隐写中的使用建议第 0 位0/1肉眼基本不可见弱JPEG 保存后容易被破坏LSB 首选第 1 位0/2很轻微弱可结合纠错码使用第 2 位0/4灰度渐变处可见较弱不推荐单独使用第 7 位0/128明显色块强完全不用于隐藏在动手前还要选好 Python 运行环境。如果机器上还没有解释器先按 Python 安装教程装好 3.9 以上版本如果用的是 PyCharm注意在 pycharm 配置 python 环境时选到实际解释器路径否则命令行和 IDE 里容易各用各的版本。之后安装两个依赖就够了Pillow 负责读图NumPy 负责把像素批量转成数组。用代码做一件事之前先用数组形态理解它。读取一张彩色图会得到(H, W, 3)的数组读取灰度图则得到(H, W)。设计 LSB 算法时我一般先把图像统一转成 RGB再决定把秘密数据嵌入到哪个通道。选择红色通道是常见的毕业设计做法因为红色通道分布相对平稳视觉上对最低位变化不敏感也可以把三个通道都利用起来提高容量代价是某些带透明通道的 PNG 在保存时会产生额外复杂度。2.2 用 Python 写最小可运行的 LSB 嵌入与提取下面是一组可以直接保存运行的最小实现。先写文本转比特的函数为了提取时能知道秘密数据长度我在前 32 个比特写入一个 4 字节大端整数表示后续内容的字节数。import numpy as np from PIL import Image MSG_HEADER_BITS 32 def _text_to_bits(text: str) - str: data text.encode(utf-8) header len(data).to_bytes(4, byteorderbig) payload header data return .join(f{byte:08b} for byte in payload) def embed_lsb(cover_path: str, secret_text: str, output_path: str, channel: int 0) - int: img np.array(Image.open(cover_path).convert(RGB), dtypenp.uint8) bits _text_to_bits(secret_text) total_pixels img.shape[0] * img.shape[1] if len(bits) total_pixels: raise ValueError(f载体容量不足需要 {len(bits)} 位图片只有 {total_pixels} 位) plane img[..., channel].reshape(-1) for i, bit in enumerate(bits): plane[i] (plane[i] 0b11111110) | int(bit) img[..., channel] plane.reshape(img.shape[:2]) Image.fromarray(img).save(output_path) return len(bits) def extract_lsb(stego_path: str, channel: int 0) - str: img np.array(Image.open(stego_path).convert(RGB), dtypenp.uint8) plane img[..., channel].reshape(-1) head_bits .join(str(plane[i] 1) for i in range(MSG_HEADER_BITS)) byte_len int(head_bits, 2) payload_bits .join(str(plane[MSG_HEADER_BITS i] 1) for i in range(byte_len * 8)) payload int(payload_bits, 2).to_bytes(byte_len, byteorderbig) return payload.decode(utf-8)这段代码的核心是plane[i] (plane[i] 0b11111110) | int(bit)。 0b11111110把最低位清零| int(bit)再把秘密比特放进去。为什么用按位操作而不是 bit因为如果最低位原先是 1 且秘密比特是 1加法会把进位带到高一位破坏原始像素值。按位操作保证只变更第 0 位。参数表的含义要写清楚便于直接改参数含义建议取值channel嵌入使用的颜色通道0 为 R1 为 G2 为 B毕业答辩通常固定 0secret_text要藏入的文本中文会按 UTF-8 转成多字节先算好容量output_path生成隐写图路径建议保存为 PNG避免重新编码破坏 LSB返回值实际写入的比特数等于32 8 * 文本字节数提取时先用前 32 个像素的最低位拼出byte_len再按这个长度取后续比特。注意这里默认载体图像在嵌入后没有做过缩放、剪裁或转 JPEG否则最低位信息会大量丢失。运行最小示例时可以直接在项目根目录执行下面两条命令分别完成嵌入和提取。python embed_lsb.py cover.png 这是隐藏信息 stego.png python extract_lsb.py stego.png如果上一次嵌入后的图像被保存成了 JPG第二次提取往往会得到乱码。原因是 JPEG 是有损压缩会把最低位的信号当作噪声丢掉。这不是算法写错而是载体格式选错。我一直坚持在毕业设计里用 PNG 作为隐写图的唯一输出格式并在 README 里用一行字写清楚嵌入后的图像不要再次经过有损压缩。3. 给LSB加上加密与随机选点从“藏得住”到“搜不到”3.1 为什么明文LSB扛不住检测前面的顺序嵌入实现在代码量上是最短的但安全性也是最弱的。当我用统计工具去分析一张隐写图时会发现最低位不再服从自然图像的随机分布而是跟随秘密信息出现规律性。原因很简单顺序嵌入会把前 N 个像素的最低位全部改成秘密比特如果秘密信息本身有大量重复字节图像低位会出现明显统计偏置即使秘密信息做了压缩固定位置嵌入也会让图片左上角承担全部修改局部噪声偏高。解决思路有两条一是让秘密信息先加密让密文看起来像随机字节二是打乱嵌入位置让修改均匀分布到全图。两者结合后隐写图的最低位统计会和自然噪声更接近常规检测工具会明显失效。这一层是毕业设计拉开档次的关键也是很多答辩老师一定会追问的“你怎么抵抗隐写分析”。实现上我建议把加密和选点拆成两个独立模块。加密负责把明文变成不可读且变长的数据包选点负责决定这些数据包里的比特落在图像的哪些像素上。两个模块互不依赖论文里可以分别写章节测试也能单独做。3.2 使用AES-GCM先加密再嵌入加密不是为了隐藏存在而是为了让秘密数据在嵌入前不再有明显结构。常见的对称加密即可我一般用pycryptodome的 AES 模式因为它同时提供加密和完整性认证。下面的函数把原始字节封装成nonce tag ciphertext三段接收方必须同时拥有正确密钥才能同时完成解密和校验。from Crypto.Cipher import AES from Crypto.Random import get_random_bytes def secret_encrypt(raw: bytes, key: bytes) - bytes: nonce get_random_bytes(12) cipher AES.new(key, AES.MODE_EAX, noncenonce) ciphertext, tag cipher.encrypt_and_digest(raw) return nonce tag ciphertext def secret_decrypt(pkg: bytes, key: bytes) - bytes: nonce pkg[0:12] tag pkg[12:28] ciphertext pkg[28:] cipher AES.new(key, AES.MODE_EAX, noncenonce) return cipher.decrypt_and_verify(ciphertext, tag)使用MODE_EAX时nonce是随机生成的 12 字节初始向量tag是 16 字节认证标签。每次加密都换 nonce因此同样的明文在不同时刻加密后得到的密文不同。提取端拿到数据包后先切出 nonce 和 tag再解密并验证。如果载体图像被人为拉伸、裁剪并重新保存解密就会报ValueError: MAC check failed这实际上是给隐写方案加了一层完整性保护。将这段代码接入 LSB 的流程是先调用secret_encrypt(secret_text.encode(utf-8), key)得到密文再把密文的每个字节按 8 位拆开嵌入图像提取时先从图像中取回足够长度的字节拼接还原成数据包再调用secret_decrypt。这里的key建议从用户口令派生不要直接拿口令字符串作为 AES 密钥。常见做法是import hashlib key hashlib.sha256(password.encode(utf-8)).digest()需要说明的是加密后数据包比原文多出 28 字节如果原文是 500 字节那么需要多嵌 224 个比特。对一张 1024x1024 的图来说完全无感但对 64x64 的小图就要提前计算容量。我在毕业设计里会把“嵌入开销”单独做成一个表格写进论文说明加密对容量的影响。3.3 用密钥种子做随机选点避免固定区域痕迹顺序嵌入的问题在于位置可预测。改为随机选点后同样的秘密信息会被散布到整张图片中单点区域的人为修改密度下降。选点算法不复杂关键是接收方需要能重建相同的顺序import random def shuffled_indices(total: int, seed: bytes) - list: rng random.Random(seed) idx list(range(total)) rng.shuffle(idx) return idxseed用派生的 AES 密钥或者口令哈希值都可以。嵌入时先拿到idx再按idx指定的顺序写比特idx shuffled_indices(plane.size, key) for i, bit in enumerate(bits): p idx[i] plane[p] (plane[p] 0b11111110) | int(bit)提取时使用相同的key和plane.size生成同样的idx然后按顺序读。注意plane.size发生变化例如图像被缩放时洗牌结果会完全不同。因此协议里必须固定只从原始尺寸图像上提取这也是为什么要在 README 里强调“隐写图不可缩放、不可裁剪”。随机选点和顺序嵌入在视觉上没有显著差异但在抗检测上有本质区别顺序嵌入的修改集中在图像左上角统计窗口一扫描就能发现随机选点则把错误分布到全图局部噪声维度更接近自然图像。对论文来说可以在相同嵌入量下分别生成两种隐写图再用卡方检测工具比较 p 值随机选点的 p 值通常更高说明异常更不显著。下面这个表可以用在论文的对比实验里。方案嵌入位置平均视觉损失最低位统计异常工程复杂度明文 LSB顺序低明显低加密 LSB顺序低中中加密 随机选点 LSB伪随机分布低不明显中有人会问为什么不直接使用secrets模块生成随机位置而要用random.Random因为secrets的随机性不可复现接收方无法知道同一把种子生成的位置random.Random虽然不面向密码学安全但这里只负责位置扰动真正的安全由 AES 保证因此可控复现比不可预测更重要。答辩时如果能说出这一层取舍会比单纯说“我加了密”更有说服力。4. 把毕业设计源码整理成可交付的zip包目录、依赖与排错4.1 先读zip再谈运行检查包完整性和压缩路径从标题看最终交付物是一个 zip 压缩包里面是 Python 源码。很多同学拿到压缩包后第一件事是解压然后双击 main.py报错后才发现文件损坏。我的习惯是先把它当作一个独立对象检查一遍。Python 自带的zipfile模块可以做三件事判断是不是有效 zip、列出成员名、校验每个文件的 CRC。import zipfile def check_zip(path): if not zipfile.is_zipfile(path): print(不是有效 zip 文件) return with zipfile.ZipFile(path) as zf: for name in zf.namelist(): if name.startswith(/) or .. in name: print(f可疑路径{name}) bad zf.testzip() if bad: print(fCRC 校验失败{bad}) else: print(zip 完整性检查通过) zf.extractall(extracted)is_zipfile会查找 zip 结构尾部的 End of Central Directory 记录如果报invalid zip archive: could not find eocd通常意味着文件被截断或者文件根本不是 zip 而是改了个后缀。我的处理方法是先用 7-Zip 打开一次能够在资源管理器中看到目录树再谈运行。testzip()会返回 CRC 校验失败的文件名返回None时代表所有文件都能解压。安全方面也要注意路径穿越某些压缩包会在成员名里写入../../evil.py按传统extractall解压时可能写到目标目录之外。上面的check_zip会把包含..的成员打印出来实践里我会直接过滤这类文件只解压安全的成员。这一点在毕业设计答辩中可以作为“工程化细节”提一下。注意解压前先检查 zip 成员名避免路径穿越写入到项目目录外。4.2 源码包的合理目录结构与requirements.txt一个能拿得出手的毕业设计源码不能把所有代码堆在一个.py文件里。常见的做法是按功能拆成四层入口、核心算法、界面、测试。下面是一个可以直接对照的结构image_hiding/ ├── main.py ├── core/ │ ├── __init__.py │ ├── lsb.py │ ├── crypto_io.py │ └── protocols.py ├── ui/ │ ├── __init__.py │ └── terminal_ui.py ├── tests/ │ ├── __init__.py │ ├── test_lsb.py │ └── test_crypto.py ├── requirements.txt └── README.mdlsb.py放嵌入和提取函数crypto_io.py放 AES 封装和口令派生protocols.py定义头部格式、随机选点协议tests/里面是用pytest写的回归测试。依赖清单建议锁定到主版本号不写死到 patch否则换一台机器容易装不上Pillow9.0 numpy1.21 pycryptodome3.15 pytest7.0入口文件main.py用argparse提供命令行参数而不是让用户手动改代码。最小设计如下import argparse def build_parser(): parser argparse.ArgumentParser(description图像信息隐藏命令行工具) parser.add_argument(mode, choices[embed, extract], help模式embed 嵌入extract 提取) parser.add_argument(-c, --cover, help载体图像路径) parser.add_argument(-s, --secret, help要隐藏的文本) parser.add_argument(-k, --key, help加密口令) parser.add_argument(-o, --output, defaultstego.png, help输出隐写图路径) return parser.parse_args()参数mode是必选位置参数用来区分是向载体里藏信息还是从隐写图里取信息。-c、-s、-k分别对应输入、秘密、密钥-o指定输出位置。一次完整的使用流程如下python -m venv .venv .venv/Scripts/activate pip install -r requirements.txt python main.py embed -c cover.png -s 机密信息 -o stego.png python main.py extract -c stego.png -k mypassword在 Linux 上把激活命令换成source .venv/bin/activate。如果机器本身没有 Python先按对应系统的 python 安装教程装好再继续。我看到不少同学习惯在网上搜“免费python源码大全”下载一堆 100 个 python 实战项目压缩包其实不如把自己要交的这一套目录整理干净。4.3 运行的三条命令与高频报错对照运行中遇到最多的问题往往集中在依赖安装和图像尺寸上。我整理了一份高频报错对照表直接放到论文附录也够用。报错信息常见原因处理步骤invalid zip archive: could not find eocdzip 下载不完整或后缀伪装用check_zip检测或 7-Zip 重新打开ModuleNotFoundError: No module named PIL依赖未安装或未进入虚拟环境激活.venv后执行pip install -r requirements.txtValueError: 载体容量不足秘密文本超过可嵌入位数换高分辨率图、改用三通道或先压缩文本ValueError: MAC check failed密钥错误或图像被改过确认口令重新使用原始隐写图UnicodeDecodeError: utf-8 codec cant decode byte提取到错误长度或图像损坏检查头部长度是否读取正常必要时打印前 32 位最后再提一句 zip 密码的问题。如果源码包是老师加密后分发的直接找老师要密码如果是自己加密后忘了密码优先找回备份文件。网上常见的“zip压缩包密码移除”或“zip压缩包密码破解工具”对现代压缩算法来说暴力破解几乎不可行不要在答辩前浪费时间。技术上可以使用zipfile的pwd参数完成加密 zip 的解压但前提是你本来就拥有正确密码。5. 验证论文结论PSNR、嵌入容量与误码率5.1 用PSNR量化隐蔽性隐蔽性不能只靠肉眼说“看起来差不多”要用峰值信噪比量化。两张尺寸相同的图像PSNR 越高表示修改越少。计算方式很简单import numpy as np def calc_psnr(original, modified): if original.shape ! modified.shape: raise ValueError(图像尺寸不一致) mse np.mean((original.astype(np.float32) - modified.astype(np.float32)) ** 2) if mse 0: return float(inf) return 10 * np.log10(255.0 ** 2 / mse)当嵌入了 100 个汉字大约 800 字节后512x512 的 RGB 图只用一个通道PSNR 通常还在 48dB 以上。经验上高于 40dB 就能说视觉不可感知。5.2 嵌入容量估算与隐藏头部的开销容量公式为可用字节 (通道像素数 - 32) / 8。下表是常见的三档尺寸方便论文实验设计时直接引用。图像尺寸单通道可用空间三通道可用空间128x128约 2.02 KB约 6.14 KB512x512约 32.51 KB约 98.30 KB1024x1024约 130.98 KB约 393.22 KB计算时注意头部的 32 位只会占用 4 个像素几乎可以忽略但要在公式里体现不然和实际嵌入结果会对不上。5.3 误码率测试与答辩现场演示技巧误码率衡量的是“提取出来的比特和原秘密比特有多少不一致”。在无攻击、PNG 无失真保存时误码率为 0一旦保存为 JPEG误码率会迅速上升。演示时可以用一个小脚本统计def bit_error_rate(original_bits, extracted_bits): if len(original_bits) ! len(extracted_bits): return 1.0 err sum(a ! b for a, b in zip(original_bits, extracted_bits)) return err / len(original_bits)毕业设计答辩的演示我建议准备一个失败用例选一张 32x32 的小图嵌入一段明显的长文本让程序报出“载体容量不足”。这个报错比一张成功图更能说明你对容量边界有数也能引出对载体尺寸和通道选择的讨论。本文还有配套的精品资源点击获取