Loading

Linux C++ 使用 OpenCV 实现盲水印

基于离散傅里叶变换在频域添加文字盲水印

主要使用的 OpenCV 函数为 cv::dft()cv::idft()

说明:名为 DFT(离散傅里叶变换),其实采用的是 FFT(快速傅里叶变换,一种快速计算 DFT 的方法)

1 开发环境

  • linux 版本:统信 UOS 1030(可以认为是特殊的 ubuntu)

  • Opencv 版本:4.1.1

  • 开发语言:C++

OpenCV 开发环境搭建请参考 Linux 下编译 OpenCV4

2 名词解释

2.1 盲水印

盲水印,也叫隐水印,意思是肉眼看不见的水印,需要对图片进行特殊处理,才能看到的水印

2.2 频域

描述信号在频率方面特性时用到的一种坐标系。在图像中就是图像灰度变化强烈的情况,图像的频率

2.3 空域

即空间域,我们日常所见的图像就是空域

2 原理

频域添加数字水印的方法,是指通过某种变换手段(傅里叶变换,离散余弦变换,小波变换等)将图像变换到频域(小波域),在频域对图像添加水印,再通过逆变换,将图像转换为空间域

可参考 从零开始的频域水印完全解析 - immenma - https://zhuanlan.zhihu.com/p/27632585

3 处理流程

3.1 添加水印

原图 -> 傅里叶变换并在频域上添加水印 -> 优化由 dft 操作产生的图像,使其能显示 -> 频域图 -> 傅里叶逆变换 -> 空间域含有隐水印的图片

3.2 水印提取

空间域含有隐水印的图片 -> 傅里叶变换 -> 优化由 dft 操作产生的图像,使其能显示 -> 频域图 -> 傅里叶逆变换 -> 原图

4 代码

cvUtil.h:

// opencv 工具类,用来实现盲水印

#ifndef CVUTIL_H
#define CVUTIL_H

#include <stdlib.h>
#include <string>
#include <vector>

#include <opencv2/core/utility.hpp>
#include <opencv2/video/tracking.hpp>
#include <opencv2/highgui.hpp>

using namespace std;

class CvUtil
{
    public:
        void enc(const string &filename);
        void dec(const string &filename);

    private:
        cv::Mat complexImage;   // 傅里叶变换结果,复数
        vector<cv::Mat> planes;
        vector<cv::Mat> allPlanes;

        cv::Mat optimizeImageDim(cv::Mat image);
        cv::Mat splitSrc(cv::Mat image);

        void addImageWatermarkWithText(cv::Mat image, string watermarkText);
        void getImageWatermarkWithText(cv::Mat image);

        void shiftDFT(cv::Mat &magnitudeImage);
        cv::Mat createOptimizedMagnitude(cv::Mat complexImage);

        cv::Mat antitransformImage(cv::Mat complexImage, vector<cv::Mat> allPlanes);   
};

#endif // CVUTIL_H

cvUtil.cpp

#include "cvUtil.h"

/*
 * 功能:
 *      为加快傅里叶变换的速度,优化图像尺寸
 * 参数:
 *      image:原图像
 * 返回值:
 *      cv::Mat:填充后的图像
 * 注意:
 *      该函数会导致生成的图像右边和下边有黑边,因为边界用 0 填充了
 */
cv::Mat CvUtil::optimizeImageDim(cv::Mat image) 
{
// 因为不想要黑边使图片好看,所以注释了
# if 0
    cv::Mat padded = cv::Mat();

    // 1 计算需要扩展的行数和列数
    int addPixelRows = cv::getOptimalDFTSize(image.rows);
    int addPixelCols = cv::getOptimalDFTSize(image.cols);

    // 2 扩展面积至最优,边界用 0 填充
    cv::copyMakeBorder(image, padded, 0, addPixelRows - image.rows, 0, addPixelCols - image.cols,
            cv::BORDER_CONSTANT, cv::Scalar::all(0));

    return padded;
#endif

#if 1
    return image;
#endif
}

/*
 * 功能:
 *      分离多通道获取 B 通道(因傅里叶变换只能处理单通道)
 * 参数:
 *      image:多通道原图像
 * 返回值:
 *      cv::Mat:B 通道的图像
 */ 
cv::Mat CvUtil::splitSrc(cv::Mat image) 
{
    // 清空 allPlanes
    if (!this->allPlanes.empty()) {
        this->allPlanes.clear();
    }

    // 优化图像尺寸
    cv::Mat optimizeImage = this->optimizeImageDim(image);

    // 分离多通道
    cv::split(optimizeImage, this->allPlanes);

    // 获取 B 通道
    cv::Mat padded = cv::Mat();
    if (this->allPlanes.size() > 1) {
        for (int i = 0; i < this->allPlanes.size(); i++) {
            if (i == 0) {
                padded = this->allPlanes[i];
                break;
            }
        }
    } 
    else {
        padded = image;
    }

    return padded;
}

/*
 * 功能:
 *     对图片进行傅里叶转换并在频域上添加文本
 * 参数:
 *      image:空间域图像
 *      watermarkText:水印文字
 * 返回值:
 *      无
 * 说明:
 *      对 complexImage 进行操作
 */ 
void CvUtil::addImageWatermarkWithText(cv::Mat image, string watermarkText)
{
    if (!this->planes.empty()) {
        this->planes.clear();
    }

    // ------------- DFT ------------------------
    // 1 将多通道分为单通道(因为读入的是彩色图)
    cv::Mat padded = this->splitSrc(image);
    padded.convertTo(padded, CV_32F);

    // 2 将单通道扩展至双通道,以接收 DFT 的复数结果
    this->planes.push_back(padded);
    this->planes.push_back(cv::Mat::zeros(padded.size(), CV_32F));
    // 将 planes 数组组合合并成一个多通道 Mat
    cv::merge(this->planes, this->complexImage);

    // 3 进行离散傅里叶变换
    cv::dft(this->complexImage, this->complexImage);
    // ------------- DFT ------------------------

    // 添加文本水印
    cv::Scalar scalar = cv::Scalar(0, 0, 0, 0);
    cv::Point point = cv::Point(40, 40);
    cv::putText(this->complexImage, watermarkText, point, cv::FONT_HERSHEY_DUPLEX, 2.0, scalar);
    cv::flip(this->complexImage, this->complexImage, -1);
    cv::putText(this->complexImage, watermarkText, point, cv::FONT_HERSHEY_DUPLEX, 2.0, scalar);
    cv::flip(this->complexImage, this->complexImage, -1);

    this->planes.clear();
}

/*
 * 功能:
 *      从含隐水印的图像中获取傅里叶变换结果
 * 参数:
 *      image:含隐水印的图像
 * 说明:
 *      对 this->complexImage 进行操作
 */
void CvUtil::getImageWatermarkWithText(cv::Mat image) 
{
    // planes 数组中存的通道数若开始不为空,需清空.
    if (!this->planes.empty()) {
        this->planes.clear();
    }

    // ------------- DFT ------------------------
    // 1 将多通道分为单通道(因为读入的是彩色图)
    cv::Mat padded = splitSrc(image);
    padded.convertTo(padded, CV_32F);

    // 2 将单通道扩展至双通道,以接收 DFT 的复数结果
    this->planes.push_back(padded);
    this->planes.push_back(cv::Mat::zeros(padded.size(), CV_32F));
    // 将 planes 合并成一个多通道 Mat
    cv::merge(this->planes, this->complexImage);

    // 3 进行离散傅里叶变换
    cv::dft(this->complexImage, this->complexImage);
    // ------------- DFT ------------------------

    this->planes.clear();
}

/*
 * 功能:
 *      剪切和重分布幅度图象限
 * 参数:
 *      image:幅度图
 * 返回值:
 *      无
 */
void CvUtil::shiftDFT(cv::Mat &magnitudeImage) 
{
    // 如果图像的尺寸是奇数的话对图像进行裁剪并重新排列(减去补充部分)
    magnitudeImage = magnitudeImage(cv::Rect(0, 0, magnitudeImage.cols & -2, magnitudeImage.rows & -2));

    // 重新排列图像的象限,使得图像的中心在象限的原点
    int cx = magnitudeImage.cols / 2;
    int cy = magnitudeImage.rows / 2;

    cv::Mat q0 = cv::Mat(magnitudeImage, cv::Rect(0, 0, cx, cy));    // 左上
    cv::Mat q1 = cv::Mat(magnitudeImage, cv::Rect(cx, 0, cx, cy));   // 右上
    cv::Mat q2 = cv::Mat(magnitudeImage, cv::Rect(0, cy, cx, cy));   // 左下
    cv::Mat q3 = cv::Mat(magnitudeImage, cv::Rect(cx, cy, cx, cy));  // 右下

    // 交换象限
    cv::Mat tmp = cv::Mat();

    // 左上与右下交换
    q0.copyTo(tmp);
    q3.copyTo(q0);
    tmp.copyTo(q3);

    // 右上与左下交换
    q1.copyTo(tmp);
    q2.copyTo(q1);
    tmp.copyTo(q2);
}

/*
 * 功能:
 *      优化由 dft 操作产生的图像,使其能显示
 * 参数:
 *      complexImage:傅里叶变换结果
 * 返回值:
 *      cv::Mat:转化的频域图
 */
cv::Mat CvUtil::createOptimizedMagnitude(cv::Mat complexImage) 
{
    vector<cv::Mat> newPlanes;

    // 1 将傅里叶变化结果即复数转换为幅值,转换到对数尺度,即 log(1+sqrt(Re(DFT(I))^2 + Im(DFT(I))^2)
    /* 将多通道数组分离成几个单通道数组,
     * newPlanes[0] = Re(DFT(I), newPlanes[1]=Im(DFT(I))
     * 即 newPlanes[0] 为实部, newPlanes[1] 为虚部
    */
    cv::split(complexImage, newPlanes);
    // 计算幅值矩阵
    cv::magnitude(newPlanes[0], newPlanes[1], newPlanes[0]);
    cv::Mat mag = newPlanes[0];
    mag += cv::Scalar::all(1);
    // 转换到对数尺度
    cv::log(mag, mag);

    // 2 剪切和重分布幅度图象限
    this->shiftDFT(mag);

    // 3 归一化,用 0 到 255 之间的浮点值将矩阵变换为可视化的图像格式
    mag.convertTo(mag, CV_8UC1);
    cv::normalize(mag, mag, 0, 255, cv::NORM_MINMAX, CV_8UC1);

    return mag;
}

/*
 * 功能:
 *     将频域的图转换为空间域
 * 参数:
 *      complexImage:频域图像
 *      allPlanes:所有通道的图像
 * 返回值:
 *      cv::Mat:空间域的图像
 */ 
cv::Mat CvUtil::antitransformImage(cv::Mat complexImage, vector<cv::Mat> allPlanes) 
{
    cv::Mat invDFT = cv::Mat();
    cv::idft(complexImage, invDFT, cv::DFT_SCALE | cv::DFT_REAL_OUTPUT, 0);
    
    cv::Mat restoredImage = cv::Mat();
    invDFT.convertTo(restoredImage, CV_8U);

    // 合并多通道
    allPlanes.erase(allPlanes.begin());
    allPlanes.insert(allPlanes.begin(), restoredImage);
    cv::Mat lastImage = cv::Mat();
    cv::merge(allPlanes, lastImage);

    planes.clear();

    return lastImage;
}

void CvUtil::enc(const string &filename)
{
    // 读取图片 
    cv::Mat img1 = cv::imread(filename, cv::IMREAD_COLOR);
    cv::imshow("原图", img1);

    // 加水印
    addImageWatermarkWithText(img1, "zyw");

    cv::Mat img2 = createOptimizedMagnitude(this->complexImage);
    cv::imshow("频域", img2);
    cv::imwrite("enc_img2.png", img2);

    // 注意该反傅里叶变换的图,需要用 .png 格式保存,如果用 jpg 会导致水印文字丢失
    cv::Mat img3 = antitransformImage(this->complexImage, this->allPlanes);
    cv::imshow("空间域", img3);
    cv::imwrite("enc_img3.png", img3);

    cv::waitKey(0);
    cv::destroyAllWindows();
}

void CvUtil::dec(const string &filename)
{
    // 读取图片 
    cv::Mat img1 = cv::imread(filename, cv::IMREAD_COLOR);
    cv::imshow("原图", img1);

    // 读取图片水印
    getImageWatermarkWithText(img1);

    cv::Mat img2 = createOptimizedMagnitude(this->complexImage);
    cv::imshow("频域", img2);
    cv::imwrite("dec_img2.png", img2);

    cv::Mat img3 = antitransformImage(this->complexImage, this->allPlanes);
    cv::imshow("空间域", img3);
    cv::imwrite("dec_img3.png", img3);

    cv::waitKey(0);
    cv::destroyAllWindows(); 
}

main.cpp

/*
 * 用法:
 * 为图片添加隐水印,或者获取隐水印
 * 
 * 编译命令:
 * g++ `pkg-config --cflags --libs opencv4` cvUtil.h cvUtil.cpp main.cpp -o out
 * 
 * 开发环境:
 * Linux + C++ + opencv 4.1.1
 */

#include "cvUtil.h"

int main(int argc, char* argv[])
{
    if (argc < 3)
    {
        printf("usage: %s enc/dec file_name\n", argv[0]);
    }
    else
    {
        if (strcmp(argv[1], "enc") == 0)
        {
            printf("read file %s\n", argv[2]);

            CvUtil cvUtil;
            cvUtil.enc(argv[2]);
        }
        else if (strcmp(argv[1], "dec") == 0)
        {
            printf("read file %s\n", argv[2]);

            CvUtil cvUtil;
            cvUtil.dec(argv[2]);
        }
    }

    return 1;
}

5 运行效果

5.1 编译

g++ `pkg-config --cflags --libs opencv4` cvUtil.h cvUtil.cpp main.cpp -o out

5.2 添加水印

命令:

./out enc pika.jpg

读取图片(pika.jpg):

频域(enc_img2.png):

加完水印的空间域(enc_img3.png):

5.3 水印提取

命令:

./out enc enc_img3.png

读取图片:

频域(dec_img2.png):

去掉水印的空间域(dec_img3.png):

6 参考资料

1、从零开始的频域水印完全解析 - immenma - https://zhuanlan.zhihu.com/p/27632585

2、【基于 Object-C 实现的】OpenCV-图像处理-频域手段添加盲水印 - Miaoz0070 - https://www.jianshu.com/p/62e52c4ab5c4

3、【基于 Java 实现的】Java使用OpenCV 基于离散傅里叶变换算法 实现图片盲水印添加 - 清晨先生2 - https://www.jianshu.com/p/341dc97801ee

4、【基于 C++ 实现,不足在于只支持单通道,即只能处理灰度图】OPENCV实现隐藏水印 - shennung - https://blog.csdn.net/xinchen1234/article/details/82761391

5、OpenCV离散傅里叶变换 - HeoLis - https://www.cnblogs.com/ishero/p/11136317.html

6、opencv学习(十五)之图像傅里叶变换dft - 梧桐栖鸦 - https://blog.csdn.net/keith_bb/article/details/53389819

posted @ 2022-02-10 15:07  她爱喝水  阅读(1682)  评论(3编辑  收藏  举报