
数字水印技术是信息安全领域里很经典的一个研究方向平时做毕业设计或者图像处理相关项目时经常会遇到 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); %% 攻击 2JPEG 压缩 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.98JPEG 压缩Quality500.85 ~ 0.95裁剪 1/40.80 ~ 0.92中值滤波 3x30.88 ~ 0.96如果攻击后 NC 值偏低可以尝试增大嵌入强度 $\alpha$提升鲁棒性。使用更小分块让水印分布更分散。对提取结果做后处理如形态学滤波、中值滤波去除孤立点。6. 性能评价指标衡量水印算法好坏通常需要关注不可见性和鲁棒性两个维度。6.1 不可见性评价PSNRPSNR峰值信噪比用来衡量含水印图像与原始载体图像之间的差异单位是 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 鲁棒性评价NCNC归一化相关系数计算提取水印与原始水印的相似程度$$NC \frac{\sum_{i1}^{M} \sum_{j1}^{N} W(i,j) \cdot W(i,j)}{\sqrt{\sum_{i1}^{M} \sum_{j1}^{N} W(i,j)^2} \cdot \sqrt{\sum_{i1}^{M} \sum_{j1}^{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 图转为灰度图统一使用rgb2gray7.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 值这样可以更直观地找到最优参数组合。如果遇到代码问题欢迎对照本文的排查表格逐项检查。