离散傅里叶变换
目标:
我们将寻找下面这些问题的答案:
什么是傅里叶变换以及为什么我们使用它;
在OpenCV中怎么做傅里叶变换;
copyMakeBorder(),merge(),dft(),getOptimalDFTSize(),log()以及normalize()等函数的使用;
源代码:
- ///////////////////////////////////////////////////////////
- // Translator:York. ///
- // Email:[email protected] <span style="white-space:pre"> </span>///
- // Date:2016/03/05 ///
- ///////////////////////////////////////////////////////////
- #include<cv.h>
- #include<highgui.h>
- #include<imgproc\imgproc_c.h>
- #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 = "F:/Photo/OpenCV_Photo/lena.jpg";
- Mat I = imread(filename, CV_LOAD_IMAGE_GRAYSCALE);
- if (I.empty())
- return -1;
- //OpenCV中的DFT采用的是快速算法,这种算法要求图像的尺寸是2、3或5的倍数时处理速度最快
- //所以需要用getOptimalDFTSize()找到最适合的尺寸,然后用copyMakeBorder()填充多余的部分
- //这里是让原图像和扩大的图像的左上角对齐。填充的颜色如果是纯白色对变换结果的影响不会很大
- //后面寻找倾斜线的过程又会完全忽略这一点的影响
- 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));
- //DFT要分别计算实部和虚部,把要处理的图像作为输入的实部、一个全零的图像作为输入的虚部。
- //dft()输入和输出应该分别为单张图像,所以要先用merge()把实部和虚部图像合并,分别处于图像
- //comlexI的两个通道内。计算得到的实部和虚部仍然保存在comlexI的两个通道内
- 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
- //一般都会用幅度图像来表示图像傅里叶的变换结果(傅里叶谱)。
- //由于幅度的变化范围很大,而一般图像亮度范围只有[0,255],容易造成一大片漆黑,只有
- //几个点很亮。所以要用log函数把数值的范围缩小
- // 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;
- //magnitude(x,y,dst),dst=sqrt(x(I)^2+y(I)^2);)
- Mat magI = planes[0];
- magI += Scalar::all(1); // switch to logarithmic scale,这里是保证magI的所有像素值
- //大于0,不等于0,为下面log()函数的应用做准备
- log(magI, magI); //log(I,dst),如果Iij!=0;则dstij=log(|Iij|)
- //dft()直接获得的结果中,低频部分位于四角,高频部分位于中间。习惯上会把图像做四等分
- //互相对调,使低频部分位于图像中心,也就是让频域原点位于中心
- //虽然用log()缩小了数据范围,但仍然不能保证数值都落在[0,255]之内,所以要先用normalize()
- //规范化到[0,1]内,再用convertTo()把小数映射到[0,255]内的整数,结果保存在一幅单通道图像内
- // crop the spectrum, if it has an odd number of rows or columns
- //magI的宽度和高度应该是偶数,这样他们才能被2整除
- //-2在二进制中的表示是11111110,&操作符保证宽度和高度一直都是偶数
- //magI.cols&-2就是按位与,大概意思就是取不大于magI.cols的最大偶数,这样就可以
- //保证宽度和高度一直都是偶数
- //主要目的是保证裁切的时候方便,能均切成4块
- 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是其的频域值。转换的结果是复数。我们可以通过一幅实数的图像以及一幅复数的图像或者通过幅度图以及相图来显示它。然而,纵观图像处理算法,只有幅度图是比较适合的,因为它包含了我们对一幅图像几何形状的所有信息。因此,如果你像要以这些方式对一幅图像做一些修改,你就需要将其再变回到原型,并且你需要把他们都保存起来。
在这个例子中主要展现如何去计算以及显示一个傅里叶变换的幅度图像(傅里叶谱)。从数字的角度看图像是离散的。这就意味着他们可以从一个给定的阈值中获得一个值。例如,在基本的灰度图像中,值通常都在0到255之间。因此傅里叶变换也需要是离散值,这就是离散傅里叶变换(DFT)。在任何时候当你需要从一个几何的角度去确定一幅图像的结构的时候,你将想要使用这种傅里叶变换。下面是你要遵循的步骤(以输入图像是灰度图像为例):
1、 将图像延伸到最优的尺寸。DFT的性能取决于图像的尺寸。当图像的尺寸是数字2,3或5的倍数时,DFT趋近于最快。因此,为了达到这种最优的性能,通过填充图像的边界值来得到这种特征的尺寸通常是一种很好的方法。函数getOptimalDFTSize()返回这种最优的尺寸,并且我们能够使用函数copyMakeBorder()去扩展图像的边界:
- Mat padded; //expand input image to optimal size
- int m = getOptimalDFTSize( I.rows );
- int n = getOptimalDFTSize( I.cols ); // on the border add zero pixels
- copyMakeBorder(I, padded, 0, m - I.rows, 0, n - I.cols, BORDER_CONSTANT, Scalar::all(0));
附加的像素值都被初始化为0.
2、 为复数和实数创造空间。傅里叶变换的结果是复数。这就意味着,对于每一个图像值,其结果是两幅图像的值(每一个组成部分是一幅图像)。甚至,频域范围比其空间域更大。因此,我们通常至少使用float形式来储存这些值。因此,我们将我们的输入图像转换成这种类型,并且以另外一种通道来扩展它来保存复数值:
- 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
3、 做离散傅里叶变换。使用in-place计算(输入和输出一样):
- dft(complexI, complexI); // this way the result may fit in the source matrix
4、 将实数和复数值转换为量值。一个复数值有实数部分(Re)和复数部分(Im)。DFT的结果是复数值。
转换为opencv代码就是:
- 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];
5、 转换成对数的形式。结果表明傅里叶系数的动态范围对于在显示器上显示来说是非常大的。我们有一些小的以及一些高的值是我们观察不到的。因此高的值都将转变为白点,而小的值都想是黑点。去使用这种灰度值来进行可视化我们可以将我们的线性量转换为对数形式:
转换为OpenCV代码:
- magI += Scalar::all(1); // switch to logarithmic scale
- log(magI, magI);
6、 截断以及重新调整。还记得我们在第一步中将我们的图像扩展了吗?是的,是时候把这些新产生的值给丢弃了。为了可视化的目的,我们也可以重新调整结果的象限,这样的话原点就对应着图像的中心。
- magI = magI(Rect(0, 0, magI.cols & -2, magI.rows & -2));
- 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);
7、 规范化。这样做还是为了可视化的目的。我们现在有了量值,然而这样还是在我们的0到的现实范围之外。我们可以使用normalize()函数来将我们的值归一化到这个范围之内。
- normalize(magI, magI, 0, 1, CV_MINMAX); // Transform the matrix with float values into a
- // viewable image form (float between values 0 and 1).