AI摘要
概述
盲水印(Blind Watermark)是一种信息隐藏技术,将水印数据嵌入到载体图像中,肉眼不可见,只有通过特定算法才能提取。
概念辨析
| 概念 | 说明 | 是否需原图 |
|---|---|---|
| 盲水印 (Blind Watermark) | 提取水印时不需要原图和原始水印 | 否 |
| 非盲水印 (Non-Blind) | 提取时需要原图参与计算 | 是(本文实现) |
| 可见水印 | 图片上肉眼可见的叠加标记(如版权 LOGO) | — |
本文实现的是频域非盲水印,但通过种子(seed)对水印位置进行随机置乱,使攻击者即使知道算法也无法在无种子时恢复可读水印,达到变相"盲"提取的效果。
数学原理
傅里叶频域变换
图像在空间域(像素点阵)和频率域之间存在对应关系:
- 低频分量:图像中变化缓慢的区域(大面积色块、背景)
- 高频分量:图像中变化剧烈的区域(边缘、纹理、噪点)
通过 二维快速傅里叶变换(FFT-2D),可以将 $M \times N$ 的图像矩阵从空间域转换到频率域:
$$ F(u,v) = \sum_{x=0}^{M-1} \sum_{y=0}^{N-1} f(x,y) \cdot e^{-j2\pi(ux/M + vy/N)} $$
逆变换(IFFT)可将频率域还原回空间域:
$$ f(x,y) = \frac{1}{MN} \sum_{u=0}^{M-1} \sum_{v=0}^{N-1} F(u,v) \cdot e^{j2\pi(ux/M + vy/N)} $$
频域嵌入原理
在频率域中修改图像的傅里叶系数(尤其是中高频分量),不会在视觉上引起明显变化,但编码了信息:
空间域图像 频率域 逆变换后
┌──────┐ FFT ┌──────────┐ IFFT ┌──────┐
│ 原图 │ ────────→ │ F(u,v) │ ───────→ │ 图像 │
└──────┘ │ + │ └──────┘
│ α·W(u,v) │
└──────────┘
↑
水印频率分量嵌入公式:
$$ F'(u,v) = F(u,v) + \alpha \cdot W(u,v) $$
其中:
- $F(u,v)$ — 原始图像的第 $(u,v)$ 个频率系数
- $W(u,v)$ — 水印图像在频率域的系数
- $\alpha$ — 嵌入强度系数(默认 3.0),越大则水印越清晰但可见性越高
- $F'(u,v)$ — 嵌入水印后的频率系数
提取公式:
$$ W(u,v) = \frac{F'(u,v) - F(u,v)}{\alpha} $$
从公式可见:提取需要已知 原始图像 $F(u,v)$ 和 alpha 参数。这是非盲水印的根本原因。
攻击链路图
嵌入过程 (Encode)
═══════════════════
原图 img 水印 wm
│ │
▼ ▼
FFT(img) 随机置乱 (seed)
│ │
│ ┌─────────────────┘
▼ ▼
F' = F + α × wm_shuffled
│
▼
IFFT(F') → 含水印图像
│
▼
保存为 JPEG (有损压缩引入微量误差)
提取过程 (Decode)
═══════════════════
原图 img 含水印图 img_encoded
│ │
▼ ▼
FFT(img) = F1 FFT(img_encoded) = F2
│ │
│ ┌─────────────────┘
▼ ▼
W = (F2 - F1) / α → 得到置乱后的水印
│
▼
用相同 seed 逆置乱 → 原始水印图像代码实现
环境依赖
pip install opencv-python numpy matplotlib核心导入与参数
import cv2
import numpy as np
import random
alpha = 3.0 # 嵌入强度:越大水印越清晰,但对原图像素影响越大
seed = 20160930 # 随机种子:作为水印置乱的"密钥"BGR → RGB 转换工具
OpenCV 使用 BGR 通道顺序,Matplotlib 使用 RGB 顺序,显示时需转换:
def bgr_to_rgb(img):
b, g, r = cv2.split(img)
return cv2.merge([r, g, b])编码(Encode):嵌入水印
# 1. 读取原图与水印
img = cv2.imread('hui.png')
wm = cv2.imread('wm.png')
h, w = img.shape[0], img.shape[1]
# 2. 将水印置入半高画布 (h/2 × w)
hwm = np.zeros((int(h * 0.5), w, img.shape[2]))
hwm2 = np.copy(hwm)
for i in range(wm.shape[0]):
for j in range(wm.shape[1]):
hwm2[i][j] = wm[i][j]
# 3. 用 seed 随机打乱 watermark 的像素位置
random.seed(seed)
m, n = list(range(hwm.shape[0])), list(range(hwm.shape[1]))
random.shuffle(m) # 行索引随机排列
random.shuffle(n) # 列索引随机排列
for i in range(hwm.shape[0]):
for j in range(hwm.shape[1]):
hwm[i][j] = hwm2[m[i]][n[j]]
# 4. 对称复制(上半部分镜像到下半部分)
rwm = np.zeros(img.shape)
for i in range(hwm.shape[0]):
for j in range(hwm.shape[1]):
rwm[i][j] = hwm[i][j]
rwm[rwm.shape[0] - i - 1][rwm.shape[1] - j - 1] = hwm[i][j]对称复制示意图:
┌──────────────────┐
│ hwm (置乱后) │ ← 上半部分
│ │
│ ╲ │
│ mirror axis │ ← 水平中心线
│ ╱ │
│ hwm 对称镜像 │ ← 下半部分
└──────────────────┘# 5. FFT 频域嵌入
f1 = np.fft.fft2(img) # 原图 → 频域
f2 = f1 + alpha * rwm # 频域加法嵌入
img_encoded = np.fft.ifft2(f2) # 逆变换回空间域
img_encoded = np.real(img_encoded) # 取实部
# 6. 保存为 JPEG(无损质量)
cv2.imwrite('output.jpg', img_encoded, [cv2.IMWRITE_JPEG_QUALITY, 100])为什么要取 np.real?
FFT 结果包含实部和虚部。嵌入操作仅修改实部,但 IFFT 后可能因浮点精度产生极小虚部(~10⁻¹⁰),取实部即丢弃这些计算噪声。
解码(Decode):提取水印
# 1. 读取原图与含水印图
img = cv2.imread('hui.png')
img_wm = cv2.imread('output.jpg')
# 2. 两种图分别做 FFT
f1 = np.fft.fft2(img) # 原图频域
f2 = np.fft.fft2(img_wm) # 含水印图频域
# 3. 频域相减除以 alpha → 恢复置乱后的水印
rwm = (f2 - f1) / alpha
rwm = np.real(rwm)
# 4. 用相同的 seed 生成相同的置乱序列
random.seed(seed)
m = list(range(int(rwm.shape[0] * 0.5)))
n = list(range(rwm.shape[1]))
random.shuffle(m)
random.shuffle(n)
# 5. 逆置乱 → 还原水印
wm = np.zeros(rwm.shape)
for i in range(int(rwm.shape[0] * 0.5)):
for j in range(rwm.shape[1]):
wm[m[i]][n[j]] = np.uint8(rwm[i][j])
# 下半部分镜像
for i in range(rwm.shape[0] // 2):
for j in range(rwm.shape[1]):
wm[rwm.shape[0] - i - 1][rwm.shape[1] - j - 1] = wm[i][j]
cv2.imwrite('extracted_wm.jpg', wm)保存误差度量
JPEG 有损压缩会引入噪点。程序计算了写入前后的均方根误差:
img_wm = np.real(img_encoded) # 内存中的原值
img_wm2 = cv2.imread(fn3) # JPEG 读取后的值
# 计算 RMS 误差百分比
sum = 0
for i in range(h):
for j in range(w):
for k in range(3):
sum += (img_wm[i][j][k] - img_wm2[i][j][k]) ** 2
miss = np.sqrt(sum) / (h * w * 3) * 100
# 输出: "Miss X.XX% in save"命令行用法
# 嵌入水印
python bwm.py encode hui.png wm.png output.jpg
# 提取水印(必须提供原图)
python bwm.py decode hui.png output.jpg extracted_wm.jpg
# 调试模式(显示每步的可视化输出)
python bwm.py encode hui.png wm.png output.jpg --debug
# 自定义种子(密钥)
python bwm.py encode hui.png wm.png output.jpg --seed 12345678
python bwm.py decode hui.png output.jpg extracted_wm.jpg --seed 12345678
# Python3 兼容 Python2 的 random 算法
python3 bwm.py decode hui.png output.jpg extracted_wm.jpg --oldseed
# 调整嵌入强度(数值越小越隐蔽)
python bwm.py encode hui.png wm.png output.jpg --alpha 1.5参数说明:
| 参数 | 默认值 | 说明 |
|---|---|---|
--debug | 关闭 | 开启后弹出 Matplotlib 窗口,显示每一步的频域/空间域图像 |
--seed <int> | 20160930 | 随机置乱的种子,相当于水印"加密密钥" |
--oldseed | 关闭 | Python3 环境启用 Python2 兼容的 random 算法 |
--alpha <float> | 3.0 | 嵌入强度:越小越隐蔽;越大提取越清晰但可见性增加 |
CTF 应用
典型题目特征
CTF MISC 题目中出现盲水印时,通常呈现以下形态:
- 题目给出一张"正常"图片(原图)和一张"略有差异"的图片(含水印图)
- 两张图片像素级肉眼不可区分
- 利用频域相减可提取隐藏信息(Flag)
通用解题流程
获得两张相似图片
│
▼
┌──────────────────────┐
│ 判断水印类型 │
│ · 频域水印 (FFT) │ ← 本工具
│ · LSB 水印 │
│ · DCT/DWT 水印 │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ 尝试默认 seed 提取 │
│ seed = 20160930 │
└──────────┬───────────┘
├─ 成功 → 得到 Flag
│
▼ 失败
┌──────────────────────┐
│ 暴力枚举 seed │
│ 或分析提示信息 │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ 调整 alpha 值 │
│ 从 0.5 逐步增加到 10 │
└──────────┬───────────┘
│
▼
得到 Flag延伸阅读
水印技术的数学分类
| 类别 | 变换域 | 鲁棒性 | CTF 出现频率 |
|---|---|---|---|
| FFT (快速傅里叶变换) | 频率域 | 中 | 高 (本工具) |
| DCT (离散余弦变换) | 频域 (JPEG 核心) | 高 | 中 |
| DWT (离散小波变换) | 时频域 | 高 | 低 |
| LSB (最低有效位) | 空间域 | 极低 | 最高 |
| SVD (奇异值分解) | 矩阵分解 | 高 | 低 |
alpha 选择指南
alpha 过小 (0.1~0.5) alpha 适中 (2~5) alpha 过大 (>10)
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 水印几乎不可见 │ │ 水印清晰可读 │ │ 原图出现可见噪点 │
│ 提取失败 / 噪声 │ │ 对原图视觉影响小 │ │ 水印暴露 │
│ 需原图精度极高 │ │ **推荐范围** │ │ 失去隐蔽意义 │
└─────────────────┘ └─────────────────┘ └─────────────────┘Python2/3 random 算法差异的根本原因
Python2 的 random.shuffle 使用 Wichmann-Hill (1984) 算法;Python3 默认使用 Mersenne Twister (MT19937)。相同 seed 在不同算法下产生的置乱序列不同,导致提取的水印为噪点。解决方式:
# 法 1:使用 Python2 运行
python2 bwm.py decode hui.png encoded.jpg out.jpg
# 法 2:Python3 加 --oldseed(切换为 Python2 的 random 实现)
python3 bwm.py decode hui.png encoded.jpg out.jpg --oldseed
参考文献
- chishaxie. BlindWaterMark - 盲水印 by Python [EB/OL]. GitHub, 2021. https://github.com/chishaxie/BlindWaterMark
- NumPy Contributors. numpy.fft.fft2 — Discrete Fourier Transform [EB/OL]. https://numpy.org/doc/stable/reference/generated/numpy.fft.fft2.html
- OpenCV. Image file reading and writing [EB/OL]. https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html
- Python Software Foundation. random — Generate pseudo-random numbers [EB/OL]. https://docs.python.org/3/library/random.html
- Mallat S. A Wavelet Tour of Signal Processing [M]. Academic Press, 1999.
