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

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

关键词:rPPG、非接触心率、Android、CameraX、MediaPipe、POS 算法、开源 Demo

源码地址:github.com/liyufengrex...


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!

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

相关推荐
阿巴斯甜4 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker5 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95276 小时前
Andorid Google 登录接入文档
android
黄林晴7 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab19 小时前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android