You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于AFE49I30传感器的ECG/PPG字节数组计算心率(BPM)

基于AFE49I30传感器的PPG/ECG心率计算实现(Xamarin.Forms)

一、字节数组解析前置步骤

AFE49I30的蓝牙传输字节是按传感器输出格式封装的,首先需将字节数组转换为原始PPG/ECG采样值:

1. 确定采样数据的字节格式

AFE49I30的PPG和ECG输出一般为16位有符号整数(需匹配硬件配置的12/14/16位输出调整),字节顺序通常为小端模式。解析逻辑如下:

PPG/ECG采样值解析代码

// 解析字节数组为采样值列表
private List<int> ParseRawSamples(byte[] receivedBytes, int sampleSize = 2)
{
    var samples = new List<int>();
    // 按每个采样占sampleSize字节拆分
    for (int i = 0; i < receivedBytes.Length; i += sampleSize)
    {
        if (i + sampleSize > receivedBytes.Length)
            break;
        // 小端转int16
        short rawValue = BitConverter.ToInt16(receivedBytes, i);
        // 若为12/14位采样,需做位对齐(示例:12位输出右移4位)
        // int adjustedValue = rawValue >> (16 - 12);
        samples.Add(rawValue);
    }
    return samples;
}

二、心率计算核心逻辑

1. ECG心率计算(R波检测法)

ECG心率依赖检测QRS波群中的R波峰值,步骤如下:

  • 滤波:去除基线漂移(滑动平均)和高频噪声(低通滤波)
  • R波检测:设置动态阈值(当前窗口最大值的70%左右),捕捉超过阈值的峰值点
  • BPM计算:通过相邻R波的时间间隔(RR间期)转换为心率:BPM = 60 / (RR间期秒数)

实现代码示例

// 全局变量:存储R波时间戳、滑动窗口数据
private List<long> _rWaveTimestamps = new List<long>();
private const int EcgWindowSize = 100; // 滑动窗口大小
private List<int> _ecgBuffer = new List<int>();

private void CalculateEcgBpm(List<int> ecgSamples, long currentTimestampMs)
{
    _ecgBuffer.AddRange(ecgSamples);
    // 维持窗口大小
    if (_ecgBuffer.Count > EcgWindowSize)
        _ecgBuffer.RemoveRange(0, _ecgBuffer.Count - EcgWindowSize);

    // 1. 滑动平均滤波去基线
    var filteredEcg = ApplyMovingAverage(_ecgBuffer, 5);

    // 2. 检测R波峰值
    int threshold = (int)(filteredEcg.Max() * 0.7);
    for (int i = 1; i < filteredEcg.Count - 1; i++)
    {
        if (filteredEcg[i] > threshold && filteredEcg[i] > filteredEcg[i-1] && filteredEcg[i] > filteredEcg[i+1])
        {
            // 去重:避免连续帧重复检测同一R波
            if (_rWaveTimestamps.Count == 0 || currentTimestampMs - _rWaveTimestamps.Last() > 300)
            {
                _rWaveTimestamps.Add(currentTimestampMs);
                // 仅保留最近5个R波数据
                if (_rWaveTimestamps.Count > 5)
                    _rWaveTimestamps.RemoveAt(0);
            }
        }
    }

    // 3. 计算BPM
    if (_rWaveTimestamps.Count >= 2)
    {
        long totalIntervalMs = 0;
        for (int i = 1; i < _rWaveTimestamps.Count; i++)
        {
            totalIntervalMs += _rWaveTimestamps[i] - _rWaveTimestamps[i-1];
        }
        long averageIntervalMs = totalIntervalMs / (_rWaveTimestamps.Count - 1);
        int bpm = (int)(60000.0 / averageIntervalMs);
        // 更新UI显示
        // HeartRateLabel.Text = $"{bpm} BPM";
    }
}

// 滑动平均滤波
private List<int> ApplyMovingAverage(List<int> data, int windowSize)
{
    var result = new List<int>();
    for (int i = 0; i < data.Count; i++)
    {
        int sum = 0;
        int count = 0;
        for (int j = Math.Max(0, i - windowSize/2); j <= Math.Min(data.Count-1, i + windowSize/2); j++)
        {
            sum += data[j];
            count++;
        }
        result.Add(sum / count);
    }
    return result;
}

2. PPG心率计算(峰值检测法)

PPG是光电容积脉搏波,心率计算依赖检测脉搏波峰值:

  • 滤波:去除运动噪声(自适应滤波或带通滤波)和直流分量
  • 峰值检测:设置动态阈值(基于实时信号波动调整)
  • BPM计算:通过相邻峰值的时间间隔转换为心率

实现代码示例

// 全局变量:存储PPG峰值时间戳、滑动窗口
private List<long> _ppgPeakTimestamps = new List<long>();
private const int PpgWindowSize = 150;
private List<int> _ppgBuffer = new List<int>();

private void CalculatePpgBpm(List<int> ppgSamples, long currentTimestampMs)
{
    _ppgBuffer.AddRange(ppgSamples);
    if (_ppgBuffer.Count > PpgWindowSize)
        _ppgBuffer.RemoveRange(0, _ppgBuffer.Count - PpgWindowSize);

    // 1. 带通滤波(去除基线和高频噪声)
    var filteredPpg = ApplyBandPassFilter(_ppgBuffer);

    // 2. 检测脉搏波峰值
    int baseline = (int)filteredPpg.Average();
    int peakThreshold = baseline + (int)(filteredPpg.Max() - baseline) * 0.6;
    for (int i = 1; i < filteredPpg.Count - 1; i++)
    {
        if (filteredPpg[i] > peakThreshold && filteredPpg[i] > filteredPpg[i-1] && filteredPpg[i] > filteredPpg[i+1])
        {
            if (_ppgPeakTimestamps.Count == 0 || currentTimestampMs - _ppgPeakTimestamps.Last() > 400)
            {
                _ppgPeakTimestamps.Add(currentTimestampMs);
                if (_ppgPeakTimestamps.Count > 5)
                    _ppgPeakTimestamps.RemoveAt(0);
            }
        }
    }

    // 3. 计算BPM
    if (_ppgPeakTimestamps.Count >= 2)
    {
        long totalIntervalMs = 0;
        for (int i = 1; i < _ppgPeakTimestamps.Count; i++)
        {
            totalIntervalMs += _ppgPeakTimestamps[i] - _ppgPeakTimestamps[i-1];
        }
        long averageIntervalMs = totalIntervalMs / (_ppgPeakTimestamps.Count - 1);
        int bpm = (int)(60000.0 / averageIntervalMs);
        // 更新UI
        // PpgHeartRateLabel.Text = $"{bpm} BPM";
    }
}

// 简单带通滤波(去除基线和高频)
private List<int> ApplyBandPassFilter(List<int> data)
{
    // 滑动平均去基线
    var noBaseline = ApplyMovingAverage(data, 20);
    // 差分去高频
    var filtered = new List<int>();
    for (int i = 1; i < noBaseline.Count; i++)
    {
        filtered.Add(noBaseline[i] - noBaseline[i-1]);
    }
    return filtered;
}

三、集成到现有代码中

修改你的GetPPGLiveData和GetECGLiveData方法,加入解析与计算逻辑:

修改后的PPG数据处理

PPGList.ValueUpdated += (o, args) =>
{
    var receivedBytes = args.Characteristic.Value;
    var currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

    Xamarin.Essentials.MainThread.BeginInvokeOnMainThread(() =>
    {
        var ppgSamples = ParseRawSamples(receivedBytes);
        CalculatePpgBpm(ppgSamples, currentTimestamp);
    });
};

修改后的ECG数据处理

ECGList.ValueUpdated += (o, args) =>
{
    var receivedBytes = args.Characteristic.Value;
    var currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

    Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(() =>
    {
        var ecgSamples = ParseRawSamples(receivedBytes);
        CalculateEcgBpm(ecgSamples, currentTimestamp);
        return Task.CompletedTask;
    });
};

四、优化建议

  • 硬件配置对齐:确认AFE49I30的采样率、输出位宽,调整解析代码中的位对齐逻辑,避免采样值失真
  • 滤波优化:ECG可替换为巴特沃斯低通滤波,PPG可结合加速度传感器数据做运动 artifact 检测
  • 阈值自适应:根据实时信号波动动态调整峰值检测阈值,提升不同场景下的准确率
  • 线程安全:全局采样缓存和时间戳列表需加锁,避免多线程读写冲突
  • 异常处理:加入字节数组长度检查、采样值范围校验,避免计算时出现异常

内容的提问来源于stack exchange,提问作者Makwana Prahlad

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 08:10:48