智能手环的心率监测技术主要依赖光电容积描记法(Photoplethysmography, PPG),通过光学传感器检测血流变化来估算心率。此外,结合加速度传感器和算法优化,可以提升精度并适应不同场景(如运动、睡眠)。以下是详细介绍,包括技术原理、实现步骤,以及 C#, C++, Java, 和 Python 的代码示例,结合你之前提到的 REM 检测(RMSSD)、功耗优化 和 Sleep-EDF 数据集验证的需求,提供具体实现和调试指导。


1. 心率监测技术原理

1.1 PPG 技术

  • 工作原理:

    • PPG 使用 LED(通常绿色或红外光)照射皮肤,光被血液和组织吸收或反射。

    • 血液体积随心跳变化,引起反射光强度的周期性波动。

    • 光电传感器(如光电二极管)捕获反射光,生成 PPG 信号。

    • 通过检测 PPG 信号的峰值,计算心跳间隔(RR 间隔),进而估算心率(bpm = 60 / RR)。

  • 传感器:常见如 MAX30102(红外 + 红光 LED,适合心率和血氧检测)。

  • 优点:

    • 非侵入式,适合可穿戴设备。

    • 低成本,易集成。

  • 挑战:

    • 运动伪影:手腕运动干扰 PPG 信号。

    • 皮肤差异:肤色、纹身、佩戴松紧度影响信号质量。

    • 功耗:高采样率和 LED 亮度增加能耗。

1.2 信号处理与算法

  • 滤波:

    • 带通滤波(0.5-4Hz):去除低频漂移(如呼吸)和高频噪声(如运动)。

    • 卡尔曼滤波或指数移动平均(EMA):平滑信号。

  • 峰值检测:

    • 检测 PPG 信号的波峰,计算 RR 间隔。

    • 算法:滑动窗口、阈值法或自适应阈值。

  • 心率变异性(HRV):

    • RMSSD:连续 RR 间隔差的均方根,用于 REM 睡眠检测或压力分析。

    • 频域分析(LF/HF 比):评估自主神经系统状态。

  • 运动补偿:

    • 使用加速度传感器数据,识别运动伪影,剔除异常 RR 间隔。

  • 功耗优化:

    • 降低采样率(如 25Hz 代替 100Hz)。

    • 动态调整 LED 亮度。

    • 使用硬件中断触发采样。

1.3 典型应用

  • 实时心率:运动监测(如跑步心率)。

  • 睡眠分析:结合 RMSSD 检测 REM 睡眠(见前述)。

  • 压力监测:通过 HRV 分析压力水平。

  • 健康预警:检测异常心率(如心动过速)。


2. 实现方案

以下实现基于 PPG 传感器(如 MAX30102)的心率监测,结合加速度数据进行运动补偿,包含 RMSSD 计算和功耗优化。代码涵盖:

  • 数据处理:带通滤波、峰值检测、RMSSD 计算。

  • 功耗优化:低采样率(25Hz)、硬件中断(C++)。

  • 验证:使用 Sleep-EDF 数据集(Python)。

2.1 C# 实现(桌面验证)

适合后处理或模拟分析。

csharp

using System;
using System.Collections.Generic;
using System.Linq;

class HeartRateMonitor
{
    public class HeartRateData
    {
        public double Time { get; set; }
        public double BPM { get; set; }
        public double RMSSD { get; set; }
    }

    public static List<HeartRateData> MonitorHeartRate(double[] ppg, double[] accelX, double[] accelY, double[] accelZ, 
                                                       double[] timestamps, double sfreq = 25.0, int windowSize = 750)
    {
        List<HeartRateData> results = new List<HeartRateData>();
        double accelThreshold = 0.3; // g,运动伪影阈值

        // 带通滤波(简化,使用 EMA)
        double[] FilterEMA(double[] data, double alpha = 0.1)
        {
            double[] filtered = new double[data.Length];
            filtered[0] = data[0];
            for (int i = 1; i < data.Length; i++)
                filtered[i] = alpha * data[i] + (1 - alpha) * filtered[i - 1];
            return filtered;
        }

        // 峰值检测
        List<double> DetectPeaks(double[] signal, double threshold)
        {
            List<double> peaks = new List<double>();
            for (int i = 1; i < signal.Length - 1; i++)
                if (signal[i] > signal[i-1] && signal[i] > signal[i+1] && signal[i] > threshold)
                    peaks.Add(timestamps[i]);
            return peaks;
        }

        // 计算 RMSSD
        double CalculateRMSSD(List<double> rr_intervals)
        {
            if (rr_intervals.Count < 2) return 0;
            double sum = 0.0;
            for (int i = 1; i < rr_intervals.Count; i++)
                sum += Math.Pow(rr_intervals[i] - rr_intervals[i-1], 2);
            return Math.Sqrt(sum / (rr_intervals.Count - 1));
        }

        double[] filteredPPG = FilterEMA(ppg);
        double[] accelMag = new double[accelX.Length];
        for (int i = 0; i < accelX.Length; i++)
            accelMag[i] = Math.Sqrt(accelX[i] * accelX[i] + accelY[i] * accelY[i] + accelZ[i] * accelZ[i]);

        for (int i = 0; i <= ppg.Length - windowSize; i += windowSize)
        {
            double[] windowPPG = filteredPPG.Skip(i).Take(windowSize).ToArray();
            double[] windowAccel = accelMag.Skip(i).Take(windowSize).ToArray();
            double windowTime = timestamps[i];

            // 检查运动伪影
            double accelStd = Math.Sqrt(windowAccel.Average(v => Math.Pow(v - windowAccel.Average(), 2)));
            if (accelStd > accelThreshold) continue; // 跳过高运动窗口

            // 峰值检测
            double threshold = windowPPG.Average() + windowPPG.StandardDeviation();
            List<double> peaks = DetectPeaks(windowPPG, threshold);
            List<double> rr_intervals = peaks.Skip(1).Select((t, idx) => (t - peaks[idx]) * 1000).ToList();
            double bpm = peaks.Count > 1 ? 60.0 / (rr_intervals.Average() / 1000) : 0;
            double rmssd = CalculateRMSSD(rr_intervals);

            results.Add(new HeartRateData { Time = windowTime, BPM = bpm, RMSSD = rmssd });
        }

        return results;
    }

    static void Main()
    {
        int n = 3000;
        double[] ppg = new double[n];
        double[] accelX = new double[n];
        double[] accelY = new double[n];
        double[] accelZ = new double[n];
        double[] timestamps = new double[n];
        Random rand = new Random();
        for (int i = 0; i < n; i++)
        {
            timestamps[i] = i / 25.0; // 25Hz
            ppg[i] = 1.0 + 0.5 * Math.Sin(2 * Math.PI * 1.0 * timestamps[i]) + rand.NextDouble() * 0.1;
            accelX[i] = 0.05 * Math.Sin(0.01 * i) + rand.NextDouble() * 0.1;
            accelY[i] = 0.05 * Math.Sin(0.01 * i + 1) + rand.NextDouble() * 0.1;
            accelZ[i] = 1.0 + 0.05 * Math.Sin(0.01 * i + 2) + rand.NextDouble() * 0.1;
        }

        var results = MonitorHeartRate(ppg, accelX, accelY, accelZ, timestamps);
        foreach (var result in results)
            Console.WriteLine($"Time: {result.Time:F2}s, BPM: {result.BPM:F1}, RMSSD: {result.RMSSD:F1}ms");
    }
}

说明:

  • 滤波:EMA 滤波,适合桌面验证。

  • 峰值检测:动态阈值(均值 + 标准差)。

  • RMSSD:计算 RR 间隔差均方根。

  • 功耗:25Hz 采样率。


2.2 C++ 实现(Arduino + MAX30102 + MPU6050)

cpp

#include <Wire.h>
#include <MPU6050.h>
#include <SparkFun_MAX3010x_Sensor_Library.h> // 需安装 MAX3010x 库
#include <LowPower.h>

MAX30105 particleSensor;
MPU6050 mpu;
const int windowSize = 750; // 30秒,25Hz
float ppgWindow[windowSize];
float accelWindow[windowSize];
int windowIndex = 0;
float accelThreshold = 0.3;
volatile bool motionFlag = false;

class KalmanFilter {
private:
    float x = 0.0, P = 1.0, Q = 0.01, R = 0.1;
public:
    float filter(float z) {
        float x_pred = x;
        float P_pred = P + Q;
        float K = P_pred / (P_pred + R);
        x = x_pred + K * (z - x_pred);
        P = (1 - K) * P_pred;
        return x;
    }
};

KalmanFilter filterPPG, filterAccel;

void setup() {
    Serial.begin(9600);
    Wire.begin();
    mpu.initialize();
    if (!mpu.testConnection() || !particleSensor.begin(Wire, I2C_SPEED_FAST)) {
        Serial.println("Sensor connection failed");
        while (1);
    }
    particleSensor.setup(); // 默认设置
    mpu.setIntMotionEnabled(true);
    mpu.setMotionDetectionThreshold(20);
    mpu.setMotionDetectionDuration(2);
    attachInterrupt(digitalPinToInterrupt(2), motionDetected, RISING);
    for (int i = 0; i < windowSize; i++) {
        ppgWindow[i] = 0.0;
        accelWindow[i] = 0.0;
    }
}

void motionDetected() {
    motionFlag = true;
}

float calculateRMSSD(float* ppg, int size) {
    float rr_intervals[size];
    int peakCount = 0;
    for (int i = 1; i < size - 1; i++)
        if (ppg[i] > ppg[i-1] && ppg[i] > ppg[i+1] && ppg[i] > 0.5)
            rr_intervals[peakCount++] = i / 25.0 * 1000; // ms
    if (peakCount < 2) return 0;
    float sum = 0.0;
    for (int i = 1; i < peakCount; i++)
        sum += pow(rr_intervals[i] - rr_intervals[i-1], 2);
    return sqrt(sum / (peakCount - 1));
}

void loop() {
    if (!motionFlag) {
        LowPower.powerDown(SLEEP_120MS, ADC_OFF, BOD_OFF);
        return;
    }

    int16_t ax, ay, az;
    mpu.getAcceleration(&ax, &ay, &az);
    float accel = sqrt(ax * ax + ay * ay + az * az) / 16384.0;
    float filteredAccel = filterAccel.filter(accel);
    float ppg = particleSensor.getIR(); // 红外光数据
    float filteredPPG = filterPPG.filter(ppg);

    ppgWindow[windowIndex] = filteredPPG;
    accelWindow[windowIndex] = filteredAccel;
    windowIndex = (windowIndex + 1) % windowSize;

    if (windowIndex == 0) {
        float accelMean = 0.0, accelStd = 0.0;
        for (int i = 0; i < windowSize; i++)
            accelMean += accelWindow[i];
        accelMean /= windowSize;
        for (int i = 0; i < windowSize; i++)
            accelStd += pow(accelWindow[i] - accelMean, 2);
        accelStd = sqrt(accelStd / windowSize);
        if (accelStd > accelThreshold) return;

        float rmssd = calculateRMSSD(ppgWindow, windowSize);
        float bpm = 0;
        int peakCount = 0;
        for (int i = 1; i < windowSize - 1; i++)
            if (ppgWindow[i] > ppgWindow[i-1] && ppgWindow[i] > ppgWindow[i+1] && ppgWindow[i] > 0.5)
                peakCount++;
        if (peakCount > 1)
            bpm = 60.0 / ((windowSize / peakCount) / 25.0);

        Serial.print("Time: ");
        Serial.print(millis() / 1000.0);
        Serial.print("s, BPM: ");
        Serial.print(bpm);
        Serial.print(", RMSSD: ");
        Serial.println(rmssd);
    }

    motionFlag = false;
    delay(40); // 25Hz
}

说明:

  • 功耗优化:25Hz 采样,MPU6050 运动中断,LowPower 睡眠。

  • RMSSD:基于 PPG 峰值计算。

  • 硬件:MAX30102 + MPU6050,INT 引脚接 D2。

  • 依赖:MPU6050, SparkFun_MAX3010x.


2.3 Java 实现(Android)

java

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.util.ArrayDeque;
import java.util.Deque;

public class HeartRateActivity extends AppCompatActivity implements SensorEventListener {
    private SensorManager sensorManager;
    private Sensor accelerometer;
    private TextView hrView;
    private final int windowSize = 750; // 30秒,25Hz
    private final Deque<Double> ppgWindow = new ArrayDeque<>();
    private final Deque<Double> accelWindow = new ArrayDeque<>();
    private final LowPassFilter filterPPG = new LowPassFilter();
    private final LowPassFilter filterAccel = new LowPassFilter();

    static class LowPassFilter {
        private double filteredValue = 0.0;
        private final double alpha = 0.1;

        public double filter(double input) {
            filteredValue = alpha * input + (1 - alpha) * filteredValue;
            return(filteredValue);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        hrView = findViewById(R.id.hr_view);

        sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL);
    }

    private double calculateRMSSD(Deque<Double> ppg) {
        Double[] ppgArray = ppg.toArray(new Double[0]);
        List<Double> rr_intervals = new ArrayList<>();
        for (int i = 1; i < ppgArray.length - 1; i++)
            if (ppgArray[i] > ppgArray[i-1] && ppgArray[i] > ppgArray[i+1] && ppgArray[i] > 0.5)
                rr_intervals.add(i / 25.0 * 1000);
        if (rr_intervals.size() < 2) return 0;
        double sum = 0.0;
        for (int i = 1; i < rr_intervals.size(); i++)
            sum += Math.pow(rr_intervals.get(i) - rr_intervals.get(i-1), 2);
        return Math.sqrt(sum / (rr_intervals.size() - 1));
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
            float x = event.values[0] / 9.81f;
            float y = event.values[1] / 9.81f;
            float z = event.values[2] / 9.81f;
            double accel = Math.sqrt(x * x + y * y + z * z);
            double filteredAccel = filterAccel.filter(accel);
            double ppg = 1.0 + 0.5 * Math.sin(2 * Math.PI * 1.0 * (System.currentTimeMillis() / 1000.0)) + Math.random() * 0.1; // 模拟 PPG
            double filteredPPG = filterPPG.filter(ppg);

            ppgWindow.add(filteredPPG);
            accelWindow.add(filteredAccel);
            if (ppgWindow.size() > windowSize) {
                ppgWindow.removeFirst();
                accelWindow.removeFirst();
            }

            if (ppgWindow.size() == windowSize) {
                double accelMean = 0.0, accelStd = 0.0;
                for (double v : accelWindow) accelMean += v;
                accelMean /= windowSize;
                for (double v : accelWindow) accelStd += Math.pow(v - accelMean, 2);
                accelStd = Math.sqrt(accelStd / windowSize);
                if (accelStd > 0.3) return;

                double rmssd = calculateRMSSD(ppgWindow);
                int peakCount = 0;
                Double[] ppgArray = ppgWindow.toArray(new Double[0]);
                for (int i = 1; i < ppgArray.length - 1; i++)
                    if (ppgArray[i] > ppgArray[i-1] && ppgArray[i] > ppgArray[i+1] && ppgArray[i] > 0.5)
                        peakCount++;
                double bpm = peakCount > 1 ? 60.0 / ((windowSize / peakCount) / 25.0) : 0;

                double currentTime = System.currentTimeMillis() / 1000.0;
                runOnUiThread(() -> hrView.setText(String.format("BPM: %.1f, RMSSD: %.1fms", bpm, rmssd)));
            }
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {}

    @Override
    protected void onPause() {
        super.onPause();
        sensorManager.unregisterListener(this);
    }
}

布局(res/layout/activity_main.xml):

xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <TextView
        android:id="@+id/hr_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="BPM: 0"/>
</LinearLayout>

说明:

  • 功耗优化:SENSOR_DELAY_NORMAL(约 25Hz)。

  • RMSSD:检测 HRV。

  • 注意:PPG 数据模拟,需替换为实际传感器(如 Android Wear PPG)。


2.4 Python 实现(Sleep-EDF 验证)

python

import numpy as np
import pandas as pd
import tensorflow as tf
from sklearn.preprocessing import StandardScaler
import mne
import os

def load_sleep_edf(data_dir):
    psg_file = os.path.join(data_dir, "SC4001E0-PSG.edf")
    hypno_file = os.path.join(data_dir, "SC4001E0-Hypnogram.edf")
    psg = mne.io.read_raw_edf(psg_file, preload=True)
    hypno = mne.read_annotations(hypno_file)
    
    data = psg.get_data(picks=['ECG', 'Accel-X', 'Accel-Y', 'Accel-Z'])
    sfreq = psg.info['sfreq']
    timestamps = psg.times
    heart_rate = np.random.uniform(50, 90, len(timestamps))  // Replace with ECG-derived HR
    features = np.column_stack([data[1:4].T, heart_rate])
    
    labels = []
    for t in range(0, len(timestamps), int(sfreq * 30)):
        label = hypno[int(t / sfreq)]['description']
        label_id = {'W': 0, 'R': 1, '1': 2, '2': 2, '3': 3, '4': 3}.get(label, 2)
        labels.append(label_id)
    labels = np.array(labels[:len(features) // int(sfreq * 30)])
    
    return features, timestamps, labels, sfreq

def calculate_rmssd(ppg_data, window_size, sfreq):
    peaks = []
    for i in range(1, len(ppg_data) - 1):
        if ppg_data[i] > ppg_data[i-1] and ppg_data[i] > ppg_data[i+1] and ppg_data[i] > 0.5:
            peaks.append(i / sfreq * 1000)
    if len(peaks) < 2: return 0
    diffs = np.diff(peaks)
    return np.sqrt(np.mean(diffs ** 2))

def monitor_heart_rate(data, timestamps, sfreq, window_size=750):
    results = []
    for i in range(0, len(data) - window_size, window_size):
        window = data[i:i + window_size]
        accel_mag = np.sqrt(window[:, 0]**2 + window[:, 1]**2 + window[:, 2]**2)
        if np.std(accel_mag) > 0.3: continue
        ppg = window[:, 3]
        rmssd = calculate_rmssd(ppg, window_size, sfreq)
        peaks = []
        for j in range(1, len(ppg) - 1):
            if ppg[j] > ppg[j-1] and ppg[j] > ppg[j+1] and ppg[j] > 0.5:
                peaks.append(j / sfreq)
        bpm = len(peaks) > 1 ? 60.0 / (np.mean(np.diff(peaks))) : 0
        results.append({"time": timestamps[i], "bpm": bpm, "rmssd": rmssd})
    return results

if __name__ == "__main__":
    data_dir = "path_to_sleep_edf"
    features, timestamps, labels, sfreq = load_sleep_edf(data_dir)
    results = monitor_heart_rate(features, timestamps, sfreq)
    for result in results:
        print(f"Time: {result['time']:.2f}s, BPM: {result['bpm']:.1f}, RMSSD: {result['rmssd']:.1f}ms")

说明:

  • Sleep-EDF:验证心率和 RMSSD。

  • 功耗:25Hz 采样。

  • 依赖:mne, numpy, pandas.


3. 数据集验证:Sleep-EDF

  • 下载:

  • 处理:

    • 提取 ECG 和加速度,计算心率和 RMSSD。

    • 验证 REM 检测(RMSSD > 30ms)。

  • 验证:

    • 比较心率和 Hypnogram 标签,检查 REM 阶段准确性。


4. 硬件调试

  • 连接:MPU6050 (VCC→5V, GND→GND, SCL→A5, SDA→A4, INT→D2), MAX30102 (VCC→3.3V, GND→GND, SCL→A5, SDA→A4).

  • 问题解决:

    • PPG 信号弱:调整 MAX30102 LED 电流,紧贴皮肤。

    • 运动伪影:验证加速度阈值(0.3g)。

    • 中断失效:检查 INT 引脚和阈值设置。

  • 工具:串口监视器打印 PPG、加速度、RMSSD。


5. 总结

  • 心率监测:PPG 技术,结合滤波、峰值检测、RMSSD。

  • 实现:C#(验证)、C++(嵌入式)、Java(Android)、Python(Sleep-EDF)。

  • 功耗优化:25Hz 采样,硬件中断。

  • 数据集:Sleep-EDF 验证 RMSSD 和心率。

下一步:需要 LF/HF 比、Android 数据库存储、或具体硬件调试?请告诉我,我将提供更详细代码或指导!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐