第一次用手机摄像头测心率时,我真正意外的不是算法,而是信号居然已经在画面里了。手指盖住镜头和闪光灯后,预览画面只是一片柔和的红色,但连续看几百帧,会发现红色强度在轻微起伏。

MonoPump 的起点就是这个现象。它不是在照片里识别心脏,而是把摄像头当作光学传感器:用闪光灯照亮指尖,观察血液流动带来的红色通道变化,再从变化周期估算 BPM。
先看到现象,再谈算法
当手指覆盖摄像头时,闪光灯的光进入指尖。每一次心跳都会带来血容量变化,光的吸收和散射也随之变化。单帧没有意义,连续帧才会组成波形。
This diagram could not be rendered. Its source is still available below.
View diagram source
flowchart LR
A[闪光灯照亮指尖] --> B[血容量随心跳变化]
B --> C[红色通道逐帧轻微波动]
C --> D[形成时间序列信号]
D --> E[检测峰值并估算 BPM]
我一开始以为难点会是复杂 DSP。后来发现,更早的难点是信号到底有没有资格被拿来算。如果手指没盖严、漏光、用力不稳定,或者相机曝光一直自动调整,最终得到的就不是脉搏,而是相机和手指姿势的混合噪声。
先稳住相机,再处理信号
MonoPump 的采集链路刻意保持简单:后置广角摄像头、BGRA 像素帧、30 fps、打开闪光灯,并在光线稳定后锁定曝光。锁曝光很关键,因为自动曝光如果持续变化,会把我们要观察的微小亮度波动补偿掉。
This diagram could not be rendered. Its source is still available below.
View diagram source
sequenceDiagram
participant UI as 测量界面
participant C as CameraManager
participant F as 帧分析器
UI->>C: 请求相机帧
C->>C: 配置 30 fps BGRA 输出
C->>C: 打开闪光灯并锁定曝光
C-->>F: 发送 CMSampleBuffer
F-->>UI: 覆盖状态、波形、置信度
一帧图像本身不是心率样本。MonoPump 采样画面中心区域,计算平均 R/G/B、亮度、红色比例、红色占优程度,以及红色明显领先的像素比例。
redRatio = avgRed / max((avgGreen + avgBlue) / 2, 1)
redDominance = avgRed / max(avgGreen, avgBlue)
redIntensity = redRatio * (avgRed / 255.0)
这里最重要的是拆开两个问题:这一帧像不像手指覆盖镜头的画面;如果像,它应该贡献什么信号值。早期把这两个问题混在一起,坏帧也会进入波形,后面再怎么滤波都很被动。
摄像头 PPG 有价值,但它不是魔法。肤色、指压、温度、相机硬件、闪光灯发热、环境漏光都会影响结果。所以 MonoPump 把结果定位为带质量分和置信度的描述性估算,而不是医疗诊断。