OpenCV入门教程03.13:DFT(离散傅利叶变换)

索引地址:系列索引

DFT

离散傅里叶变换(Discrete Fourier Transform,DFT)傅里叶分析方法是信号分析的最基本方法,傅里叶变换是傅里叶分析的核心,通过它把信号从时间域变换到频率域,进而研究信号的频谱结构和变化规律。

具体介绍:DFT

OpenCV提供的dft函数为:

1
void dft(InputArray src, OutputArray dst, int flags=0, int nonzeroRows=0);

参数解释:

  • InputArray src: 输入图像,可以是实数或虚数
  • OutputArray dst: 输出图像,其大小和类型取决于第三个参数flags
  • int flags = 0: 转换的标识符,有默认值0.其可取的值如下所示:
    • DFT_INVERSE: 用一维或二维逆变换取代默认的正向变换
    • DFT_SCALE: 缩放比例标识符,根据数据元素个数平均求出其缩放结果,如有N个元素,则输出结果以1/N缩放输出,常与DFT_INVERSE搭配使用。
    • DFT_ROWS: 对输入矩阵的每行进行正向或反向的傅里叶变换;此标识符可在处理多种适量的的时候用于减小资源的开销,这些处理常常是三维或高维变换等复杂操作。
    • DFT_COMPLEX_OUTPUT: 对一维或二维的实数数组进行正向变换,这样的结果虽然是复数阵列,但拥有复数的共轭对称性(CCS),可以以一个和原数组尺寸大小相同的实数数组进行填充,这是最快的选择也是函数默认的方法。你可能想要得到一个全尺寸的复数数组(像简单光谱分析等等),通过设置标志位可以使函数生成一个全尺寸的复数输出数组。
    • DFT_REAL_OUTPUT: 对一维二维复数数组进行逆向变换,这样的结果通常是一个尺寸相同的复数矩阵,但是如果输入矩阵有复数的共轭对称性(比如是一个带有DFT_COMPLEX_OUTPUT标识符的正变换结果),便会输出实数矩阵。
  • int nonzeroRows = 0: 当这个参数不为0,函数会假设只有输入数组(没有设置DFT_INVERSE)的第一行或第一个输出数组(设置了DFT_INVERSE)包含非零值。这样的话函数就可以对其他的行进行更高效的处理节省一些时间,这项技术尤其是在采用DFT计算矩阵卷积时非常有效。

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// 功能:代码 4-6 图像傅里叶变换
// 作者:朱伟 zhu1988wei@163.com
// 来源:《OpenCV图像处理编程实例》
// 博客:http://blog.csdn.net/zhuwei1988
// 更新:2016-8-1
// 说明:版权所有,引用或摘录请联系作者,并按照上面格式注明出处,谢谢。//
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace cv;
using namespace std;

cv::Mat DFT(cv::Mat srcImage) {
cv::Mat srcGray;
cvtColor(srcImage, srcGray, COLOR_RGB2GRAY);
// 将输入图像延扩到最佳的尺寸
int nRows = getOptimalDFTSize(srcGray.rows);
int nCols = getOptimalDFTSize(srcGray.cols);
cv::Mat resultImage;
// 把灰度图像放在左上角,在右边和下边扩展图像,
// 添加的像素初始化为0
copyMakeBorder(srcGray, resultImage, 0, nRows - srcGray.rows, 0, nCols - srcGray.cols,
BORDER_CONSTANT, Scalar::all(0));
// 为傅立叶变换的结果(实部和虚部)分配存储空间
cv::Mat planes[] = {cv::Mat_<float>(resultImage), cv::Mat::zeros(resultImage.size(), CV_32F)};
Mat completeI;
// 为延扩后的图像增添一个初始化为0的通道
merge(planes, 2, completeI);
// 进行离散傅立叶变换
dft(completeI, completeI);
// 将复数转换为幅度
split(completeI, planes);
magnitude(planes[ 0 ], planes[ 1 ], planes[ 0 ]);
cv::Mat dftResultImage = planes[ 0 ];
// 对数尺度(logarithmic scale)缩放
dftResultImage += 1;
log(dftResultImage, dftResultImage);
// 剪切和重分布幅度图象限
dftResultImage = dftResultImage(Rect(0, 0, srcGray.cols, srcGray.rows));
// 归一化图像
normalize(dftResultImage, dftResultImage, 0, 1, cv::NORM_MINMAX);
int cx = dftResultImage.cols / 2;
int cy = dftResultImage.rows / 2;
Mat tmp;
// Top-Left - 为每一个象限创建ROI
Mat q0(dftResultImage, Rect(0, 0, cx, cy));
// Top-Right
Mat q1(dftResultImage, Rect(cx, 0, cx, cy));
// Bottom-Left
Mat q2(dftResultImage, Rect(0, cy, cx, cy));
// Bottom-Right
Mat q3(dftResultImage, Rect(cx, cy, cx, cy));
// 交换象限 (Top-Left with Bottom-Right)
q0.copyTo(tmp);
q3.copyTo(q0);
tmp.copyTo(q3);
// 交换象限 (Top-Right with Bottom-Left)
q1.copyTo(tmp);
q2.copyTo(q1);
tmp.copyTo(q2);
return dftResultImage;
}
int main() {
cv::Mat srcImage = imread("lena.jpg");
if (srcImage.empty())
return -1;
imshow("srcImage", srcImage);
cv::Mat resultImage = DFT(srcImage);
imshow("resultImage", resultImage);
cv::waitKey(0);
return 0;
}

测试效果:

dft

盲水印

水印

普通的水印就是在原始图片上添加文字、图像来表示所有权。

但是这类水印可以通过打马赛克、图片截取的方式规避。

那么实现就简单一点,使用Qt读取图片,然后在图片上绘制文字即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//水印位置
QRect rect(_mPosX,_mPosY,_mPosW,_mPosH);

//水印内容
if(_mWatermarkCtx.isEmpty()){
return QImage();
}

QFont font;
font.setPointSize(_mPointSize);//字体大小
QPen pen;
pen.setColor(_mColor);

//如果待处理图片为空
if(_mImg.isNull()){
return QImage();
}

QImage tmpImg = _mImg;

QPainter painter(&tmpImg);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(pen);
painter.setFont(font);
if(_mFrameVisible){
painter.drawRect(rect);
}
painter.drawText(_mPosX,_mPosY,_mPosW,_mPosH,Qt::AlignCenter,_mWatermarkCtx);

return tmpImg;

使用类包装接口,动态修改字体大小、水印内容、水印位置、水印框

普通水印

带水印框效果(水印框没有什么用,只是用来矫正效果)

普通水印带指示框

盲水印

盲水印,即不可见水印,使用算法将水印信息嵌入图片中。

就算图片二次修改、截取、破坏,水印信息也可以完整恢复出来。

要求

水印要求在以下操作后还能恢复:

  • 涂抹、椒盐、模糊
  • 亮度
  • 对比度
  • 截取、横向裁剪、纵向裁剪
  • 旋转
  • 缩放
  • 格式转换、jpg压缩
  • 截图
  • 拍照

LSB法

LSB(Least Signifcant Bit,最低有效位)

1
2
3
4
5
6
7
8
11111110
11111111
11111111
11111110
11111110
11111110
11111110
11111111

最低有效位为01100001 = 0x61 = A

这样我们就把字母A藏到了图片数据里。

如果图片被截取、拍照,那么数据就丢失了。

DFT(离散傅里叶变换)

OpenCV入门教程044:DFT(离散傅利叶变换)中介绍过DFT。

原理是:

  • 将图片进行傅里叶变换,得到表示四向限灰度情况的灰度图
  • 在灰度图上添加文字、图片
  • 将灰度图变换为正常图

解析水印的原理就是

  • 将图片解析傅里叶变换,就可以得到灰度图,此灰度图上包含水印

添加水印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//为加快傅里叶变换的速度,对要处理的图片尺寸进行优化
cv::Mat BlindWatermark::optimizeImageDim(cv::Mat image)
{
cv::Mat padded;
//get the optimal rows size for dft
int addPixelRows = cv::getOptimalDFTSize(image.rows);
//get the optimal cols size for dft
int addPixelCols = cv::getOptimalDFTSize(image.cols);
//apply the optimal cols and rows size to the image
cv::copyMakeBorder(image,padded,0,addPixelRows - image.rows,0,addPixelCols - image.cols,cv::BORDER_CONSTANT,cv::Scalar::all(0));

return padded;
}

//图片通道分离
cv::Mat BlindWatermark::splitSrc(cv::Mat mat)
{
allPlanes.clear();
mat = optimizeImageDim(mat);
cv::split(mat,allPlanes);
cv::Mat padded;
if(allPlanes.size()>1){
padded = allPlanes.at(0);
}else{
padded = mat;
}
return padded;
}

//把DFT灰度图转换为普通图
cv::Mat BlindWatermark::antiTransformImage(cv::Mat complexImage, std::vector<cv::Mat> allPlanes)
{
cv::Mat invDFT;
cv::idft(complexImage,invDFT,cv::DFT_SCALE | cv::DFT_REAL_OUTPUT,0);
cv::Mat restoredImage;
invDFT.convertTo(restoredImage,CV_8U);
if(allPlanes.size()==0){
allPlanes.push_back(restoredImage);
}else{
auto iter = allPlanes.begin();
*iter = restoredImage;
}
cv::Mat lastImage;
cv::merge(allPlanes,lastImage);
allPlanes.clear();
planes.clear();//连续多次操作时防止奔溃

return lastImage;
}

//添加文字水印
cv::Mat BlindWatermark::addImageWatermarkWithText(cv::Mat image)
{
cv::Mat complexImage;
//优化图像尺寸
cv::Mat padded = splitSrc(image);
padded.convertTo(padded,CV_32F);
planes.push_back(padded);
planes.push_back(cv::Mat::zeros(padded.size(),CV_32F));

cv::merge(planes,complexImage);
//dft
cv::dft(complexImage,complexImage);
//添加文本水印
cv::putText(complexImage,_mWMCtx,_mPos,cv::FONT_HERSHEY_DUPLEX,1.0,_mColor,_mThickness);
cv::flip(complexImage,complexImage,-1);
cv::putText(complexImage,_mWMCtx,_mPos,cv::FONT_HERSHEY_DUPLEX,1.0,_mColor,_mThickness);
cv::flip(complexImage,complexImage,-1);

return antiTransformImage(complexImage,allPlanes);
}

结果和原图没什么变换。

解水印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//交换四个向限内容
void BlindWatermark::shiftDFT(cv::Mat &image)
{
image = cv::Mat(image,cv::Rect(0,0,image.cols & -2,image.rows & -2));
int cx = image.cols /2;
int cy = image.rows /2;

cv::Mat q0 = cv::Mat(image,cv::Rect(0,0,cx,cy));
cv::Mat q1 = cv::Mat(image,cv::Rect(cx,0,cx,cy));
cv::Mat q2 = cv::Mat(image,cv::Rect(0,cy,cx,cy));
cv::Mat q3 = cv::Mat(image,cv::Rect(cx,cy,cx,cy));

cv::Mat tmp;

q0.copyTo(tmp);
q3.copyTo(q0);
tmp.copyTo(q3);

q1.copyTo(tmp);
q2.copyTo(q1);
tmp.copyTo(q2);
}

cv::Mat BlindWatermark::createOptimizedMagnitude(cv::Mat complexImage)
{
std::vector<cv::Mat> newPlanes;
cv::Mat mag;

//split the comples image in two planes
cv::split(complexImage,newPlanes);
//compute the magnitude
cv::magnitude(newPlanes.at(0),newPlanes.at(1),mag);

//move to a logarithmic scale
cv::add(cv::Mat::ones(mag.size(),CV_32F),mag,mag);
cv::log(mag,mag);
//optionally reorder the 4 quadrants of the magnitude image
shiftDFT(mag);
// normalize the magnitude image for the visualization since both JavaFX
// and OpenCV need images with value between 0 and 255
// convert back to CV_8UC1
mag.convertTo(mag,CV_8UC1);
cv::normalize(mag,mag,0,255,cv::NORM_MINMAX,CV_8UC1);

return mag;
}

cv::Mat BlindWatermark::getImageWatermarkWithText(cv::Mat image)
{
std::vector<cv::Mat> planes;
cv::Mat complexImage;
cv::Mat padded = splitSrc(image);
padded.convertTo(padded, CV_32F);
planes.push_back(padded);
planes.push_back(cv::Mat::zeros(padded.size(), CV_32F));
cv::merge(planes, complexImage);
// dft
cv::dft(complexImage, complexImage);
cv::Mat magnitude = createOptimizedMagnitude(complexImage);
planes.clear();
return magnitude;
}

这一步最重要,要可以从添加水印的图片结果中提取出水印,否则就没有意义了

解水印

频域变换法

离散余弦变换(Discrete Cosine Transform,DCT)

离散小波变换(Discrete Wavelet Transform,DWT)

字形变换法

patchwork

基于神经网络的算法


OpenCV入门教程03.13:DFT(离散傅利叶变换)
https://blog.jackeylea.com/opencv/opencv-dft/
作者
JackeyLea
发布于
2020年9月13日
许可协议