前言
早在16年,阿里的内部泄密事件发生后,一位大神就已经对这种技术已经给出了很详细的解释,帖子中也给出了相关理论和matlab代码及对其水印的安全测评 :前几年“阿里月饼事件”阿里是怎么找到泄露者的,用的什么黑科技?(数字盲水印)
整个过程大概如下
打水印
先将原图片进行 离散傅里叶变换 到频域,加上水印后再通过离散傅里叶逆变换到空间域恢复图片
解水印
将打有水印的图片通过傅里叶变换到频域,提取出水印
本篇文章主要介绍 JAVA 结合OpenCV实现盲水印服务,并对其进行封装,供整个系统各个服务进行调用
搭建OpenCV开发环境,加载OpenCV动态库
环境:JDK1.8 + Maven3.x + IntelliJ IDEA 2018.2.5 + OpenCV2.4.13 + Windows
OpenCV2.4.13下载地址
安装OpenCV
其实安装程序做的也就是把Opencv内容解压到你所选择的目录下面而已
新建一个Maven项目 File --> Project Strcture --> Project Settings --> Libraries 点击+号 把opencv-2413.jar引入

添加 OpenCV动态库


点击 Apply

创建工具类 ImgWatermarkUtil.java


import org.opencv.core.*;import org.opencv.imgproc.Imgproc;import java.util.ArrayList;import java.util.List;/**
 * @author yangxiaohui
 * @Date: Create by 2018-10-25 19:14
 * @Description: 添加图片盲水印工具类
 */public class ImgWatermarkUtil {
    private static List<Mat> planes = new ArrayList<Mat>();
    private static List<Mat> allPlanes = new ArrayList<Mat>();
    /**
     * <pre>
     *     添加图片文字水印
     * <pre>
     * @author Yangxiaohui
     * @date 2018-10-25 19:16
     * @param image             图片对象
     * @param watermarkText     水印文字
     */
    public static Mat addImageWatermarkWithText(Mat image, String watermarkText){
        Mat complexImage = new Mat();
        //优化图像的尺寸
        //Mat padded = optimizeImageDim(image);
        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);
        // dft
        Core.dft(complexImage, complexImage);
        // 添加文本水印
        Scalar scalar = new Scalar(0, 0, 0);
        Point point = new Point(40, 40);
        Core.putText(complexImage, watermarkText, point, Core.FONT_HERSHEY_DUPLEX, 1D, scalar);
        Core.flip(complexImage, complexImage, -1);
        Core.putText(complexImage, watermarkText, point, Core.FONT_HERSHEY_DUPLEX, 1D, scalar);
        Core.flip(complexImage, complexImage, -1);
        return antitransformImage(complexImage, allPlanes);
    }
    /**
     * <pre>
     *     获取图片水印
     * <pre>
     * @author Yangxiaohui
     * @date 2018-10-25 19:58
     * @param image
     */
    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);
        // dft
        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;
    }
    /**
     * <pre>
     *     为加快傅里叶变换的速度,对要处理的图片尺寸进行优化
     * <pre>
     * @author Yangxiaohui
     * @date 2018-10-25 19:33
      * @param image
     * @return
     */
    private static Mat optimizeImageDim(Mat image) {
        Mat padded = new Mat();
        int addPixelRows = Core.getOptimalDFTSize(image.rows());
        int addPixelCols = Core.getOptimalDFTSize(image.cols());
        Imgproc.copyMakeBorder(image, padded, 0, addPixelRows - image.rows(), 0, addPixelCols - image.cols(),
                Imgproc.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.core.Core;import org.opencv.core.Mat;import static org.opencv.highgui.Highgui.imread;import static org.opencv.highgui.Highgui.imwrite;/**
 * @author yangxiaohui
 * @Date: Create by 2018-10-25 19:42
 * @Description:
 */public class Main {
    static{
        //加载opencv动态库
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
    }
    public static void main(String[] args){
        Mat img = imread("stzz.jpg");//加载图片
        Mat outImg = ImgWatermarkUtil.addImageWatermarkWithText(img,"testwatermark");
        imwrite("stzz-out.jpg",outImg);//保存加过水印的图片
        //读取图片水印
        Mat watermarkImg = ImgWatermarkUtil.getImageWatermarkWithText(outImg);
        imwrite("stzz-watermark.jpg",watermarkImg);//保存获取到的水印
    }}

加水印前:

加水印后:

读取的水印:

全部评论:
hf_911
15楼 06.08 14:54
代码是对保存前的图片对象进行解密水印的,修改后,不支持图片剪切后的水印文字解析,求教下
路旁的落叶
14楼 2019.07.18 11:54
想请教下针对图片做了裁剪跟尺寸缩放的有没什么办法可以还原?
后生的靓仔
13楼 2019.06.07 20:19
博主你好,请问为什么我运行你的代码,添加水印之后的图片左边和下边会出现黑线呢?
孤独的风_47b6
12楼 2019.05.17 15:27
大佬,这个是添加文字水印的,很好使用,谢谢。但有没有添加图片水印的呢?该如何做呢?
cyh_85e3
11楼 2019.04.25 17:53
imwrite("stzz-out.jpg",outImg);//保存加过水印的图片
遇到了一个奇怪的问题,以jpg格式保存图片在提取的时候没有水印;以png格式保存的时候可以提取水印。原图片是jpg格式
Fenix木凡
10楼 2019.04.24 18:59
就是想问一下,博主,就是我将你的代码中的这个部分,改成了以下的样子,为什么,这样提取不出水印呢?(其他地方都没有变换)
//Mat img = imread("G:\\stzz.jpg");//加载图片
//Mat outImg = ImgWatermarkUtil.addImageWatermarkWithText(img,"123456789");
//imwrite("G:\\stzz-out.jpg",outImg);//保存加过水印的图片
//读取图片水印
Mat outImg = imread("G:\\stzz-out.jpg");
Mat watermarkImg = ImgWatermarkUtil.getImageWatermarkWithText(outImg);
imwrite("G:\\stzz-watermark.jpg",watermarkImg);//保存获取到的水印
Fenix木凡
2019.04.24 19:00
补充一下,就是先进行你的代码,然后在改的
清晨先生2
2019.04.25 12:21
@Fenix木凡 用来读取水印的图片,通过代码加过水印吗
Fenix木凡
2019.04.25 19:00
@洛书瓛 通过代码,加过水印
Fenix木凡
2019.04.25 19:11
博主,我刚刚用了,那个 11楼的方法,将图片格式从 jpg 改到 png,发现就可以提取水印了,这是为什么呀?
嵌入水印(其他不变),然后在提取水印
Mat img = Highgui.imread("G:\\stzz.png");//加载图片
Mat outImg = ImgWatermarkUtil.addImageWatermarkWithText(img,"123456789");
imwrite("G:\\stzz-out.png",outImg);//保存加过水印的图片
//读取图片水印
//Mat outImg = imread("G:\\stzz-out.png");
Mat watermarkImg = ImgWatermarkUtil.getImageWatermarkWithText(outImg);
imwrite("G:\\stzz-watermark1.png",watermarkImg);//保存获取到的水印
提取水印(其他不变)
//Mat img = Highgui.imread("G:\\stzz.png");//加载图片
//Mat outImg = ImgWatermarkUtil.addImageWatermarkWithText(img,"123456789");
//imwrite("G:\\stzz-out.png",outImg);//保存加过水印的图片
//读取图片水印
Mat outImg = imread("G:\\stzz-out.png");
Mat watermarkImg = ImgWatermarkUtil.getImageWatermarkWithText(outImg);
imwrite("G:\\stzz-watermark1.png",watermarkImg);//保存获取到的水印
这是为什么吗?
后生的靓仔
2019.06.10 16:14
@Fenix木凡 我的也是这样,而且这样操作后图片会有黑边,有些图片添加水印后黑边还特别明显
Untilyou_429d
9楼 2019.03.28 10:20
再请教作者一个问题啊 , 水印怎么鉴伪了。就是有一张盲水印的图片,怎么提取出水印信息
清晨先生2
2019.03.28 17:41
这个上文代码里是有的:
//读取图片水印
Mat watermarkImg = ImgWatermarkUtil.getImageWatermarkWithText(outImg);
imwrite("stzz-watermark.jpg",watermarkImg);//保存获取到的水印
Leasonxxx
8楼 2019.03.13 15:40
为什么我生成的图片是全黑或者全灰的呢
Leasonxxx
2019.03.13 22:11
知道了,路径指向错了,发现即使没有找到原始图片,也会生成一张黑的图片
呀呀呸_c4df
7楼 2019.03.04 22:24
再请教作者一个问题啊,我试了剪切,水印是能解析的,但是压缩水印就查不到了,有什么办法能提高这个鲁棒性啊
清晨先生2
2019.03.08 19:58
就是图片被压缩后水印就提取不出来了吗
admin_9634
6楼 2019.03.04 15:38
对了,作者大大,我这里发现一个问题,如果重新去读取有盲水印的图片,会发现水印变得模糊了,作者大大知道什么原因吗?是Core需要在读取盲水印的时候设置吗?
admin_9634
5楼 2019.03.04 14:30
作者在吗,咨询一下,设置图片水印是修改了Scalar设置了颜色为红色,那怎么才能读到相同颜色的水印呢
呀呀呸_c4df
4楼 2019.02.26 17:06
作者在吗,咨询一下,怎么才能获取到水印里的文字呢,解析出来是个图片嘛
清晨先生2
2019.03.01 19:55
@呀呀呸_c4df 嗯嗯,解析出来是个图片文件
呀呀呸_c4df
2019.03.04 18:38
@洛书瓛 那请问下,我想做文字水印,就是解析出来也是个文字,有没有什么办法啊
清晨先生2
2019.03.08 19:57
@呀呀呸_c4df OpenCV本身好像不能解析图片文字 如果想解析出文字水印的话只能通过其他的第三方工具进行图片文字提取再输出了,google有个工具ocr你可以了解下
最爱的诺兰
3楼 2019.01.09 17:24
我是直接运行博主的代码,不过我添加水印后,生成的图片,是黑白的,不是彩色的呢
最爱的诺兰
2019.01.10 09:38
我明白什么问题了,是自己搞错了。我在传mat的时候,读取的图片Imgcodecs.imread的code用的是IMREAD_GRAYSCALE,所以返回的也是灰图,如果改成IMREAD_COLOR就ok了
蓅哖伊人为谁笑
2楼 2018.11.26 20:32
求源码 大佬,正好要使用 谢谢