第1章:Python信号处理基础
各位同学好,我是老张。在信号处理这行摸爬滚打了十几年,今天咱们聊聊Python做信号处理的基础。说实话,刚入行那会儿我用的是Matlab,后来转到Python,一开始还挺不习惯的。但用久了你会发现,Python这套生态确实香——免费、开源、社区活跃,关键是库与库之间配合得天衣无缝。
这一章,我带你过一遍最核心的几个工具:NumPy、SciPy、Matplotlib。别小看这些基础,我见过太多人上来就调库,结果数据格式不对、维度搞错,折腾半天。嗯,基础打牢了,后面才跑得快。
1.1 NumPy数组操作——信号处理的基石
信号处理说白了就是跟数组打交道。一个正弦波,在计算机里就是一组离散的采样点。NumPy的ndarray就是专门干这个的。
我个人习惯,拿到信号数据第一件事就是看shape和dtype。为什么?因为数据类型不对,后面计算全错。我曾经在项目里调试一个滤波器,折腾了两天,最后发现是数据是int16,计算时溢出了。你说冤不冤?
核心要点:NumPy数组是连续内存块,支持向量化运算。这意味着你写y = 2 * x + 1,它会对每个元素并行操作,比Python原生循环快几十倍。
import numpy as np
# 创建一个一维数组,模拟采样点
t = np.linspace(0, 1, 1000) # 0到1秒,1000个点
print(t.shape) # (1000,)
print(t.dtype) # float64
# 数组切片——提取前100个点
t_first_100 = t[:100]
# 向量化运算——生成正弦波
f = 5 # 频率5Hz
x = np.sin(2 * np.pi * f * t) # 一行代码,搞定1000个点的计算
你想想看,如果用Python原生列表,得写个for循环,1000个点还好,要是100万个点呢?NumPy的底层是C语言实现的,性能差距就在这里。
小技巧:创建数组时尽量指定dtype。比如你知道信号是浮点数,就用np.float32,比默认的float64省一半内存。我在嵌入式信号处理项目里经常这么干。
1.2 SciPy信号模块入门——工具箱来了
NumPy给了你数组,SciPy给了你算法。scipy.signal这个模块,我几乎每个项目都会用到。它包含了滤波器设计、频谱分析、信号生成等常用函数。
为什么单独讲SciPy?因为很多人不知道NumPy和SciPy的分工。NumPy管数据结构,SciPy管算法实现。举个例子:你要生成一个方波,NumPy没有现成的函数,但SciPy有。
from scipy import signal
import numpy as np
# 生成方波信号
t = np.linspace(0, 1, 1000)
square_wave = signal.square(2 * np.pi * 5 * t) # 5Hz方波
# 生成锯齿波
sawtooth_wave = signal.sawtooth(2 * np.pi * 5 * t)
# 生成高斯噪声
noise = np.random.randn(1000) # 均值为0,标准差为1
这里要注意一点:signal.square返回的是-1到1之间的值,不是0到1。我刚开始用的时候没注意,直接把方波当开关信号用,结果逻辑全反了。嗯,这种坑踩过一次就记住了。
避坑指南:SciPy的signal模块函数参数单位不统一。有的用Hz,有的用归一化频率。比如signal.firwin的截止频率是归一化的(0到1之间,1对应采样率的一半)。我曾经把Hz直接传进去,滤波器完全不对。记住:先归一化,再传参。
1.3 Matplotlib可视化基础——让信号说话
信号处理有个特点:光看数字看不出问题,画出来就一目了然。Matplotlib就是干这个的。我个人习惯,写信号处理代码时,每生成一个信号就立刻画出来看看。别等到最后一起画,万一中间错了,排查起来很麻烦。
import matplotlib.pyplot as plt
# 创建一个画布,两个子图
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6))
# 第一个子图:正弦波
t = np.linspace(0, 1, 1000)
x = np.sin(2 * np.pi * 5 * t)
ax1.plot(t, x, 'b-', linewidth=1.5)
ax1.set_title('正弦波 (5Hz)')
ax1.set_xlabel('时间 (s)')
ax1.set_ylabel('幅度')
ax1.grid(True, alpha=0.3)
# 第二个子图:方波
y = signal.square(2 * np.pi * 5 * t)
ax2.plot(t, y, 'r-', linewidth=1.5)
ax2.set_title('方波 (5Hz)')
ax2.set_xlabel('时间 (s)')
ax2.set_ylabel('幅度')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
画图时我建议你养成几个习惯:
- 加网格:
grid(True),方便读数值 - 标单位:x轴标时间,y轴标幅度,别偷懒
- 控制线宽:默认线宽太细,打印出来看不清,我一般设1.5或2
经验之谈:调试信号时,我经常用plt.figure()开多个窗口,把原始信号、滤波后信号、频谱图并排显示。一眼就能看出问题在哪。比如滤波器有没有生效、有没有相位延迟,画出来比算半天快多了。
1.4 实战:生成与绘制基本信号
理论说完了,咱们来点实际的。下面这段代码,我带你生成三种基本信号:正弦波、方波、噪声,然后画在一起对比。
import numpy as np
from scipy import signal
import matplotlib.pyplot as plt
# 参数设置
fs = 1000 # 采样率 1000Hz
T = 1 # 时长 1秒
t = np.linspace(0, T, int(fs*T), endpoint=False)
# 1. 正弦波
f_sin = 5 # 频率5Hz
sin_wave = np.sin(2 * np.pi * f_sin * t)
# 2. 方波
f_sq = 5 # 频率5Hz
sq_wave = signal.square(2 * np.pi * f_sq * t, duty=0.5)
# 3. 高斯白噪声
noise = 0.5 * np.random.randn(len(t))
# 画图
plt.figure(figsize=(12, 8))
plt.subplot(3, 1, 1)
plt.plot(t, sin_wave, 'b-', linewidth=1.5)
plt.title('正弦波 (5Hz)')
plt.ylabel('幅度')
plt.grid(True, alpha=0.3)
plt.xlim(0, 0.5) # 只看前0.5秒,更清晰
plt.subplot(3, 1, 2)
plt.plot(t, sq_wave, 'r-', linewidth=1.5)
plt.title('方波 (5Hz, 占空比50%)')
plt.ylabel('幅度')
plt.grid(True, alpha=0.3)
plt.xlim(0, 0.5)
plt.subplot(3, 1, 3)
plt.plot(t, noise, 'g-', linewidth=0.8)
plt.title('高斯白噪声 (标准差0.5)')
plt.xlabel('时间 (s)')
plt.ylabel('幅度')
plt.grid(True, alpha=0.3)
plt.xlim(0, 0.5)
plt.tight_layout()
plt.show()
这段代码你直接复制就能跑。我特意把x轴限制在0.5秒内,因为1秒太长,波形挤在一起看不清。你想想看,5Hz的正弦波1秒才5个周期,画在1000个点上,前0.5秒能看清波形细节就够了。
关键参数说明:
| 参数 | 含义 | 我的建议 |
|---|---|---|
| fs | 采样率 (Hz) | 至少是信号最高频率的2倍,实际用5-10倍 |
| T | 信号时长 (s) | 至少包含几个完整周期,便于观察 |
| duty | 方波占空比 | 0.5是标准方波,可以调成0.3试试效果 |
| np.random.randn | 生成标准正态分布 | 乘以系数控制噪声强度 |
最后说一句:这一章的内容,说白了就是三板斧——NumPy造数据、SciPy处理数据、Matplotlib看数据。这三样玩熟了,后面讲滤波器、频谱分析、系统仿真,你上手会非常快。
下一章咱们聊聊离散时间信号和采样定理。到时候我会拿一个实际项目里的例子——音频信号采集,带你看看采样率不够会发生什么。嗯,那个例子挺有意思的,到时候见。