简介:这是一份面向生物医学工程、电子信息类专业课程设计与数字信号处理学习者的PDF文档,围绕“基于MATLAB平台的心电信号分析系统设计及仿真”展开,可作为心电信号分析、滤波器应用及Simulink动态建模仿真的课题参考方案。内容以MIT-BIH数据库心电数据为对象,覆盖信号读取、线性插值、低通/高通与50Hz工频陷波器设计、时域波形和频谱对比分析等环节,并涉及巴特沃斯或切比雪夫滤波器的幅频特性、零极点、阶跃响应等分析任务,同时给出MATLAB静态编程与Simulink动态仿真两种实现路径。资源包为1个PDF文件,大小约1019KB,结构紧凑,便于查阅和打印。目前已有357人学习下载,适合需要完成相关课程设计、理解心电信号频域特点并练习滤波器选型的读者参考,可据此建立从数据预处理、建模仿真到结果对比的完整思路。
1. 从一段噪声心电到可复现的分析系统:这个课题要交付什么
把 MIT-BIH 的 100 号记录读进 MATLAB,plot 出来的波形大概率不像教科书插图:基线被呼吸拖着上下漂,R 波顶上叠着 50 Hz 毛刺,靠后的片段全是肌电噪声。很多人做「基于 MATLAB 平台的心电信号分析系统设计及仿真」时,第一反应是直接 findpeaks 找 R 波,结果心率算出来 200 多次——问题不在峰值检测,而在信号还没净化就送进了检测器。
这个课题要交付的从来不是一段能跑的脚本,而是一条参数可查、结果可复现的链路:数据读取、预处理滤波、QRS 波检测、心率与 RR 间期计算、特征提取、分类或异常标注、界面展示与仿真指标验证。适合电子信息与生物医学工程方向做课设毕设的人,也适合想借心电这条线把 MATLAB 信号处理、滤波器设计、神经网络工具箱串一遍的从业者。下面按模块落地,每一步都给出可直接抄的命令与参数。
2. 用 MATLAB 搭起心电信号分析系统的数据链路
2.1 心电数据从哪来、怎么读进 MATLAB
做仿真最省事的数据源是 PhysioNet 上的公开心电库,MIT-BIH Arrhythmia Database 是课程设计里出现频率最高的一个:双通道、采样率 360 Hz、11 位量化、量程约 ±10 mV,每条记录 30 分钟,自带注释文件标注了每个心拍的类型。文件通常是.dat(二进制采样值)、.hea(头文件,含采样率、增益、导联数)、.atr(注释)。用 WFDB Toolbox 的rdrecord一行读进来,不想装工具箱就直接按头文件里的增益手动换算。
% 读取 MIT-BIH 记录,fs 与 gain 从头文件获得 [signal, Fs, tm] = rdsamp('mitdb/100'); % signal: N×2,两路导联 ann = rdann('mitdb/100', 'atr'); % 专家标注的 R 波位置 % 若手动读 .dat(212 格式),必须按头文件做增益与偏移换算 fid = fopen('100.dat', 'r', 'ieee-le'); raw = fread(fid, [3, inf], 'uint8')'; % 212 格式:3 字节装 2 个样点 fclose(fid); v1 = double(bitshift(raw(:,1), 8) + raw(:,2)); % 高 8 位 + 低 8 位 v2 = double(bitshift(raw(:,3), 8) + bitand(raw(:,2), 15) * 16); v1(v1 > 2047) = v1(v1 > 2047) - 4096; % 补码还原 v2(v2 > 2047) = v2(v2 > 2047) - 4096; ecg = (v1 - 1024) / 200; % 减去零点并除以增益逻辑说明:rdsamp返回的是已经换算好的物理量,单位 mV,直接用最省心;手动解析那条路径是为了在答辩时能解释清楚「212 格式」的位打包——每 3 个字节装 2 个 12 位样点,低 4 位是第二个样点的高位,容易踩坑。参数上要注意gain(增益,MIT-BIH 常为 200 ADU/mV)和baseline(零点,通常 1024),这两个值必须从头文件读,写死会导致波形整体偏移。采样率 360 Hz 决定了后面所有滤波器截止频率的归一化方式,butter的Wn参数是相对 Nyquist 频率的比例,不是绝对 Hz。
2.2 三类噪声的频段划分与针对性预处理
心电的有效能量集中在 0.05 到 100 Hz,QRS 波主频在 10 到 25 Hz。按这个先验,噪声可以切成三块处理:基线漂移低于 0.5 Hz,主要来自呼吸和电极移动;工频干扰集中在 50 Hz(国内电网)及其谐波;肌电噪声是 20 到 200 Hz 的宽带随机信号。三类噪声频段不重叠,所以可以用不同手段分别打掉,而不是一个滤波器包打天下。
| 噪声类型 | 主要频段 | 常用处理手段 | 典型参数 | 副作用 |
|---|---|---|---|---|
| 基线漂移 | < 0.5 Hz | 中值滤波、高通滤波 | 200 ms + 600 ms 两级中值 | 阶数过高会削掉 ST 段 |
| 工频干扰 | 50 Hz 及谐波 | IIR 陷波 | Q = 35,中心 50 Hz | Q 过高导致振铃 |
| 肌电噪声 | 20–200 Hz | 低通或小波去噪 | 40 Hz 低通 / db4 5 层 | 截止过低会压平 R 波 |
| 高频毛刺 | > 100 Hz | 滑动平均 | 窗长 5 点 | 引入约 7 ms 延迟 |
提示:先用
pwelch看一眼功率谱再定滤波器,比直接套模板靠谱。哪根谱线冒尖打哪根,别一上来就上小波。
2.3 带通、陷波、中值滤波的参数怎么定
我一般按「中值滤波去漂移 → 陷波去工频 → 带通做整体整形」的顺序串,因为带通放在最后能顺手把前两步的边角削平。滤波器设计推荐用designfilt而不是老的butter,一是参数语义清楚,二是便于在报告里截图说明设计指标。
Fs = 360; % 采样率 % 1) 两级中值滤波去基线漂移 base = medfilt1(ecg, 0.2*Fs); % 200 ms 窗去掉 QRS 与 P 波 base = medfilt1(base, 0.6*Fs); % 600 ms 窗估计漂移 ecg_hp = ecg - base; % 原信号减漂移分量 % 2) 50 Hz 陷波,Q 决定陷波宽度 wo = 50/(Fs/2); bw = wo/35; [b, a] = iirnotch(wo, bw); ecg_nf = filtfilt(b, a, ecg_hp); % 零相位滤波,避免 T 波位移 % 3) 0.5–40 Hz 带通做整体整形 d = designfilt('bandpassiir', ... 'FilterOrder', 4, ... 'HalfPowerFrequency1', 0.5, ... 'HalfPowerFrequency2', 40, ... 'SampleRate', Fs); ecg_clean = filtfilt(d, ecg_nf);逻辑说明:中值滤波的窗长必须大于 QRS 宽度(约 80 到 100 ms)才能把 QRS 当异常值剔除,所以取 200 ms;第二级 600 ms 是为了覆盖一个完整心动周期,保证 T 波不被误当漂移。陷波用iirnotch的带宽比bw控制,Q 取 35 是折中,Q 越大陷波越窄、对 50 Hz 越精准,但对频率漂移越敏感。所有滤波统一用filtfilt做零相位滤波,代价是首尾各损失约 3 倍阶数个样点,计算心率前要把这段剪掉,否则会出现虚假的 RR 间期。
3. QRS 波检测与心率计算的 MATLAB 实现
3.1 Pan-Tompkins 检测链路的五个环节
QRS 检测主流做法仍是 Pan-Tompkins 那套:带通滤波、微分、平方、移动窗积分、双阈值判定。它的价值在于把尖锐的 R 波转成一个平滑的包络,让阈值判定不再依赖单点幅值,抗噪能力比裸findpeaks高一个量级。五个环节各管一件事:带通把能量收敛到 5 到 15 Hz,微分突出陡峭上升沿,平方让幅值全为正且放大高频,积分把能量在时间上摊平,阈值负责从包络里挑出真正的峰。
3.2 微分、平方、移动窗积分的参数设置
工程上最容易出问题的不是原理,而是窗长和阈值更新策略。移动窗积分窗宽通常取 0.15 s,也就是 QRS 宽度的 1.5 倍左右;窗太窄包络不平滑,太宽会把 T 波也包进来,导致 T 波误检。阈值不能写死,必须随信号自适应调整。
% 微分:5 点差分,突出 R 波上升沿 h = [1 2 0 -2 -1] / 8; diff_ecg = filtfilt(h, 1, ecg_clean); % 平方:全波整流,放大高频 sq = diff_ecg .^ 2; % 移动窗积分:0.15 s 窗,用 filter 做滑动求和 win = round(0.15 * Fs); integ = filter(ones(1, win)/win, 1, sq); % 自适应双阈值:SPKI 放信号峰,NPKI 放噪声峰 SPKI = max(integ(1:2*Fs)); NPKI = mean(integ(1:2*Fs)); thr1 = NPKI + 0.25*(SPKI - NPKI); % 第一阈值 thr2 = 0.5 * thr1; % 第二阈值,用于回溯 refr = round(0.2 * Fs); % 200 ms 不应期逻辑说明:5 点差分的系数[1 2 0 -2 -1]/8是对理想微分器的近似,长度奇数是保证零相位的条件之一。平方之后所有值非负,积分窗宽win决定了包络的时间分辨率,0.15 s 对应约 54 个样点。阈值更新的经典规则是:检测到峰后SPKI = 0.125*peak + 0.875*SPKI,判断为噪声则NPKI = 0.125*peak + 0.875*NPKI,这种指数加权让阈值能跟上信号幅度变化。不应期设 200 ms 是因为生理上两次心搏不会靠得比这更近,能挡掉大部分 T 波误检。
3.3 漏检误检的回退与心率计算
双阈值的作用体现在回退:超过thr1直接判定为 QRS;落在thr1和thr2之间则先记下来,若前面 200 ms 内已有 QRS 就丢弃,否则回溯搜索局部最大值再确认。漏检多半发生在幅度骤降的片段,此时靠 RR 间期预测下一个峰的大致位置,在预测点前后 50 ms 内强制搜索一次,能把漏检率压下来。
[pks, loc] = findpeaks(integ, 'MinPeakHeight', thr1, ... 'MinPeakDistance', refr); RR = diff(loc) / Fs; % RR 间期(秒) hr = 60 ./ RR; % 瞬时心率 hr_smooth = movmean(hr, 5); % 5 拍滑动平均,压抖动 % 剔除滤波边界带来的伪峰 valid = loc > 3*Fs & loc < length(ecg_clean) - 3*Fs; loc = loc(valid); hr = hr(valid(1:end-1));逻辑说明:MinPeakDistance直接等价于不应期,是最省事的一道保险。心率用60./RR算的是瞬时值,窦性心律下波动本来就大,展示时用movmean平滑,但报告里两个都要给——平滑值看趋势,瞬时值看变异性。剪掉首尾各 3 秒是为了躲开filtfilt的边界效应,这段信号的群延迟补偿不完整,容易产生幅度异常。
4. 心电特征提取与分类模型的设计及仿真
4.1 时域、频域、小波三类特征怎么选
检测出 R 波之后,每个心拍可以做特征。时域特征包括 RR 间期、QRS 宽度、R 波幅值、相邻 RR 比值,计算快、可解释性强,做心律失常粗分类够用;频域特征把 RR 序列做功率谱,低频与高频功率比能反映自主神经活动;小波特征用wavedec对心拍做 5 层 db4 分解,各层系数的能量占比对波形形态变化敏感,适合区分形态差异大的心拍类型。课程设计里我一般三个都提,实际送入分类器的用「RR 间期 + 小波能量比」这一组,维度不高,训练收敛快。
feat = zeros(numel(loc)-1, 7); for k = 1:numel(loc)-1 beat = ecg_clean(loc(k):min(loc(k)+0.6*Fs, end)); % 小波 5 层分解,取各层能量占比 [C, L] = wavedec(beat, 5, 'db4'); for j = 1:5 Dj = detcoeff(C, L, j); feat(k, j) = sum(Dj.^2) / sum(C.^2); end feat(k, 6) = RR(k); % 前一个 RR 间期 feat(k, 7) = max(beat) - min(beat); % 峰峰值 end feat = mapminmax(feat', 0, 1)'; % 归一化到 [0,1]逻辑说明:每个心拍截 0.6 s(约 216 点)是为了覆盖 QRS 加 T 波。wavedec返回的系数向量C和长度向量L是打包格式,必须用detcoeff取出指定层的细节系数,直接用下标切容易错位。能量比做特征的好处是对整体幅度不敏感,电极贴得不一致时更稳。归一化用mapminmax而不是手写(x-min)/(max-min),是因为训练集和测试集要共用同一套映射参数,否则测试样本的分布对不上。
4.2 用 BP 神经网络做心拍分类的训练脚本
分类器用patternnet或feedforwardnet都行,前者自带交叉熵与混淆矩阵输出,做课设展示更直观。隐藏层节点数从 10 起步往上试,输入维度 7 的情况下 10 到 20 个节点足够,再多就是过拟合。训练算法选trainscg,占内存小、收敛稳,比默认的trainlm更适合样本量不大的场景。
load('featLabel.mat'); % feat: N×7, label: N×1 类别 net = patternnet([15 8]); % 两个隐藏层,15 与 8 个神经元 net.trainFcn = 'trainscg'; % 量化共轭梯度,省内存 net.performFcn = 'crossentropy'; net.divideParam.trainRatio = 0.7; net.divideParam.valRatio = 0.15; net.divideParam.testRatio = 0.15; net.trainParam.epochs = 500; net.trainParam.goal = 1e-4; [net, tr] = train(net, feat', label'); y = net(feat'); [~, pred] = max(y); acc = mean(pred' == label); plotconfusion(label', y); % 混淆矩阵,逐类看召回逻辑说明:patternnet的输出层是 softmax,标签需转成 one-hot 由工具箱内部处理,传入label'即可。divideParam的划分是随机分层抽样,小样本时结果波动大,建议设rng(1)固定随机种子,保证仿真可复现。trainscg每轮只算梯度不算 Hessian,速度快但对学习率敏感,学习率默认 0.01 一般不用改。混淆矩阵要重点看少数类的召回率,整体准确率 95% 但某类全错的情况在类别不平衡时很常见。
4.3 数据划分与仿真发散的排查
训练损失突然变成 NaN,或者验证误差一路向上不收敛,绝大多数出在数据而不是网络结构。先查三处:特征里有没有 NaN 或 Inf(除零、log(0)都是源头);归一化是不是在划分训练测试之前做的(会造成信息泄漏,准确率虚高);学习率是不是被调大到 0.1 以上。仿真发散还有一个隐藏原因——mapminmax对测试集单独归一化,导致两批数据落在不同尺度上。排查时直接打印any(isnan(feat))、range(feat),比盯着损失曲线猜快得多。
注意:别用整段信号一次性训练。心拍之间高度相关,随机划分会让相邻心拍同时出现在训练和测试集里,准确率虚高十几个点。按记录划分更接近真实场景。
5. 系统仿真的验证指标与 GUI 落地的关键技巧
5.1 用 Se、PPV 量化检测链路
检测环节不能只看「波形对得上」,要拿专家标注算两个指标:灵敏度 Se = TP/(TP+FN),阳性预测率 PPV = TP/(TP+FP)。容许误差按 ANSI/AAMI 惯例取 150 ms,即检测位置落在标注位置 ±150 ms 内算命中。拿 MIT-BIH 几条典型记录跑一遍,正常段 Se 能到 99% 以上,含室性早搏的段 PPV 会掉到 95% 左右,这正是调阈值和不应期的依据。
tol = round(0.15 * Fs); % 150 ms 容许窗 TP = 0; FP = 0; FN = 0; for i = 1:numel(ann) d = min(abs(loc - ann(i))); if d <= tol, TP = TP + 1; else, FN = FN + 1; end end FP = numel(loc) - TP; Se = TP / (TP + FN); PPV = TP / (TP + FP); fprintf('Se=%.2f%% PPV=%.2f%%\n', Se*100, PPV*100);参数说明:tol对应 AAMI 标准的 150 ms 容差,改小会更严格、指标整体下移,横向比较时必须固定。这个循环是 O(n²),30 分钟记录约 2000 个心拍,跑起来毫无压力,不用急着向量化。
5.2 App Designer 界面刷新与卡顿的处理
把整条链路塞进界面时,最常见的抱怨是拖动滑块卡顿。原因是每次回调都重跑一遍filtfilt和findpeaks,而 30 分钟数据有 65 万个点。处理办法是三层:滤波结果缓存到属性变量,滑块只触发重绘不触发重算;绘图用animatedline或只更新YData而不是plot重建对象;坐标轴限定显示 10 秒窗,靠xlim平移,不重绘全量数据。
function SliderValueChanged(app, event) win = round(app.Fs * 10); % 固定 10 s 显示窗 idx = max(1, round(event.Value)); seg = app.ecgClean(idx : min(idx+win, numel(app.ecgClean))); set(app.UIAxes.Children, 'YData', seg); % 只改 YData,不重建 app.UIAxes.XLim = [idx, idx + win]; end首次绘图时用plot(app.UIAxes, seg)建立句柄,后续回调只改YData,这样 MATLAB 不用重新分配图形对象。app.ecgClean作为属性保存滤波后的信号,避免回调里重复滤波。数据量超过显卡能顺畅渲染的规模时,还可以先做 10 倍抽取再画,视觉上看不出差别,刷新率能提上来。整套跑完再回看,真正决定这个系统好不好用的,是参数有没有暴露到界面上、每个中间结果能不能单独看到,而不是算法本身有多新。
本文还有配套的精品资源,点击获取