拓冰建站拓冰建站
首页 / 资讯中心 / 正文

用Python脚本点亮信号与系统课堂:卷积、傅里叶与滤波演示

简介面向中山大学信号与系统课程教学的一套Python脚本设计源码集合主要服务于电子信息、电气工程等专业本科生及任课教师。资源紧扣卷积、傅里叶变换、离散余弦变换等核心知识点以代码与可视化结合的方式降低理论理解门槛。包内共104个文件约16.91MB既有68幅PNG信号波形与频谱图、18个Python算法脚本还包含GIF动态演示、Jupyter Notebook交互文档及XML/Markdown配置说明能覆盖课堂教学、课后实验与自主拓展多种场景。目前已有308人学习下载。通过这套资源学习者可以对照Python实现逐段理解卷积、滤波、频谱分析等操作的代码逻辑借助可视化图像与动态动画观察信号变化过程教师也可直接引用Notebook开展随堂演示或利用脚本快速生成教学用图有效提升信号与系统课程的实践性与互动性。1. 面向信号与系统课程教学的 Python 脚本源码先从课堂断层说起老师在讲台上推导傅里叶变换公式学生抄下来了可课件一翻页频谱长什么样全靠想象。信号与系统课程的课堂断层就落在这里时域波形直观可见频域看不见摸不着卷积的翻转平移在教材上是三张交替的箭头图到了编程环节却不知道从哪一行代码开始写。这套 Python 脚本源码要解决的是把课程里十几个抽象概念变成可运行、可调参、可输出图片的演示工具。它面向中山大学信号与系统课程教学也适用于其他高校同类课程服务正在修课的学生、备课的助教以及想快速搭建信号处理原型的工程师。脚本不追求算法性能上的极致只求把概念讲清楚、把参数调明白。2. 从课程知识点到 Python 脚本源码的模块划分与参数约定2.1 教学脚本源码按信号生成、运算、可视化三层划分写这类教学源码第一原则是别把逻辑写成一坨。很多初学者习惯一个文件从头写到底信号生成、FFT、画图全纠缠在一起改一个采样率要滚动半个编辑器窗口。我一般会按课程内容把脚本拆成三层层与层之间单向依赖。信号生成层只做一件事给定时间轴返回信号取值数组。单位阶跃、矩形脉冲、指数衰减、正弦调幅全部写成统一签名的函数输入参数风格一致返回值都是numpy.ndarray。这一层不关心信号拿去卷积还是画图职责单一。运算层负责卷积、傅里叶变换、离散序列变换这些核心计算。这一层不import matplotlib不读任何配置文件输入数组、输出数组。保持纯粹的好处是方便单独做数值验证也能在命令行环境里快速跑算法而不用初始化图形界面。可视化层统一封装 matplotlib 绘图逻辑。时域波形子图、幅度谱子图、卷积过程三联动图都收敛成几个绘图函数。助教调整配色、字体、图注时不需要碰前两层代码学生修改参数时也不会误删核心算法。三层之间的调用方向是固定的可视化层调用运算层和生成层运算层只依赖生成层的数据结构生成层不依赖任何上层模块。2.2 连续信号与离散序列在脚本里的统一存储方式信号与系统课程里同时存在连续时间信号和离散时间序列两类对象但落到 Python 脚本里它们都只能是有限长度的数组。常见做法是全部用numpy.ndarray存储并始终配套维护一个时间轴或序号轴避免只用裸数据。连续信号用t np.arange(N) / fs生成时间轴fs是采样率单位 Hz脚本里默认取 1000。这个默认值不是随手写的课程演示常用的 1 Hz 到 100 Hz 信号用 1000 Hz 采样画出来波形足够平滑而且时间轴每格换算成毫秒非常直观学生读图时容易建立量级感。离散序列则直接用n np.arange(N)作为横轴n是整数序号。两套轴不能在脚本里混用。我在源码命名上做了强制区分连续信号函数统一带ct_前缀离散序列带dt_前缀靠命名约束提醒调用者当前对象属于哪一类。最常见的错误是拿连续信号的时间轴去画离散序列的 stem 图横轴小数点多得离谱封装层通过参数类型校验能提前挡住这一类低级轴错位。2.3 公共参数约定采样率 fs、时长 T、点数 N 的关系教学脚本能不能在课堂上直接演示取决于参数约定是否一致。源码头部固定一组公共参数并写成表格注释放在配置区助教上课前只改配置区的数字不深入函数体。参数含义默认值脚本内约定fs采样率 (Hz)1000所有连续信号共用不可单独覆盖T演示时长 (s)1.0配合 fs 决定总点数N采样点数int(fs * T)不手动设置避免与 fs/T 矛盾t0跳变时刻 (s)0.0阶跃、脉冲边沿位置N不设成手动参数是这个约定的关键。一旦学生手动设N 500又另设fs 1000、T 1.0时间轴长度和采样率就对不上画出来的波形横轴与实际物理时间完全脱节。源码在make_time_axis内部强制用int(fs * T)计算N并在函数入口检查传入数组长度与时间轴长度是否一致不一致直接抛异常从根上排除这类自相矛盾的参数组合。3. 波形生成与卷积运算的 Python 脚本落地实现3.1 生成阶跃、矩形脉冲和指数衰减信号的脚本模板信号生成层的代码模板如下这套函数在每个演示脚本里都会直接 import属于最基础的一块地基。import numpy as np def make_time_axis(fs1000, T1.0): 生成时间轴0 到 T 秒共 fs*T 个采样点 N int(fs * T) return np.arange(N) / fs def unit_step(t, t00.0): 单位阶跃信号t t0 时值为 1否则为 0 return (t t0).astype(float) def rect_pulse(t, start-0.1, stop0.1): 矩形脉冲区间 [start, stop) 内为 1其余为 0 return ((t start) (t stop)).astype(float) def exp_decay(t, alpha5.0): 单边指数衰减信号exp(-alpha*t) * u(t) return np.exp(-alpha * t) * (t 0)逻辑不复杂但三个细节值得展开说明。astype(float)把布尔比较结果转成 0/1 浮点数组后续和np.exp这类浮点运算混合时不会出现类型断裂。矩形脉冲右边界用左闭右开即start包含、stop不包含这样两个脉冲首尾相接时不会在接缝处重复计数卷积边界也更干净。指数衰减信号里(t 0)是硬截断数学上等价于乘单位阶跃写成这样比np.where更贴近教材公式学生对照起来不费劲。参数设计也经过权衡。t0、start、stop全部带默认值课堂上演示时只需要调用exp_decay(t, alpha8.0)就能改变衰减速度不用把时间轴重复传一遍。每个函数的 docstring 第一句就是一句话功能说明学生用help()查询时能秒懂用途这对课程脚本的传播很重要。3.2 卷积和的三种实现教材定义、numpy 卷积与 FFT 方法卷积和的课程定义是y[n] Σ x[k] h[n-k]教材上用箭头图解释翻转平移。落到代码里有三种不同实现我全部保留在源码里分别对应不同教学阶段。第一种完全按定义实现三层循环可读性最高def conv_direct(x, h): 按卷积和定义直接实现三重循环仅供理解原理 nx, nh len(x), len(h) y np.zeros(nx nh - 1) for n in range(nx nh - 1): for k in range(nx): if 0 n - k nh: y[n] x[k] * h[n - k] return y外层循环遍历输出序列的每个索引n内层遍历x的索引k条件判断n-k是否落在h的有效区间内。这段代码的时间复杂度是 O(N²)但和教材公式逐项对应讲完这段再切换到 numpy 版本学生才真正理解后者内部在算什么。第二种用np.convolve(x, h)一行出结果内部是编译好的 C 实现。第三种用 FFT 在频域相乘再逆变换时间 O(N log N)但必须补零到NM-1以上否则循环卷积会把尾部混叠到头部。三种方式的结果差异和适用场景固定为一张表放在源码注释里实现方式复杂度结果长度适用场景conv_direct 循环实现O(N²)NM-1课堂讲原理np.convolveO(NM)NM-1一般数值计算FFT 频域相乘O(N log N)需补零到 2 的幂大数据量课堂上我会把 5000 点信号与 500 点冲激响应分别用三种方式跑一遍直接循环实现有明显卡顿FFT 几乎瞬间出结果。这时候再引出时域卷积等于频域相乘学生就有了计算复杂度的直观体感而不仅是背一个结论。3.3 卷积结果绘图时横轴错位与边界效应的处理画卷积图是脚本里最容易翻车的地方。np.convolve返回数组长度是NM-1但默认横轴从 0 开始如果输入信号的时间起点不是 0画出来的卷积在时间轴上整体错位图形形状是正确的位置却是错的。我封装了专用绘图函数解决这个问题def plot_conv(x, t_x, h, t_h, fs): 绘制 x 与 h 的卷积结果横轴按物理时间对齐 t_x, t_h: 两个信号各自的时间轴 y np.convolve(x, h) t_start t_x[0] t_h[0] t_step 1.0 / fs t_y t_start np.arange(len(y)) * t_step plt.plot(t_x, x, labelx(t)) plt.plot(t_h, h, labelh(t)) plt.plot(t_y, y, labely(t) x(t) * h(t)) plt.legend()关键在t_start t_x[0] t_h[0]这一行。卷积结果的起始时刻是两信号起始时刻之和步长取公共采样间隔。这一行必须配注释写清楚否则学生改了信号起点后卷积波形和输入波形怎么都对不齐。边界效应的处理同样重要。np.convolve默认modefull输出完整卷积段首尾各有一段信号未完全重叠的过渡区modesame则裁剪到与x等长方便叠加对比。教学脚本默认展示full因为它完整呈现了信号进入、重叠、退出的全过程这正是课程要讲的东西。源码里通过mode参数开放切换课堂上可以现场演示两种模式的区别。4. 傅里叶变换与滤波仿真的脚本参数调优4.1 用 numpy.fft 做频谱分析时频率轴与幅度归一化给信号做完 FFT第一步是构造频率轴。np.fft.fftfreq(N, d1/fs)返回对称的双边频率数组单位 Hzd是采样间隔。教学脚本里我把频谱绘制封装成下面的函数参数说明直接写在 docstring 里def plot_single_spectrum(t, x, fs, fmaxNone): 绘制单边幅度谱 t: 时间轴; x: 信号数组; fs: 采样率 fmax: 只显示 0~fmax Hz 范围 N len(x) X np.fft.fft(x) freqs np.fft.fftfreq(N, d1/fs) half N // 2 mag 2.0 / N * np.abs(X[:half]) plt.plot(freqs[:half], mag) plt.xlabel(Frequency (Hz)) plt.ylabel(Magnitude)调整参数时有三个高频出错点。第一是幅度系数np.fft.fft的结果是原信号幅值的 N 倍直接画出来峰值高得离谱必须除以 N。第二是单边谱的 2 倍系数负频率的能量合并到正频率一侧除直流分量外都要乘 2。第三是half取N // 2当 N 是奇数时多出的那个点归入负频侧单边谱只保留前一半。用这个脚本画 5 Hz 正弦波峰值应精确落在 5 Hz 且幅值接近 1。如果峰值明显偏移说明频率分辨率fs/N不够小。想区分频率差 1 Hz 的两个分量点数 N 必须大于 fs。fmax参数则把频率轴裁剪到指定范围避免高频段的毛刺把低频主峰的幅度视觉上压小。4.2 理想低通与巴特沃斯滤波脚本的截止频率归一化滤波仿真脚本里最常出错的参数是scipy.signal.butter的截止频率。butter的Wn参数要求归一化到奈奎斯特频率fs/2漏掉这一步截止频率的实际位置会比预期偏大fs/2倍低通直接变成几乎不起作用。对比代码写成下面这样from scipy import signal def lowpass_butter(x, fs, cutoff_hz, order1): 巴特沃斯低通滤波cutoff_hz 为以 Hz 为单位的截止频率 wn cutoff_hz / (fs / 2) # 归一化到奈奎斯特频率 b, a signal.butter(order, wn, btypelow) return signal.lfilter(b, a, x) def lowpass_ideal(x, fs, cutoff_hz): 理想低通频域直接截断再逆变换用于演示吉布斯现象 X np.fft.fft(x) freqs np.fft.fftfreq(len(x), d1/fs) X[np.abs(freqs) cutoff_hz] 0 return np.fft.ifft(X).real巴特沃斯版本把cutoff_hz / (fs / 2)这一步单独拎出来做变量理由是想让学生一眼看到归一化过程。理想低通直接在频域把高于截止频率的分量置零逆变换后波形在跳变处会出现振铃幅度约为原信号幅度的 9%这就是吉布斯现象。课堂上把两种滤波结果画在同一张图里。理想低通边缘锐利但有振铃巴特沃斯平滑无振铃但过渡带倾斜一阶滤波器在截止频率处已经衰减到 -3 dB 并伴随约 45 度相位滞后。把阶数从 1 调到 4过渡带变陡相位失真却更明显。这个频率选择性与相位失真的互相制约关系靠讲理论很难建立直觉脚本里调一次阶数就清楚了。4.3 抽样定理演示脚本的混叠假峰与采样率选取抽样定理的脚本演示是信号与系统课程里最能出效果的部分。演示脚本默认生成 20 Hz 正弦信号采样率从 10 Hz 到 80 Hz 分档切换每档输出一张频谱图观察假峰如何出现、如何移动。混叠的核心表现是欠采样时频谱上出现假峰位置在fs - f处。20 Hz 信号用 15 Hz 采样时假峰出现在 15 - 20 -5 Hz单边谱显示为 5 Hz 的正频率峰值。脚本里把假峰保留并在图上用箭头标注混叠来源学生能直接看到假峰与真实频率之间的距离关系。这里有一个值得专门讲的反直觉细节当采样率刚好是信号频率的整数倍时采样点可能恰好落在正弦波的零交叉处频谱主峰反而消失。因此演示抽样定理时采样率要刻意避开信号频率的整数倍。20 Hz 信号用 45 Hz 采样混叠假峰出现在 45 - 20 25 Hz和教材上的镜像频谱图完全对应讲课解释起来非常顺畅。这个场景通过命令行参数直接启动现场改采样率就能观察假峰位置的变化规律。5. 用解析解断言与命令行参数提升教学脚本源码的可用性5.1 在脚本里嵌入解析解断言画图之前先验算教学脚本最大的风险是数值算错了但图看起来一切正常。我习惯在每个核心算法后、绘图调用前用课程里已有的解析解做断言验证。卷积示例中两个幅度为 1、宽度分别为 a 和 b 的矩形脉冲卷积得到三角波峰值等于 a*b。脚本里这样写# 两个矩形脉冲宽度 0.2s 和 0.1s幅度均为 1 y np.convolve(rect_pulse(t, 0, 0.2), rect_pulse(t, 0, 0.1)) assert abs(np.max(y) - 0.2) 1e-9, 卷积峰值与解析解不符断言写在画图之前一旦算法被改动导致峰值偏差脚本会在生成图片前直接报错中断错误消息指向明确。傅里叶变换脚本里同样可以验证 5 Hz 正弦在 5 Hz 处的幅值应等于 1容差取1e-9。这套断言相当于给教学脚本加了一层自动化单元测试学生改完参数跑一遍不报错至少说明数值计算没有偏离基本原理。5.2 通过命令行参数切换演示场景避免上课改代码助教上课时最怕临时改源码。教学脚本库统一加了argparse命令行入口演示场景通过参数切换python demos/conv_demo.py --demo conv --fs 1000 python demos/sampling_demo.py --demo sampling --fs 45 python demos/fourier_demo.py --demo spectrum --fs 200 --fmax 50--fs覆盖默认采样率--fmax限制频谱横轴显示范围。所有演示脚本默认输出 png 图片而不是弹出 matplotlib 窗口课堂现场弹窗容易卡死预渲染成图片插入课件更流畅图片统一dpi120投影仪上坐标刻度依然清晰。这套设计的核心是把脚本的使用方式和内部实现分离学生上课只需记住几条命令深入理解时再打开源码阅读函数。5.3 公共函数库与演示脚本分离的源码组织方式教学脚本源码不建议一个文件塞几百行。我现在的组织方式是公共函数库加多个演示脚本目录结构固定不变signal_lib/ # 公共函数库 waveform.py # 信号生成函数 operations.py # 卷积、FFT 封装 plotters.py # 绘图封装 demos/ # 每个知识点一个演示脚本 conv_demo.py # 卷积演示 fourier_demo.py # 频谱分析演示 sampling_demo.py # 抽样定理演示公共函数库不依赖任何演示脚本演示脚本只 import 自己需要的模块。依赖方面每个脚本头部写明所需库版本requirements.txt固定 numpy、matplotlib、scipy 的大版本号避免 numpy 2.x 的接口变化把旧脚本跑挂。学生 clone 后创建虚拟环境安装依赖直接运行命令即可。源码用 git 管理每个演示脚本对应一次独立提交提交信息写清楚该脚本演示的知识点。前后两个脚本若要共享新工具函数先加进signal_lib再做独立提交学生看提交历史就能理解公共函数库是怎么逐步积累出来的。最后确认整套脚本在干净环境里能跑通再交付给助教这是教学脚本源码验收前最后一道工序。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门