深入解析图片水印技术:从 DCT 频域嵌入到深度学习盲去除的工程实践
今天,我们将深入探讨 AI 无痕改字背后的硬核技术,并看看作为国内领先的图像处理平台 —— 图片猫 是如何将这些前沿学术成果落地为极简的用户体验的。
一、水印技术的本质:信息隐藏与载体博弈
从信息论角度看,图片水印的本质是一个有约束的信道编码问题:
- 载体(Cover):原始图像 II
- 水印信息(Message):需要嵌入的比特流 W={w1,w2,...,wn}W={w1,w2,...,wn}
- 嵌入函数(Encoder):Iw=E(I,W,K)Iw=E(I,W,K),其中 KK 为密钥
- 攻击信道(Channel):JPEG 压缩、缩放、裁剪、截屏、打印扫描等
- 提取函数(Decoder):W′=D(Iw′,K)W′=D(Iw′,K)
水印系统的核心矛盾在于三个指标的不可能三角:
不可感知性(Imperceptibility)
▲
/ \
/ \
/ \
鲁棒性 ◀───────▶ 嵌入容量
(Robustness) (Capacity)
- 不可感知性要求水印对人眼不可见(PSNR > 40dB)
- 鲁棒性要求水印在经历各种攻击后仍可正确提取
- 嵌入容量要求能够承载足够的信息量
实际工程中,不同场景需要在三者之间做取舍。
二、可见水印:简单粗暴但有效的版权声明
2.1 Alpha 混合模型
最基础的可见水印就是简单的透明度叠加:
# 经典 Alpha Blending 水印叠加
def add_visible_watermark(image, watermark, position, alpha=0.3):
"""
image: 原图 (H, W, 3),uint8
watermark: 水印图 (h, w, 3),uint8,建议带透明通道
alpha: 水印不透明度,0=全透明,1=完全覆盖
"""
h, w = watermark.shape[:2]
x, y = position
roi = image[y:y+h, x:x+w]
# 线性混合:I_out = (1 - α) * I_original + α * I_watermark
blended = cv2.addWeighted(roi, 1 - alpha, watermark, alpha, 0)
image[y:y+h, x:x+w] = blended
return image
2.2 工程实践中的可见水印增强
实际生产环境中,简单的 Alpha 混合很容易被去除(仅需一次减法运算)。工业级方案通常会加入以下增强:
| 增强策略 | 原理 | 抗去除效果 |
|---|---|---|
| 自适应透明度 | 根据局部纹理复杂度动态调整 α 值 | ★★★ |
| 非线性混合 | 使用 gamma 矫正或 S 曲线替代线性混合 | ★★★★ |
| 结构嵌入 | 将水印与图像边缘信息耦合 | ★★★★★ |
| 随机扰动 | 每次嵌入引入微量随机噪声 | ★★★ |
💡 实践建议:如果你只是需要快速给商品图、设计稿加上半透明 Logo 水印,不需要自己写代码,像 图片猫 AI改图网站 这类在线工具已经内置了批量水印功能,支持自定义位置、透明度、平铺密度,还能对整个文件夹批量处理,省去了不少工程适配的时间。
三、不可见水印(数字盲水印):频域中的信息隐写
不可见水印才是水印技术的核心研究方向。其基本思路是:将水印信息嵌入到人眼不敏感的信号分量中。
3.1 空域方法:LSB(最低有效位)
最简单的空域方法是修改像素值的最低有效位:
def lsb_embed(image, message_bits):
"""
将消息比特流嵌入到图像蓝色通道的 LSB 中。
容量:每像素 1 bit,一张 1920x1080 的图可以嵌入约 256KB 数据。
"""
flat = image[:, :, 0].flatten() # 取蓝色通道
for i, bit in enumerate(message_bits):
# 清除最低位,然后写入水印 bit
flat[i] = (flat[i] & 0xFE) | int(bit)
image[:, :, 0] = flat.reshape(image.shape[:2])
return image
def lsb_extract(image, length):
flat = image[:, :, 0].flatten()
return [flat[i] & 1 for i in range(length)]
LSB 的致命缺陷:几乎无法抵抗任何攻击。一次 JPEG 压缩(质量 95%)就足以完全破坏嵌入的水印。因此 LSB 在实际版权保护场景中几乎不可用,更多用于隐写术(Steganography)领域。
3.2 频域方法:DCT 域水印
离散余弦变换(DCT) 是 JPEG 压缩的核心算法,也是水印嵌入最常用的频域工具。
3.2.1 DCT 变换基础
对 8×8 图像块进行 DCT 变换后,得到的系数矩阵呈现明显的能量分布特征:
DCT 系数矩阵(频率从左上到右下递增):
┌────────────────────────────────┐
│ DC 低频 低频 中频 ... │ ← 人眼最敏感(亮度/大块色彩)
│ 低频 低频 中频 中频 ... │
│ 低频 中频 中频 高频 ... │
│ 中频 中频 高频 高频 ... │ ← 水印嵌入的最佳区域
│ ... ... ... ... 高频 │
│ ... ... ... 高频 高频 │ ← JPEG 压缩会丢弃(不鲁棒)
└────────────────────────────────┘
- 低频系数:修改会导致明显的视觉失真 → 不可感知性差
- 高频系数:JPEG 压缩时会被量化为零 → 鲁棒性差
- 中频系数:兼顾不可感知性和鲁棒性 → 最佳嵌入位置
3.2.2 基于量化索引调制(QIM)的 DCT 水印嵌入
import numpy as np
from scipy.fft import dctn, idctn
def dct_watermark_embed(image_block, watermark_bit, alpha=25):
"""
在 8x8 图像块的中频 DCT 系数中嵌入 1 bit 水印。
使用 QIM(Quantization Index Modulation)方法:
- 将选定的 DCT 系数量化到特定网格
- 根据水印 bit 选择奇数或偶数量化步长
"""
# Step 1: DCT 变换
dct_block = dctn(image_block.astype(float), norm='ortho')
# Step 2: 选择中频系数(例如 (4,1) 和 (1,4) 位置)
coeff = dct_block[4, 1]
# Step 3: QIM 量化嵌入
# 将系数量化到步长为 alpha 的网格,
# 偶数网格表示 bit=0,奇数网格表示 bit=1
quantized = np.round(coeff / alpha) * alpha
if watermark_bit == 1:
quantized += alpha / 2 # 偏移半个步长
dct_block[4, 1] = quantized
# Step 4: IDCT 反变换
return idctn(dct_block, norm='ortho')
def dct_watermark_extract(image_block, alpha=25):
"""提取嵌入的 1 bit 水印"""
dct_block = dctn(image_block.astype(float), norm='ortho')
coeff = dct_block[4, 1]
# 判断系数更接近偶数网格还是奇数网格
remainder = (coeff / alpha) % 1
return 1 if 0.25 < remainder < 0.75 else 0
3.3 小波域方法:DWT 水印
离散小波变换(DWT) 相比 DCT 具有更好的空频局部化特性,尤其适合处理几何攻击:
DWT 一级分解:
┌──────────┬──────────┐
│ │ │
│ LL │ LH │ LL = 低频近似(缩小版原图)
│ (近似) │ (水平) │ LH = 水平细节
│ │ │ HL = 垂直细节
├──────────┼──────────┤ HH = 对角细节
│ │ │
│ HL │ HH │ 水印通常嵌入 LL 的子带
│ (垂直) │ (对角) │ 或 LH/HL 的中等能量系数
│ │ │
└──────────┴──────────┘
DWT 水印的优势在于多分辨率分析:可以在不同尺度上嵌入冗余水印,即使图像被裁剪或缩放,仍有部分尺度的水印可以存活。
3.4 扩频水印:通信理论的跨界应用
扩频水印(Spread Spectrum Watermarking)借鉴了军事通信中的 CDMA 思想:
def spread_spectrum_embed(image, watermark_bits, key_seed=42, strength=5):
"""
扩频水印嵌入:
1. 用密钥生成与图像等大的伪随机序列 PN
2. 将每个水印 bit 扩展到整幅图像
3. 叠加到 DCT/DWT 系数上
抗攻击的直觉:水印能量被均匀分散到整幅图像,
类似于 GPS 信号 —— 即使淹没在噪声中也能被正确解调。
"""
np.random.seed(key_seed)
h, w = image.shape[:2]
watermarked = image.astype(float).copy()
for bit in watermark_bits:
# 生成 {-1, +1} 伪随机序列
pn = np.random.choice([-1, 1], size=(h, w))
# bit=1 → 加上 PN,bit=0 → 减去 PN
signal = pn if bit == 1 else -pn
watermarked[:, :, 0] += strength * signal
return np.clip(watermarked, 0, 255).astype(np.uint8)
扩频水印的误码率(BER) 可以用以下公式估算:
BER≈Q(2⋅α2⋅Nσn2)BER≈Q(σn22⋅α2⋅N)
其中 αα 是嵌入强度,NN 是扩频因子(载体像素数/水印比特数),σn2σn2 是攻击引入的噪声方差。
四、深度学习时代:端到端水印网络
4.1 编码器-噪声层-解码器架构
2017 年 Google 提出的 HiDDeN(Hiding Data with Deep Networks)开创了深度学习水印的范式:
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ │ │ │ │ │
│ Encoder │────▶│ Noise Layer │────▶│ Decoder │
│ (U-Net) │ │ (模拟攻击) │ │ (CNN) │
│ │ │ │ │ │
│ Input: │ │ • JPEG 压缩 │ │ Output: │
│ - 原图 I │ │ • 高斯噪声 │ │ - 提取的 │
│ - 水印 W │ │ • 裁剪 │ │ 水印 W' │
│ │ │ • 缩放 │ │ │
│ Output: │ │ • 色彩变换 │ │ │
│ - 含水印 │ │ • Dropout │ │ │
│ 图 I_w │ │ │ │ │
└─────────────┘ └──────────────┘ └─────────────┘
│ │
│ 训练目标 │
├──── L_image = MSE(I, I_w) ◀────────────┤
└──── L_message = BCE(W, W') ◀────────────┘
关键创新:Noise Layer 在训练时可微分地模拟各种真实攻击,使得网络学会将水印嵌入到对这些攻击鲁棒的特征空间中。
4.2 JPEG 可微分近似
训练中最大的工程难点是 JPEG 压缩不可微。常用的解决方案:
class DifferentiableJPEG(nn.Module):
"""
用可微分的近似实现 JPEG 压缩效果:
1. 8x8 块 DCT(矩阵乘法实现,天然可微)
2. 量化步骤用 soft quantization 近似
3. IDCT 还原
"""
def __init__(self, quality=50):
super().__init__()
self.quality = quality
# 预计算 8x8 DCT 变换矩阵
self.register_buffer('dct_matrix', self._build_dct_matrix())
self.register_buffer('q_table', self._jpeg_quality_table(quality))
def forward(self, x):
# Step 1: 分块 + DCT
blocks = x.unfold(2, 8, 8).unfold(3, 8, 8) # 提取 8x8 块
dct_coeffs = self.dct_matrix @ blocks @ self.dct_matrix.T
# Step 2: 可微分量化(用 tanh 平滑近似 round)
normalized = dct_coeffs / self.q_table
# 直通估计器:前向做 round,反向传 soft gradient
quantized = normalized + (torch.round(normalized) - normalized).detach()
dequantized = quantized * self.q_table
# Step 3: IDCT
reconstructed = self.dct_matrix.T @ dequantized @ self.dct_matrix
return self._blocks_to_image(reconstructed)
4.3 最新进展:基于 Stable Diffusion 的水印
2024-2025 年的最新研究趋势是将水印嵌入到生成模型的潜空间中:
- Tree-Ring Watermark:在扩散模型的初始噪声中嵌入环形频率模式
- Gaussian Shading:将水印编码为潜空间中高斯分布的偏移
- StegaStamp:端到端训练的物理世界水印(能抗打印-拍照攻击)
五、水印去除:攻防对抗的另一面
5.1 可见水印去除的技术演进
| 阶段 | 方法 | 核心思路 |
|---|---|---|
| 传统方法 | 频域滤波 + 图像修复 | 在频域中定位水印能量峰,滤除后重建 |
| 早期 DL | U-Net / Pix2Pix | 训练有监督的图像翻译网络 |
| 中期 DL | 多任务网络 | 同时预测水印掩码和修复后图像 |
| 前沿方案 | 大模型 Inpainting | 利用预训练扩散模型的图像先验 |
5.2 基于 GAN 的水印去除网络
class WatermarkRemovalGenerator(nn.Module):
"""
典型的水印去除生成器架构:
编码器-瓶颈-解码器 + 跳跃连接 + 注意力模块
"""
def __init__(self):
super().__init__()
# 编码器:逐步下采样,提取多尺度特征
self.enc1 = ConvBlock(3, 64) # 512 → 256
self.enc2 = ConvBlock(64, 128) # 256 → 128
self.enc3 = ConvBlock(128, 256) # 128 → 64
# 瓶颈:用注意力机制定位水印区域
self.bottleneck = SelfAttentionBlock(256)
# 解码器:逐步上采样 + 跳跃连接修复细节
self.dec3 = UpConvBlock(256, 128)
self.dec2 = UpConvBlock(256, 64) # 128 + 128 = 256(跳跃连接)
self.dec1 = UpConvBlock(128, 3) # 64 + 64 = 128
def forward(self, watermarked_image):
e1 = self.enc1(watermarked_image)
e2 = self.enc2(e1)
e3 = self.enc3(e2)
b = self.bottleneck(e3)
d3 = self.dec3(b)
d2 = self.dec2(torch.cat([d3, e2], dim=1))
d1 = self.dec1(torch.cat([d2, e1], dim=1))
# 残差学习:预测水印残差而非完整图像
return watermarked_image - d1
5.3 不可见水印的攻击与检测
不可见水印面临的主要攻击手段及对应防御:
攻击类型 │ 破坏机理 │ 防御策略
────────────────┼───────────────────────────┼─────────────────────
JPEG 压缩 │ 高频系数被量化丢弃 │ 嵌入中频段 + 纠错编码
几何变换(旋缩裁) │ 像素重采样破坏空域位置关系 │ 特征点同步 + 模板匹配
直方图均衡化 │ 灰度映射改变统计特征 │ 归一化互相关检测
打印-扫描 │ D/A + A/D 转换引入大量噪声 │ 扩频 + 极低码率
对抗性攻击 │ 针对性梯度扰动 │ 对抗训练 + 模型集成
六、工程实践中的考量
6.1 选型指南
根据业务场景选择合适的水印方案:
你的需求是什么?
│
├── 防止盗图 / 声明版权
│ └── ✅ 可见水印(Logo 叠加、文字平铺)
│ → 快速有效,推荐在线工具批量处理
│
├── 追踪泄露源头 / 内部文档溯源
│ └── ✅ 不可见水印(盲水印)
│ → 每份文档嵌入不同 ID,泄露后可提取追溯
│
├── AI 生成内容标识(C2PA / AIGC 溯源)
│ └── ✅ 潜空间水印 + 元数据签名
│ → 在生成阶段直接嵌入,无需后处理
│
└── 数字资产交易 / NFT 确权
└── ✅ 鲁棒水印 + 区块链存证
→ 水印提取结果与链上哈希交叉验证
6.2 性能基准参考
一个合格的工业级不可见水印系统,应当达到以下指标:
| 指标 | 合格线 | 优秀线 |
|---|---|---|
| PSNR(嵌入质量) | > 38 dB | > 42 dB |
| SSIM | > 0.95 | > 0.98 |
| BER(JPEG Q75 后) | < 10% | < 2% |
| BER(缩放 50% 后) | < 15% | < 5% |
| BER(裁剪 25% 后) | < 20% | < 8% |
| 嵌入速度(1080p) | < 500ms | < 100ms |
6.3 关于水印去除的法律与伦理边界
需要特别指出的是,水印去除技术本身是中性的,但恶意去除他人版权水印属于违法行为(《著作权法》第五十三条)。合理的使用场景包括:
- 去除自己图片上被误加的水印
- 去除已购买授权图片的样图水印
- 学术研究与算法评估
📌 工具推荐:在日常工作中,如果你需要处理自有图片的水印问题(添加或去除),可以试试 piccat.cn,它提供了 AI 智能消除和批量加水印等实用功能,背后用到的就是本文提到的图像修复与混合叠加技术。相比命令行脚本,可视化操作更适合非技术同事使用。
七、动手实验:5 分钟实现一个 DCT 盲水印
下面提供一个完整的、可直接运行的 DCT 盲水印 Demo,将一张二值图像作为水印嵌入到载体图中:
"""
DCT 盲水印完整 Demo
依赖:pip install numpy opencv-python scipy
"""
import numpy as np
import cv2
from scipy.fft import dctn, idctn
class DCTBlindWatermark:
def __init__(self, block_size=8, strength=25):
self.block_size = block_size
self.strength = strength
# 中频系数位置(zigzag 扫描的第 10~15 个位置)
self.embed_positions = [(2, 3), (3, 2), (4, 1), (1, 4)]
def embed(self, cover_path, watermark_path, output_path):
"""将二值水印图嵌入到载体图中"""
cover = cv2.imread(cover_path, cv2.IMREAD_GRAYSCALE).astype(float)
wm = cv2.imread(watermark_path, cv2.IMREAD_GRAYSCALE)
# 将水印缩放到合适尺寸
bh = cover.shape[0] // self.block_size
bw = cover.shape[1] // self.block_size
wm_resized = cv2.resize(wm, (bw, bh))
_, wm_binary = cv2.threshold(wm_resized, 128, 1, cv2.THRESH_BINARY)
# 逐块嵌入
watermarked = cover.copy()
for i in range(bh):
for j in range(bw):
block = cover[i*8:(i+1)*8, j*8:(j+1)*8]
dct = dctn(block, norm='ortho')
bit = wm_binary[i, j]
pos = self.embed_positions[0]
# QIM 嵌入
coeff = dct[pos]
q = np.round(coeff / self.strength) * self.strength
dct[pos] = q + (self.strength/2 if bit else 0)
watermarked[i*8:(i+1)*8, j*8:(j+1)*8] = idctn(dct, norm='ortho')
cv2.imwrite(output_path, np.clip(watermarked, 0, 255).astype(np.uint8))
print(f"✅ 水印已嵌入,PSNR = {self._psnr(cover, watermarked):.2f} dB")
def extract(self, watermarked_path, shape):
"""从含水印图中盲提取水印"""
img = cv2.imread(watermarked_path, cv2.IMREAD_GRAYSCALE).astype(float)
bh, bw = shape
extracted = np.zeros((bh, bw), dtype=np.uint8)
for i in range(bh):
for j in range(bw):
block = img[i*8:(i+1)*8, j*8:(j+1)*8]
dct = dctn(block, norm='ortho')
pos = self.embed_positions[0]
coeff = dct[pos]
remainder = (coeff / self.strength) % 1
extracted[i, j] = 255 if 0.25 < remainder < 0.75 else 0
return extracted
@staticmethod
def _psnr(original, processed):
mse = np.mean((original - processed) ** 2)
return 10 * np.log10(255**2 / mse) if mse > 0 else float('inf')
# 使用示例
if __name__ == '__main__':
dwm = DCTBlindWatermark(strength=30)
dwm.embed('lena.png', 'logo.png', 'lena_watermarked.png')
wm = dwm.extract('lena_watermarked.png', shape=(64, 64))
cv2.imwrite('extracted_watermark.png', wm)
print("✅ 水印提取完成")
八、总结与展望
| 维度 | 当前水平 | 未来趋势 |
|---|---|---|
| 嵌入质量 | PSNR > 42dB 已成熟 | 感知质量指标(LPIPS/FID)优化 |
| 鲁棒性 | 抗 JPEG/缩放较好 | 抗 AI 重生成(SD img2img) |
| 检测方式 | 需要原图或密钥 | 完全盲检测 + 零知识证明 |
| 标准化 | C2PA / IPTC 推进中 | 全球统一 AIGC 水印标准 |
图片水印技术从经典信号处理走到深度学习时代,核心矛盾始终是不可感知性、鲁棒性、容量的三角博弈。随着 AIGC 内容爆发式增长,水印技术正在从"版权保护工具"演变为"AI 内容可信基础设施"。
对于开发者而言,理解底层原理有助于选择正确的技术方案;而对于日常图片处理需求,善用成熟工具(如在线平台 图片猫 提供的水印添加/去除、图片批处理等能力)能显著提升工作效率。
参考文献:
- Cox I J, et al. Digital Watermarking and Steganography. Morgan Kaufmann, 2007.
- Zhu J, Kaplan R, Johnson J, et al. "HiDDeN: Hiding Data with Deep Networks." ECCV, 2018.
- Tancik M, Mildenhall B, Ng R. "StegaStamp: Invisible Hyperlinks in Physical Photographs." CVPR, 2020.
- Wen Y, et al. "Tree-Ring Watermarks: Fingerprints for Diffusion Images that are Invisible and Robust." NeurIPS, 2023.
作者说:本文首发于 CSDN,转载请注明出处。文中涉及的图片处理能力(如 AI 去水印、批量加水印、图片修复等),均可在 “”图片猫 免费AI改图“” 上免费体验。欢迎交流讨论 🐱
更多推荐

所有评论(0)