全部文章

【TECH】如何数心跳:峰值检测与 BPM 估算

MonoPump 如何把清洗后的 PPG 波形,通过峰间距、时间戳、离群过滤和 EMA 平滑转成 BPM。

BPM峰值检测PPG算法
【TECH】如何数心跳:峰值检测与 BPM 估算

清洗后的波形看起来让人安心,但 App 最后还是要回答用户真正关心的问题:我现在心率是多少?答案不在峰值有多高,而在峰和峰之间隔了多久。

健康应用界面上的脉搏波形,峰值被清晰标记
BPM 来自峰与峰之间的时间距离,而不是波形有多高。

MonoPump 的算法刻意保持朴素:找可信峰值,用时间戳计算峰间隔,过滤可疑间隔,把秒换算成 BPM,再做平滑,避免界面上的数字乱跳。

Flow diagram Preparing diagram
View diagram source
flowchart LR
    A[清洗后的 PPG 窗口] --> B[自适应峰值阈值]
    B --> C[局部最大值]
    C --> D[峰间隔列表]
    D --> E[中位数离群过滤]
    E --> F[BPM = 60 / 平均间隔]
    F --> G[EMA 平滑]

为什么没有一上来用 FFT

频域方法很有吸引力。FFT 的模型很漂亮:在脉搏频段里找最强频率,再换算成 BPM。但手机摄像头的短窗口、脏数据不一定奖励这种优雅。运动、丢帧、接触不稳定,都可能让一个错误频率看起来很自信。

峰值检测没那么酷,但容易调试。估错了,我可以直接看峰和峰间隔。对消费级 App 来说,大多数失败来自接触质量,而不是数学不够高级。

峰值本质上是时间标记

MonoPump 寻找超过自适应阈值的局部最大值,同时限制峰之间的最小距离,避免一次心跳被拆成多个小峰。

minPeakDistance = sampleRate * 60 / maxBPM
threshold = mean + 0.5 * standardDeviation

固定阈值只适合幅度稳定的场景,而手机摄像头 PPG 不是这样的场景。自适应阈值会跟随当前窗口,同时避免每个小波动都变成心跳。

真相在间隔里

找到峰之后,用时间戳计算相邻峰之间的间隔。一个坏峰不应该决定结果,所以 MonoPump 会围绕中位数过滤间隔,再对剩余间隔求平均。

Flow diagram Preparing diagram
View diagram source
flowchart TD
    P1[t1 的峰] --> I1[t2 - t1]
    P2[t2 的峰] --> I2[t3 - t2]
    P3[t3 的峰] --> I3[t4 - t3]
    I1 --> M[中位数间隔]
    I2 --> M
    I3 --> M
    M --> F[剔除偏离过大的间隔]

平滑数字,但不掩盖变化

原始 BPM 每次更新都会自然波动。每个跳动都展示出来,App 会显得紧张;把数字完全冻住,又显得不真实。MonoPump 用指数移动平均,并限制每次变化幅度,让数字能动,但不要瞬移。

smoothed = 0.35 * current + 0.65 * previous
smoothed = clamp(smoothed, previous - maxDelta, previous + maxDelta)

置信度不是装饰。它来自可用峰间隔数量,质量分来自近期信号是否有足够变化。心率 App 永远可以打印一个数字,更难的是知道什么时候不该打印。

来自 MonoWare

MonoPump 背后还有更多上下文。

打开产品网站,或继续阅读按 MonoPump 筛选的笔记。

通讯

隐私与产品工程笔记,偶尔发送。