きれいになった波形を見ると安心しますが、ユーザーが知りたいのは結局ひとつです。今の心拍数はいくつか。答えはピークの高さではなく、ピークとピークの距離にあります。

MonoPump の方法は控えめです。もっともらしいピークを見つけ、タイムスタンプで間隔を測り、怪しい間隔を外し、秒を BPM に変換し、最後に UI が暴れない程度に平滑化します。
This diagram could not be rendered. Its source is still available below.
View diagram source
flowchart LR
A[整えた PPG 窓] --> B[適応的なしきい値]
B --> C[局所最大値]
C --> D[ピーク間隔リスト]
D --> E[中央値ベースの外れ値除去]
E --> F[BPM = 60 / 平均間隔]
F --> G[EMA 平滑化]
最初から FFT にしなかった理由
FFT は分かりやすく、脈拍帯域の強い周波数を見つければ BPM にできます。ただ、スマートフォンカメラの短く汚い窓では、その美しさが必ずしも勝ちません。動き、フレーム落ち、接触不良があると、間違った周波数が強く見えることがあります。
ピーク検出は派手ではありませんが、デバッグしやすい。推定が外れたら、ピークと間隔を見れば理由を追えます。
ピークは時間の目印
MonoPump は適応的なしきい値を超える局所最大値を探します。同時に、1 つの拍が複数の小さなピークに分かれないよう、ピーク間の最小距離を設けます。
minPeakDistance = sampleRate * 60 / maxBPM
threshold = mean + 0.5 * standardDeviation
固定しきい値は振幅が安定している場合にだけ強いです。カメラ PPG の振幅はそこまで素直ではないため、窓ごとにしきい値を動かします。
This diagram could not be rendered. Its source is still available below.
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 は更新ごとに揺れます。全部見せるとアプリが落ち着きません。逆に固定しすぎると不自然です。MonoPump は EMA を使い、さらに更新ごとの変化幅に上限を設けます。
smoothed = 0.35 * current + 0.65 * previous
smoothed = clamp(smoothed, previous - maxDelta, previous + maxDelta)
信頼度は飾りではありません。使えるピーク間隔の数と、最近の信号変動が最終結果の門番になります。数字を出すのは簡単です。出さない判断の方が難しいのです。