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):
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
作者:PikapBai
出处:https://www.cnblogs.com/PikapBai/p/15875524.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
备注:转载请注明出处并附加链接
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具