如何在心率检测图表中过滤小峰值,准确识别有效峰值?
如何在摄像头心率检测中过滤无效小峰值,仅统计有效心跳峰值?
当前基于手机摄像头的心率检测器图表输出效果良好,但峰值计算时会出现黄色标记的小峰值(有效峰值间可能出现多个),需要排除这类无效峰值,只保留对应心跳的有效峰值。此前尝试平滑处理效果不佳,用“最近5个值是否递增”的逻辑识别峰值稳定性很差,现有代码如下:
void _scanImage(CameraImage image) { _actual = DateTime.now(); _avgRedValue = image.planes.first.bytes.reduce((value, element) => value + element) / image.planes.first.bytes.length; if (_data.length >= _displayLength) { _data.removeAt(0); } setState(() { _data.add(Value(_actual, 255 - _avgRedValue)); if (_data.length >= 7) { int lastIndex = _data.length - 1; List<double> last7Values = _data .sublist(lastIndex - 5, lastIndex) .map((value) => value.value) .toList(); double lastValue = _data[lastIndex].value; double previousValue = _data[lastIndex - 1].value; if (lastValue > previousValue && lastValue > _data[lastIndex - 2].value && _isIncreasing(last7Values)) { // 检测到潜在峰值 peakValues.add(lastValue); } else if (peakValues.isNotEmpty && lastValue < previousValue) { // 检测到心跳 double heartbeatValue = peakValues.reduce((a, b) => a > b ? a : b); peakValues.clear(); int beatNow = DateTime.now().millisecondsSinceEpoch; if(previous != 0) { } if(beatNow - previous > 500){ print('Heartbeat detected: $heartbeatValue'); heartbeatCount++; } } } }); } bool _isIncreasing(List<double> values) { if (values.length < 2) { return false; } for (int i = 1; i < values.length; i++) { if (values[i] <= values[i - 1]) { return false; } } return true; }
解决方案
可以从峰值显著性、心跳时间周期、峰值判定逻辑三个维度优化,过滤无效小峰值:
1. 基于滑动基线的阈值过滤
通过计算最近一段时间数据的均值作为基线,只有显著高于基线的峰值才判定为有效,过滤掉正常波动产生的小峰值。
2. 结合心率范围的时间过滤
正常心率范围为60-180次/分钟,对应心跳间隔333ms-1000ms。限制两次有效峰值的时间间隔在这个范围内,避免短时间内的连续小峰值被误判。
3. 改进峰值判定逻辑
放弃“连续严格递增”的严苛要求,改为判定当前值是局部范围内的最高点(比如前后3个点中的最大值),减少局部波动的干扰。
修改后的代码实现
// 先在类中添加几个参数变量 int _lastValidPeakTime = 0; final int _minBeatInterval = 333; // 最小心跳间隔(对应180BPM) final int _maxBeatInterval = 1000; // 最大心跳间隔(对应60BPM) final double _peakThresholdRatio = 1.15; // 峰值需高于基线的比例(可根据实际调整) void _scanImage(CameraImage image) { _actual = DateTime.now(); _avgRedValue = image.planes.first.bytes.reduce((value, element) => value + element) / image.planes.first.bytes.length; if (_data.length >= _displayLength) { _data.removeAt(0); } setState(() { _data.add(Value(_actual, 255 - _avgRedValue)); // 确保有足够的数据计算基线和判定峰值 if (_data.length >= 10) { int lastIndex = _data.length - 1; double lastValue = _data[lastIndex].value; double prevValue = _data[lastIndex - 1].value; double prevPrevValue = _data[lastIndex - 2].value; // 1. 判定是否为局部峰值:比前后至少2个点都高 bool isLocalPeak = lastValue > prevValue && lastValue > prevPrevValue; if (lastIndex >= 3) { isLocalPeak &= lastValue > _data[lastIndex - 3].value; } if (isLocalPeak) { // 计算滑动基线:取最近20个数据的均值 int baselineStart = max(0, lastIndex - 20); List<double> baselineData = _data.sublist(baselineStart, lastIndex) .map((value) => value.value) .toList(); double baseline = baselineData.reduce((a, b) => a + b) / baselineData.length; // 2. 检查是否高于阈值 bool meetsThreshold = lastValue > baseline * _peakThresholdRatio; // 3. 检查时间间隔是否符合心率范围 int currentTime = _actual.millisecondsSinceEpoch; bool meetsTimeInterval = currentTime - _lastValidPeakTime > _minBeatInterval; if (meetsThreshold && meetsTimeInterval) { int beatNow = currentTime; // 同时确保间隔不超过最大心率范围 if (_lastValidPeakTime != 0 && beatNow - _lastValidPeakTime <= _maxBeatInterval) { print('心跳检测到: $lastValue,间隔: ${beatNow - _lastValidPeakTime}ms'); heartbeatCount++; } _lastValidPeakTime = beatNow; } } } }); }
关键优化点说明
- 滑动基线:用最近20个数据的均值作为基线,能自动适应光照等环境变化,避免固定阈值的局限性
- 时间间隔过滤:严格限制有效峰值的时间范围,符合人体心率的生理规律,直接过滤掉密集的小峰值
- 局部峰值判定:扩大参考范围到前后3个点,减少单次波动带来的误判
内容的提问来源于stack exchange,提问作者Leader ride
相关产品推荐
相关产品推荐

