尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

DWT-DCT-SVD鲁棒水印与deflate压缩:从原理到PyQt工程实践

DWT-DCT-SVD鲁棒水印与deflate压缩:从原理到PyQt工程实践 简介这是一份基于DWT-DCT-SVD与deflate压缩的数字水印完整实现含Python源码、GUI界面和演示视频主要面向高校计算机、人工智能、通信工程等专业需要完成图像安全类毕业设计或课程设计的学生。项目采用离散小波、离散余弦与奇异值分解融合的水印嵌入策略并结合deflate压缩优化秘密信息载荷核心模块涵盖水印嵌入/提取、常见攻击测试、盲水印恢复、秘密信息压缩/旋转/格式转换/尺寸调整等GUI可一键调用。压缩包共78个文件包括17个Python脚本、41张样例图片、9张PNG素材、UI界面定义文件、图标及演示视频整体约100.76MB并附有README和许可证说明目录结构清晰。已有146人学习下载代码运行稳定适合二次开发与答辩演示也可作为数字水印方向课题的完整参考。1. 基于DWT-DCT-SVD的鲁棒水印方案为什么deflate压缩能成为第四块拼图图像水印领域里DWT-DCT-SVD是常被拿来做论文组合的老三样但大部分实现止步于在Lena图上跑个PSNR就交差。这个项目把三者串成一条完整的嵌入提取链路还额外引入deflate压缩来压缩水印负载再做了一套PyQt可视化界面和批量处理管线属于能直接拿去答辩演示的完整工程。它解决的问题很具体当水印本身是一张图像甚至一段文本时如何在不破坏载体视觉质量的前提下把水印数据量压小、塞进SVD奇异值里同时保持对JPEG压缩、旋转、缩放等常见攻击的鲁棒性。适合正在做数字水印方向毕设、或者想快速搭建一套可演示水印系统的同学深入研究。2. 水印框架的技术拆解DWT分频、DCT分块、SVD奇异值嵌入与deflate预处理2.1 三种变换的职责划分与串联逻辑DWT、DCT、SVD在水印系统里的角色不是并列的而是逐层递进的关系。DWT首先把载体图像分解成LL、LH、HL、HH四个子带其中LL是低频近似分量集中了图像大部分能量人眼对它的改动最不敏感但恰恰是抗压缩攻击的关键区域。常见做法是选择对LL子带做后续处理因为JPEG压缩主要丢弃高频信息水印嵌入在低频段能保留更长时间。DCT的作用是把LL子带进一步分块处理。将LL子带划分成8×8或16×16的小块对每个块单独做离散余弦变换得到频率系数矩阵。这个步骤的核心目的是把能量集中到少数低频系数上便于后续SVD分解时提取出稳定的奇异值。DCT分块还有一个隐含优势它能天然适配JPEG的编码流程让水印的嵌入位置和压缩算法的处理单元对齐。SVD则是整个嵌入机制的核心承载者。对每个DCT系数块做奇异值分解得到U、S、V三个矩阵其中S矩阵对角线上的奇异值代表矩阵的固有属性。把水印比特调制进奇异值时对图像的几何结构影响最小而且奇异值具有较好的数值稳定性即使图像经过一定程度的攻击奇异值的变化幅度也有限。这就是为什么SVD水印方案的鲁棒性普遍优于直接在空域修改像素的方案。deflate压缩在整个链条中的位置容易被人忽略但实际上它解决了一个被很多水印论文回避的问题水印数据量。当水印是二值图像或文本时原始数据可能达到几KB甚至几十KB直接嵌入会导致载体图像出现肉眼可见的失真。deflate算法作为无损压缩手段能在嵌入前显著压缩水印负载减少需要写入奇异值的比特数。Python的zlib库原生支持deflate调用成本极低。2.2 deflate压缩与容量-鲁棒性权衡水印容量和鲁棒性是一对天然矛盾。嵌入的信息量越大对载体的修改就越剧烈视觉质量下降越明显同时单位比特的嵌入强度被稀释抗攻击能力反而会变弱。通过deflate压缩水印数据等于在不牺牲嵌入强度的前提下提升了有效容量。具体的压缩收益取决于水印内容的冗余度。二值水印图像的行程冗余和结构冗余都很高deflate一般能达到50%以上的压缩率。文本水印的压缩效果更夸张重复字符和常见词序列会让压缩率达到70%以上。这意味着原本需要嵌入1000个比特的水印压缩后只需要嵌入300个比特载体图像的峰值信噪比PSNR能得到明显提升。不过要注意deflate压缩不是免费的。压缩后的水印数据对比特错误更加敏感因为压缩编码的熵编码部分Huffman编码对单个比特的错误有放大效应。一个比特翻转变成了不同的码字解压出来的内容可能面目全非。所以工程上要在水印数据前加校验信息或者在嵌入时增加冗余编码如重复嵌入多次取多数表决来补偿。import zlib import numpy as np from scipy.fftpack import dct, idct import pywt # 水印数据预处理归一化 - 二值化 - deflate压缩 def preprocess_watermark(watermark_gray, threshold128): # 将灰度水印转为二值矩阵降低待嵌入数据量 binary_wm (watermark_gray threshold).astype(np.uint8) # 压缩为字节流zlib底层就是deflate算法 compressed_bytes zlib.compress(binary_wm.tobytes(), level9) return compressed_bytes, binary_wm.shape这段代码做了两件事先把灰度水印按阈值二值化减少每个像素所需的比特数再用zlib.level9做最大压缩。level的取值范围是0到90表示不压缩9表示压缩率最高但耗时最长。实际使用时如果水印是随机噪点图压缩率会很差可以考虑换成level6在速度和压缩率之间取平衡。压缩后的字节流长度要记录并传给嵌入端保证提取时能准确解压出原始尺寸。2.3 完整嵌入与提取流程的文字版流程图解把整个流程串起来看嵌入阶段分为七个步骤。第一步读入载体图像并转为灰度图第二步对灰度图做一级DWT分解取出LL子带第三步把LL子带按固定块大小切块并逐块做DCT变换第四步对每个DCT系数矩阵执行SVD分解第五步将压缩后的水印比特流按序调制进各块的S矩阵对角元素第六步用修改后的S矩阵重建DCT系数块再做逆DCT得到含水印的LL子带第七步用逆DWT合并四个子带输出含水印图像。提取阶段是嵌入的逆过程但有一个前提条件接收方必须知道块大小、嵌入强度、水印长度和SVD分解时的U、V矩阵。这也是SVD水印方案的一个经典争论点因为严格来说如果U和V矩阵需要作为密钥传输系统就不是完全盲提取。这个项目的做法是把U、V矩阵通过伪随机种子生成接收方只要知道种子就能复现实现了实际意义上的盲提取。3. 源码模块分析与嵌入提取核心代码复现3.1 wmLibs各模块的职责边界这个项目的源码组织非常清晰wmLibs目录下的每个文件都有明确分工。bwm_core.py承载了水印嵌入和提取的最核心算法包括DWT分解、DCT分块、SVD嵌入和逆变换的全过程。pool.py是图像分块工具负责把图像矩阵切割成指定大小的子块同时也提供从子块恢复为完整图像的反向操作。att.py专门用于攻击模拟定义了多种图像攻击方式的接口包括JPEG压缩、高斯噪声、旋转、缩放、裁剪等用来验证水印的鲁棒性。cli_tools.py提供了命令行入口方便在不启动GUI的情况下跑完整流程。recover.py处理的是提取后的水印恢复逻辑包括解压和去二值化。blind_watermark.py封装了盲水印的高层接口内部组合调用bwm_core和pool对外暴露embed和extract两个函数。secret.py以及outSecret系列脚本负责处理秘密信息的封装和输出包括压缩、旋转、格式转换、尺寸调整等操作相当于为水印数据做了多种预处理和后处理的工具箱。这种模块划分方式值得借鉴它把算法的纯计算部分bwm_core.py、数据结构部分pool.py、攻击测试部分att.py和应用层接口部分剥离清楚替换任何一层都不会影响其他层。3.2 基于DWT-DCT-SVD的嵌入函数实现以下是从代码结构推演出的标准嵌入流程实现与项目的核心逻辑一致import pywt import numpy as np from scipy.linalg import svd def embed_watermark(cover_img, wm_bits, block_size8, alpha0.05, seed42): # 一级DWT分解取LL子带 coeffs pywt.dwt2(cover_img, haar) LL, (LH, HL, HH) coeffs h, w LL.shape # 计算块数量不足部分裁剪 blocks_h, blocks_w h // block_size, w // block_size LL_crop LL[:blocks_h*block_size, :blocks_w*block_size] bits_idx 0 modified_blocks [] rng np.random.default_rng(seed) # 固定随机种子用于生成U、V矩阵 for i in range(blocks_h): for j in range(blocks_w): block LL_crop[i*block_size:(i1)*block_size, j*block_size:(j1)*block_size] # DCT变换 dct_block dct(dct(block.T, normortho).T, normortho) if bits_idx len(wm_bits): # 对DCT块做SVD分解 U, S, Vt svd(dct_block) # 修改最大奇异值嵌入当前比特 bit int(wm_bits[bits_idx]) S[0] S[0] * (1 alpha) if bit 1 else S[0] * (1 - alpha) bits_idx 1 # 用修改后的奇异值重构DCT块 dct_block_mod U np.diag(S) Vt modified_blocks.append(dct_block_mod) else: modified_blocks.append(dct_block) # 重建LL子带 LL_modified np.zeros_like(LL_crop) idx 0 for i in range(blocks_h): for j in range(blocks_w): LL_modified[i*block_size:(i1)*block_size, j*block_size:(j1)*block_size] modified_blocks[idx] idx 1 # 逆DCT LL_modified idct(idct(LL_modified.T, normortho).T, normortho) # 逆DWT得到含水印图像 modified_coeffs (LL_modified, (LH, HL, HH)) watermarked pywt.idwt2(modified_coeffs, haar) return np.clip(watermarked, 0, 255).astype(np.uint8)这段代码的关键参数有三个block_size控制DCT分块的大小数值越大单个块的频率分辨率越高但可嵌入的块数量越少alpha是嵌入强度取值范围通常在0.01到0.1之间太小会导致提取时比特判断不准确太大会在图像上留下可见的块效应seed是随机种子保证接收方能复现与发送方相同的U、V矩阵。需要注意这段代码在计算上只修改了每个块SVD后的最大奇异值这属于最基础的做法项目源码中还会同时调整几个奇异值以提高冗余度。使用scipy.linalg.svd时默认返回的Vt已经是V的转置重构时直接用U diag(S) Vt即可不需要再转置。pywt.dwt2返回的系数是四个子带注意赋值时LL在前通道顺序不能搞反。3.3 提取函数与盲水印判断策略def extract_watermark(watermarked_img, wm_len, block_size8, alpha0.05, seed42): coeffs pywt.dwt2(watermarked_img, haar) LL, _ coeffs # 提取阶段不需要用高频子带 h, w LL.shape blocks_h, blocks_w h // block_size, w // block_size LL_crop LL[:blocks_h*block_size, :blocks_w*block_size] extracted_bits [] rng np.random.default_rng(seed) for i in range(blocks_h): for j in range(blocks_w): if len(extracted_bits) wm_len: break block LL_crop[i*block_size:(i1)*block_size, j*block_size:(j1)*block_size] dct_block dct(dct(block.T, normortho).T, normortho) # SVD分解这里不需要U和Vt只关注奇异值 U, S, Vt svd(dct_block) # 判断嵌入的比特奇异值放大则视为1缩小则视为0 if S[0] S[0] / (1 alpha) * (1 alpha / 2): # 实际用干净块的估计阈值 extracted_bits.append(1) else: extracted_bits.append(0) return extracted_bits[:wm_len]提取时的比特判断逻辑比较讲究理论上如果嵌入端让S[0]乘以(1alpha)提取时S[0]会显著大于原始值反之为0。但问题在于接收方并不持有干净块的S[0]作对照。项目源码中采用的办法是用SVD后第一个奇异值与后续奇异值的比例关系来判断。由于自然图像的S[0]通常是S[1]的5到10倍嵌入强度alpha会让这个比值发生可检测的变化。具体做法是计算S[0]与S均值的关系设定一个偏移阈值超过则判为1低于则判为0。需要注意在实际代码中由于DCT系数块的SVD结果并非完全稳定提取时经常会配合多数表决策略同一个水印比特重复嵌入到多个块中提取时取出现次数最多的值作为最终判断结果。3.4 deflate与图像格式转换交互的小坑zlib压缩水印数据后输出是bytes对象嵌入时需要逐位转换成0/1数组。常见错误是把bytes直接当作水印比特数组导致每个字节被当作一个比特。正确的转换方法是把每个byte拆成8个bitdef bytes_to_bits(byte_data): bits [] for byte in byte_data: for i in range(7, -1, -1): bits.append((byte i) 1) return np.array(bits, dtypenp.uint8)同时在嵌入前要记录水印的原始尺寸和压缩后长度。因为在提取阶段需要先收集足够多的比特位组装成完整的bytes对象再调用zlib.decompress还原成原始二值矩阵。如果比特数不对齐解压会直接报错。项目里secret.py和outSecret.py之间传递的不仅仅是水印数据本身还有封装好的元信息这也是为什么要单独做一层输出格式控制的原因。4. GUI界面操作流程与图像预处理管线解析4.1 mainGui.py的交互设计与信号槽逻辑这个项目属于典型的PyQt5工程mainGui.py加载同目录下的mainGui.ui文件构建界面。主界面围绕嵌入和提取两个核心操作展开同时提供图像格式转换、压缩、旋转、缩放等辅助功能按钮。用户可以通过文件选择对话框加载载体图像和水印图像参数区域支持调整嵌入强度、块大小、随机种子等关键参数。界面操作背后的信号槽连接逻辑是这样的点击嵌入水印按钮后主线程读取界面上的参数调用blind_watermark.py中封装的embed接口执行完整的水印嵌入流程。由于DWT-SVD计算相对耗时如果载体图像较大界面会短暂无响应。项目里用了Qt的QThread子类化来跑计算任务避免阻塞UI线程这一设计在批量处理多张图片时尤其重要。演示视频中应该能看到这个细节处理过程中进度条在动界面没有冻结。4.2 outSecret系列脚本图像预处理的流水线设计outSecret.py以及它派生出的outSecretCompress、outSecretRotate、outSecretFormatPng、outSecretFormatJpeg、outSecretResize这五个模块实际上构成了一套图像预处理流水线。它们按职责拆分成了独立脚本共享一个全局配置模块globalvar.py通过修改配置文件就能改变整个管线的输出行为。processPic.py是通用处理器formatPic.py负责图片格式封装PNG/JPEG切换compressPic.py调用PIL或OpenCV的质量参数模拟JPEG压缩rotatePic.py执行任意角度旋转resizePic.py做尺寸缩放。这些操作和水印系统有什么关系答案是攻击测试。att.py中的JPEG压缩攻击、旋转攻击、缩放攻击都可以直接复用这些工具的能力它们本质上是同一套图像操作的不同入口。4.3 GUI参数与底层函数的映射关系表格呈现GUI控件与底层参数的对应关系界面控件底层参数建议取值范围影响说明嵌入强度滑块alpha0.01 ~ 0.1数值越大鲁棒性越强但图像失真越明显块大小下拉框block_size8 / 16 / 32决定DCT变换的分块粒度影响嵌入容量随机种子输入框seed任意整数控制SVD的U、V矩阵生成用于提取端的密钥复现压缩级别选择zlib.level1 ~ 9影响水印压缩率级别越高压缩耗时越长DWT小波选择wavelethaar / db2 / db4不同小波基对图像重建质量有影响# mainGui.py 中典型的参数传递模式 def on_embed_clicked(self): alpha float(self.alpha_spinbox.value()) block_size int(self.block_combo.currentText()) seed int(self.seed_lineedit.text()) wm_path self.wm_file_lineedit.text() cover_path self.cover_file_lineedit.text() # 调用底层嵌入接口 result blind_watermark.embed(cover_path, wm_path, alphaalpha, block_sizeblock_size, seedseed) # 结果预览和保存 self.result_label.setPixmap(QPixmap.fromImage(result)) cv2.imwrite(self.output_path, result)这段代码展示了GUI层如何从控件取值并传递到底层函数的过程。注意alpha_spinbox需要在界面编辑器里设置小数位数setDecimals(2)和步长setSingleStep(0.01)否则用户无法输入0.05这样的小数。seed输入框需要做输入校验防止用户输入非数字导致程序崩溃。4.4 演示视频中值得留意的操作顺序演示视频展示的关键操作序列是导入载体图→导入水印图→设置参数→执行嵌入→查看PSNR数值→对含水印图执行JPEG压缩攻击→执行提取→对比提取出的水印与原图的相似度。这个顺序覆盖了毕设答辩中最常见的提问路径。中间还会演示一个细节把水印旋转90度后嵌入再用不同角度旋转攻击观察提取结果。这是为了说明SVD对旋转攻击的敏感程度以及为什么rotatePic.py要支持任意角度而非只支持90度的倍数。5. 抗攻击测试的验证方法与参数调优的实战经验5.1 att.py的扩展方式与攻击结果评估攻击测试模块采用插件式架构每种攻击类型是一个独立函数输入含水印图像输出攻击后的图像。新增加一种攻击方式只需要在att.py中追加一个函数并让cli_tools.py的测试入口自动识别到即可。评估指标通常用归一化相关系数NC或比特错误率BER衡量提取水印与原始水印的相似度NC值大于0.9视为提取成功。# 扩展JPEG压缩攻击的示例 def jpeg_attack(img, quality50): import cv2 encode_param [int(cv2.IMWRITE_JPEG_QUALITY), quality] _, encoded cv2.imencode(.jpg, img, encode_param) decoded cv2.imdecode(encoded, 1) return decodedJPEG压缩攻击的quality参数是关键变量真实场景中的社交平台传播会把图片压缩到quality 30以下。测试时应该对quality从10到90做梯度扫描找到水印方案能承受的最低质量值。项目源码中att.py还包含高斯噪声mean, variance参数和裁剪攻击裁剪比例参数的接口复用时直接传参即可。5.2 攻击强度与水印可提取性的边界实验实际测试中最常出现的现象是JPEG quality降到40以下后提取出的水印噪声明显增大但NC指标还在0.85以上视觉上已能辨别出水印内容。如果继续压到quality 20DWT低频系数被严重篡改奇异值发生偏移比特判断错误率飙升。旋转攻击对小角度不敏感5度以内提取稳定但超过15度后由于插值算法引入的像素偏移DWT子带的对齐关系被破坏SVD奇异值发生量级变化提取基本失败。此时需要通过旋转校正先估计旋转角度再逆旋转来预处理被攻击图像。调整alpha参数时0.05附近是视觉质量和鲁棒性的甜点区。alpha提到0.1PSNR会降到38dB以下人眼能看出块状伪影。alpha降到0.01JPEG quality 60的攻击就能让NC跌破0.7失去实用价值。5.3 工程化使用中的三个实际坑和对应处理第一个坑是DWT后子带的尺寸奇偶问题。当载体图的宽度或高度是奇数时dwt2分解后LL子带尺寸会向下取整重建图像时尺寸对不上。解决方案是在预处理阶段统一将图像resize到偶数尺寸再进入水印流程。第二个坑是U、V矩阵的复现问题。正常使用scipy.linalg.svd每次调用都会因LAPACK的底层实现差异产生不完全一致的U、V矩阵符号可能翻转。这会导致嵌入端和提取端使用不同的U、V来重构造成误码。解决方式是在嵌入时把U、V矩阵按序列化方式存储或者用图固定随机种子不使用numpy默认的随机状态因为不同版本的numpy随机数生成器实现有差异。项目源码里采用方案是后者全程使用np.random.default_rng(seed)效果稳定。第三个坑是水印图像与载体图像的类型匹配问题。如果载体加载为0~255的uint8而水印嵌入过程中返回了float数组在调用cv2.imwrite保存时会导致像素截断异常。Python中比较隐蔽的是这个错误不会抛异常只是输出的图像颜色发白。处理方式是在保存前显式调用np.clip再加astype(np.uint8)。验证方法上推荐写一个自动化脚本循环遍历多个alpha值和多个JPEG质量参数将结果矩阵输出为CSV直观看到参数边界在哪里。python cli_tools.py --test robustness --alpha 0.03 0.05 0.08 --quality 30 50 70可以让项目自动完成批量攻击测试并把指标汇总到表格里几分钟就能得到完整的鲁棒性报告这在答辩演示时也会是加分项。本文还有配套的精品资源点击获取
返回列表