欢迎光临

Python科学计算中的傅里叶变换实战:NumPy与SciPy信号处理从频谱分析到滤波设计

傅里叶变换是科学计算与信号处理领域最核心的数学工具之一。它将时域信号分解为不同频率的正弦波叠加,使我们能够在频域中分析、处理和重构信号。在Python生态中,NumPy和SciPy提供了完整而高效的傅里叶变换实现,从基本的FFT到高级的滤波器设计,覆盖了工程与科研的绝大多数需求。

本文将从傅里叶变换的基本原理出发,结合大量代码实战,系统讲解如何使用NumPy和SciPy完成频谱分析、窗口函数应用、信号滤波与滤波器设计等核心任务。所有示例均可直接运行,适合有一定Python基础的开发者快速上手。

一、傅里叶变换基础与NumPy FFT

傅里叶变换的核心思想是:任何满足一定条件的周期函数,都可以表示为不同频率、不同幅值和相位的正弦波的叠加。离散傅里叶变换(DFT)是其在计算机上的实现形式,而快速傅里叶变换(FFT)则是DFT的高效算法,将时间复杂度从O(N²)降低到O(N log N)。

NumPy的

1
numpy.fft

模块提供了完整的FFT实现。让我们从最基础的频谱分析开始:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import numpy as np
import matplotlib.pyplot as plt

# 生成含三个频率分量的混合信号
fs = 1000  # 采样率 1000Hz
t = np.arange(0, 1, 1/fs)  # 1秒时长

# 信号 = 50Hz(幅值3) + 120Hz(幅值1.5) + 300Hz(幅值0.8)
signal = 3 * np.sin(2 * np.pi * 50 * t) + \
         1.5 * np.sin(2 * np.pi * 120 * t) + \
         0.8 * np.sin(2 * np.pi * 300 * t)

# 执行FFT
N = len(signal)
fft_result = np.fft.fft(signal)
freq = np.fft.fftfreq(N, 1/fs)

# 取单边频谱
magnitude = np.abs(fft_result[:N//2]) / N * 2  # 乘2还原单边幅值
freq_half = freq[:N//2]

print(f"50Hz处幅值: {magnitude[np.argmin(np.abs(freq_half - 50))]:.2f}")
print(f"120Hz处幅值: {magnitude[np.argmin(np.abs(freq_half - 120))]:.2f}")
print(f"300Hz处幅值: {magnitude[np.argmin(np.abs(freq_half - 300))]:.2f}")
# 输出:
# 50Hz处幅值: 3.00
# 120Hz处幅值: 1.50
# 300Hz处幅值: 0.80

上面的代码清晰地展示了FFT的基本用法。几个关键点需要特别注意:

  • 采样定理:采样率fs必须大于信号最高频率的2倍(奈奎斯特频率),否则会产生频谱混叠
  • 频率分辨率:等于fs/N,即采样率除以采样点数,增加采样时长可以提高频率分辨率
  • 幅值还原:FFT结果是复数,取模后需要除以N再乘2(单边谱)才能还原真实幅值

1.1 实数信号的rfft优化

对于实数信号,FFT结果具有共轭对称性——正频率和负频率的幅值相同。NumPy提供了

1
rfft

函数,只计算正频率部分,计算量减半:


1
2
3
4
5
6
7
8
9
10
# 使用rfft处理实数信号,效率更高
fft_r = np.fft.rfft(signal)
magnitude_r = np.abs(fft_r) / N * 2
freq_r = np.fft.rfftfreq(N, 1/fs)

print(f"频率点数对比: fft={N}, rfft={len(fft_r)}")
print(f"rfft 50Hz幅值: {magnitude_r[np.argmin(np.abs(freq_r - 50))]:.2f}")
# 输出:
# 频率点数对比: fft=1000, rfft=501
# rfft 50Hz幅值: 3.00

1.2 逆变换与信号重构

使用

1
ifft

可以从频域数据完美还原时域信号:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 逆FFT还原信号
reconstructed = np.fft.ifft(fft_result)

# 验证还原精度
max_error = np.max(np.abs(signal - reconstructed.real))
print(f"最大重构误差: {max_error:.2e}")  # ~1.1e-15,浮点精度内完美还原

# 实际应用:频域滤波后重构
# 频域置零去除300Hz分量
fft_filtered = fft_result.copy()
fft_filtered[np.abs(freq - 300) < 1] = 0  # 将300Hz附近置零
fft_filtered[np.abs(freq + 300) < 1] = 0  # 负频率也要处理

filtered_signal = np.fft.ifft(fft_filtered).real
print(f"滤波后300Hz分量残余: "
      f"{np.max(np.abs(filtered_signal - 3*np.sin(2*np.pi*50*t) - 1.5*np.sin(2*np.pi*120*t))):.2e}")
# 输出: ~2.2e-16,300Hz分量被成功移除

二、频谱泄漏与窗口函数

在实际应用中,我们只能截取有限长度的信号进行分析。如果截取的信号在边界处不连续(即不满足周期性),就会产生频谱泄漏——信号的能量从真实频率”泄漏”到邻近频率,导致频谱模糊。

窗口函数通过在信号两端平滑衰减到零来减少边界不连续性,从而抑制频谱泄漏。NumPy和SciPy提供了丰富的窗口函数:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from scipy.signal import get_window

# 生成一个非整数周期的50Hz信号(频谱泄漏场景)
fs = 256
t = np.arange(0, 1, 1/fs)
signal = np.sin(2 * np.pi * 50.5 * t)  # 50.5Hz,非整数周期

# 不同窗口函数对比
windows = {
    '矩形窗(无窗)': np.ones(len(t)),
    'Hanning窗': np.hanning(len(t)),
    'Hamming窗': np.hamming(len(t)),
    'Blackman窗': np.blackman(len(t)),
    'Kaiser窗(beta=8)': np.kaiser(len(t), 8)
}

print("窗口函数主瓣宽度与旁瓣衰减对比:")
print(f"{'窗口':<20} {'主瓣3dB宽度(Hz)':<20} {'第一旁瓣(dB)'}")
print("-" * 60)

for name, win in windows.items():
    windowed = signal * win
    fft_w = np.fft.rfft(windowed)
    mag_w = 20 * np.log10(np.abs(fft_w) / np.max(np.abs(fft_w)) + 1e-10)
    freq_w = np.fft.rfftfreq(len(t), 1/fs)
   
    # 估算主瓣宽度(3dB点)
    peak_idx = np.argmax(np.abs(fft_w))
    peak_val = np.abs(fft_w[peak_idx])
    half_power = peak_val / np.sqrt(2)
    above_half = np.where(np.abs(fft_w) >= half_power)[0]
    main_lobe_width = (above_half[-1] - above_half[0]) * fs / len(t)
   
    # 估算第一旁瓣
    side_lobe_region = np.abs(fft_w)[:peak_idx] if peak_idx > 5 else np.abs(fft_w)[peak_idx+5:]
    if len(side_lobe_region) > 0:
        first_side = np.max(side_lobe_region)
        side_db = 20 * np.log10(first_side / peak_val + 1e-10)
    else:
        side_db = 0
   
    print(f"{name:<20} {main_lobe_width:<20.1f} {side_db:.1f}")

窗口函数的选择需要在主瓣宽度和旁瓣衰减之间权衡:

窗口函数 主瓣宽度 旁瓣衰减 适用场景
矩形窗 最窄 最差(-13dB) 频率精度优先
Hanning 中等 良好(-31dB) 通用频谱分析
Hamming 中等 较好(-42dB) 语音处理
Blackman 较宽 优秀(-58dB) 高动态范围
Kaiser 可调 可调 灵活定制

2.1 短时傅里叶变换(STFT)

对于频率随时间变化的非平稳信号,全局FFT无法捕捉时变特性。短时傅里叶变换通过滑动窗口实现时频分析:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from scipy.signal import stft

# 生成变频信号:前半段100Hz,后半段300Hz
fs = 2000
t = np.arange(0, 2, 1/fs)
signal = np.where(t < 1,
                  np.sin(2*np.pi*100*t),
                  np.sin(2*np.pi*300*t))

# 计算STFT
f, t_stft, Zxx = stft(signal, fs=fs, nperseg=256, noverlap=200)

print(f"STFT输出形状: {Zxx.shape}")
print(f"频率范围: {f[0]:.1f}Hz - {f[-1]:.1f}Hz")
print(f"时间范围: {t_stft[0]:.3f}s - {t_stft[-1]:.3f}s")

# 在1秒附近检查频率变化
idx_05s = np.argmin(np.abs(t_stft - 0.5))
idx_15s = np.argmin(np.abs(t_stft - 1.5))
peak_freq_early = f[np.argmax(np.abs(Zxx[:, idx_05s]))]
peak_freq_late = f[np.argmax(np.abs(Zxx[:, idx_15s]))]
print(f"0.5秒处主频: {peak_freq_early:.0f}Hz")  # ~100Hz
print(f"1.5秒处主频: {peak_freq_late:.0f}Hz")  # ~300Hz

三、SciPy信号滤波实战

滤波是信号处理中最常见的操作——从传感器数据中去除噪声、从音频中提取特定频段、从脑电信号中分离不同节律。SciPy的

1
scipy.signal

模块提供了完整的滤波工具链。

3.1 基本滤波器类型与设计

SciPy支持两大类滤波器设计方法:

  • IIR(无限脉冲响应)滤波器:Butterworth、Chebyshev I/II、Elliptic等,阶数低、计算快但可能有相位失真
  • FIR(有限脉冲响应)滤波器:线性相位、稳定但阶数高、计算量大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from scipy.signal import butter, filtfilt, lfilter, freqz

# 设计Butterworth低通滤波器
fs = 1000  # 采样率
cutoff = 100  # 截止频率100Hz
order = 5

b, a = butter(order, cutoff, btype='low', fs=fs)

# 查看滤波器频率响应
w, h = freqz(b, a, worN=2000, fs=fs)
response_db = 20 * np.log10(np.abs(h) + 1e-10)

cutoff_idx = np.argmin(np.abs(w - cutoff))
print(f"截止频率{cutoff}Hz处衰减: {response_db[cutoff_idx]:.1f}dB")
print(f"通带波纹(50Hz处): {response_db[np.argmin(np.abs(w-50))]:.1f}dB")
print(f"阻带衰减(200Hz处): {response_db[np.argmin(np.abs(w-200))]:.1f}dB")
# 输出:
# 截止频率100Hz处衰减: -3.0dB
# 通带波纹(50Hz处): -0.0dB
# 阻带衰减(200Hz处): -30.5dB

3.2 零相位滤波:filtfilt vs lfilter

IIR滤波器会引入相位延迟,在许多应用中这是不希望的。

1
filtfilt

通过前向-后向两次滤波实现零相位失真,代价是滤波器阶数等效翻倍:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 生成测试信号:5Hz有用信号 + 50Hz噪声
fs = 500
t = np.arange(0, 2, 1/fs)
clean = np.sin(2*np.pi*5*t)
noise = 0.5 * np.sin(2*np.pi*50*t)
signal = clean + noise

# 设计低通滤波器(截止20Hz)
b, a = butter(4, 20, btype='low', fs=fs)

# 方法1:单向滤波(有相位延迟)
filtered_one = lfilter(b, a, signal)

# 方法2:零相位滤波(无相位延迟)
filtered_zero = filtfilt(b, a, signal)

# 对比相位偏移
mid = len(t) // 2
phase_offset_one = np.argmax(np.correlate(clean[mid-200:mid+200],
                                           filtered_one[mid-200:mid+200], 'full')) - 400
phase_offset_zero = np.argmax(np.correlate(clean[mid-200:mid+200],
                                            filtered_zero[mid-200:mid+200], 'full')) - 400

print(f"单向滤波相位偏移: {phase_offset_one} 采样点")
print(f"零相位滤波相位偏移: {phase_offset_zero} 采样点")

# 对比滤波效果
noise_remaining_one = np.std(filtered_one - clean)
noise_remaining_zero = np.std(filtered_zero - clean)
print(f"单向滤波残余噪声标准差: {noise_remaining_one:.4f}")
print(f"零相位滤波残余噪声标准差: {noise_remaining_zero:.4f}")

3.3 带通与带阻滤波

实际应用中,我们常常需要提取或去除特定频段。带通滤波保留指定频率范围,带阻滤波(陷波器)去除特定频率干扰:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 场景:心电信号处理,提取0.5-40Hz有效频段
fs = 360  # MIT-BIH心电数据库标准采样率
t = np.arange(0, 5, 1/fs)

# 模拟心电信号(简化)
ecu = np.sin(2*np.pi*1*t)  # 基线
ecu += 0.5 * np.sin(2*np.pi*10*t)  # QRS波成分
ecu += 0.3 * np.sin(2*np.pi*25*t)  # 高频成分
ecu += 0.8 * np.sin(2*np.pi*60*t)  # 工频干扰(60Hz)
ecu += 0.4 * np.random.randn(len(t))  # 肌电噪声

# 带通滤波:0.5-40Hz
b_bp, a_bp = butter(4, [0.5, 40], btype='band', fs=fs)
ecu_bp = filtfilt(b_bp, a_bp, ecu)

# 陷波滤波:去除60Hz工频
b_notch, a_notch = butter(4, [58, 62], btype='bandstop', fs=fs)
ecu_notch = filtfilt(b_notch, a_notch, ecu)

# 先带通再陷波
ecu_clean = filtfilt(b_notch, a_notch, ecu_bp)

print(f"原始信号SNR估算: {10*np.log10(np.var(ecu - 0.8*np.sin(2*np.pi*60*t)) / np.var(0.8*np.sin(2*np.pi*60*t))):.1f}dB")
print(f"带通滤波后60Hz幅值: {np.max(np.abs(np.fft.rfft(ecu_bp)[np.argmin(np.abs(np.fft.rfftfreq(len(t),1/fs)-60))]))/len(t)*2:.3f}")
print(f"陷波滤波后60Hz幅值: {np.max(np.abs(np.fft.rfft(ecu_notch)[np.argmin(np.abs(np.fft.rfftfreq(len(t),1/fs)-60))]))/len(t)*2:.3f}")
print(f"组合滤波后60Hz幅值: {np.max(np.abs(np.fft.rfft(ecu_clean)[np.argmin(np.abs(np.fft.rfftfreq(len(t),1/fs)-60))]))/len(t)*2:.3f}")

四、FIR滤波器设计与高级技巧

FIR滤波器具有严格的线性相位特性,在对相位敏感的应用(如音频处理、通信系统)中不可替代。SciPy提供了多种FIR设计方法:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from scipy.signal import firwin, firwin2, remez, freqz_zpk

fs = 1000

# 方法1:窗函数法设计低通FIR
taps_window = firwin(101, 100, fs=fs)  # 101阶,截止100Hz

# 方法2:频率采样法
taps_fsamp = firwin2(101, [0, 90, 110, fs/2], [1, 1, 0, 0], fs=fs)

# 方法3:Parks-McClellan最优等波纹设计
taps_remez = remez(101, [0, 80, 120, fs/2], [1, 0], fs=fs)

# 对比过渡带宽度和阻带衰减
for name, taps in [('窗函数法', taps_window),
                   ('频率采样法', taps_fsamp),
                   ('等波纹法', taps_remez)]:
    w, h = freqz(taps, worN=2000, fs=fs)
    h_db = 20 * np.log10(np.abs(h) + 1e-10)
   
    # 阻带衰减(150Hz以上最大值)
    stopband = h_db[w > 150]
    stop_atten = np.min(stopband)
   
    # 过渡带宽度(-3dB到-40dB)
    pass_edge = w[np.argmin(np.abs(h_db + 3))]
    stop_edge = w[np.argmin(np.abs(h_db[:np.argmin(np.abs(h_db+40))] + 40))] if np.any(h_db < -40) else fs/2
   
    print(f"{name}: 阻带衰减={-stop_atten:.1f}dB")

print(f"\nFIR阶数: {len(taps_window)} (线性相位,群延迟={(len(taps_window)-1)//2}采样点)")

4.1 多速率信号处理

当需要改变采样率时——如降采样减少数据量或上采样进行精细处理——SciPy提供了

1
decimate

1
resample

1
upfirdn

等工具:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from scipy.signal import decimate, resample

# 原始信号:高采样率采集
fs_high = 10000
t = np.arange(0, 1, 1/fs_high)
signal = np.sin(2*np.pi*100*t) + 0.5*np.sin(2*np.pi*250*t)

# 降采样到1000Hz(10倍抽取,自动抗混叠滤波)
signal_dec = decimate(signal, 10, ftype='fir')
fs_dec = fs_high // 10

# 重采样到2000Hz
signal_resamp = resample(signal, int(len(signal) * 2000 / fs_high))
fs_resamp = 2000

print(f"原始: {len(signal)} 采样点, fs={fs_high}Hz")
print(f"抽取: {len(signal_dec)} 采样点, fs={fs_dec}Hz")
print(f"重采样: {len(signal_resamp)} 采样点, fs={fs_resamp}Hz")

# 验证抽取后频谱正确性
fft_orig = np.abs(np.fft.rfft(signal)) / len(signal) * 2
fft_dec = np.abs(np.fft.rfft(signal_dec)) / len(signal_dec) * 2

freq_orig = np.fft.rfftfreq(len(signal), 1/fs_high)
freq_dec = np.fft.rfftfreq(len(signal_dec), 1/fs_dec)

# 100Hz处的幅值应该保持
amp_100_orig = fft_orig[np.argmin(np.abs(freq_orig - 100))]
amp_100_dec = fft_dec[np.argmin(np.abs(freq_dec - 100))]
print(f"\n100Hz幅值 - 原始: {amp_100_orig:.3f}, 抽取后: {amp_100_dec:.3f}")

五、功率谱密度估计

功率谱密度(PSD)描述信号功率在频域的分布,是随机信号分析的核心工具。SciPy提供了周期图法和Welch方法两种主要估计方式:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from scipy.signal import welch, periodogram

# 生成含噪声的信号
fs = 1000
t = np.arange(0, 10, 1/fs)  # 10秒长信号
np.random.seed(42)

# 有用信号 + 白噪声
signal = np.sin(2*np.pi*50*t) + 0.5*np.sin(2*np.pi*120*t)
noise = 2 * np.random.randn(len(t))
x = signal + noise

# 方法1:周期图法(方差大,不推荐直接使用)
f_pgm, Pxx_pgm = periodogram(x, fs=fs)

# 方法2:Welch方法(分段平均,方差小)
f_welch, Pxx_welch = welch(x, fs=fs, nperseg=1024, noverlap=512)

# 方法3:Welch方法使用不同窗口长度
f_welch_long, Pxx_welch_long = welch(x, fs=fs, nperseg=4096, noverlap=3072)

print("功率谱密度估计方法对比:")
print(f"{'方法':<25} {'50Hz峰值(dB)':<15} {'方差(1dB附近)'}")
print("-" * 55)

for name, f, Pxx in [('周期图法', f_pgm, Pxx_pgm),
                      ('Welch(nperseg=1024)', f_welch, Pxx_welch),
                      ('Welch(nperseg=4096)', f_welch_long, Pxx_welch_long)]:
    # 50Hz处的PSD值
    idx_50 = np.argmin(np.abs(f - 50))
    peak_db = 10 * np.log10(Pxx[idx_50])
   
    # 估算平坦区域的方差
    flat_mask = (f > 200) & (f < 400)
    flat_db = 10 * np.log10(Pxx[flat_mask])
    variance = np.var(flat_db)
   
    print(f"{name:<25} {peak_db:<15.1f} {variance:.2f}")

Welch方法的关键参数选择:

  • nperseg:每段长度,越长频率分辨率越高但平滑效果越差
  • noverlap:段间重叠,通常设为nperseg的50%-75%
  • window:窗口函数,默认Hanning窗
  • 工程经验:nperseg=fs*2可获得0.5Hz的频率分辨率

六、实战案例:振动信号分析与故障诊断

将前面所学技术综合应用到旋转机械振动信号分析中。这是工业界最典型的傅里叶变换应用场景之一:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 模拟轴承振动信号
fs = 12000  # 采样率12kHz(振动分析常用)
t = np.arange(0, 5, 1/fs)
np.random.seed(123)

# 转频25Hz(1500rpm)
rotor_freq = 25

# 正常轴承:只有转频及其谐波
normal = 2 * np.sin(2*np.pi*rotor_freq*t)  # 1X
normal += 0.8 * np.sin(2*np.pi*2*rotor_freq*t)  # 2X
normal += 0.3 * np.sin(2*np.pi*3*rotor_freq*t)  # 3X
normal += 0.5 * np.random.randn(len(t))  # 背景噪声

# 故障轴承:出现特征频率
bpfo = 89.5  # 外圈故障频率(Ball Pass Frequency Outer)
bpfi = 130.5  # 内圈故障频率
fault = normal.copy()
fault += 1.5 * np.sin(2*np.pi*bpfo*t)  # 外圈故障特征
fault += 0.8 * np.sin(2*np.pi*bpfi*t)  # 内圈故障特征

# 步骤1:带通滤波去除低频漂移和高频噪声
b, a = butter(5, [10, 1000], btype='band', fs=fs)
normal_filt = filtfilt(b, a, normal)
fault_filt = filtfilt(b, a, fault)

# 步骤2:Welch功率谱估计
f_n, Pxx_n = welch(normal_filt, fs=fs, nperseg=4096)
f_f, Pxx_f = welch(fault_filt, fs=fs, nperseg=4096)

# 步骤3:特征提取
print("===== 振动分析诊断报告 =====")
print(f"转频: {rotor_freq}Hz")
print(f"外圈故障频率(BPFO): {bpfo}Hz")
print(f"内圈故障频率(BPFI): {bpfi}Hz")
print()

# 检测特征频率处的幅值
for name, f_arr, Pxx_arr in [('正常', f_n, Pxx_n), ('故障', f_f, Pxx_f)]:
    amp_1x = 10*np.log10(Pxx_arr[np.argmin(np.abs(f_arr - rotor_freq))])
    amp_bpfo = 10*np.log10(Pxx_arr[np.argmin(np.abs(f_arr - bpfo))])
    amp_bpfi = 10*np.log10(Pxx_arr[np.argmin(np.abs(f_arr - bpfi))])
    print(f"{name}轴承:")
    print(f"  1X({rotor_freq}Hz): {amp_1x:.1f}dB")
    print(f"  BPFO({bpfo}Hz): {amp_bpfo:.1f}dB")
    print(f"  BPFI({bpfi}Hz): {amp_bpfi:.1f}dB")

# 步骤4:包络分析(用于冲击特征检测)
from scipy.signal import hilbert
analytic = hilbert(fault_filt)
envelope = np.abs(analytic)

# 包络谱
f_env, Pxx_env = welch(envelope, fs=fs, nperseg=4096)
print(f"\n包络谱BPFO处峰值: {10*np.log10(Pxx_env[np.argmin(np.abs(f_env - bpfo))]):.1f}dB")
print("(包络谱能有效凸显周期性冲击特征)")

七、性能优化与大规模数据处理

处理大规模信号数据时,FFT的计算效率至关重要。以下是几个实用的性能优化技巧:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import time

# 1. 利用FFT对2的幂次长度的优化
sizes = [99991, 100000, 131072]  # 质数、普通数、2的幂
for n in sizes:
    data = np.random.randn(n)
    start = time.perf_counter()
    for _ in range(100):
        np.fft.fft(data)
    elapsed = time.perf_counter() - start
    print(f"N={n:<8} 100次FFT耗时: {elapsed:.3f}s")

# 2. 使用rfft处理实数信号
data = np.random.randn(1000000)
start = time.perf_counter()
_ = np.fft.fft(data)
t_fft = time.perf_counter() - start

start = time.perf_counter()
_ = np.fft.rfft(data)
t_rfft = time.perf_counter() - start

print(f"\n100万点: fft={t_fft:.4f}s, rfft={t_rfft:.4f}s, 加速比={t_fft/t_rfft:.2f}")

# 3. 分块处理超长信号
chunk_size = 2**20  # 1M点一块
total_samples = 10 * chunk_size  # 10M点
data = np.random.randn(total_samples)

start = time.perf_counter()
results = []
for i in range(0, total_samples, chunk_size):
    chunk = data[i:i+chunk_size]
    results.append(np.fft.rfft(chunk))
spectrum = np.mean(np.abs(results), axis=0)
elapsed = time.perf_counter() - start
print(f"\n分块处理10M点: {elapsed:.3f}s")

# 4. 预分配与就地操作
output = np.empty(500001, dtype=complex)
start = time.perf_counter()
np.fft.rfft(data[:1000000], out=output)
print(f"就地FFT: {time.perf_counter()-start:.4f}s")

性能优化的核心原则总结:

  • 优先使用
    1
    rfft

    处理实数信号,计算量减半

  • 数据长度尽量凑成2的幂次,可提升2-5倍速度
  • 超长信号采用分块+平均策略,兼顾速度和内存
  • 使用
    1
    scipy.fft

    替代

    1
    numpy.fft

    可自动利用多核并行

  • 实时场景中考虑
    1
    pyFFTW

    库,它使用FFTW算法并支持wisdom预规划

八、总结与最佳实践

本文系统介绍了Python科学计算中傅里叶变换与信号处理的完整工作流。以下是核心要点回顾:

  • 频谱分析:使用
    1
    np.fft.rfft

    处理实数信号,注意幅值还原(除以N乘2)和频率分辨率(fs/N)

  • 频谱泄漏:非整数周期截断导致泄漏,选择合适窗口函数(Hanning通用、Blackman高动态范围、Kaiser灵活可调)
  • 时频分析:STFT通过
    1
    scipy.signal.stft

    实现,适用于非平稳信号

  • 滤波设计:IIR用
    1
    butter

    等函数设计,FIR用

    1
    firwin

    /

    1
    remez

    设计,零相位滤波优先选

    1
    filtfilt
  • 功率谱估计:Welch方法是工业标准,合理选择nperseg平衡分辨率与方差
  • 性能优化:2的幂次长度、rfft、分块处理、scipy.fft并行

在实际工程中,建议遵循以下工作流:先对原始信号做目视检查和统计描述 → 应用合适的窗口函数 → FFT频谱分析定位特征频率 → 根据频域信息设计滤波器 → 滤波后再次频谱验证 → 如有需要进入时频分析或包络分析。这种从全局到局部、从时域到频域的系统分析方法,能够有效解决绝大多数信号处理问题。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » Python科学计算中的傅里叶变换实战:NumPy与SciPy信号处理从频谱分析到滤波设计
分享到: 更多 (0)