blind-watermark是一个能够给图片添加/解析基于频域的数字盲水印的Python库。图像水印image watermark是指在图片里添加文本或图形,以标记图片的来源。但是图像水印会破坏原图。因此另外一种水印形式,即图像盲水印blind image watermark在实践中更多地用于标记图像来源。图像盲水印是一种肉眼不可见的水印,以不可见的形式添加到原始图像中,不会对原始图像的质量产生很大影响。图像盲水印的具体原理见给你的图片加上盲水印。
blind-watermark安装命令如下:
pip install blind-watermark
下面的代码会读取图片并加入二进制数据盲水印。
import blind_watermark
# 关闭输出消息
blind_watermark.bw_notes.close()
from blind_watermark import att
from blind_watermark import WaterMark
import cv2
from blind_watermark import WaterMarkCore
import numpy as np
# 水印的长宽wm_shape
bwm = WaterMark(password_img=1, password_wm=1)
# 读取原图
imgpath = 'input.jpg'
bwm.read_img(imgpath)
wm = [True, False, True, False, True, False, True, False, True, False]
# 嵌入二进制bit数据
bwm.read_wm(wm, mode='bit')
# 打上盲水印
outputpath = 'output.png'
# 保存输出图片
bwm.embed(outputpath)
# 解水印需要用到长度
len_wm = len(wm)
# 抗攻击需要知道原图的shape
ori_img_shape = cv2.imread(imgpath).shape[:2]
上面的代码会往图片中添加二进制数据的盲水印,对比原图和加入盲水印的图片,可以发现虽然看不到水印,但是实际上图像质量有一定下降。
from PIL import Image
# 展示原图
image = Image.open(imgpath)
image.show()
# 展示添加盲水印后的图
image = Image.open(outputpath)
image.show()
以下代码会从加入盲水印的图像中提取水印结果。
# 注意设定水印的长宽wm_shape
bwm1 = WaterMark(password_img=1, password_wm=1)
# 提取水印
wm_extract = bwm1.extract(outputpath, wm_shape=len_wm, mode='bit')
print("不攻击的提取结果:", wm_extract)
assert np.all(wm == wm_extract), '提取水印和原水印不一致'
不攻击的提取结果: [ True False True False True False True False True False]
以下代码展示了对添加水印的图片进行截图后依然能够提取水印,这种方式只是将非截取区域用白色遮挡,不是真正的截图。
# 截取区域设置
# 截取方式x1, y1, x2, y2 = shape[0] * loc[0][0], shape[1] * loc[0][1], shape[0] * loc[1][0], shape[1] * loc[1][1]
# (x1,y1),(x2,y2)
loc = ((0.3, 0.1), (0.7, 0.9))
outputpath_ = '截屏攻击.png'
# 保存截屏后的图片
att.cut_att(input_filename=outputpath, output_file_name=outputpath_, loc=loc)
bwm1 = WaterMark(password_wm=1, password_img=1)
wm_extract = bwm1.extract(outputpath_, wm_shape=len_wm, mode='bit')
print("截屏攻击{loc}后的提取结果:".format(loc=loc), wm_extract)
assert np.all(wm == wm_extract), '提取水印和原水印不一致'
# 展示添加攻击后的盲水印图
image = Image.open(outputpath_)
image.show()
截屏攻击((0.3, 0.1), (0.7, 0.9))后的提取结果: [ True False True False True False True False True False]
以下代码展示了对添加水印的图片进行横向剪裁后依然能够提取水印。
r = 0.5
outputpath = 'output.png'
outputpath_ = '横向裁剪攻击.png'
outputpath_r = '横向裁剪攻击_填补.png'
att.cut_att_width(input_filename=outputpath, output_file_name=outputpath_, ratio=r)
# 需要填补图像,用空白填补图像
att.anti_cut_att(input_filename=outputpath_, output_file_name=outputpath_r,
origin_shape=ori_img_shape)
# extract:
bwm1 = WaterMark(password_wm=1, password_img=1)
wm_extract = bwm1.extract(outputpath_r, wm_shape=len_wm, mode='bit')
print(f"横向裁剪攻击r={r}后的提取结果:", wm_extract)
横向裁剪攻击r=0.5后的提取结果: [ True False True False True False True False True False]
# 展示添加横向裁剪攻击后的盲水印图
image = Image.open(outputpath_)
print(image.size)
image.show()
(177, 354)
# 展示添加横向裁剪攻击_填补后的盲水印图,缺失区域用白色填充,以保持和原图尺寸一致
image = Image.open(outputpath_r)
print(image.size)
image.show()
(354, 354)
以下代码展示了对添加水印的图片进行遮挡后依然能够提取水印。
outputpath_ = '遮挡攻击.png'
n = 60
att.shelter_att(input_filename=outputpath, output_file_name=outputpath_, ratio=0.1, n=n)
# 提取
bwm1 = WaterMark(password_wm=1, password_img=1)
wm_extract = bwm1.extract(outputpath_, wm_shape=len_wm, mode='bit')
print(f"遮挡攻击{n}后的提取结果:", wm_extract)
assert np.all(wm == wm_extract), '提取水印和原水印不一致'
# 展示添加攻击后的盲水印图
image = Image.open(outputpath_)
image.show()
遮挡攻击60后的提取结果: [ True False True False True False True False True False]
以下代码展示了对添加水印的图片进行旋转后依然能够提取水印,但是需要将旋转后的图片再旋转回来。
outputpath_ = '旋转攻击.png'
outputpath_r = '旋转攻击还原.png'
att.rot_att(input_filename=outputpath, output_file_name=outputpath_, angle=45)
att.rot_att(input_filename=outputpath_, output_file_name=outputpath_r, angle=-45)
# 提取水印
bwm1 = WaterMark(password_wm=1, password_img=1)
wm_extract = bwm1.extract(outputpath_r, wm_shape=len_wm, mode='bit')
print("旋转攻击后的提取结果:", wm_extract)
assert np.all(wm == wm_extract), '提取水印和原水印不一致'
# 展示添加攻击后的盲水印图
image = Image.open(outputpath_)
image.show()
旋转攻击后的提取结果: [ True False True False True False True False True False]
总之,blind_watermark提供了很稳定的盲水印添加和恢复方式,还有其他不同的攻击效果,比如亮度椒盐缩放。具体可以查看代码blind_watermark_bit。但是要注意的是,对于特定图像,添加某些图像处理效果blind_watermark是没法准确提取水印的。
下面的代码会读取图片并加入水印图片,水印图片不能大于1.936kb,恢复后的水印图片会丢失色彩信息。
import cv2
from blind_watermark import WaterMark
bwm = WaterMark(password_wm=1, password_img=1)
# 读取原图
imgpath = 'input.jpg'
bwm.read_img(filename = imgpath)
# 设置水印图片,水印图片不能大于1.936kb
markimgpath = 'watermark.bmp'
bwm.read_wm(markimgpath, mode='img')
outputpath = 'output.png'
# 打上盲水印
bwm.embed(outputpath)
wm_shape = cv2.imread(markimgpath, flags=cv2.IMREAD_GRAYSCALE).shape
bwm1 = WaterMark(password_wm=1, password_img=1)
# 注意需要设定水印的长宽wm_shape
wm_extract = bwm1.extract(outputpath, wm_shape=wm_shape, out_wm_name='wm_extracted.png', mode='img')
# 展示盲水印图
image = Image.open(outputpath)
image.show()
# 展示添加的水印图
image = Image.open(markimgpath)
image.show()
# 展示提取的水印图
image = Image.open('wm_extracted.png')
image.show()
下面的代码会读取图片并加入文字数据盲水印,这种方式也是最常见添加水印方法。
bwm = WaterMark(password_img=1, password_wm=1)
imgpath = 'input.jpg'
bwm.read_img(imgpath)
wm = 'hello 世界!'
bwm.read_wm(wm, mode='str')
outputpath = 'output.png'
bwm.embed(outputpath)
len_wm = len(bwm.wm_bit) # 解水印需要用到长度
print('Put down the length of wm_bit {len_wm}'.format(len_wm=len_wm))
ori_img_shape = cv2.imread(outputpath).shape[:2]
# 解水印
bwm1 = WaterMark(password_img=1, password_wm=1)
wm_extract = bwm1.extract(outputpath, wm_shape=len_wm, mode='str')
print("不攻击的提取结果:", wm_extract)
assert wm == wm_extract, '提取水印和原水印不一致'
Put down the length of wm_bit 119
不攻击的提取结果: hello 世界!
当然对存入水印后的图片进行图像变换也是可以恢复水印结果,具体使用可以参考blind_watermark_str。
以下代码展示了对添加水印的图片进行椒盐效果添加后依然能够提取水印。
# 往水印图片添加椒盐效果
# ratio是椒盐概率,太高恢复不了
ratio = 0.02
outputpath_ = '椒盐攻击.png'
att.salt_pepper_att(input_filename=outputpath, output_file_name=outputpath_, ratio=ratio)
# 提取
wm_extract = bwm1.extract(outputpath_, wm_shape=len_wm, mode='str')
print(f"椒盐攻击ratio={ratio}后的提取结果:", wm_extract)
assert np.all(wm == wm_extract), '提取水印和原水印不一致'
# 展示添加椒盐水印后的盲水印图
image = Image.open(outputpath_)
image.show()
椒盐攻击ratio=0.02后的提取结果: hello 世界!
以下代码展示了对添加水印的图片进行纵向剪裁后依然能够提取水印。
# 纵向剪裁图片
r = 0.4
outputpath = 'output.png'
outputpath_ = '纵向裁剪攻击.png'
outputpath_r = '纵向裁剪攻击_填补.png'
att.cut_att_height(input_filename=outputpath, output_file_name=outputpath_, ratio=r)
# 需要填补图像,用空白填补图像
att.anti_cut_att(input_filename=outputpath_, output_file_name=outputpath_r,
origin_shape=ori_img_shape)
# extract:
bwm1 = WaterMark(password_wm=1, password_img=1)
wm_extract = bwm1.extract(outputpath_r, wm_shape=len_wm, mode='str')
print(f"纵向裁剪攻击r={r}后的提取结果:", wm_extract)
纵向裁剪攻击r=0.4后的提取结果: hello 世界!
# 展示添加纵向裁剪攻击后的盲水印图
image = Image.open(outputpath_)
print(image.size)
image.show()
(354, 141)
# 展示添加纵向裁剪攻击_填补后的盲水印图,缺失区域用白色填充,以保持和原图尺寸一致
image = Image.open(outputpath_r)
print(image.size)
image.show()
(354, 354)