把手機變成聽診器!Android 攝像頭 30 秒隔空測心率 —— 基於 MediaPipe + POS 算法的 rPPG 實戰
關鍵詞:rPPG、非接觸心率、Android、CameraX、MediaPipe、POS 算法、開源 Demo
源碼地址:<https://github.com/liyufengrex/RPPGAPK體驗:https://github.com/liyufengrex/RPPG/blob/main/apk/app.apk
1. 引言:為什麼刷臉就能知道心跳?
傳統心率測量需要佩戴手環、電極或血氧探頭,而 遠程光電容積脈搏波描記法(rPPG) 只需要手機攝像頭。
原理一句話:**血液對光的吸收量隨心跳週期性變化 → 皮膚顏色發生微弱變化 → 用算法把“顏色變化”翻譯成“心率”**。
本文基於開源項目 RPPG-Android,帶你拆解「檢測-跟蹤-提取-濾波-計算」5 步流程,30 行核心 Kotlin 代碼即可跑通 Demo,誤差 ≤ 3 bpm(靜息狀態)。
2. 參考方案與依賴
| 模塊 | 選型 | 版本 |
|---|---|---|
| 相機框架 | CameraX | 1.3.0 |
| 人臉關鍵點 | MediaPipe Face Landmarker | com.google.mediapipe:tasks-vision:0.10.9 |
| 信號處理 | POS(Plane-Orthogonal-to-Skin) | 2014 IEEE T-IP 論文算法 |
| 語言 & IDE | Kotlin + Android Studio Hedgehog | JDK 17 |
POS 算法優勢:
① 無需訓練數據;② 對光照變化、頭部平移/旋轉魯棒;③ 計算量小,中端手機 30 ms/幀。
3. 實現步驟(含關鍵代碼片段)
3.1 項目結構速覽
app/
├─ RPPGAct.kt // UI + CameraX 生命週期
├─ FaceAnalyzer.kt // 人臉檢測 & ROI 提取
└─ RppgProcessor.kt // POS 濾波 + 峯值檢測 + 生理指標
3.2 第 1 步:CameraX 實時採集
val analysis = ImageAnalysis.Builder()
.setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST)
.build()
analysis.setAnalyzer(executor, FaceAnalyzer(::onFrame))
cameraProvider.bindToLifecycle(lifecycle, cameraSelector, preview, analysis)
- 分辨率 640×480,YUV_420_888 格式,幀率 30 fps。
- 每幀耗時 < 50 ms 即可保證不丟幀。
3.3 第 2 步:MediaPipe 人臉關鍵點檢測
val baseOptions = BaseOptions.builder()
.setModelAssetPath("face_landmarker.task") // 確保 assets 目錄下有此模型文件
.build()
val options = FaceLandmarkerOptions.builder()
.setBaseOptions(baseOptions)
.setRunningMode(RunningMode.IMAGE) // 使用同步模式
.setNumFaces(1)
.build()
faceLandmarker = FaceLandmarker.createFromOptions(context, options)
- 檢測策略:每 15 幀全量檢測一次,其餘幀複用上一幀 ROI,降低 CPU 佔用 40%。
- ROI 選取:額頭中心關鍵點 10、左角 109、右角 338 → 正方形邊長 = 0.2 × |109-338|,避開頭髮/眉毛。
3.4 第 3 步:空間平均 → RGB 信號
val roi = getForeheadRect(landmarks)
var rSum = 0L; var gSum = 0L; var bSum = 0L
for (y in roi.top until roi.bottom) {
for (x in roi.left until roi.right) {
val px = rgbBitmap.getPixel(x, y)
rSum += red(px); gSum += green(px); bSum += blue(px)
}
}
val pixelCount = roi.width() * roi.height()
val rgb = floatArrayOf(rSum/pixelCount, gSum/pixelCount, bSum/pixelCount)
- 640×480 幀中 ROI 約 4 000 像素,空間平均有效抑制隨機噪聲。
- 輸出 3 條時間序列
R(t), G(t), B(t),採樣率 = 30 Hz。
3.5 第 4 步:POS 算法消除鏡面反射
// 1. 歸一化
val mean = rgb.clone().apply { forEachIndexed { i, _ -> this[i] /= windowSize } }
val norm = rgb.map { it / mean }.toFloatArray()
// 2. 正交投影
val s1 = norm[1] - norm[2] // G - B
val s2 = norm[1] + norm[2] - 2*norm[0] // G + B - 2R
val alpha = std(s1) / (std(s2) + 1e-6f)
val bvp = s1 + alpha * s2
- 僅 6 行代碼,0 浮點矩陣分解,在普通手機上耗時 < 0.5 ms。
- 有效去除燈光鏡面高光、頭部抖動帶來的共模干擾。
3.6 第 5 步:帶通濾波 + 峯值檢測
| 生理指標 | 頻段 | 實現方式 |
|---|---|---|
| 心率 | 0.7–4 Hz (42–240 bpm) | 滑動平均差分 + 4 階 Butterworth IIR |
| 呼吸率 | 0.15–0.5 Hz (9–30 rpm) | 同上,低頻通道 |
val peaks = findPeaks(bvp, minDistance = 30) // 30 幀 ≈ 1 s
val ibi = peaks.zipWithNext { a, b -> b - a } // 單位:幀
val hr = 60f * fps / ibi.average()
- SDNN(心率變異性)=
ibi.map{ it * 1000 / fps }.std(),單位 ms。 - 呼吸率同理,在低頻通道做峯值檢測即可。
3.7 第 6 步:UI 實時展示
| 組件 | 更新頻率 | 數據來源 |
|---|---|---|
TextView hrText |
1 Hz | RppgProcessor.hr |
TextView rrText |
0.2 Hz | RppgProcessor.rr |
| LineChart | 15 fps | 原始 BVP 曲線 |
- 使用
MPAndroidChart庫,橫軸 5 s 滑動窗口,縱軸自動縮放。 - 心率數字做 3 點滑動平均,防止跳變造成用户焦慮。
4. 實驗結果
| 場景 | 樣本數 | 平均誤差 | 備註 |
|---|---|---|---|
| 靜息室內 | 20 人 | 1.8 bpm | 光照 300–500 lux |
| 步行後 | 10 人 | 3.4 bpm | 頭部輕微晃動 |
| 室外逆光 | 10 人 | 5.1 bpm | 鏡面反射強烈,誤差增大 |
提示:室外建議開啓 前置攝像頭 + 手動遮擋頭髮,可降誤差到 3 bpm 以內。
5. 常見問題 FAQ
Q1: 必須 30 fps 嗎?
15 fps 也能跑,但頻域分辨率減半,心率上限降到 120 bpm。
Q2: 支持多人臉嗎?
MediaPipe 已支持,但 ROI 重疊會導致信號串擾,建議單人場景。
Q3: 能否測血氧?
理論上可用雙波長,但手機無可控紅外光源,誤差 > 5%,不建議醫療用途。
6. 結論 & 展望
- 整套方案 零硬件成本,在千元機上 30 秒給出心率+RR+SDNN,適合居家健康篩查。
- 下一步:
① 引入 BCG(頭部微動)多模態融合,提升運動魯棒性;
② 使用 TensorFlow Lite 端到端迴歸,直接輸出 HR,跳過傳統信號處理;
③ 通過 Health Service API 將數據同步到 Google Fit。
7. 源碼 & 引用 & 效果演示
GitHub - RPPG-Android 歡迎 Star & PR!
如果本文幫到了你,記得點個贊 ❤️ 再走~