把手机变成听诊器!Android 摄像头 30 秒隔空测心率 ------ 基于 MediaPipe + POS 算法的 rPPG 实战
关键词:rPPG、非接触心率、Android、CameraX、MediaPipe、POS 算法、开源 Demo
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 项目结构速览
arduino
app/
├─ RPPGAct.kt // UI + CameraX 生命周期
├─ FaceAnalyzer.kt // 人脸检测 & ROI 提取
└─ RppgProcessor.kt // POS 滤波 + 峰值检测 + 生理指标
3.2 第 1 步:CameraX 实时采集
kotlin
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 人脸关键点检测
kotlin
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 信号
kotlin
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 算法消除镜面反射
kotlin
// 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) | 同上,低频通道 |
kotlin
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!

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