资讯动态

手写DCT与DWT数字水印:从原理到鲁棒性调优

发布时间:2026/9/11 14:21:45 来源:尧图企业网站定制
简介这套数字水印技术资源以Matlab为运行环境围绕DCT与DWT两种经典变换以及二者混合的水印嵌入与提取方法展开适合图像处理、多媒体安全方向的研究者与初学者参考学习可用于理解频域水印原理并快速开展对比实验。资源共27个文件压缩包仅1.13MB其中8个Matlab源码文件实现水印嵌入、常见攻击模拟、水印提取以及归一化相关、峰值信噪比等性能指标计算12个jpg和3个bmp、2个png等测试图像用于验证算法在不同载体上的表现docx运行文档与asv自动保存文件可辅助梳理流程。已有679人学习下载。资源内提供可直接运行的DCT/DWT水印Matlab代码、标准Lena等图像素材和说明文档包含从嵌入、攻击、提取到质量评价的完整流程便于在此基础上改进嵌入强度与鲁棒性策略学习者能迅速掌握混合水印的实现思路与评价方法。1. 数字水印不是玄学DCT与DWT到底在解决什么问题给一张图片塞进一段不可见的身份信息听起来像特工电影里的桥段但这套东西早就在版权声明、泄露溯源、医疗影像归档里跑了好多年。数字水印做的不是加密而是把一段信息嵌入到载体数据里让它在正常使用中不干扰观感同时在被裁剪、压缩、缩放之后还能被提取出来。这里的关键点在“还能被提取出来”所以问题从来不是怎么把比特写进像素而是怎么在图像经过一轮又一轮常规处理之后仍然能从残留里找回那串比特。DCT与DWT是两条最常见的嵌入路径。DCT把图像分块后转到频率域把水印藏在适合人眼感知的中间频段DWT则把图像拆成不同分辨率下的逼近子带与细节子带在不同尺度上分别调整系数。两者都能做水印也都有各自的弱点——DCT对压缩抗性强但对几何攻击敏感DWT多尺度鲁棒性更好却更容易被低通滤波抹平。所以实践里多是把两者叠起来用比如DCT嵌入DWT的低频子带或者反过来在DWT中频系数上再切块做DCT。这样做的收益不是简单11而是让整个嵌入过程同时拿到DWT的结构分解能力和DCT的局部能量分布特征。这篇文章面向的读者是已经会写Python、调过OpenCV或PyTorch的人。不考虑深度学习端到端方案只讲DCT、DWT以及DWT-DCT混合水印的手写实现——因为只有手写过一遍系数选择你才会知道那些所谓“鲁棒性参数”是怎么来的也才会在真实业务里遇到图像被转码后水印消失时知道往哪个方向排查。整个过程用Python和NumPy就能跑通不需要GPU。2. 数字水印的拆解从空间域到频域为什么绕不开DCT与DWT2.1 把水印系统拆成三个模块别一上来就调库一个数字水印系统无论用哪种变换结构都是固定的三段嵌入端、传输信道、提取端。嵌入端把水印信息调制到载体图像的某些系数上传输信道包括正常的图像压缩、尺寸调整、滤波也包括恶意的裁剪和涂改提取端则只依赖接收到的图像在不知道原始载体的条件下尽可能恢复水印比特。搞清楚这三段之后就会明白一个事实算法报告的PSNR和鲁棒性都只是实验室条件下的指标真实管道的瓶颈往往不在算法本身而在格式转换时引入的量化误差。比如把PNG换成JPEGDCT块边界被压缩算法进一步量化高频系数先被丢弃这个时候水印嵌在哪些频带上就直接决定了它还能不能活着出来。所以下一节先讲清楚频域系数在常规图像处理里到底损失得有多快。2.1.1 等价的数学直觉变换不是特征提取是换坐标DCT和DWT做的事情在数学上都属于正交变换本质是把一个二维图像矩阵从空间坐标换到频率坐标。空间域里的一个像素值跟它周围像素的关系是局部的但在变换域里一个系数代表的是这个局部区域在整个频段上的能量贡献。水印要藏进这些系数里就要找一个区域——这个区域的系数值在人眼感知上是次要的但在各种信号处理下衰减又是可控的。JPEG的本质就是在DCT域里做量化。对8x8块逐块做DCT然后按量化表把系数除以一个步长再取整。这个过程直接决定了基于DCT的水印的生死水印嵌入到与JPEG量化步长相近的系数幅度上压缩后被量化到一个整数格子里就会产生不可逆的偏差。DWT虽然不直接对应JPEG标准但大多数图像处理库在缩放和滤波时会改变小波子带系数的统计分布尤其会影响高频细节子带。这两个现象是所有后续工程决策的出发点。2.2 空间域的直接替换有硬伤但它不是没用最朴素的空间域方案是LSB替换直接把最低比特位写成水印信息。优点是容量极大嵌入速度快提取时只要取余数就能恢复。缺点是极脆弱任何有损压缩、颜色抖动甚至是一次格式转换都会把最低比特位抹掉。所以空间域做法只适合那些对鲁棒性要求不高的场景比如图片在内部系统里以无损格式流转、且只需要标记归属方的场景。一旦内容需要对外发布或会被平台二次压缩空间域方案就基本退出竞争了。那为什么不干脆把所有水印都丢给频域因为频域嵌入有一个代价——它没有办法在嵌入过程中精确定位水印的空间位置。这对某些需要“在图像内容里隐藏一段空间上集中的信息”比如二维码形式的可见水印的需求来说是不适用的。归根结底空间域和频域不是替代关系而是两种坐标系统下的取舍。2.3 DCT与DWT的角色分工一个管局部纹理一个管多尺度结构DCT的强项在于局部能量集中。对自然图像的分块DCT系数做统计会发现能量集中在左上角的低频系数上而右下角的高频系数普遍接近零值。嵌入算法通常会选择中频系数因为低频系数变化会引起明显块效应高频系数又容易在JPEG压缩时被量化掉。中频带的典型位置在一张8x8块的坐标(4,1)到(6,3)这一片区域。DWT的强项在于多尺度分解。它通过低通和高通滤波器把图像拆成长宽各一半的四个子带LL低频逼近子带、LH水平细节、HL垂直细节、HH对角细节。对LL带继续做分解就形成多级塔式结构。低频子带对一般信号处理有更强的抵抗能力但直接修改LL子带也最容易被察觉因为LL决定了图像整体亮度分布。因此经典做法是把水印嵌到二级或三级分解的中等尺度细节子带或者对LL子带再做一次DCT把水印嵌进DCT中频系数里——这个思路就是后文要展开的DWT-DCT混合数字水印。3. 用DCT做数字水印系数选择、嵌入步骤与完整进程3.1 对图像做分块DCT前的三个前置决策动手前先明确三件事。第一灰度图还是彩色图。灰度图直接做二维DCT彩色图一般是先转YUV只在Y通道亮度上嵌入水印这样能减少颜色失真。第二块尺寸。块尺寸直接影响嵌入容量与鲁棒性的取舍。8x8是常见的选择与JPEG块对齐在压缩后保留率上会更好16x16块频率分辨率更高但块数量减少嵌入容量随之下降。第三嵌入强度。强度越高肉眼可见性越强但抗攻击能力也会上升所以这是一个工程上的权衡。这三个决策决定了后面所有参数表的起点。建议先固定在8x8、嵌入强度适中、灰度图跑通全流程再逐步调整变量看效果。3.1.1 读取图像并分块的边缘情况处理图像长宽不一定是块尺寸的整数倍这是最容易踩的坑。一个简单做法是在分块前用常量填充把图像补齐到8的倍数。OpenCV的copyMakeBorder函数或NumPy的pad都能做。另一方面要考虑灰度图和彩色图的通道数差异cv2.imread(path, cv2.IMREAD_GRAYSCALE)是常用的方式。3.2 DCT系数标定哪些位置适合嵌入水印对一张典型自然图像的8x8分块DCT系数做统计会得到一种稳定的分布左上角DC系数数值最大代表了整块的平均亮度右下角高频系数几乎都逼近零。嵌入到DC系数上抗压缩能力最强但图像会出现明显的块间亮度跳变嵌入到高频系数上则几乎不可见但JPEG一压缩就没了所以折中区间在中间频带上。这张表说明了嵌入位置与攻击损耗的对照关系实际工程里需要根据载体内容做出选择。图像纹理越丰富系数方差越大能容纳的嵌入强度就越高。低频系数幅值大相同嵌入强度下的相对扰动比例小因此不易察觉但压缩后保留度好。3.2.1 用Zigzag扫描选择中频候选块JPEG标准用Zigzag顺序把8x8系数矩阵重排成一维数组前面的低频后面的高频。嵌入时只要确定一个起始索引范围比如从索引3到索引10对应中低频位置就可以对每个候选位置重复嵌入同一个水印比特。重复嵌入是提升提取成功率最直接的手段。3.3 Python实现DCT水印嵌入代码每一步在干什么下面是一段可以直接运行的嵌入脚本负责把一个二进制字符串扩展到整幅图的中频系数里。import cv2 import numpy as np from scipy.fftpack import dct, idct block_size 8 alpha 30 def embed_watermark_dct(img, watermark_bits): h, w img.shape # 填充到8的倍数 pad_h (block_size - h % block_size) % block_size pad_w (block_size - w % block_size) % block_size img_padded cv2.copyMakeBorder(img, 0, pad_h, 0, pad_w, cv2.BORDER_CONSTANT, value0) H, W img_padded.shape embed_pos [4, 5, 6, 7, 8] n_blocks (H // block_size) * (W // block_size) bit_len len(watermark_bits) # 反复重复水印保证每个块都嵌到 repeated_bits (watermark_bits * (n_blocks // bit_len 1))[:n_blocks] idx 0 for i in range(0, H, block_size): for j in range(0, W, block_size): block img_padded[i:iblock_size, j:jblock_size] coeff dct(dct(block, axis0, normortho), axis1, normortho) bit repeated_bits[idx] for p in embed_pos: # 按水印比特调整系数量化为特定步长 coeff.ravel()[p] (coeff.ravel()[p] // alpha) * alpha alpha // 2 bit * alpha // 2 block_watermarked idct(idct(coeff, axis0, normortho), axis1, normortho) img_padded[i:iblock_size, j:jblock_size] block_watermarked idx 1 # 去掉填充部分 return img_padded[:h, :w]这段代码的逻辑并不复杂。对每个8x8块做二维DCT之后coeff.ravel()[p]把二维系数矩阵按行展平然后取索引4到8的位置。alpha是量化步长它控制水印的一个比特在系数上产生的偏移大小。coeff // alpha * alpha alpha // 2 bit * alpha // 2的本质是把系数往一个特定量化格子上靠拢比特为0时靠拢到格子中间偏下比特为1时靠拢到格子中间偏上这样提取时只要对alpha取模就能判断。normortho参数是正交归一化选项缺少它会导致变换后的系数尺度不一致进而影响嵌入强度的物理含义。3.4 提取流程设计与代码实现提取端不知道原始图像所以只能通过量化取模来判断每个块承载的比特。def extract_watermark_dct(img_watermarked, orig_shape, expected_len): h, w img_watermarked.shape pad_h (block_size - h % block_size) % block_size pad_w (block_size - w % block_size) % block_size img_padded cv2.copyMakeBorder(img_watermarked, 0, pad_h, 0, pad_w, cv2.BORDER_CONSTANT, value0) H, W img_padded.shape embed_pos [4, 5, 6, 7, 8] extracted [] idx 0 for i in range(0, H, block_size): for j in range(0, W, block_size): block img_padded[i:iblock_size, j:jblock_size] coeff dct(dct(block, axis0, normortho), axis1, normortho) # 多个位置的系数取平均后做量化判断 vals [coeff.ravel()[p] for p in embed_pos] mean_val np.mean(vals) bit 1 if mean_val % alpha alpha / 2 else 0 extracted.append(bit) idx 1 return extracted[:expected_len]提取时用5个位置系数幅值的平均值来减少单点误差这是一个低成本的降噪方式。mean_val % alpha得到的余数如果大于alpha的一半就认为是1否则是0。这个方案的优点在于不需要原始图像的无参考提取缺点在于如果载体本身系数分布极不均匀——比如纯色块区域所有DCT中频系数都趋近于0——嵌入后的量化偏移也可能被噪声干扰导致某些块提取错误。3.5 JPEG压缩后水印还能提取吗一个验证实验步骤把嵌入后的图像用cv2.imwrite(out.jpg, img, [cv2.IMWRITE_JPEG_QUALITY, 85])保存为质量系数85的JPEG再读回来执行提取统计误码率。这里有一个常见误区如果嵌入位置与JPEG量化表的中频系数步长兼容性不够质量系数一旦低于70提取误码率会急剧升高。想验证算法极限就做一组不同质量系数的对比实验从90、80、70逐级下降。90以上应该接近零误码低于70出现明显跳崖是正常的。如果从一开始就高误码先检查alpha是否过小或者嵌入位置是否处于JPEG量化表极限截止位置附近。4. 用DWT提升鲁棒性DWT数字水印与DWT-DCT数字水印的关键差异4.1 为什么单靠DCT不够几何攻击与压缩残留的边界纯DCT分块水印的核心问题是分块独立性。每8x8块各自嵌入、各自提取块与块之间没有协同关系。图像一旦发生少许平移、旋转或者缩放块的边界就与原始位置错位提取时你根本不知道应该对齐到哪里。这种对几何攻击的脆弱性是DCT方案的结构性缺陷单纯调嵌入强度解决不了。DWT的做法是从整体上分解产生金字塔式的多分辨率表示。即使图像尺寸有轻微变化LL子带的整体结构仍然能保留主要的能量分布只要通过后期同步或对嵌入位置做一定的范围冗余就可以在一定程度上抵抗这种错位。当然DWT也不是万能的它在纯高频细节上的鲁棒性不如DCT在中频局部区域的控制力所以两者结合才具备更好的实用价值。4.2 小波子带的性格哪个子带能扛攻击、哪个会留痕迹对图像做一层小波分解后四个子带的特征差异非常明显。LL子带保留了图像大部分能量肉眼看上去就是个缩小的原图修改这里最容易引起视觉退化但任何攻击只要不摧毁图像的主体亮度结构水印就会留在里面。LH、HL子带分别对应水平边缘和垂直边缘人眼对这些方向性的高频噪声有一定容忍度是中庸的嵌入位置。HH子带包含的是对角方向细节能量低嵌入式不易察觉但往往是图像压缩首先丢弃的对象。实际项目里最常见的处理是对LL子带做一次DCT然后继续嵌DCT中频系数。这种DWT-DCT数字水印方案既拿到LL子带较强的抗并行攻击能力又能通过DCT把比特放在LL子带的局部中频系数上从而保留较高的嵌入质量。需要注意如果直接在一级DWT的LL子带上再嵌水印可嵌入的容量会降低到原始的1/4但单位比特的鲁棒性提高了不少。4.3 结合实现DWT-DCT混合水印的具体流程与代码4.3.1 DWT-DCT嵌入代码import pywt from scipy.fftpack import dct, idct def embed_dwt_dct(img, watermark_bits, level1): # 使用haar小波做一级分解 coeffs pywt.wavedec2(img, haar, levellevel) LL, (LH, HL, HH) coeffs[0], coeffs[1:] h, w LL.shape block_size_dct 4 alpha_dwt 20 n_blocks (h // block_size_dct) * (w // block_size_dct) bit_len len(watermark_bits) repeated (watermark_bits * (n_blocks // bit_len 1))[:n_blocks] LL_watermarked LL.copy() idx 0 for i in range(0, h, block_size_dct): for j in range(0, w, block_size_dct): block LL_watermarked[i:iblock_size_dct, j:jblock_size_dct] coeff dct(dct(block, axis0, normortho), axis1, normortho) bit repeated[idx] # 只修改(1,1)和(1,2)这种低频位置避开DC coeff[1, 1] (coeff[1, 1] // alpha_dwt) * alpha_dwt alpha_dwt // 2 bit * alpha_dwt // 2 coeff[1, 2] (coeff[1, 2] // alpha_dwt) * alpha_dwt alpha_dwt // 2 bit * alpha_dwt // 2 block_back idct(idct(coeff, axis0, normortho), axis1, normortho) LL_watermarked[i:iblock_size_dct, j:jblock_size_dct] block_back idx 1 # 重构图像 coeffs_new [LL_watermarked, (LH, HL, HH)] return pywt.waverec2(coeffs_new, haar)这里有几个值得解释的细节。第一pywt.wavedec2返回的是顶层LL加上每一层的三个细节子带元组代码里对LL做4x4分块DCT比8x8更细粒度嵌入容量更大但每个块承载的信息量也小。第二修改位置选(1,1)和(1,2)是在4x4块的DCT系数里避开DC后最靠前的位置因为4x4的系数空间本来就小选太靠后的高频位置意义不大。第三alpha_dwt比纯DCT的alpha小是因为LL子带本身就是低频逼近同样的绝对偏移在重构后会作用到更大的空间区域上视觉影响更明显。4.3.2 提取端的同步问题DWT-DCT的提取端同样做一级小波分解取LL子带再分块DCT然后对(1,1)和(1,2)位置的系数取模判比特。这里有一个比纯DCT更麻烦的事情如果用cv2.resize或某些库缩放图像再做DWTLL子带的尺寸会变化直接拿原嵌入时的块坐标去读就会错位。实际做法是在嵌入时就把水印比特重复很多遍并且每个比特分布在多个空间位置上。提取时不需要精确知道块的边界只要在候选区域内滑动窗口取多个可能的偏移位置对每个偏移位置分别提取并做投票取通过率最高的那组作为结果。这个思路叫自同步虽然朴素但实用。4.4 DCT水印、DWT水印、DWT-DCT水印的参数对照与选型方案嵌入位置视觉影响抗JPEG压缩抗缩放抗旋转典型嵌入容量空间域LSB最低位平面小极差差差高DCT中频8x8块中频系数中较好差差中DWT LL子带最低频逼近系数较大中较好中较低DWT-DCTLL子带DCT中频中较好较好中中选型不是看哪个算法名字厉害而是看内容管道。如果图片最终是JPEG对外分发DCT方案天然与JPEG的变换域一致有优势如果图片会在系统内部做缩放处理DWT-DCT的更粗粒度结构抗缩放能力更强。实际中我一般先用DWT-DCT做基线再根据具体攻击测试结果往纯DCT方向调。5. 数字水印的验证、攻击测试与可落地的参数调优经验5.1 如何科学地判断水印“成功”了PSNR与误码率的关系很多初学者只盯着提取结果能不能看到字符串这是不够的。嵌入水印后的图像需要计算峰值信噪比PSNR一般要求在30dB以上才认为视觉不可见。提取端的评估指标是误比特率BER也就是错误的比特数除以总比特数。两者需要同时汇报因为一味降低嵌入强度可以让PSNR好看但误码率必然升高。一个简单可复现的完整评估函数如下def evaluate_watermark(original, watermarked, extracted_bits, original_bits): mse np.mean((original.astype(float) - watermarked.astype(float)) ** 2) psnr 10 * np.log10(255 * 255 / mse) ber np.mean(np.array(extracted_bits) ! np.array(original_bits)) return psnr, ber这里的PSNR使用灰度图的逐像素差计算mse越小说明视觉失真越小。BER如果超过15%实际应用里就得考虑纠错码或重复投票来挽救超过30%基本宣告算法参数失效。观察指标的方式也要讲逻辑先固定攻击条件比如固定JPEG质量80然后画一条横轴为嵌入强度、纵轴为误码率的曲线找到拐点再定参数。5.2 必做的5类鲁棒性测试代码与阈值参考5.2.1 JPEG压缩测试cv2.imwrite(tmp.jpg, img_watermarked, [cv2.IMWRITE_JPEG_QUALITY, Q]) img_attacked cv2.imread(tmp.jpg, cv2.IMREAD_GRAYSCALE)质量系数Q的梯度用90、85、75、60、40来测。质量系数低于50以后中频系数被大幅量化这时算法退化是正常的关键是看质量系数在75左右能不能保持BER低于8%。5.2.2 缩放与旋转攻击缩放攻击的常见做法是缩小到原本的1/2或1/4再放大回原尺寸。旋转攻击在OpenCV里用仿射变换做但如果只测算法不测同步直接对全图旋转45度后提取意义不大。更贴近实际的做法是旋转后先做中心裁剪裁掉旋转产生的黑边再提取。中心裁剪本身也是一种攻击能同时测出裁剪鲁棒性。5.2.3 高斯噪声与中值滤波noise np.random.normal(0, sigma, img_watermarked.shape) img_noisy np.clip(img_watermarked noise, 0, 255).astype(np.uint8) img_filtered cv2.medianBlur(img_watermarked, kernel_size)高斯噪声的sigma从5到20逐步增加。中值滤波kernel从3x3增加到5x5时如果水印BER上升过于明显说明嵌入位置偏高频需要向DC方向调整。5.3 参数调整的实操顺序先定鲁棒性目标再调alpha最后换位置调参有个反直觉的规律——一上来就同时改alpha、块大小、嵌入位置永远调不出好看的结果。正确顺序是先固定所有参数单测嵌入强度alpha。alpha每翻一倍PSNR大约下降2-3dB但BER会有显著改善。找到alpha的合理区间后再调嵌入位置。只移动嵌入位置把系数从中频移到偏高一段BER会在JPEG攻击下大幅升高而PSNR几乎不变。块大小的调整要放在最后。因为块大小改变直接改变容量上限如果水印长度固定块越大冗余越高但每块嵌入的比特数变少。如果水印过长就要优先考虑升级成DWT方案而不是硬改块大小。5.4 一个容易忽略的坑颜色空间转换带来的偏差灰度图跑通后迁移到彩色图很多人直接在RGB三个通道都嵌一遍提取时取三个通道的结果投票。这个做法的问题在于RGB通道之间存在强相关性转JPEG时YCbCr空间经过色度下采样嵌入在RGB高频系数上的信息会被大幅削弱。正确做法是先转YUV只在Y通道嵌入色度通道保持原样。这样既不损失太多容量也能规避色度下采样的影响。5.5 进阶技巧用格雷码把相邻比特的误判率降下来如果提取误码率稳定在5%到10%之间又不想牺牲视觉质量可以考虑映射层优化。把原始水印比特通过格雷码编码后再嵌入提取后解码恢复。格雷码的物理意义在于两个相邻数值在二进制表示中只差一个比特这样由一个数值的偏移误判引起的比特错误最多也只有一位。这比直接二进制编码在抗单点噪声上的表现更稳定实现成本极低效果却非常明显。真正上线的时候还可以在格雷码之外再加一层BCH或卷积码做前向纠错但这已经是编码层的工作和DCT/DWT的嵌入位置无关了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价