简介:一份基于MATLAB的数字水印系统课程设计资源包,围绕DCT离散余弦变换与小波变换DWT两种主流算法展开对比实现,并配套完整的GUI交互界面。资源适合计算机、通信、自动化、电子信息等专业在校生用于课程设计、毕业设计或算法入门,也可作为数字水印方向的初级进阶练习。包内共23个文件,包含MATLAB源码脚本、GUI布局文件fig、测试图像bmp、预处理数据mat,以及txt操作说明和doc技术文档,整体压缩包19.6MB,结构紧凑,便于按功能模块调用与二次开发。系统支持嵌入、加噪、旋转、剪切、压缩等多种常见攻击模拟,并提供PSNR、NC等评价指标用于量化比较算法的鲁棒性,能够直观理解不同水印算法在抗攻击能力上的差异。下载后如需指导,可联系作者远程协助运行。目前已有121人学习下载,值得作为课程设计或项目演示的参考模板。
1. 课程设计里数字水印系统真正的门槛
课程设计选 MATLAB 做数字水印系统,最常见的翻车点不在算法,而在答辩演示:老师顺手把含水印图像存成 JPEG,再让你从 GUI 界面点一次提取,结果空白一片。空域 LSB 藏的那点比特,一张有损压缩图回来就散了大半;界面画得再漂亮,回答不上来 alpha 为什么取 0.05、提取端为什么知道水印嵌在哪个子带,也一样过不了关。
这个课程设计真正要打通的是两件事:水印该嵌在哪个频带,以及 GUI 回调之间怎么传递带水印的中间结果。前者决定了系统能不能在压缩、加噪之后仍把水印提出来,后者决定了演示的时候点按钮会不会报错或者提出一张乱码。
下面这套方案以离散小波变换为主线,给出可复现的嵌入/提取源码、JPEG 与噪声攻击测试脚本,以及 App Designer 界面整合方案。适合想交课程设计或毕设、又不满足于随便跑通的读者。读完后你手上会有一套完整的系统骨架,以及一套用来定参数、应付追问的判断依据。
2. DWT选型:LSB、DCT与DWT在数字水印系统里的取舍
2.1 三种嵌入域对比:为什么课程设计首选DWT
如果只看实现难度,LSB 是瞬时能跑通的选择:把水印的每个比特写进载体图像像素的最低位即可。问题在于 JPEG 压缩本身就是频域量化,低位比特在量化表里基本被整体丢弃,所以 LSB 方案在面对“压缩攻击”时基本没有还手之力。课程设计的答辩追问通常从这开始,因此至少要往频域走一步。
DCT 是 JPEG 的同族思想,需要把图像切成 8x8 块逐一变换,嵌入系数还要考虑块效应,代码量上去之后调试并不容易。DWT 的好处是整图分解、天然具有多分辨率结构,低频子带集中了主要能量,而人类视觉对低频的微小变化又不敏感,这正好满足“看不见又能抗压缩”的矛盾需求。MATLAB 里只靠 Wavelet Toolbox 的两个函数 dwt2 和 idwt2 就能完成正向和逆向变换,课程设计阶段这种低编码成本非常关键。
| 嵌入域 | 实现难度 | 抗JPEG压缩 | 抗几何攻击 | 典型用途 |
|---|---|---|---|---|
| LSB空域 | 最低 | 差 | 差 | 实验演示、信息隐藏入门 |
| DCT频域 | 中 | 中 | 中 | 与JPEG流程兼容,适合看文献扩展 |
| DWT频域 | 中低 | 较好 | 一般 | 课程设计/毕业论文最常用路线 |
上表里的“抗几何攻击”要说明一下:三种方法遇到旋转、缩放都要头疼,DWT 只是没有更差而已,解决办法后面第 5 章会提到。课程设计选 DWT,不是因为它在所有指标上碾压,而是因为它在写得出代码、讲得清原理、扛得住压缩这三件事上平衡得最好。
2.2 水印预处理:Arnold置乱与灰度缩放
直接把 logo 图像按像素铺进载体子带,提取结果里会残留明显的原始图案轮廓,这是因为自然图像邻域相关性太强。常见做法是嵌入前先把水印做一次位置置乱,把相邻像素打散到整个画面,嵌入能量在频域里分布得均匀一些,提取回来时再做逆置乱恢复。
Arnold 变换是经典的选择,公式可以写成 [x'; y'] = [1 1; 1 2] * [x; y] mod N,其中 N 是方形水印的边长。编程实现不需要矩阵库,两层循环直接映射坐标就行:
function img_s = arnold_scramble(img, times) % 输入img应为方形灰度图或二值图 % times为置乱次数,同时也是提取端需要的还原参数之一 n = size(img, 1); img_s = img; for k = 1:times tmp = zeros(size(img)); for i = 1:n for j = 1:n % Arnold映射:新坐标由原坐标线性组合后取模 new_i = mod(i + j - 1, n) + 1; new_j = mod(i + 2*j - 1, n) + 1; tmp(new_i, new_j) = img_s(i, j); end end img_s = tmp; end end这个函数把置乱次数 times 暴露给调用方,实际工程里 times 和尺寸 n 要一起保存起来,提取端没有这两个值是无法还原水印的。置乱次数一般取 10 到 20 次,超过一定次数会回到原点,所以不要随手填一个很大的数。另外 Arnold 只适用于方形图像,水印若不是方的,可以先用 imresize 或 padarray 处理成方形再置乱。
如果水印是灰度 logo、不是二值图,建议在预处理阶段先做一次归一化:im2double 之后缩放到 [0,1],保证嵌入时和 double 类型的载体在同一个数值尺度上。灰度水印能藏的信息量更大,但课程设计演示时二值 logo 更好讲,提取结果也更容易被老师一眼看懂。
2.3 嵌入位置的确定:二级DWT低频子带的嵌入公式
载体不做任何处理,整图直接嵌入也可以,但常见做法是分解两级:先对灰度图做一级 dwt2,得到 LL1、LH1、HL1、HH1;再对 LL1 做一级 dwt2,得到 LL2。水印嵌在 LL2 上,理由有三个:低频能量集中、抗 JPEG 量化能力强、后续逆变换不会把水印扩散到高频细节里。
嵌入公式分加性和乘性两种。加性写成 c' = c + alpha * w,理解简单、代码直观;乘性写成 c' = c * (1 + alpha * w),能量大的系数被改动的绝对量也大,主观不可见性会好一点,但需要小心低频系数出现负值的问题。课程设计一般用加性就足够,控制 alpha 在 0.02 到 0.1 之间:低于 0.02,提取时信噪比不够;高于 0.1,图像能看出纹理变化,PSNR 会掉到 35dB 以下。具体数值不是拍脑袋定的,第 5 章会给一条实测曲线来定边界。
提示:dwt2 默认小波是 haar,代码里建议显式指定 'db4'。haar 保留边缘更锐利但重构振铃明显,db4 是 Daubechies 中长度较短的选择,重建质量与运算速度之间更均衡。
3. 可复现源码:DWT水印嵌入、提取与攻击测试
3.1 嵌入函数:两级DWT的加性嵌入实现
这里给出完整的嵌入函数,输入是灰度载体、灰度水印和嵌入强度 alpha,输出是 double 类型的含水印图像。代码在处理水印尺寸时用了 imresize,避免出现“矩阵维度不匹配”的低级报错。
function wm_img = embed_watermark(cover, wm, alpha) % cover: 灰度图像,uint8或double均可 % wm: 灰度水印,uint8或double均可,不要求与cover同尺寸 % alpha: 嵌入强度,典型范围0.02~0.1 cover = im2double(cover); wm = im2double(wm); % 第一级DWT,得到低频LL1和高频子带 [LL1, LH1, HL1, HH1] = dwt2(cover, 'db4'); % 第二级DWT,继续对LL1分解 [LL2, LH2, HL2, HH2] = dwt2(LL1, 'db4'); % 把水印缩放到LL2相同尺寸 [r, c] = size(LL2); wm_r = imresize(wm, [r, c]); % 加性嵌入 LL2_wm = LL2 + alpha * wm_r; % 用修改后的LL2重建一级低频,再重建整图 LL1_wm = idwt2(LL2_wm, LH2, HL2, HH2, 'db4'); wm_img = idwt2(LL1_wm, LH1, HL1, HH1, 'db4'); end代码逻辑分四步:先把输入统一成 double,避免 uint8 运算溢出;接着连续两次 dwt2 分解;然后缩放水印并相加;最后用 idwt2 逐级恢复。alpha 乘在 wm_r 上而不是乘在 LL2 上,这样水印能量跟载体自身的亮度无关,调参数时行为更可控。
需要注意 idwt2 后面跟的是 LH2、HL2、HH2,也就是二级分解时产生的高频子带,一级恢复时用的是 LH1、HL1、HH1。如果传错层级,图像会重建出严重的棋盘纹理,这是初学最容易踩的坑。输出一律保持 double,显示时用 im2uint8 转一下即可,存储时再用 imwrite 转换,不要在嵌入中途就截断到 8bit。
3.2 提取函数:非盲提取与归一化相关系数NC
提取端采用非盲方案,也就是默认手里有原始载体。这样做课程设计最稳,因为公式推导和代码都简单;如果要做盲提取,需要改成交替嵌入加相关检测的思路,代码量会翻倍且稳定性依赖统计特性,答辩反而不容易讲清楚。
提取过程就是把嵌入过程反过来:分别对含水印图和原始图做两级 DWT,两者 LL2 的差值除以 alpha,理论上就能还原水印。DWT 是线性变换,加性嵌入在前,差分提取在后,噪声只来自攻击环节。
function wm_ext = extract_watermark(wm_img, cover, alpha) % wm_img: 含水印图,double % cover: 原始载体图,double,非盲提取的参考 % alpha: 必须与嵌入时一致,否则提取结果整体偏大或偏小 [LL1_c, ~, ~, ~] = dwt2(cover, 'db4'); [LL2_c, ~, ~, ~] = dwt2(LL1_c, 'db4'); [LL1_w, ~, ~, ~] = dwt2(wm_img, 'db4'); [LL2_w, ~, ~, ~] = dwt2(LL1_w, 'db4'); wm_ext = (LL2_w - LL2_c) / alpha; wm_ext = imresize(wm_ext, [size(cover, 1), size(cover, 2)]); end提取结果是一个和载体同尺寸的浮点图,不能直接拿它跟原始水印比。课程设计里评价提取质量最常用的是归一化相关系数 NC,公式上可以去掉均值以忽略直流偏移。下面这个实现加了均值中心化和 eps 防除零,数值稳定:
function nc = calc_nc(ref, test) a = ref(:) - mean(ref(:)); b = test(:) - mean(test(:)); nc = sum(a .* b) / sqrt(sum(a.^2) * sum(b.^2) + eps); endNC 在 0 到 1 之间,通常 0.9 以上肉眼已经能看出水印轮廓。比 NC 更严格的是第 5 章要讲的 BER,但先跑通 NC 就能证明系统闭环了。注意 calc_nc 的两个输入必须是同一尺寸,ref 建议用置乱前或置乱后的水印保持一致,否则算出来会偏低。
3.3 攻击测试:JPEG压缩、高斯噪声与旋转裁剪怎么模拟
课程设计报告里没有攻击测试就等于没有验证鲁棒性。最常见的三个攻击场景按复杂度排序:JPEG 压缩最简单,imwrite 指定 Quality 再读回即可;高斯噪声直接 imnoise;旋转裁剪稍微麻烦一点,imrotate 默认会改变图像尺寸,需要保持与嵌入时一致才能提取。
% JPEG压缩,质量因子40,数值越小压缩越狠 imwrite(im2uint8(wm_img), 'attack.jpg', 'Quality', 40); attacked = im2double(imread('attack.jpg')); % 高斯噪声,方差0.001,相当于轻度噪点 attacked = imnoise(wm_img, 'gaussian', 0, 0.001); % 旋转5度并裁剪保持原尺寸 attacked = imrotate(wm_img, 5, 'bicubic', 'crop');三种攻击的验证方式相同:把 attacked 送进 extract_watermark,再和原始水印算 NC。JPEG 攻击重点关注质量因子降到多少时 NC 跌破 0.7;高斯噪声要分清均值和方差的区别,imnoise 第二个参数 0 是均值,第三个参数才是方差;旋转攻击最难处理,因为像素重采样会破坏水印的同步位置,'crop' 只能保证尺寸,不能保证内容对齐。
到这里三个核心函数都齐了,把嵌入、提取、攻击三个函数存成独立 .m 文件,给 GUI 界面预留好函数签名,接下来就是界面层的事。函数签名保持输入输出清晰,是前面所有步骤里对后期工程化最重要的一步。
4. GUI界面整合:用App Designer做成可演示的数字水印系统
4.1 App Designer还是GUIDE:课程设计怎么选
老教程里大量用 GUIDE 写界面,但新版 MATLAB 打开 GUIDE 时会看到提醒,新装的环境里 GUIDE 相关组件也不再是默认推荐。App Designer 的代码是类结构,控件回调自动生成,属性面板可以直接改文字和范围,更适合课程设计这种“要改参数、要现场演示”的场景。如果老师指定必须用 GUIDE,回调逻辑是一样的,只是 handles 换成 app 对象,后面代码可以对应改写。
我一般会直接选 App Designer,理由很实际:界面自动生成 .mlapp 文件,里面已经带好控件初始化的方法,不需要手工维护 figure 的位置和回调注册。对课程设计来说,少写 30 行界面代码就多 30 行精力去调水印参数。
4.2 界面布局与控件回调的读写链
界面按“导入-参数-操作-结果”四个区域排:左侧放“导入载体”“导入水印”两个按钮和一张预览轴;中间放 alpha 滑块、分解层数下拉框和嵌入按钮;右侧上下两个 UIAxes 显示结果和提取水印;底部放一个文本区显示 PSNR 和 NC 数值。
App Designer 里每个控件都有自动生成的名称,比如滑块叫 AlphaSlider,读取当前值就是 app.AlphaSlider.Value。这个值默认是 double,直接传给嵌入函数即可。下拉框用 Items 属性配置 {'2','3'},回调里要做一次 str2double,因为控件返回的是字符串。
跨回调共享数据是界面层最容易出错的地方。水印图像、载体图像、嵌入后的图像都要挂在 app 对象的自定义属性上,而不是局部变量。在代码视图顶部 properties (Access = public) 区域声明:
properties (Access = public) coverImg = []; wmImg = []; watermarkedImg = []; lastAlpha = 0.05; end这样嵌入按钮写完的数据,提取按钮随时能读。最典型的翻车场景是:用户嵌入后把 alpha 滑块从 0.05 拖到 0.1,再点提取,提取端拿 0.1 去还原一张按 0.05 嵌的图,结果整张水印都偏暗,NC 直接掉到 0.3 以下。所以嵌入成功那一刻要把当时的 alpha 存进 app.lastAlpha,提取回调只认这个值,滑块只负责“下次嵌入”的参数。
4.3 把嵌入、提取、攻击函数接进按钮回调
导入载体按钮的回调只需要做三件事:弹文件选择框、读入图像、显示并保存到属性。灰度图直接存,彩色图先 rgb2gray,否则后续 DWT 会拿到三维矩阵报错。代码结构如下:
function ImportCoverButtonPushed(app, event) [file, path] = uigetfile({'*.png;*.bmp;*.jpg'}, '选择载体图像'); if isequal(file, 0) return; % 用户取消选择 end app.coverImg = imread(fullfile(path, file)); if size(app.coverImg, 3) == 3 app.coverImg = rgb2gray(app.coverImg); end imshow(app.coverImg, 'Parent', app.CoverAxes); enduigetfile 返回的 file 也可能是 0,说明用户点了取消,不判断就直接 fullfile 会报错。这一步代码虽短,但答辩现场很容易因为文件格式选错而崩溃,在取消分支里直接 return 是好习惯。
嵌入按钮的回调把前面的模块串起来:从属性读图像,从滑块读 alpha,调用 embed_watermark,把结果存属性并显示,同时算一次 PSNR 刷新底部标签。
function EmbedButtonPushed(app, event) if isempty(app.coverImg) || isempty(app.wmImg) app.ResultLabel.Text = '请先导入载体和水印'; return; end alpha = app.AlphaSlider.Value; app.watermarkedImg = embed_watermark(app.coverImg, app.wmImg, alpha); app.lastAlpha = alpha; % 保存嵌入参数,提取时保持一致 imshow(im2uint8(app.watermarkedImg), 'Parent', app.ResultAxes); p = psnr(app.watermarkedImg, im2double(app.coverImg)); app.ResultLabel.Text = sprintf('PSNR: %.2f dB', p); endpsnr 函数来自 Image Processing Toolbox,如果没有这个工具箱,可以手动算 MSE 再取对数,一行公式的事:10 * log10(1 / mean((a(:) - b(:)).^2))。提取按钮的回调同样套路:先检查 app.watermarkedImg 是否为空,再调 extract_watermark 和 calc_nc,最后 imshow 到右侧轴。
攻击测试建议单独做一个下拉框加一个执行按钮,下拉框选择攻击类型,执行按钮读出类型后调用第 3.3 节对应攻击代码。攻击后的图像要覆盖 app.watermarkedImg 吗?我一般不会覆盖,而是单独存在 app.attackedImg 里,提取按钮上加一个复选框“从攻击后图像提取”,这样既能看干净图像的水印质量,又能看攻击后的,演示时多一个对比层次。
5. 鲁棒性验证与调参技巧:用NC/BER曲线定参数
5.1 批量测试alpha和JPEG质量因子,画一条能答辩的曲线
alpha 不该靠目测随机设。把 0.01 到 0.1 按步长 0.01 扫一遍,嵌入后同时算 PSNR 和提取 NC,画成两条曲线,交叉区域就是推荐参数。脚本结构用一个循环最直观:
alphas = 0.01:0.01:0.1; for i = 1:length(alphas) wm_img = embed_watermark(cover, wm, alphas(i)); psnr_v(i) = psnr(wm_img, cover); wm_ext = extract_watermark(wm_img, cover, alphas(i)); nc_v(i) = calc_nc(wm, wm_ext); end % 画图时用plotyy或tiledlayout,左轴PSNR,右轴NC无攻击时 NC 会接近 1,这条曲线主要用来找“不可见性”的边界;真正决定参数的是加上 JPEG 攻击后的曲线。固定质量因子 50,重复上面循环,NC 开始明显下滑的 alpha 值就是最低可用强度。常见结果是 alpha 取 0.03 到 0.05 时,PSNR 在 38dB 附近,JPEG 质量 50 下 NC 仍能保持 0.85 以上,这个区间可以直接写进设计报告。
5.2 提取结果带噪怎么办:二值化与BER评价
受攻击后提取出的水印会蒙上一层灰白色的压缩噪声,直接用 calc_nc 看数值还行,但显示出来很难看。常见做法是把提取结果归一化后再做二值化,二值图和嵌入前的二值水印逐像素比较,用误码率 BER 来评价:BER 是错误比特占总比特的比例,越接近 0 越好。
% 提取结果先拉伸到[0,1],再用imbinarize做全局阈值二值化 wm_norm = mat2gray(wm_ext); wm_bw = imbinarize(wm_norm); % 嵌入前把水印转成logical,保证类型一致 % 如果用水印灰度图做嵌入,提取端就走灰度评价 ber = sum(wm_bw(:) ~= wm_ref(:)) / numel(wm_ref);这里 wm_ref 必须和嵌入用的水印完全一致,最好是 logical 类型。如果嵌入前做的是灰度水印,那么提取端也走灰度评价,不要混着用。BER 低于 0.01 在课程设计里已经算优秀,答辩时用 BER 曲线替代 NC 曲线会更“像论文”。imbinarize 同样来自 Image Processing Toolbox,没有工具箱时可以用 graythresh 加 im2bw 等价实现。
提一个容易被忽略的同步细节:旋转攻击后的提取图像要先按嵌入时保存的置乱次数做逆 Arnold 置乱,再与原始水印比对,否则 NC 再高也对不上。
本文还有配套的精品资源,点击获取