65.9K
CodeProject 正在变化。 阅读更多。
Home

快速傅里叶变换

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (31投票s)

2013年5月9日

CPOL

4分钟阅读

viewsIcon

36939

downloadIcon

2808

一些使离散傅里叶变换更快的想法,并实现了一个精简的DFT算法版本。

带有复数的基本DFT算法

根据傅里叶变换的主要原则,一个在N内周期性的信号f(n),并且我们有它的N个采样,可以像下面这样转换成它的傅里叶分量

 

 

复数傅里叶分量 ck 的另一个等式看起来像

 

 

 

这意味着复数傅里叶分量 ck 是 f(n) 和围绕单位圆旋转的复数向量的复数和。对于一个完整的傅里叶变换,我们需要 N 个傅里叶分量 (ck),并且对于每个分量,我们需要 N 个正弦和 N 个余弦值。对于 1000 个样本,这将需要计算 100 万个正弦和余弦值。这消耗了大量的 CPU 能力和时间。但它确实有效 J

public void CalcDFT(int N)
{
    int k, n;
    TKomplex w;
    if (N > 0)
    {
        for (k = 0; k < N; k++)
        {
            c[k].real = 0;
            c[k].imag = 0;
            for (n = 0; n < N; n++)
            {
                w.real = Math.Cos((double)(2.0 * Math.PI * (double)(k * n) / (double)(N)));
                w.imag = -Math.Sin((double)(2.0 * Math.PI * (double)(k * n) / (double)(N)));
                c[k] = ksum(c[k], kprod(w, y[n]));
            }
            c[k].real = c[k].real / (double)(N) * 2.0;
            c[k].imag = -c[k].imag / (double)(N) * 2.0;
        }
    }
    c[0].real = c[0].real / 2;
    c[0].imag = c[0].imag / 2;
}

通过这个标准实现计算,1000 个样本的傅里叶变换在我的电脑上花费了 140 毫秒(当然世界上有更快的电脑 J)。

可以在 Visual C# 项目 DFT_std 中找到此标准实现的源代码。

ksum()kprod() 是用于复数加法和乘法的简单函数。

public TKomplex ksum(TKomplex a, TKomplex b)
{
    TKomplex res;
    res.real = a.real + b.real;
    res.imag = a.imag + b.imag;
    return (res);
}
public TKomplex kprod(TKomplex a, TKomplex b)
{
    TKomplex res;
    res.real = a.real * b.real - a.imag * b.imag;
    res.imag = a.real * b.imag + a.imag * b.real;
    return (res);
}

关于周期性的思考

现在,正弦和余弦函数都是 2π 的周期函数,对于 k = 1,我们必须计算正弦和余弦的值,从 0 到 2π,步长为 2π /N。 对于 k = 2,我们有 0 到 4 π,步长为 2/N,依此类推。 所以对于 k = 2,我们得到的值与 k = 1 时已经得到的值相同。 只是,我们得到每个第二个值,当 n 大于 N/2 时,我们完成了一圈单位圆并再次从与 n = 0 相同的值开始。 k 越大,步长越大,我们经过的单位圆圈数越多。 但我们得到的正弦和余弦值仍然与 k = 1 时得到的相同。 这意味着基本上只需要计算 0 到 2π 之间的 N 个正弦和 N 个余弦值,步长为 2π/N,并将它们保存在查找表中以供整个变换使用。 对于 1000 个样本,我们只需要计算 1000 个正弦值和 1000 个余弦值,而不是 999000 个,这节省了相当多的时间。

我们使用以下方法构建查找表:

for (k = 1; k < N; k++)
{
    we[k].real = Math.Cos((2.0 * Math.PI * (double)(k) / (double)(N)));
    we[k].imag = -Math.Sin((2.0 * Math.PI * (double)(k) / (double)(N)));
}

我们可以通过 k * n 除以 N 的简单取模运算从查找表中获得相应的正弦和余弦值。 这样,离散傅里叶变换变得不是“快”,而是相当“快”和精简 J

public void CalcDFT()   //  Quick and lean Fourier transformation
{
    int k, n;
    if (N > 0)
    {
        for (k = 0; k < N; k++)
        {
            c[k].real = 0;
            c[k].imag = 0;
            for (n = 0; n < (N - 1); n++)
            {
                c[k] = ksum(c[k], kprod(we[(k * n) % N], y[n]));
            }
            c[k].real = c[k].real / N * 2;
            c[k].imag = -c[k].imag / N * 2;
        }
        c[0].real = c[0].real / 2;
        c[0].imag = c[0].imag / 2;
    }
}

对于与上述相同的 1000 个样本,此实现花费 53 毫秒。 快了很多。

此实现的 code 在 DFT_lean 中。

复数?

现在,我的脑海中又出现了一些想法。 在主要条款中没有复数。 输入是实数序列(至少在我的应用程序中来自电信号分析 J),我们最终得到另一个实数序列。 那么,为什么我们在中间使用复数呢?

在我们的算法中,我们有一个复数乘法和一个复数加法。 所以让我们看看它们。 首先是乘法,we[x] * y[n]。 看起来像

(we[x].re * y[n].re - we[x].im * y[n].im) + j(we[x].re * y[n].im + we[x].im * y[n].re)

y[n].im 总是 0,因此第二部分和第三部分也是 0,我们的复数乘法简化为

(we[x].re * y[n].re) + j(we[x].im * y[n].re)

这意味着

y[n].re * cosine(x) + j(y[n].re * sine(x))

这看起来很像主要条款中的 ak 和 bk。 在下面的加法中,我们只需要对实部和虚部求和。 因此,在整个计算中,实部和虚部没有混合,最后我们在 ck 的实部得到 ak 部分,在 ck 的虚部得到 bk 部分,我们可以忘记所有复杂的東西 J

有了输入序列不包含虚数的限制,我们可以实现没有复数的傅里叶变换

public void CalcDFT()
{
    int k, n;
    if (N > 0)
    {
        for (k = 0; k < N; k++)
        {
            a[k] = 0;
            b[k] = 0;
            for (n = 0; n < (N - 1); n++)
            {
                a[k] = a[k] + ((cosine[(k * n) % N] * y[n]));
                b[k] = b[k] + ((sine[(k * n) % N] * y[n]));
            }
            a[k] = a[k] / N * 2;
            b[k] = b[k] / N * 2;
        }
        a[0] = a[0] / 2;
        b[0] = b[0] / 2;
    }
}

初始化变成这样

public TFftAlgorithm(int order)
{
    int k;
    N = order;
    y = new double[N + 1];
    a = new double[N + 1];
    b = new double[N + 1];
    xw = new double[N + 1];
    sine = new double[N + 1];
    cosine = new double[N + 1];
    cosine[0] = 1.0;    // we don't have to calculate cos(0) = 1
    sine[0]   = 0;      //                        and sin(0) = 0
    for (k = 1; k < N; k++) //  init vectors of unit circle
    {
        cosine[k] = Math.Cos((2.0 * Math.PI * (double)(k) / (double)(N)));
        sine[k] = Math.Sin((2.0 * Math.PI * (double)(k) / (double)(N)));
    }
}

通过这种实现,1000 个样本在 20 毫秒内被转换。 这几乎比本文顶部的第一种实现快 5 倍。 它不是快速傅里叶变换。 但至少这是一个快速傅里叶变换,它看起来非常精简 J

这个实现位于项目 DFT_lean_quick 中。

对于矩形信号,结果应该看起来像这样

蓝色形状是输入序列:一个有 1000 个样本的矩形信号。幅度为 20。红色形状是用前 30 个傅里叶分量进行的反傅里叶变换。

欲了解更多数学知识,请访问 www.mosismath.com :-)

© . All rights reserved.