第二章:光电容积脉搏波(PPG)原理

各位同学,欢迎来到血氧计算的第二关。上一章我们聊了血氧饱和度是什么,这一章咱们来拆解它的物理基础——PPG信号是怎么来的。

说实话,我刚入行那会儿,觉得PPG就是个简单的红光红外光交替照射。直到有一次,我在做运动场景下的血氧监测,信号被运动伪迹搞得一塌糊涂,才逼着我回头重新啃了一遍光学原理。嗯,基础不牢,地动山摇。

2.1 朗伯-比尔定律:光吸收的底层逻辑

要理解PPG,必须先搞懂朗伯-比尔定律。说白了,就是一束光穿过某种介质,被吸收了多少。

公式长这样:

A = ε × c × L

其中:

  • A:吸光度(无量纲)
  • ε:摩尔吸光系数(L·mol⁻¹·cm⁻¹)——每种物质有自己的“吸光指纹”
  • c:物质浓度(mol/L)
  • L:光程路径长度(cm)

你想想看,这个公式告诉我们三件事:

  1. 物质越浓,吸光越多
  2. 光路越长,吸光越多
  3. 不同物质对不同波长的光,吸光能力天差地别

核心要点:朗伯-比尔定律是线性的,但仅限于均匀介质、单色光、低浓度场景。人体组织可不是均匀的,所以实际应用中我们只能“近似使用”。

我的经验:我在做指尖血氧探头时,发现LED的发光波长漂移会直接破坏线性关系。所以选型时一定要挑温漂小的LED,别图便宜。

2.2 动脉血 vs 静脉血:谁在吸光?

这个问题很关键。血氧探头测的是动脉血,但手指里既有动脉血也有静脉血,还有组织、骨骼、皮肤……它们都在吸光。

为什么会这样?因为血红蛋白有两种主要形态:

  • 氧合血红蛋白(HbO₂):鲜红色,存在于动脉血
  • 脱氧血红蛋白(Hb):暗红色,存在于静脉血

它们的吸光特性完全不同。我直接给数据:

波长 HbO₂ 吸光系数 Hb 吸光系数 差异
660nm(红光) 约10倍差距
940nm(红外光) 约2倍差距

你看,红光对Hb敏感,红外光对HbO₂敏感。这就是双波长测量的物理基础。

避坑指南:我曾经在项目里直接用厂家给的吸光系数表,结果算出来的血氧值总是偏低。后来发现,不同厂家的LED实际发光波长有±10nm的偏差,吸光系数必须重新标定。切记,别偷懒。

2.3 红光与红外光双波长测量法

好了,现在我们把前面两块拼起来。PPG信号是怎么提取血氧的?

简单说,就是利用动脉搏动带来的光程变化。心脏收缩时,动脉血容量增加,光路变长,吸光增加;心脏舒张时,血容量减少,吸光减少。这个周期性变化就是PPG信号的交流分量(AC)。

而组织、静脉血、骨骼这些不随心跳变化的成分,产生的是直流分量(DC)。

双波长法的核心公式:

R = (AC_red / DC_red) / (AC_ir / DC_ir)

SpO₂ = A - B × R

其中:

  • R:比率值,反映两种波长的相对吸收变化
  • A、B:通过实验标定的系数

你想想看,为什么要用比值?因为这样可以消除很多干扰——比如手指粗细不同、皮肤颜色不同、探头压紧程度不同,这些因素对两个波长的影响是成比例的,一除就抵消了。

算法流程:

  1. 采集红光和红外光的PPG信号
  2. 分离AC分量(带通滤波,0.5-5Hz)和DC分量(低通滤波,<0.5Hz)
  3. 计算每个波长的AC/DC比值
  4. 计算R值 = 红光比值 / 红外光比值
  5. 查表或代入线性公式得到SpO₂

我的习惯:在实际代码里,我不会直接用单点计算R值。我会取一个滑动窗口(比如4秒),计算窗口内所有AC/DC比值的均值,再求R。这样能有效抑制呼吸干扰和偶然噪声。

2.4 一个简单的Python演示

下面是我早期做原型验证时写的代码片段,帮你理解计算过程:

import numpy as np

def calc_spo2(red_signal, ir_signal, fs=100):
    """
    双波长血氧计算(简化版)
    red_signal: 红光PPG信号
    ir_signal: 红外光PPG信号
    fs: 采样率,默认100Hz
    """
    # 1. 分离AC和DC
    # 用带通滤波提取AC(0.5-5Hz)
    from scipy.signal import butter, filtfilt
    
    b, a = butter(2, [0.5, 5], btype='band', fs=fs)
    red_ac = filtfilt(b, a, red_signal)
    ir_ac = filtfilt(b, a, ir_signal)
    
    # DC用低通滤波(<0.5Hz)
    b2, a2 = butter(2, 0.5, btype='low', fs=fs)
    red_dc = filtfilt(b2, a2, red_signal)
    ir_dc = filtfilt(b2, a2, ir_signal)
    
    # 2. 计算AC/DC比值
    # 取AC的RMS值作为有效幅度
    red_ratio = np.std(red_ac) / np.mean(red_dc)
    ir_ratio = np.std(ir_ac) / np.mean(ir_dc)
    
    # 3. 计算R值
    R = red_ratio / ir_ratio
    
    # 4. 查表或线性拟合(这里用经验公式)
    spo2 = 110 - 25 * R
    
    return spo2

注意:这个代码只能用于教学演示。实际产品中,你需要考虑运动伪迹去除、LED亮度自动调节、信号质量评估、异常值剔除等。我当年第一个版本的血氧算法,就是在这个基础上加了十几层保护逻辑才敢上临床。

2.5 本章小结

这一章我们聊了三件事:

  • 朗伯-比尔定律是光吸收的底层公式,但实际应用要打折
  • 动脉血和静脉血对红光、红外光的吸收差异,是测量的物理基础
  • 双波长法通过比值计算消除干扰,核心是AC/DC分离

下一章,我们会深入PPG信号的预处理——滤波、去基线漂移、运动伪迹抑制。这些都是我在项目里踩过坑的地方,到时候我会把血泪史一并分享。

记住,搞嵌入式医疗算法,理论要懂,但动手调参才是真功夫。咱们下章见。