基於離散傅里葉變換在頻域添加文字盲水印
主要使用的 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
