☰
加窗FFT演示:方窗与海明窗的频谱泄漏对比
2026/10/20 14:37:05 网站建设 项目流程

简介:一套面向数字信号处理初学者的MATLAB演示程序,聚焦加窗傅里叶变换中窗函数对频谱分析结果的影响。程序先生成特定频率的正弦波信号,随后分别对原始信号施加方窗与海明窗,再调用fft函数完成快速傅里叶变换,通过频谱对比直观展示加窗后旁瓣幅度的变化与泄漏抑制效果。用户可自由调整窗长,观察频率分辨率与时间分辨率之间的权衡,有助于在课程实验或自学中理解窗函数的核心作用。资源仅包含1个m脚本文件,压缩包大小759B,代码结构简洁,运行便捷,适合作为教学演示或本科数字信号处理实验的辅助材料。目前已有456人学习或下载。内容中对方窗的陡峭截断缺陷与海明窗的平滑过渡特点均有展开,配合代码实践可深入体会不同窗函数对频谱精确度的影响,并为后续滤波器和频谱分析相关任务打下基础。

1. 加窗傅里叶变换演示程序:一次频谱“漏油”排查,把方窗和海明窗的差别看清楚

某开发者调试振动信号检测流程时,对一段 50Hz 出头的小正弦波直接做 FFT,频谱上硬生生多出了五六条不该有的谱线,而且幅度还不小。当时的第一反应是采样率设错了,但查了半天参数全没问题,最后换成海明窗重跑一遍,那些“毛刺”瞬间消失得干干净净。这件事促成了这个 MATLAB 演示程序:对同一路正弦信号分别加方窗和海明窗做傅里叶变换,把时域波形、窗函数形态、加窗后的幅频谱放在同一张图上,直接看出频谱泄漏为什么发生、海明窗凭什么把它压下去。适合刚接触数字信号处理的同学,也适合那些面对 FFT 结果“频谱很脏”但找不到原因的开发者。整个脚本单文件可跑,改几个参数就能复现不同场景。

2. 技术原理拆解:为什么同一路正弦波,加不同窗做 FFT 结果差这么多

2.1 频谱泄漏从哪来:非整周期截断是元凶

FFT 处理的是一个有限长序列,但它的数学内核默认这个序列是无限周期信号的“一段”。也就是说,MATLAB 在算 FFT 时,会把你给的 N 个点看成周期信号的一个周期,然后按周期延拓到整个时间轴上。

问题就出在这个延拓上。如果截取的时间长度刚好是信号周期的整数倍,比如 1000Hz 采样率下取 1000 个点,对一个 100Hz 的正弦波,刚好是 10 个完整周期,首尾相位连续,延拓之后依然是一个光滑的正弦波。频谱上只有一条干净的谱线,幅度准确,位置准确。但如果你取 1024 个点,同样的 100Hz 信号在 1.024 秒内跑了 102.4 个周期,第 1024 个采样点处的相位和第 1 个采样点处对不上。周期延拓的一瞬间,波形从“结束值”直接跳回“起始值”,这个跳变在频域里就表现为一串额外的高频分量。能量本来是集中在一根谱线上的,现在扩散到好几根谱线上去了,这就是常说的频谱泄漏。

从工程角度说,泄漏的本质不是 FFT 算法算错了,而是“你给的数据不满足 FFT 的周期假设”。要解决它,思路有两条:一是凑整周期,把采样点数调到刚好覆盖整数个信号周期,但实际信号往往频率未知,凑不齐;二是让数据在边界处“软着陆”,用窗函数把序列两端平滑地衰减到零,这样延拓之后就没有跳变了。这个 demo 演示的就是第二条路。

2.2 方窗与海明窗:主瓣、旁瓣和适用场景

所谓方窗,其实就是不加窗,在 MATLAB 里可以直接用rectwin(N)生成,它给出的序列在有效区间内全是 1,边界没有任何衰减。不加窗时,FFT 的结果等同于加了方窗,泄漏最大。海明窗的数学形式是:信号每个采样点乘一个权重系数,公式为w(n) = 0.54 - 0.46 * cos(2*pi*n/(N-1)),其中 n 从 0 取到 N-1。它的特点是中间接近 1、两端衰减到 0.08 左右,不是真正归零,但已经足够把边界跳变抹平。

特性方窗(矩形窗)海明窗
主瓣宽度约 4π/N约 8π/N
旁瓣峰值衰减约 -13dB约 -43dB
频率分辨率高较低
幅值精度好需补偿
典型用途整周期截断信号、瞬态信号频谱泄漏较严重的连续信号

从表格能看出一个典型的折中:海明窗把旁瓣压低了 30 个 dB,但主瓣变宽了一倍。主瓣宽意味着两个频率靠得很近的信号在频谱上可能被“糊”成一座峰,分不开。所以实际选择窗函数时,要先问自己:是更在意“频率分辨得细”,还是更在意“频谱干净不串味”。方窗分辨率最高但容易泄漏;海明窗泄漏少但会牺牲近距离频率的分辨能力。这个 demo 把两种窗放在一起对比,就是为了把这种权衡直观地摆在你面前。

3. 程序实现拆解:信号生成、加窗、FFT、绘图四段式脚本

3.1 信号生成与初始参数:故意让频率偏离谱线

写这个 demo 第一件事不是急着加窗,而是先构造一个“能出问题”的信号。如果你拿一个完美落在谱线上的正弦波去做对比,方窗和海明窗的结果几乎一样,什么泄漏都看不到。要让演示有效果,就得让信号频率落在两根谱线之间。

下边这段代码是信号生成部分,我一般按这个方式写:

% 参数区:改这里就能复现不同场景 fs = 1024; % 采样率 Hz N = 1024; % 采样点数,总时长 1 秒 f1 = 50.3; % 信号频率,故意选非整数周期 A1 = 1.0; % 幅度 t = (0:N-1) / fs; % 时间轴 x = A1 * sin(2*pi*f1*t) + 0.3 * sin(2*pi*127.6*t);

参数说明:fs=1024意味着频率分辨率是fs/N=1Hz,也就是 FFT 出来的谱线间隔 1Hz。如果信号频率是 50Hz,正好在第 51 条谱线上,属于整周期截断,看不到泄漏。这里取 50.3Hz,它落在 50Hz 和 51Hz 两条谱线之间,直接 FFT 必然泄漏。第二路信号加了个 0.3 幅值的 127.6Hz 分量,目的是让频谱里有两个峰,这样能同时看到“泄漏谱线叠加到邻近频段”的效果。

好,代码逻辑说明:这里故意不把频率取整,就是要让信号能量无法集中在一条谱线上。如果读者想快速看对比,可以把f1改成 50 再跑一次,会发现方窗和海明窗的频谱几乎重合,这就是整周期截断的典型表现。

3.2 加窗与 FFT:归一化是读对幅值的关键

MATLAB 的fft函数返回的复数数组,幅度其实是按“N 倍放大”给出的。直接abs(fft(x))读出来的是真实幅值的 N 倍,所以必须先做归一化,常见做法是除以 N。再看单边谱,要把正频率部分的幅度乘 2,才代表原始信号的真实幅度。

% 生成窗函数,注意转置为行向量 w_rect = rectwin(N)'; % 方窗,等效于不加窗 w_hamm = hamming(N)'; % 海明窗 % 加窗并做 FFT,除以 N 归一化 X_rect = fft(x .* w_rect) / N; X_hamm = fft(x .* w_hamm) / N; % 取单边幅频谱 half = floor(N/2) + 1; f_axis = (0:half-1) * fs / N; mag_rect = abs(X_rect(1:half)); mag_rect(2:end-1) = mag_rect(2:end-1) * 2; % 直流和奈奎斯特频率不乘 2 mag_hamm = abs(X_hamm(1:half)); mag_hamm(2:end-1) = mag_hamm(2:end-1) * 2;

参数说明:half = floor(N/2)+1是因为实信号 FFT 后频谱关于奈奎斯特频率对称,只取前一半就够了。f_axis是频率轴,这里用(0:half-1)*fs/N计算,才能保证第 k 条谱线的频率是k*fs/NHz。乘 2 的时候注意首尾两点要排除,直流分量和奈奎斯特频率分量在单边谱里本身就只有一条,不需要翻倍。

加窗的顺序有一点容易搞错:先做逐点乘积x .* w,再做 FFT。不是先对信号做 FFT 再去乘频谱,后者是频域卷积,效果完全不同。逐点乘积在时域上完成了对边界不连续点的平滑,这才是泄漏抑制的关键。此外,hamming(N)返回的是列向量,而x是行向量,这里我习惯直接加一撇转置,否则 MATLAB 会报维度不匹配的错。

幅值补偿再补一句:加海明窗之后,信号总能量变小了,因为窗函数在两端把信号乘了小于 1 的系数。读幅值时会发现海明窗的峰值比方窗矮一截。这是正常的,不是代码 bug。如果希望幅值依然能读到真实幅度,可以除以窗函数的相干增益,即sum(hamming(N))/N ≈ 0.54。这个细节放到后边避坑章节再展开。

3.3 三子图可视化:一张图把泄漏和抑制过程摆出来

对比演示的关键在出图。我一般用三行子图,从上到下依次是时域波形(同时叠加上窗函数轮廓)、方窗 FFT 幅频谱、海明窗 FFT 幅频谱。这样眼睛先看时域边界跳变,再看到频谱泄漏,最后看到海明窗如何把泄漏压下去。

figure('Name', '加窗傅里叶变换对比'); subplot(3, 1, 1); plot(t, x, 'b', 'LineWidth', 0.8); hold on; plot(t, w_rect, 'k--', 'LineWidth', 1.2); plot(t, w_hamm, 'r--', 'LineWidth', 1.2); xlim([0 0.15]); % 放大前 0.15 秒,看得清边界形态 legend('原始信号', '方窗', '海明窗'); title('时域波形与窗函数'); xlabel('时间 (s)'); ylabel('幅度'); subplot(3, 1, 2); stem(f_axis, mag_rect, 'k', 'MarkerSize', 3); title('方窗 FFT 幅频谱'); xlabel('频率 (Hz)'); ylabel('幅度'); xlim([0 200]); subplot(3, 1, 3); stem(f_axis, mag_hamm, 'r', 'MarkerSize', 3); title('海明窗 FFT 幅频谱'); xlabel('频率 (Hz)'); ylabel('幅度'); xlim([0 200]);

代码逻辑说明:第一张子图里窗函数和信号的幅度不在一个数量级,但plot会自动归一化显示,看起来稍微有点重合也没关系,主要是看窗函数在两端的形态差异:方窗是一条横线,海明窗在边缘明显弯下去。第二、第三张子图用stem画频谱,比连续曲线更能看出每根谱线的离散位置。频率范围限制到 200Hz 以内,是因为这个信号的有效频率成分都集中在 150Hz 以下,高频旁瓣被xlim切掉之后,主峰附近的细节看得更清晰。

运行这段脚本后,你能明显看到:方窗的频谱在 50.3Hz 附近散开了一大片,每根谱线都有残留能量;海明窗的频谱则把能量收拢在主峰附近,旁瓣低到几乎看不见。这就是整个 demo 最核心的输出。

4. 运行与复现指南:三个参数改一改,能看到三种对比场景

4.1 文件结构与环境要求

整个演示是单个.m脚本文件,没有额外依赖,也不需要 GUI 工具箱。把上一章的三段代码按顺序粘贴进一个fft_window_demo.m文件,保存后在 MATLAB 里切到对应目录,直接回车就能运行。建议 MATLAB 版本不要太老,R2019a 之后的版本对hamming、rectwin这些信号处理函数支持都比较完整。

文件放在项目根目录,目录结构如下:

fft_window_demo/ ├── fft_window_demo.m # 主脚本,信号生成、加窗、FFT、出图全在里面 └── README.md # 可选,记录参数含义和运行步骤

有两点要注意:一是hamming和rectwin属于 Signal Processing Toolbox,如果机器上没装这个工具箱,可以手写公式生成。海明窗公式就是w = 0.54 - 0.46 * cos(2*pi*(0:N-1)/(N-1)),方窗直接w = ones(1,N)也可以,效果完全等价。二是在脚本开头加上clear; close all; clc;,避免上一次运行的变量残留影响结果。

4.2 三种典型场景:整周期截断、泄漏、加窗修复

同一套代码,只动三处参数,就能观察到三种截然不同的频谱形态。第一处是信号频率f1,第二处是采样点数N,第三处是采样率fs。把参数改成下表对应的值,然后重新运行脚本,对比三张图的差异。

场景f1 (Hz)N (点)预期现象
整周期截断501024方窗与海明窗频谱几乎一致,主峰干净
非整周期不加窗50.31024方窗频谱主峰附近出现大量旁瓣
非整周期加海明窗50.31024海明窗旁瓣明显降低,主峰变宽

场景一是基线参照,告诉你“信号本身没毛病”时频谱长什么样。场景二和场景三才是演示重点,也是实际调频信号处理时最常见的状态。建议跑完场景三之后,顺手把N改成 2048 再跑一次。采样点数变多会让频率分辨率翻倍,谱线间隔从 1Hz 变成 0.5Hz,这时海明窗主峰附近看起来会更“瘦”一些,但旁瓣依然保持低水平。这能帮你理解一个关键点:窗函数决定旁瓣高度,采样点数决定谱线密度,两件事互不影响。

4.3 频谱读数技巧:频率分辨率和幅值对不上怎么办

看频谱图时有一个高频踩坑点。很多人会误以为谱线越密,频率分辨率越高,其实不是这样。谱线间隔是fs/N,它由采样率和采样点数共同决定。把N从 1024 调到 2048,如果fs不变,谱线间隔减半,相邻两根谱线之间的距离变小了,这叫“计算分辨率”提高了。但如果你把fs从 1024 提高到 2048,N不变,谱线间隔还是fs/N = 1Hz,分辨率没变,只是可分析的频率范围从 512Hz 扩大到了 1024Hz。

理解这个区别对实验很重要:当你看到海明窗频谱的峰值位置比真实频率偏了一点点,不用慌。比如信号频率是 50.3Hz,频谱峰值大概率出现在 50Hz 那条谱线上,因为谱线只能落在整数倍分辨率的位置上,50.3 并不在网格上,能量被分配到 50Hz 和 51Hz 两条谱线之间。这是离散傅里叶变换的固有特性,与窗函数无关。要提高峰值位置的读取精度,唯一方法是增加N,让谱线更密。

读幅值时也要有个心理预期:海明窗的峰值幅度不会是 1.0 而是 0.54 左右,这是窗函数能量衰减的结果。如果希望读数和原始信号幅值一致,需要做幅度补偿,公式是mag_comp = mag / sum(w) * N或等价写法mag_comp = mag / mean(w)。这个在后边改造章节会直接给代码。

5. 避坑记录:跑加窗 FFT demo 时最常碰到的五条坑

5.1 FFT 峰值幅值比信号幅值大了十倍

现象:信号幅度明明是 1.0,加方窗做 FFT,频谱图上峰值读出来是 1024 左右。

原因:MATLAB 的fft不做归一化,直接输出的幅度是原始幅值的 N 倍。你没除以 N,直接把abs(fft(x))当成频谱幅度去读了。

解决:在 FFT 之后统一除以 N,即fft(x) / N,单边谱再把中间点乘 2。养成习惯,看到频谱先问自己一句“除过 N 没”,这个动作能省掉一半排查时间。

5.2 加上海明窗之后峰值变矮,以为是代码写错了

现象:同一路信号,方窗时峰值 0.98,换成海明窗之后峰值掉到 0.53 左右。

原因:海明窗在时域上把信号两端衰减了,相当于整个信号能量打了折扣。窗函数平均增益大约是 0.54,峰值自然掉一半。这不算错,但如果你的目标是测幅值,这个衰减会直接导致读数偏小。

解决:做幅值补偿。海明窗的补偿系数是N / sum(w),也就是1 / mean(w),把补偿因子乘到 FFT 结果上,峰值就能回到接近 1.0。如果是做“不同窗函数对峰值位置的影响”这类对比实验,建议补偿后再比较,否则会被幅度差异误导。

5.3 频谱峰值的位置和信号频率对不上,偏了一整条谱线

现象:信号频率 50.3Hz,频谱峰值却出现在 52Hz,或者整体频率轴整体右移了。

原因:频率轴的计算方式写错了。常见的错误是直接用(0:N-1) * fs / N却忘了除以 N,或者把索引从 0 开始、但 MATLAB 数组从 1 开始,导致频点整体偏移一个采样间隔。

解决:画频谱时统一用f_axis = (0:half-1) * fs / N或linspace(0, fs/2 - fs/N, half),并注意 FFT 结果索引X(1:half)对应频率轴的f_axis(1)位置。用stem画之前,先打印前 10 个频率点确认起点是 0,终点接近fs/2,基本就不会错位。

5.4 把海明窗和汉宁窗混在一起,对比结果全乱套

现象:不少网上的示例代码里hamming和hanning混用,换成汉宁窗后旁瓣形状和海明窗完全不一样,读者以为程序有 bug。

原因:海明窗系数是0.54 - 0.46*cos(...),汉宁窗是0.5 - 0.5*cos(...),两者在两端点的归零方式不同。海明窗两端不严格归零(约 0.08),汉宁窗两端严格归零。这个微小差异导致旁瓣衰减相差了大约 6dB。

解决:在代码里显式写清用的是哪个窗,不要混写。工程上振动信号分析常用海明窗,语音信号处理常用汉宁窗,但两个窗是完全不同的实现,做对比实验前先确认你的参考代码里写的到底是哪个。

5.5 画窗函数曲线时只有一半看得见

现象:plot(hamming(N))画出来是一条从底部上升再回落的曲线,但左边一半看不见,或者曲线被截断。

原因:hamming(N)返回的是列向量,plot默认按列画,坐标轴范围被弄乱。另外窗函数值域在 0 到 1 之间,直接和幅度为几伏的信号画在一起,窗函数被压成一条平线。

解决:给窗函数plot之前先转置成行向量w_hamm = hamming(N)';。如果窗和信号同图对比,使用yyaxis right把窗放在右侧坐标轴,或者单独画一个子图显示窗函数形态。这个坑不大,但很容易让人误以为窗函数生成错了。

6. 二次开发思路:把双窗对比 demo 改成通用的加窗 FFT 分析工具

跑熟这个 demo 之后,你会觉得两个窗的对比已经不够用了。真实项目里信号可能来自传感器采集、音频文件、仿真数据,窗函数也想在矩形、海明、汉宁、布莱克曼之间来回切换。这时候可以把脚本的核心逻辑改造成一个可复用的分析函数,输入信号和窗函数名,输出带补偿的幅频谱。

function [f_axis, mag] = windowed_fft(x, fs, win_type) N = length(x); switch lower(win_type) case 'rect' w = rectwin(N)'; case 'hamming' w = hamming(N)'; case 'hanning' w = hanning(N)'; otherwise w = ones(1, N); end comp = N / sum(w); % 幅值补偿系数 X = fft(x .* w) / N * comp; % 加窗 + FFT + 补偿 half = floor(N / 2) + 1; f_axis = (0:half-1) * fs / N; mag = abs(X(1:half)); mag(2:end-1) = mag(2:end-1) * 2; end

封装逻辑说明:函数接收三个参数,x是信号序列,fs是采样率,win_type是窗函数名的字符串。核心步骤是把原来的三段逻辑收拢,用sum(w)做幅度补偿。调用方式是[f_axis, mag] = windowed_fft(x, fs, 'hamming'),返回的频率轴和幅值直接可用于绘图。

改造方向有两个可以立刻上手。一是支持外部数据输入:把x的来源从内部正弦波换成audioread('test.wav')读出的音频序列,或者从数据文件里读。注意audioread返回的是列向量,如果信号是双声道,先取x = x(:,1)'转成行向量再传入函数。二是做一个多窗对比的批量脚本:循环调用windowed_fft,把矩形、海明、汉宁、布莱克曼四种窗的频谱画在同一张图上,用legend标出各窗函数名。这样一次能清楚看到主瓣宽度和旁瓣高度的取舍,比单看两个窗直观得多。

我自己后来做实测振动数据的 FFT 分析时,就是从这个 demo 扩出去写了个小的批处理脚本:读数据、加窗、补偿、出图,顺便自动输出峰值频率和泄漏带宽两个指标。从那以后我每次拿到新信号,都强制走一遍“整周期还是泄漏、要不要加窗、加哪种窗、幅值补没补”这四步确认流程,再也没被“神秘谱线”坑过。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询