• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录

limekebab

  • 博客园
  • 联系
  • 订阅
  • 管理

公告

View Post

离散傅里叶变换(DFT)的Python实现

离散傅里叶变换(DFT)的Python实现 本文主要讨论DFT的实现方法,主要包括DFT算法原始实现、频谱截断与幅值标准化,不过多涉及Fourier变换的理论。插图来源:https://www.thefouriertransform.com/。

离散傅里叶变换(Discrete Fourier Transform,DFT)

Fourier分析是指通过傅里叶级数,表示和分析周期信号。Fourier变换则是将这些方法扩展至非周期信号中。

Fourier变换可以将信号分解至一系列频率不同的sin或者cos信号的和,Fourier系数是这些sin信号对应的幅值。通常我们分析这些Fourier系数的平方,绘制功率频谱,用以分析信号的频率特征。

离散傅里叶变换则是Fourier变换的离散版本。

本文主要讨论DFT的实现方法,主要包括DFT算法原始实现、频谱截断与幅值标准化,不过多涉及Fourier变换的理论。欢迎讨论或指正错误。

对于周期信号,我们可以直接应用DFT,公式定义如下:

\(X_k = \sum_{n=0}^{N-1}{x_n\cdot e^{-i2\pi{kn/N}}} = \sum_{n=0}^{N-1}{x_n[cos(2\pi{kn/N}) -i\cdot sin(2\pi{kn/N})]}\)

其中,

  • \(N\): 当前信号的总长度,
  • \(x\)为原始周期信号,
  • \(X\)为Fourier系数,
  • \(k\)为当前频率点,\(k\in [0,N-1]\)。

原始DFT算法实现

我们可以利用原始DFT公式,使用python,写出一个Fourier系数计算函数(参考Python Numerical Methods)。

点击查看代码
def dft_raw_version(x: np.array, fs: int) -> np.array, np.array:
    """
        原始DFT实现.
    :param x: 原始信号.
    :param fs: 信号采样频率.
    :return: Fourier系数.
    """

    # Fourier系数计算
    N = len(x)
    n = np.arange(N)
    k = np.reshape(n, (N, 1))
    we = np.exp(-2j * np.pi * k * n / N)
    # X为最终的Fourier系数
    X = np.dot(we, x)

    # 生成频率轴
    ts = N / fs
    freq = n / ts

    return freq, X

随后我们生成一个多个频率混合的sin函数,\(f(x) = 3 * sin(2\pi t) + 1 * sin(2\pi 4t) + 2 * sin(2\pi 8t) + 0.5 * sin(2\pi 16t)\),基于python的生成函数如下:

点击查看代码
    # 信号原始采样频率以及信号长度
    fs, length = 100, 4
    ts = np.arange(0, length, 1 / fs)
    # 设置4个频率分量
    frequency_factor = [1, 4, 8, 16]
    amplitude_factor = [3, 1, 2, 0.5]
    # 生成周期信号
    periodic_signal_x = None
    for f, a in zip(frequency_factor, amplitude_factor):
        periodic_component = a * np.sin(2 * np.pi * f * ts)
        if periodic_signal_x is None:
            periodic_signal_x = periodic_component
        else:
            periodic_signal_x += periodic_component

生成的sin函数如下图所示。
image

我们期待利用DFT函数,得到对应的Fourier系数,并能与我们设置的频率分量能够对应。下图是我们根据dft_raw_version函数得到的频谱图,横轴为频率,纵轴为傅里叶系数的绝对值,具体的python代码如下:

点击查看代码
    frequencies, fourier_coefficients = dft_raw_version(periodic_signal_x, fs)
    amplitudes = np.abs(fourier_coefficients)

对应的频谱图如下所示。
image

从Figure1-2中,我们可以该频谱图的特点:

  • 以横轴中心点(在图中是50Hz)轴对称;
  • 观察0-50Hz,频谱中对应的频率分量点能够对应,但频率分量点对应的幅值要明显高于预设的幅值。(在示例中,我们设置的频率分量点分别为(1,4,8,16)Hz,与图中Fourier系数绝对值较高的频率点对应。但幅值不对应,我们设置的幅值为(3,1,2,0.5),频谱中为(600,200,400,100)。)

50-100Hz通常称为负频率,即-50-0Hz,其中\(X_{-k} = \overline{X_k})\),在绘制频谱时我们只关心正频率即可。
幅值与我们预设幅值不对应,主要是我们未对Fourier系数做标准化。

所以我们需要再做两件事儿,就可以得到我们想要的频谱图:

  • 截断0-50Hz的频谱;
  • 对Fourier系数进行标准化。由于正、负频率的Fourier共同构成频谱的总能量,但我们只画了正频率部分,要想得到与我们预设的幅值一样的频谱,我们需要除以\(\frac{N}{2}\)而不是\(N\)。

经过标准化的DFT函数如下:

点击查看代码
def dft_normalized_version(x: np.array, fs: int) -> np.array, np.array:
    """
    得到标准化后的Fourier系数和频率轴. 对比dft_raw_version, 增加了两个操作.
    1. 只保留0-50Hz的频谱;
    2. 对频谱进行标准化, 除以N/2.

    :param x: 原始信号.
    :param fs: 信号采样频率.
    :return: Fourier系数.
    """

    # Fourier系数计算
    N = len(x)
    n = np.arange(N)
    k = np.reshape(n, (N, 1))
    we = np.exp(-2j * np.pi * k * n / N)
    # X为最终的Fourier系数
    X = np.dot(we, x)

    # 生成频率轴
    oneside_N = N // 2
    ts = N / fs
    freq_half = n[:oneside_N] / ts
    # Fourier系数截断与标准化后
    oneside_X_norm = X[:oneside_N] / oneside_N

    return freq_half, oneside_X_norm

使用标准化DFT函数得到的频谱图如下所示。
image

在实际应用中,使用单边、经过标准化的频谱更多一些。

数据长度N对频谱的影响

在我们的DFT算法中,有两个参数,数据的采样频率fs和数据长度N。
从实现代码中可以看出,数据长度N越大,频谱的间隔越小,频谱也就更加地平滑。
image

在周期信号的分析中,单个周期的信号就可以获得频谱中所有的信息,所以没必要延长数据的长度。在本例中,sin分量最大周期为1,所以理论上我们使用1s的数据即可。

关于fs的内容后续补充。

完整代码
点击查看代码
"""
    DFT简要实现与验证demo.
    Author: limekebab.
    E-mail: zzklove3344@hotmail.com.
"""

import numpy as np
import matplotlib.pyplot as plt


def dft_raw_version(x: np.array, fs: int) -> np.array:
    """
        原始DFT实现.

    :param x: 原始信号.
    :param fs: 信号采样频率.
    :return: Fourier系数.
    """

    # Fourier系数计算
    N = len(x)
    n = np.arange(N)
    k = np.reshape(n, (N, 1))
    we = np.exp(-2j * np.pi * k * n / N)
    # X为最终的Fourier系数
    X = np.dot(we, x)

    # 生成频率轴
    ts = N / fs
    freq = n / ts

    return freq, X


def dft_normalized_version(x: np.array, fs: int) -> np.array:
    """
    得到标准化后的Fourier系数和频率轴. 对比dft_raw_version, 增加了两个操作.
    1. 只保留0-50Hz的频谱;
    2. 对频谱进行标准化, 除以N/2.

    :param x: 原始信号.
    :param fs: 信号采样频率.
    :return: Fourier系数.
    """

    # Fourier系数计算
    N = len(x)
    n = np.arange(N)
    k = np.reshape(n, (N, 1))
    we = np.exp(-2j * np.pi * k * n / N)
    # X为最终的Fourier系数
    X = np.dot(we, x)

    # 生成频率轴
    oneside_N = N // 2
    ts = N / fs
    freq_half = n[:oneside_N] / ts
    # Fourier系数截断与标准化后
    oneside_X_norm = X[:oneside_N] / oneside_N

    return freq_half, oneside_X_norm


def dft_demo() -> None:
    """
        DFT Demo.
    :return:
    """

    # 信号原始采样频率以及信号长度
    fs, length = 100, 4
    ts = np.arange(0, length, 1 / fs)
    # 设置4个频率分量
    frequency_factor = [1, 4, 8, 16]
    amplitude_factor = [3, 1, 2, 0.5]
    # 生成周期信号
    periodic_signal_x = None
    for f, a in zip(frequency_factor, amplitude_factor):
        periodic_component = a * np.sin(2 * np.pi * f * ts)
        if periodic_signal_x is None:
            periodic_signal_x = periodic_component
        else:
            periodic_signal_x += periodic_component

    # 计算Fourier系数
    frequencies, fourier_coefficients = dft_raw_version(periodic_signal_x, fs)
    amplitudes = np.abs(fourier_coefficients)

    # 原始信号绘图
    fig, ax = plt.subplots(1, 1, figsize=[8, 6])
    ax.plot(ts, periodic_signal_x, c='blue')
    ax.set_title("Figure 1-1: Raw Periodic Signal(Length = 4s)")
    ax.set_xlabel("Time (s)")
    ax.set_ylabel("Amplitude")
    ax.grid()
    plt.show(dpi=400)

    # 绘制频率-幅值图, 双边频谱
    fig, ax = plt.subplots(1, 1, figsize=[8, 6])
    # Fourier系数是复数, 可以取平方或者取模
    markerline, stemline, baseline = ax.stem(frequencies, amplitudes)
    ax.set_title("Figure 1-2: Twoside DFT Spectrum of Figure 1-1")
    ax.set_xlabel("Frequency(Hz)")
    ax.set_ylabel("Fourier Coefficients Amplitude |X|")
    plt.setp(markerline, markersize=4)
    ax.grid()
    plt.show(dpi=400)

    # Fourier系数按频率中轴线对称, 所以我们只画一半的频谱就可以
    # 可以对Fourier系数的幅值进行标准化, 得到与我们预设的幅值相等的序列
    frequencies_oneside, fourier_coefficients_oneside = dft_normalized_version(periodic_signal_x, fs)
    amplitudes_oneside = np.abs(fourier_coefficients_oneside)

    # 绘制频率-幅值图, 单边频谱,
    fig, ax = plt.subplots(1, 1, figsize=[8, 6])
    # Fourier系数是复数, 可以取平方或者取模
    markerline, stemline, baseline = ax.stem(frequencies_oneside, amplitudes_oneside)
    ax.set_title(f"Figure 1-4: Normalized Oneside DFT Spectrum(length = {length}s)")
    ax.set_xlabel("Frequency(Hz)")
    ax.set_ylabel("Normalize DFT Amplitude |X|")
    ax.grid()
    plt.setp(markerline, markersize=4)
    plt.show()


if __name__ == '__main__':
    dft_demo()

posted on 2023-08-21 16:00  青柠味烤串  阅读(926)  评论(0)    收藏  举报

刷新页面返回顶部
 
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3