盲水印笔记

简述

水印(Water-Mark)是一种保护数字产品版权、完整性、防复制或去向追踪的技术,通常作为署名的一种。但水印的缺点显而易见,不止 PhotoShop 有去除水印的功能,同时可以使用截图等手段将水印抹除。

于是盲水印(Blind-Water-Mask)被发明出来,即字面意思——一种没办法用肉眼直接观察的水印。

盲水印有着隐蔽性(Imperceptible)、不易移除性(Non-removable)、强健性(Robustness)和明确性(Unambiguous),可以很好的满足当前数字版权保护的所需。

频域隐写技术

原理

盲水印流程图如下:

image-20220725205419764

傅里叶变换

盲水印用到的傅里叶变换是将信号在时域空域的函数转变为用频域表示。

图像是二维离散的,对该二维信号在各个方向上进行一次一维的傅里叶变换即可得到二维频域图(使用的是离散傅里叶变换 DFT 算法),其中二维信号为时域中图像的灰度 \(f(u,v)\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import cv2
import numpy as np
import matplotlib.pyplot as plt

# 直接读取为灰度图
img = cv2.imread('./lena.jpeg', 0)
# 进行快速傅里叶变换
f = np.fft.fft2(img)
# 取绝对值,将复数域映射到实数域
s = np.log(np.abs(f))
plt.subplot(121)
plt.imshow(img)
plt.subplot(122)
plt.imshow(s)

我们可以发现灰度信号被转化为了频域信号。

对图像进行离散傅里叶变换

如果我们将图片的低频部分移动到中间,将高频部分移动到外围:

1
2
3
4
5
6
7
8
# 将图像中的低频部分移动到图像的中心
fshift = np.fft.fftshift(f)
# 取绝对值,将复数域映射到实数域
s = np.log(np.abs(fshift))
plt.subplot(121)
plt.imshow(img)
plt.subplot(122)
plt.imshow(s)
对DFT结果进行fftshift

同样的逆变换就是做相反的操作

1
2
3
4
5
6
7
8
9
# 逆变换
ifshift = np.fft.ifftshift(fshift)
iimg = np.fft.ifft2(ifshift)
# 取绝对值,将复数域映射到实数域
s = np.abs(iimg)
plt.subplot(121)
plt.imshow(img)
plt.subplot(122)
plt.imshow(s)

我们可以发现完美地还原了图片。

DFT和IDFT的结果

那么根据这个原理,我们在变换和逆变换的中间,加入水印图像即可。

加入了水印后的DFT

水印对称性

由于傅里叶变换后,频域图 \(f(u, v)\)\(f(-u,-v)\) 是共轭的,所以其实部应该相等(如果不相等的话,逆变换后结果将在虚部上损失一部分信息,因为我们只写入实部数据),故我们应使得水印编码后的数据是对称的。

工具

BlindWaterMark

在盲水印的加密和解密中,我们通常使用 GitHub 上的开源脚本 BlindWaterMark

需要注意的是,该项目需要原图才能提取盲水印。

  • bwm.py 程序文件python2版本
  • bwmforpy3.py 程序文件python3版本
  • hui.png 无水印的原图
  • wm.png 水印图
  • hui_with_wm.png 有盲水印的图
  • wm_from_hui.png 解出来的水印图

加密语法:

1
python bwm.py encode hui.png wm.png hui_with_wm.png

解密语法:

1
python bwm.py decode hui.png hui_with_wm.png wm_from_hui.png

缺陷是该工具需要一张原图,且注意python2和python3版本程序的 encode 和 decode 结果会有所不同。主要原因是 python2 和 python3 的随机算法不同。如果想让 python3 兼容 python2 的随机算法,请添加 --oldseed 参数。

同时由于其水印编码方式是使用默认随机数的,如果需要指定随机数,需要使用 --seed 参数。

blind_watermark

该项目的优点是无需原图即可还原盲水印,同时支持文字水印等。

该项目在 GitHub 上开源,主页链接:https://github.com/guofei9987/blind_watermark

安装方式:

1
pip install blind-watermark

在 bash 中使用

1
2
3
4
# embed watermark into image:
blind_watermark --embed --pwd 1234 examples/pic/ori_img.jpeg "watermark text" examples/output/embedded.png
# extract watermark from image:
blind_watermark --extract --pwd 1234 --wm_shape 111 examples/output/embedded.png

在 Python 中使用

嵌入文字

嵌入水印:

1
2
3
4
5
6
7
8
9
from blind_watermark import WaterMark

bwm1 = WaterMark(password_img=1, password_wm=1)
bwm1.read_img('pic/ori_img.jpg')
wm = '@guofei9987 开源万岁!'
bwm1.read_wm(wm, mode='str')
bwm1.embed('output/embedded.png')
len_wm = len(bwm1.wm_bit)
print('Put down the length of wm_bit {len_wm}'.format(len_wm=len_wm))

提取水印:

1
2
3
bwm1 = WaterMark(password_img=1, password_wm=1)
wm_extract = bwm1.extract('output/embedded.png', wm_shape=len_wm, mode='str')
print(wm_extract)

输出:

@guofei9987 开源万岁!

嵌入图像

嵌入水印:

1
2
3
4
5
6
7
8
9
from blind_watermark import WaterMark

bwm1 = WaterMark(password_wm=1, password_img=1)
# read original image
bwm1.read_img('pic/ori_img.jpg')
# read watermark
bwm1.read_wm('pic/watermark.png')
# embed
bwm1.embed('output/embedded.png')

提取水印:

1
2
3
bwm1 = WaterMark(password_wm=1, password_img=1)
# notice that wm_shape is necessary
bwm1.extract(filename='output/embedded.png', wm_shape=(128, 128), out_wm_name='output/extracted.png', )

嵌入位数组

作为演示,我们嵌入了 6 个字节的数据:

1
wm = [True, False, True, True, True, False]

嵌入:

1
2
3
4
5
6
from blind_watermark import WaterMark

bwm1 = WaterMark(password_img=1, password_wm=1)
bwm1.read_ori_img('pic/ori_img.jpg')
bwm1.read_wm([True, False, True, True, True, False], mode='bit')
bwm1.embed('output/embedded.png')

提炼:

1
2
3
bwm1 = WaterMark(password_img=1, password_wm=1, wm_shape=6)
wm_extract = bwm1.extract('output/打上水印的图.png', mode='bit')
print(wm_extract)

注意wm_shape(水印的形状)是必要的

输出wm_extract是一个浮点数组。设置一个阈值,例如 0.5。