把手機變成聽診器!Android 攝像頭 30 秒隔空測心率 —— 基於 MediaPipe + POS 算法的 rPPG 實戰

image.png

關鍵詞:rPPG、非接觸心率、Android、CameraX、MediaPipe、POS 算法、開源 Demo
源碼地址:<https://github.com/liyufengrex/RPPG

APK體驗: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)

image.png

  • 分辨率 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!

Screen_Recording_20251224_161208_RPPG_1(1).gif

如果本文幫到了你,記得點個贊 ❤️ 再走~