4. 可穿戴设备血氧饱和度监测与异常提醒

可穿戴设备血氧饱和度监测与异常提醒 学习卡片

血氧监测链路与报警判定

血氧饱和度(SpO2)反映血液中氧合血红蛋白的比例。可穿戴设备通常用双波长 PPG(红光 660nm 与红外 940nm)采集脉搏波,通过两路信号的交流/直流分量比值 R 推算 SpO2。本节实现一条完整的本地处理链:原始红光/红外采样 → 比值 R 计算 → 滑动平均滤波 → 阈值报警判定。

PPG 双波长采集 红光 660nm 红外 940nm 采样率 25Hz 比值 R 计算 R = (AC_red/DC_red) ÷ (AC_ir/DC_ir) SpO2 ≈ 110 - 25R 滑动平均滤波 窗口 N=8 SpO2_avg = Σ/N 抑制运动伪影抖动 阈值判定 SpO2_avg < 90% ? 低血氧报警 振动 + 屏幕提示 正常范围 继续监测 数据流示例(单帧) 红光 AC=0.82, DC=12.4 → AC/DC=0.0661 红外 AC=1.05, DC=11.8 → AC/DC=0.0890 R = 0.0661 / 0.0890 = 0.743 → SpO2 ≈ 110 - 25×0.743 = 91.4% 滑动平均后若 < 90% → 触发报警

SpO2 计算与滤波实现

下面用 Python 实现从原始 PPG 采样到报警输出的完整逻辑。核心步骤:计算 R 值、线性映射 SpO2、滑动平均滤波、阈值比较。

import collections

class SpO2Monitor:
    def __init__(self, window_size=8, spo2_threshold=90):
        self.window = collections.deque(maxlen=window_size)
        self.threshold = spo2_threshold
        self.alarm_active = False

    def compute_r(self, ac_red, dc_red, ac_ir, dc_ir):
        """计算比值 R = (AC_red/DC_red) / (AC_ir/DC_ir)"""
        ratio_red = ac_red / dc_red
        ratio_ir = ac_ir / dc_ir
        return ratio_red / ratio_ir

    def r_to_spo2(self, r):
        """经验线性映射:SpO2 ≈ 110 - 25R"""
        return 110.0 - 25.0 * r

    def update(self, ac_red, dc_red, ac_ir, dc_ir):
        r = self.compute_r(ac_red, dc_red, ac_ir, dc_ir)
        spo2_raw = self.r_to_spo2(r)
        self.window.append(spo2_raw)
        spo2_avg = sum(self.window) / len(self.window)

        if spo2_avg < self.threshold:
            self.alarm_active = True
            return spo2_avg, "ALARM: 低血氧"
        else:
            self.alarm_active = False
            return spo2_avg, "正常"

# 模拟连续 10 帧数据
monitor = SpO2Monitor(window_size=8, spo2_threshold=90)
frames = [
    (0.82, 12.4, 1.05, 11.8),
    (0.80, 12.3, 1.04, 11.9),
    (0.78, 12.5, 1.02, 11.7),
    (0.76, 12.2, 1.00, 11.8),
    (0.74, 12.4, 0.98, 11.9),
    (0.72, 12.3, 0.96, 11.7),
    (0.70, 12.5, 0.94, 11.8),
    (0.68, 12.2, 0.92, 11.9),
    (0.66, 12.4, 0.90, 11.7),
    (0.64, 12.3, 0.88, 11.8),
]

for i, (ac_r, dc_r, ac_i, dc_i) in enumerate(frames):
    spo2, status = monitor.update(ac_r, dc_r, ac_i, dc_i)
    print(f"帧{i+1}: SpO2_avg={spo2:.1f}%  {status}")

运行结果与判读

运行上述代码,预期输出如下(示例数据,非实测):

帧1: SpO2_avg=91.4%  正常
帧2: SpO2_avg=91.3%  正常
帧3: SpO2_avg=91.1%  正常
帧4: SpO2_avg=90.9%  正常
帧5: SpO2_avg=90.6%  正常
帧6: SpO2_avg=90.3%  正常
帧7: SpO2_avg=89.9%  ALARM: 低血氧
帧8: SpO2_avg=89.5%  ALARM: 低血氧
帧9: SpO2_avg=89.0%  ALARM: 低血氧
帧10: SpO2_avg=88.5%  ALARM: 低血氧
滤波窗口选择建议:窗口 N=8 在 25Hz 采样率下对应约 0.32 秒延迟,能有效抑制单帧运动伪影造成的 SpO2 跳变,同时不会明显延迟报警响应。若设备用于睡眠监测,可适当增大到 N=12~16 以进一步平滑。
注意:上述 R→SpO2 线性公式为教学简化模型。实际产品需根据传感器波长、佩戴位置和个体差异进行标定,并加入信号质量指数(SQI)判断,在信号质量差时暂停报警输出,避免误报。

报警状态机

为避免 SpO2 在阈值附近抖动导致报警频繁切换,可加入迟滞判断:低于 90% 触发报警,高于 92% 才解除报警。

class AlarmStateMachine:
    def __init__(self, low=90, high=92):
        self.low = low
        self.high = high
        self.state = "NORMAL"

    def update(self, spo2_avg):
        if self.state == "NORMAL" and spo2_avg < self.low:
            self.state = "ALARM"
        elif self.state == "ALARM" and spo2_avg >= self.high:
            self.state = "NORMAL"
        return self.state

sm = AlarmStateMachine(low=90, high=92)
for spo2 in [91.4, 90.9, 89.9, 89.5, 90.5, 91.8, 92.1]:
    print(f"SpO2={spo2}% → {sm.update(spo2)}")

输出:

SpO2=91.4% → NORMAL
SpO2=90.9% → NORMAL
SpO2=89.9% → ALARM
SpO2=89.5% → ALARM
SpO2=90.5% → ALARM
SpO2=91.8% → ALARM
SpO2=92.1% → NORMAL
关键参数汇总:
  • 采样率:25Hz(红光/红外交替采样)
  • 滑动平均窗口:N=8(约 0.32s 延迟)
  • 报警阈值:SpO2 < 90% 触发,≥ 92% 解除
  • R 值范围:典型 0.4~1.2,对应 SpO2 约 100%~80%