赞
踩
由于工作需要,最近学习了盲水印相关的知识,本文对学习过程中做一个整理和总结。主要内容包括:
盲水印(blind watermark)算法是一种将数字水印嵌入到数字媒体中的技术,而不需要原始媒体文件。与传统的数字水印技术不同,盲水印算法不需要原始媒体文件来提取数字水印,因此更加安全和隐私保护。
盲水印算法的基本原理是将数字水印嵌入到数字媒体的频域或空域中,使得数字水印能够在不影响原始媒体质量的情况下被提取出来。盲水印算法通常包括两个主要步骤:嵌入和提取。
在嵌入阶段,数字水印被嵌入到数字媒体中。这通常涉及到将数字水印转换为频域或空域信号,并将其嵌入到数字媒体中。嵌入过程需要考虑数字水印的鲁棒性和不可见性,以确保数字水印能够在不影响原始媒体质量的情况下被提取出来。
在提取阶段,数字水印被提取出来。这通常涉及到对数字媒体进行一些处理,以提取数字水印。提取过程需要考虑数字水印的鲁棒性和准确性,以确保数字水印能够被正确地提取出来。
盲水印算法在数字版权保护、数字身份认证和数字隐私保护等领域具有广泛的应用。它可以帮助数字内容提供商保护其版权,防止盗版和侵权行为;也可以帮助用户保护其数字身份和隐私,防止个人信息被泄露。
本章分析 blind_watermark 中算法逻辑
整体流程如下图,接下来对各个阶段做详细的解释。
水印可以是各种形式的信息,包括字符串、图片等。为了嵌入这些信息,我们首先需要将水印转换为二进制形式,即由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() 函数和字符串操作来实现。
读取图片后,我们进行图像预处理,包括:
得到 YUV 数据后,接着对各通道进行二维小波变换。
for channel in range(3):
self.ca[channel], self.hvd[channel] = dwt2(self.img_YUV[:, :, channel], 'haar')
对于小波变换,具体原理我们暂时不进行深究,只需要了解:
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
更简单,我们做一个简短的说明:
(s[0] // self.d1 + 1 / 4 + 1 / 2 * wm_1) * self.d1
将 1bit 数据嵌入第一个奇异值中。重复上述过程,将每一个 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 数据:
在 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)
至此,我们完成了盲水印的嵌入。
值得注意的是,我在这里省略了blind_watermark中关于password参数的描述。这个参数用于设置一个随机种子,以便在嵌入过程中打乱嵌入数据的顺序。我认为这是一个可选的部分,对于不熟悉该算法的人来说,这部分代码可能会让人感到困惑。因此,我想先把其他核心流程讲清楚,而password的乱序功能只是对整个过程的一个补充。
水印的提取过程是嵌入的逆过程。整体流程如下图,对各流程做详细的解释。
与嵌入流程类似,读取图片进行预处理:
接着对 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
在嵌入过程中,我们实际上是在冗余地嵌入数据。这包括在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 的数组
wm_avg 是一个浮点数组,如果你要完全地恢复数据到 01 的状态,那么需要做进一步计算。blind_watermark 作者给出的方法是使用 kmeans 进行二分类。本人实测这种方法确实比较准确。
但如果你嵌入的是图片,那么可以不用做 kmeans,因为浮点值可以对应亮度。
可以看到在提取水印时,我们需要知道水印的长度,以便对 01 数组去平均值。这就带来一个问题:在提取水印时,你无法知道该图片水印的长度。例如 A 图片嵌入 4 bit 数据,B 图片嵌入了 5 bit 数据,当你要提取 A 和 B 图片暗水印时,要如何处理?
我们可以固定水印长度,例如约定都是 64 个 bit,如果水印数据超过了这个长度,那么对不起,算法没法嵌入;如果不足 64 bit,那么剩余 bit 全部置为 0 即可。
在 example_str 中,作者展示了 blind_watermark 抗攻击的能力,包括截屏、缩放等等。
但需要说明的是,这里的测试是有要求的:无论做了哪种攻击,在提取水印前你需要将图片位置正确的排放。例如:
之所以有这种要求,算法在嵌入和提取的过程中,隐式地依赖了 block 的位置信息。在实际使用中,上述条件比较难满足,因为你不知道用户会对图片做什么操作。这也是盲水印算法的某种局限和难点。
你可以在 cpp_blind_watermark 仓库中看到对应 C/C++ 版本,它的依赖项目有:
在 main.cpp 中有具体的代码示例;在 tests 下有各个模块的单元测试,如果你对某个接口不了解,可以在这里找到一些信息。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。