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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM