DWT-DCT-SVD数字水印算法原理与MATLAB实现详解
2026/9/24 17:02:13 网站建设 项目流程

数字水印技术是信息安全领域里很经典的一个研究方向,平时做毕业设计或者图像处理相关项目时,经常会遇到 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 组合算法的改进思路是:

  1. 对载体图像做 DWT,分离出 LL 低频子带。
  2. 对 LL 子带做分块 DCT,得到 DCT 系数矩阵。
  3. 对 DCT 系数矩阵做 SVD,得到奇异值矩阵。
  4. 对水印图像也做 SVD,将水印奇异值加权叠加到载体奇异值上。
  5. 依次做逆 SVD、逆 DCT、逆 DWT,得到含水印图像。

提取过程正好反过来:对含水印图像做 DWT → DCT → SVD,结合原始载体图像的奇异值,提取出水印奇异值,做逆 SVD 得到水印图像。

换句话说,DWT 负责多分辨率分解,DCT 负责频率域能量集中,SVD 负责提供稳定的奇异值特征,三者结合比单一变换域算法有更好的鲁棒性和隐蔽性。

2. 环境准备与版本说明

2.1 MATLAB 版本

本文代码基于 MATLAB R2020a 及以上版本测试,核心函数dwt2idwt2dct2idct2svd在旧版本中也长期存在,因此版本兼容性较好。如果你使用的是 R2016a、R2018b 等版本,代码基本可以直接运行。

需要提醒的是,不同版本对图像读取函数imread的支持有所差异,建议图片使用 PNG、BMP 等无损格式,避免 JPEG 压缩对实验结果造成干扰。

2.2 必要工具箱

本文用到的函数都属于 MATLAB 基础功能:

  • 图像处理工具箱(Image Processing Toolbox),用于imreadimresizeimshow等函数。
  • 小波变换函数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。这样做的原因有三个:

  1. 计算效率更高。
  2. 水印信息可以分散到多个块中,增强鲁棒性。
  3. 与 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 行:需要把变量声明顺序调整一下,建议把wmRowwmCol的赋值放到循环前。
  • 第 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.9998

NC 值(归一化相关系数)越接近 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/40.80 ~ 0.92
中值滤波 3x30.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 低说明图像失真严重。以下建议按优先级尝试:

  1. 降低 $\alpha$。
  2. 改用更大的分块(如 16×16),减少嵌入块数量。
  3. 只在部分块中嵌入水印,而不是全部块。
  4. 对载体图像先做直方图均衡或归一化,提高变换系数的稳定性。

7.5 提取水印 NC 值不稳定

NC 值不稳定可能与以下因素有关:

  • 攻击太强,奇异值发生了较大变化。
  • 水印图像本身纹理复杂,二值化后信息冗余不足。
  • 测试图像尺寸和水印尺寸比例不合适。

解决方案:对同一组实验重复多次取平均,同时增加水印冗余编码,例如把同一份水印重复嵌入多个块中,提取时投票表决。

8. 改进方向与工程实践建议

8.1 性能优化

本文代码为了便于理解,使用了双重 for 循环,在图像尺寸较大时速度较慢。工程化时可以优化:

  1. 矩阵化操作:尽量用矩阵运算替代循环分块。
  2. 预计算:如果载体图像固定,可以预先计算 DWT 和 SVD 结果,避免提取时重复计算。
  3. 并行计算:分块之间相互独立,可以使用parfor并行循环加速。
% 使用 parfor 需要注意循环内不能存在相互依赖 parfor i = 1:numBlockRow % 分块处理逻辑 end

8.2 工程实践建议

在实际项目中使用数字水印算法,除了关注算法本身,还需要把握以下几点:

参数配置统一管理

嵌入和提取过程必须使用相同的参数。建议将alphablockSizewatermarkScale提取到配置文件或常量文件中,避免手工修改导致不一致。

% 文件路径: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 水印算法,后续可以从以下方向继续优化:

  1. 自适应嵌入强度:根据图像局部纹理复杂度动态调整 $\alpha$,纹理丰富区域使用较大强度,平滑区域使用较小强度。
  2. 多尺度 DWT:对图像做多层小波分解,在更深层嵌入水印,进一步提高鲁棒性。
  3. 水印预处理:对水印图像做置乱(如 Arnold 置乱、混沌置乱),即使水印被提取出来,攻击者也难以直接读取。
  4. 结合机器学习:使用神经网络学习最优嵌入位置和嵌入强度。
  5. 半脆弱水印:既要求版权保护,又要求能检测图像是否被篡改,适合内容认证场景。

9. 总结与下一步学习建议

本文完成了一套基于 DWT-DCT-SVD 的 MATLAB 水印算法完整实现。你掌握了以下关键点:

  • DWT、DCT、SVD 三种变换的基本原理及其在水印算法中的作用。
  • DWT-DCT-SVD 组合算法相比传统 DWT-SVD 的改进思路:在小波低频子带上再做分块 DCT 和 SVD,利用奇异值的稳定性提高水印鲁棒性。
  • 完整的水印嵌入、提取、攻击测试 MATLAB 代码。
  • 使用 PSNR 和 NC 指标评价水印算法性能。
  • 常见问题的排查方法。

下一步,建议你动手改变以下条件,观察算法性能变化:

  1. 将水印尺寸改为 32×32,对比 NC 值变化。
  2. 将嵌入强度 $\alpha$ 从 0.01 调到 0.2,观察 PSNR 和 NC 的权衡关系。
  3. 将 Haar 小波换成 db2、sym4 等小波基,比较不同小波基下的鲁棒性。
  4. 增加更多攻击类型,如缩放攻击、旋转攻击、亮度调整攻击。

实验时建议做好记录,例如表格记录不同参数下的 PSNR、NC 值,这样可以更直观地找到最优参数组合。如果遇到代码问题,欢迎对照本文的排查表格逐项检查。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询