MATLAB 5 点滑动平均:filter 与 conv 的输出对齐,以及 2 样本延时的含义

MATLAB 5 点滑动平均:filter 与 conv 的输出对齐,以及 2 样本延时的含义

filter(ones(1,5)/5,1,x) 和 conv(x,ones(1,5)/5,'same') 都能得到一条平滑曲线,长度也都与 x 相同,但同一数组下标上的结果不一定相等。另一方面,5 点对称 FIR 的群延时是 2 个样本,也不代表输出就是把输入原样向后挪两格。

这篇用冲激、阶跃和两个频率叠加的信号,分别检查输出截取、启动暂态和频率相关增益。代码只用基础 MATLAB,不调用 freqz、grpdelay 或 filtfilt。

1. 运行方式与实测结果

把第 3 节完整代码保存为 example03_average.m,在 MATLAB 中切换到该文件所在目录后执行:

matlab 复制代码
example03_average

终端配置好 MATLAB 命令行路径后,也可执行:

text 复制代码
matlab -batch "example03_average"

本次实际测试版本为 25.2.0.2998904 (R2025b)。所有输入在函数内构造,没有外部数据。正常运行会在后台导出 output03/moving_average.png,并保存数值指标到 output03/average_metrics.csv。

同一输出目录重跑会覆盖示例文件。需要保留不同运行时,可以执行 example03_average('output03_run2',false)。

2026 年 10 月 14 日正向 batch 退出码为 0,10 项断言通过。关键结果如下。

检查项 实测结果
filter 与 full 卷积前 N 项的最大绝对误差 2.22044604925e-16
N=100 时 full 卷积长度 104
单位阶跃前五个输出 0.2、0.4、0.6、0.8、1
稳态两频信号与解析式的最大绝对误差 4.60742555219e-15
稳态输出与单位增益纯延时的最大差异 0.810372220831
0.08π rad/sample 对应的有符号增益 0.937955936469
0.6π rad/sample 对应的有符号增益 -0.2472135955
避开频响零点后计算的群延时范围 1.99999999996 至 2.00000000001 样本

很小的数值误差反映 double 浮点运算的舍入;0.81037 的差异则说明"只做两样本纯延时"的模型与平均滤波器并不相同。

2. 先说明时间坐标和边界

五点因果平均定义为:

y[n] = (x[n] + x[n-1] + x[n-2] + x[n-3] + x[n-4])/5。

每个输出使用当前样本和前四个样本。本文令记录前的输入为零,采用 filter 的默认零初始状态。这种初始化和输出同长规则见 filter R2025b 文档。

长度 N 的 x 与长度 5 的 b 做 full 卷积,共有 N+4 项。filter(b,1,x) 输出前 N 项;full 卷积还保留输入记录结束后、历史数据逐步离开窗口的四项尾部。

same 取的是 full 卷积的中心 N 项。对本例的五点核,相当于 full 结果的第 3 到第 N+2 项,而因果记录取第 1 到第 N 项。因此,把两者都画在原来的 n=0:N-1 上,会产生两样本的对齐差异,末尾处理也不相同。same 的定义可查 conv R2025b 文档。

如果把居中结果放在原输入时刻上理解,它需要使用该时刻之后的两个样本;用于在线处理时,需要明确等待这些样本的时间。

3. 完整可运行代码

以下为实际测试文件。x 和 b 均为行向量,避免比较时混入行列方向问题。

matlab 复制代码
function example03_average(outDir, negativeMode)
% Causal 5-point moving average, zero initial state. Base MATLAB only.
if nargin<1, outDir=fullfile(fileparts(mfilename('fullpath')),'output03'); end
if nargin<2, negativeMode=false; end
if ~isfolder(outDir), mkdir(outDir); end
fprintf('MATLAB_VERSION=%s\nNEGATIVE_MODE=%d\n',version,negativeMode);
tol=1e-10; b=ones(1,5)/5; N=100; n=0:N-1;
w1=0.08*pi; w2=0.6*pi;
x=cos(w1*n)+0.6*cos(w2*n);
y=filter(b,1,x); fullY=conv(x,b,'full');
pureDelay=[0 0 x(1:end-2)];
if negativeMode, y=pureDelay; end % Intentionally incorrect "only a delay".
convError=max(abs(y-fullY(1:N)));
ck(convError<tol,'Convolution','Causal filter equals first N terms of full convolution');
ck(numel(fullY)==N+4,'FullLength','Full convolution includes the four-sample tail');
sameY=conv(x,b,'same');
ck(max(abs(y-sameY))>0.1,'NotSame','Centered same convolution is not the causal record');
impulse=filter(b,1,[1 zeros(1,11)]);
ck(max(abs(impulse-[b zeros(1,7)]))<tol,'Impulse','Impulse response has five taps of 0.2');
step=filter(b,1,ones(1,12));
ck(max(abs(step(1:5)-(1:5)/5))<tol,'Transient','Zero-state step front is a ramp');
ck(max(abs(step(5:end)-1))<tol && abs(sum(b)-1)<tol,'DC','Steady-state DC gain is one');
% H(w)=exp(-j*2*w)*A(w), A real, possibly negative.
gain=@(w) sin(5*w/2)./(5*sin(w/2));
predicted=gain(w1)*cos(w1*(n-2))+0.6*gain(w2)*cos(w2*(n-2));
steadyError=max(abs(y(5:end)-predicted(5:end)));
ck(steadyError<tol,'SteadyTone','Steady sinusoids have frequency-dependent signed gain');
delayError=max(abs(y(5:end)-pureDelay(5:end)));
ck(delayError>0.1,'NotPureDelay','Mixed frequencies are not a unit-gain pure shift');
% Compute phase derivative analytically, without freqz or grpdelay.
omega=linspace(0.001,pi,2000)'; E=exp(-1i*omega*(0:4));
H=E*b'; Hprime=-1i*E*((0:4).*b)';
away=abs(H)>1e-3;
gd=-imag(Hprime(away)./H(away));
ck(max(abs(gd-2))<1e-9,'GroupDelay','Group delay is two away from response zeros');
zeroH=exp(-1i*[2*pi/5;4*pi/5]*(0:4))*b';
ck(max(abs(zeroH))<tol,'Zeros','Response zeros: group delay is undefined there');
fprintf('CONV_MAX_ERROR=%.12g STEADY_MAX_ERROR=%.12g PURE_DELAY_MAX_ERROR=%.12g\n',convError,steadyError,delayError);
fprintf('GAIN_LOW=%.12g GAIN_HIGH=%.12g GROUP_DELAY_RANGE=[%.12g %.12g]\n',gain(w1),gain(w2),min(gd),max(gd));
fprintf('STEP_FRONT=%s\nIMPULSE=%s\n',mat2str(step(1:5),8),mat2str(impulse,8));
fprintf('LIMIT: group delay only where H is nonzero; startup and frequency-dependent gains prevent generic pure shifts.\n');
fig=figure('Visible','off','Color','w','Position',[100 100 1100 760]);
cleanup=onCleanup(@() close(fig));
tiledlayout(2,2,'TileSpacing','compact');
nexttile; stem(0:11,impulse,'filled'); grid on; ylim([0 0.25]);
title('Impulse: five coefficients of 0.2'); xlabel('Sample n'); ylabel('h[n]');
nexttile; stairs(0:11,step,'LineWidth',1.5); grid on; ylim([0 1.1]);
title('Zero-state step: startup ramp then unity'); xlabel('Sample n'); ylabel('Output');
nexttile; plot(n,y,n,pureDelay,'--','LineWidth',1.1); xlim([0 45]); grid on;
legend('5-point average','Pure delay of 2','Location','best');
title('Same nominal delay does not mean same waveform'); xlabel('Sample n'); ylabel('Amplitude');
nexttile; plot(omega/pi,abs(H),'LineWidth',1.3); grid on;
xlabel('Normalized frequency (x pi rad/sample)'); ylabel('|H|');
title('Frequency-dependent gain; zeros at 0.4 and 0.8');
exportgraphics(fig,fullfile(outDir,'moving_average.png'),'Resolution',150);
metrics=table(convError,steadyError,delayError,min(gd),max(gd),...
    'VariableNames',{'conv_error','steady_error','pure_delay_error','gd_min','gd_max'});
writetable(metrics,fullfile(outDir,'average_metrics.csv'));
fprintf('RESULT=PASS example03_average\n');
end
function ck(ok,id,msg)
assert(ok,['lessonMA:' id],msg); fprintf('ASSERT_PASS=%s\n',id);
end

4. 从冲激和阶跃读懂初始状态

左上图中,单位冲激只有第一个输入为 1,后面全部为 0。这个 1 在五个连续窗口中各贡献 1/5,因此冲激响应前五项都是 0.2,之后为 0。这也直接对应代码里的五个滤波系数。

右上图输入是从记录开始就为 1 的单位阶跃。由于记录前四个输入按零处理,第一项是 (1+0+0+0+0)/5=0.2,之后依次为 0.4、0.6、0.8,到第五个输出才达到 1。

图中的横轴 n 从 0 开始,所以第五个输出位于 n=4。它与 MATLAB 数组里的 step(5) 是同一个点,别在读图时多算一拍。

系数和为 1,说明稳态直流增益为 1;启动阶段没有填满窗口,仍会有这段暂态。如果希望从第一项就符合另一种历史输入,需要明确初始条件,不能只把曲线开头的偏低判成算法错误。

默认 movmean(x,5) 也不等价于这里的零初态因果平均:它采用居中窗口,端点默认缩小窗口。movmean 的端点说明可用于核对这类差别。

5. 为什么群延时是 2,波形却没有原样平移

五点平均的频率响应可写为:

H(ω) = exp(-j*2*ω) * sin(5*ω/2)/(5*sin(ω/2))。

其中 ω 的单位是 rad/sample;ω=0 时按极限得到 H(0)=1。前面的指数项对应两样本延时,后面的实因子随频率变化,并且可能为负。

在 H 不为零的连续频段上,这个实因子的符号不会突然改变,相位对 ω 的斜率为 -2,因此群延时为 2。跨过零点会发生相位跳变,零点处输出分量被消除,群延时没有可用定义。群延时与相位延时的概念可参阅 Group Delay and Phase Delay。

本例不用数值差分相位,而是直接对有限 FIR 和求导:计算 H、H′,再由 -imag(Hprime./H) 得到群延时。为了避开零点附近的除法放大,检查范围限定为 abs(H)>1e-3。实测范围非常接近 2,同时另有断言检查 0.4π、0.8π 两个零点。

5.1 两个频率会受到不同衰减

输入由下面两部分相加而成:

  • 低频余弦:ω₁=0.08π,输入幅度为 1
  • 高频余弦:ω₂=0.6π,输入幅度为 0.6

进入稳态后,低频项的有符号增益约为 0.937956,高频项约为 -0.247214。负号表示相对于共同的两样本延时形式,还存在符号反转;它不代表幅频响应为负。右下图画的是 abs(H),因此始终非负。

两个分量经历不同的衰减和符号变化,叠加出来的形状自然会改变。左下图蓝线是平均滤波器输出,虚线是把原始输入仅向后移两格,两条曲线明显不同。代码从第五项开始比较稳态解析式,最大误差约为 4.61e-15;与纯延时的最大差异约为 0.81037。

因此,群延时描述的是相位随频率变化的性质。只有在相关频带的幅度响应也足够接近所需增益、并妥善处理暂态时,才能把某些输入近似理解为延后而形状基本保持。

5.2 换算成秒时要用采样率

两样本的算法群延时对应 2/Fs 秒。假设采样率为 1 kHz,这个换算值是 2 ms;这是单位换算举例,本文代码没有指定 1 kHz 的硬件采样率。

如果实现到 FPGA,额外寄存器流水线通常还会引入系统时钟周期延迟。采样间隔与系统时钟周期可能不同,不能把"2 个样本"直接说成"2 个 FPGA 时钟周期"。

6. 让"只是延时"的错误模型失败

执行:

matlab 复制代码
example03_average('output03_negative',true)

负对照把 y 故意替换为两样本纯延时,再与 full 卷积的前 N 项比较。本次实际在 lessonMA:Convolution 失败,batch 退出码为 1。

这个负对照验证的是错误的数学模型能否被检查抓住;它没有运行硬件,也没有证明任何固定点位宽、溢出策略或 RTL 实现正确。

7. 使用范围和参考资料

本例只验证了零初态、固定五点系数、double 浮点输入。平均会削弱快速变化,也会让边沿展开。不能只凭平滑后的曲线更顺眼,就认为有效信号全部保留了。

分块处理时需要传递滤波器状态,否则每块开头都会重新产生零初态暂态。官方 filter 文档给出了 zi/zf 用法;本文代码没有运行分块状态实验,也没有把它列入上述 10 项断言。

选题参考 Convolution in Digital Signal Processing 固定版本 README中的卷积、LTI 与平均滤波概念。完整课程使用 Signal Processing Toolbox,部分案例另需图像处理和深度学习工具箱。

该版本同时包含 BSD 3-Clause 文本的 LICENSE.md,以及带 MathWorks 产品及服务使用限制的 license.txt。本文仅作概念参考,实验代码和图片独立生成,不复制仓库素材。

需要进一步区分算法延时和硬件流水线延时,可以阅读 流水线设计 Pipeline:提升时序性能的核心技巧。

最后留一个具体问题:如果把同一段输入分成两块,第二块仍直接调用没有初始状态参数的 filter,它的前四个输出为什么可能与整段处理不同?这与本篇阶跃前沿的四个过渡点有什么联系?

相关推荐
Wang's Blog1 小时前
Java 项目实战: 交友项目 - 项目模块划分与依赖关系
java·开发语言·交友
Keep_Trying_Go1 小时前
分割算法FastSAM论文详解
深度学习·算法·计算机视觉
努力进修1 小时前
时序数据长期承载难题:从压缩、批量写入到数据组织优化
java·开发语言·数据库
Dovis(誓平步青云)1 小时前
总用电下降了,为什么家庭能源看板仍然提示异常
java·开发语言·数据库·人工智能·算法·电脑·能源
程序猿乐锅1 小时前
【从0-1学习Python | 第四篇】Python 字符串和元组
java·开发语言·笔记·后端·python·学习
开开心心就好1 小时前
Windows视频播放器推荐,精简版体积更小
java·服务器·开发语言·eclipse·pdf·ocr·音视频
开开心心就好1 小时前
A3的PDF怎么切成A4?拖一下分割线就行
服务器·开发语言·python·算法·pdf·ocr·tornado
小黄人软件1 小时前
python 模块(Module)与包(Package)的区别,python xxx.py、python -m xxx 和 import xxx 三者的区别
开发语言·python
PHP实战开发录1 小时前
PHP中文文件名为什么乱码
开发语言·php