摘要 :数字水印技术是信息隐藏学科的重要分支,通过将特定标识信息嵌入图像、音频、视频等媒体内容中,实现版权保护、防伪验真与秘密信息传递。本文以图像数字水印为核心,系统讲解其基本特性(可证明性、不可感知性、鲁棒性等)与最低有效位(LSB)原理,详细介绍水印的嵌入与提取过程,并基于 OpenCV 的
bitwise_and、bitwise_or位运算函数,给出完整的 C++ 代码实现,帮助读者从原理到实战掌握数字水印技术。
OpenC数字水印技术是信息隐藏学科的一个重要分支,它将特定的标识信息嵌入媒体内容(如图像、音频、视频等)中,用以证明版权所有,防止非法复制或传递秘密信息。这些嵌入的标识信息在不影响原始数据使用价值的前提下,可以是可见的或不可见的。数字水印的基本原理是将一个特定的信息或标识作为水印,通过一定的算法嵌入宿主媒体数据中。嵌入过程需要在不明显影响媒体内容质量的前提下进行,以确保水印信息的隐蔽性。提取水印时,需要一个特定的密钥或算法,用于识别和提取嵌入的水印数据。本章将通过OpenCV来实现数字水印。
1. 数字水印概述
近年来,生成式人工智能(Artificial Intelligence Generated Content,AIGC)的火爆引燃了数字水印(Digital Watermark)。数字水印的作用是什么呢?顾名思义,它和PDF中水印的作用差不多,都用于明确版权、防伪验真。
然而,不同于传统肉眼可见的水印,数字水印也叫隐藏式水印,能够在人眼几乎无法察觉的情况下将水印信息秘密嵌入音频、图像或视频中。若要复制有数字水印的信号,则所嵌入的信息也会一并被复制。数字水印除了减少对画质的影响外,还有个重要的功能就是保护著作权,使得盗版者无法感知水印存在,让版权鉴定的溯源变得更轻松。
数字水印可分为浮现式和隐藏式两种,前者是可被看见的水印(Visible Watermarking),其所包含的信息可在观看图片或视频时被看见。一般来说,浮现式的水印通常包含版权拥有者的名称或标志。
隐藏式的水印是以数字数据的方式加入音频、图片或视频中,但在一般的状况下无法被看见。隐藏式水印的重要应用之一是保护版权,期望能借此避免或阻止数字媒体未经授权的复制。隐写术(Steganography)也是数字水印的一种应用,双方可利用隐藏在数字信号中的信息进行沟通。数字照片中的注释数据能记录照片拍摄的时间、使用的光圈和快门,甚至是相机的厂牌等信息,这也是数字水印的应用之一。
1.1 数字水印的特点
数字水印是一种信息隐藏技术,它利用人体感官的限制,将数字信号,如图像、文
字、符号、数字等一切可以作为标记、标识的信息与原始数据(如图像、音频、视频数
据)紧密结合并隐藏其中,并在经历一些不破坏原数据价值的操作后也能保存下来。
一般地,数字水印应具有如下的基本特性:
(1)可证明性:水印应能为受到版权保护的信息产品的归属提供完全和可靠的证据。
(2)不可感知性:不可感知包含两方面的意思,一方面指视觉上的不可感知性(对
听觉也是同样的要求),即因嵌入水印而导致的图像变化对观察者的视觉系统来讲应该是
不可察觉的,最理想的情况是水印图像与原始图像在视觉上一模一样,这是绝大多数水印
算法所应达到的要求;另一方面水印用统计方法也是不能恢复的,比如对于大量的用同样
方法和水印处理过的信息产品,即使使用统计方法也无法提取水印或确定水印的存在。
(3)鲁棒性:鲁棒性即健壮性,它对水印而言极为重要。一个鲁棒性强的数字水印
应该能够承受大量的、不同的物理和几何失真,包括有意的(如恶意攻击)或无意的(如
图像压缩、滤波、扫描与复印、噪声污染、尺寸变化等)。易碎水印技术恰恰与之相反,
其鲁棒性很低,它所保护的信息的微小变化都会使得水印被破坏。
1.2 图像数字水印
图像数字水印是利用图像作品中普遍存在的冗余数据,在图像的空间域(或变换域),通过一定的算法来改变像素(或变换域系数)的位置或值,在不影响原作品的价值和使用的前提下,将一些版权标记信息(水印)嵌入图像作品中,并且不能被人类视觉系统觉察。只有专用的检测器或阅读器才能提取,以达到保护图像作品版权或完整性验证的的。
图像数字水印算法必须具有以下基本特性:
(1)透明性:图像水印的嵌入不能引起人类视觉系统的察觉,即不能使得被保护图像作品在质量上发生可感觉到的失真。
(2)鲁棒性:当被保护的图像作品经过某种改动或者攻击(如添加高斯噪声、JPEG压缩和添加椒盐噪声等)之后,嵌入的水印信息应保持一定的完整性,并能以一定的正确概率被检测到。
(3)安全性:图像水印应该难以被伪造或者加工。同时,未经授权的个体不得阅读和修改水印。理想情况是未经授权的客户将不能检测到产品中的水印。
(4)可证明性:水印必须能为图像作品的归属提供完全和可靠的证据。
1.3 数字水印原理
最低有效位(Least Significant Bit,LSB)指的是一个二进制数中的第0位(即最低位)。最低有效位信息隐藏指的是,将一个需要隐藏的二值图像信息嵌入载体图像的最低有效位,即将载体图像的最低有效位替换为当前需要隐藏的二值图像,从而实现将二值图像隐藏的的。由于二值图像处于载体图像的最低有效位上,所以对于载体图像的影响非常不明显,具有较高的隐蔽性。
这种信息隐藏也被称为数字水印,通过该方式可以实现信息隐藏、版权认证、身份认证等功能。例如,如果嵌入载体图像内的信息是秘密信息,就实现了信息隐藏;如果嵌入载体图像内的信息是版权信息,就能够实现版权认证;如果嵌入载体图像内的信息是身份信息,就可以实现数字签名;等等。
因此,被嵌入载体图像内的信息也被称为数字水印信息。数字水印信息可以是文本、视频、音频等多种形式,这里我们仅讨论数字水印信息是二值图像的情况。最低有效位水印的实现包含嵌入过程和提取过程,下面对具体的实现方法进行简单介绍。
1.4 嵌入过程
嵌入过程完成的操作是将数字水印信息嵌入载体图像内,其主要步骤如下:
1)载体图像预处理
读取原始载体图像,并获取载体图像的行数M和列数N。
2)建立提取矩阵
建立一个M×N大小、元素值均为254的提取矩阵(数组),用来提取载体图像的高7位。
3)保留载体图像的高7位,将最低位置零
为了实现该操作,需要将载体图像与元素值均为254的提取矩阵进行按位与运算。将一个值在0,255区间的像素值P与数值254进行按位与运算,则会将像素值P的最低有效位置0,只保留其高7位。
4)水印图像处理
有些情况下需要对水印进行简单处理。例如,当水印图像为8位灰度图的二值图像时,就需要将其转换为二进制二值图像,以方便将其嵌入载体图像的最低位。
5)嵌入水印
将原始载体图像进行"保留高7位、最低位置0"的操作后,我们得到一幅新的图像,将新图像与水印图像进行按位或运算,就能实现将水印信息嵌入原始载体图像内的果。
6)显示图像
完成上述处理后,分别显示原始载体图像、水印图像、含水印图像。
1.5 提取过程
提取过程将完成数字水印的提取,具体步骤如下:
1)含水印载体图像处理
读取包含水印的载体图像,获取含水印载体图像的大小M×N。
2)建立提取矩阵
定义一个与含水印载体图像等大小的值为1的矩阵(数组)作为提取矩阵。
3)提取水印信息
将含水印载体图像与提取矩阵进行按位与运算,提取水印信息。
4)计算去除水印后的载体图像
有时需要删除包含在水印载体图像内的水印信息。通过将含水印载体图像的最低有效位置零,即可实现删除水印信息。
5)显示图像
根据需要,分别显示提取出来的水印图像、删除了水印信息的载体图像。
2. OpenCV相关函数
用OpenCV来实现数字水印功能,需要使用一些位操作函数,我们需要先了解一下这些函数。
2.1 bitwise_and函数
bitwise_and函数是OpenCV中的位运算函数之一,用于对两幅二值图像进行按位与操作。具体来说,对于每个像素,将两幅输入图像相应位置的像素值分别进行按位与运算,输出的结果图像的对应像素值即为这两幅输入图像对应像素值的按位与结果。bitwise_and函数的语法如下:
cpp
void bitwise_and(InputArray src1, InputArray src2,
OutputArray dst, InputArray mask = noArray());
其中,src1和src2表示要进行按位与操作的两幅输入图像;mask是可选参数,如果指定了掩码,则只对掩码对应位置的像素进行按位与操作;dst表示按位与运算的结果。
【例14.1】创建空白图像进行按位与操作
cpp
#include "opencv2/opencv.hpp"
using namespace cv;
int main()
{
cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_ERROR);
//空白图像创建
Mat m1 = Mat::zeros(Size(256, 256), CV_8UC3);
Mat m2 = Mat::zeros(Size(256, 256), CV_8UC3);
//在图像内添加矩阵
rectangle(m1, Rect(100, 100, 80, 80), Scalar(255, 255, 0), -1, LINE_8, 0);
rectangle(m2, Rect(150, 150, 80, 80), Scalar(0, 255, 255), -1, LINE_8, 0);
imshow("m1", m1);
imshow("m2", m2);
Mat dst;
bitwise_and(m1, m2, dst);
//bitwise_or(m1, m2, dst);
//bitwise_xor(m1, m2, dst);
imshow("result", dst);
waitKey();
return 0;
}

【例14.2】对现成图像进行按位与操作
cpp
#include "opencv2/opencv.hpp"
using namespace cv;
int main()
{
cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_ERROR);
Mat dog, cat, img_and;
resize(imread("cat.png"), cat, Size(400, 360));
resize(imread("dog.png"), dog, Size(400, 360));
bitwise_and(cat, dog, img_and); //与运算 1 & 1 = 1, 其它为0
imshow("result", img_and);
waitKey(0);
return 0;
}

可以看出,与运算的结果是使图像变小,最后的图像也会偏暗。
2. bitwise_or函数
在OpenCV中进行或运算使用bitwise_or函数,其声明如下:
cpp
void bitwise_or(InputArray src1, InputArray src2, OutputArray dst, InputArray
mask = noArray());
其中输入参数src1和src2可为灰度图或彩色图,src1和src2大小需相同;输出参数dst存放或运算的结果,尺寸和类型与src保持一致;掩码mask可通俗理解为一个遮罩,只对mask设定的有效区域进行操作。
或运算0 | 0 = 0,其他情况为1。下面将猫和狗的图片进行或运算。
【例14.3】对现成图像进行按位或操作
cpp
#include "opencv2/opencv.hpp"
using namespace cv;
int main()
{
cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_ERROR);
Mat dog, cat, img_and;
resize(imread("cat.png"), cat, Size(400, 360));
resize(imread("dog.png"), dog, Size(400, 360));
bitwise_or(cat, dog, img_and); //或运算 1 & 1 = 1, 其它为0
imshow("result", img_and);
waitKey(0);
return 0;
}

可以看出,或运算的结果是使图像变大,最后的图像也偏亮。
3. 代码实现数字水印
前面讲解了数字水印的嵌入过程和提取过程,步骤比较清晰。本节将根据这些步骤,通过代码来实现数字水印的嵌入和提取。
【例14.4】实现数字水印的嵌入和提取

cpp
#include "opencv2/opencv.hpp"
using namespace cv;
int main()
{
Mat gray1H7,dst,getWatermark;
Mat src= imread("src.bmp",0);
// 读取水印图像
Mat watermark= imread("watermark.bmp",0);
//将水印图像内的值255处理为1,以方便嵌入,相当于把变成二值图像
for(int i=0;i<watermark.rows;i++)
{
for(int j=0;j<watermark.cols;j++)
{
if(watermark.at<uchar>(i,j)>0)
watermark.at<uchar>(i,j)=1;
}
}
//读取原始载体图像的行列
int r=src.rows;
int c=src.cols;
// ----------------------嵌入式过程------------------------
//生成元素值都是254的数组
Mat t254 = Mat::ones(r, c, CV_8UC1) * 254;
//获取girl图像的高七位
bitwise_and(src,t254,gray1H7);
//将watermark嵌入girlH7内
bitwise_or(gray1H7,watermark,dst);
// ----------------------提取过程--------------------------
// 生成元素值都是1的数组
Mat t1= Mat::ones(r, c, CV_8UC1);
//从目标载体图像内提取水印图像
bitwise_and(dst,t1,getWatermark);
//将水印图像内的值1处理为255,以方便显示
for(int i=0;i<getWatermark.rows;i++)
{
for(int j=0;j<getWatermark.cols;j++)
{
if(getWatermark.at<uchar>(i,j)>0)
getWatermark.at<uchar>(i,j)=255;
}
}
//---------显示--------
imshow("srcImg",src);
imshow("watermark",watermark*255); //当前watermark内最大值为1
imshow("dstImg",dst);
imshow("getWatermark",getWatermark);
waitKey();
destroyAllWindows();
waitKey(0);
return 0;
}
可以看出,上述代码是按照嵌入过程的步骤和提取过程的步骤来实现的。我们把一副水印图像(watermark.bmp)嵌入原始载体图像(src.bmp)中变为目标载体图像(也称含水印的载体图像)dst,然后从dst中提取出水印数据存于getWatermark中,最后显示4种图像。