图像的傅里叶变换(将图像转为频率域再进行角度校正)

使用C++、opencv对图像进行傅里叶变换,再根据频谱图对原图进行旋转

        离散傅里叶变换(Discrete Fourier transform,缩写为DFT),是指傅里叶变换在时域和频域上都呈现离散的形式,将时域信号的采样变换为在离散时间傅里叶变换(DIFT)频域的采样。在形式上,变换两端(时域和频域上)的序列是有限长的,而实际上这两组序列都应当被认为是离散周期信号的主值序列。即使对有限长的离散信号做DFT,也应当对其经过周期延拓成为周期信号再进行变换。在实际应用中,通常采用快速傅里叶变换来高效计算DFT。

        简单来说,对一张图像使用傅里叶变换就是将它分解成正弦和余弦两部分,也就是将图像从空间域(spatial domain)转换到频域(frequency domain)。这一转换的理论基础为:任一函数都可以表示成无数个正弦和余弦函数的和的形式。傅里叶变换就是一个用来将函数分解的工具二维图像的傅里叶变换可以用以下数学公式表达。

        式中f是空间域(spatial domain)值,F是频域(frequency domain)值。转换之后的频域值是复数,因此,显示傅里叶变换之后的结果需要使用实数图像(real image)加虚数图像(complex image),或者幅度图像(magitude image)加相位图像(phase image)的形式。在实际的图像处理过程中,仅仅使用了幅度图像,因为幅度图像包含了原图像的几乎所有我们需要的几何信息。然而,如果想通过修改幅度图像或者相位图像的方法来间接修改原空间图像,需要使用逆傅里叶变换得到修改后的空间图像,这样就必须同时保留幅度图像和相位图像了。

        在频域里面,对于一幅图像,高频部分代表了图像的细节、纹理信息;低频部分代表了图像的轮廓信息。如果对一幅精细的图像使用低通滤波器,那么滤波后的结果就只剩下轮廓了。这与信号处理的基本思想是相通的。如果图像受到的噪声恰好位于某个特定的“频率”范围内,则可以通过滤波器来恢复原来的图像。傅里叶变换在图像处理中可以做到图像増强与图像去噪、图像分割之边缘检测、图像特征提取、图像压缩等。

关于傅里叶变换更详细的原理可参考博客:https://blog.csdn.net/u013921430/article/details/79683853


opencv中关于傅里叶变换的API:

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

dft函数的作用是对一维或二维浮点数数组进行正向或反向离散傅里叶变换。

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计算矩阵卷积时非常有效。

int getOptimalDFTSize(int vecsize);

getOptimalDFTSize函数返回给定向量尺寸的傅里叶最优尺寸大小。为了提高离散傅里叶变换的运行速度,需要扩充图像,而具体扩充多少,就由这个函数来计算得到。

此函数的唯一一个参数为int类型的 vecsize,向量尺寸,即图片的rows、cols。

DFT变换在一个向量尺寸上不是一个单调函数,当计算两个数组卷积或对一个数组进行光学分析,它常常会用0扩充一些数组来得到稍微大点的数组以达到比原来数组计算更快的目的。一个尺寸是2阶指数(2,4,8,16,32…)的数组计算速度最快,一个数组尺寸是2、3、5的倍数(例如:300 = 5*5*3*2*2)同样有很高的处理效率。 
getOptimalDFTSize()函数返回大于或等于vecsize的最小数值N,这样尺寸为N的向量进行DFT变换能得到更高的处理效率。在当前N通过p,q,r等一些整数得出N = 2^p*3^q*5^r. 
这个函数不能直接用于DCT(离散余弦变换)最优尺寸的估计,可以通过getOptimalDFTSize((vecsize+1)/2)*2得到。

void copyMakeBorder(InputArray src, OutputArray dst, int top, int bottom, int left, int right, int borderType, const Scalar& value=Scalar() );

copyMakeBorder函数的作用是扩充图像边界。

第一个参数, InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。
第二个参数, OutputArray类型的dst,函数调用后的运算结果存在这里,即这个参数用于存放函数调用后的输出结果,需和源图片有一样的尺寸和类型,且size应该为Size(src.cols+left+right,src.rows+top+bottom)。
接下来的4个参数分别为int类型的top、 bottom、left、 right,分别表示在源图像的四个方向上扩充多少像素,例如top=2, bottom=2,left=2, right=2就意味着在源图像的上下左右各扩充两个像素宽度的边界
第七个参数, border Type类型的,边界类型,常见取值为BORDER CONSTANT,可参考 borderInterpolateO得到更多的细节。
第八个参数, const Scalar&类型的 value,有默认值 Scalar,可以理解为默认值为0。当 border Type取值为 BORDER CONSTANT时,这个参数表示边界值。

C++: void magnitude(InputArray x, InputArray y, OutputArray magnitude);

magnitude函数用于计算二维矢量的幅值。

InputArray x: 浮点型数组的x坐标矢量,也就是实部。
InputArray y: 浮点型数组的y坐标矢量,必须和x尺寸相同。
OutputArray magnitude: 与x类型和尺寸相同的输出数组,即幅度值。
其计算公式如下: 

void normalize(InputArray src, OutputArray dst, double alpha=1, double beta=0, int norm_type=NORM_L2, int dtype=-1, InputArray mask=noArray() )

normalize函数的作用是进行矩阵的归一化。归一化就是把要处理的数据经过某种算法的处理限制在所需要的范围内。首先归一化是为了后面数据处理的方便,其次归一化能够保证程序运行时收敛加快。归一化的具体作用是归纳同意样本的统计分布性,归一化在0-1之间是统计的概率分布,归一化在某个区间上是统计的坐标分布,在机器学习算法的数据预处理阶段,归一化也是非常重要的步骤。

第一个参数,InputArray类型的src。输入图像,即源图像,填Mat类的对象即可。
第二个参数,OutputArray类型的dst。函数调用后的运算结果存在这里,和源图片有一样的尺寸和类型。
第三个参数,double类型的 alpha。归一化后的最大值,有默认值1。
第四个参数,double类型的beta。归一化后的最大值,有默认值0。
第五个参数,int类型的 norm type。归一化类型,有NORM_INF、 NORM_L1、NORM_L2和 NORM_MINMAX等参数可选,有默认值 NORM_L2。

第六个参数,int类型的 dtype,有默认值-1。当此参数取负值时,输出矩阵和src有同样的类型,否则,它和src有同样的通道数,且此时图像深度为CV_MAT_DEPTH(dtype)。
第七个参数,InputArray类型的mask,可选的操作掩膜,有默认值noArray()。


代码如下:

#include "stdafx.h"
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
#include "math.h"

using namespace cv;
using namespace std;

#define GRAY_THRESH 150
#define HOUGH_VOTE 100

//#define DEGREE 27

int main(int argc, char **argv)
{
	//以单通道(灰度)方式读取图像
	const char* filename = "C:\\Users\\lenovo\\Desktop\\1.jpg";
	Mat srcImg = imread(filename, CV_LOAD_IMAGE_GRAYSCALE);
	if (srcImg.empty())
		return -1;
	imshow("source", srcImg);

	Point center(srcImg.cols / 2, srcImg.rows / 2);

#ifdef DEGREE
	//旋转原图
	Mat rotMatS = getRotationMatrix2D(center, DEGREE, 1.0);
	warpAffine(srcImg, srcImg, rotMatS, srcImg.size(), 1, 0, Scalar(255, 255, 255));
	imshow("RotatedSrc", srcImg);
	//imwrite("imageText_R.jpg",srcImg);
#endif

	//将图像扩展到最佳大小,以获得更快的处理速度
	//设置4个方向的边框宽度
	//如果 borderType==BORDER_CONSTANT, 用(0,0,0)填充
	Mat padded;
	int opWidth = getOptimalDFTSize(srcImg.rows);
	int opHeight = getOptimalDFTSize(srcImg.cols);
	copyMakeBorder(srcImg, padded, 0, opWidth - srcImg.rows, 0, opHeight - srcImg.cols, BORDER_CONSTANT, Scalar::all(0));

	Mat planes[] = { Mat_<float>(padded), Mat::zeros(padded.size(), CV_32F) };
	Mat comImg;
	//合并成一个双通道图像
	merge(planes, 2, comImg);

	//傅里叶变换
	dft(comImg, comImg);

	//计算幅值
	//复数形式:planes[0]=Re(DFT(I)), planes[1]=Im(DFT(I))
	//magnitude=sqrt(Re^2+Im^2)
	split(comImg, planes);
	magnitude(planes[0], planes[1], planes[0]);

	//转换到log的尺度下,从而能观察到图像(否则数据过界)
	//M2=log(1+M1)
	Mat magMat = planes[0];
	magMat += Scalar::all(1);
	log(magMat, magMat);

	//剪切和重分布幅度图象限

	//若有奇数行或奇数列,进行裁剪
	magMat = magMat(Rect(0, 0, magMat.cols & -2, magMat.rows & -2));

	//重新排布傅里叶图像的4个象限,使原点位于图像中心,高频在角落
	int cx = magMat.cols / 2;
	int cy = magMat.rows / 2;

	Mat q0(magMat, Rect(0, 0, cx, cy));
	Mat q1(magMat, Rect(0, cy, cx, cy));
	Mat q2(magMat, Rect(cx, cy, cx, cy));
	Mat q3(magMat, Rect(cx, 0, cx, cy));

	//交换象限
	Mat tmp;
	q0.copyTo(tmp);
	q2.copyTo(q0);
	tmp.copyTo(q2);

	q1.copyTo(tmp);
	q3.copyTo(q1);
	tmp.copyTo(q3);

	//将幅度标准化至 [0,1], 再转为[0,255]
	normalize(magMat, magMat, 0, 1, CV_MINMAX);
	Mat magImg(magMat.size(), CV_8UC1);
	magMat.convertTo(magImg, CV_8UC1, 255, 0);
	imshow("magnitude", magImg);
	//imwrite("imageText_mag.jpg",magImg);



	//以下部分是对频谱图像进行霍夫直线变换,找到角度值,利用该角度值对原图像进行旋转
	//多适用于文字图像

	//转为二值图像
	threshold(magImg, magImg, GRAY_THRESH, 255, CV_THRESH_BINARY);
	//imshow("mag_binary", magImg);
	//imwrite("imageText_bin.jpg",magImg);

	//旋转原图
	//使用霍夫变换查找直线
	vector<Vec2f> lines;
	float pi180 = (float)CV_PI / 180;
	Mat linImg(magImg.size(), CV_8UC3);
	HoughLines(magImg, lines, 1, pi180, HOUGH_VOTE, 0, 0);
	int numLines = lines.size();
	for (int l = 0; l<numLines; l++)
	{
		float rho = lines[l][0], theta = lines[l][1];
		Point pt1, pt2;
		double a = cos(theta), b = sin(theta);
		double x0 = a*rho, y0 = b*rho;
		pt1.x = cvRound(x0 + 1000 * (-b));
		pt1.y = cvRound(y0 + 1000 * (a));
		pt2.x = cvRound(x0 - 1000 * (-b));
		pt2.y = cvRound(y0 - 1000 * (a));
		line(linImg, pt1, pt2, Scalar(255, 0, 0), 1, 8, 0);
	}
	imshow("lines", linImg);
	//imwrite("imageText_line.jpg",linImg);
	if (lines.size() == 3) {
		cout << "found three angels:" << endl;
		cout << lines[0][1] * 180 / CV_PI << endl << lines[1][1] * 180 / CV_PI << endl << lines[2][1] * 180 / CV_PI << endl << endl;
	}

	//Find the proper angel from the three found angels
	float angel = 0;
	float piThresh = (float)CV_PI / 90;
	float pi2 = CV_PI / 2;
	for (int l = 0; l<numLines; l++)
	{
		float theta = lines[l][1];
		if (abs(theta) < piThresh || abs(theta - pi2) < piThresh)
			continue;
		else {
			angel = theta;
			break;
		}
	}

	//计算旋转角度
	//如果图像不是方的,旋转后可能会达不到预期效果
	angel = angel<pi2 ? angel : angel - CV_PI;
	if (angel != pi2) {
		float angelT = srcImg.rows*tan(angel) / srcImg.cols;
		angel = atan(angelT);
	}
	float angelD = angel * 180 / (float)CV_PI;
	cout << "the rotation angel to be applied:" << endl << angelD << endl << endl;

	//旋转图像
	Mat rotMat = getRotationMatrix2D(center, angelD, 1.0);
	Mat dstImg = Mat::ones(srcImg.size(), CV_8UC3);
	warpAffine(srcImg, dstImg, rotMat, srcImg.size(), 1, 0, Scalar(255, 255, 255));
	imshow("result", dstImg);
	//imwrite("imageText_D.jpg",dstImg);
	
	waitKey(0);

	return 0;
}

源图像:

转化为频率域的幅度图像:

霍夫直线变换找到的直线:

旋转后的图像:

以上参考:毛星云 《OpenCV3编程入门》

https://blog.csdn.net/keith_bb/article/details/53389819

更多关于傅里叶变换在图像中的应用可翻看:

https://www.cnblogs.com/xh6300/p/5956503.html

https://blog.csdn.net/u013921430/article/details/79934162

https://www.jianshu.com/p/89ce7fdb9e12

http://johnhany.net/2013/11/dft-based-text-rotation-correction/

猜你喜欢

转载自blog.csdn.net/Lemon_jay/article/details/89404199