盲水印、暗水印(Blind Watermark)算法简明教程:算法原理、流程以及基于C/C++ 的代码实现

前言

由于工作需要,最近学习了盲水印相关的知识,本文对学习过程中做一个整理和总结。主要内容包括:

  1. 对盲水印相关概念做基本介绍
  2. 对开源的 python 算法 blind_watermark 进行解析,给出算法流程
  3. 基于 blind_watermark,给出了对应的 C/C++ 实现代码,你可以在 cpp_blind_watermark 找到所有代码

一、Blind Watermark 简介

盲水印(blind watermark)算法是一种将数字水印嵌入到数字媒体中的技术,而不需要原始媒体文件。与传统的数字水印技术不同,盲水印算法不需要原始媒体文件来提取数字水印,因此更加安全和隐私保护。

盲水印算法的基本原理是将数字水印嵌入到数字媒体的频域或空域中,使得数字水印能够在不影响原始媒体质量的情况下被提取出来。盲水印算法通常包括两个主要步骤:嵌入和提取。

在嵌入阶段,数字水印被嵌入到数字媒体中。这通常涉及到将数字水印转换为频域或空域信号,并将其嵌入到数字媒体中。嵌入过程需要考虑数字水印的鲁棒性和不可见性,以确保数字水印能够在不影响原始媒体质量的情况下被提取出来。

在提取阶段,数字水印被提取出来。这通常涉及到对数字媒体进行一些处理,以提取数字水印。提取过程需要考虑数字水印的鲁棒性和准确性,以确保数字水印能够被正确地提取出来。

盲水印算法在数字版权保护、数字身份认证和数字隐私保护等领域具有广泛的应用。它可以帮助数字内容提供商保护其版权,防止盗版和侵权行为;也可以帮助用户保护其数字身份和隐私,防止个人信息被泄露。

二、算法流程

本章分析 blind_watermark 中算法逻辑

2.1 水印嵌入

整体流程如下图,接下来对各个阶段做详细的解释。
在这里插入图片描述

2.1.1 水印二进制化

水印可以是各种形式的信息,包括字符串、图片等。为了嵌入这些信息,我们首先需要将水印转换为二进制形式,即由0和1组成的数组。例如,我们可以将字符串转换为二进制数组。以下是一种实现方法:

byte = bin(int(wm_content.encode('utf-8').hex(), base=16))[2:]
self.wm_bit = (np.array(list(byte)) == '1')

在上面的代码中,首先将字符串类型的水印信息 wm_content 转换为十六进制格式,然后使用 int() 函数将其转换为整数。接着,使用 bin() 函数将整数转换为二进制格式,并去掉前缀 ‘0b’,得到一个字符串类型的二进制数。最后,使用 np.array() 函数将字符串转换为一个由 ‘0’ 和 ‘1’ 组成的数组,并将其与 ‘1’ 做比较,得到一个布尔类型的数组 self.wm_bit,其中 True 表示对应位置为 ‘1’,False 表示对应位置为 ‘0’。

除了上述方法外,如果水印信息由 ASCII 码组成,也可以直接将每个字符转换为 8 位的二进制字符串,然后将这些字符串拼接成一个二进制数组。这个过程可以使用 Python 内置的 bin() 函数和字符串操作来实现。

2.1.2 图像预处理

读取图片后,我们进行图像预处理,包括:

  1. 如果图片包含 Alpha 通道,那么忽略它,只保留 RGB 颜色通道
  2. 如果图片的大小不是偶数的,那么对图片进行填充,使其大小为偶数。做这一步是因为后续需要对图片进行分块处理,块大小是 4x4,如果图片大小不是偶数的话,处理起来比较麻烦
  3. 将 RGB 转换为 YUV444 格式

2.1.3 YUV 数据进行小波变换(DTW)

得到 YUV 数据后,接着对各通道进行二维小波变换。

for channel in range(3):
   self.ca[channel], self.hvd[channel] = dwt2(self.img_YUV[:, :, channel], 'haar')

对于小波变换,具体原理我们暂时不进行深究,只需要了解:

  1. 在二维离散小波变换中,将图像分解为四个子图像,分别表示原图像的近似部分(Approximation,简称 cA)和水平(Horizontal,简称 cH)、垂直(Vertical,简称 cV)以及对角线(Diagonal,简称 cD)方向的细节部分。具体地,cA 表示原图像的低频部分,即近似部分,包含了图像的大部分能量;cH、cV 和 cD 分别表示原图像在水平、垂直和对角线方向上的高频部分,包含了图像的细节信息。这些子图像可以通过多级小波分解得到,其中每一级分解都将近似部分进一步分解为更低频的近似部分和更高频的细节部分。
  2. 如果输入图像为 M x N,那么四个子图像大小为 M/2 * N/2。
  3. 通过二维离散小波逆变换,将四个子图像恢复为原来图像。

2.1.4 在 cA 图像中分块嵌入数据

YUV 通道进行小波变换后得到 cA 子图像,接着对 cA 矩阵进行分块处理(block),块大小为 4x4。对于每个 block,我们嵌入一个 bit 的信息。

在 blind_watermark 中,提供了两种嵌入 bit 的算法:

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)))

block_add_wm_fast 更简单,我们做一个简短的说明:

  1. 首先对 block 进行 DCT(离散余弦变换),block 大小为 4x4,DCT 输出大小也是 4x4。
  2. 对 DCT 输出矩阵进行奇异值分解(SVD),返回值 S 中包含奇异值。通过 (s[0] // self.d1 + 1 / 4 + 1 / 2 * wm_1) * self.d1 将 1bit 数据嵌入第一个奇异值中。
  3. 嵌入数据后,恢复 block。 将 SVD 分解后的矩阵重新构造为原始矩阵。 接着,使用 idct() 函数对这个矩阵进行逆离散余弦变换(IDCT),得到重构后的矩阵。

重复上述过程,将每一个 bit 都嵌入到对应 block 中,则完成了数据嵌入,伪代码如下:

# Y_ca,U_ca 和 V_ca 即 YUV 经过小波变换后得到的 ca 矩阵
for ca in (Y_ca, U_ca, V_ca):
	i = 0
	# 遍历每一个 block
	for block_index in range(0, ca.get_block_count()):
		block = ca.getBlock(block_index) # 获取 block 数据
		bit_data = wm_bit[i % wm_size] # 获取 1 bit数据
		embeded_block = block_add_wm(block, bit_data) # 在当前 block 上嵌入 1 bit 数据
		ca.setBlock(block_index, embeded_block) # 将嵌入后的 block 写入 ca 矩阵中

根据这个嵌入的逻辑,我们可以计算下一张图片最多能够嵌入多少 bit 数据:

  1. 假设输入图片大小为 W x H,图片矩阵则是 H x W,转换为 YUV 后其矩阵大小仍然是 H x W
  2. 进行小波变换后,ca 矩阵大小是输入矩阵的一半,即 (H/2) x (W/2)
  3. 假设 block 大小为 (4, 4),那么 ca 矩阵上一共有 ( H / 2 ∗ W / 2 ) 4 ∗ 4 = H ∗ W 64 \frac{(H/2 * W/2)}{4*4} = \frac{H*W}{64} 44(H/2W/2)=64HW 个 block;每个 block 嵌入 1 bit 数据,那么最多能够嵌入 H ∗ W 64 \frac{H*W}{64} 64HW 个 bit
  4. 以 512 x 512 图片大小为例,最多嵌入 4096 个 bit,也就是一张 64x64 的黑白图片,或者 512 个 ASCII 字符

2.1.5 恢复为 RGB 数据

在 ca 矩阵上嵌入数据后,通过逆小波变换转换为 YUV 数据,接着 YUV 数据可以转为 RGB 以便保存为图片文件,或者用于其他处理,伪代码如下:

for channel in range(3):
	embed_YUV[channel] = idwt2(embed_ca[channel], hvd[channel])

embed_img = yuv2rgb(embed_YUV)
save_image("embed.jpg", embed_img)

至此,我们完成了盲水印的嵌入。

2.1.6 乱序

值得注意的是,我在这里省略了blind_watermark中关于password参数的描述。这个参数用于设置一个随机种子,以便在嵌入过程中打乱嵌入数据的顺序。我认为这是一个可选的部分,对于不熟悉该算法的人来说,这部分代码可能会让人感到困惑。因此,我想先把其他核心流程讲清楚,而password的乱序功能只是对整个过程的一个补充。

2.2 水印提取

水印的提取过程是嵌入的逆过程。整体流程如下图,对各流程做详细的解释。
在这里插入图片描述

2.2.1 图片数据预处理

与嵌入流程类似,读取图片进行预处理:

  1. RGB 转到 YUV
  2. 对 YUV 数据进行二维小波变换,得到四个子图像

2.2.2 在 cA 图像分块中提取数据

接着对 cA 矩阵进行分块处理(block),块大小为 4x4。对于每个 block,我们提取一个 bit 的信息。提取的算法为嵌入的逆过程:

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

2.2.3 取平均

在嵌入过程中,我们实际上是在冗余地嵌入数据。这包括在YUV的三个通道中嵌入相同的数据,以及在数据长度较短的情况下,例如只有4个比特,我们会重复地将这4个比特写入到块中。这种冗余性可以增强盲水印的抵抗攻击能力。因此,在提取数据时,我们可以采取取平均值的方法来获取实际的数据。伪代码如下:

# 从 YUV 数据中,提取 01 数组
for channel in range(0):
	wm_block_bit[channel] = get_wm_block_bit(embed_YUV[channel])

# wm_block_bit 是一个 3xN 的数组,其中 N 为 bit 的个数
# 我们对它取平均,得到 N 个数据
wm_block_channel_avg = np.average(wm_block_bit, axis=0)

# 此时 wm_block_channel_avg 长度为 N,假设 wm_bit_size = m
# 对于循环嵌入的 bit 数据取平均
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()

# 得到 wm_avg 是一个长度为 m 的数组

2.2.4 Kmeans 聚类

wm_avg 是一个浮点数组,如果你要完全地恢复数据到 01 的状态,那么需要做进一步计算。blind_watermark 作者给出的方法是使用 kmeans 进行二分类。本人实测这种方法确实比较准确。

但如果你嵌入的是图片,那么可以不用做 kmeans,因为浮点值可以对应亮度。

三、其他问题

3.1 关于水印长度的问题

可以看到在提取水印时,我们需要知道水印的长度,以便对 01 数组去平均值。这就带来一个问题:在提取水印时,你无法知道该图片水印的长度。例如 A 图片嵌入 4 bit 数据,B 图片嵌入了 5 bit 数据,当你要提取 A 和 B 图片暗水印时,要如何处理?

我们可以固定水印长度,例如约定都是 64 个 bit,如果水印数据超过了这个长度,那么对不起,算法没法嵌入;如果不足 64 bit,那么剩余 bit 全部置为 0 即可。

3.2 关于抗攻击的说明

在 example_str 中,作者展示了 blind_watermark 抗攻击的能力,包括截屏、缩放等等。

但需要说明的是,这里的测试是有要求的:无论做了哪种攻击,在提取水印前你需要将图片位置正确的排放。例如:

  • 缩放攻击。假设图片从 512 * 512 缩放到 256 * 256,那么首先将图片恢复至 512 * 512,再送给算法
  • 截屏攻击。从 (x, y) 点截取 200*200 的图片,你不能直接用着 200 * 200 的数据送给提取水印的算法,而是创建一个与原图一致的矩阵,将 200 * 200 的数据填充回 (x, y) 点,矩阵其他地方数据可以为 0。完成了这项操作后,再把数据输入到算法

之所以有这种要求,算法在嵌入和提取的过程中,隐式地依赖了 block 的位置信息。在实际使用中,上述条件比较难满足,因为你不知道用户会对图片做什么操作。这也是盲水印算法的某种局限和难点。

四、关于 C/C++ 算法实现

你可以在 cpp_blind_watermark 仓库中看到对应 C/C++ 版本,它的依赖项目有:

  1. opencv,提供了处理图片、矩阵操作、离散余弦变换等能力
  2. wavelib,提供了小波变换能力

在 main.cpp 中有具体的代码示例;在 tests 下有各个模块的单元测试,如果你对某个接口不了解,可以在这里找到一些信息。

参考

  • blind_watermark
  • cpp_blind_watermark

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/400506.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

qt - 19种精美软件样式

qt - 19种精美软件样式 一、效果演示二、核心程序三、下载链接 一、效果演示 二、核心程序 #include "mainwindow.h"#include <QtAdvancedStylesheet.h> #include <QmlStyleUrlInterceptor.h>#include "ui_mainwindow.h" #include <QDir&g…

提高工作效率,体验ONLYOFFICE办公软件

ONLYOFFICE办公软件 一、前言二、特点完整办公套件协作过程更容易 三、访问地址 一、前言 随着数字化办公的普及&#xff0c;办公软件在我们的工作中扮演着越来越重要的角色。为了提高工作效率&#xff0c;我们需要一个功能强大、易于使用的办公软件。ONLYOFFICE作为一款全功能…

如何通过SQL语句获取表/视图的DDL,表/列/索引的统计信息,查询的执行计划(MySQL)

文章目录 获取对象的定义SQL语句列出库中的表和视图表的DDL语句索引的DDL语句视图的DDL语句物化视图的DDL语句 获取统计信息的SQL语句表级统计信息索引统计信息列级统计信息 获取执行计划的Explain语句ExplainExplain JsonExplain Tree (8.0.16及以上)Explain Analyze (8.0.18及…

Java语言实现五子棋

目录 内容 题目 解题 代码 实现 内容 题目 五子棋 使用二维数组,实现五子棋功能. 1.使用二维数组存储五子棋棋盘 如下图 2.在控制台通过Scanner输入黑白棋坐标(例如:1,2 2,1格式 表示二维数组坐标),使用实心五角星和空心五角星表示黑白棋子. 如下图: 输入后重新输出…

解决方案——文本生成图像DF-GAN配置Oxford-102 Flower 花数据集全流程

目录 一、Oxford-102 Flower简介二、DF-GAN配置Oxford-102 Flower 数据集2.1、下载数据集2.2、配置数据集 三、修改代码四、资源下载 一、Oxford-102 Flower简介 Oxford-102 Flower是牛津工程大学于2008年发布的用于图像分类的花卉数据集&#xff0c;原论文链接&#xff1a;Au…

适配器模式:转换接口,无缝对接不同系统

文章目录 **一、技术背景与应用场景****为什么使用适配器模式&#xff1f;****典型应用场景包括但不限于&#xff1a;** **二、适配器模式定义与结构****三、使用步骤举例****四、优缺点分析****总结** 一、技术背景与应用场景 适配器模式在软件设计中扮演着桥梁角色&#xff…

.zip文件如何在centos7解压

在CentOS 7中解压.zip文件&#xff0c;您可以使用unzip命令。如果您的系统上没有安装unzip工具&#xff0c;您首先需要安装它。以下是步骤指南&#xff1a; 安装unzip工具 打开终端并运行以下命令来安装unzip&#xff1a; sudo yum install unzip解压.zip文件 安装unzip之后&am…

2023年12月 Python(六级)真题解析#中国电子学会#全国青少年软件编程等级考试

Python等级考试(1~6级)全部真题・点这里 一、单选题(共25题,共50分) 第1题 运行以下程序,输出的结果是?( ) class A():def __init__(self,x):self.x=x

leetcode hot100组合综合四

本题中&#xff0c;是要求nums中求的总和为target的排列数&#xff0c;因为题中说了&#xff0c;元素顺序不同&#xff0c;则可以视为不同的结果之一。 所以&#xff0c;根据对背包问题的总结&#xff0c;本题中元素可以重复使用&#xff0c;是完全背包并且需要求排列数&#…

多线程——threading和queue模块的理解。加实例+详解+思路

并发&#xff1a;假的多任务 并行&#xff1a;真的多任务 实现多线程用——threading模块 import threading import timedef shuru():for i in range(1,4):print("正在输入")time.sleep(1) def shuchu():for i in range(1,4):print("正在输出")time.sle…

通俗易懂的双亲委派机制

当你超过别人一点点&#xff0c;别人会嫉妒你&#xff1b;当你超过别人一大截&#xff0c;别人就会羡慕你 据说给我点关注的都成了大佬&#xff0c;点关注的我都会私发一份好东西 ​​​​你得先知道 在介绍双亲委派机制的时候&#xff0c;不得不提ClassLoader&#xff08;类…

Windows / Linux dir 命令

Windows / Linux dir 命令 1. dir2. dir *.* > data.txt3. dir - list directory contentsReferences 1. dir 显示目录的文件和子目录的列表。 Microsoft Windows [版本 10.0.18363.900] (c) 2019 Microsoft Corporation。保留所有权利。C:\Users\cheng>dir驱动器 C 中…

xxl-job架构原理讲解

1、调度中心 调度中心是一个单独的Web服务&#xff0c;主要是用来触发定时任务的执行 它提供了一些页面操作&#xff0c;我们可以很方便地去管理这些定时任务的触发逻辑 调度中心依赖数据库&#xff0c;所以数据都是存在数据库中的 调度中心也支持集群模式&#xff0c;但是…

mybatis数据操作语句

//基于注解 Mapper public interface reboudapt {Select("select * from dept")List<dept> huoqudept();//删除语句Delete("delete from dept where id #{id}")void deletesc(Integer id);//增加语句Insert("insert into dept(name, create_t…

【flutter】第一个flutter项目

前言 我们通过Android Studio来创建flutter项目。 安装dart和flutter插件 新版编译器需要先安装flutter插件才能构建flutter项目。 项目目录 我们基本就在lib中写代码 项目启动

PiflowX-组件UnionAll

UnionAll组件 组件说明 Union多个输入源。输入源必须具有相同的字段类型。 计算引擎 flink 组件分组 common 端口 Inport&#xff1a;Any outport&#xff1a;默认端口 组件属性 名称展示名称默认值允许值是否必填描述例子inportsInports“”无否Inports string are…

C# 使用onnxruntime部署夜间雾霾图像的可见度增强

目录 介绍 模型信息 效果 项目 代码 下载 C# Onnx 使用onnxruntime部署夜间雾霾图像的可见度增强 介绍 github地址&#xff1a;GitHub - jinyeying/nighttime_dehaze: [ACMMM2023] "Enhancing Visibility in Nighttime Haze Images Using Guided APSF and Gradien…

机器视觉系统选型-为什么还要选用工业光源控制器

工业光源控制器最主要的用途是给光源供电&#xff0c;实现光源的正常工作。 1.开关电源启动时&#xff0c;电压是具有波浪的不稳定电压&#xff0c;其瞬间峰值电压超过了LED灯的耐压值&#xff0c;灯珠在多次高压冲击下严重降低了使用寿命&#xff1b; 2.使用专用的光源控制器&…

day2:信号与槽

思维导图 使用手动连接&#xff0c;将登录框中的取消按钮使用t4版本的连接到自定义的槽函数中&#xff0c;在自定义的槽函数中调用关闭函数 将登录按钮使用qt5版本的连接到自定义的槽函数中&#xff0c;在槽函数中判断u界面上输入的账号是否为"123",密码是否为"…

【Java系列】JDK

目录 JDK介绍JDK版本系列文章版本记录JDK介绍 JDK是 Java 语言的软件开发工具包,主要用于移动设备、嵌入式设备上的java应用程序。JDK是整个java开发的核心,它包含了JAVA的运行环境(JVM+Java系统类库)和JAVA工具。 JDK版本 SE(JavaSE),standard edition,标准版,是我们通…