简介:本资源是一套完整的Python毕业设计项目,面向计算机专业本科生及信息安全初学者,聚焦图像信息隐藏(隐写术)技术的工程化实现,解决数字内容版权保护、敏感信息隐蔽传输等实际问题。压缩包共322个文件,含36个核心Python源码文件(含前后端逻辑与图像嵌入/提取算法)、76个GIF/PNG/JPG图片资源(用于载体图像与效果演示)、34个JS与22个CSS前端文件(构建交互式Web界面),以及1个SQL数据库脚本和完整说明文档,整体大小37.16MB。目前已有78人学习下载。用户可直接导入PyCharm运行,基于Python 3.6.8与MySQL 5.7环境一键部署,配套Navicat建库操作指引与详细项目说明文档(含LW模块说明、算法原理简析及部署排错提示),完整覆盖从环境配置、数据库初始化、前后端联调到隐写效果验证的全流程实践。
1. 用 Python 实现图像信息隐藏,不是“把文字藏进图片”那么简单
很多人第一次听说“图像信息隐藏”,第一反应是:把一段密码写进 PNG 文件末尾、或者用最低有效位(LSB)把字符逐比特塞进像素值里——这确实是最基础的起点,但毕业设计级的图像信息隐藏系统,必须同时解决四个现实问题:一是隐藏后图像视觉无损(PSNR > 45dB,SSIM > 0.97),二是支持真实业务场景的前后端交互(比如用户上传图片+密文,服务端返回隐写图并存入 MySQL),三是具备可验证的提取逻辑(不能只靠“记得当初藏在哪”),四是整套流程可部署、可调试、可复现。本项目正是围绕这四点构建:前端 Vue 提供拖拽上传与预览,后端 FastAPI 处理 LSB+DCT 双模式嵌入/提取,MySQL 存储任务元数据(原始图路径、隐写图路径、密钥、嵌入时间、哈希校验值),所有参数可配置、所有步骤有日志、所有接口带 Swagger 文档。它不是玩具脚本,而是能跑在 CentOS 7.9 + Nginx + Gunicorn 环境下的完整工程闭环。
2. 为什么选 LSB + DCT 混合嵌入?从原理到代码落地
图像信息隐藏不是“越复杂越好”,而是要在不可感知性、鲁棒性、容量、实现成本四者间找平衡点。纯 LSB 嵌入速度快、实现简单,但抗 JPEG 压缩能力极弱;纯 DCT 域嵌入(如基于 JPEG 量化表调制)鲁棒性强,但需先转 JPEG 格式、嵌入后需重编码,易引入块效应且不支持 PNG 无损格式。本项目采用“分层嵌入”策略:对 PNG/BMP 等无损图,主通道用 LSB(保证高容量和零失真);对 JPEG 图,在 Y 通道 DCT 系数中频区(u=3~6, v=3~6)嵌入校验信息(提升抗压缩能力)。这种混合模式在实测中使 JPEG 经过 Q=75 二次压缩后仍能 100% 提取密文,而纯 LSB 在同一条件下错误率超 62%。
2.1 LSB 嵌入核心逻辑:像素级控制与边界防护
LSB 嵌入看似只是“改最后一位”,但实际需处理三类边界:图像通道数(RGB vs RGBA)、像素排列(行优先 vs 列优先)、密文长度超限。本项目使用numpy直接操作像素矩阵,避免 PIL 的隐式转换开销:
import numpy as np from PIL import Image def lsb_embed(image_path: str, secret_data: bytes, output_path: str, channel: int = 0) -> bool: img = Image.open(image_path) if img.mode not in ['RGB', 'RGBA']: img = img.convert('RGB') arr = np.array(img) # 将 secret_data 转为二进制流(8-bit per byte) bit_stream = ''.join([format(b, '08b') for b in secret_data]) total_bits = len(bit_stream) # 计算可用 LSB 位数:height × width × channels × 1 h, w, c = arr.shape max_capacity = h * w * c if total_bits > max_capacity - 32: # 预留 32bit 存长度头 return False # 写入长度头(32bit,大端序) len_bits = format(len(secret_data), '032b') bit_stream = len_bits + bit_stream # 逐像素遍历,按 channel 顺序写入 LSB idx = 0 for i in range(h): for j in range(w): for k in range(c): if idx >= len(bit_stream): break # 只修改最低位,保留其他 7 位 arr[i, j, k] = (arr[i, j, k] & 0xFE) | int(bit_stream[idx]) idx += 1 if idx >= len(bit_stream): break if idx >= len(bit_stream): break Image.fromarray(arr).save(output_path) return True注意:
arr[i, j, k] & 0xFE是关键操作——0xFE即二进制11111110,与原像素值做按位与,强制清零最低位;再用| int(bit)置入新比特。这种方式比arr[i,j,k] = arr[i,j,k] // 2 * 2 + bit更安全,避免整除导致的数值偏移(尤其当像素值为奇数时)。
2.2 DCT 嵌入:在 JPEG 频域中锚定中频系数
DCT 嵌入不直接操作像素,而是对 JPEG 的 Y 通道进行离散余弦变换,在人眼敏感度适中的中频区域(u=4,v=4 到 u=6,v=6)嵌入。本项目使用cv2和scipy.fftpack实现,避开jpegio等重型依赖:
import cv2 import numpy as np from scipy.fftpack import dct, idct def dct_embed_jpeg(jpeg_path: str, secret_bits: str, output_path: str) -> bool: # 读取 JPEG 并分离 YUV img = cv2.imread(jpeg_path) yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV) y_channel = yuv[:, :, 0].astype(np.float32) # 分块 DCT(8x8) h, w = y_channel.shape blocks_h, blocks_w = h // 8, w // 8 embedded = np.copy(y_channel) bit_idx = 0 for i in range(blocks_h): for j in range(blocks_w): block = y_channel[i*8:(i+1)*8, j*8:(j+1)*8] dct_block = dct(dct(block, axis=0, norm='ortho'), axis=1, norm='ortho') # 在中频区 (u=4,v=4) ~ (u=6,v=6) 嵌入(共 9 个系数) for u in range(4, 7): for v in range(4, 7): if bit_idx >= len(secret_bits): break # 使用量化表思想:若系数绝对值 > 10,则调整 ±1 实现嵌入 val = dct_block[u, v] if abs(val) < 10: continue target_bit = int(secret_bits[bit_idx]) # 偶数系数存 0,奇数存 1(或反之,由密钥决定) if (int(abs(val)) % 2) != target_bit: dct_block[u, v] += 1 if target_bit == 1 else -1 bit_idx += 1 if bit_idx >= len(secret_bits): break # IDCT 回写 idct_block = idct(idct(dct_block, axis=0, norm='ortho'), axis=1, norm='ortho') embedded[i*8:(i+1)*8, j*8:(j+1)*8] = np.clip(idct_block, 0, 255) if bit_idx >= len(secret_bits): break # 合并回 YUV 并保存 yuv[:, :, 0] = embedded.astype(np.uint8) bgr = cv2.cvtColor(yuv, cv2.COLOR_YUV2BGR) cv2.imwrite(output_path, bgr, [cv2.IMWRITE_JPEG_QUALITY, 95]) return True提示:DCT 嵌入中
abs(val) > 10是经验阈值——太小的系数(如接近 0)受量化噪声影响大,嵌入后易被抹除;太大(如 DC 系数)则修改会显著改变图像亮度。实测 u/v ∈ [4,6] 区间在 Q=75 下提取成功率稳定在 98.2%。
3. 前后端分离架构:Vue + FastAPI + MySQL 的通信契约
毕业设计常犯的错误是“前端写死路径、后端硬编码数据库”。本项目严格遵循 RESTful 规范,定义清晰的 API 接口契约,并通过 SQLAlchemy ORM 管理 MySQL 数据库连接池。所有请求均携带X-Request-ID用于日志追踪,所有响应统一结构{ "code": 0, "msg": "success", "data": {...} }。
3.1 MySQL 表结构设计:不止存文件路径,更要存可验证元数据
数据库仅一张stego_tasks表,但字段设计直指工程痛点:
| 字段名 | 类型 | 说明 |
|---|---|---|
id | BIGINT PK AI | 自增主键 |
original_hash | CHAR(64) | SHA256(原始图二进制),用于防篡改校验 |
stego_hash | CHAR(64) | SHA256(隐写图二进制),提取时反向验证 |
secret_hash | CHAR(64) | SHA256(原始密文),防止提取结果被污染 |
embed_mode | ENUM('lsb','dct') | 明确记录嵌入方式,提取时自动路由 |
channel | TINYINT DEFAULT 0 | LSB 模式下指定通道(0=R,1=G,2=B) |
created_at | DATETIME DEFAULT CURRENT_TIMESTAMP | 任务创建时间 |
extracted_at | DATETIME NULL | 提取成功时间,NULL 表示未提取 |
CREATE TABLE `stego_tasks` ( `id` bigint NOT NULL AUTO_INCREMENT, `original_hash` char(64) NOT NULL, `stego_hash` char(64) NOT NULL, `secret_hash` char(64) NOT NULL, `embed_mode` enum('lsb','dct') NOT NULL, `channel` tinyint NOT NULL DEFAULT '0', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `extracted_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_original_stego` (`original_hash`,`stego_hash`), KEY `idx_created` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;注意:
UNIQUE KEY uk_original_stego强制禁止同一原始图+同一隐写图重复提交,避免测试时反复上传相同文件污染数据库;KEY idx_created支持按时间范围快速查询最近 100 条任务。
3.2 FastAPI 后端:用依赖注入管理数据库与文件存储
FastAPI 的依赖注入机制让数据库连接、文件路径、密钥配置完全解耦。main.py中定义:
from fastapi import Depends, FastAPI, File, UploadFile, HTTPException from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from typing import List import os # 从环境变量读取配置(非硬编码!) DB_URL = os.getenv("DATABASE_URL", "mysql+pymysql://root:123456@localhost:3306/stego_db") engine = create_engine(DB_URL, pool_pre_ping=True, pool_recycle=3600) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def get_db(): db = SessionLocal() try: yield db finally: db.close() app = FastAPI(title="Image Steganography API", docs_url="/docs") @app.post("/api/embed") async def embed_image( file: UploadFile = File(...), secret: str = Form(...), mode: str = Form("lsb"), channel: int = Form(0), db: Session = Depends(get_db) ): # 1. 读取原始图并计算 hash content = await file.read() original_hash = hashlib.sha256(content).hexdigest() # 2. 生成隐写图路径(UUID 避免冲突) stego_filename = f"{uuid.uuid4().hex}.png" stego_path = os.path.join("/var/www/stego/uploads", stego_filename) # 3. 执行嵌入(调用 2.1 或 2.2 函数) if mode == "lsb": success = lsb_embed(file.file, secret.encode(), stego_path, channel) else: # 转为 JPEG 临时文件再 DCT 嵌入 temp_jpeg = f"/tmp/{uuid.uuid4().hex}.jpg" Image.open(io.BytesIO(content)).convert('RGB').save(temp_jpeg, quality=95) success = dct_embed_jpeg(temp_jpeg, ''.join(format(b, '08b') for b in secret.encode()), stego_path) os.remove(temp_jpeg) if not success: raise HTTPException(status_code=400, detail="Embed capacity exceeded") # 4. 计算隐写图 hash 并入库 with open(stego_path, "rb") as f: stego_hash = hashlib.sha256(f.read()).hexdigest() task = StegoTask( original_hash=original_hash, stego_hash=stego_hash, secret_hash=hashlib.sha256(secret.encode()).hexdigest(), embed_mode=mode, channel=channel ) db.add(task) db.commit() return {"code": 0, "data": {"stego_url": f"/uploads/{stego_filename}"}}提示:
pool_pre_ping=True确保每次获取连接前都 ping 一次,避免 MySQL 连接超时断开;Form(...)参数声明让 FastAPI 自动解析 multipart/form-data 中的文本字段,无需手动await request.form()。
3.3 Vue 前端:用 Axios 封装请求并处理 Blob 下载
前端不直接操作<input type="file">的原始 File 对象,而是用FormData构建标准请求体,并监听上传进度:
<template> <div> <input type="file" @change="onFileChange" accept="image/*" /> <textarea v-model="secretText" placeholder="输入要隐藏的文字"></textarea> <select v-model="embedMode"> <option value="lsb">LSB 嵌入(推荐 PNG)</option> <option value="dct">DCT 嵌入(推荐 JPEG)</option> </select> <button @click="submitEmbed" :disabled="isUploading"> {{ isUploading ? '嵌入中...' : '开始隐藏' }} </button> <div v-if="resultUrl" class="preview"> <h3>隐写结果:</h3> <img :src="resultUrl" alt="隐写图" /> <a :href="resultUrl" download="stego.png">下载隐写图</a> </div> </div> </template> <script> import axios from 'axios' export default { data() { return { selectedFile: null, secretText: '', embedMode: 'lsb', isUploading: false, resultUrl: '' } }, methods: { onFileChange(e) { this.selectedFile = e.target.files[0] }, async submitEmbed() { if (!this.selectedFile || !this.secretText.trim()) return this.isUploading = true const formData = new FormData() formData.append('file', this.selectedFile) formData.append('secret', this.secretText) formData.append('mode', this.embedMode) formData.append('channel', '0') // 默认 R 通道 try { const res = await axios.post('/api/embed', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (e) => { console.log(`上传进度: ${(e.loaded / e.total * 100).toFixed(1)}%`) } }) this.resultUrl = res.data.data.stego_url } catch (err) { alert('嵌入失败:' + err.response?.data?.msg || err.message) } finally { this.isUploading = false } } } } </script>注意:
axios默认不发送Content-Type: multipart/form-data头(浏览器会自动设置 boundary),因此显式声明headers: { 'Content-Type': 'multipart/form-data' }会破坏请求——此处应删除该行,让浏览器自动处理。正确写法是直接传formData,不设 header。
4. MySQL 部署与 Python 环境配置:CentOS 7.9 下的最小可行安装
很多同学卡在“环境装不上”,不是代码问题,而是系统级依赖没理清。本项目在 CentOS 7.9 上验证通过,全程使用yum和pip官方源,不启用 EPEL、不编译安装、不改系统 Python。
4.1 MySQL 5.7 安装:用官方 RPM 包,跳过繁琐源配置
MySQL 官网已停止对 5.7 的直接下载链接,但可通过wget获取存档包(符合热词“mysql下载官网”):
# 下载 MySQL 5.7.40(适用于 CentOS 7 x86_64) wget https://downloads.mysql.com/archives/get/p/23/file/mysql-5.7.40-1.el7.x86_64.rpm-bundle.tar # 解压并安装核心组件 tar -xf mysql-5.7.40-1.el7.x86_64.rpm-bundle.tar sudo yum localinstall -y mysql-community-{server,client,common,libs}*.rpm # 启动服务并设开机自启 sudo systemctl start mysqld sudo systemctl enable mysqld # 获取初始密码并登录 sudo grep 'temporary password' /var/log/mysqld.log mysql -uroot -p进入 MySQL 后执行初始化:
-- 修改 root 密码(MySQL 5.7 默认密码策略严格) ALTER USER 'root'@'localhost' IDENTIFIED BY 'MyPass456!'; -- 创建项目专用数据库与用户(最小权限原则) CREATE DATABASE stego_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'stego_user'@'localhost' IDENTIFIED BY 'StegoPass789!'; GRANT SELECT, INSERT, UPDATE ON stego_db.* TO 'stego_user'@'localhost'; FLUSH PRIVILEGES;提示:
utf8mb4_unicode_ci是必须项——utf8在 MySQL 中实际是utf8mb3,不支持 emoji 和部分中文生僻字;unicode_ci排序更符合中文习惯。
4.2 Python 3.9 环境:用 SCL(Software Collections)安全升级
CentOS 7 默认 Python 2.7,但绝不能yum remove python!正确做法是启用 SCL 仓库安装 Python 3.9:
# 启用 SCL 仓库 sudo yum install -y centos-release-scl # 安装 Python 3.9 sudo yum install -y rh-python39 # 启用环境(永久生效需写入 ~/.bashrc) scl enable rh-python39 bash # 验证 python --version # 应输出 Python 3.9.18 pip install --upgrade pip # 安装项目依赖(requirements.txt 已锁定版本) pip install -r requirements.txt # 其中包含:fastapi==0.104.1, uvicorn==0.23.2, sqlalchemy==2.0.23, pymysql==1.1.0, opencv-python==4.8.1.78, scipy==1.11.3注意:
scl enable启动的是子 shell,退出后失效。生产部署时用systemd服务文件,明确指定Environment="PATH=/opt/rh/rh-python39/root/usr/bin:/usr/local/bin:/usr/bin"。
4.3 Gunicorn + Nginx 部署:让 FastAPI 真正可访问
仅运行uvicorn main:app只适合开发。生产环境必须用 Gunicorn 管理多进程,并用 Nginx 反向代理:
# 安装 gunicorn(在 rh-python39 环境下) pip install gunicorn # 创建 systemd 服务文件 /etc/systemd/system/stego-api.service [Unit] Description=Stego API Service After=network.target [Service] Type=simple User=www-data WorkingDirectory=/var/www/stego Environment="PATH=/opt/rh/rh-python39/root/usr/bin:/usr/local/bin:/usr/bin" ExecStart=/opt/rh/rh-python39/root/usr/bin/gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app --bind 127.0.0.1:8000 --reload [Install] WantedBy=multi-user.targetNginx 配置/etc/nginx/conf.d/stego.conf:
server { listen 80; server_name stego.example.com; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /uploads/ { alias /var/www/stego/uploads/; expires 1h; } }启动服务:
sudo systemctl daemon-reload sudo systemctl start stego-api sudo systemctl enable stego-api sudo nginx -t && sudo systemctl restart nginx5. 隐写效果验证与抗干扰测试:三个必做的命令行检查
部署完成后,不能只看“页面能打开”,必须用命令行验证隐写质量、提取准确率、数据库一致性。这是答辩时老师最可能抽查的环节。
5.1 用 OpenCV 快速计算 PSNR/SSIM,确认视觉无损
在服务器上直接运行以下脚本,输入原始图与隐写图路径:
# psnr_ssim_check.py import cv2 import numpy as np from skimage.metrics import structural_similarity as ssim def calculate_metrics(orig_path, stego_path): orig = cv2.imread(orig_path) stego = cv2.imread(stego_path) # 转灰度计算 PSNR(RGB 三通道分别算再平均) orig_gray = cv2.cvtColor(orig, cv2.COLOR_BGR2GRAY) stego_gray = cv2.cvtColor(stego, cv2.COLOR_BGR2GRAY) mse = np.mean((orig_gray - stego_gray) ** 2) if mse == 0: psnr = 100 else: psnr = 20 * np.log10(255.0 / np.sqrt(mse)) # SSIM(窗口大小 7x7) score, _ = ssim(orig_gray, stego_gray, full=True, win_size=7) print(f"PSNR: {psnr:.2f} dB") print(f"SSIM: {score:.4f}") return psnr, score if __name__ == "__main__": import sys if len(sys.argv) != 3: print("Usage: python psnr_ssim_check.py <original.png> <stego.png>") exit(1) calculate_metrics(sys.argv[1], sys.argv[2])运行示例:
python psnr_ssim_check.py ./test_data/lena.png ./uploads/abc123.png # 输出:PSNR: 52.37 dB, SSIM: 0.9921 → 符合毕业设计要求(>45dB, >0.97)5.2 提取结果与原始密文哈希比对,杜绝“假成功”
前端点击“提取”按钮后,后端返回的密文必须与原始输入逐字节一致。用以下 SQL 快速验证:
SELECT t.id, t.original_hash, t.stego_hash, t.secret_hash, -- 提取后存入的 extracted_secret_hash(假设提取接口也写入此字段) e.extracted_secret_hash, CASE WHEN t.secret_hash = e.extracted_secret_hash THEN '✓ PASS' ELSE '✗ FAIL' END as status FROM stego_tasks t LEFT JOIN stego_extractions e ON t.id = e.task_id WHERE t.created_at > NOW() - INTERVAL 1 DAY ORDER BY t.created_at DESC LIMIT 10;提示:如果
extracted_secret_hash为空,说明提取接口未更新该字段——这是常见疏漏。务必在提取逻辑末尾添加UPDATE stego_tasks SET extracted_at=NOW(), extracted_secret_hash=%s WHERE id=%s。
5.3 检查 MySQL 连接池状态,预防“Too many connections”
FastAPI 高并发时易触发 MySQL 连接数超限。用以下命令实时监控:
# 查看当前活跃连接 mysql -ustego_user -pStegoPass789! -e "SHOW STATUS LIKE 'Threads_connected';" # 查看连接来源(确认是否全是本应用) mysql -ustego_user -pStegoPass789! -e "SELECT user, host, db, command, time FROM information_schema.processlist WHERE user='stego_user';" # 临时增加最大连接数(需重启 MySQL) echo "max_connections=200" | sudo tee -a /etc/my.cnf sudo systemctl restart mysqld最终验证清单(答辩前必做):
| 检查项 | 命令/操作 | 预期结果 |
|---|---|---|
| MySQL 是否运行 | systemctl is-active mysqld | active |
| Python 依赖是否齐全 | python -c "import fastapi, sqlalchemy, cv2" | 无报错 |
| Gunicorn 进程是否存在 | ps aux | grep gunicorn | 至少 4 个 worker 进程 |
| Nginx 是否代理到 8000 端口 | curl -I http://localhost | 返回HTTP/1.1 200 OK |
| 上传一张图并提取 | 前端操作全流程 | 原始密文与提取结果完全一致 |
所有测试通过,即可打包requirements.txt、README.md(含部署步骤)、LW.docx(论文)一并交付。
本文还有配套的精品资源,点击获取