最近对图像处理算法比较感兴趣,也看了一些数字图像处理相关书籍,自己也实现过一些简单的图像处理算法。随着了解的深入,发现要真正理解图像处理各种骚操作,绕不开基于傅里叶变换的各种频率域滤波。对于lz这种数学渣来说,看到各种数学公式推导,简直令人头大,通过傅里叶变换把直观图像转换为抽象的频谱图更是让人难以理解。
上面是一张傅里叶变换后的频谱图,如果不了解傅里叶变换,很难从上图中分析出原图包含的一些特征。研究了一段时间傅里叶变换,按照lz的理解,其实图像的傅里叶变换就是根据空间位置对图像灰度值进行拆分,分离出从高到低不同频率,频谱图就是展示一张图像从低频(频谱中心,图像平均灰度)到高频(远离频谱中心,对应灰度变化大的区域)的分布。而频率域滤波则是根据图像的频率特征,采用不同滤波器,理想低通、高通,高斯低通、高通等,对频率域的图像数据进行处理,之后通过逆傅里叶变换还原出滤波后的图像。
冈萨雷斯的《数字图像处理》第四章节用了将近70页的篇幅对傅里叶变换进行了逐步讲解,同时也表明了频率域滤波在图像滤波中的重要性。虽然是图像处理的业余爱好者,lz还是决定有必要把傅里叶变换尽量搞清楚一些。因为没有接触过MATLAB和OpenCV(暂时也不打算使用第三方工具),所以文章所涉及到的代码都使用lz比较熟悉的java来实现。
为了更好的理解傅里叶变换,lz将傅里叶变换从一些基本概念到应用拆分为不同层次,基本概念及推导、一维离散傅里叶变换、快速傅里叶变换、二维傅里叶变换、频率域滤波。这里就对傅里叶变换的一些基本概念做个总结。
傅里叶变换涉及的一些基本概念:复数、欧拉公式、傅里叶公式、取样和卷积。
一、复数
复数的形式:C = a + jb
a为实部,jb为虚部,其中b为实数,,为虚数。复数也可以理解为一个二维平面上的点,实部为x轴,虚部为y轴。
复数的共轭复数:C’ = a - jb,实部相同,虚部符号相反。
复数满足四则运算法则
例如:C’’ = c + jd;
复数相加:C + C’’ = (a + c) + j(b+d)
复数相减:C - C” = (a - c) + j(b - d)
复数相乘:C * C” = (a + jb) * (c + jd) = a*c - bd + j(ad + bc)
复数相除:C / C” = (a + jb)/(c + jd) = (a + jb)*(c - jd)/(c + jd)(c - jd)
=[(ac + bd) - j(bc - ad)] / (c2 + d2)
复数的模长:C = a + jb模长为|C| =
根据三角函数,复数可变换为:复数可变换为:C = |C|()形式,实际上在编程中傅里叶变换也是通过这种形式进行计算的。
二、欧拉公式
傅里叶变换公式也就是通过欧拉公式变换为方便计算的复数形式。
三、傅里叶变换公式
傅里叶变换公式:
傅里叶反变换公式:
以上两个公式就是傅里叶变换和逆变换,但这是在连续函数上的变换,我们实际使用的是离散函数的傅里叶变换。
傅里叶变换公式的简单推导过程:
例:常数函数f(t)=A ,在有限区间内,傅里叶公式可做如下变换:
根据欧拉公式
上面公式可以做进一步变换
上式结果也可由三角恒等式直接变换过来:
三角恒等式:
通常,为了显示,需要处理幅值,变换为傅里叶谱或频谱:
四、取样和卷积
上面所讲的傅里叶公式及推导都是在连续函数上进行的,而要实现计算机能够处理的数据必须是离散的信号,所以这就 涉及到取样。连续的数字信号,例如音频、电磁波信号等,需要对原始信号采样后再进行处理,而图像处理方面,可 以把每一个像素上的灰度值看做已经采样后的数据,我们只需要把图像数据作为离散的信号输入即可。
至于卷积,做过图像空间域滤波以后不难理解,图像的傅里叶变换,是以整个图像所有对图像上每个像素进行傅里叶计 算,也就是说以整个图像作为一个周期,计算不同频率在某个像素位置上的叠加。那么一幅像素数量为N的图像,需要进 行的离散傅里叶变换计算次数为N * N。例如,对一幅2000万像素的图片做傅里叶变换计算,需要计算2000万 * 2000万 次,这对超级计算机来说也是一个不小的挑战,于是有了快速傅里叶变换,把计算量降低为N * log2N,这才使傅里叶变换 有了实用价值,至于快速傅里叶变换,后面再详细讲。
五、复数类的实现
上面只是对傅里叶变换概念及公式做了个简单的讲解,其实从代码上实现傅里叶变换并没有那么复杂。由于傅里叶变换 是在复数基础上进行计算的,接下来就迈出傅里叶变换的第一步,复数的代码实现。
附上java代码:
package FourierTransform;
/**
* 复数类
* @author admin
*
*/
public class Complex {
private double a; //复数实部
private double b; //复数虚部
//创建一个复数类实例
public Complex(double a,double b) {
this.a = a;
this.b = b;
}
//获取复数实部
public double getRealPart() {
return this.a;
}
//获取复数虚部
public double getImaginePart() {
return this.b;
}
//对复数重新赋值
public void changeVaule(double a, double b) {
this.a = a;
this.b = b;
}
//共轭复数
public Complex conjugate() {
return new Complex(this.a,-this.b);
}
//计算复数模长
public double absValue() {
return Math.sqrt(this.a * this.a + this.b * this.b);
}
//复数加运算,实部与实部相加,虚部与虚部相加
public Complex add(Complex C) {
return new Complex(this.a + C.a, this.b + C.b);
}
//复数相减
public Complex minus(Complex C) {
return new Complex(this.a - C.a, this.b - C.b);
}
/**
* 复数相乘
* a*a1+a*jb1+jb*a1*-b*b1
* @param C
* @return
*/
public Complex multiply(Complex C) {
double A = this.a * C.a - this.b * C.b;
double B = this.a * C.b + this.b * C.a;
return new Complex(A, B);
}
/**
* 复数相除
* (a + jb) / (c + jd) = [(a + jb) * (c - jd)] / [(c + jd) * (c - jd)]
* =[(ac + bd) + j(bc - ad)] / [(c^2 + d^2)]
* @param C
* @return
*/
public Complex divide(Complex C) {
if (C.a == 0 && C.b == 0) {
System.out.println("被除数不能为空!");
return null;
}
double A = (this.a * C.a + this.b * C.b) / (C.a * C.a + C.b * C.b);
double B = (this.b * C.a - this.a * C.b) / (C.a * C.a + C.b * C.b);
return new Complex(A,B);
}
//toString方法
public String toString() {
if (a == 0 && b == 0) {
return "复数为空!";
}else if (a == 0) {
return this.b + "j";
}else if (b == 0) {
return a + "";
}else if (b > 0) {
return this.a + "+" + this.b + "j";
}else{
return this.a + "" + this.b + "j";
}
}
//测试
public static void main(String[] args) {
Complex c1 = new Complex(1, 2);
System.out.println(c1.toString());
Complex c2 = c1.conjugate();
System.out.println(c2.toString());
System.out.println(c1.absValue());
}
}