实战:心电图信号处理与ESP32心率血氧计
实战:心电图信号处理与ESP32心率血氧计
心电图信号处理
做这个小项目的想法来源于 HPC WEEK 比赛中 CT 图像并行计算的那道题。当时虽然我看不懂题目,但是大受震撼。因为不管是各种OJ还是Leetcode上我平时都不太会看到实际数据的处理,一般都是想象出来的场景,选择合适的算法和数据结构解决(毕竟以我们这种水平也做不了什么复杂的现实场景的东西)。
我后来问了下GPT,了解到了信号处理这个方向。打算从最基础的心电图信号处理做一个小项目进行实战。
环境配置
在项目文件夹下用venv创建python3.12环境,然后安装了numpy,scipy,matplotlib,wfdb库。
1 | pip install numpy scipy matplotlib wfdb |
其中:
wfdb:读 MIT-BIH 这类心电数据库
scipy:滤波、信号处理
matplotlib:画图
数据获取
MIT-BIH 数据库的文件通常成对出现(.dat 信号文件和 .hea 头文件)。可以使用 wfdb 库来处理它们。wfdb.rdrecord() 是 WFDB 库的核心函数,用于读取生理信号记录(如 ECG 心电信号)。它返回一个record对象,主要包括:
信号数据
record.p_signal:一个多维数组,表示 ECG 的多个导联,通常为(样本数, 通道数)。例如:包含 2 个导联、持续 10 秒、采样率为 360Hz 的信号,返回的数组形状是(3600,2)。元数据字典:包含采样率(
record.fs)、导联名称(record.sig_name)、单位(record.units)等。
1 | record.p_signal # 生理信号数据(numpy 数组) |
**MLII (Modified Limb Lead II)**是第1个通道,是最常用的监测导联。问GPT后说这个最常用,所以我就取了这个。
给原始数据画图
根据传入的start和duration参数创建一个时间轴time_axis,并使用loader数据画图。
去噪
如果观察刚才画出的原始信号,可能会发现两个问题:
基线漂移 (Baseline Wander):波形整体像海浪一样上下起伏。这通常是由于呼吸或身体晃动引起的(属于低频干扰)。
高频噪声 (High-frequency Noise):波形上有细小的锯齿。这可能是肌肉电信号 (EMG) 或设备电子噪声。
带通滤波
带通滤波只保留需要的频段(0.5–40 Hz的频段)。Google后就可以知道,< 0.5 Hz的低频是基线漂移噪声,> 40 Hz的是高频噪声。
我们构建一个4阶巴特沃斯带通滤波器,截止频率设置为0.5 Hz和40 Hz。使用scipy.signal.butter函数设计滤波器,并用scipy.signal.filtfilt函数进行零相位滤波,避免相位失真。
R波检测
R波是心电图中最显著的特征,代表心脏的收缩。检测R波的位置对于计算心率和分析心律非常重要。
ECG 信号即使滤了波,也还包含 P 波、T 波等。为了让计算机只盯着 R 波看,我们需要放大它们之间的差距,进行平方处理。
我们使用scipy.signal.find_peaks函数来检测R波峰值。设置适当的高度和距离参数,以确保只检测到真正的R波峰值。然后将返回的峰值索引转换为时间,并在图上标注出来。
然后在draw.py的draw_signal函数里添加可选参数rwave,如果传入了R波索引,就在图上用蓝色圆点标注出来。
算心率
心率(BPM)可以通过计算连续R波之间的时间间隔(RR间期)来得到。我们计算每对连续R波之间的时间差,并将其转换为心率(BPM)。同时,我们也计算了整个信号段的平均心率。
异常检测
用前面的instant_heartrate计算出的瞬时心率,统计心动过速(>100 BPM)和心动过缓(<60 BPM)的频次。
HRV(心率变异性)是指连续心跳之间时间间隔的变化程度。我计算了RR间期的标准差(SDNN)作为HRV的一个指标。
Claude Code辅助与完善
下载.atr标注文件,新增load_annotations()方法。用wfdb.rdann()读取标注,过滤掉非搏动标注符号(+, ~, | 等),仅保留 19 种 AAMI 搏动类型。
evaluate.py — 评估核心模块,包含:
- match_peaks() — 逐一匹配检测点与标注点(150ms 容差,符合 ANSI/AAMI 标准)
- evaluate_detector() — 计算 Se、+P、F1
- print_evaluation() — 单条记录详细输出
- print_summary() — 多记录汇总表
ESP32 心率血氧计