当手指覆盖稳定之后,最容易犯的错就是立刻算 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[平滑高频噪声]
E --> F[归一化波形]
F --> G[峰值检测]
原始红色值不能直接用
指尖不是固定在实验夹具上的。用户会稍微按紧,又稍微放松;闪光灯会让皮肤变热;曝光会逐渐稳定;手会移动一毫米。这些都会让红色值产生慢变化。如果直接在原始值上找峰,找到的可能是姿势和曝光,而不是心跳。
用真实时间,不要只相信 30fps
默认 30fps 再用样本下标换算时间很简单,但真实手机会丢帧,处理队列也会丢弃迟到帧。MonoPump 保存每个样本的 presentation timestamp,并用实际窗口估算采样率。
This diagram could not be rendered. Its source is still available below.
View diagram source
sequenceDiagram
participant C as Camera
participant B as Buffer
participant P as Preprocessor
C->>B: t0 时刻的值
C->>B: t1 时刻的值
C->>B: t2 时刻的值
B->>P: 值 + 时间戳
P->>P: 估算真实采样率
BPM 是时间测量,不是数组下标测量。这个选择能消掉一类很隐蔽的错误。
去趋势和轻度平滑
脉搏是相对快的振荡,手指压力和曝光漂移更慢。MonoPump 用局部移动平均作为基线,从原始信号里减掉它,再用很小的移动平均压掉高频像素噪声。
detrended[i] = signal[i] - average(signal around i)
This diagram could not be rendered. Its source is still available below.
View diagram source
flowchart TD
R[原始红色序列] --> D[减去局部基线]
D --> S[3 点移动平均]
S --> N[归一化]
N --> W[显示给用户的波形]
N --> P[峰值检测输入]
漂亮波形不等于有用波形。目标不是动画好看,而是保住时间信息。最终这一层会产出给界面看的波形、给峰值检测用的清洗信号,以及质量分。坏信号不应该变成一个看起来很确定的数字。