AIGC 的火爆引燃了数字水印,说实话数字水印并不是一项新的技术,但是这时候某些公司拿出来宣传一下特别应景,相应股票蹭蹭地涨。数字水印是什么呢,顾名思义,和我们在pdf中打的水印作用差不多,起到明确版权、防伪验真的作用。但是不同于传统肉眼可见的水印,数字水印也叫隐藏式水印,能够在人眼几乎无法察觉的情况下将水印信息秘密嵌入到音频、图像或视频中去,除了减少对画质的影响外,有个重要的功能就是保护著作权,使得盗版者无法感知水印存在,让版权的鉴定的溯源变得更轻松。
提到数字水印,有个经典案例经常被提到,阿里巴巴的一名员工擅网页截图外传,造成很大的恶劣影响,结果利用数字水印,很快就定位到这名员工,这名员工还奇怪,发的时候我还特意留意图片上没有水印,怎么就能定位到我呢,可见数字水印的强大,数字水印于无形中发挥着强大作用。
背后的科学奥秘
那么数字水印到底是如何实现的呢,那就不得不提到傅里叶变换,任何函数都可以写成正弦函数之和,用直白的话说就是任何二维空间的波形,都可以用简单的正弦和余弦波叠加而成,傅里叶变换交互式入门这篇文章详细的可视化地介绍了这一原理,并且我们可以随意画一条线看看,是不是可以由多条正弦波叠加而成。
我们拓展到三维空间,同样的道理,任何凹凸不平的面都可以用正弦平面波叠加而成,如下图所示,看一下大脑的图片,是一张灰度图,每个像素都有灰度值,我们加个坐标,Z轴为灰度值的大小,这样整张图就成为凹凸不平的曲面,那么这张凹凸不平的曲面就可以用多个正弦平面波叠加而成,所以在我们也可以用这些个正弦平面波来存储这张图,简而言之,我们能将图像用频域表示,接下来我们聊聊二维频率域K-SPACE,或者叫傅里叶空间。
对于正弦平面波,可以这样理解,在一个方向上存在一个正弦函数,在法线方向上将其拉伸。前面说过三个参数可以确定一个一维的正弦波。哪几个参数可以确定一个二维的正弦平面波呢?答案是四个,其中三个和一维的情况一样,即频率ω,幅度A,相位φ,但是具有相同这些参数的平面波却可以有不同的方向 n ⃗ \vec{n} n ,如下图所示,频率ω,幅度A,相位φ,都有一样,方向 n ⃗ \vec{n} n ,两个平面波叠加出来的效果。
类比一维中,幅度和相位可以用一个复数表示,它可以作为我们存储的内容。但是还有两个:一个频率一个方向。这时想到向量是有方向的,也是有长度的。所以我们用一个二维的矩阵的来保存分解之后得到的信息。这个矩阵就是K空间。就是说一个二维矩阵点 (μ ,ν) 代表这个平面波的法向量 n ⃗ \vec{n} n ,这个向量的模 μ 2 + ν 2 \sqrt{\mu^2+\nu^2} μ2+ν2代表这个平面波的频率ω ,这个点里面保存的内容复数就是此平面波的幅度和相位。
复数(complex number):形如a+bi(a、b均为实数)的数为复数,其中,a被称为实部,b被称为虚部,i为虚数单位。
K空间(K Space):也称傅里叶空间,k空间是寻常空间在傅利叶转换下的对偶空间。“K”代表什么,字母“k”在光学、声学、力学和电磁学领域已经使用了一个多世纪,k=1/ λ \lambda λ,其中 λ \lambda λ表示波长,因此,k是每单位距离的波数或周期数。
好了讲到这里,我们回归正题,数字水印到底怎么实现,以下就是基本流程,简单步骤如下:
- 原始图像A经过傅里叶变换得到K空间图像B。
- 将水印内容写到K空间图像B,得到叠加图像C。
- 对图像C进行傅里叶逆变换等到添加了数字水印的图像D,该图像D在视觉上和A没什么区别。
那么解密流程就很简单了,对D进行一次傅里叶变换就能得到图像C,视觉上就能看到水印了。
java+OpenCV实现
可以参考这篇文章opencv的java-maven-idea开发环境配置进行配置OpenCV的开发环境。我用的opencv的4.6.0版本,下面是数据盲水印的java代码实现,仅供学习参考:
package tools;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
import java.util.ArrayList;
import java.util.List;
public class ImgWatermarkUtil {
private static List<Mat> planes = new ArrayList<Mat>();
private static List<Mat> allPlanes = new ArrayList<Mat>();
public static Mat addImageWatermarkWithText(Mat image, String watermarkText){
Mat complexImage = new Mat();
Mat padded = splitSrc(image);
padded.convertTo(padded, CvType.CV_32F);
planes.add(padded);
planes.add(Mat.zeros(padded.size(), CvType.CV_32F));
Core.merge(planes, complexImage);
Core.dft(complexImage, complexImage);
Scalar scalar = new Scalar(0, 0, 0);
Point point = new Point(40, 40);
Imgproc.putText(complexImage, watermarkText, point, Imgproc.FONT_HERSHEY_DUPLEX, 1D, scalar);
Core.flip(complexImage, complexImage, -1);
Imgproc.putText(complexImage, watermarkText, point, Imgproc.FONT_HERSHEY_DUPLEX, 1D, scalar);
Core.flip(complexImage, complexImage, -1);
return antitransformImage(complexImage, allPlanes);
}
public static Mat getImageKSpace(Mat image){
Mat complexImage = new Mat();
Mat padded = splitSrc(image);
padded.convertTo(padded, CvType.CV_32F);
planes.add(padded);
planes.add(Mat.zeros(padded.size(), CvType.CV_32F));
Core.merge(planes, complexImage);
Core.dft(complexImage, complexImage);
Scalar scalar = new Scalar(0, 0, 0);
Point point = new Point(40, 40);
Mat magnitude = createOptimizedMagnitude(complexImage);
planes.clear();
return magnitude;
}
public static Mat getImageWatermarkWithText(Mat image){
List<Mat> planes = new ArrayList<Mat>();
Mat complexImage = new Mat();
Mat padded = splitSrc(image);
padded.convertTo(padded, CvType.CV_32F);
planes.add(padded);
planes.add(Mat.zeros(padded.size(), CvType.CV_32F));
Core.merge(planes, complexImage);
Core.dft(complexImage, complexImage);
Mat magnitude = createOptimizedMagnitude(complexImage);
planes.clear();
return magnitude;
}
private static Mat splitSrc(Mat mat) {
mat = optimizeImageDim(mat);
Core.split(mat, allPlanes);
Mat padded = new Mat();
if (allPlanes.size() > 1) {
for (int i = 0; i < allPlanes.size(); i++) {
if (i == 0) {
padded = allPlanes.get(i);
break;
}
}
} else {
padded = mat;
}
return padded;
}
private static Mat antitransformImage(Mat complexImage, List<Mat> allPlanes) {
Mat invDFT = new Mat();
Core.idft(complexImage, invDFT, Core.DFT_SCALE | Core.DFT_REAL_OUTPUT, 0);
Mat restoredImage = new Mat();
invDFT.convertTo(restoredImage, CvType.CV_8U);
if (allPlanes.size() == 0) {
allPlanes.add(restoredImage);
} else {
allPlanes.set(0, restoredImage);
}
Mat lastImage = new Mat();
Core.merge(allPlanes, lastImage);
return lastImage;
}
private static Mat optimizeImageDim(Mat image) {
Mat padded = new Mat();
int addPixelRows = Core.getOptimalDFTSize(image.rows());
int addPixelCols = Core.getOptimalDFTSize(image.cols());
Core.copyMakeBorder(image, padded, 0, addPixelRows - image.rows(), 0, addPixelCols - image.cols(),
Core.BORDER_CONSTANT, Scalar.all(0));
return padded;
}
private static Mat createOptimizedMagnitude(Mat complexImage) {
List<Mat> newPlanes = new ArrayList<Mat>();
Mat mag = new Mat();
Core.split(complexImage, newPlanes);
Core.magnitude(newPlanes.get(0), newPlanes.get(1), mag);
Core.add(Mat.ones(mag.size(), CvType.CV_32F), mag, mag);
Core.log(mag, mag);
shiftDFT(mag);
mag.convertTo(mag, CvType.CV_8UC1);
Core.normalize(mag, mag, 0, 255, Core.NORM_MINMAX, CvType.CV_8UC1);
return mag;
}
private static void shiftDFT(Mat image) {
image = image.submat(new Rect(0, 0, image.cols() & -2, image.rows() & -2));
int cx = image.cols() / 2;
int cy = image.rows() / 2;
Mat q0 = new Mat(image, new Rect(0, 0, cx, cy));
Mat q1 = new Mat(image, new Rect(cx, 0, cx, cy));
Mat q2 = new Mat(image, new Rect(0, cy, cx, cy));
Mat q3 = new Mat(image, new Rect(cx, cy, cx, cy));
Mat tmp = new Mat();
q0.copyTo(tmp);
q3.copyTo(q0);
tmp.copyTo(q3);
q1.copyTo(tmp);
q2.copyTo(q1);
tmp.copyTo(q2);
}
}
import org.opencv.imgcodecs.Imgcodecs;
import java.net.URL;
import static org.opencv.imgcodecs.Imgcodecs.imread;
import static org.opencv.imgcodecs.Imgcodecs.imwrite;
public class Main {
static{
loadDll();
}
public static void main(String[] args){
Mat img = imread("E:/software/opencv/Img.jpg");
Mat kSpaceImg = ImgWatermarkUtil.getImageKSpace(img);
Mat outImg = ImgWatermarkUtil.addImageWatermarkWithText(img,"zhulangfly");
imwrite("E:/software/opencv/Img-kSpaceImg.jpg",kSpaceImg);
imwrite("E:/software/opencv/Img-out.jpg",outImg);
Mat watermarkImg = ImgWatermarkUtil.getImageWatermarkWithText(outImg);
imwrite("E:/software/opencv/Img-watermark.jpg",watermarkImg);
}
public static void loadDll() {
System.setProperty("java.awt.headless", "false");
System.out.println(System.getProperty("java.library.path"));
URL url = ClassLoader.getSystemResource("dlls/opencv_java460.dll");
System.load(url.getPath());
}
}
<dependency>
<groupId>org.openpnp</groupId>
<artifactId>opencv</artifactId>
<version>4.6.0-0</version>
</dependency>
参考文献
- 数字水印技术在前端落地的思考
- 阿里巴巴公司根据截图查到泄露信息的具体员工的技术是什么?
- 通俗讲解:图像傅里叶变换
- 形象理解二维傅里叶变换
- 傅里叶变换交互式入门
- opencv的java-maven-idea开发环境配置
- Java使用OpenCV:基于DCT变换 实现 图片 数字 的盲水印添加和提取