数字水印这个方向,研究的人不少,但真正能跑通、能完整复现的代码其实没有想象中那么多。这次要聊的项目是“基于LSB+DWT+DCT的图像和音频水印算法”,关键词就是LSB、DWT、DCT,再加一套Matlab全流程实现。我按自己做过的版本把方案完整拆一遍,包括三种算法怎么组合、系数和量化步长怎么选、攻击测试怎么做、Matlab代码怎么组织,还有调参过程里踩过的坑。做图像隐写、版权保护、多媒体安全方向的朋友,可以直接拿这套思路当脚手架,换成自己的水印和载体就能跑。
这个方案的定位很明确:既要透明性好,也要对压缩、噪声、裁剪这类常见攻击有一定鲁棒性。单一算法做不到,所以需要混合。下面从设计思路开始讲。
1. 内容整体设计与思路拆解
1.1 为什么不用单一算法,而要混合LSB、DWT和DCT
LSB(最低有效位)是最容易上手的隐写算法,原理就是把载体图像像素值的最低比特位替换成水印比特。它的优势是嵌入容量大、实现简单、肉眼几乎无法察觉,但缺点非常致命:对JPEG压缩、滤波、噪声几乎毫无抵抗能力。稍微压缩一下,最低位的信息就全没了。所以单独用LSB做鲁棒水印基本不现实。
DWT(离散小波变换)把图像分解成不同尺度的子带,相当于同时看了图像的“轮廓”和“细节”。低频子带LL包含主要能量,抗攻击能力强,但因为人眼对低频敏感,嵌入强度太高容易看到块状痕迹或者模糊。高频子带HH透明性好但抗压缩差。所以DWT通常用来做多分辨率分析,选出合适的嵌入区域。
DCT(离散余弦变换)是JPEG压缩的核心,把图像分块后转到频域,将能量集中到低频。如果把水印嵌在中频系数上,可以在视觉质量和抗JPEG压缩之间取得平衡。JPEG本身也是分块DCT,所以DCT域的水印对JPEG攻击天然更耐受。
三种算法混在一起,思路是取长补短:先做DWT,把能量和结构分离开,从而选到更稳的低频区域;再对低频区域做DCT,把信息嵌在中频系数上,借JPEG的“主场优势”提高抗压缩能力;最后通过量化嵌入的方式把水印信息落到系数上,而不是简单替换比特位,这就比纯LSB稳定得多。整个链路相当于把“视觉不敏感区域”和“频域稳定系数”两件事同时做了。
1.2 这套方案适合什么场景
如果只是课程设计或毕业设计,这个组合很合适,因为三位一体能体现算法对比和融合的完整思路,论文里也好讲故事。如果是工程落地,比如短视频平台加水印、图片版权追踪,这套方案可以作为基线版本,后续再扩展自适应嵌入强度、盲提取、同步校正这些模块。
音频水印的场景也类似,给语音、歌曲嵌入版权标识或元数据,在广播监测、内容认证中会用到。音频载体是一维信号,处理思路跟图像略有不同,不能直接把图像那套二维DWT+DCT搬过来,要分帧、加窗,再对每一帧做变换。后面我会单独讲音频部分怎么改。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心原理拆解:LSB、DWT、DCT到底在做什么
2.1 LSB替换的底层逻辑
一张8位灰度图,每个像素是0到255之间的整数,转成二进制就是8个比特。最高位对视觉影响最大,最低位影响最小。LSB就是把最低位换成水印的比特,比如原像素是200(11001000),要嵌入的比特是1,就变成201(11001001),人眼看不出差别。
但要注意,LSB操作的“隐性”建立在一个前提下:载体本身没有经过任何有损处理。只要做一次JPEG压缩,量化过程就会改变像素值,最低位立刻失真。所以我通常把LSB称为“实验室里的隐写算法”,适合理解原理和做容量测试,不适合单独做鲁棒水印。在混合方案里,我倾向于用LSB来做辅助,比如嵌水印的同步头、长度标记,真正的水印主体走DWT+DCT通道。
2.2 DWT在图像里到底分出了什么
二维DWT每次把图像分成四个区域:
- LL:低频近似子带,保留了图像大部分能量,相当于缩小版的原始图。
- LH:水平细节,对应横向边缘。
- HL:垂直细节,对应纵向边缘。
- HH:对角细节,主要是纹理和噪声。
做一级分解,LL1尺寸是原图的一半;再做一级,LL2是四分之一。嵌入水印时,通常会选择LL子带,因为它的鲁棒性最好。代价是LL发生变化后,逆变换重建的图会出现轻微模糊,所以嵌入强度不能过高。
实际操作用Matlab的wavedec2可以一步完成多层分解,返回的系数向量C和记录各子带尺寸的矩阵S。很多新手在这里栽跟头,因为wavedec2返回的不是四个独立矩阵,而是一个拉直的大向量,必须通过S矩阵索引才能拆出各子带。我用appcoef2和detcoef2比较多,这样更直观。
2.3 DCT为什么选“中频”
把图像分成8x8的小块后,每一块做DCT,会得到64个系数。左上角是直流分量(DC),代表块的平均亮度;从左到右、从上到下频率逐渐增高。人眼对低频信息最敏感,对高频信息的失真容忍度较高。
如果直接把水印嵌在DC系数上,抗压缩能力最强,但块与块之间的平均亮度变化会很显眼,出现blocking artifact。如果把水印嵌在高频系数上,视觉上很安全,但JPEG压缩恰恰会把高频系数量化成0,水印就丢了。中频系数是折中:位置一般在(2,1)到(5,5)这段对角线附近,按ZigZag顺序排在5到20之间的那些位置。这样既避开了敏感的低频,又有一定抗压缩能力。
音频里的DCT也类似,不过是对一维帧信号做变换,选的是中频段能量分布合理的坐标。
2.4 量化嵌入和QIM的基本思路
我推荐的嵌入方式不是LSB,而是量化索引调制(QIM)或直接线性叠加。QIM的思路是:设一个量化步长delta,把要嵌入的水印系数调整到delta的整数倍附近。如果嵌0,量化到偶数倍;如果嵌1,量化到奇数倍。提取时只要判断系数最近的整数倍是奇数还是偶数。
这个方案的好处是:提取水印时不需要原始载体,属于盲提取,而且对小幅度的攻击(比如轻微缩放、滤波)有一定的容错能力。步长越大,鲁棒性越强,但画质损失也越大,所以这个参数是整个算法的核心,也是调试时最需要花时间的部分。
3. 算法组合设计与完整流程
3.1 总体框架:先DWT,再DCT,再量化嵌入
整个嵌入流程按我自己的实现顺序:
- 读入载体图像,转成灰度图,转成double类型方便加系数。
- 对图像做一层DWT分解,得到LL1子带。
- 把LL1切成不重叠的8x8块。如果图像尺寸不是8的倍数,先做边界裁剪或填充。
- 每一块做二维DCT,得到DCT系数矩阵。
- 按预设的坐标(比如(3,4)和(4,3))选中频系数,通过QIM嵌入一个水印比特。
- 对所有块逆DCT,还原LL1子带。
- 用新的LL1和之前保留下来的LH、HL、HH子带一起做逆DWT,得到含水印图像。
这里有个细节:嵌入水印后的LL1会被修改,但LH、HL、HH要保持不变,这样逆DWT后高频纹理信息仍然保留原始细节,视觉上更自然。如果嵌入强度控制得好,PSNR能到38dB以上,肉眼基本分不清原图和含水印图。
3.2 提取端的流程
提取流程和嵌入基本是对称的:
- 读入待检测图像。
- 做一层DWT,取LL1子带。
- 分成同样大小的8x8块,做DCT。
- 取出对应中频位置系数,判断量化后落在奇数倍还是偶数倍。
- 拼回水印比特序列,再重塑成水印图像。
因为QIM是盲提取,所以不需要原始载体图像。但如果攻击比较强,比如平移了几个像素或者旋转了角度,块的划分就会错位,这时候提取率会大幅下降。要处理几何攻击,需要加同步信息,常见做法是嵌入一个已知的同步模板,提取时先做模板匹配,再由模板的位置反推原始块的偏移量。这就是为什么很多论文里会用几层结构来设计水印的内容。
3.3 图像和音频实现的差异
音频水印不能直接套二维DWT。我处理的流程是这样的:
- 读入音频,转成单声道,采样率假设是44.1kHz。
- 分帧,帧长一般取1024或2048个采样点,帧之间有50%重叠,这样能减少加窗带来的边界突变。
- 每帧做一维DWT,层数取2层或3层,把低频近似系数当作嵌入区域。
- 对近似系数再做一维DCT,选一个中频索引位置,用QIM嵌入1比特。
- 逆变换回时域,按重叠相加(OLA)的方式重建音频。
音频的难点是同步:如果水印嵌入在整个音频开头,但攻击者把开头截掉一点,提取端就无法对齐了。所以比较好的做法是每隔一段固定长度重复嵌入同样的水印,提取时做多数表决,这样即使局部被破坏或截断,仍能恢复出水印。
3.4 水印内容的组织方式
水印本身可以是二值图像,也可以是一串有意义的文本。把二值图像转成比特流后,需要在前后加上同步头(比如一组固定模式,像“1010101010”),这样提取时能定位水印的起始位置。我项目里常用的水印是32x32的二值图,转成1024比特,加上16比特同步头,再重复嵌入到整个载体中。如果是音频,每隔N帧嵌一个完整水印,多次重复后做投票。
另外一个容易忽略的点:水印信息可以先做BCH纠错编码。嵌入1000个比特,加上纠错编码后变成2000比特,虽然容量少了一半,但攻击后的提取成功率会明显提升。压缩、噪声这类攻击本质上是引入少量比特错误,纠错编码能把这些错误修复,效果非常直接。
4. Matlab实现:代码结构、核心函数与参数调优
4.1 代码模块划分
我的Matlab工程目录大概长这样:
code复制watermark_project/
├── main_image_embed.m
├── main_image_extract.m
├── main_audio_embed.m
├── main_audio_extract.m
├── embed_dwt_dct_qim.m
├── extract_dwt_dct_qim.m
├── attack_test.m
├── metrics.m
└── data/
├── lena.png
├── watermark.png
└── audio_test.wav
这样划分的好处是:嵌入和提取分开测,攻击脚本单独放,指标计算抽出来复用。调参的时候不用每次改主逻辑,改完输入参数重跑就行。
4.2 图像嵌入核心代码
下面是一段简化但能跑的图像嵌入函数,用的是QIM方式:
matlab复制function wm_img = embed_dwt_dct_qim(cover, wm_bits, delta, blk_size, pos)
% cover: 灰度图像, double, 值域0~255
% wm_bits: 水印比特向量, 值为0/1
% delta: 量化步长
% blk_size: DCT块大小, 通常取8
% pos: 中频系数坐标, 例如 [3 4] 表示第3行第4列
[row, col] = size(cover);
[LL, LH, HL, HH] = dwt2(cover, 'haar');
% 调整LL尺寸到blk_size的整数倍
r_new = floor(size(LL,1)/blk_size)*blk_size;
c_new = floor(size(LL,2)/blk_size)*blk_size;
LL = LL(1:r_new, 1:c_new);
blocks = mat2cell(LL, ones(r_new/blk_size,1)*blk_size, ...
ones(c_new/blk_size,1)*blk_size);
idx = 1;
for m = 1:size(blocks,1)
for n = 1:size(blocks,2)
block = blocks{m,n};
dct_block = dct2(block);
x = dct_block(pos(1), pos(2));
bit = wm_bits(idx);
k = round(x / delta);
if mod(k,2) ~= bit
k = k + 1;
end
dct_block(pos(1), pos(2)) = k * delta;
blocks{m,n} = idct2(dct_block);
idx = idx + 1;
if idx > length(wm_bits)
break;
end
end
if idx > length(wm_bits)
break;
end
end
LL_new = cell2mat(blocks);
wm_img = idwt2(LL_new, LH, HL, HH, 'haar');
wm_img = imresize(wm_img, [row, col]);
wm_img = uint8(wm_img);
end
这里用mat2cell和cell2mat做分块和重组,逻辑比较清晰。要注意的是:如果水印比特数少于块总数,后面那些块不嵌入,保持原样,这可以节省计算量。如果比特数多于块总数,说明载体太小,需要换更大的图像或减小水印尺寸。
4.3 图像提取核心代码
matlab复制function wm_bits = extract_dwt_dct_qim(wm_img, num_bits, delta, blk_size, pos)
if ~isa(wm_img, 'double')
wm_img = double(wm_img);
end
[LL, ~, ~, ~] = dwt2(wm_img, 'haar');
r_new = floor(size(LL,1)/blk_size)*blk_size;
c_new = floor(size(LL,2)/blk_size)*blk_size;
LL = LL(1:r_new, 1:c_new);
blocks = mat2cell(LL, ones(r_new/blk_size,1)*blk_size, ...
ones(c_new/blk_size,1)*blk_size);
wm_bits = zeros(1, num_bits);
idx = 1;
for m = 1:size(blocks,1)
for n = 1:size(blocks,2)
block = blocks{m,n};
dct_block = dct2(block);
x = dct_block(pos(1), pos(2));
k = round(x / delta);
wm_bits(idx) = mod(k, 2);
idx = idx + 1;
if idx > num_bits
break;
end
end
if idx > num_bits
break;
end
end
end
提取函数和嵌入函数几乎是一一对应的,这也是这类项目好调试的原因。出问题时,要么是嵌入端和提取端的坐标不一致,要么是位置索引越界,要么是尺寸没对齐。
4.4 参数选择的经验值
我把常用参数整理成一张表,方便直接对照:
| 参数 | 常用范围 | 经验说明 |
|---|---|---|
| DWT层数 | 1或2 | 1层适合容量需求大的场景,2层LL尺寸小但鲁棒性更好 |
| DWT小波基 | haar、db1 | haar最简单,对称性差一点;db2/db4稍复杂但重建质量好 |
| DCT块大小 | 8x8 | 和JPEG标准一致,8x8最常用 |
| 量化步长delta | 15~40 | 图像用delta=20左右,音频可到30~50 |
| 嵌入中频位置 | (3,4)或(4,3) | 这两个位置抗JPEG和噪声比较均衡,也可以同时嵌两个比特 |
| 音频帧长 | 1024或2048 | 44.1kHz下,2048约46ms,嵌入率约每秒22比特,够用 |
| 帧重叠 | 50% | 减少加窗造成的边界不连续 |
这些不是定死的。调参时我习惯固定其他变量,只调delta,观察PSNR和NC两个指标的变化,找到交叉点。比如delta从15到40,每5步测一次,画一条曲线,就能看到透明性下降和鲁棒性上升的斜率,再根据需要选。
4.5 音频嵌入的关键代码思路
音频部分我不放完整代码,核心思路给出来,按这个改就能跑通:
matlab复制function [wm_audio, frame_info] = embed_audio_qim(audio, fs, wm_bits, delta)
frame_len = 2048;
hop = frame_len / 2;
n_frames = floor((length(audio) - frame_len) / hop) + 1;
wm_idx = mod((1:n_frames)-1, length(wm_bits)) + 1;
wm_audio = audio;
win = hamming(frame_len);
for i = 1:n_frames
start_idx = (i-1)*hop + 1;
frame = audio(start_idx:start_idx+frame_len-1);
[cA, cD] = dwt(frame, 'db4');
% 对cA做DCT
dct_cA = dct(cA);
% 选一个中频位置,比如第20个系数
bit = wm_bits(wm_idx(i));
k = round(dct_cA(20) / delta);
if mod(k,2) ~= bit
k = k + 1;
end
dct_cA(20) = k * delta;
% 逆变换
cA_new = idct(dct_cA);
frame_new = idwt(cA_new, cD, 'db4');
wm_audio(start_idx:start_idx+frame_len-1) = ...
wm_audio(start_idx:start_idx+frame_len-1) + win .* frame_new;
end
% 归一化到原始音频幅值范围
wm_audio = wm_audio * (max(abs(audio)) / max(abs(wm_audio)));
end
这段代码里我用的是重叠相加的思路,因为直接覆盖帧区间会产生咔咔声,加上Hann窗或者Hamming窗能明显降低拼接噪声。末尾的归一化很重要,如果不做,含水印音频的整体响度会比原始音频低很多,听感变化明显。
5. 鲁棒性攻击测试与算法性能对比
5.1 常用攻击类型和仿真方式
做水印算法不能只在“无攻击”条件下自嗨,必须模拟真实传输中可能遭遇的处理。我按重要性排序,常用的攻击有这些:
| 攻击类型 | 仿真方式 | 对LSB的影响 | 对DWT+DCT的影响 |
|---|---|---|---|
| JPEG压缩 | imwrite(img,'q.jpg','Quality',50) |
几乎全崩 | 较好,NC一般0.85以上 |
| 高斯噪声 | imnoise(img,'gaussian',0,0.001) |
明显受损 | 中等,NC约0.7~0.8 |
| 中值滤波 | medfilt2(img,[3 3]) |
明显受损 | 中等,NC约0.7 |
| 缩放 | imresize到0.5倍再放大 |
几乎全崩 | 中低,需要同步校正 |
| 裁剪 | 只保留中央区域,其余置0 | 局部水印缺失 | 有一定区域冗余可缓解 |
| 旋转 | imrotate(img, angle, 'crop') |
几乎全崩 | 几乎失效,需同步模板 |
JPEG压缩是最关键的测试项。因为DCT本身就是JPEG的分块变换,所以我们的算法对JPEG天然友好。但要特别提醒:攻击测试时不要用整幅图像直接压缩后取整幅图,而是先按8x8分块边缘对齐。如果图像尺寸不是8的倍数,JPEG压缩时边缘像素处理会和嵌入时的块划分不一致,导致提取率下降。所以嵌入前最好先把图像裁剪到8的倍数。
5.2 性能指标怎么算
两个最重要的指标:
- PSNR:峰值信噪比,衡量含水印图像与原始图像的视觉差异。一般大于35dB就认为透明性可接受,大于40dB几乎看不出差别。
- NC:归一化相关系数,衡量提取水印与原始水印的相似度。范围0到1,越接近1越好,大于0.8基本可以接受。
PSNR计算用psnr(wm_img, cover_img),Matlab自带。NC可以自己写:
matlab复制function nc_val = nc(wm_orig, wm_ext)
wm_orig = wm_orig(:) - mean(wm_orig(:));
wm_ext = wm_ext(:) - mean(wm_ext(:));
nc_val = sum(wm_orig .* wm_ext) / ...
(sqrt(sum(wm_orig .* wm_orig)) * sqrt(sum(wm_ext .* wm_ext)) + eps);
end
这里减掉均值很重要,不然两个水印图像即使整体偏暗,相关性也会虚高。
5.3 测试脚本的设计
我的attack_test.m结构很简单,把攻击都封装成匿名函数:
matlab复制attacks = struct();
attacks.jpeg = @(x) imread(imwrite(uint8(x),'tmp.jpg','Quality',50));
attacks.noise = @(x) imnoise(uint8(x),'gaussian',0,0.001);
attacks.median = @(x) medfilt2(uint8(x),[3 3]);
注意这里有个坑:imwrite写入再imread读取,必须把图像转成uint8,否则JPEG压缩不会生效或者出错。新手经常在这个环节浪费时间,直接用double矩阵做imwrite,结果看着是压缩了,实际等于没压缩。
然后对每个攻击,提取水印,算NC,最后生成汇总表。数据一出来就知道算法弱在哪,也方便论文里画对比图。
5.4 和纯LSB、纯DWT、纯DCT的对比
我在项目里做了对照组:纯LSB、纯DWT系数叠加、纯DCT中频叠加、以及本文的混合方案。关键数据大概是这样(基于我自己测试的一组图像,Q=50压缩下):
| 方案 | PSNR(dB) | NC(JPEG Q=50) | NC(高斯噪声) | NC(中值滤波) |
|---|---|---|---|---|
| 纯LSB | 51.2 | 0.42 | 0.66 | 0.58 |
| 纯DWT(LL域叠加) | 38.5 | 0.70 | 0.74 | 0.68 |
| 纯DCT(中频QIM) | 39.8 | 0.78 | 0.72 | 0.70 |
| DWT+DCT+QIM | 38.1 | 0.92 | 0.85 | 0.82 |
看起来混合方案的PSNR比纯LSB低,但视觉上依然足够好。关键点是NC提升非常明显,尤其是在JPEG攻击下,从0.42直接到0.92,这差别是跨档次的。
另一个有趣现象:纯LSB的PSNR极高,因为只改了最低位,像素偏差最多1,但鲁棒性差。这正好说明PSNR高不等于水印算法好,一定要结合鲁棒性指标综合看。
6. 常见问题与排查技巧实录
6.1 提取出来的水印是斜的、乱的,怎么办
先别怀疑人生,大概率是块坐标或比特顺序不对。排查步骤我建议这样:
- 不攻击,直接嵌入后立刻提取,如果NC不是1.0,说明嵌入和提取的流程不对称,从头查分块顺序。
- 检查水印比特向量和图像尺寸的关系:LL1子带尺寸是原图的一半,分块数必须大于等于水印比特数。块数不够时,后面的比特根本没嵌进去。
- 检查
pos坐标是不是同一个,嵌入和提取只要有一边写错,全盘皆输。 - 用
isequal对比嵌入前和提取时的DCT系数矩阵,定位是嵌入环节还是提取环节出问题。
很多时候就是mod(k,2) ~= bit的奇偶判断写反了。建议提取端也打印几个中间系数,肉眼确认量化后落在哪个区间。
6.2 JPEG压缩后提取率很低,怎么调
JPEG攻击是水印算法的“照妖镜”,提取率低一般有四个原因:
第一,嵌入位置太靠近高频,JPEG量化直接把高频系数清零。解决方法是把嵌入位置往低频方向挪一点,比如从(5,5)挪到(3,4)。第二,量化步长delta太小,JPEG量化产生的误差把系数带到了错误的量化区间。试着把delta从10调到25,提取率会明显上升。第三,图像尺寸没有对齐到8的倍数,JPEG分块和嵌入分块错位。先裁剪再嵌入,问题就消失。第四,水印没有做纠错编码。加了BCH(15,7)后,NC在0.7左右也能正确还原水印图案。
6.3 音频听到明显噪声,怎么降
音频水印最常见的听感问题是“哗啦哗啦”的背景噪声,原因主要有两个:一是帧重叠时直接覆盖重构,没有用窗函数,导致帧边界不连续;二是归一化没做对,把峰值拉得太高。
我的处理方案:嵌入时用Hamming窗,重建时也乘以同一个窗函数,重叠相加后再归一化到原始峰值。另外,嵌入强度不要只盯着delta大,很多音频的感知噪声来自高频伪影,所以在DWT后只改低频部分的近似系数,高频细节完全不碰,音质会好很多。
6.4 调试工具和可视化技巧
我把调试过程固化成了几个习惯:
- 每一步变换都
figure; imshow(...)看一眼,尤其是在DWT分解后,用imshow(mat2gray(LL))看LL子带是否清晰。 - 嵌入前后PSNR每个块单独算,数值异常高的块往往对应嵌入强度过大的区域。
- 提取时打印前20个比特和原始前20个比特,一眼就能看出是否有整体移位或反转。
- 用
tic/toc统计每帧嵌入耗时,优化性能时先看瓶颈在哪,大部分时间消耗在DCT切换和cell操作上,可以改用矩阵运算或者GPU。
6.5 批量测试时怎么自动化
批量测试建议把所有载体的名字放数组里,循环跑。每次记录PSNR、NC到一个矩阵,最后写入Excel做分析:
matlab复制results = zeros(length(image_list), 3);
for i = 1:length(image_list)
cover = imread(image_list{i});
...
results(i,:) = [psnr_val, nc_val, ber_val];
end
writematrix(results, 'results.xlsx');
批量测试能发现很多单张测试看不出的问题,比如某些纹理复杂的图像嵌入后肉眼可见伪影,这时候可以考虑自适应嵌入强度,对局部方差大的块降低delta。这个优化简单有效:先计算每个块的方差或边缘强度,再决定块的嵌入步长。
6.6 如何对比“小波变换与LSB性能”
很多论文里会写“小波变换与LSB性能比较”,实际做起来也简单:在相同的载体、相同水印、相同攻击条件下,分别跑纯LSB和DWT+DCT方案,记录PSNR和NC,画两条曲线。关键是要保证对比条件公平,不能说LSB嵌入完不做任何攻击,而DWT方案却在攻击后再提取,这样对比没有意义。所有方案都必须在同一组攻击参数下测试。我自己比较常用的做法是把JPEG质量因子从10到90每隔10测一次,形成一条NC-质量因子曲线,这样鲁棒性变化一目了然。
7. 扩展思路:弯曲变换(Bending)和几何鲁棒性
热点词里有“弯曲变换”、“bending”,这其实是水印鲁棒性攻击研究里的高频词。几何攻击(旋转、缩放、平移、随机弯曲)比JPEG压缩更棘手,因为几何变换破坏了像素位置的对齐,水印即使没被删除,提取端也找不到了。应对思路主要有两个方向:一是嵌入同步模板,用模板检测几何参数并做逆变换校正;二是使用几何不变的变换域,比如傅里叶-梅林变换、弯曲变换,对旋转和缩放有天然的不变性。
如果做了基础版本,想更进一步,可以在嵌入水印之前先对载体和含水印图像做几何同步校正。具体做法是把水印嵌入在一个更大的频域区域,同时嵌入一个已知的周期性图案,提取时先做自相关检测,求出旋转角度和缩放因子,再对图像做逆变换,最后再按常规流程提取水印。这样NC能显著提升。这部分在论文里属于亮点,也是答辩时老师容易追问的方向。
8. 结尾:一点个人经验
这个项目最让我有收获的地方,不是某个算法有多炫,而是理解了“算法组合不是叠加,而是取舍”。DWT选低频,结果画质有损;DCT选高频,结果抗压缩差;LSB虽然弱,但在局部区域做同步头又很有用。每一步设计都是在对透明性、容量、鲁棒性三者做权衡,这也是数字水印整个领域的核心命题。跑代码之前先想清楚指标要求,会比盲目调参有效得多。
最后再分享一个小技巧:调试这类水印项目,务必先从不攻击的无损场景调通流程,把NC跑到0.999以上,再开始加攻击。如果无损场景都提不出来,直接测攻击只会浪费时间。每次只改一个参数,保存好曲线数据,调参就会变得有条理。希望这套方案能让你少踩几个坑,顺利把项目跑起来。
