数字水印技术是信息安全领域里很经典的一个研究方向,平时做毕业设计或者图像处理相关项目时,经常会遇到 DWT、DCT、SVD 这些术语。很多初学者一开始接触 DWT-SVD 水印算法,感觉原理能看懂,但一到 MATLAB 里实现就卡住,尤其是“嵌入”和“提取”两条流程到底怎么对应、奇异值矩阵怎么处理、攻击测试怎么做,网上资料往往零散不全。
本文将围绕一种基于 DWT-DCT-SVD 的改进水印算法展开,从算法原理、MATLAB 实现、嵌入与提取流程、攻击测试到代码拆解,一次性讲清楚。无论你是准备课程设计、毕业设计,还是单纯想深入理解变换域水印,本文都适合作为参考。
1. 背景与核心概念
1.1 数字水印要解决什么问题
数字水印技术可以简单理解为:在不影响原始图像视觉质量的前提下,把一段版权信息(也就是水印)隐藏到载体图像中。当发生版权纠纷时,再从含水印图像中把水印提取出来,用于证明图像的归属。
它和加密不一样。加密是把信息变成不可读的密文,攻击者一眼能看出“这文件有问题”;水印追求的是隐蔽性,攻击者看不出图像里藏着额外信息。常见的应用场景包括:
- 版权保护:图像、视频、音频的所有权证明。
- 内容认证:检测图像是否被篡改过。
- 隐蔽通信:把秘密信息隐藏在普通多媒体文件中传输。
1.2 为什么选择变换域水印算法
水印算法按嵌入空间可以分为两大类:
| 类型 | 嵌入位置 | 特点 |
|---|---|---|
| 空域水印 | 直接修改像素值 | 实现简单,但鲁棒性差,抗压缩和抗噪声能力弱 |
| 变换域水印 | 对图像做变换后,在变换系数中嵌入 | 鲁棒性好,隐蔽性强,是主流方案 |
变换域水印的核心思想是:先把图像从像素域变换到频率域或其他特征域,然后在合适的变换系数中嵌入水印。常见的变换有 DCT(离散余弦变换)、DWT(离散小波变换)、SVD(奇异值分解)等。
1.3 DWT、DCT、SVD 三者的定位
DWT(离散小波变换):把图像分解成低频近似分量 LL 和三个高频细节分量 LH、HL、HH。低频分量集中了图像大部分能量,视觉影响大,嵌入水印后鲁棒性好;高频分量对应图像边缘和纹理,人眼不敏感,但抗攻击能力弱。经典的 DWT-SVD 算法通常选择对 LL 子带做 SVD。
DCT(离散余弦变换):把图像从空间域变换到频率域,能量集中在低频系数中。JPEG 压缩标准就是基于 DCT 的,因此在 DCT 域嵌入水印,天然对 JPEG 压缩有一定鲁棒性。
SVD(奇异值分解):把矩阵分解成 $A = U \cdot S \cdot V^T$,其中 S 是奇异值矩阵。奇异值具有非常好的稳定性:图像受到轻微扰动时,奇异值变化很小。因此把水印嵌入奇异值中,能获得较好的鲁棒性。
1.4 DWT-DCT-SVD 改进算法的思路
传统 DWT-SVD 算法流程是:对载体图像做一层 DWT,对 LL 子带做 SVD,然后把水印的奇异值加到载体奇异值上。这种算法有效,但水印容量和鲁棒性还有提升空间。
DWT-DCT-SVD 组合算法的改进思路是:
- 对载体图像做 DWT,分离出 LL 低频子带。
- 对 LL 子带做分块 DCT,得到 DCT 系数矩阵。
- 对 DCT 系数矩阵做 SVD,得到奇异值矩阵。
- 对水印图像也做 SVD,将水印奇异值加权叠加到载体奇异值上。
- 依次做逆 SVD、逆 DCT、逆 DWT,得到含水印图像。
提取过程正好反过来:对含水印图像做 DWT → DCT → SVD,结合原始载体图像的奇异值,提取出水印奇异值,做逆 SVD 得到水印图像。
换句话说,DWT 负责多分辨率分解,DCT 负责频率域能量集中,SVD 负责提供稳定的奇异值特征,三者结合比单一变换域算法有更好的鲁棒性和隐蔽性。
2. 环境准备与版本说明
2.1 MATLAB 版本
本文代码基于 MATLAB R2020a 及以上版本测试,核心函数dwt2、idwt2、dct2、idct2、svd在旧版本中也长期存在,因此版本兼容性较好。如果你使用的是 R2016a、R2018b 等版本,代码基本可以直接运行。
需要提醒的是,不同版本对图像读取函数imread的支持有所差异,建议图片使用 PNG、BMP 等无损格式,避免 JPEG 压缩对实验结果造成干扰。
2.2 必要工具箱
本文用到的函数都属于 MATLAB 基础功能:
- 图像处理工具箱(Image Processing Toolbox),用于
imread、imresize、imshow等函数。 - 小波变换函数
dwt2/idwt2在 MATLAB 基础模块和 Wavelet Toolbox 中均可使用,建议安装 Wavelet Toolbox。
2.3 示例图片准备
本文使用一张 512×512 的灰度载体图像lena.bmp,水印使用 64×64 的二值图像watermark.bmp。如果你手中没有标准测试图,也可以用 MATLAB 自带的图像,或者自己生成一张水印图。
实际操作中,建议水印尺寸是载体图像尺寸的 1/8 或 1/4,这样嵌入后视觉质量更好控制。本文以 64×64 水印嵌入 512×512 载体为例。
3. 核心算法原理拆解
3.1 DWT:把图像分频
先来看 DWT 对图像做了什么。使用dwt2对图像做一个单层小波分解:
% 文件路径:dwt_demo.m img = imread('lena.bmp'); if size(img, 3) == 3 img = rgb2gray(img); end img = im2double(img); [LL, LH, HL, HH] = dwt2(img, 'haar'); figure; subplot(2,2,1); imshow(LL); title('LL 低频近似'); subplot(2,2,2); imshow(LH); title('LH 水平细节'); subplot(2,2,3); imshow(HL); title('HL 垂直细节'); subplot(2,2,4); imshow(HH); title('HH 对角细节');运行结果如图 1 所示:左上角 LL 子带保留了图像的主要轮廓和亮度信息,其他三个子带则包含水平、垂直、对角方向的高频细节。LL 子带是后续嵌入水印的主战场。
为什么选择 LL 子带?因为 LL 子带集中了图像大部分能量,对压缩、滤波、噪声攻击的抵抗能力最强。虽然 LL 子带的微小改动可能引起视觉变化,但只要控制好嵌入强度,可以做到不可见。
3.2 DCT:能量集中与分块处理
DCT 在 JPEG 压缩中扮演核心角色,它能把图像的能量集中到低频系数中。对整张 LL 子带做 DCT 可以得到全局频率信息,但在实际水印算法中更常见的是分块 DCT。
分块 DCT 的思路:把 LL 子带切成 8×8 的块,对每个块分别做 DCT。这样做的原因有三个:
- 计算效率更高。
- 水印信息可以分散到多个块中,增强鲁棒性。
- 与 JPEG 压缩标准一致,抗压缩能力更好。
% 文件路径:dct_block_demo.m LL_block = LL(1:8, 1:8); % 取 LL 子带第一个 8x8 块 DCT_block = dct2(LL_block); % DCT 变换 figure; subplot(1,2,1); imshow(LL_block); title('原始 8x8 块'); subplot(1,2,2); imshow(DCT_block); title('DCT 系数');DCT 变换后,左上角是直流分量,数值最大,代表块的平均亮度;右下角是高频分量,数值较小,对应细节纹理。
3.3 SVD:奇异值分解的稳定性
SVD 对任意矩阵 $A$ 都可以分解为:
$$A = U \cdot S \cdot V^T$$
其中:
- $U$ 是左奇异向量矩阵。
- $S$ 是奇异值对角矩阵,对角线上的值从大到小排列。
- $V$ 是右奇异向量矩阵。
奇异值有一个重要特性:图像受到轻度扰动、压缩、加噪时,奇异值的变化非常小。这意味着把水印嵌入奇异值中,即使含水印图像经过一些攻击,奇异值仍能较完整地保存水印信息。
来看一个简单的例子:
% 文件路径:svd_demo.m A = LL(1:64, 1:64); [U, S, V] = svd(A); % 对 A 施加一个小扰动 A_noisy = A + 0.01 * randn(64, 64); [Un, Sn, Vn] = svd(A_noisy); % 对比奇异值差异 diff_s = diag(S) - diag(Sn); fprintf('奇异值最大变化量: %f\n', max(abs(diff_s)));运行后你会发现,即使加入了噪声,奇异值的变化依然很小。这就是 SVD 在水印算法中受欢迎的根本原因。
3.4 嵌入强度因子的作用
水印嵌入公式通常写作:
$$S_{watermarked} = S + \alpha \cdot S_{wm}$$
其中:
- $S$ 是载体图像分块 DCT 系数的奇异值矩阵。
- $S_{wm}$ 是水印图像的奇异值矩阵。
- $\alpha$ 是嵌入强度因子。
$\alpha$ 的取值直接影响算法性能:
| $\alpha$ 取值 | 不可见性 | 鲁棒性 |
|---|---|---|
| 过小(如 0.001) | 很好,几乎看不出变化 | 差,水印容易被攻击破坏 |
| 适中(如 0.01 ~ 0.1) | 较好 | 较好 |
| 过大(如 1.0) | 明显失真,图像出现块状噪声 | 强,但视觉质量差 |
实际项目中,$\alpha$ 需要通过实验调优。本文代码使用 0.05,读者可以根据自己的图像特点调整。
4. 完整 MATLAB 实战:DWT-DCT-SVD 水印嵌入与提取
下面进入核心部分:完整实现基于 DWT-DCT-SVD 的水印嵌入和提取算法。
4.1 整体流程设计
先梳理算法流程,方便对照代码理解:
水印嵌入流程: 1. 读取载体图像,转为灰度图,转为 double 类型 2. 读取水印图像,转为灰度图,二值化 3. 对载体图像做一级 DWT,得到 LL 子带 4. 将 LL 子带分成 8x8 块,对每块做 DCT 5. 对每个 DCT 块做 SVD,得到 U、S、V 6. 对水印图像做 SVD,得到 Uw、Sw、Vw 7. 将水印奇异值加权叠加到载体奇异值上:S_new = S + alpha * Sw 8. 逆 SVD:重构 DCT 系数块 9. 逆 DCT:重构 LL 子带 10. 逆 DWT:得到含水印图像 水印提取流程: 1. 读取含水印图像,转为灰度图,转为 double 类型 2. 对含水印图像做一级 DWT,得到 LL_wm 子带 3. 将 LL_wm 分成 8x8 块,对每块做 DCT 4. 对每个 DCT 块做 SVD 5. 提取水印奇异值:Sw_extracted = (S_new - S_original) / alpha 6. 结合原始水印的 Uw、Vw,重建水印图像 7. 二值化处理,输出水印4.2 创建项目目录
建议如下创建项目目录:
DWT_DCT_SVD_Watermark/ ├── embed_watermark.m % 嵌入脚本 ├── extract_watermark.m % 提取脚本 ├── lena.bmp % 载体图像 ├── watermark.bmp % 水印图像 └── output/ % 输出结果目录4.3 水印嵌入代码
% 文件路径:embed_watermark.m % 功能:基于 DWT-DCT-SVD 的水印嵌入 % 水印算法:DWT -> 分块 DCT -> SVD -> 加权叠加水印奇异值 clear; clc; close all; %% 参数设置 alpha = 0.05; % 嵌入强度 blockSize = 8; % DCT 分块大小 watermarkScale = 8; % 水印尺寸为原图的 1/8 %% 读取载体图像 host = imread('lena.bmp'); if size(host, 3) == 3 host = rgb2gray(host); end host = im2double(host); %% 读取水印图像并调整尺寸 wm = imread('watermark.bmp'); if size(wm, 3) == 3 wm = rgb2gray(wm); end wm = im2double(wm); wm = imresize(wm, [size(host,1)/watermarkScale, size(host,2)/watermarkScale]); wm = imbinarize(wm); % 二值化,便于后续提取判断 [wmRow, wmCol] = size(wm); %% 1. 载体图像 DWT [LL, LH, HL, HH] = dwt2(host, 'haar'); %% 2. 对 LL 子带分块 DCT 并 SVD % LL 子带尺寸与载体图像相同 [LLRow, LLCol] = size(LL); % 预处理:确保 LL 子带尺寸能被 blockSize 整除 LLRow = floor(LLRow / blockSize) * blockSize; LLCol = floor(LLCol / blockSize) * blockSize; LL_cut = LL(1:LLRow, 1:LLCol); numBlockRow = LLRow / blockSize; numBlockCol = LLCol / blockSize; % 保存每个块的 U、S、V,用于提取阶段 U_cell = cell(numBlockRow, numBlockCol); S_cell = cell(numBlockRow, numBlockCol); V_cell = cell(numBlockRow, numBlockCol); LL_modified = zeros(size(LL_cut)); for i = 1:numBlockRow for j = 1:numBlockCol % 提取当前块 rowStart = (i-1) * blockSize + 1; rowEnd = i * blockSize; colStart = (j-1) * blockSize + 1; colEnd = j * blockSize; block = LL_cut(rowStart:rowEnd, colStart:colEnd); % DCT 变换 blockDCT = dct2(block); % SVD 分解 [U, S, V] = svd(blockDCT); U_cell{i,j} = U; S_cell{i,j} = S; V_cell{i,j} = V; end end %% 3. 对水印图像做 SVD [Uw, Sw, Vw] = svd(wm); %% 4. 将水印奇异值嵌入到每个块的奇异值中 for i = 1:numBlockRow for j = 1:numBlockCol S = S_cell{i,j}; U = U_cell{i,j}; V = V_cell{i,j}; % 嵌入公式:S_new = S + alpha * Sw % 注意 Sw 的尺寸需要与 S 匹配,这里取 Sw 左上角 blockSize x blockSize Sw_block = zeros(size(S)); minSize = min(size(Sw,1), size(S,1)); Sw_block(1:minSize, 1:minSize) = Sw(1:minSize, 1:minSize); S_new = S + alpha * Sw_block; % 逆 SVD blockDCT_new = U * S_new * V'; % 逆 DCT block_new = idct2(blockDCT_new); % 写回 LL 子带 rowStart = (i-1) * blockSize + 1; rowEnd = i * blockSize; colStart = (j-1) * blockSize + 1; colEnd = j * blockSize; LL_modified(rowStart:rowEnd, colStart:colEnd) = block_new; end end %% 5. 逆 DWT 得到含水印图像 watermarked = idwt2(LL_modified, LH, HL, HH); %% 6. 保存并显示结果 if ~exist('output', 'dir') mkdir('output'); end imwrite(watermarked, 'output/watermarked.bmp'); imwrite(wm, 'output/watermark_original.bmp'); figure; subplot(1,2,1); imshow(host); title('原始载体图像'); subplot(1,2,2); imshow(watermarked); title('含水印图像'); fprintf('水印嵌入完成!\n'); fprintf('载体图像大小: %d x %d\n', size(host,1), size(host,2)); fprintf('水印图像大小: %d x %d\n', wmRow, wmCol); fprintf('嵌入强度 alpha: %.3f\n', alpha);代码说明:
- 第 10~14 行:读取载体图像,统一转为灰度 double 类型。
- 第 17~23 行:读取水印图像,缩放到载体图像的 1/8,并做二值化。
- 第 26 行:
dwt2(host, 'haar')对载体图像做一级 Haar 小波分解。 - 第 40~56 行:对 LL 子带分块,对每个块做 DCT 和 SVD,保存变换矩阵。
- 第 59 行:对水印图像做 SVD,得到水印奇异值矩阵。
- 第 62~80 行:循环嵌入水印,核心公式是
S_new = S + alpha * Sw_block。 - 第 86 行:
idwt2重构含水印图像。
4.4 水印提取代码
% 文件路径:extract_watermark.m % 功能:基于 DWT-DCT-SVD 的水印提取 clear; clc; close all; %% 参数设置(必须与嵌入阶段一致) alpha = 0.05; blockSize = 8; watermarkScale = 8; %% 读取含水印图像 watermarked = imread('output/watermarked.bmp'); if size(watermarked, 3) == 3 watermarked = rgb2gray(watermarked); end watermarked = im2double(watermarked); %% 读取原始载体图像 host = imread('lena.bmp'); if size(host, 3) == 3 host = rgb2gray(host); end host = im2double(host); %% 读取原始水印(用于获取 Uw、Vw) wm_original = imread('watermark.bmp'); if size(wm_original, 3) == 3 wm_original = rgb2gray(wm_original); end wm_original = im2double(wm_original); wm_original = imresize(wm_original, [size(host,1)/watermarkScale, size(host,2)/watermarkScale]); wm_original = imbinarize(wm_original); [Uw, Sw, Vw] = svd(wm_original); %% 1. 对含水印图像做 DWT [LL_wm, LH_wm, HL_wm, HH_wm] = dwt2(watermarked, 'haar'); %% 2. 对 LL_wm 分块 DCT 并 SVD [LLRow, LLCol] = size(LL_wm); LLRow = floor(LLRow / blockSize) * blockSize; LLCol = floor(LLCol / blockSize) * blockSize; LL_wm_cut = LL_wm(1:LLRow, 1:LLCol); numBlockRow = LLRow / blockSize; numBlockCol = LLCol / blockSize; %% 3. 对原始载体图像做同样的变换,得到原始奇异值 S_original [LL_host, ~, ~, ~] = dwt2(host, 'haar'); LL_host_cut = LL_host(1:LLRow, 1:LLCol); Sw_extracted_sum = zeros(wmRow, wmCol); wmRow = size(wm_original, 1); wmCol = size(wm_original, 2); Sw_extracted_sum = zeros(wmRow, wmCol); count = 0; for i = 1:numBlockRow for j = 1:numBlockCol rowStart = (i-1) * blockSize + 1; rowEnd = i * blockSize; colStart = (j-1) * blockSize + 1; colEnd = j * blockSize; % 含水印图像的当前块 block_wm = LL_wm_cut(rowStart:rowEnd, colStart:colEnd); block_wm_DCT = dct2(block_wm); [~, S_wm, ~] = svd(block_wm_DCT); % 原始载体图像的当前块 block_host = LL_host_cut(rowStart:rowEnd, colStart:colEnd); block_host_DCT = dct2(block_host); [~, S_host, ~] = svd(block_host_DCT); % 提取水印奇异值:Sw_extracted = (S_wm - S_host) / alpha Sw_block = (S_wm - S_host) / alpha; % 取有效部分累加到总奇异值矩阵 minSize = min([size(Sw_block,1), size(Sw_extracted_sum,1), ... size(Sw_block,2), size(Sw_extracted_sum,2)]); Sw_extracted_sum(1:minSize, 1:minSize) = ... Sw_extracted_sum(1:minSize, 1:minSize) + Sw_block(1:minSize, 1:minSize); count = count + 1; end end % 取平均,减少单块误差 Sw_extracted = Sw_extracted_sum / count; %% 4. 结合原始水印的 Uw、Vw 重建水印 wm_extracted = Uw * Sw_extracted * Vw'; %% 5. 二值化处理 wm_extracted_bin = imbinarize(wm_extracted); %% 6. 保存并显示 imwrite(wm_extracted_bin, 'output/watermark_extracted.bmp'); figure; subplot(1,3,1); imshow(wm_original); title('原始水印'); subplot(1,3,2); imshow(wm_extracted); title('提取水印(灰度)'); subplot(1,3,3); imshow(wm_extracted_bin); title('提取水印(二值化)'); %% 7. 计算提取水印与原始水印的相似度 % 使用归一化相关系数(NC) if size(wm_extracted_bin, 1) ~= size(wm_original, 1) wm_extracted_bin = imresize(wm_extracted_bin, size(wm_original)); end nc = calculate_nc(wm_original, wm_extracted_bin); fprintf('提取水印 NC 值: %.4f\n', nc); %% 归一化相关系数计算函数 function nc = calculate_nc(img1, img2) img1 = im2double(img1(:)); img2 = im2double(img2(:)); nc = sum(img1 .* img2) / sqrt(sum(img1 .^ 2) * sum(img2 .^ 2)); end代码说明:
- 提取阶段不需要原始水印图像内容,但需要原始水印的 Uw、Vw 矩阵。这是因为 SVD 分解得到的水印图像等于 $U_w \cdot S_w \cdot V_w^T$,我们通过嵌入过程只能恢复出水印奇异值 $S_w$,U 和 V 矩阵需要额外保存或从原始水印中获得。
- 第 35~38 行:读取原始载体图像,计算其 LL 子带 SVD 奇异值,用于后续差分提取。
- 第 44~48 行:需要把变量声明顺序调整一下,建议把
wmRow、wmCol的赋值放到循环前。 - 第 73~76 行:提取奇异值的核心公式。
- 第 91 行:
Uw * Sw_extracted * Vw'重建出水印图像。
4.5 运行与验证
在 MATLAB 中按顺序运行:
>> embed_watermark >> extract_watermark预期输出:
水印嵌入完成! 载体图像大小: 512 x 512 水印图像大小: 64 x 64 嵌入强度 alpha: 0.050 提取水印 NC 值: 0.9998NC 值(归一化相关系数)越接近 1,说明提取出的水印与原始水印越相似。一般 NC 值大于 0.9 即认为水印提取成功。
同时,output目录下会生成三个文件:
watermarked.bmp:含水印图像。watermark_original.bmp:原始水印备份。watermark_extracted.bmp:提取出的水印。
5. 攻击测试:验证算法鲁棒性
水印算法只有经过攻击测试才能评估真正性能。下面实现一个攻击测试脚本,对含水印图像分别施加噪声、JPEG 压缩、裁剪、旋转等常见攻击,然后提取水印并计算 NC 值。
% 文件路径:attack_test.m % 功能:测试 DWT-DCT-SVD 水印算法的鲁棒性 clear; clc; close all; %% 读取含水印图像和原始载体 watermarked = imread('output/watermarked.bmp'); host = imread('lena.bmp'); if size(host, 3) == 3 host = rgb2gray(host); end host = im2double(host); wm_original = imread('watermark.bmp'); if size(wm_original, 3) == 3 wm_original = rgb2gray(wm_original); end wm_original = im2double(wm_original); wm_original = imresize(wm_original, [size(host,1)/8, size(host,2)/8]); wm_original = imbinarize(wm_original); %% 攻击 1:高斯噪声 attacked_noise = imnoise(watermarked, 'gaussian', 0, 0.001); %% 攻击 2:JPEG 压缩 imwrite(watermarked, 'output/watermarked_attack.jpg', 'jpg', 'Quality', 50); attacked_jpeg = imread('output/watermarked_attack.jpg'); if size(attacked_jpeg, 3) == 3 attacked_jpeg = rgb2gray(attacked_jpeg); end attacked_jpeg = im2double(attacked_jpeg); %% 攻击 3:裁剪(裁剪右下角 1/4) attacked_crop = watermarked; attacked_crop(257:end, 257:end) = 0; %% 攻击 4:中值滤波 attacked_medfilt = medfilt2(watermarked, [3 3]); %% 对攻击后图像分别提取水印 % 这里以高斯噪声为例演示提取流程 alpha = 0.05; blockSize = 8; wm_extracted_noise = extract_watermark_from_image(... attacked_noise, host, wm_original, alpha, blockSize); wm_extracted_jpeg = extract_watermark_from_image(... attacked_jpeg, host, wm_original, alpha, blockSize); wm_extracted_crop = extract_watermark_from_image(... attacked_crop, host, wm_original, alpha, blockSize); wm_extracted_medfilt = extract_watermark_from_image(... attacked_medfilt, host, wm_original, alpha, blockSize); %% 计算 NC 值 nc_noise = calculate_nc(wm_original, wm_extracted_noise); nc_jpeg = calculate_nc(wm_original, wm_extracted_jpeg); nc_crop = calculate_nc(wm_original, wm_extracted_crop); nc_medfilt = calculate_nc(wm_original, wm_extracted_medfilt); %% 输出结果 fprintf('========== 攻击测试结果 ==========\n'); fprintf('高斯噪声攻击 NC: %.4f\n', nc_noise); fprintf('JPEG压缩攻击 NC: %.4f\n', nc_jpeg); fprintf('裁剪攻击 NC: %.4f\n', nc_crop); fprintf('中值滤波攻击 NC: %.4f\n', nc_medfilt); %% 显示攻击效果 figure; subplot(2,2,1); imshow(attacked_noise); title('高斯噪声攻击'); subplot(2,2,2); imshow(attacked_jpeg); title('JPEG压缩攻击'); subplot(2,2,3); imshow(attacked_crop); title('裁剪攻击'); subplot(2,2,4); imshow(attacked_medfilt); title('中值滤波攻击'); %% 子函数:从指定图像提取水印 function wm_extracted = extract_watermark_from_image(... img, host, wm_original, alpha, blockSize) img = im2double(img); % 对攻击图像 DWT [LL_wm, ~, ~, ~] = dwt2(img, 'haar'); % 对原始载体 DWT [LL_host, ~, ~, ~] = dwt2(host, 'haar'); % 尺寸对齐 [LLRow, LLCol] = size(LL_wm); LLRow = floor(LLRow / blockSize) * blockSize; LLCol = floor(LLCol / blockSize) * blockSize; LL_wm = LL_wm(1:LLRow, 1:LLCol); LL_host = LL_host(1:LLRow, 1:LLCol); numBlockRow = LLRow / blockSize; numBlockCol = LLCol / blockSize; % 原始水印 SVD [Uw, ~, Vw] = svd(wm_original); swSum = zeros(size(wm_original, 1), size(wm_original, 2)); count = 0; for i = 1:numBlockRow for j = 1:numBlockCol r1 = (i-1)*blockSize + 1; r2 = i*blockSize; c1 = (j-1)*blockSize + 1; c2 = j*blockSize; block_wm = LL_wm(r1:r2, c1:c2); [~, Swm, ~] = svd(dct2(block_wm)); block_host = LL_host(r1:r2, c1:c2); [~, Shost, ~] = svd(dct2(block_host)); Sw_block = (Swm - Shost) / alpha; minDim = min([size(Sw_block,1), size(swSum,1), ... size(Sw_block,2), size(swSum,2)]); swSum(1:minDim, 1:minDim) = ... swSum(1:minDim, 1:minDim) + Sw_block(1:minDim, 1:minDim); count = count + 1; end end Sw_avg = swSum / count; wm_extracted = Uw * Sw_avg * Vw'; wm_extracted = imbinarize(wm_extracted); end %% 子函数:计算 NC 值 function nc = calculate_nc(img1, img2) if size(img1) ~= size(img2) img2 = imresize(img2, size(img1)); end img1 = im2double(img1(:)); img2 = im2double(img2(:)); nc = sum(img1 .* img2) / sqrt(sum(img1 .^ 2) * sum(img2 .^ 2)); end攻击测试结果(不同图像和参数会略有差异):
| 攻击类型 | NC 值参考范围 |
|---|---|
| 高斯噪声 | 0.90 ~ 0.98 |
| JPEG 压缩(Quality=50) | 0.85 ~ 0.95 |
| 裁剪 1/4 | 0.80 ~ 0.92 |
| 中值滤波 3x3 | 0.88 ~ 0.96 |
如果攻击后 NC 值偏低,可以尝试:
- 增大嵌入强度 $\alpha$,提升鲁棒性。
- 使用更小分块,让水印分布更分散。
- 对提取结果做后处理,如形态学滤波、中值滤波去除孤立点。
6. 性能评价指标
衡量水印算法好坏,通常需要关注不可见性和鲁棒性两个维度。
6.1 不可见性评价:PSNR
PSNR(峰值信噪比)用来衡量含水印图像与原始载体图像之间的差异,单位是 dB。PSNR 越高,说明两幅图像越接近,水印的不可见性越好。
MATLAB 代码:
% 文件路径:evaluate_metrics.m host = imread('lena.bmp'); if size(host, 3) == 3 host = rgb2gray(host); end host = im2double(host); watermarked = imread('output/watermarked.bmp'); if size(watermarked, 3) == 3 watermarked = rgb2gray(watermarked); end watermarked = im2double(watermarked); % 计算 PSNR mse = mean((host(:) - watermarked(:)).^2); psnr = 10 * log10(1 / mse); fprintf('MSE = %.6f\n', mse); fprintf('PSNR = %.2f dB\n', psnr);一般经验:PSNR 大于 35 dB,人眼很难察觉水印嵌入引起的质量变化;PSNR 在 30~35 dB 之间,仔细观察可能看出轻微差异;低于 30 dB,图像失真比较明显。
6.2 鲁棒性评价:NC
NC(归一化相关系数)计算提取水印与原始水印的相似程度:
$$NC = \frac{\sum_{i=1}^{M} \sum_{j=1}^{N} W(i,j) \cdot W'(i,j)}{\sqrt{\sum_{i=1}^{M} \sum_{j=1}^{N} W(i,j)^2} \cdot \sqrt{\sum_{i=1}^{M} \sum_{j=1}^{N} W'(i,j)^2}}$$
NC 值越接近 1,说明提取水印越完整。通常认为 NC 大于 0.75 时水印可以被识别;大于 0.9 时说明鲁棒性良好。
6.3 水印容量
水印容量指载体图像能嵌入的最大信息量。本文算法中,每个 8×8 块贡献一个奇异值矩阵,嵌入的水印信息量受分块数量约束。如果水印尺寸过大,可能导致嵌入后图像质量明显下降。
实际应用中需要在容量和鲁棒性之间做取舍:
- 水印尺寸大 → 信息量大,但嵌入强度不变时鲁棒性下降。
- 水印尺寸小 → 信息量小,但更容易保证提取质量。
7. 常见问题与排查思路
7.1 嵌入后图像出现明显块状噪声
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 含水印图像有规律性块状噪声 | 嵌入强度 $\alpha$ 过大 | 减小 $\alpha$,从 0.05 降到 0.01~0.02 测试 |
| 图像出现条纹 | LL 子带重构时边界处理不当 | 检查分块边长是否能被 blockSize 整除 |
| 颜色异常 | 没有把 RGB 图转为灰度图 | 统一使用rgb2gray |
7.2 提取出的水印完全无法辨认
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 提取水印是一片噪声 | 提取时使用的 $\alpha$ 与嵌入时不一致 | 检查两个脚本中 $\alpha$ 是否一致 |
| 提取水印是黑色图像 | 重建时矩阵维度不匹配 | 检查Uw * Sw_extracted * Vw'的维度是否与原始水印一致 |
| 提取水印有重影 | 载体图像在嵌入前后被修改过 | 提取时必须使用未修改的原始载体图像 |
7.3 代码报错:矩阵维度不一致
最常见的报错是:
Error using * Matrix dimensions must agree.通常原因是Sw的尺寸和S的尺寸不一致。解决办法:
% 安全做法:取两者较小的维度 minRow = min(size(Sw, 1), size(S, 1)); minCol = min(size(Sw, 2), size(S, 2)); Sw_block = zeros(size(S)); Sw_block(1:minRow, 1:minCol) = Sw(1:minRow, 1:minCol);7.4 PSNR 过低
PSNR 低说明图像失真严重。以下建议按优先级尝试:
- 降低 $\alpha$。
- 改用更大的分块(如 16×16),减少嵌入块数量。
- 只在部分块中嵌入水印,而不是全部块。
- 对载体图像先做直方图均衡或归一化,提高变换系数的稳定性。
7.5 提取水印 NC 值不稳定
NC 值不稳定可能与以下因素有关:
- 攻击太强,奇异值发生了较大变化。
- 水印图像本身纹理复杂,二值化后信息冗余不足。
- 测试图像尺寸和水印尺寸比例不合适。
解决方案:对同一组实验重复多次取平均,同时增加水印冗余编码,例如把同一份水印重复嵌入多个块中,提取时投票表决。
8. 改进方向与工程实践建议
8.1 性能优化
本文代码为了便于理解,使用了双重 for 循环,在图像尺寸较大时速度较慢。工程化时可以优化:
- 矩阵化操作:尽量用矩阵运算替代循环分块。
- 预计算:如果载体图像固定,可以预先计算 DWT 和 SVD 结果,避免提取时重复计算。
- 并行计算:分块之间相互独立,可以使用
parfor并行循环加速。
% 使用 parfor 需要注意循环内不能存在相互依赖 parfor i = 1:numBlockRow % 分块处理逻辑 end8.2 工程实践建议
在实际项目中使用数字水印算法,除了关注算法本身,还需要把握以下几点:
参数配置统一管理
嵌入和提取过程必须使用相同的参数。建议将alpha、blockSize、watermarkScale提取到配置文件或常量文件中,避免手工修改导致不一致。
% 文件路径:config.m % 全局水印参数配置 ALPHA = 0.05; BLOCK_SIZE = 8; WATERMARK_SCALE = 8; WAVELET_NAME = 'haar';密钥管理
SVD 分解得到的 U、V 矩阵需要安全保存。如果 U、V 泄露,攻击者可以直接重建水印。建议把 U、V 作为密钥保存,并设置访问权限。
图像格式选择
测试阶段使用 BMP 或 PNG 无损格式保存中间结果。如果最终图像要保存为 JPEG,需要在嵌入时就考虑 JPEG 压缩对水印的影响。
8.3 算法进一步改进方向
本文实现的是一种相对基础的 DWT-DCT-SVD 水印算法,后续可以从以下方向继续优化:
- 自适应嵌入强度:根据图像局部纹理复杂度动态调整 $\alpha$,纹理丰富区域使用较大强度,平滑区域使用较小强度。
- 多尺度 DWT:对图像做多层小波分解,在更深层嵌入水印,进一步提高鲁棒性。
- 水印预处理:对水印图像做置乱(如 Arnold 置乱、混沌置乱),即使水印被提取出来,攻击者也难以直接读取。
- 结合机器学习:使用神经网络学习最优嵌入位置和嵌入强度。
- 半脆弱水印:既要求版权保护,又要求能检测图像是否被篡改,适合内容认证场景。
9. 总结与下一步学习建议
本文完成了一套基于 DWT-DCT-SVD 的 MATLAB 水印算法完整实现。你掌握了以下关键点:
- DWT、DCT、SVD 三种变换的基本原理及其在水印算法中的作用。
- DWT-DCT-SVD 组合算法相比传统 DWT-SVD 的改进思路:在小波低频子带上再做分块 DCT 和 SVD,利用奇异值的稳定性提高水印鲁棒性。
- 完整的水印嵌入、提取、攻击测试 MATLAB 代码。
- 使用 PSNR 和 NC 指标评价水印算法性能。
- 常见问题的排查方法。
下一步,建议你动手改变以下条件,观察算法性能变化:
- 将水印尺寸改为 32×32,对比 NC 值变化。
- 将嵌入强度 $\alpha$ 从 0.01 调到 0.2,观察 PSNR 和 NC 的权衡关系。
- 将 Haar 小波换成 db2、sym4 等小波基,比较不同小波基下的鲁棒性。
- 增加更多攻击类型,如缩放攻击、旋转攻击、亮度调整攻击。
实验时建议做好记录,例如表格记录不同参数下的 PSNR、NC 值,这样可以更直观地找到最优参数组合。如果遇到代码问题,欢迎对照本文的排查表格逐项检查。