这次来看一个基于 MATLAB 的数字水印提取算法项目,编号 453。它的核心思路是在传统 DWT-SVD 水印方案中引入 DCT,形成 DWT-DCT-SVD 三域级联结构。简单来说,嵌入时先对载体图像做离散小波变换,取低频子带做离散余弦变换,再对 DCT 系数矩阵做奇异值分解,把水印的奇异值加权叠加到载体奇异值上;提取时用同样的正向变换流程,做差值运算还原水印奇异值,最终重构水印图像。
这个项目适合正在做课程设计、毕业设计,或者刚开始接触图像水印算法的人。相比于单纯 DWT 或 DCT 水印,DWT-DCT-SVD 组合的优势体现在两点:DWT 提供多分辨率分解,适合把水印藏进低频区域;DCT 进一步做频域能量集中,让嵌入对常见图像处理攻击更稳定;而 SVD 的奇异值本身具有非常好的稳定性,微小的扰动不会让提取结果严重失真。三项级联之后,不可见性和鲁棒性会更均衡。
本文会给出完整的 MATLAB 实现,包括水印嵌入脚本、水印提取脚本和鲁棒性批量测试脚本,并说明 PSNR、NC 两个指标怎么算、怎么判断效果。还会把环境依赖、常见报错、调参建议一并整理出来。你可以直接照着跑通一个“嵌入 -> 攻击 -> 提取 -> 评估”的完整闭环。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | MATLAB 图像水印算法实现 |
| 核心技术 | DWT(离散小波变换)+ DCT(离散余弦变换)+ SVD(奇异值分解) |
| 主要功能 | 灰度和二值水印的嵌入、提取、抗攻击鲁棒性评估 |
| 运行环境 | MATLAB,建议使用 R2018a 以上版本 |
| 依赖工具箱 | Wavelet Toolbox(小波变换)、Image Processing Toolbox(图像读写与 PSNR 计算) |
| 输入素材 | 载体图像(灰度或彩色)、水印图像(灰度或二值 Logo) |
| 输出结果 | 含水印图像、提取水印图像、密钥文件、PSNR/NC 指标 |
| 提取类型 | 非盲提取,需要嵌入阶段保存的密钥矩阵 |
| 批量支持 | 通过脚本循环即可批量处理多张图像 |
| 接口扩展 | 可封装为.m函数供其他脚本调用,也可借助 MATLAB Web App Server 或 Production Server 发布 |
这个项目不是一个开箱即用的 GUI 工具,而是一套清晰可复用的算法框架。核心代码量不大,重点是理解三个变换如何级联,以及提取时的密钥依赖关系。
2. 适用场景与使用边界
2.1 适合什么场景
DWT-DCT-SVD 水印算法最典型的应用是图像版权标识和内容溯源。你可以把一段标识信息转成水印图像嵌入到待保护的图片中,发生纠纷时通过提取算法证明图片的来源关系。这类需求常见于课程设计、毕业论文演示,以及数字资产管理的前期方案验证。
这个项目非常适合下面几类人:
- 正在写图像水印相关课设、毕设的学生,需要一套可运行的 MATLAB 代码。
- 刚开始接触 DWT、DCT、SVD 三个变换关系,想通过代码理解原理的研究者。
- 想对比不同嵌入强度、小波基函数对水印效果影响的技术人员。
2.2 不适合什么场景
如果你的目标是“对任意图片直接提取水印、不需要任何额外信息”,这套非盲提取方案并不合适,因为提取时依赖嵌入阶段保存的密钥矩阵。如果追求盲提取,需要换半盲或盲水印方案,复杂度会高很多。
如果你的图片是彩色高清大图,且要求实时处理,纯 MATLAB 脚本的性能可能达不到要求,建议先做小尺寸验证,再决定是否改用 C++ 或 GPU 加速。
2.3 合规与授权边界
水印技术的核心是标识内容归属,而不是隐藏或盗用内容。嵌入水印的载体图像必须是本人创作、已获得授权或处于测试范围内的素材。使用他人照片、商用图片或受版权保护的 Logo 做实验,必须事先确认授权。对视频、人脸照片、声像作品做水印处理时,还要注意肖像权和隐私保护。任何绕过版权保护、窃取标识信息的做法,都不在本项目使用范围之内。
3. DWT-DCT-SVD 算法原理拆解
要跑通代码,先得理解三个变换在算法里各自干了什么。不要把 DWT、DCT、SVD 当成三个黑盒,它们的组合逻辑并不复杂。
3.1 DWT:多分辨率分解
离散小波变换把图像分解成一个低频近似子带和三个高频细节子带,分别记为LL、LH、HL、HH。LL是原图的低频近似,包含图像大部分能量,视觉上相当于一个缩小后的原图;三个高频子带包含边缘、纹理等细节信息。
水印嵌入通常选LL子带,因为人眼对低频区域的细微变化不敏感,低频子带经历噪声、压缩、缩放等攻击后也更稳定。MATLAB 中使用dwt2做一级分解,重构时使用idwt2。
3.2 DCT:频域能量集中
离散余弦变换能把图像从空间域转到频率域,并让能量集中到低频系数上。JPEG 压缩本身就是基于 DCT 设计的,所以对LL子带再做一次 DCT,会让后续嵌入的区域更贴近压缩算法的保留部分,从而增强抗 JPEG 压缩的能力。
MATLAB 中使用dct2做二维 DCT,用idct2做逆变换。经过 DCT 后,矩阵左上角的低频系数集中了大部分能量,这也为 SVD 分解提供了更稳定的输入矩阵。
3.3 SVD:奇异值的强稳定性
奇异值分解把任意实矩阵A分解为U * S * V',其中S是对角矩阵,对角线上的值就是奇异值。SVD 在图像水印中有两个明显优点:
- 奇异值对图像微小扰动不敏感。加入小幅度水印或受到轻度噪声干扰时,奇异值变化很小。
- 奇异值反映矩阵的固有信息,抗几何攻击的能力比直接在像素域嵌入更强。
因此水印嵌入通常是对载体矩阵做 SVD 后,把水印的奇异值或水印本身叠加到载体奇异值上。
3.4 三域组合的嵌入与提取流程
整个算法的正向流程可以写成:
- 对载体图像做一级 DWT,取
LL子带。 - 对
LL子带做 DCT,得到LL_dct。 - 对
LL_dct做 SVD,得到U * S * V'。 - 对水印图像做 SVD,得到
Uw * Sw * Vw'。 - 用嵌入强度
alpha将水印奇异值叠加到载体奇异值上:S_new = S + alpha * Sw。 - 用
U * S_new * V'重构 DCT 系数,再idct2回到空间域,最后idwt2重构出含水印图像。
提取时,对含水印图像重复步骤 1 到 3,得到含水印图像的奇异值矩阵S_w,然后计算:
Sw_extracted = (S_w - S) / alpha最后用保存的Uw、Vw重构水印:
W = Uw * Sw_extracted * Vw'这里的S、Uw、Vw、alpha就是提取水印必须的密钥。这也是非盲水印的典型特征。
4. 环境准备与前置条件
4.1 MATLAB 版本与工具箱检查
代码用到了dwt2、idwt2、dct2、idct2、svd、psnr、corr2、imresize等函数,分别来自 Wavelet Toolbox 和 Image Processing Toolbox。建议使用 R2018a 及以上版本,低版本也能运行,但个别函数可能存在差异。
在 MATLAB 命令行输入以下命令,可以确认当前环境下相关工具箱是否可用:
% 查看已安装的工具箱 ver % 确认关键函数是否存在 which dwt2 which dct2 which psnr如果提示找不到函数,默认是没有安装对应工具箱或 MATLAB 版本过低。
4.2 工作目录与素材准备
建议把所有脚本和测试素材放在同一个目录下,方便路径管理。目录结构可以参考下面这种方式:
DWT-DCT-SVD/ ├── watermark_embed.m ├── watermark_extract.m ├── test_robustness.m ├── lena.png ├── watermark.png └── results/其中lena.png是载体图像,watermark.png是水印图。results目录放含水印图像、密钥文件和提取结果。如果不想使用 Lena 图片,可以换成任意自拍的灰度或彩色测试图,注意代码会自动转灰度。
4.3 水印图像建议
水印图像不一定是二值图,但二值 Logo 的效果最好判断。如果使用复杂灰度图做水印,提取结果会受alpha取值影响,容易出现灰度偏移。建议先用黑白文字或简单图形 Logo 测试,跑通之后再换更复杂的水印。
5. MATLAB 水印嵌入完整实现
5.1 嵌入脚本
将下面的代码保存为watermark_embed.m,放在当前工作目录:
% watermark_embed.m % 基于 DWT-DCT-SVD 的数字水印嵌入 clear; clc; close all; % ---------- 参数设置 ---------- alpha = 0.05; % 嵌入强度,越大鲁棒性越强,但不可见性下降 wname = 'haar'; % 小波基函数,可选 haar / db1 / db2 / sym4 % ---------- 读取载体图像 ---------- coverImg = imread('lena.png'); if size(coverImg, 3) == 3 coverImg = rgb2gray(coverImg); end coverImg = double(coverImg); % ---------- 读取水印图像 ---------- watermark = imread('watermark.png'); if size(watermark, 3) == 3 watermark = rgb2gray(watermark); end wm_size = size(watermark); % 保存原始水印尺寸 watermark = double(watermark); % ---------- 一级小波分解 ---------- [LL, LH, HL, HH] = dwt2(coverImg, wname); % 将水印缩放到 LL 子带尺寸 watermark = imresize(watermark, [size(LL, 1), size(LL, 2)]); % ---------- DCT 变换 ---------- LL_dct = dct2(LL); % ---------- SVD 分解 ---------- [U, S, V] = svd(LL_dct); [Uw, Sw, Vw] = svd(watermark); % ---------- 水印嵌入 ---------- S_new = S + alpha * Sw; % ---------- 重构图像 ---------- LL_dct_new = U * S_new * V'; LL_new = idct2(LL_dct_new); watermarkedImg = idwt2(LL_new, LH, HL, HH, wname); watermarkedImg = uint8(watermarkedImg); % ---------- 保存结果 ---------- imwrite(watermarkedImg, 'watermarked_lena.png'); save('watermark_key.mat', 'U', 'V', 'Uw', 'Vw', 'S', 'alpha', 'wname', 'wm_size'); % ---------- 输出指标 ---------- psnr_val = psnr(watermarkedImg, uint8(coverImg)); fprintf('水印嵌入完成:PSNR = %.4f dB\n', psnr_val); % ---------- 显示结果 ---------- figure; subplot(1, 2, 1); imshow(uint8(coverImg)); title('原始载体图像'); subplot(1, 2, 2); imshow(watermarkedImg); title('含水印图像');5.2 关键参数说明
| 参数 | 作用 | 建议 |
|---|---|---|
alpha | 嵌入强度,控制水印叠加幅度 | 从 0.01 到 0.1 之间调试,越小越隐蔽,越大越鲁棒 |
wname | 小波基函数 | haar计算最快;db2、sym4鲁棒性更好但耗时增加 |
wm_size | 原始水印尺寸 | 提取阶段需要恢复到原尺寸保存 |
脚本运行结束后,会在当前目录生成两个文件:
watermarked_lena.png:嵌入水印后的图像。watermark_key.mat:后续提取水印所需的密钥文件。
6. MATLAB 水印提取算法实现
6.1 提取脚本
将下面的代码保存为watermark_extract.m:
% watermark_extract.m % 基于 DWT-DCT-SVD 的数字水印提取 clear; clc; close all; % ---------- 加载密钥 ---------- load('watermark_key.mat', 'U', 'V', 'Uw', 'Vw', 'S', 'alpha', 'wname', 'wm_size'); % ---------- 读取含水印图像 ---------- watermarkedImg = imread('watermarked_lena.png'); if size(watermarkedImg, 3) == 3 watermarkedImg = rgb2gray(watermarkedImg); end watermarkedImg = double(watermarkedImg); % ---------- 与嵌入相同的正向变换 ---------- [LL_w, ~, ~, ~] = dwt2(watermarkedImg, wname); LL_w_dct = dct2(LL_w); [~, S_w, ~] = svd(LL_w_dct); % ---------- 提取水印奇异值 ---------- Sw_extracted = (S_w - S) / alpha; % ---------- 重构水印 ---------- watermark_extracted = Uw * Sw_extracted * Vw'; % ---------- 归一化并恢复原始尺寸 ---------- watermark_extracted = mat2gray(watermark_extracted); watermark_extracted = imresize(watermark_extracted, wm_size); watermark_extracted = uint8(watermark_extracted * 255); % ---------- 保存提取结果 ---------- imwrite(watermark_extracted, 'extracted_watermark.png'); % ---------- 若原始水印存在,计算 NC ---------- if exist('watermark.png', 'file') orig = imread('watermark.png'); if size(orig, 3) == 3 orig = rgb2gray(orig); end nc_val = corr2(double(orig), double(watermark_extracted)); fprintf('水印提取完成:NC = %.4f\n', nc_val); end % ---------- 显示结果 ---------- figure; subplot(1, 2, 1); imshow(watermark_extracted); title('提取水印'); subplot(1, 2, 2); if exist('orig', 'var') imshow(uint8(orig)); title('原始水印'); end运行之后会生成extracted_watermark.png,同时在命令行打印 NC 值。NC 越接近 1,说明提取水印与原始水印越相似。一般情况下 NC 在 0.95 以上,肉眼几乎看不出区别;如果 NC 低于 0.8,基本可以判定水印提取失败。
6.2 为什么提取必须依赖密钥
提取脚本刚开始就load('watermark_key.mat', ...)。S是嵌入前载体图像的 DCT 系数奇异值矩阵,(S_w - S) / alpha能还原水印奇异值,本质上是因为嵌入过程做的是“先知道底数,再算差值”。
如果你在提取环境里没有密钥文件,就没有办法从S_w中分离出水印信息。这一点既是限制,也是安全性的来源:没有密钥的人无法轻易提取或篡改水印,适合用来做版权归属声明。
6.3 提取结果判断标准
- 提取水印肉眼可辨识。
- NC 大于 0.9,说明提取质量高。
- 如果提取水印出现大面积噪点,优先检查
alpha是否太小,以及载体图像在嵌入后是否被压缩过。
7. 功能测试与鲁棒性验证
7.1 评估指标
水印效果通常看两个指标:
| 指标 | 全称 | 作用 | 判断标准 |
|---|---|---|---|
| PSNR | Peak Signal-to-Noise Ratio | 衡量含水印图像相比原始载体图像的质量损失 | 越高越好,通常 30 dB 以上肉眼难察 |
| NC | Normalized Correlation | 衡量提取水印与原始水印的相似度 | 越接近 1 越好,0.9 以上视为效果好 |
PSNR 不是水印检测指标,而是水印不可见性指标。NC 才直接反映“水印提取成不成功”。测试时两个指标一起看。
7.2 无攻击下的嵌入提取验证
先跑一遍最基本的闭环:
- 运行
watermark_embed.m,生成含水印图像。 - 运行
watermark_extract.m,提取水印。 - 观察 PSNR 和 NC 两个值。
如果这一步 PSNR 低于 25 dB,说明alpha偏大,图像质量损失明显。如果 NC 低于 0.9,大概率是alpha偏小,水印太弱,提取困难。
7.3 常见攻击测试
水印鲁棒性测试的核心是模拟真实场景中的图像处理过程。最常见的攻击包括高斯噪声、JPEG 压缩、中心裁剪、图像缩放等。攻击强度不同,水印表现也不同,所以不要只看一种攻击结果。
以高斯噪声为例,可以在含水印图像上叠加少量噪声,再做水印提取:
% 高斯噪声攻击示例 attacked = imnoise(watermarkedImg, 'gaussian', 0, 0.001); imwrite(attacked, 'attacked_noise.png');这里的watermarkedImg需要是 uint8 类型,如果已经是 double 类型,先转换。
JPEG 压缩攻击示例,可以通过imwrite的Quality参数控制压缩强度:
% JPEG 压缩攻击示例,质量参数越小压缩越强 imwrite(watermarkedImg, 'attacked.jpg', 'Quality', 70); attacked = imread('attacked.jpg');中心裁剪攻击示例:
% 中心区域置黑模拟裁剪攻击 attacked = watermarkedImg; [h, w] = size(attacked); attacked(round(h/4):round(3*h/4), round(w/4):round(3*w/4)) = 0;缩放攻击示例:
% 先缩小再放大回原尺寸 attacked = imresize(watermarkedImg, 0.5); attacked = imresize(attacked, [h, w]);对攻击后的图像直接运行提取脚本逻辑,即可观察 NC 的下降情况。
7.4 批量鲁棒性测试脚本
为了对比不同攻击对水印的影响,可以写一个循环将所有攻击跑完,输出一张结果表。把下面代码保存为test_robustness.m:
% test_robustness.m % DWT-DCT-SVD 水印鲁棒性批量测试 clear; clc; close all; % 读取含水印图像 watermarkedImg = imread('watermarked_lena.png'); if size(watermarkedImg, 3) == 3 watermarkedImg = rgb2gray(watermarkedImg); end watermarkedImg = uint8(watermarkedImg); % 加载密钥 load('watermark_key.mat', 'U', 'V', 'Uw', 'Vw', 'S', 'alpha', 'wname', 'wm_size'); % 原始水印 orig_wm = imread('watermark.png'); if size(orig_wm, 3) == 3 orig_wm = rgb2gray(orig_wm); end % 攻击列表 attackNames = {'无攻击', '高斯噪声', 'JPEG压缩70', '中心裁剪', '缩放攻击'}; PSNR_list = zeros(length(attackNames), 1); NC_list = zeros(length(attackNames), 1); for k = 1:length(attackNames) switch k case 1 attacked = watermarkedImg; case 2 attacked = imnoise(watermarkedImg, 'gaussian', 0, 0.001); case 3 imwrite(watermarkedImg, 'tmp_attack.jpg', 'Quality', 70); attacked = imread('tmp_attack.jpg'); case 4 attacked = watermarkedImg; [h, w] = size(attacked); attacked(round(h/4):round(3*h/4), round(w/4):round(3*w/4)) = 0; case 5 [h, w] = size(watermarkedImg); attacked = imresize(watermarkedImg, 0.5); attacked = imresize(attacked, [h, w]); end if size(attacked, 3) == 3 attacked = rgb2gray(attacked); end attacked = double(attacked); % 攻击后图像相对含水印图像的 PSNR PSNR_list(k) = psnr(uint8(attacked), watermarkedImg); % 提取水印 [LL_a, ~, ~, ~] = dwt2(attacked, wname); LL_a_dct = dct2(LL_a); [~, S_a, ~] = svd(LL_a_dct); Sw_ext = (S_a - S) / alpha; wm_ext = Uw * Sw_ext * Vw'; wm_ext = mat2gray(wm_ext); wm_ext = imresize(wm_ext, wm_size); NC_list(k) = corr2(double(orig_wm), double(wm_ext)); end % 输出结果表 fprintf('%-16s %-16s %-16s\n', '攻击类型', 'PSNR(dB)', 'NC'); for k = 1:length(attackNames) fprintf('%-16s %-16.4f %-16.4f\n', attackNames{k}, PSNR_list(k), NC_list(k)); end运行后可以得到每种攻击下的 PSNR 和 NC 变化趋势。强攻击下 NC 下降是正常现象,对比不同alpha值下的结果,才知道哪个参数更适合你的素材。
8. 资源占用与运行效率观察
8.1 耗时观察
MATLAB 脚本的耗时主要集中在dwt2、dct2、svd和图像缩放。其中 SVD 的复杂度最高,矩阵尺寸越大耗时越明显。可以在嵌入脚本中加tic/toc观察单次耗时:
tic; % 这里放嵌入或提取核心过程 elapsed = toc; fprintf('单次处理耗时:%.4f 秒\n', elapsed);以常见 512x512 灰度图像为例,haar小波基下单次嵌入通常在秒级以内完成,但具体耗时取决于本机 CPU 性能和 MATLAB 版本,建议不要直接套用别人给出的秒数。
8.2 内存占用
代码中图像被转为double之后,内存占用会比 uint8 大很多。512x512 的 double 矩阵大约是 2 MB,如果做多层分解或大量循环,内存增长会很明显。批量测试时建议每轮循环后清理无用的中间变量,或者直接把图像保持在 uint8,只在 SVD 前做局部转换。
8.3 降低计算量的方法
- 用
haar小波,不要一开始就选高复杂度小波基。 - 载体图像先缩放到 256x256 或 512x512 做验证。
- 水印尺寸尽量小,缩放计算量更小。
- 批量测试时关掉
figure显示,避免图形窗口拖慢速度。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
imread报错,找不到图片 | 当前工作目录错误或文件名拼写有误 | pwd查看当前目录,dir查看文件列表 | 使用绝对路径,或把图片放到脚本同目录 |
提示未定义函数dwt2 | 缺少 Wavelet Toolbox | 命令行输入ver查看工具箱 | 安装对应工具箱,或更换 MATLAB 版本 |
提示未定义函数psnr | 缺少 Image Processing Toolbox | which psnr检查函数路径 | 安装工具箱,或自定义 PSNR 计算函数 |
| 含水印图像明显花屏、噪点 | alpha过大导致嵌入过度 | 查看 PSNR,通常越低说明失真越大 | 降低alpha,从 0.01 开始重新测试 |
| 提取水印全是噪点,无法辨认 | alpha太小,或水印本身过小过复杂 | 检查 NC 值,观察水印图复杂度 | 增大alpha,优先用二值 Logo 测试 |
| JPEG 压缩后 NC 明显下降 | 压缩强度过大,奇异值变化超出预期 | 比较Quality=90/70/50三组结果 | 提高alpha,或对载体做分块嵌入 |
| 批处理脚本运行越跑越慢 | 中间变量未清理,图形窗口堆积 | 观察内存占用 | 每轮循环clear中间变量,关闭显示 |
| 提取的水印是反色或亮度异常 | mat2gray归一化导致 | 检查保存前的数值范围 | 增加二值化阈值:wm_ext = wm_ext > 0.5 |
10. 最佳实践与使用建议
10.1 调参策略
不要一开始就追求高鲁棒性。先用alpha = 0.02跑通闭环,观察 PSNR 和 NC,再逐步增大到 0.05、0.1,找到“图像质量可接受且提取水印可辨认”的平衡点。鲁棒性测试最好固定其他参数,只改变alpha,这样结果才有可比性。
10.2 水印素材选择
水印图建议优先使用二值 Logo,背景干净、文字清晰。如果水印是复杂灰度图,提取后肉眼效果会差一些,NC 的阈值判断也更严格。想做更真实的效果,可以把水印设计成带简单图案的 64x64 小图,减少嵌入对载体图像的影响。