智能手表背面那组绿光LED和光电二极管,记录的并不是心电图那种电活动,而是皮下血管容积随心跳发生的周期性变化,这个信号通常叫做光电容积脉搏波,缩写为PPG。它容易获得、适合长期佩戴,但原始波形里混着基线漂移、工频干扰、运动伪影,要从中可靠地识别房颤、早搏等异常心律,需要一整套信号处理和分类算法。

一、PPG信号预处理:从原始波形中恢复脉搏成分
PPG传感器一般以25Hz到100Hz的采样率工作,得到的是包含直流和交流分量的复合波形。直流分量主要对应皮肤组织、静脉血等静态吸收,交流分量才真正携带脉搏信息。原始波形常出现基线缓慢漂移,比如呼吸引起的低频成分,还可能叠加50Hz工频干扰以及环境光变化带来的阶跃噪声。预处理的第一步通常是设计一个0.5Hz到5Hz的带通滤波器,既能抑制基线漂移,也能去掉高频噪声。实际工程中常用4阶巴特沃斯滤波器,并使用零相位滤波避免波形峰点发生偏移。下面这段Python代码展示了一个可复用的带通滤波函数。
from scipy.signal import butter, filtfilt
def bandpass_ppg(signal, fs=50.0, low=0.5, high=5.0, order=4):
nyq = 0.5 * fs
low = max(low, 0.01) # 防止截止频率接近0
high = min(high, nyq * 0.95) # 防止超过奈奎斯特频率
b, a = butter(order, [low / nyq, high / nyq], btype='band')
return filtfilt(b, a, signal)
光有带通滤波还不够,运动伪影才是手表端最棘手的问题。手腕在走路、摆臂或打字时,光路会发生明显变化,由此产生的伪影幅度可能远大于真实脉搏波。常见的做法是利用手表内置加速度计作为参考信号,采用LMS自适应滤波或谱减方法进行抑制。但在工程实践中,更稳妥的策略是先做活动检测:如果三轴加速度幅值持续超过阈值,就直接把该段信号标记为低质量,而不是强行去噪。信号质量评估通常会综合几个指标:
- 信噪比:把心动周期模板能量与噪声能量进行比较,信噪比过低时直接丢弃;
- 峰度:正常脉搏波峰度偏高,运动噪声段的峰度往往偏低;
- 加速度幅值:超过约0.2g时通常认为存在明显运动干扰。
低于可用阈值的数据不进入后续心律分析,这比盲目去噪更可靠,也能避免把噪声误判成房颤。
二、PPG峰点检测与HRV特征提取
经过预处理的PPG需要定位每个脉搏波的收缩期峰点。与ECG的R波不同,PPG峰点受脉搏波传播时间影响,通常滞后于R波几十毫秒到两百毫秒。峰点检测可以使用局部极大值、一阶导数过零和动态阈值等方法。Python中scipy.signal.find_peaks可以完成基本任务,但需要根据实际信号调整最小间隔和突出度,否则容易把重搏波也当成主峰。下面代码展示了峰值检测与PPI提取的基本流程。
import numpy as np
from scipy.signal import find_peaks
def detect_ppg_peaks(ppg, fs=50.0, min_interval=0.3, prominence=0.15):
min_dist = int(min_interval * fs) # 最小间隔对应180次/分上限
peaks, props = find_peaks(ppg, distance=min_dist, prominence=prominence)
ppi = np.diff(peaks) / fs # 峰峰间期,单位秒
return peaks, ppi, props
得到峰峰间期PPI序列后,可以把它近似看作心跳间期,用于心率变异性分析。对房颤识别来说,最有价值的不是平均心率,而是间期的逐搏波动程度。常用时域特征包括SDNN、RMSSD、pNN50等。房颤时PPI序列呈现绝对不规则,RMSSD和样本熵会明显升高;早搏则可能表现为一次短间期后跟着一次长代偿间期,再恢复到正常节律。还可以绘制Poincare散点图,正常窦性心律通常呈椭圆或棒状分布,房颤则呈扇形或随机散布,这些形态特征对后续分类非常关键。
需要说明的是,PPG的PPI序列不能完全等同于ECG的NN间期。在异位搏动、脉搏波到达时间变化或明显运动伪影下,两者可能出现偏差。因此,很多研究在提取HRV特征时会同步利用加速度信号和信号质量分数,给每个时间窗口一个置信度,只有置信度较高时才输出异常心律判断。
三、异常心律检测算法:从规则到深度学习
早期可穿戴设备使用阈值规则,比如计算PPI的变异系数、香农熵或样本熵,超过某个阈值就判为可能房颤。这类方法可解释性强、算力开销低,但在复杂噪声和多种心律失常并存时误报率较高。现在通常结合手工特征和机器学习模型。例如随机森林或XGBoost输入几十个HRV特征、信号质量特征和加速度统计量,输出正常、房颤、早搏、噪声等类别。训练数据需要以心电图标注作为金标准,并对类别不平衡做重采样或代价敏感处理。下面是一个XGBoost推理示例。
import xgboost as xgb import numpy as np # 假设 model 是训练好的 XGBoost 分类器 features = np.array([[0.12, 0.08, 0.31, 0.77, 0.02]]) # 示例特征 labels = ['normal', 'afib', 'pvc', 'noise'] proba = model.predict_proba(features)[0] pred = labels[int(np.argmax(proba))] print(pred, proba)
深度学习直接处理原始PPG或PPI序列正在成为主流。1D-CNN可以自动学习局部波形形态特征,LSTM适合建模时间依赖性。常见做法是把30秒PPG窗口作为输入,经过若干卷积层和池化层后进入全连接层。有的研究采用多任务学习,同时判断是否为房颤、是否可分析、是否早搏。相比手工特征,深度模型在数据量充足时能取得更高灵敏度,但对运动伪影和异源设备泛化能力需要特别小心。训练时常用Focal loss缓解类别不平衡,增强时加入随机增益、基线漂移和运动伪影混合。以下是一个简化的一维卷积网络示例。
import torch
import torch.nn as nn
class PPG1DCNN(nn.Module):
def __init__(self, num_classes=4):
super().__init__()
self.conv = nn.Sequential(
nn.Conv1d(1, 16, kernel_size=7, stride=2, padding=3),
nn.BatchNorm1d(16),
nn.ReLU(),
nn.Conv1d(16, 32, kernel_size=5, stride=2, padding=2),
nn.BatchNorm1d(32),
nn.ReLU(),
nn.AdaptiveAvgPool1d(1)
)
self.fc = nn.Linear(32, num_classes)
def forward(self, x):
x = self.conv(x)
x = x.view(x.size(0), -1)
return self.fc(x)
从工程部署角度看,模型大小和推理频率必须严格控制。很多手表不会持续运行完整深度模型,而是采用分级策略:先以低功耗规则检测片段是否可疑,再唤醒高精度模型进行短时分析,或者每隔五分钟才对PPG窗口做一次房颤筛查。这种设计可以在电池续航和检测灵敏度之间取得平衡。
四、性能评估与可穿戴落地限制
算法评估不能只看准确率。房颤筛查任务中,由于普通人群房颤患病率较低,阳性预测值往往不高。论文中常报告灵敏度、特异度、F1和受试者工作特征曲线下面积。一个典型的手表PPG房颤检测算法可能达到90%以上的灵敏度,但特异性在95%左右,实际阳性预测值可能只有30%到60%,尤其是偶发或短阵房颤。因此,评估时必须明确数据集的样本来源和划分方式,避免同源数据泄漏导致指标虚高。
工程落地还要考虑功耗、采样率和计算资源。PPG采样率越高、模型越复杂,耗电越大。传感器融合、肤色补偿、佩戴松紧检测等细节决定真实体验。算法更新还需要通过医疗器械认证的监管路径,不同国家和地区对光学心率监测和房颤筛查有不同的审批要求。厂商通常会区分健康监测功能和医疗级功能,后者的算法版本需要经过更严格的临床验证。
最后要明确一点:PPG不是心电图,智能手表给出的可疑房颤提示只能作为筛查信号,不能作为临床诊断依据。如果设备频繁报警或提示异常,应到医院做12导联心电图或动态心电图确认。未来基于自监督学习的去噪方法、多波长PPG和更大规模真实世界数据,有望进一步提升异常心律检测的稳定性和可解释性,同时减少因运动伪影和个体差异造成的误报。