把手机变成听诊器!摄像头 30 秒隔空测心率 - 开箱即用

1,656 阅读4分钟

把手机变成听诊器!Android 摄像头 30 秒隔空测心率 —— 基于 MediaPipe + POS 算法的 rPPG 实战

关键词:rPPG、非接触心率、Android、CameraX、MediaPipe、POS 算法、开源 Demo
源码地址:<github.com/liyufengrex…

APK体验:github.com/liyufengrex…


1. 引言:为什么刷脸就能知道心跳?

传统心率测量需要佩戴手环、电极或血氧探头,而 远程光电容积脉搏波描记法(rPPG) 只需要手机摄像头。
原理一句话:血液对光的吸收量随心跳周期性变化 → 皮肤颜色发生微弱变化 → 用算法把“颜色变化”翻译成“心率”

本文基于开源项目 RPPG-Android,带你拆解「检测-跟踪-提取-滤波-计算」5 步流程,30 行核心 Kotlin 代码即可跑通 Demo,误差 ≤ 3 bpm(静息状态)。


2. 参考方案与依赖

模块选型版本
相机框架CameraX1.3.0
人脸关键点MediaPipe Face Landmarkercom.google.mediapipe:tasks-vision:0.10.9
信号处理POS(Plane-Orthogonal-to-Skin)2014 IEEE T-IP 论文算法
语言 & IDEKotlin + Android Studio HedgehogJDK 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)

3c13f78bf4d99fbbc1e1127571deef07.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 hrText1 HzRppgProcessor.hr
TextView rrText0.2 HzRppgProcessor.rr
LineChart15 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!

rppg.gif

如果本文帮到了你,记得点个赞 ❤️ 再走~