OpenCV_Tutorials——CORE MODULE.THE CORE FUNCTIONALITY—— Discrete Fourier Transform
2.8 离散的傅立叶变换
目标
我们要寻找以下问题的答案:
1、什么是傅立叶变换,为什么我们要用这个?
2、在OpenCV中如何做到?
3、例如copyMakeBorder(),merge(),dft(),getOptimalDFGSize(),log()以及normalize()函数的用法。
源代码
你可以从这里下载或者从samples/cpp/tutorial_code/core/discrete_fourier_transform/discrete找到代码。
#include "opencv2/core/core.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
using namespace cv;
using namespace std;
static void help(char* progName)
{
cout << endl
<< "This program demonstrated the use of the discrete Fourier transform (DFT). " << endl
<< "The dft of an image is taken and it's power spectrum is displayed." << endl
<< "Usage:" << endl
<< progName << " [image_name -- default lena.jpg] " << endl << endl;
}
int main(int argc, char ** argv)
{
help(argv[0]);
const char* filename = argc >=2 ? argv[1] : "lena.jpg";
Mat I = imread(filename, CV_LOAD_IMAGE_GRAYSCALE);
if( I.empty())
return -1;
Mat padded; //expand input image to optimal size
int m = getOptimalDFTSize( I.rows );
int n = getOptimalDFTSize( I.cols ); // on the border add zero values
copyMakeBorder(I, padded, 0, m - I.rows, 0, n - I.cols, BORDER_CONSTANT, Scalar::all(0));
Mat planes[] = {Mat_<float>(padded), Mat::zeros(padded.size(), CV_32F)};
Mat complexI;
merge(planes, 2, complexI); // Add to the expanded another plane with zeros
dft(complexI, complexI); // this way the result may fit in the source matrix
// compute the magnitude and switch to logarithmic scale
// => log(1 + sqrt(Re(DFT(I))^2 + Im(DFT(I))^2))
split(complexI, planes); // planes[0] = Re(DFT(I), planes[1] = Im(DFT(I))
magnitude(planes[0], planes[1], planes[0]);// planes[0] = magnitude
Mat magI = planes[0];
magI += Scalar::all(1); // switch to logarithmic scale
log(magI, magI);
// crop the spectrum, if it has an odd number of rows or columns
magI = magI(Rect(0, 0, magI.cols & -2, magI.rows & -2));
// rearrange the quadrants of Fourier image so that the origin is at the image center
int cx = magI.cols/2;
int cy = magI.rows/2;
Mat q0(magI, Rect(0, 0, cx, cy)); // Top-Left - Create a ROI per quadrant
Mat q1(magI, Rect(cx, 0, cx, cy)); // Top-Right
Mat q2(magI, Rect(0, cy, cx, cy)); // Bottom-Left
Mat q3(magI, Rect(cx, cy, cx, cy)); // Bottom-Right
Mat tmp; // swap quadrants (Top-Left with Bottom-Right)
q0.copyTo(tmp);
q3.copyTo(q0);
tmp.copyTo(q3);
q1.copyTo(tmp); // swap quadrant (Top-Right with Bottom-Left)
q2.copyTo(q1);
tmp.copyTo(q2);
normalize(magI, magI, 0, 1, CV_MINMAX); // Transform the matrix with float values into a
// viewable image form (float between values 0 and 1).
imshow("Input Image" , I ); // Show the result
imshow("spectrum magnitude", magI);
waitKey();
return 0;
}
解释
傅里叶变换将图像拆分为组成它的正弦和余弦部分。换句话说,他将一幅图像从它的空间与转换为他的频度域。这个思想来源于任何函数可以无限接近于正弦和余弦函数之和。傅立叶变换就是这样的一个方法。二维图像在数学上的傅立叶变换就是:
f是图像在空间域的值,F是频率域的值。变换的结果是一个复数。显示这些可能要通过real格式的图像和一个复数图像或者是通过幅值图像以及相位图像。然而,整个图像处理算法只有在幅值图像中才有意思,因为这个图像包含所有的我们需要的图像几何结构的信息。
然而,如果你想要在图像中做一些类似于这种形式的修改并且你需要重新转换它,你就需要保留他们两个。
在这个示例中,我会给你展示如何计算和显示傅里叶变换产生的幅值图像。数字图像本身就是离散的。这就意味着他们可能从给定的域值中取值。例如在基本的灰度图像中,数值一般在0-255之间。因此傅里叶变换同样需要最终转换为离散的形式,也就是离散的傅里叶变换(DFT)。当你在需要从一个图像的几何点中决定图像的类型时,你会用到它。下面就是步骤(输入图像I是灰度的情况):
1、拉伸图像到好的(optimal)尺寸。DFT的性能是依据图像的尺寸。通过将图像的尺寸乘以数字2,3以及5来达到最快的效果。因此,为了达到最大性能,将边缘值垫衬到这样的一个数值也不是为一种好的办法。getOptimalDFTSize()函数返回最有尺寸并且我们可以而使用copyMakeBorder()函数来拉伸一个图像的边缘:
Mat padded;
int m=getOptimalDFRSize(I.rows);
Int n=getOptimalDFTSize(I.cols);
copyMakeBorder(I,Padded,0,m-I.rows,0,n-I.cols,BORDER_CONSTANT,Scalar::all(0));
附加的像素的数值被初始化为0。
2、为复数和真值腾出空间。傅立叶变换的结果是复数。这就表明对于每一个ie图像的结果的数值也就是两个图像的值(一个组成部分就是一个)。此外频率域的范围远远大于对应的空间(域)。因此我们经常最起码使用float类型来存储它们。因此,我们将我们的输入图像转换为这个类型的并且使用另外的通道来存储复数。
Mat planes[]={Mat_<float>(padded),Mat::zeros(padded.size(),CV_32F};
Mat complexI;
Merge(planes,2,complesI);
3、进行离散傅立叶变换。原地(in-place 输入和输出是一样的)执行计算是可能的:
dft(complexI,complexI);
4、将真值和复数部分转换为幅值图像。一个复数由真值(Re)和复数(imaginary-Im)部分组成。DFT的结果是复数。幅值图像的DFT:
使用OpenCV代码表示:
split(complexI,planes);
magnitude(planes[0],planes[1],planes[0]);
Mat magI=planes[0];
5、选择一个对数范围。傅里叶变换得出来的动态范围的系数大到不能够在屏幕中显示。我们无法在这样的数值中观察到小的以及一些高的变化值。因此高的数值将会被转化为白点,低的数值就是黑点。为了使用灰度数值来可视化,我们可以说将线性范围转换为对数范围:
转变为OpenCV代码:
magI+=Scalar::all(1);
log(magI,magI);
6、修剪和重排列。还记得我们在第一步重的图像延伸吗?现在就到了要把那些新引入的数值扔掉的时候了。为了可视化的目的,我们可以重排列结果的象限,因此origin(zero,zero)最应于图像的中心点。
magI=magI(Rect(0,0,magI.cols&-2,magI.rows&02));
int cx=magI.cols/2;
Int cy=magI.rows/2;
Mat q0(magI,Rect(0,0,cx,cy));
Mat q1(magI,Rect(cx,0,cx,cy));
Mat q2(magI,Rect(0,cy,cx,cy));
Mat q3(magI,Rect(cx,cy,cx,cy));
Mat tmp;
q0.copyTo(tmp);
q3.copyTo(q0);
tmp.copyTo(q3);
q1.copyTo(tmp);
q2.copyTo(q1);
tmp.copyTo(q2);
7、正常化。这也是为了可视化的目的。我们现在拥有了幅值图像,然而这个还是在我们的0-1的显示范围之外。我们使用normalize()函数将我们的数值正常化到这个范围中。
normalize(magI,magI,0,1,CV_MINMAX);
结果
一个应用的观点可能是决定图像中几何的存在的方位。例如,让我们发现文字是否水平。看一些文字,你会发现文字行安排为水平,书信形式的被安排为竖直行。这两个主要的文字片段组成可能同样被看作傅立叶转换。让我们做一下水平的和有文字的图形被旋转。
在水平文字情况:
旋转文字情况:
你可以看到频率域的最有影响的组成部分(在幅值图像重的最亮的点)在图像中跟随对象进行几何旋转。从这里我们就可以计算出偏移量以及一张图像旋转去更正最后的缺失的偏移量。