diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..81fb752 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +language: python +python: + - "3.8" + + +# command to install dependencies +install: + - pip install -r requirements.txt + - pip install numpy==1.18.1 + - pip install coverage codecov + - python setup.py install + +# command to run tests +script: + - cd examples + - coverage run -p example_no_writing.py + - coverage run -p example_bit.py + - coverage run -p example_str.py + - coverage run -p example_img.py + - cp .coverage.* .. + - cd .. + +# Push the results back to codecov +after_success: + - coverage combine + - codecov \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..21d3ccf --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 郭飞 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..34dee47 --- /dev/null +++ b/README.md @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + +
+ + + +# blind-watermark + +Blind watermark based on dct and svd. + + +[![PyPI](https://img.shields.io/pypi/v/blind_watermark)](https://pypi.org/project/blind_watermark/) +[![Build Status](https://travis-ci.com/guofei9987/blind_watermark.svg?branch=master)](https://travis-ci.com/guofei9987/blind_watermark) +[![codecov](https://codecov.io/gh/guofei9987/blind_watermark/branch/master/graph/badge.svg)](https://codecov.io/gh/guofei9987/blind_watermark) +[![License](https://img.shields.io/pypi/l/blind_watermark.svg)](https://github.com/guofei9987/blind_watermark/blob/master/LICENSE) +![Python](https://img.shields.io/badge/python->=3.5-green.svg) +![Platform](https://img.shields.io/badge/platform-windows%20|%20linux%20|%20macos-green.svg) +[![stars](https://img.shields.io/github/stars/guofei9987/blind_watermark.svg?style=social)](https://github.com/guofei9987/blind_watermark/) +[![fork](https://img.shields.io/github/forks/guofei9987/blind_watermark?style=social)](https://github.com/guofei9987/blind_watermark/fork) +[![Downloads](https://pepy.tech/badge/blind-watermark)](https://pepy.tech/project/blind-watermark) +[![Discussions](https://img.shields.io/badge/discussions-green.svg)](https://github.com/guofei9987/blind_watermark/discussions) + + +- **Documentation:** [https://BlindWatermark.github.io/blind_watermark/#/en/](https://BlindWatermark.github.io/blind_watermark/#/en/) +- **文档:** [https://BlindWatermark.github.io/blind_watermark/#/zh/](https://BlindWatermark.github.io/blind_watermark/#/zh/) +- **中文 readme** [README_cn.md](README_cn.md) +- **Source code:** [https://github.com/guofei9987/blind_watermark](https://github.com/guofei9987/blind_watermark) + + + +# install +```bash +pip install blind-watermark +``` + +For the current developer version: +```bach +git clone git@github.com:guofei9987/blind_watermark.git +cd blind_watermark +pip install . +``` + +# How to use + + +## Use in bash + + +```bash +# 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 +``` + + + +## Use in Python + +Original Image + Watermark = Watermarked Image + +![origin_image](docs/原图.jpeg) + '@guofei9987 开源万岁!' = ![打上水印的图](docs/打上水印的图.jpg) + + +See the [codes](/examples/example_str.py) + +Embed watermark: +```python +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)) +``` + +Extract watermark: +```python +bwm1 = WaterMark(password_img=1, password_wm=1) +wm_extract = bwm1.extract('output/embedded.png', wm_shape=len_wm, mode='str') +print(wm_extract) +``` +Output: +>@guofei9987 开源万岁! + +### attacks on Watermarked Image + + +|attack method|image after attack|extracted watermark| +|--|--|--| +|Rotate 45 Degrees|![旋转攻击](docs/旋转攻击.jpg)|'@guofei9987 开源万岁!'| +|Random crop|![截屏攻击](docs/截屏攻击2_还原.jpg)|'@guofei9987 开源万岁!'| +|Masks| ![多遮挡攻击](docs/多遮挡攻击.jpg) |'@guofei9987 开源万岁!'| +|Vertical cut|![横向裁剪攻击](docs/横向裁剪攻击_填补.jpg)|'@guofei9987 开源万岁!'| +|Horizontal cut|![纵向裁剪攻击](docs/纵向裁剪攻击_填补.jpg)|'@guofei9987 开源万岁!'| +|Resize|![缩放攻击](docs/缩放攻击.jpg)|'@guofei9987 开源万岁!'| +|Pepper Noise|![椒盐攻击](docs/椒盐攻击.jpg)|'@guofei9987 开源万岁!'| +|Brightness 10% Down|![亮度攻击](docs/亮度攻击.jpg)|'@guofei9987 开源万岁!'| + + + + + + +### embed images + +embed watermark: +```python +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') +``` + + +Extract watermark: +```python +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', ) +``` + + +|attack method|image after attack|extracted watermark| +|--|--|--| +|Rotate 45 Degrees|![旋转攻击](docs/旋转攻击.jpg)|![](docs/旋转攻击_提取水印.png)| +|Random crop|![截屏攻击](docs/截屏攻击2_还原.jpg)|![多遮挡_提取水印](docs/多遮挡攻击_提取水印.png)| +|Mask| ![多遮挡攻击](docs/多遮挡攻击.jpg) |![多遮挡_提取水印](docs/多遮挡攻击_提取水印.png)| + + +### embed array of bits + +See it [here](/examples/example_bit.py) + + +As demo, we embed 6 bytes data: +```python +wm = [True, False, True, True, True, False] +``` + +Embed: +```python +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') +``` + +Extract: +```python +bwm1 = WaterMark(password_img=1, password_wm=1, wm_shape=6) +wm_extract = bwm1.extract('output/打上水印的图.png', mode='bit') +print(wm_extract) +``` +Notice that `wm_shape` (shape of watermark) is necessary + +The output `wm_extract` is an array of float. set a threshold such as 0.5. + + +# Concurrency + +```python +WaterMark(..., processes=None) +``` +- `processes`: number of processes, can be integer. Default `None`, meaning use all processes. + +## Related Project + +text_blind_watermark: [https://github.com/guofei9987/text_blind_watermark](https://github.com/guofei9987/text_blind_watermark) +Embed message into text. diff --git a/README_cn.md b/README_cn.md new file mode 100644 index 0000000..a2c7b52 --- /dev/null +++ b/README_cn.md @@ -0,0 +1,173 @@ +# blind-watermark + +基于频域的数字盲水印 + + +[![PyPI](https://img.shields.io/pypi/v/blind_watermark)](https://pypi.org/project/blind_watermark/) +[![Build Status](https://travis-ci.com/guofei9987/blind_watermark.svg?branch=master)](https://travis-ci.com/guofei9987/blind_watermark) +[![codecov](https://codecov.io/gh/guofei9987/blind_watermark/branch/master/graph/badge.svg)](https://codecov.io/gh/guofei9987/blind_watermark) +[![License](https://img.shields.io/pypi/l/blind_watermark.svg)](https://github.com/guofei9987/blind_watermark/blob/master/LICENSE) +![Python](https://img.shields.io/badge/python->=3.5-green.svg) +![Platform](https://img.shields.io/badge/platform-windows%20|%20linux%20|%20macos-green.svg) +[![stars](https://img.shields.io/github/stars/guofei9987/blind_watermark.svg?style=social)](https://github.com/guofei9987/blind_watermark/) +[![fork](https://img.shields.io/github/forks/guofei9987/blind_watermark?style=social)](https://github.com/guofei9987/blind_watermark/fork) +[![Downloads](https://pepy.tech/badge/blind-watermark)](https://pepy.tech/project/blind-watermark) +[![Discussions](https://img.shields.io/badge/discussions-green.svg)](https://github.com/guofei9987/blind_watermark/discussions) + + +- **Documentation:** [https://BlindWatermark.github.io/blind_watermark/#/en/](https://BlindWatermark.github.io/blind_watermark/#/en/) +- **文档:** [https://BlindWatermark.github.io/blind_watermark/#/zh/](https://BlindWatermark.github.io/blind_watermark/#/zh/) +- **English readme** [README.md](README.md) +- **Source code:** [https://github.com/guofei9987/blind_watermark](https://github.com/guofei9987/blind_watermark) + +# 安装 +```bash +pip install blind-watermark +``` + +或者安装最新开发版本 +```bach +git clone git@github.com:guofei9987/blind_watermark.git +cd blind_watermark +pip install . +``` + +# 如何使用 + +## 命令行中使用 + +```bash +# 嵌入水印: +blind_watermark --embed --pwd 1234 examples/pic/ori_img.jpeg "watermark text" examples/output/embedded.png +# 提取水印: +blind_watermark --extract --pwd 1234 --wm_shape 111 examples/output/embedded.png +``` + + + +## Python 中使用 + +原图 + 水印 = 打上水印的图 + +![origin_image](docs/原图.jpeg) + '@guofei9987 开源万岁!' = ![打上水印的图](docs/打上水印的图.jpg) + + + +参考 [代码](/examples/example_str.py) + + +嵌入水印 +```python +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)) +``` + + +提取水印 +```python +bwm1 = WaterMark(password_img=1, password_wm=1) +wm_extract = bwm1.extract('output/embedded.png', wm_shape=len_wm, mode='str') +print(wm_extract) +``` +Output: +>@guofei9987 开源万岁! + + +### 各种攻击后的效果 + +|攻击方式|攻击后的图片|提取的水印| +|--|--|--| +|旋转攻击45度|![旋转攻击](docs/旋转攻击.jpg)|'@guofei9987 开源万岁!'| +|随机截图|![截屏攻击](docs/截屏攻击2_还原.jpg)|'@guofei9987 开源万岁!'| +|多遮挡| ![多遮挡攻击](docs/多遮挡攻击.jpg) |'@guofei9987 开源万岁!'| +|纵向裁剪|![横向裁剪攻击](docs/横向裁剪攻击_填补.jpg)|'@guofei9987 开源万岁!'| +|横向裁剪|![纵向裁剪攻击](docs/纵向裁剪攻击_填补.jpg)|'@guofei9987 开源万岁!'| +|缩放攻击|![缩放攻击](docs/缩放攻击.jpg)|'@guofei9987 开源万岁!'| +|椒盐攻击|![椒盐攻击](docs/椒盐攻击.jpg)|'@guofei9987 开源万岁!'| +|亮度攻击|![亮度攻击](docs/亮度攻击.jpg)|'@guofei9987 开源万岁!'| + + + +### 嵌入图片 + +参考 [代码](/examples/example_str.py) + + +嵌入: +```python +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') +``` + +提取: +```python +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', ) +``` + +|攻击方式|攻击后的图片|提取的水印| +|--|--|--| +|旋转攻击45度|![旋转攻击](docs/旋转攻击.jpg)|![](docs/旋转攻击_提取水印.png)| +|随机截图|![截屏攻击](docs/截屏攻击2_还原.jpg)|![](docs/旋转攻击_提取水印.png)| +|多遮挡| ![多遮挡攻击](docs/多遮挡攻击.jpg) |![多遮挡_提取水印](docs/多遮挡攻击_提取水印.png)| + + + +### 隐水印还可以是二进制数据 + +参考 [代码](/examples/example_bit.py) + + +作为 demo, 如果要嵌入是如下长度为6的二进制数据 +```python +wm = [True, False, True, True, True, False] +``` + +嵌入水印 + +```python +# 除了嵌入图片,也可以嵌入比特类数据 +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/打上水印的图.png') +``` + +解水印:(注意设定水印形状 `wm_shape`) +```python +bwm1 = WaterMark(password_img=1, password_wm=1, wm_shape=6) +wm_extract = bwm1.extract('output/打上水印的图.png', mode='bit') +print(wm_extract) +``` + +解出的水印是一个0~1之间的实数,方便用户自行卡阈值。如果水印信息量远小于图片可容纳量,偏差极小。 + +# 并行计算 + +```python +WaterMark(..., processes=None) +``` +- `processes`: 整数,指定线程数。默认为 `None`, 表示使用全部线程。 + + +## 相关项目 + +text_blind_watermark: [https://github.com/guofei9987/text_blind_watermark](https://github.com/guofei9987/text_blind_watermark) +文本盲水印,把信息隐秘地打入文本. diff --git a/blind_watermark/__init__.py b/blind_watermark/__init__.py new file mode 100644 index 0000000..ceb842b --- /dev/null +++ b/blind_watermark/__init__.py @@ -0,0 +1,6 @@ +from .blind_watermark import WaterMark +from .bwm_core import WaterMarkCore +from .att import * +from .recover import recover_crop + +__version__ = '0.3.1' diff --git a/blind_watermark/att.py b/blind_watermark/att.py new file mode 100644 index 0000000..6eaea05 --- /dev/null +++ b/blind_watermark/att.py @@ -0,0 +1,199 @@ +# coding=utf-8 + +# attack on the watermark +import cv2 +import numpy as np + + +def cut_att_height(input_filename=None, input_img=None, output_file_name=None, ratio=0.8): + # 纵向剪切攻击 + if input_filename: + input_img = cv2.imread(input_filename) + input_img_shape = input_img.shape + height = int(input_img_shape[0] * ratio) + + output_img = input_img[:height, :, :] + if output_file_name: + cv2.imwrite(output_file_name, output_img) + return output_img + + +def cut_att_width(input_filename=None, input_img=None, output_file_name=None, ratio=0.8): + # 横向裁剪攻击 + if input_filename: + input_img = cv2.imread(input_filename) + input_img_shape = input_img.shape + width = int(input_img_shape[1] * ratio) + + output_img = input_img[:, :width, :] + if output_file_name: + cv2.imwrite(output_file_name, output_img) + return output_img + + +def cut_att(input_filename=None, output_file_name=None, input_img=None, loc=((0.3, 0.1), (0.7, 0.9)), resize=0.6): + # 截屏攻击 = 裁剪攻击 + 缩放攻击 + 知道攻击参数(按照参数还原) + # 裁剪攻击:其它部分都补0 + if input_filename: + input_img = cv2.imread(input_filename) + + output_img = input_img.copy() + shape = output_img.shape + x1, y1, x2, y2 = shape[0] * loc[0][0], shape[1] * loc[0][1], shape[0] * loc[1][0], shape[1] * loc[1][1] + output_img[:int(x1), :] = 255 + output_img[int(x2):, :] = 255 + output_img[:, :int(y1)] = 255 + output_img[:, int(y2):] = 255 + + # 缩放一次,然后还原 + output_img = cv2.resize(output_img, + dsize=(int(shape[1] * resize), int(shape[0] * resize)) + ) + + output_img = cv2.resize(output_img, dsize=(int(shape[1]), int(shape[0]))) + + if output_file_name is not None: + cv2.imwrite(output_file_name, output_img) + return output_img + + +def cut_att2(input_filename=None, input_img=None, output_file_name=None, loc_r=((0.3, 0.1), (0.9, 0.9)), scale=1.1): + # 截屏攻击 = 剪切攻击 + 缩放攻击 + 不知道攻击参数 + if input_filename: + input_img = cv2.imread(input_filename) + h, w, _ = input_img.shape + + output_img = input_img.copy() + # 剪切攻击 + x1, y1, x2, y2 = w * loc_r[0][0], h * loc_r[0][1], w * loc_r[1][0], h * loc_r[1][1] + x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) + output_img = output_img[y1:y2, x1:x2] + + # 缩放攻击 + h, w, _ = output_img.shape + output_img = cv2.resize(output_img, dsize=(int(w * scale), int(h * scale))) + + if output_file_name: + cv2.imwrite(output_file_name, output_img) + return output_img, (x1, y1, x2, y2) + + +def anti_cut_att_old(input_filename, output_file_name, origin_shape): + # 反裁剪攻击:复制一块范围,然后补全 + # origin_shape 分辨率与约定理解的是颠倒的,约定的是列数*行数 + input_img = cv2.imread(input_filename) + output_img = input_img.copy() + output_img_shape = output_img.shape + if output_img_shape[0] > origin_shape[0] or output_img_shape[0] > origin_shape[0]: + print('裁剪打击后的图片,不可能比原始图片大,检查一下') + return + + # 还原纵向打击 + while output_img_shape[0] < origin_shape[0]: + output_img = np.concatenate([output_img, output_img[:origin_shape[0] - output_img_shape[0], :, :]], axis=0) + output_img_shape = output_img.shape + while output_img_shape[1] < origin_shape[1]: + output_img = np.concatenate([output_img, output_img[:, :origin_shape[1] - output_img_shape[1], :]], axis=1) + output_img_shape = output_img.shape + + cv2.imwrite(output_file_name, output_img) + + +def anti_cut_att(input_filename=None, input_img=None, output_file_name=None, origin_shape=None): + # 反裁剪攻击:补0 + # origin_shape 分辨率与约定理解的是颠倒的,约定的是列数*行数 + if input_filename: + input_img = cv2.imread(input_filename) + output_img = input_img.copy() + output_img_shape = output_img.shape + if output_img_shape[0] > origin_shape[0] or output_img_shape[0] > origin_shape[0]: + print('裁剪打击后的图片,不可能比原始图片大,检查一下') + return + + # 还原纵向打击 + if output_img_shape[0] < origin_shape[0]: + output_img = np.concatenate( + [output_img, 255 * np.ones((origin_shape[0] - output_img_shape[0], output_img_shape[1], 3))] + , axis=0) + output_img_shape = output_img.shape + + if output_img_shape[1] < origin_shape[1]: + output_img = np.concatenate( + [output_img, 255 * np.ones((output_img_shape[0], origin_shape[1] - output_img_shape[1], 3))] + , axis=1) + + if output_file_name: + cv2.imwrite(output_file_name, output_img) + return output_img + + +def resize_att(input_filename=None, input_img=None, output_file_name=None, out_shape=(500, 500)): + # 缩放攻击:因为攻击和还原都是缩放,所以攻击和还原都调用这个函数 + if input_filename: + input_img = cv2.imread(input_filename) + output_img = cv2.resize(input_img, dsize=out_shape) + if output_file_name: + cv2.imwrite(output_file_name, output_img) + return output_img + + +def bright_att(input_filename=None, input_img=None, output_file_name=None, ratio=0.8): + # 亮度调整攻击,ratio应当多于0 + # ratio>1是调得更亮,ratio<1是亮度更暗 + if input_filename: + input_img = cv2.imread(input_filename) + output_img = input_img * ratio + output_img[output_img > 255] = 255 + if output_file_name: + cv2.imwrite(output_file_name, output_img) + return output_img + + +def shelter_att(input_filename=None, input_img=None, output_file_name=None, ratio=0.1, n=3): + # 遮挡攻击:遮挡图像中的一部分 + # n个遮挡块 + # 每个遮挡块所占比例为ratio + if input_filename: + output_img = cv2.imread(input_filename) + else: + output_img = input_img.copy() + input_img_shape = output_img.shape + + for i in range(n): + tmp = np.random.rand() * (1 - ratio) # 随机选择一个地方,1-ratio是为了防止溢出 + start_height, end_height = int(tmp * input_img_shape[0]), int((tmp + ratio) * input_img_shape[0]) + tmp = np.random.rand() * (1 - ratio) + start_width, end_width = int(tmp * input_img_shape[1]), int((tmp + ratio) * input_img_shape[1]) + + output_img[start_height:end_height, start_width:end_width, :] = 255 + + if output_file_name: + cv2.imwrite(output_file_name, output_img) + return output_img + + +def salt_pepper_att(input_filename=None, input_img=None, output_file_name=None, ratio=0.01): + # 椒盐攻击 + if input_filename: + input_img = cv2.imread(input_filename) + input_img_shape = input_img.shape + output_img = input_img.copy() + for i in range(input_img_shape[0]): + for j in range(input_img_shape[1]): + if np.random.rand() < ratio: + output_img[i, j, :] = 255 + if output_file_name: + cv2.imwrite(output_file_name, output_img) + return output_img + + +def rot_att(input_filename=None, input_img=None, output_file_name=None, angle=45): + # 旋转攻击 + if input_filename: + input_img = cv2.imread(input_filename) + rows, cols, _ = input_img.shape + M = cv2.getRotationMatrix2D(center=(cols / 2, rows / 2), angle=angle, scale=1) + output_img = cv2.warpAffine(input_img, M, (cols, rows)) + if output_file_name: + cv2.imwrite(output_file_name, output_img) + return output_img diff --git a/blind_watermark/blind_watermark.py b/blind_watermark/blind_watermark.py new file mode 100644 index 0000000..ca58be6 --- /dev/null +++ b/blind_watermark/blind_watermark.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# coding=utf-8 +# @Time : 2020/8/13 +# @Author : github.com/guofei9987 +import warnings + +import numpy as np +import cv2 + +from .bwm_core import WaterMarkCore + + +class WaterMark: + def __init__(self, password_wm=1, password_img=1, block_shape=(4, 4), mode='common', processes=None): + + self.bwm_core = WaterMarkCore(password_img=password_img, mode=mode, processes=processes) + + self.password_wm = password_wm + + self.wm_bit = None + self.wm_size = 0 + + def read_img(self, filename=None, img=None): + if filename is not None: + # 从文件读入图片 + img = cv2.imread(filename, flags=cv2.IMREAD_UNCHANGED) + assert img is not None, "image file '{filename}' not read".format(filename=filename) + + self.bwm_core.read_img_arr(img=img) + return img + + def read_wm(self, wm_content, mode='img'): + assert mode in ('img', 'str', 'bit'), "mode in ('img','str','bit')" + if mode == 'img': + wm = cv2.imread(filename=wm_content, flags=cv2.IMREAD_GRAYSCALE) + assert wm is not None, 'file "{filename}" not read'.format(filename=wm_content) + + # 读入图片格式的水印,并转为一维 bit 格式,抛弃灰度级别 + self.wm_bit = wm.flatten() > 128 + + elif mode == 'str': + byte = bin(int(wm_content.encode('utf-8').hex(), base=16))[2:] + self.wm_bit = (np.array(list(byte)) == '1') + else: + self.wm_bit = np.array(wm_content) + + self.wm_size = self.wm_bit.size + + # 水印加密: + np.random.RandomState(self.password_wm).shuffle(self.wm_bit) + + self.bwm_core.read_wm(self.wm_bit) + + def embed(self, filename=None): + embed_img = self.bwm_core.embed() + + if filename is not None: + cv2.imwrite(filename, embed_img) + return embed_img + + def extract_decrypt(self, wm_avg): + wm_index = np.arange(self.wm_size) + np.random.RandomState(self.password_wm).shuffle(wm_index) + wm_avg[wm_index] = wm_avg.copy() + return wm_avg + + def extract(self, filename=None, embed_img=None, wm_shape=None, out_wm_name=None, mode='img'): + assert wm_shape is not None, 'wm_shape needed' + + if filename is not None: + embed_img = cv2.imread(filename, flags=cv2.IMREAD_COLOR) + assert embed_img is not None, "{filename} not read".format(filename=filename) + + self.wm_size = np.array(wm_shape).prod() + + if mode in ('str', 'bit'): + wm_avg = self.bwm_core.extract_with_kmeans(img=embed_img, wm_shape=wm_shape) + else: + wm_avg = self.bwm_core.extract(img=embed_img, wm_shape=wm_shape) + + # 解密: + wm = self.extract_decrypt(wm_avg=wm_avg) + + # 转化为指定格式: + if mode == 'img': + wm = 255 * wm.reshape(wm_shape[0], wm_shape[1]) + cv2.imwrite(out_wm_name, wm) + elif mode == 'str': + byte = ''.join((np.round(wm)).astype(np.int).astype(np.str)) + wm = bytes.fromhex(hex(int(byte, base=2))[2:]).decode('utf-8', errors='replace') + + return wm diff --git a/blind_watermark/bwm_core.py b/blind_watermark/bwm_core.py new file mode 100644 index 0000000..4f4ee90 --- /dev/null +++ b/blind_watermark/bwm_core.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# coding=utf-8 +# @Time : 2021/12/17 +# @Author : github.com/guofei9987 +import numpy as np +from numpy.linalg import svd +import copy +import cv2 +from cv2 import dct, idct +from pywt import dwt2, idwt2 +from .pool import AutoPool + + +class WaterMarkCore: + def __init__(self, password_img=1, mode='common', processes=None): + self.block_shape = np.array([4, 4]) + self.password_img = password_img + self.d1, self.d2 = 36, 20 # d1/d2 越大鲁棒性越强,但输出图片的失真越大 + + # init data + self.img, self.img_YUV = None, None # self.img 是原图,self.img_YUV 对像素做了加白偶数化 + self.ca, self.hvd, = [np.array([])] * 3, [np.array([])] * 3 # 每个通道 dct 的结果 + self.ca_block = [np.array([])] * 3 # 每个 channel 存一个四维 array,代表四维分块后的结果 + self.ca_part = [np.array([])] * 3 # 四维分块后,有时因不整除而少一部分,self.ca_part 是少这一部分的 self.ca + + self.wm_size, self.block_num = 0, 0 # 水印的长度,原图片可插入信息的个数 + self.pool = AutoPool(mode=mode, processes=processes) + + self.fast_mode = False + self.alpha = None # 用于处理透明图 + + def init_block_index(self): + self.block_num = self.ca_block_shape[0] * self.ca_block_shape[1] + assert self.wm_size < self.block_num, IndexError( + '最多可嵌入{}kb信息,多于水印的{}kb信息,溢出'.format(self.block_num / 1000, self.wm_size / 1000)) + # self.part_shape 是取整后的ca二维大小,用于嵌入时忽略右边和下面对不齐的细条部分。 + self.part_shape = self.ca_block_shape[:2] * self.block_shape + self.block_index = [(i, j) for i in range(self.ca_block_shape[0]) for j in range(self.ca_block_shape[1])] + + def read_img_arr(self, img): + # 处理透明图 + self.alpha = None + if img.shape[2] == 4: + if img[:, :, 3].min() < 255: + self.alpha = img[:, :, 3] + img = img[:, :, :3] + + # 读入图片->YUV化->加白边使像素变偶数->四维分块 + self.img = img.astype(np.float32) + self.img_shape = self.img.shape[:2] + + # 如果不是偶数,那么补上白边,Y(明亮度)UV(颜色) + self.img_YUV = cv2.copyMakeBorder(cv2.cvtColor(self.img, cv2.COLOR_BGR2YUV), + 0, self.img.shape[0] % 2, 0, self.img.shape[1] % 2, + cv2.BORDER_CONSTANT, value=(0, 0, 0)) + + self.ca_shape = [(i + 1) // 2 for i in self.img_shape] + + self.ca_block_shape = (self.ca_shape[0] // self.block_shape[0], self.ca_shape[1] // self.block_shape[1], + self.block_shape[0], self.block_shape[1]) + strides = 4 * np.array([self.ca_shape[1] * self.block_shape[0], self.block_shape[1], self.ca_shape[1], 1]) + + for channel in range(3): + self.ca[channel], self.hvd[channel] = dwt2(self.img_YUV[:, :, channel], 'haar') + # 转为4维度 + self.ca_block[channel] = np.lib.stride_tricks.as_strided(self.ca[channel].astype(np.float32), + self.ca_block_shape, strides) + + def read_wm(self, wm_bit): + self.wm_bit = wm_bit + self.wm_size = wm_bit.size + + def block_add_wm(self, arg): + if self.fast_mode: + return self.block_add_wm_fast(arg) + else: + return self.block_add_wm_slow(arg) + + def block_add_wm_slow(self, arg): + block, shuffler, i = arg + # dct->(flatten->加密->逆flatten)->svd->打水印->逆svd->(flatten->解密->逆flatten)->逆dct + wm_1 = self.wm_bit[i % self.wm_size] + block_dct = dct(block) + + # 加密(打乱顺序) + block_dct_shuffled = block_dct.flatten()[shuffler].reshape(self.block_shape) + u, s, v = svd(block_dct_shuffled) + s[0] = (s[0] // self.d1 + 1 / 4 + 1 / 2 * wm_1) * self.d1 + if self.d2: + s[1] = (s[1] // self.d2 + 1 / 4 + 1 / 2 * wm_1) * self.d2 + + block_dct_flatten = np.dot(u, np.dot(np.diag(s), v)).flatten() + block_dct_flatten[shuffler] = block_dct_flatten.copy() + return idct(block_dct_flatten.reshape(self.block_shape)) + + def block_add_wm_fast(self, arg): + # dct->svd->打水印->逆svd->逆dct + block, shuffler, i = arg + wm_1 = self.wm_bit[i % self.wm_size] + + u, s, v = svd(dct(block)) + s[0] = (s[0] // self.d1 + 1 / 4 + 1 / 2 * wm_1) * self.d1 + + return idct(np.dot(u, np.dot(np.diag(s), v))) + + def embed(self): + self.init_block_index() + + embed_ca = copy.deepcopy(self.ca) + embed_YUV = [np.array([])] * 3 + + self.idx_shuffle = random_strategy1(self.password_img, self.block_num, + self.block_shape[0] * self.block_shape[1]) + for channel in range(3): + tmp = self.pool.map(self.block_add_wm, + [(self.ca_block[channel][self.block_index[i]], self.idx_shuffle[i], i) + for i in range(self.block_num)]) + + for i in range(self.block_num): + self.ca_block[channel][self.block_index[i]] = tmp[i] + + # 4维分块变回2维 + self.ca_part[channel] = np.concatenate(np.concatenate(self.ca_block[channel], 1), 1) + # 4维分块时右边和下边不能整除的长条保留,其余是主体部分,换成 embed 之后的频域的数据 + embed_ca[channel][:self.part_shape[0], :self.part_shape[1]] = self.ca_part[channel] + # 逆变换回去 + embed_YUV[channel] = idwt2((embed_ca[channel], self.hvd[channel]), "haar") + + # 合并3通道 + embed_img_YUV = np.stack(embed_YUV, axis=2) + # 之前如果不是2的整数,增加了白边,这里去除掉 + embed_img_YUV = embed_img_YUV[:self.img_shape[0], :self.img_shape[1]] + embed_img = cv2.cvtColor(embed_img_YUV, cv2.COLOR_YUV2BGR) + embed_img = np.clip(embed_img, a_min=0, a_max=255) + + if self.alpha is not None: + embed_img = cv2.merge([embed_img.astype(np.uint8), self.alpha]) + return embed_img + + def block_get_wm(self, args): + if self.fast_mode: + return self.block_get_wm_fast(args) + else: + return self.block_get_wm_slow(args) + + def block_get_wm_slow(self, args): + block, shuffler = args + # dct->flatten->加密->逆flatten->svd->解水印 + block_dct_shuffled = dct(block).flatten()[shuffler].reshape(self.block_shape) + + u, s, v = svd(block_dct_shuffled) + wm = (s[0] % self.d1 > self.d1 / 2) * 1 + if self.d2: + tmp = (s[1] % self.d2 > self.d2 / 2) * 1 + wm = (wm * 3 + tmp * 1) / 4 + return wm + + def block_get_wm_fast(self, args): + block, shuffler = args + # dct->flatten->加密->逆flatten->svd->解水印 + u, s, v = svd(dct(block)) + wm = (s[0] % self.d1 > self.d1 / 2) * 1 + + return wm + + def extract_raw(self, img): + # 每个分块提取 1 bit 信息 + self.read_img_arr(img=img) + self.init_block_index() + + wm_block_bit = np.zeros(shape=(3, self.block_num)) # 3个channel,length 个分块提取的水印,全都记录下来 + + self.idx_shuffle = random_strategy1(seed=self.password_img, + size=self.block_num, + block_shape=self.block_shape[0] * self.block_shape[1], # 16 + ) + for channel in range(3): + wm_block_bit[channel, :] = self.pool.map(self.block_get_wm, + [(self.ca_block[channel][self.block_index[i]], self.idx_shuffle[i]) + for i in range(self.block_num)]) + return wm_block_bit + + def extract_avg(self, wm_block_bit): + # 对循环嵌入+3个 channel 求平均 + wm_avg = np.zeros(shape=self.wm_size) + for i in range(self.wm_size): + wm_avg[i] = wm_block_bit[:, i::self.wm_size].mean() + return wm_avg + + def extract(self, img, wm_shape): + self.wm_size = np.array(wm_shape).prod() + + # 提取每个分块埋入的 bit: + wm_block_bit = self.extract_raw(img=img) + # 做平均: + wm_avg = self.extract_avg(wm_block_bit) + return wm_avg + + def extract_with_kmeans(self, img, wm_shape): + wm_avg = self.extract(img=img, wm_shape=wm_shape) + + return one_dim_kmeans(wm_avg) + + +def one_dim_kmeans(inputs): + threshold = 0 + e_tol = 10 ** (-6) + center = [inputs.min(), inputs.max()] # 1. 初始化中心点 + for i in range(300): + threshold = (center[0] + center[1]) / 2 + is_class01 = inputs > threshold # 2. 检查所有点与这k个点之间的距离,每个点归类到最近的中心 + center = [inputs[~is_class01].mean(), inputs[is_class01].mean()] # 3. 重新找中心点 + if np.abs((center[0] + center[1]) / 2 - threshold) < e_tol: # 4. 停止条件 + threshold = (center[0] + center[1]) / 2 + break + + is_class01 = inputs > threshold + return is_class01 + + +def random_strategy1(seed, size, block_shape): + return np.random.RandomState(seed) \ + .random(size=(size, block_shape)) \ + .argsort(axis=1) + + +def random_strategy2(seed, size, block_shape): + one_line = np.random.RandomState(seed) \ + .random(size=(1, block_shape)) \ + .argsort(axis=1) + + return np.repeat(one_line, repeats=size, axis=0) diff --git a/blind_watermark/cli_tools.py b/blind_watermark/cli_tools.py new file mode 100644 index 0000000..011dec4 --- /dev/null +++ b/blind_watermark/cli_tools.py @@ -0,0 +1,53 @@ +from optparse import OptionParser +from .blind_watermark import WaterMark + +usage1 = 'blind_watermark --embed --pwd 1234 image.jpg "watermark text" embed.png' +usage2 = 'blind_watermark --extract --pwd 1234 --wm_shape 111 embed.png' +optParser = OptionParser(usage=usage1 + '\n' + usage2) + +optParser.add_option('--embed', dest='work_mode', action='store_const', const='embed' + , help='Embed watermark into images') +optParser.add_option('--extract', dest='work_mode', action='store_const', const='extract' + , help='Extract watermark from images') + +optParser.add_option('-p', '--pwd', dest='password', help='password, like 1234') +optParser.add_option('--wm_shape', dest='wm_shape', help='Watermark shape, like 120') + +(opts, args) = optParser.parse_args() + + +def main(): + bwm1 = WaterMark(password_img=int(opts.password)) + if opts.work_mode == 'embed': + if not len(args) == 3: + print('Error! Usage: ') + print(usage1) + return + else: + bwm1.read_img(args[0]) + bwm1.read_wm(args[1], mode='str') + bwm1.embed(args[2]) + print('Embed succeed! to file ', args[2]) + print('Put down watermark size:', len(bwm1.wm_bit)) + + if opts.work_mode == 'extract': + if not len(args) == 1: + print('Error! Usage: ') + print(usage2) + return + + else: + wm_str = bwm1.extract(filename=args[0], wm_shape=int(opts.wm_shape), mode='str') + print('Extract succeed! watermark is:') + print(wm_str) + + +''' +python -m blind_watermark.cli_tools --embed --pwd 1234 examples/pic/ori_img.jpeg "watermark text" examples/output/embedded.png +python -m blind_watermark.cli_tools --extract --pwd 1234 --wm_shape 111 examples/output/embedded.png + + +cd examples +blind_watermark --embed --pwd 1234 examples/pic/ori_img.jpeg "watermark text" examples/output/embedded.png +blind_watermark --extract --pwd 1234 --wm_shape 111 examples/output/embedded.png +''' diff --git a/blind_watermark/pool.py b/blind_watermark/pool.py new file mode 100644 index 0000000..8dfa7cc --- /dev/null +++ b/blind_watermark/pool.py @@ -0,0 +1,38 @@ +import sys +import multiprocessing +import warnings + +if sys.platform != 'win32': + multiprocessing.set_start_method('fork') + + +class CommonPool(object): + def map(self, func, args): + return list(map(func, args)) + + +class AutoPool(object): + def __init__(self, mode, processes): + + if mode == 'multiprocessing' and sys.platform == 'win32': + warnings.warn('multiprocessing not support in windows, turning to multithreading') + mode = 'multithreading' + + self.mode = mode + self.processes = processes + + if mode == 'vectorization': + pass + elif mode == 'cached': + pass + elif mode == 'multithreading': + from multiprocessing.dummy import Pool as ThreadPool + self.pool = ThreadPool(processes=processes) + elif mode == 'multiprocessing': + from multiprocessing import Pool + self.pool = Pool(processes=processes) + else: # common + self.pool = CommonPool() + + def map(self, func, args): + return self.pool.map(func, args) diff --git a/blind_watermark/recover.py b/blind_watermark/recover.py new file mode 100644 index 0000000..8243363 --- /dev/null +++ b/blind_watermark/recover.py @@ -0,0 +1,94 @@ +import cv2 +import numpy as np + +import functools + + +# 一个帮助缓存化加速的类,引入事实上的全局变量 +class MyValues: + def __init__(self): + self.idx = 0 + self.image, self.template = None, None + + def set_val(self, image, template): + self.idx += 1 + self.image, self.template = image, template + + +my_value = MyValues() + + +@functools.lru_cache(maxsize=None, typed=False) +def match_template(w, h, idx): + image, template = my_value.image, my_value.template + resized = cv2.resize(template, dsize=(w, h)) + scores = cv2.matchTemplate(image, resized, cv2.TM_CCOEFF_NORMED) + ind = np.unravel_index(np.argmax(scores, axis=None), scores.shape) + return ind, scores[ind] + + +def match_template_by_scale(scale): + image, template = my_value.image, my_value.template + w, h = int(np.round(template.shape[1] * scale)), int(template.shape[0] * scale) + ind, score = match_template(w, h, idx=my_value.idx) + return ind, score, scale + + +def search_template(scale=(0.5, 2), search_num=200): + image, template = my_value.image, my_value.template + # 局部暴力搜索算法,寻找最优的scale + tmp = [] + min_scale, max_scale = scale + + max_scale = min(max_scale, image.shape[0] / template.shape[0], image.shape[1] / template.shape[1]) + + max_idx = 0 + + for i in range(2): + for scale in np.linspace(min_scale, max_scale, search_num): + ind, score, scale = match_template_by_scale(scale) + tmp.append([ind, score, scale]) + + # 寻找最佳 + max_idx = 0 + max_score = 0 + for idx, (ind, score, scale) in enumerate(tmp): + if score > max_score: + max_idx, max_score = idx, score + + min_scale, max_scale = tmp[max(0, max_idx - 1)][2], tmp[max(0, max_idx + 1)][2] + + search_num = 2 * int((max_scale - min_scale) * max(template.shape[1], template.shape[0])) + 1 + + return tmp[max_idx] + + +def estimate_crop_parameters(original_file=None, template_file=None, ori_img=None, tem_img=None + , scale=(0.5, 2), search_num=200): + # 推测攻击后的图片,在原图片中的位置、大小 + if template_file: + tem_img = cv2.imread(template_file, cv2.IMREAD_GRAYSCALE) # template image + if original_file: + ori_img = cv2.imread(original_file, cv2.IMREAD_GRAYSCALE) # image + + my_value.set_val(image=ori_img, template=tem_img) + ind, score, scale_infer = search_template(scale=scale, search_num=search_num) + w, h = int(tem_img.shape[1] * scale_infer), int(tem_img.shape[0] * scale_infer) + # x1, y1, x2, y2 = ind[0], ind[1], ind[0] + h, ind[1] + w + x1, y1, x2, y2 = ind[1], ind[0], ind[1] + w, ind[0] + h + return (x1, y1, x2, y2), ori_img.shape, score, scale_infer + + +def recover_crop(template_file=None, tem_img=None, output_file_name=None, loc=None, image_o_shape=None): + if template_file: + tem_img = cv2.imread(template_file) # template image + + (x1, y1, x2, y2) = loc + + img_recovered = np.zeros((image_o_shape[0], image_o_shape[1], 3)) + + img_recovered[y1:y2, x1:x2, :] = cv2.resize(tem_img, dsize=(x2 - x1, y2 - y1)) + + if output_file_name: + cv2.imwrite(output_file_name, img_recovered) + return img_recovered diff --git a/blind_watermark/requirements.txt b/blind_watermark/requirements.txt new file mode 100644 index 0000000..2f55497 --- /dev/null +++ b/blind_watermark/requirements.txt @@ -0,0 +1 @@ +blind-watermark \ No newline at end of file diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..d3180d2 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,22 @@ + +# [blind_watermark](https://github.com/guofei9987/blind_watermark) + +[![PyPI](https://img.shields.io/pypi/v/blind_watermark)](https://pypi.org/project/blind_watermark/) +[![release](https://img.shields.io/github/v/release/guofei9987/blind_watermark)](https://github.com/guofei9987/blind_watermark) +[![Build Status](https://travis-ci.com/guofei9987/blind_watermark.svg?branch=master)](https://travis-ci.com/guofei9987/blind_watermark) +[![codecov](https://codecov.io/gh/guofei9987/blind_watermark/branch/master/graph/badge.svg)](https://codecov.io/gh/guofei9987/blind_watermark) +[![PyPI_downloads](https://img.shields.io/pypi/dm/blind_watermark)](https://pypi.org/project/blind_watermark/) +[![Stars](https://img.shields.io/github/stars/guofei9987/blind_watermark?style=social)](https://github.com/guofei9987/blind_watermark/stargazers) +[![Forks](https://img.shields.io/github/forks/guofei9987/blind_watermark.svg?style=social)](https://github.com/guofei9987/blind_watermark/network/members) +[![Join the chat at https://gitter.im/guofei9987/blind_watermark](https://badges.gitter.im/guofei9987/blind_watermark.svg)](https://gitter.im/guofei9987/blind_watermark?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + + + +Heuristic Algorithms in Python +(Genetic Algorithm, Particle Swarm Optimization, Simulated Annealing, Ant Colony Algorithm, Immune Algorithm,Artificial Fish Swarm Algorithm in Python) + + +- **Documentation:** [https://blind_watermark.github.io/blind_watermark/#/en/](https://blind_watermark.github.io/blind_watermark/#/en/) +- **文档:** [https://blind_watermark.github.io/blind_watermark/#/zh/](https://blind_watermark.github.io/blind_watermark/#/zh/) +- **Source code:** [https://github.com/guofei9987/blind_watermark](https://github.com/guofei9987/blind_watermark) + diff --git a/docs/_coverpage.md b/docs/_coverpage.md new file mode 100644 index 0000000..d6cb70f --- /dev/null +++ b/docs/_coverpage.md @@ -0,0 +1,11 @@ + + +# blind_watermark + +> 数字盲水印 + +* 图片水印 +* 比特水印 + +[GitHub](https://github.com/guofei9987/blind_watermark/) +[Get Started](/en/README) diff --git a/docs/_navbar.md b/docs/_navbar.md new file mode 100644 index 0000000..e8e1c66 --- /dev/null +++ b/docs/_navbar.md @@ -0,0 +1,3 @@ +- Translations + - [:uk: English](/en/) + - [:cn: 中文](/zh/) diff --git a/docs/_sidebar.md b/docs/_sidebar.md new file mode 100644 index 0000000..0467701 --- /dev/null +++ b/docs/_sidebar.md @@ -0,0 +1,3 @@ + +* [English Document](docs/en.md) +* [中文文档](docs/zh.md) diff --git a/docs/en/README.md b/docs/en/README.md new file mode 100644 index 0000000..6f29b8d --- /dev/null +++ b/docs/en/README.md @@ -0,0 +1,175 @@ + +# blind-watermark + +Blind watermark based on dct and svd. + + +[![PyPI](https://img.shields.io/pypi/v/blind_watermark)](https://pypi.org/project/blind_watermark/) +[![Build Status](https://travis-ci.com/guofei9987/blind_watermark.svg?branch=master)](https://travis-ci.com/guofei9987/blind_watermark) +[![codecov](https://codecov.io/gh/guofei9987/blind_watermark/branch/master/graph/badge.svg)](https://codecov.io/gh/guofei9987/blind_watermark) +[![License](https://img.shields.io/pypi/l/blind_watermark.svg)](https://github.com/guofei9987/blind_watermark/blob/master/LICENSE) +![Python](https://img.shields.io/badge/python->=3.5-green.svg) +![Platform](https://img.shields.io/badge/platform-windows%20|%20linux%20|%20macos-green.svg) +[![stars](https://img.shields.io/github/stars/guofei9987/blind_watermark.svg?style=social)](https://github.com/guofei9987/blind_watermark/) +[![fork](https://img.shields.io/github/forks/guofei9987/blind_watermark?style=social)](https://github.com/guofei9987/blind_watermark/fork) +[![Downloads](https://pepy.tech/badge/blind-watermark)](https://pepy.tech/project/blind-watermark) +[![Discussions](https://img.shields.io/badge/discussions-green.svg)](https://github.com/guofei9987/blind_watermark/discussions) + + +- **Documentation:** [https://BlindWatermark.github.io/blind_watermark/#/en/](https://BlindWatermark.github.io/blind_watermark/#/en/) +- **文档:** [https://BlindWatermark.github.io/blind_watermark/#/zh/](https://BlindWatermark.github.io/blind_watermark/#/zh/) +- **中文 readme** [README_cn.md](README_cn.md) +- **Source code:** [https://github.com/guofei9987/blind_watermark](https://github.com/guofei9987/blind_watermark) + + + +# install +```bash +pip install blind-watermark +``` + +For the current developer version: +```bach +git clone git@github.com:guofei9987/blind_watermark.git +cd blind_watermark +pip install . +``` + +# How to use + + +### Use in bash + + +```bash +# 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 +``` + + + +## Use in Python + +Original Image + Watermark = Watermarked Image + +![origin_image](https://blindwatermark.github.io/blind_watermark/原图.jpeg) + '@guofei9987 开源万岁!' = ![打上水印的图](https://blindwatermark.github.io/blind_watermark/打上水印的图.jpg) + + +See the [codes](/examples/example_str.py) + +Embed watermark: +```python +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)) +``` + +Extract watermark: +```python +bwm1 = WaterMark(password_img=1, password_wm=1) +wm_extract = bwm1.extract('output/embedded.png', wm_shape=len_wm, mode='str') +print(wm_extract) +``` +Output: +>@guofei9987 开源万岁! + +### attacks on Watermarked Image + + +|attack method|image after attack|extracted watermark| +|--|--|--| +|Rotate 45 Degrees|![旋转攻击](https://blindwatermark.github.io/blind_watermark/旋转攻击.jpg)|'@guofei9987 开源万岁!'| +|Random crop|![截屏攻击](https://blindwatermark.github.io/blind_watermark/截屏攻击2_还原.jpg)|'@guofei9987 开源万岁!'| +|Masks| ![多遮挡攻击](https://blindwatermark.github.io/blind_watermark/多遮挡攻击.jpg) |'@guofei9987 开源万岁!'| +|50% Horizontal cut|![横向裁剪攻击](https://blindwatermark.github.io/blind_watermark/横向裁剪攻击_填补.jpg)|'@guofei9987 开源万岁!'| +|50% Vertical cut|![纵向裁剪攻击](https://blindwatermark.github.io/blind_watermark/纵向裁剪攻击_填补.jpg)|'@guofei9987 开源万岁!'| +|Resize 0.5|![缩放攻击](https://blindwatermark.github.io/blind_watermark/缩放攻击.jpg)|'@guofei9987 开源万岁!'| +|Pepper Noise|![椒盐攻击](https://blindwatermark.github.io/blind_watermark/椒盐攻击.jpg)|'@guofei9987 开源万岁!'| +|Brightness 10% Down|![亮度攻击](https://blindwatermark.github.io/blind_watermark/亮度攻击.jpg)|'@guofei9987 开源万岁!'| + + + + + + +### embed images + +embed watermark: +```python +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') +``` + + +Extract watermark: +```python +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', ) +``` + + +|attack method|image after attack|extracted watermark| +|--|--|--| +|Rotate 45 Degrees|![旋转攻击](https://blindwatermark.github.io/blind_watermark/旋转攻击.jpg)|![](https://blindwatermark.github.io/blind_watermark/旋转攻击_提取水印.png)| +|Random crop|![截屏攻击](https://blindwatermark.github.io/blind_watermark/截屏攻击2_还原.jpg)|![多遮挡_提取水印](https://blindwatermark.github.io/blind_watermark/多遮挡攻击_提取水印.png)| +|Mask| ![多遮挡攻击](https://blindwatermark.github.io/blind_watermark/多遮挡攻击.jpg) |![多遮挡_提取水印](https://blindwatermark.github.io/blind_watermark/多遮挡攻击_提取水印.png)| + + +### embed array of bits + +See it [here](/examples/example_bit.py) + + +As demo, we embed 6 bytes data: +```python +wm = [True, False, True, True, True, False] +``` + +Embed: +```python +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') +``` + +Extract: +```python +bwm1 = WaterMark(password_img=1, password_wm=1, wm_shape=6) +wm_extract = bwm1.extract('output/打上水印的图.png', mode='bit') +print(wm_extract) +``` +Notice that `wm_shape` (shape of watermark) is necessary + +The output `wm_extract` is an array of float. set a threshold such as 0.5. + + +# Concurrency + +```python +WaterMark(..., processes=None) +``` +- `processes`: number of processes, can be integer. Default `None`, meaning use all processes. + +## Related Project + +text_blind_watermark: [https://github.com/guofei9987/text_blind_watermark](https://github.com/guofei9987/text_blind_watermark) +Embed message into text. diff --git a/docs/en/_coverpage.md b/docs/en/_coverpage.md new file mode 100644 index 0000000..15c33a1 --- /dev/null +++ b/docs/en/_coverpage.md @@ -0,0 +1,22 @@ + + +# blind_watermark + +> Blind Watermark + +* [![PyPI](https://img.shields.io/pypi/v/blind_watermark)](https://pypi.org/project/blind_watermark/) +[![Build Status](https://travis-ci.com/guofei9987/blind_watermark.svg?branch=master)](https://travis-ci.com/guofei9987/blind_watermark) +[![codecov](https://codecov.io/gh/guofei9987/blind_watermark/branch/master/graph/badge.svg)](https://codecov.io/gh/guofei9987/blind_watermark) +[![License](https://img.shields.io/pypi/l/blind_watermark.svg)](https://github.com/guofei9987/blind_watermark/blob/master/LICENSE) +![Python](https://img.shields.io/badge/python->=3.5-green.svg) +![Platform](https://img.shields.io/badge/platform-windows%20|%20linux%20|%20macos-green.svg) +[![stars](https://img.shields.io/github/stars/guofei9987/blind_watermark.svg?style=social)](https://github.com/guofei9987/blind_watermark/) +[![fork](https://img.shields.io/github/forks/guofei9987/blind_watermark?style=social)](https://github.com/guofei9987/blind_watermark/fork) +[![Downloads](https://pepy.tech/badge/blind-watermark)](https://pepy.tech/project/blind-watermark) +[![Discussions](https://img.shields.io/badge/discussions-green.svg)](https://github.com/guofei9987/blind_watermark/discussions) +* embed a picture +* embed a string +* embed byte-file + +[GitHub](https://github.com/guofei9987/blind_watermark/) +[Get Started](/en/README) diff --git a/docs/en/_sidebar.md b/docs/en/_sidebar.md new file mode 100644 index 0000000..98d56f2 --- /dev/null +++ b/docs/en/_sidebar.md @@ -0,0 +1,2 @@ +* [Document](en/README.md) + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..e881898 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,61 @@ + + + + + blind_watermark + + + + + + + +
+ + + + + + + + + + + + + + + diff --git a/docs/make_doc.py b/docs/make_doc.py new file mode 100644 index 0000000..f669e1b --- /dev/null +++ b/docs/make_doc.py @@ -0,0 +1,104 @@ +# 不想用 Sphinx,也不像弄一堆静态html文件,所以自己写个咯 + + +''' +需要从readme中解析出: +1. "-> Demo code: [examples/demo_pso.py](examples/demo_pso.py)" +2. 三个```python为开头,三个 ``` 为结尾 +3. 从py文件中读出文本,并替换 +4. 前几行是求star,只在readme中出现 + + +需要从py文件中解析出: +1. # %% 做断点后赋予index值,然后插入readme +''' +import os +import sys + +import re + + +def search_code(py_file_name, section_idx): + ''' + 给定py文件名和section序号,返回一个list,内容是py文件中的code(markdown格式) + :param py_file_name: + :param section_idx: + :return: + ''' + with open('../' + py_file_name, encoding='utf-8', mode="r") as f: + content = f.readlines() + content_new, i, search_idx, idx_first_match = [], 0, 0, None + while i < len(content) and search_idx <= section_idx: + if content[i].startswith('# %%'): + search_idx += 1 + i += 1 # 带井号百分号的那一行也跳过去,不要放到文档里面 + if search_idx < section_idx: + pass + elif search_idx == section_idx: + idx_first_match = idx_first_match or i # record first match line + content_new.append(content[i]) + i += 1 + return [ + '-> Demo code: [{py_file_name}#s{section_idx}](https://github.com/guofei9987/blind_watermark/blob/master/{py_file_name}#L{idx_first_match})\n'. + format(py_file_name=py_file_name, section_idx=section_idx + 1, idx_first_match=idx_first_match), + '```python\n'] \ + + content_new \ + + ['```\n'] + + +# %% + + +def make_doc(origin_file): + with open(origin_file, encoding='utf-8', mode="r") as f_readme: + readme = f_readme.readlines() + + regex = re.compile('\[examples/[\w#.]+\]') + readme_idx = 0 + readme_new = [] + while readme_idx < len(readme): + readme_line = readme[readme_idx] + if readme_line.startswith('-> Demo code: ['): + # 找到中括号里面的内容,解析为文件名,section号 + py_file_name, section_idx = regex.findall(readme[readme_idx])[0][1:-1].split('#s') + section_idx = int(section_idx) - 1 + + print('插入代码: ', py_file_name, section_idx) + content_new = search_code(py_file_name, section_idx) + readme_new.extend(content_new) + + # 往下寻找第一个代码结束位置 + while readme[readme_idx] != '```\n': + readme_idx += 1 + else: + # 如果不需要插入代码,就用原本的内容 + readme_new.append(readme_line) + + readme_idx += 1 + return readme_new + + +# 主页 README 和 en/README +readme_new = make_doc(origin_file='../README.md') +with open('../README.md', encoding='utf-8', mode="w") as f_readme: + f_readme.writelines(readme_new) + +with open('en/README.md', encoding='utf-8', mode="w") as f_readme_en: + f_readme_en.writelines(readme_new[20:]) + +# 跟目录的 README_cn.md 和 zh/README.md +readme_zh = make_doc(origin_file='../README_cn.md') +with open('../README_cn.md', encoding='utf-8', mode="w") as f_readme: + f_readme.writelines(readme_zh) + +with open('zh/README.md', encoding='utf-8', mode="w") as f_readme_en: + f_readme_en.writelines(readme_zh) + +# docs = ['zh/README.md','en/README.md' +# ] +# for i in docs: +# docs_new = make_doc(origin_file=i) +# with open(i, encoding='utf-8', mode="w") as f: +# f.writelines(docs_new) + +# sys.exit() diff --git a/docs/run_server.bat b/docs/run_server.bat new file mode 100644 index 0000000..7175c07 --- /dev/null +++ b/docs/run_server.bat @@ -0,0 +1 @@ +docsify serve \ No newline at end of file diff --git a/docs/vue.css b/docs/vue.css new file mode 100644 index 0000000..c7a3140 --- /dev/null +++ b/docs/vue.css @@ -0,0 +1,939 @@ +@import url("https://fonts.googleapis.com/css?family=Roboto+Mono|Source+Sans+Pro:300,400,600"); + +* { + -webkit-font-smoothing: antialiased; + -webkit-overflow-scrolling: touch; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -webkit-text-size-adjust: none; + -webkit-touch-callout: none; + box-sizing: border-box +} + +body:not(.ready) { + overflow: hidden +} + +body:not(.ready) .app-nav, body:not(.ready) > nav, body:not(.ready) [data-cloak] { + display: none +} + +div#app { + font-size: 30px; + font-weight: lighter; + margin: 40vh auto; + text-align: center +} + +div#app:empty:before { + content: "Loading..." +} + +.emoji { + height: 1.2rem; + vertical-align: middle +} + +.progress { + background-color: var(--theme-color, #42b983); + height: 2px; + left: 0; + position: fixed; + right: 0; + top: 0; + transition: width .2s, opacity .4s; + width: 0; + z-index: 5 +} + +.search .search-keyword, .search a:hover { + color: var(--theme-color, #42b983) +} + +.search .search-keyword { + font-style: normal; + font-weight: 700 +} + +body, html { + height: 100% +} + +body { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + color: #34495e; + font-family: Source Sans Pro, Helvetica Neue, Arial, sans-serif; + font-size: 15px; + letter-spacing: 0; + margin: 0; + overflow-x: hidden +} + +img { + max-width: 100% +} + +a[disabled] { + cursor: not-allowed; + opacity: .6 +} + +kbd { + border: 1px solid #ccc; + border-radius: 3px; + display: inline-block; + font-size: 12px !important; + line-height: 12px; + margin-bottom: 3px; + padding: 3px 5px; + vertical-align: middle +} + +li input[type=checkbox] { + margin: 0 .2em .25em 0; + vertical-align: middle +} + +.app-nav { + margin: 25px 60px 0 0; + position: absolute; + right: 0; + text-align: right; + z-index: 2 +} + +.app-nav.no-badge { + margin-right: 0px +} + +.app-nav.no-badge ul{ + padding: 0 0 0 0 +} + +.app-nav p { + margin: 0 +} + +.app-nav > a { + margin: 0 1rem; + padding: 5px 0 +} + +.app-nav li, .app-nav ul { + display: inline-block; + list-style: none; + margin: 0 +} + +.app-nav a { + color: inherit; + font-size: 16px; + text-decoration: none; + transition: color .3s +} + +.app-nav a.active, .app-nav a:hover { + color: var(--theme-color, #42b983) +} + +.app-nav a.active { + border-bottom: 2px solid var(--theme-color, #42b983) +} + +.app-nav li { + display: inline-block; + margin: 0 1rem; + padding: 5px 0; + position: relative +} + +.app-nav li ul { + background-color: #fff; + border: 1px solid #ddd; + border-bottom-color: #ccc; + border-radius: 4px; + box-sizing: border-box; + display: none; + max-height: calc(100vh - 61px); + overflow-y: auto; + padding: 10px 0; + position: absolute; + right: -15px; + text-align: left; + top: 100%; + white-space: nowrap +} + +.app-nav li ul li { + display: block; + font-size: 14px; + line-height: 1rem; + margin: 0; + margin: 8px 14px; + white-space: nowrap +} + +.app-nav li ul a { + display: block; + font-size: inherit; + margin: 0; + padding: 0 +} + +.app-nav li ul a.active { + border-bottom: 0 +} + +.app-nav li:hover ul { + display: block +} + +.github-corner { + border-bottom: 0; + position: fixed; + right: 0; + text-decoration: none; + top: 0; + z-index: 1 +} + +.github-corner:hover .octo-arm { + animation: a .56s ease-in-out +} + +.github-corner svg { + color: #fff; + fill: var(--theme-color, #42b983); + height: 80px; + width: 80px +} + +main { + display: block; + position: relative; + width: 100vw; + height: 100%; + z-index: 0 +} + +main.hidden { + display: none +} + +.anchor { + display: inline-block; + text-decoration: none; + transition: all .3s +} + +.anchor span { + color: #34495e +} + +.anchor:hover { + text-decoration: underline +} + +.sidebar { + border-right: 1px solid rgba(0, 0, 0, .07); + overflow-y: auto; + padding: 40px 0 0; + position: absolute; + top: 0; + bottom: 0; + left: 0; + transition: transform .25s ease-out; + width: 300px; + z-index: 3 +} + +.sidebar > h1 { + margin: 0 auto 1rem; + font-size: 1.5rem; + font-weight: 300; + text-align: center +} + +.sidebar > h1 a { + color: inherit; + text-decoration: none +} + +.sidebar > h1 .app-nav { + display: block; + position: static +} + +.sidebar .sidebar-nav { + line-height: 1em; + padding-bottom: 40px +} + +.sidebar li.collapse .app-sub-sidebar { + display: none +} + +.sidebar ul { + margin: 0 0 0 15px; + padding: 0 +} + +.sidebar li > p { + font-weight: 700; + margin: 0 +} + +.sidebar ul, .sidebar ul li { + list-style: none +} + +.sidebar ul li a { + border-bottom: none; + display: block +} + +.sidebar ul li ul { + padding-left: 20px +} + +.sidebar::-webkit-scrollbar { + width: 4px +} + +.sidebar::-webkit-scrollbar-thumb { + background: transparent; + border-radius: 4px +} + +.sidebar:hover::-webkit-scrollbar-thumb { + background: hsla(0, 0%, 53%, .4) +} + +.sidebar:hover::-webkit-scrollbar-track { + background: hsla(0, 0%, 53%, .1) +} + +.sidebar-toggle { + background-color: transparent; + background-color: hsla(0, 0%, 100%, .8); + border: 0; + outline: none; + padding: 10px; + position: absolute; + bottom: 0; + left: 0; + text-align: center; + transition: opacity .3s; + width: 284px; + z-index: 4 +} + +.sidebar-toggle .sidebar-toggle-button:hover { + opacity: .4 +} + +.sidebar-toggle span { + background-color: var(--theme-color, #42b983); + display: block; + margin-bottom: 4px; + width: 16px; + height: 2px +} + +body.sticky .sidebar, body.sticky .sidebar-toggle { + position: fixed +} + +.content { + padding-top: 60px; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 300px; + transition: left .25s ease +} + +.markdown-section { + /*margin: 0 auto;*/ + margin: 1em auto; + max-width: 1300px; + padding: 30px 0 40px 0; + position: relative +} + +.markdown-section > * { + box-sizing: border-box; + font-size: inherit +} + +.markdown-section > :first-child { + margin-top: 0 !important +} + +.markdown-section hr { + border: none; + border-bottom: 1px solid #eee; + margin: 2em 0 +} + +.markdown-section iframe { + border: 1px solid #eee +} + +.markdown-section table { + border-collapse: collapse; + border-spacing: 0; + display: block; + margin-bottom: 1rem; + overflow: auto; + width: 100% +} + +.markdown-section th { + font-weight: 700 +} + +.markdown-section td, .markdown-section th { + border: 1px solid #ddd; + padding: 6px 13px +} + +.markdown-section tr { + border-top: 1px solid #ccc +} + +.markdown-section p.tip, .markdown-section tr:nth-child(2n) { + background-color: #f8f8f8 +} + +.markdown-section p.tip { + border-bottom-right-radius: 2px; + border-left: 4px solid #f66; + border-top-right-radius: 2px; + margin: 2em 0; + padding: 12px 24px 12px 30px; + position: relative +} + +.markdown-section p.tip:before { + background-color: #f66; + border-radius: 100%; + color: #fff; + content: "!"; + font-family: Dosis, Source Sans Pro, Helvetica Neue, Arial, sans-serif; + font-size: 14px; + font-weight: 700; + left: -12px; + line-height: 20px; + position: absolute; + height: 20px; + width: 20px; + text-align: center; + top: 14px +} + +.markdown-section p.tip code { + background-color: #efefef +} + +.markdown-section p.tip em { + color: #34495e +} + +.markdown-section p.warn { + background: rgba(66, 185, 131, .1); + border-radius: 2px; + padding: 1rem +} + +.markdown-section ul.task-list > li { + list-style-type: none +} + +body.close .sidebar { + transform: translateX(-300px) +} + +body.close .sidebar-toggle { + width: auto +} + +body.close .content { + left: 0 +} + +@media print { + .app-nav, .github-corner, .sidebar, .sidebar-toggle { + display: none + } +} + +@media screen and (max-width: 768px) { + .github-corner, .sidebar, .sidebar-toggle { + position: fixed + } + + .app-nav { + margin-top: 16px + } + + .app-nav li ul { + top: 30px + } + + main { + height: auto; + overflow-x: hidden + } + + .sidebar { + left: -300px; + transition: transform .25s ease-out + } + + .content { + left: 0; + max-width: 100vw; + position: static; + padding-top: 20px; + transition: transform .25s ease + } + + .app-nav, .github-corner { + transition: transform .25s ease-out + } + + .sidebar-toggle { + background-color: transparent; + width: auto; + padding: 30px 30px 10px 10px + } + + body.close .sidebar { + transform: translateX(300px) + } + + body.close .sidebar-toggle { + background-color: hsla(0, 0%, 100%, .8); + transition: background-color 1s; + width: 284px; + padding: 10px + } + + body.close .content { + transform: translateX(300px) + } + + body.close .app-nav, body.close .github-corner { + display: none + } + + .github-corner:hover .octo-arm { + animation: none + } + + .github-corner .octo-arm { + animation: a .56s ease-in-out + } +} + +@keyframes a { + 0%, to { + transform: rotate(0) + } + 20%, 60% { + transform: rotate(-25deg) + } + 40%, 80% { + transform: rotate(10deg) + } +} + +section.cover { + -ms-flex-align: center; + align-items: center; + background-position: 50%; + background-repeat: no-repeat; + background-size: cover; + height: 100vh; + display: none +} + +section.cover.show { + display: -ms-flexbox; + display: flex +} + +section.cover.has-mask .mask { + background-color: #fff; + opacity: .8; + position: absolute; + top: 0; + height: 100%; + width: 100% +} + +section.cover .cover-main { + -ms-flex: 1; + flex: 1; + margin: -20px 16px 0; + text-align: center; + z-index: 1 +} + +section.cover a { + color: inherit +} + +section.cover a, section.cover a:hover { + text-decoration: none +} + +section.cover p { + line-height: 1.5rem; + margin: 1em 0 +} + +section.cover h1 { + color: inherit; + font-size: 2.5rem; + font-weight: 300; + margin: .625rem 0 2.5rem; + position: relative; + text-align: center +} + +section.cover h1 a { + display: block +} + +section.cover h1 small { + bottom: -.4375rem; + font-size: 1rem; + position: absolute +} + +section.cover blockquote { + font-size: 1.5rem; + text-align: center +} + +section.cover ul { + line-height: 1.8; + list-style-type: none; + margin: 1em auto; + max-width: 500px; + padding: 0 +} + +section.cover .cover-main > p:last-child a { + border: 1px solid var(--theme-color, #42b983); + border-radius: 2rem; + box-sizing: border-box; + color: var(--theme-color, #42b983); + display: inline-block; + font-size: 1.05rem; + letter-spacing: .1rem; + margin: .5rem 1rem; + padding: .75em 2rem; + text-decoration: none; + transition: all .15s ease +} + +section.cover .cover-main > p:last-child a:last-child { + background-color: var(--theme-color, #42b983); + color: #fff +} + +section.cover .cover-main > p:last-child a:last-child:hover { + color: inherit; + opacity: .8 +} + +section.cover .cover-main > p:last-child a:hover { + color: inherit +} + +section.cover blockquote > p > a { + border-bottom: 2px solid var(--theme-color, #42b983); + transition: color .3s +} + +section.cover blockquote > p > a:hover { + color: var(--theme-color, #42b983) +} + +.sidebar, body { + background-color: #fff +} + +.sidebar { + color: #364149 +} + +.sidebar li { + margin: 6px 0 +} + +.sidebar ul li a { + color: #505d6b; + font-size: 14px; + font-weight: 400; + overflow: hidden; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap +} + +.sidebar ul li a:hover { + text-decoration: underline +} + +.sidebar ul li ul { + padding: 0 +} + +.sidebar ul li.active > a { + border-right: 2px solid; + color: var(--theme-color, #42b983); + font-weight: 600 +} + +.app-sub-sidebar li:before { + content: ""; + padding-right: 4px; + float: left +} + +.markdown-section h1, .markdown-section h2, .markdown-section h3, .markdown-section h4 { + color: #2c3e50; + font-weight: 600 +} + +.markdown-section strong { + color: red; + font-weight: 600 +} + +.markdown-section a { + color: var(--theme-color, #42b983); + font-weight: 600 +} + + +.markdown-section h1 { + font-size: 2.25rem; + margin: 0 0 1rem +} + +.markdown-section h2 { + background: #c0cda5; + font-size: 2.25rem; + margin: 45px 0 .8rem +} + +.markdown-section h3,h4,h5 { + background: #e5f7f3; +} + +.markdown-section h3 { + font-size: 1.5rem; + margin: 40px 0 .6rem +} + +.markdown-section h4 { + font-size: 1rem +} + +.markdown-section h5 { + font-size: 0.875rem +} + +.markdown-section h6 { + color: #777; + font-size: 0.75rem +} + +.markdown-section figure, .markdown-section p { + margin: 1.5em 0 0 0 + /*上右下左*/ +} + +.markdown-section ol, .markdown-section p, .markdown-section ul { + line-height: 1.6rem; + word-spacing: .05rem +} + +.markdown-section ol, .markdown-section ul { + margin-top: 0px; + padding-left: 1.5rem +} + +.markdown-section blockquote { + border-left: 8px solid var(--theme-color, #42b983); + color: #858585; + margin: 0 0; + padding-left: 20px +} + +.markdown-section blockquote p { + font-weight: 600; + margin-left: 0; + margin-top: 0 +} + +.markdown-section iframe { + margin: 1em 0 +} + +.markdown-section em { + color: #7f8c8d +} + +.markdown-section code { + border-radius: 2px; + color: #e96900; + font-size: .8rem; + margin: 0 2px; + padding: 3px 5px; + white-space: pre-wrap +} + +.markdown-section code, .markdown-section pre { + background-color: #f8f8f8; + font-family: Roboto Mono, Monaco, courier, monospace +} + +.markdown-section pre { + -moz-osx-font-smoothing: initial; + -webkit-font-smoothing: initial; + line-height: 1.5rem; + margin: 1.2em 0; + overflow: auto; + padding: 0 1.4rem; + position: relative; + word-wrap: normal +} + +.token.cdata, .token.comment, .token.doctype, .token.prolog { + color: #8e908c +} + +.token.namespace { + opacity: .7 +} + +.token.boolean, .token.number { + color: #c76b29 +} + +.token.punctuation { + color: #525252 +} + +.token.property { + color: #c08b30 +} + +.token.tag { + color: #2973b7 +} + +.token.string { + color: var(--theme-color, #42b983) +} + +.token.selector { + color: #6679cc +} + +.token.attr-name { + color: #2973b7 +} + +.language-css .token.string, .style .token.string, .token.entity, .token.url { + color: #22a2c9 +} + +.token.attr-value, .token.control, .token.directive, .token.unit { + color: var(--theme-color, #42b983) +} + +.token.function, .token.keyword { + color: #e96900 +} + +.token.atrule, .token.regex, .token.statement { + color: #22a2c9 +} + +.token.placeholder, .token.variable { + color: #3d8fd1 +} + +.token.deleted { + text-decoration: line-through +} + +.token.inserted { + border-bottom: 1px dotted #202746; + text-decoration: none +} + +.token.italic { + font-style: italic +} + +.token.bold, .token.important { + font-weight: 700 +} + +.token.important { + color: #c94922 +} + +.token.entity { + cursor: help +} + +.markdown-section pre > code { + -moz-osx-font-smoothing: initial; + -webkit-font-smoothing: initial; + background-color: #f8f8f8; + border-radius: 2px; + color: #525252; + display: block; + font-family: Roboto Mono, Monaco, courier, monospace; + font-size: .8rem; + line-height: inherit; + margin: 0 2px; + max-width: inherit; + overflow: inherit; + padding: 2.2em 5px; + white-space: inherit +} + +.markdown-section code:after, .markdown-section code:before { + letter-spacing: .05rem +} + +code .token { + -moz-osx-font-smoothing: initial; + -webkit-font-smoothing: initial; + min-height: 1.5rem +} + +pre:after { + color: #ccc; + content: attr(data-lang); + font-size: .6rem; + font-weight: 600; + height: 15px; + line-height: 15px; + padding: 5px 10px 0; + position: absolute; + right: 0; + text-align: right; + top: 0 +} diff --git a/docs/zh/README.md b/docs/zh/README.md new file mode 100644 index 0000000..cfea9af --- /dev/null +++ b/docs/zh/README.md @@ -0,0 +1,173 @@ +# blind-watermark + +基于频域的数字盲水印 + + +[![PyPI](https://img.shields.io/pypi/v/blind_watermark)](https://pypi.org/project/blind_watermark/) +[![Build Status](https://travis-ci.com/guofei9987/blind_watermark.svg?branch=master)](https://travis-ci.com/guofei9987/blind_watermark) +[![codecov](https://codecov.io/gh/guofei9987/blind_watermark/branch/master/graph/badge.svg)](https://codecov.io/gh/guofei9987/blind_watermark) +[![License](https://img.shields.io/pypi/l/blind_watermark.svg)](https://github.com/guofei9987/blind_watermark/blob/master/LICENSE) +![Python](https://img.shields.io/badge/python->=3.5-green.svg) +![Platform](https://img.shields.io/badge/platform-windows%20|%20linux%20|%20macos-green.svg) +[![stars](https://img.shields.io/github/stars/guofei9987/blind_watermark.svg?style=social)](https://github.com/guofei9987/blind_watermark/) +[![fork](https://img.shields.io/github/forks/guofei9987/blind_watermark?style=social)](https://github.com/guofei9987/blind_watermark/fork) +[![Downloads](https://pepy.tech/badge/blind-watermark)](https://pepy.tech/project/blind-watermark) +[![Discussions](https://img.shields.io/badge/discussions-green.svg)](https://github.com/guofei9987/blind_watermark/discussions) + + +- **Documentation:** [https://BlindWatermark.github.io/blind_watermark/#/en/](https://BlindWatermark.github.io/blind_watermark/#/en/) +- **文档:** [https://BlindWatermark.github.io/blind_watermark/#/zh/](https://BlindWatermark.github.io/blind_watermark/#/zh/) +- **English readme** [README.md](README.md) +- **Source code:** [https://github.com/guofei9987/blind_watermark](https://github.com/guofei9987/blind_watermark) + +# 安装 +```bash +pip install blind-watermark +``` + +或者安装最新开发版本 +```bach +git clone git@github.com:guofei9987/blind_watermark.git +cd blind_watermark +pip install . +``` + +# 如何使用 + +### 命令行中使用 + +```bash +# 嵌入水印: +blind_watermark --embed --pwd 1234 examples/pic/ori_img.jpeg "watermark text" examples/output/embedded.png +# 提取水印: +blind_watermark --extract --pwd 1234 --wm_shape 111 examples/output/embedded.png +``` + + + +## Python 中使用 + +原图 + 水印 = 打上水印的图 + +![origin_image](https://blindwatermark.github.io/blind_watermark/原图.jpeg) + '@guofei9987 开源万岁!' = ![打上水印的图](https://blindwatermark.github.io/blind_watermark/打上水印的图.jpg) + + + +参考 [代码](/examples/example_str.py) + + +嵌入水印 +```python +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)) +``` + + +提取水印 +```python +bwm1 = WaterMark(password_img=1, password_wm=1) +wm_extract = bwm1.extract('output/embedded.png', wm_shape=len_wm, mode='str') +print(wm_extract) +``` +Output: +>@guofei9987 开源万岁! + + +### 各种攻击后的效果 + +|攻击方式|攻击后的图片|提取的水印| +|--|--|--| +|旋转攻击45度|![旋转攻击](https://blindwatermark.github.io/blind_watermark/旋转攻击.jpg)|'@guofei9987 开源万岁!'| +|随机截图|![截屏攻击](https://blindwatermark.github.io/blind_watermark/截屏攻击2_还原.jpg)|'@guofei9987 开源万岁!'| +|多遮挡| ![多遮挡攻击](https://blindwatermark.github.io/blind_watermark/多遮挡攻击.jpg) |'@guofei9987 开源万岁!'| +|横向裁剪50%|![横向裁剪攻击](https://blindwatermark.github.io/blind_watermark/横向裁剪攻击_填补.jpg)|'@guofei9987 开源万岁!'| +|纵向裁剪50%|![纵向裁剪攻击](https://blindwatermark.github.io/blind_watermark/纵向裁剪攻击_填补.jpg)|'@guofei9987 开源万岁!'| +|缩放攻击|![缩放攻击](https://blindwatermark.github.io/blind_watermark/缩放攻击.jpg)|'@guofei9987 开源万岁!'| +|椒盐攻击|![椒盐攻击](https://blindwatermark.github.io/blind_watermark/椒盐攻击.jpg)|'@guofei9987 开源万岁!'| +|亮度攻击|![亮度攻击](https://blindwatermark.github.io/blind_watermark/亮度攻击.jpg)|'@guofei9987 开源万岁!'| + + + +### 嵌入图片 + +参考 [代码](/examples/example_str.py) + + +嵌入: +```python +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') +``` + +提取: +```python +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', ) +``` + +|攻击方式|攻击后的图片|提取的水印| +|--|--|--| +|旋转攻击45度|![旋转攻击](https://blindwatermark.github.io/blind_watermark/旋转攻击.jpg)|![](https://blindwatermark.github.io/blind_watermark/旋转攻击_提取水印.png)| +|随机截图|![截屏攻击](https://blindwatermark.github.io/blind_watermark/截屏攻击2_还原.jpg)|![](https://blindwatermark.github.io/blind_watermark/旋转攻击_提取水印.png)| +|多遮挡| ![多遮挡攻击](https://blindwatermark.github.io/blind_watermark/多遮挡攻击.jpg) |![多遮挡_提取水印](https://blindwatermark.github.io/blind_watermark/多遮挡攻击_提取水印.png)| + + + +### 隐水印还可以是二进制数据 + +参考 [代码](/examples/example_bit.py) + + +作为 demo, 如果要嵌入是如下长度为6的二进制数据 +```python +wm = [True, False, True, True, True, False] +``` + +嵌入水印 + +```python +# 除了嵌入图片,也可以嵌入比特类数据 +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/打上水印的图.png') +``` + +解水印:(注意设定水印形状 `wm_shape`) +```python +bwm1 = WaterMark(password_img=1, password_wm=1, wm_shape=6) +wm_extract = bwm1.extract('output/打上水印的图.png', mode='bit') +print(wm_extract) +``` + +解出的水印是一个0~1之间的实数,方便用户自行卡阈值。如果水印信息量远小于图片可容纳量,偏差极小。 + +# 并行计算 + +```python +WaterMark(..., processes=None) +``` +- `processes`: 整数,指定线程数。默认为 `None`, 表示使用全部线程。 + + +## 相关项目 + +text_blind_watermark: [https://github.com/guofei9987/text_blind_watermark](https://github.com/guofei9987/text_blind_watermark) +文本盲水印,把信息隐秘地打入文本. diff --git a/docs/zh/_coverpage.md b/docs/zh/_coverpage.md new file mode 100644 index 0000000..af20a27 --- /dev/null +++ b/docs/zh/_coverpage.md @@ -0,0 +1,22 @@ + + +# blind_watermark + +> 数字盲水印 + +* [![PyPI](https://img.shields.io/pypi/v/blind_watermark)](https://pypi.org/project/blind_watermark/) +[![Build Status](https://travis-ci.com/guofei9987/blind_watermark.svg?branch=master)](https://travis-ci.com/guofei9987/blind_watermark) +[![codecov](https://codecov.io/gh/guofei9987/blind_watermark/branch/master/graph/badge.svg)](https://codecov.io/gh/guofei9987/blind_watermark) +[![License](https://img.shields.io/pypi/l/blind_watermark.svg)](https://github.com/guofei9987/blind_watermark/blob/master/LICENSE) +![Python](https://img.shields.io/badge/python->=3.5-green.svg) +![Platform](https://img.shields.io/badge/platform-windows%20|%20linux%20|%20macos-green.svg) +[![stars](https://img.shields.io/github/stars/guofei9987/blind_watermark.svg?style=social)](https://github.com/guofei9987/blind_watermark/) +[![fork](https://img.shields.io/github/forks/guofei9987/blind_watermark?style=social)](https://github.com/guofei9987/blind_watermark/fork) +[![Downloads](https://pepy.tech/badge/blind-watermark)](https://pepy.tech/project/blind-watermark) +[![Discussions](https://img.shields.io/badge/discussions-green.svg)](https://github.com/guofei9987/blind_watermark/discussions) +* 嵌入图片 +* 嵌入文本 +* 嵌入二进制 + +[GitHub](https://github.com/guofei9987/blind_watermark/) +[开始](/zh/README) diff --git a/docs/zh/_sidebar.md b/docs/zh/_sidebar.md new file mode 100644 index 0000000..766f183 --- /dev/null +++ b/docs/zh/_sidebar.md @@ -0,0 +1,2 @@ +* [文档](zh/README.md) + diff --git "a/docs/\344\272\256\345\272\246\346\224\273\345\207\273.jpg" "b/docs/\344\272\256\345\272\246\346\224\273\345\207\273.jpg" new file mode 100644 index 0000000..22b46f8 Binary files /dev/null and "b/docs/\344\272\256\345\272\246\346\224\273\345\207\273.jpg" differ diff --git "a/docs/\344\272\256\345\272\246\350\260\203\344\275\216\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" "b/docs/\344\272\256\345\272\246\350\260\203\344\275\216\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" new file mode 100644 index 0000000..d8bdeeb Binary files /dev/null and "b/docs/\344\272\256\345\272\246\350\260\203\344\275\216\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" differ diff --git "a/docs/\344\272\256\345\272\246\350\260\203\351\253\230\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" "b/docs/\344\272\256\345\272\246\350\260\203\351\253\230\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" new file mode 100644 index 0000000..0505dce Binary files /dev/null and "b/docs/\344\272\256\345\272\246\350\260\203\351\253\230\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" differ diff --git "a/docs/\345\216\237\345\233\276.jpeg" "b/docs/\345\216\237\345\233\276.jpeg" new file mode 100644 index 0000000..50b0580 Binary files /dev/null and "b/docs/\345\216\237\345\233\276.jpeg" differ diff --git "a/docs/\345\244\232\351\201\256\346\214\241\346\224\273\345\207\273.jpg" "b/docs/\345\244\232\351\201\256\346\214\241\346\224\273\345\207\273.jpg" new file mode 100644 index 0000000..d7eab43 Binary files /dev/null and "b/docs/\345\244\232\351\201\256\346\214\241\346\224\273\345\207\273.jpg" differ diff --git "a/docs/\345\244\232\351\201\256\346\214\241\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" "b/docs/\345\244\232\351\201\256\346\214\241\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" new file mode 100644 index 0000000..0b38cd5 Binary files /dev/null and "b/docs/\345\244\232\351\201\256\346\214\241\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" differ diff --git "a/docs/\346\210\252\345\261\217\346\224\273\345\207\2732_\350\277\230\345\216\237.jpg" "b/docs/\346\210\252\345\261\217\346\224\273\345\207\2732_\350\277\230\345\216\237.jpg" new file mode 100644 index 0000000..aabd456 Binary files /dev/null and "b/docs/\346\210\252\345\261\217\346\224\273\345\207\2732_\350\277\230\345\216\237.jpg" differ diff --git "a/docs/\346\211\223\344\270\212\346\260\264\345\215\260\347\232\204\345\233\276.jpg" "b/docs/\346\211\223\344\270\212\346\260\264\345\215\260\347\232\204\345\233\276.jpg" new file mode 100644 index 0000000..b9f0dcb Binary files /dev/null and "b/docs/\346\211\223\344\270\212\346\260\264\345\215\260\347\232\204\345\233\276.jpg" differ diff --git "a/docs/\346\227\213\350\275\254\346\224\273\345\207\273.jpg" "b/docs/\346\227\213\350\275\254\346\224\273\345\207\273.jpg" new file mode 100644 index 0000000..733ea2f Binary files /dev/null and "b/docs/\346\227\213\350\275\254\346\224\273\345\207\273.jpg" differ diff --git "a/docs/\346\227\213\350\275\254\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" "b/docs/\346\227\213\350\275\254\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" new file mode 100644 index 0000000..adc5f18 Binary files /dev/null and "b/docs/\346\227\213\350\275\254\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" differ diff --git "a/docs/\346\244\222\347\233\220\346\224\273\345\207\273.jpg" "b/docs/\346\244\222\347\233\220\346\224\273\345\207\273.jpg" new file mode 100644 index 0000000..5249cf6 Binary files /dev/null and "b/docs/\346\244\222\347\233\220\346\224\273\345\207\273.jpg" differ diff --git "a/docs/\346\244\222\347\233\220\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" "b/docs/\346\244\222\347\233\220\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" new file mode 100644 index 0000000..2ca4fb1 Binary files /dev/null and "b/docs/\346\244\222\347\233\220\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" differ diff --git "a/docs/\346\250\252\345\220\221\350\243\201\345\211\252\346\224\273\345\207\273_\345\241\253\350\241\245.jpg" "b/docs/\346\250\252\345\220\221\350\243\201\345\211\252\346\224\273\345\207\273_\345\241\253\350\241\245.jpg" new file mode 100644 index 0000000..0167dc6 Binary files /dev/null and "b/docs/\346\250\252\345\220\221\350\243\201\345\211\252\346\224\273\345\207\273_\345\241\253\350\241\245.jpg" differ diff --git "a/docs/\346\250\252\345\220\221\350\243\201\345\211\252\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" "b/docs/\346\250\252\345\220\221\350\243\201\345\211\252\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" new file mode 100644 index 0000000..ba9daf5 Binary files /dev/null and "b/docs/\346\250\252\345\220\221\350\243\201\345\211\252\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" differ diff --git "a/docs/\346\260\264\345\215\260.png" "b/docs/\346\260\264\345\215\260.png" new file mode 100644 index 0000000..b2fdd40 Binary files /dev/null and "b/docs/\346\260\264\345\215\260.png" differ diff --git "a/docs/\347\272\265\345\220\221\350\243\201\345\211\252\346\224\273\345\207\273_\345\241\253\350\241\245.jpg" "b/docs/\347\272\265\345\220\221\350\243\201\345\211\252\346\224\273\345\207\273_\345\241\253\350\241\245.jpg" new file mode 100644 index 0000000..7a05f20 Binary files /dev/null and "b/docs/\347\272\265\345\220\221\350\243\201\345\211\252\346\224\273\345\207\273_\345\241\253\350\241\245.jpg" differ diff --git "a/docs/\347\272\265\345\220\221\350\243\201\345\211\252\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" "b/docs/\347\272\265\345\220\221\350\243\201\345\211\252\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" new file mode 100644 index 0000000..c341a20 Binary files /dev/null and "b/docs/\347\272\265\345\220\221\350\243\201\345\211\252\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" differ diff --git "a/docs/\347\274\251\346\224\276\346\224\273\345\207\273.jpg" "b/docs/\347\274\251\346\224\276\346\224\273\345\207\273.jpg" new file mode 100644 index 0000000..79b2a71 Binary files /dev/null and "b/docs/\347\274\251\346\224\276\346\224\273\345\207\273.jpg" differ diff --git "a/docs/\347\274\251\346\224\276\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" "b/docs/\347\274\251\346\224\276\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" new file mode 100644 index 0000000..0080bbd Binary files /dev/null and "b/docs/\347\274\251\346\224\276\346\224\273\345\207\273_\346\217\220\345\217\226\346\260\264\345\215\260.png" differ diff --git "a/docs/\350\247\243\345\207\272\347\232\204\346\260\264\345\215\260.png" "b/docs/\350\247\243\345\207\272\347\232\204\346\260\264\345\215\260.png" new file mode 100644 index 0000000..173df04 Binary files /dev/null and "b/docs/\350\247\243\345\207\272\347\232\204\346\260\264\345\215\260.png" differ diff --git a/examples/example_bit.py b/examples/example_bit.py new file mode 100644 index 0000000..41d05f3 --- /dev/null +++ b/examples/example_bit.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- + +from blind_watermark import att +from blind_watermark import WaterMark +import cv2 +from blind_watermark import WaterMarkCore +import numpy as np + +# %% + +bwm = WaterMark(password_img=1, password_wm=1) + +# 读取原图 +bwm.read_img('pic/ori_img.jpeg') + +# 读取水印 +wm = [True, False, True, False, True, False, True, False, True, False] +bwm.read_wm(wm, mode='bit') + +# 打上盲水印 +bwm.embed('output/embedded.png') + +len_wm = len(wm) # 解水印需要用到长度 +ori_img_shape = cv2.imread('pic/ori_img.jpeg').shape[:2] # 抗攻击需要知道原图的shape + +# %% 解水印 + +# 注意设定水印的长宽wm_shape +bwm1 = WaterMark(password_img=1, password_wm=1) +wm_extract = bwm1.extract('output/embedded.png', wm_shape=len_wm, mode='bit') +print("不攻击的提取结果:", wm_extract) + +assert np.all(wm == wm_extract), '提取水印和原水印不一致' + +# %%截屏攻击 + +loc = ((0.3, 0.1), (0.7, 0.9)) + +att.cut_att(input_filename='output/embedded.png', output_file_name='output/截屏攻击.png', loc=loc) + +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract('output/截屏攻击.png', wm_shape=len_wm, mode='bit') +print("截屏攻击{loc}后的提取结果:".format(loc=loc), wm_extract) +assert np.all(wm == wm_extract), '提取水印和原水印不一致' + +# %% +# 一次横向裁剪打击 +r = 0.2 +att.cut_att_width(input_filename='output/embedded.png', output_file_name='output/横向裁剪攻击.png', ratio=r) +att.anti_cut_att(input_filename='output/横向裁剪攻击.png', output_file_name='output/横向裁剪攻击_填补.png', + origin_shape=ori_img_shape) + +# 提取水印 +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract('output/横向裁剪攻击_填补.png', wm_shape=len_wm, mode='bit') +print(f"横向裁剪攻击r={r}后的提取结果:", wm_extract) + +assert np.all(wm == wm_extract), '提取水印和原水印不一致' + +# %%一次纵向裁剪攻击 +ratio = 0.2 +att.cut_att_height(input_filename='output/embedded.png', output_file_name='output/纵向裁剪攻击.png', ratio=ratio) +att.anti_cut_att(input_filename='output/纵向裁剪攻击.png', output_file_name='output/纵向裁剪攻击_填补.png', + origin_shape=ori_img_shape) + +# 提取 +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract('output/纵向裁剪攻击_填补.png', wm_shape=len_wm, mode='bit') +print(f"纵向裁剪攻击ratio={ratio}后的提取结果:", wm_extract) + +assert np.all(wm == wm_extract), '提取水印和原水印不一致' +# %%椒盐攻击 +ratio = 0.05 +att.salt_pepper_att(input_filename='output/embedded.png', output_file_name='output/椒盐攻击.png', ratio=ratio) +# ratio是椒盐概率 + +# 提取 +wm_extract = bwm1.extract('output/椒盐攻击.png', wm_shape=len_wm, mode='bit') +print(f"椒盐攻击ratio={ratio}后的提取结果:", wm_extract) +assert np.all(wm == wm_extract), '提取水印和原水印不一致' + +# %%旋转攻击 +att.rot_att(input_filename='output/embedded.png', output_file_name='output/旋转攻击.png', angle=45) +att.rot_att(input_filename='output/旋转攻击.png', output_file_name='output/旋转攻击_还原.png', angle=-45) + +# 提取水印 +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract('output/旋转攻击_还原.png', wm_shape=len_wm, mode='bit') +print("旋转攻击后的提取结果:", wm_extract) +assert np.all(wm == wm_extract), '提取水印和原水印不一致' + +# %%遮挡攻击 +n = 60 +att.shelter_att(input_filename='output/embedded.png', output_file_name='output/多遮挡攻击.png', ratio=0.1, n=n) + +# 提取 +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract('output/多遮挡攻击.png', wm_shape=len_wm, mode='bit') +print(f"遮挡攻击{n}后的提取结果:", wm_extract) +assert np.all(wm == wm_extract), '提取水印和原水印不一致' + +# %%缩放攻击 +att.resize_att(input_filename='output/embedded.png', output_file_name='output/缩放攻击.png', out_shape=(800, 600)) +att.resize_att(input_filename='output/缩放攻击.png', output_file_name='output/缩放攻击_还原.png', out_shape=ori_img_shape[::-1]) +# out_shape 是分辨率,需要颠倒一下 + +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract('output/缩放攻击_还原.png', wm_shape=len_wm, mode='bit') +print("缩放攻击后的提取结果:", wm_extract) +assert np.all(wm == wm_extract), '提取水印和原水印不一致' diff --git a/examples/example_img.py b/examples/example_img.py new file mode 100644 index 0000000..f3b1444 --- /dev/null +++ b/examples/example_img.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import cv2 + +from blind_watermark import WaterMark + +bwm = WaterMark(password_wm=1, password_img=1) +# 读取原图 +bwm.read_img(filename='pic/ori_img.jpeg') +# 读取水印 +bwm.read_wm('pic/watermark.png') +# 打上盲水印 +bwm.embed('output/embedded.png') +wm_shape = cv2.imread('pic/watermark.png', flags=cv2.IMREAD_GRAYSCALE).shape + +# %% 解水印 + + +bwm1 = WaterMark(password_wm=1, password_img=1) +# 注意需要设定水印的长宽wm_shape +bwm1.extract('output/embedded.png', wm_shape=wm_shape, out_wm_name='output/wm_extracted.png', mode='img') diff --git a/examples/example_no_writing.py b/examples/example_no_writing.py new file mode 100644 index 0000000..35badc8 --- /dev/null +++ b/examples/example_no_writing.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +This demonstrates how to embed and extract without writing files to local file system. +The format of images is numpy.array. +This may be useful if you want to use blind-watermark in another project. +""" + +from blind_watermark import WaterMark +from blind_watermark import att +from blind_watermark.recover import estimate_crop_parameters, recover_crop +import cv2 +import numpy as np + +ori_img = cv2.imread('pic/ori_img.jpeg', flags=cv2.IMREAD_UNCHANGED) +wm = '@guofei9987 开源万岁!' +ori_img_shape = ori_img.shape[:2] # 抗攻击有时需要知道原图的shape + +# %% embed string into image whose format is numpy.array +bwm = WaterMark(password_img=1, password_wm=1) +bwm.read_img(img=ori_img) + +bwm.read_wm(wm, mode='str') +embed_img = bwm.embed() + +len_wm = len(bwm.wm_bit) # 解水印需要用到长度 +print('Put down the length of wm_bit {len_wm}'.format(len_wm=len_wm)) + +# %% extract from image whose format is numpy.array +bwm1 = WaterMark(password_img=1, password_wm=1) +wm_extract = bwm1.extract(embed_img=embed_img, wm_shape=len_wm, mode='str') +print("不攻击的提取结果:", wm_extract) + +assert wm == wm_extract, '提取水印和原水印不一致' + +# %%截屏攻击 = 裁剪攻击 + 缩放攻击 + 知道攻击参数(按照参数还原) + +loc = ((0.1, 0.1), (0.5, 0.5)) +resize = 0.7 +img_attacked = att.cut_att(input_img=embed_img, output_file_name=None, loc=loc, resize=resize) + +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract(embed_img=img_attacked, wm_shape=len_wm, mode='str') +print("截屏攻击={loc},缩放攻击={resize},并且知道攻击参数。提取结果:".format(loc=loc, resize=resize), wm_extract) +assert wm == wm_extract, '提取水印和原水印不一致' + +# %% 截屏攻击 = 剪切攻击 + 缩放攻击 + 不知道攻击参数 +loc_r = ((0.1, 0.1), (0.7, 0.6)) +scale = 0.7 +img_attacked, (x1, y1, x2, y2) = att.cut_att2(input_img=embed_img, loc_r=loc_r, scale=scale) +print(f'Crop attack\'s real parameters: x1={x1},y1={y1},x2={x2},y2={y2}') + +# estimate crop attack parameters: +(x1, y1, x2, y2), image_o_shape, score, scale_infer = estimate_crop_parameters(ori_img=embed_img, + tem_img=img_attacked, + scale=(0.5, 2), search_num=200) + +print(f'Crop attack\'s estimate parameters: x1={x1},y1={y1},x2={x2},y2={y2}. score={score}') + +# recover from attack: +img_recovered = recover_crop(tem_img=img_attacked, loc=(x1, y1, x2, y2), image_o_shape=image_o_shape) + +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract(embed_img=embed_img, wm_shape=len_wm, mode='str') +print("截屏攻击,不知道攻击参数。提取结果:", wm_extract) +assert wm == wm_extract, '提取水印和原水印不一致' + +# %% Vertical cut +r = 0.3 +img_attacked = att.cut_att_width(input_img=embed_img, ratio=r) +img_recovered = att.anti_cut_att(input_img=img_attacked, origin_shape=ori_img_shape) + +# 提取水印 +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract(embed_img=img_recovered, wm_shape=len_wm, mode='str') +print(f"横向裁剪攻击r={r}后的提取结果:", wm_extract) + +assert wm == wm_extract, '提取水印和原水印不一致' + +# %% horizontal cut +r = 0.4 +img_attacked = att.cut_att_height(input_img=embed_img, ratio=r) +img_recovered = att.anti_cut_att(input_img=img_attacked, origin_shape=ori_img_shape) + +# extract: +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract(embed_img=img_recovered, wm_shape=len_wm, mode='str') +print(f"纵向裁剪攻击r={r}后的提取结果:", wm_extract) + +assert wm == wm_extract, '提取水印和原水印不一致' + +# %%椒盐攻击 +ratio = 0.05 +img_attacked = att.salt_pepper_att(input_img=embed_img, ratio=ratio) +# ratio是椒盐概率 + +# 提取 +wm_extract = bwm1.extract(embed_img=img_attacked, wm_shape=len_wm, mode='str') +print(f"椒盐攻击ratio={ratio}后的提取结果:", wm_extract) +assert np.all(wm == wm_extract), '提取水印和原水印不一致' + +# %%旋转攻击 +angle = 60 +img_attacked = att.rot_att(input_img=embed_img, angle=angle) +img_recovered = att.rot_att(input_img=img_attacked, angle=-angle) + +# 提取水印 +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract(embed_img=img_recovered, wm_shape=len_wm, mode='str') +print(f"旋转攻击angle={angle}后的提取结果:", wm_extract) +assert wm == wm_extract, '提取水印和原水印不一致' + +# %%遮挡攻击 +n = 60 +img_attacked = att.shelter_att(input_img=embed_img, ratio=0.1, n=n) + +# 提取 +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract(embed_img=img_attacked, wm_shape=len_wm, mode='str') +print(f"遮挡攻击{n}次后的提取结果:", wm_extract) +assert wm == wm_extract, '提取水印和原水印不一致' + +# %%缩放攻击 +img_attacked = att.resize_att(input_img=embed_img, out_shape=(400, 300)) +img_recovered = att.resize_att(input_img=img_attacked, out_shape=ori_img_shape[::-1]) +# out_shape 是分辨率,需要颠倒一下 + +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract(embed_img=img_recovered, wm_shape=len_wm, mode='str') +print("缩放攻击后的提取结果:", wm_extract) +assert np.all(wm == wm_extract), '提取水印和原水印不一致' +# %% + +img_attacked = att.bright_att(input_img=embed_img, ratio=0.9) +img_recovered = att.bright_att(input_img=img_attacked, ratio=1.1) +wm_extract = bwm1.extract(embed_img=img_recovered, wm_shape=len_wm, mode='str') + +print("亮度攻击后的提取结果:", wm_extract) +assert np.all(wm == wm_extract), '提取水印和原水印不一致' diff --git a/examples/example_str.py b/examples/example_str.py new file mode 100644 index 0000000..ef8a3ff --- /dev/null +++ b/examples/example_str.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# embed string +import numpy as np +from blind_watermark import WaterMark +from blind_watermark import att +from blind_watermark.recover import estimate_crop_parameters, recover_crop + +import cv2 + +bwm = WaterMark(password_img=1, password_wm=1) +bwm.read_img('pic/ori_img.jpeg') +wm = '@guofei9987 开源万岁!' +bwm.read_wm(wm, mode='str') +bwm.embed('output/embedded.png') + +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('pic/ori_img.jpeg').shape[:2] # 抗攻击有时需要知道原图的shape + +# %% 解水印 +bwm1 = WaterMark(password_img=1, password_wm=1) +wm_extract = bwm1.extract('output/embedded.png', wm_shape=len_wm, mode='str') +print("不攻击的提取结果:", wm_extract) + +assert wm == wm_extract, '提取水印和原水印不一致' + +# %%截屏攻击 = 裁剪攻击 + 缩放攻击 + 知道攻击参数(按照参数还原) + +loc = ((0.1, 0.1), (0.5, 0.5)) +resize = 0.7 +att.cut_att(input_filename='output/embedded.png', output_file_name='output/截屏攻击.png', loc=loc, resize=resize) + +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract('output/截屏攻击.png', wm_shape=len_wm, mode='str') +print("截屏攻击={loc},缩放攻击={resize},并且知道攻击参数。提取结果:".format(loc=loc, resize=resize), wm_extract) +assert wm == wm_extract, '提取水印和原水印不一致' + +# %% 截屏攻击 = 剪切攻击 + 缩放攻击 + 不知道攻击参数 +loc_r = ((0.1, 0.1), (0.7, 0.6)) +scale = 0.7 +_, (x1, y1, x2, y2) = att.cut_att2(input_filename='output/embedded.png', output_file_name='output/截屏攻击2.png', + loc_r=loc_r, scale=scale) +print(f'Crop attack\'s real parameters: x1={x1},y1={y1},x2={x2},y2={y2}') + +# estimate crop attack parameters: +(x1, y1, x2, y2), image_o_shape, score, scale_infer = estimate_crop_parameters(original_file='output/embedded.png', + template_file='output/截屏攻击2.png', + scale=(0.5, 2), search_num=200) + +print(f'Crop attack\'s estimate parameters: x1={x1},y1={y1},x2={x2},y2={y2}. score={score}') + +# recover from attack: +recover_crop(template_file='output/截屏攻击2.png', output_file_name='output/截屏攻击2_还原.png', + loc=(x1, y1, x2, y2), image_o_shape=image_o_shape) + +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract('output/截屏攻击2_还原.png', wm_shape=len_wm, mode='str') +print("截屏攻击,不知道攻击参数。提取结果:", wm_extract) +assert wm == wm_extract, '提取水印和原水印不一致' + +# %% Vertical cut +r = 0.3 +att.cut_att_width(input_filename='output/embedded.png', output_file_name='output/横向裁剪攻击.png', ratio=r) +att.anti_cut_att(input_filename='output/横向裁剪攻击.png', output_file_name='output/横向裁剪攻击_填补.png', + origin_shape=ori_img_shape) + +# 提取水印 +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract('output/横向裁剪攻击_填补.png', wm_shape=len_wm, mode='str') +print(f"横向裁剪攻击r={r}后的提取结果:", wm_extract) + +assert wm == wm_extract, '提取水印和原水印不一致' + +# %% horizontal cut +r = 0.4 +att.cut_att_height(input_filename='output/embedded.png', output_file_name='output/纵向裁剪攻击.png', ratio=r) +att.anti_cut_att(input_filename='output/纵向裁剪攻击.png', output_file_name='output/纵向裁剪攻击_填补.png', + origin_shape=ori_img_shape) + +# extract: +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract('output/纵向裁剪攻击_填补.png', wm_shape=len_wm, mode='str') +print(f"纵向裁剪攻击r={r}后的提取结果:", wm_extract) + +assert wm == wm_extract, '提取水印和原水印不一致' +# %%椒盐攻击 +ratio = 0.05 +att.salt_pepper_att(input_filename='output/embedded.png', output_file_name='output/椒盐攻击.png', ratio=ratio) +# ratio是椒盐概率 + +# 提取 +wm_extract = bwm1.extract('output/椒盐攻击.png', wm_shape=len_wm, mode='str') +print(f"椒盐攻击ratio={ratio}后的提取结果:", wm_extract) +assert np.all(wm == wm_extract), '提取水印和原水印不一致' + +# %%旋转攻击 +angle = 60 +att.rot_att(input_filename='output/embedded.png', output_file_name='output/旋转攻击.png', angle=angle) +att.rot_att(input_filename='output/旋转攻击.png', output_file_name='output/旋转攻击_还原.png', angle=-angle) + +# 提取水印 +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract('output/旋转攻击_还原.png', wm_shape=len_wm, mode='str') +print(f"旋转攻击angle={angle}后的提取结果:", wm_extract) +assert wm == wm_extract, '提取水印和原水印不一致' + +# %%遮挡攻击 +n = 60 +att.shelter_att(input_filename='output/embedded.png', output_file_name='output/多遮挡攻击.png', ratio=0.1, n=n) + +# 提取 +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract('output/多遮挡攻击.png', wm_shape=len_wm, mode='str') +print(f"遮挡攻击{n}次后的提取结果:", wm_extract) +assert wm == wm_extract, '提取水印和原水印不一致' + +# %%缩放攻击 +att.resize_att(input_filename='output/embedded.png', output_file_name='output/缩放攻击.png', out_shape=(400, 300)) +att.resize_att(input_filename='output/缩放攻击.png', output_file_name='output/缩放攻击_还原.png', + out_shape=ori_img_shape[::-1]) +# out_shape 是分辨率,需要颠倒一下 + +bwm1 = WaterMark(password_wm=1, password_img=1) +wm_extract = bwm1.extract('output/缩放攻击_还原.png', wm_shape=len_wm, mode='str') +print("缩放攻击后的提取结果:", wm_extract) +assert np.all(wm == wm_extract), '提取水印和原水印不一致' +# %% + +att.bright_att(input_filename='output/embedded.png', output_file_name='output/亮度攻击.png', ratio=0.9) +att.bright_att(input_filename='output/亮度攻击.png', output_file_name='output/亮度攻击_还原.png', ratio=1.1) +wm_extract = bwm1.extract('output/亮度攻击_还原.png', wm_shape=len_wm, mode='str') + +print("亮度攻击后的提取结果:", wm_extract) +assert np.all(wm == wm_extract), '提取水印和原水印不一致' diff --git a/examples/output/.keep b/examples/output/.keep new file mode 100644 index 0000000..e69de29 diff --git a/examples/pic/ori_img.jpeg b/examples/pic/ori_img.jpeg new file mode 100644 index 0000000..ef48a05 Binary files /dev/null and b/examples/pic/ori_img.jpeg differ diff --git a/examples/pic/watermark.png b/examples/pic/watermark.png new file mode 100644 index 0000000..d645a1a Binary files /dev/null and b/examples/pic/watermark.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2bc4287 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +numpy>=1.17.0 +opencv-python +setuptools +PyWavelets diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..96579a4 --- /dev/null +++ b/setup.py @@ -0,0 +1,39 @@ +from setuptools import setup, find_packages +from os import path as os_path +import blind_watermark + +this_directory = os_path.abspath(os_path.dirname(__file__)) + + +# 读取文件内容 +def read_file(filename): + with open(os_path.join(this_directory, filename), encoding='utf-8') as f: + long_description = f.read() + return long_description + + +# 获取依赖 +def read_requirements(filename): + return [line.strip() for line in read_file(filename).splitlines() + if not line.startswith('#')] + + +setup(name='blind_watermark', + python_requires='>=3.5', + version=blind_watermark.__version__, + description='Blind Watermark in Python', + long_description=read_file('docs/en/README.md'), + long_description_content_type="text/markdown", + url='https://github.com/guofei9987/blind_watermark', + author='Guo Fei', + author_email='guofei9987@foxmail.com', + license='MIT', + packages=find_packages(), + platforms=['linux', 'windows', 'macos'], + install_requires=['numpy', 'opencv-python', 'PyWavelets'], + zip_safe=False, + entry_points={ + 'console_scripts': [ + 'blind_watermark = blind_watermark.cli_tools:main' + ] + })