# 盲水印和图片隐写术 

By [ulyc](https://paragraph.com/@ulyc) · 2021-11-30

---

> The shadow is within. ​ —— 劫 《 英雄联盟》

盲水印和图片隐写术
---------

盲水印
---

### 一、演示

首先看 这是一张女朋友

![5cd152cce839e](https://storage.googleapis.com/papyrus_images/38ea9463f3ef9a372b9b50679d6b11a2ec6ec0c178184ffe434d8ec3508c66d6.jpg)

5cd152cce839e

##### 解码水印

接下来我们输入一行神奇的命令:

`python bwm.py --action decode --origin Demo.jpg --im ../Gakki.jpg --result res.jpg`

可以得到这样的一张图:

![res](https://storage.googleapis.com/papyrus_images/a964940582a60878eefdb415624ee2c17ed3db756e529ecd1f3d000203205637.jpg)

res

以后谁再跟你抢女朋友就可以这样声明版权了嘿嘿.

(脚本和原图都在最后的附录里, 有兴趣的朋友只需要将上面的图片保存为`Demo.jpg`,附录里的原图保存为`Gakki.jpg`, 就可以解码出上面的信息)

##### 加密水印

通过今天的方法你可以将信息放入任意图片,来达到加密信息的目的.

附录里的脚本, 加密用法:

`python bwm.py --action encode --origin Gakki.jpg --im wm1.png --result Demo.jpg --alpha 2`

### 二、用途

上面 的水印就叫做**盲水印**，隐藏式的水印是以数字数据的方式加入音频、图片或影片中，但在一般的状况下无法被看见。隐藏式水印的重要应用之一是保护版权，期望能借此避免或阻止数字媒体未经授权的复制和拷贝。

##### 1.不同人加相同水印

声明版权

**应用案例**：

*   某些画师、摄影师、设计师会在其作品中加入水印。
    

*   淘宝防盗图功能
    

##### 2.不同人加不同水印

将某份保密数字资料发送给不同人时，可加上不同标识，如果资料被复制、传播可根据解码出的唯一标识来追究责任人。

**应用案例**：

*   电影刚刚公映时，每个影院，影厅的 电影底片里都会加入不同的不可见水印， 如果电影流出，就可追究相关影院责任。
    
*   阿里，华为等公司内部论坛、平台会在HTML页面中加入足够数量 及不被发现的唯一标识。当有内部敏感信息通过截图等方式流出，也可追踪到个人。
    

![1551441624605](https://storage.googleapis.com/papyrus_images/ec82ed6d34236b6a1c8316002b25e12b3b763af2835944f7dd1371c4695e81e7.png)

1551441624605

### 三、原理

#### 原理图

![v2-bbecf64a76b3dbfc4539e38e46ad8223_hd](https://storage.googleapis.com/papyrus_images/15be610f1c50f8549e70016bfab830a12d9b1e500d79855b1485484eb04cac3d.png)

v2-bbecf64a76b3dbfc4539e38e46ad8223\_hd

#### 傅里叶变换

*   简单复习下傅里叶变换
    

傅里叶变换简单地说就是将信号在**时域**或**空域**的函数转变到**频域**表示，在和工程学中有许多应用。因其基本思想首先由法国学者约瑟夫·傅里叶系统地提出。

*   再理解下时域和频域
    

![Fourier_transform_time_and_frequency_domains_(small)](https://storage.googleapis.com/papyrus_images/30d3b1ad3eb3a412db11fa9e88b8bb128bc85ee8935176f848dd69f614fecc13.gif)

Fourier\_transform\_time\_and\_frequency\_domains\_(small)

![40cf849e55ed95732a60b52d4019d609_b](https://storage.googleapis.com/papyrus_images/77cb23b95df7c90dc1ea8652eb58619aa934e995abcca17d14f93048d2a180fb.jpg)

40cf849e55ed95732a60b52d4019d609\_b

那么，傅里叶变换有什么用呢，

*   先在纸上画一个sin（x），不一定标准，意思差不多就行。不是很难吧。
    
*   好，接下去画一个sin（3x）+sin（5x）的图形。这个就很难能画得出来。
    

现在把sin（3x）+sin（5x）的曲线给你，只看图是看不出这整个曲线的方程式是怎样的，现在需要将把sin（5x）从图里拿出去，看看剩下的是什么。这基本是不可能做到的。

但是在频域呢？则简单的很，无非就是几条竖线而已。

这是最简单的一种用法，其他复杂用法不在此赘述。

#### 频谱图

一维信号的变换理解之后，那么图像的频谱图长什么样呢。

![](https://storage.googleapis.com/papyrus_images/779ba7c7d6b5c818f960933a724ee7ad0b5b79effbbcb5b0e36cb97ea9fbee25.png)

![](https://storage.googleapis.com/papyrus_images/ece5cc581d7bf5c56aad6472497a7048a9b6ac24f5186a3e3c70316fecc3ed86.png)

图片中明亮的部分就是低频（平缓）部分，暗点的是高频（突变交界）部分。一般为了展示会把频谱图低频的部分移到中心。频谱图上的点跟原图不存在一一对应关系，频谱图的每一点都来自于全部的图像（类似于时域曲线的点，和频域图的点）。

这样可能还不够直观，接下来看这张图。

![img](https://storage.googleapis.com/papyrus_images/118aa333be600dd32f726b335559995d72452c5dd89ac817e76e9a73f980030c.jpg)

img

这是一张400x400的图，共有16 万个像素点。

我们平时怎么来表示一张图片呢，首先是在笛卡尔坐标系中用x,y来定位某一确定的点。那么，我们怎么来描述这个点呢？

我们知道，所有的色彩都是由三原色组成。生活中经常说的红、黄、蓝（青），其实是一种消减型的三原色，光学中的三原色是红、绿、蓝，也就是R、G、B。

通常我们用来描述图像点的方法就是RGB的值，其实图像处理中用的是**灰度**（Gray scale）来表示图片，但是为了便于理解，下面用的是RGB演示 。

![CORB-RGB.png](https://storage.googleapis.com/papyrus_images/282a9635d81fd65aa2938d68fd98450384ca777975fef28ec474b249bcc709b0.png)

CORB-RGB.png

上图是截取了某一行RGB的值做成的曲线图，可以看到，每条曲线都在不停的上下波动，且波动的频率是相同的。有些区域的波动比较小，有些区域突然出现了大幅波动。

对比一下图像就能发现，曲线波动较大的地方，也是图像出现突变的地方。

**图像的频谱可以理解为将一维的频谱绕着纵轴旋转一圈，形成一个3维的数学函数图（原图中心对称、镜像对称才可以这样干，其他类似），x、y轴代表两个方向的频率，z轴代表该频率的幅值，只不过频谱图像是一个2维图，所以用亮度来表示幅值了。**

**二维傅里叶变换的物理意义是将图像的灰度分布函数变换为图像的频率分布函数。**

#### 盲水印的特性

鲁棒性一般要能抗（压缩 、裁剪、涂画，旋转）。

![特性](https://storage.googleapis.com/papyrus_images/86b79e4af39ceec9f5e562fe4fbbf0b24de5b8098f993c09f24e286ed00b26bc.png)

特性

1.  **隐蔽性**
    
    由于不希望被察觉、不希望干扰用户体验、不希望被模仿等等原因，我们的水印不可见，也就是隐匿性。
    
2.  **不易移除性**
    
    不易移除性跟鲁棒性有些相似， 不同的是：
    
    鲁棒性更加强调的是数字资源在传播过程中不要被**不自觉**地干扰和破坏。
    
    不易移除性是在别有用心者察觉了盲水印的存在后，不被他们**自觉**地移除或者破坏。
    
3.  **强健性**
    
    强健性通常也被称作鲁棒性，来自于其英文名称（Robustness）的音译。
    
    简单地说就是耐操性。
    
    需要说明的一点是，鲁棒性和隐蔽性通常不可兼得。
    
4.  **明确性**
    
    没什么可说的，就是盲水印需要表示出明确的信息。
    

### 四、引申

#### 图种

![相貌平平](https://storage.googleapis.com/papyrus_images/5d02c6df3bcafeb966a011060096a0d98b526c046cd01d0fc9a88f022ce61b8c.jpg)

相貌平平

例如这是一张相貌平平的图片, 你可以保存下来,将后缀改为”rar”或者直接用解压工具打开,就可以看到神秘福利.

制作方法也很简单,在win下 入以下命令就可以做一张”图种”了.

`copy /b A.jpg + B.zip C.jpg`

大约十年以前，图种被广泛上传到论坛等地用来传播资源。后来由于许多网站在上传图片时会判断图片结尾标识，其之后的全部丢弃，慢慢不再有人使用。([https://sm.ms/](https://sm.ms/)这个图床还是很给力的, 经测试还是可以解析种子)

#### 隐藏文件

图片可以跟种子文件结合，当然也可以和其他文件结合。

![](https://storage.googleapis.com/papyrus_images/5688b328a2044ee91040323a919ff1396f78bd5f4e0909cd53d694c891c5d623.png)

其实隐藏文件和盲水印都属于**图片隐写术**。

图片隐写术
-----

[隐写术](https://zh.wikipedia.org/wiki/%E9%9A%90%E5%86%99%E6%9C%AF)（Steganography）也是数字水印的一种应用，双方可利用隐藏在数字信号中的信息进行沟通。

数字照片中的注释数据能记录照片拍摄的时间、使用的[光圈](https://zh.wikipedia.org/wiki/%E5%85%89%E5%9C%88)和[快门](https://zh.wikipedia.org/wiki/%E5%BF%AB%E9%96%80)，甚至是相机的厂牌等信息，这也是数字水印的应用之一。

某些文件格式可以包含这些称为“metadata”的额外信息。

### 用途

#### 规避敏感词过滤

​ 所谓的“敏感词过滤”，常翻墙的同学，应该都很熟悉了。用图片来隐藏信息，可以规避GFW的敏感词过滤。

#### 规避肉眼审查

​ 国内的很多网站，对于上传的图片，都会进行人工审查。如果能通过技术手段把信息隐藏在图片中，而图片本身又看不出什么异样，人工审核就看不出来。

#### 传递加密信息

​ 不希望被别人看到的资料、信息等。

### 常见方法

![](https://storage.googleapis.com/papyrus_images/430726d8a0df17d76e350f81f322cf85b2663b36cc6db94ed7a849d511e12b3c.png)

### 原理

#### 内容覆盖法

通常来说，图片文件都有包含2部分：文件头和数据区。

而“内容覆盖法”，就是把要隐藏的文件，直接【覆盖】到图片文件的【数据区】的【尾部】。

比方说，某图片有 100KB，其中文件头占 1KB，那么，数据区就是 99KB。也就是说，最多只能隐藏 99KB 的文件。

切记：**覆盖的时候，千万不可破坏文件头**。文件头一旦破坏，这个图片文件就不再是一个合法的图片文件了。

使用这种方法，对图片文件的格式，是有讲究的——最好用【24位色的 BMP 格式】。

*   BMP 格式本身比较简单，数据区随便覆盖，问题不大；
    
*   24位色的 BMP 相对其它的格式 BMP，文件尺寸更大，可以隐藏更多内容。
    

    import sys
    
    def embed(container_file, data_file, output_file) :
        """代码没有严格计算 BMP 的文件头尺寸，只是大致预留了 1024 字节"""
        
        container = open(container_file, "rb").read()
        data = open(data_file, "rb").read()
    
        if len(data)+1024 >= len(container) :
            print("Not enough space to save " + data_file)
        else :
            f = open(output_file, "wb")
            f.write(container[ : len(container)-len(data)])
            f.write(data)
            f.close()
    
    if "__main__" == __name__ :
        try :
            if len(sys.argv) == 4 :
                embed(sys.argv[1], sys.argv[2], sys.argv[3])
            else :
                print("Usage:\n%s container data output" % sys.argv[0])
        except Exception as err :
            print(err)
    

#### LSB最低有效位

很多商业软件使用的原理都是这个方法。

![](https://storage.googleapis.com/papyrus_images/a83b3f32ecf8bb813f26af6882f6d64563ad544aba3aa39bc98e364e08d0fb88.png)

例如在PNG图片的储存中，每个颜色会有8bit，LSB（**Least Significant Bit**）隐写就是修改了像数中的最低的1bit，在人眼看来是看不出来区别的，也把信息隐藏起来了。（每个像数可以携带3bit的信息。）

![image-20210113193335303](https://storage.googleapis.com/papyrus_images/6d1de92beb7e17c813ab21e34a014ff47e64a333e8be3a7fc90f1f2d96d857c9.png)

image-20210113193335303

譬如我们想把’A’隐藏进来的话，如下图，就可以把A转成16进制的0x61再转成二进制的01100001，再修改为红色通道的最低位为这些二进制串。

![image-20210113193350763](https://storage.googleapis.com/papyrus_images/69b5d5b18b47e5ebc9dfbc671bd55a26ebf63761dddb1a636ac68346ccb19862.png)

image-20210113193350763

### 最后

1.  附上前面演示代码的实现:
    
    (参考了几个git hub上的项目,不过鲁棒性都不太好)
    
        # coding=utf-8
        import cv2
        import numpy as np
        import random
        import os
        from argparse import ArgumentParser
           
        ALPHA = 5
           
        class BlindWaterMark():
            """盲水印加解密，无频移简单版"""
            def __init__(self):
                self.parser = ArgumentParser()
                self.parser.add_argument('--action', dest='action', required=True)
                self.parser.add_argument('--origin', dest='ori', required=True)
                self.parser.add_argument('--img', dest='img', required=True)
                self.parser.add_argument('--result', dest='res', required=True)
                self.parser.add_argument('--alpha', dest='alpha', default=ALPHA)
           
            def encode(self, ori_path, wm_path, res_path, alpha):
                img = cv2.imread(ori_path)
                img_f = np.fft.fft2(img)  # 2维离散傅里叶变换
           
                height, width, channel = np.shape(img)
                watermark = cv2.imread(wm_path)
                wm_height, wm_width = watermark.shape[0], watermark.shape[1]
           
                # 水印随机编码
                x, y = range(height / 2), range(width)
                random.seed(height + width)   # 随机数解码时可控
                random.shuffle(x)
                random.shuffle(y)
                   
                # 按目标图片大小 对水印图进行对称
                tmp = np.zeros(img.shape)  # 根据图片形状，生成0填充的矩阵
           
                for i in range(height / 2):
                    for j in range(width):
                        if x[i] < wm_height and y[j] < wm_width:
                            tmp[i][j] = watermark[x[i]][y[j]]
                            tmp[height - 1 - i][width - 1 - j] = tmp[i][j]
           
                res_f = img_f + alpha * tmp  # 原图频域值  +  水印频域值
                res = np.fft.ifft2(res_f)      # 傅里叶逆变换
                res = np.real(res)  # 转换为实数
           
                cv2.imwrite(res_path, res, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
           
           
            def decode(self, ori_path, img_path, res_path, alpha):
                ori = cv2.imread(ori_path)
                img = cv2.imread(img_path)
           
                ori_f = np.fft.fft2(ori)
                img_f = np.fft.fft2(img)
           
                height, width = ori.shape[0], ori.shape[1]
                watermark = (ori_f - img_f) / alpha
           
                watermark = np.real(watermark)
                res = np.zeros(watermark.shape)
           
                random.seed(height + width)
           
                x = range(height / 2)
                y = range(width)
                random.shuffle(x)
                random.shuffle(y)
           
                for i in range(height / 2):
                    for j in range(width):
                        res[x[i]][y[j]] = watermark[i][j]
                        res[height - i - 1][width - j - 1] = res[i][j]
           
                cv2.imwrite(res_path, res, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
           
            def run(self):
                options = self.parser.parse_args()
                action = options.action
                ori = options.ori
                img = options.img
                res = options.res
                alpha = float(options.alpha)
           
                if not os.path.isfile(ori):
                    parser.error("image %s does not exist." % ori)
                if not os.path.isfile(img):
                    parser.error("watermark %s does not exist." % img)
           
                if action == "encode":
                    self.encode(ori, img, res, alpha)
                elif action == "decode":
                    self.decode(ori, img, res, alpha)
           
           
        if __name__ == '__main__':
            bwm = BlindWaterMark()
            bwm.run()
           
        
    

2.隐写术是一门很深、应用很广泛的学问，这里讲的很泛，权当做抛砖引玉。图片隐写术只是其中一种，有兴趣的同学可以看下面这本书。

![1551625636929](https://storage.googleapis.com/papyrus_images/de38a021a33ae6e9938b704c094bd94a465eb9e70fa179cb9058c93aedea98d2.png)

1551625636929

---

*Originally published on [ulyc](https://paragraph.com/@ulyc/sUNp7w8EOYKzehK7YoNV)*
