ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

三轴加速度计计步算法实战:动态阈值与滤波技术详解

三轴加速度计计步算法实战:动态阈值与滤波技术详解 1. 从波形到步数计步算法的核心逻辑拆解三轴加速度传感器计步这件事看起来简单——不就是检测人走路时的震动嘛。但真把它做成一个能在各种场景下稳定工作的功能里面的门道比想象中多得多。我从五年前开始接触可穿戴设备的固件开发前前后后调过六七版计步算法踩过的坑足够写一本小册子。这篇文章就把我在动态阈值和滤波技术上的实战经验完整梳理一遍从原理到代码到调参尽量说透。先说清楚这个内容适合谁看。如果你正在做智能手环、智能手表、运动鞋垫、或者任何需要基于加速度传感器做步态检测的产品这篇文章里的方案可以直接参考。如果你只是对传感器算法感兴趣想了解手机里那个计步数字是怎么来的也能看明白。我尽量不堆公式用实际数据和代码来说明问题。计步算法的本质是什么一句话概括从三轴加速度传感器输出的连续波形中识别出人行走时产生的周期性特征信号并将其转化为步数。人走路时身体重心会上下起伏这个起伏反映在加速度传感器的Z轴或者合加速度上就是一个近似正弦的周期波形。每一步对应一个波峰或者一个波谷。听起来很简单对吧但问题在于这个波形会被各种噪声污染——手部晃动、传感器佩戴松动、不同人的步态差异、不同速度下的波形变化都会让一个固定的阈值方案彻底失效。我最早用的方案就是固定阈值合加速度超过某个值就计一步。这个方案在实验室里跑得挺好一到真实场景就完蛋。同一个人慢走和快跑的加速度幅值能差三倍以上不同的人同样的速度幅值也能差一倍。更别说把设备戴在手腕上和别在腰上波形特征完全不同。所以后来我转向了动态阈值方案配合滤波技术才把准确率从70%出头拉到了95%以上。注意计步算法的准确率评估没有统一标准不同测试集下的数据差异很大。我在文中给出的准确率数据都基于我们自己的测试集20名测试者每人走400步包含慢走、正常走、快走三种速度仅供参考。2. 信号预处理滤波技术怎么选怎么调2.1 原始信号的噪声来源分析在讲滤波之前得先搞清楚我们要滤掉什么。三轴加速度传感器的原始输出里除了人行走的步态信号还混杂着以下几类噪声第一类是高频噪声来自传感器本身的电子噪声和量化误差频率通常在50Hz以上。这类噪声幅值小但会让波形出现毛刺影响峰值检测的精度。第二类是运动伪影来自手部或身体的非步态运动比如挥手、打字、挠头。这类噪声的频率范围很宽从0.5Hz到20Hz都有可能而且幅值可能比步态信号还大。这是最麻烦的一类干扰。第三类是重力分量漂移当传感器姿态发生变化时重力在三轴上的投影会改变导致合加速度的基线发生偏移。比如手臂从下垂变成抬起Z轴的重力分量就会显著变化。第四类是低频漂移来自传感器的温漂和积分误差频率通常在0.1Hz以下。针对这四类噪声我的滤波方案是先做一个低通滤波去掉高频噪声再做一个带通滤波保留步态频段最后通过动态基线跟踪来消除重力分量和低频漂移的影响。2.2 滤波器选型为什么我最终选了巴特沃斯滤波器的选择上我试过好几种方案。最开始用的是简单的滑动平均实现简单但截止频率不明确过渡带太宽会把一部分步态信号也滤掉。后来试了FIR滤波器线性相位特性好但阶数高了之后计算量大在低功耗MCU上跑起来吃力。最后我选了二阶巴特沃斯低通滤波器原因是它在通带内最平坦过渡带陡峭度也够用而且二阶的计算量很小用差分方程实现只需要几个乘加运算。步态信号的频率范围是多少正常行走的步频大约在1.5Hz到3Hz之间对应加速度波形的基频就是1.5Hz到3Hz。跑步时步频可能到4Hz甚至5Hz。但加速度波形的谐波成分很丰富二次谐波、三次谐波都有一定能量。所以低通滤波器的截止频率不能设得太低否则会把谐波滤掉导致波形失真。我最终把截止频率设在10Hz这样既能保留步态信号的主要谐波成分又能滤掉大部分高频噪声。采样率的选择也很关键。根据奈奎斯特采样定理采样率至少要大于信号最高频率的两倍。但实际工程中为了保证波形质量采样率通常取信号最高频率的5到10倍。我用的采样率是50Hz对应最高能还原25Hz的信号对于10Hz截止频率的低通滤波来说绰绰有余。而且50Hz的采样率在低功耗MCU上很容易实现不会显著增加功耗。2.3 滤波器的差分方程实现二阶巴特沃斯低通滤波器的传递函数标准形式是H(s) ωc² / (s² √2·ωc·s ωc²)其中ωc是截止角频率等于2πfc。把这个传递函数通过双线性变换离散化可以得到差分方程。不过实际工程中我通常直接用现成的系数计算公式或者用MATLAB/Octave的butter函数生成系数。以50Hz采样率、10Hz截止频率为例用双线性变换得到的二阶巴特沃斯低通滤波器系数大约是b0 0.0675, b1 0.1350, b2 0.0675 a1 -1.1430, a2 0.4129差分方程就是y[n] b0·x[n] b1·x[n-1] b2·x[n-2] - a1·y[n-1] - a2·y[n-2]这个计算量非常小每次采样只需要5次乘法和4次加法在Cortex-M0级别的MCU上也能轻松跑。// 二阶巴特沃斯低通滤波器实现 typedef struct { float b0, b1, b2, a1, a2; float x1, x2, y1, y2; } BiquadFilter; float biquad_process(BiquadFilter *f, float input) { float output f-b0 * input f-b1 * f-x1 f-b2 * f-x2 - f-a1 * f-y1 - f-a2 * f-y2; f-x2 f-x1; f-x1 input; f-y2 f-y1; f-y1 output; return output; }实操心得滤波器的初始状态很重要。如果x1、x2、y1、y2初始化为0前几个采样点的输出会有明显的瞬态响应。我的做法是用前10个采样点的平均值来初始化这些状态变量这样滤波器一开始就能工作在稳态。2.4 合加速度的计算与坐标系处理三轴加速度传感器输出的是三个轴上的加速度分量。对于计步来说我们关心的是合加速度的幅值变化。合加速度的计算公式是|a| √(ax² ay² az²)但这里有个问题当传感器静止时合加速度等于重力加速度g约9.8m/s²。人行走时合加速度会在g的基础上波动。所以实际用于计步的信号是合加速度减去重力分量也就是a_step |a| - g不过直接用|a| - g有个缺陷当传感器姿态变化时重力在三轴上的投影会改变但合加速度的幅值不变。所以|a| - g这个方法对姿态变化是鲁棒的。但前提是传感器在测量过程中没有持续的线性加速度否则合加速度的幅值会偏离g。我在实际项目中用的方案是先计算合加速度然后用一个低通滤波器截止频率0.1Hz提取合加速度的直流分量这个直流分量就近似等于重力分量。然后用合加速度减去这个直流分量得到步态信号。这个方法比直接减g更准确因为它能自适应重力分量的变化。// 合加速度计算与重力分量跟踪 float acc_magnitude sqrtf(ax*ax ay*ay az*az); // 用一阶低通滤波器跟踪重力分量 // 截止频率0.1Hz采样率50Hz float alpha 0.0125f; // 2*pi*0.1/50 近似 gravity_dc gravity_dc * (1 - alpha) acc_magnitude * alpha; // 步态信号 float step_signal acc_magnitude - gravity_dc;这个一阶低通滤波器的系数计算α 2π·fc / fs其中fc0.1Hzfs50Hz所以α ≈ 0.0126。这个值很小意味着重力分量的跟踪非常缓慢不会把步态信号也跟踪进去。3. 动态阈值让计步算法适应每一个人3.1 固定阈值的致命缺陷我最早用的固定阈值方案阈值设在0.8m/s²。在实验室里测试者正常行走步态信号幅值大约在1.2m/s²到1.5m/s²之间0.8的阈值能很好地检测到每一步。但到了真实场景问题就来了。慢走的时候步态信号幅值可能只有0.5m/s²低于阈值算法完全检测不到步数。快跑的时候幅值可能到3m/s²以上波形出现双峰一个步态周期内会触发两次阈值导致步数翻倍。不同的人差异也很大体重重的人步态信号幅值大体重轻的人幅值小手臂长的人波形周期长手臂短的人周期短。我做过一个统计用固定阈值方案在20名测试者、三种速度的测试集上平均准确率只有72.3%。最差的情况是慢走准确率不到50%。这个结果显然不能接受。3.2 动态阈值的核心思路动态阈值的核心思想是阈值不是固定的而是根据最近的信号幅值动态调整。具体来说我维护一个滑动窗口窗口内保存最近N个步态信号的峰值。然后根据这些峰值的统计特征来计算当前阈值。我用的方案是阈值 滑动窗口内峰值的平均值 × 系数K。系数K通常取0.5到0.6之间。这样当步态信号幅值增大时阈值自动升高避免双峰误触发当幅值减小时阈值自动降低避免漏计。滑动窗口的长度选择很关键。窗口太短阈值波动大容易受单个异常峰值影响窗口太长阈值适应速度慢当步速突然变化时阈值跟不上。我试过5、10、15、20几种窗口长度最终选了10。在50Hz采样率下10个步态周期大约对应4到7秒的时间窗口这个时间尺度既能平滑掉单步的波动又能跟上步速的变化。3.3 峰值检测与阈值更新的协同动态阈值和峰值检测是协同工作的。我的算法流程是这样的第一步对滤波后的步态信号进行峰值检测。峰值检测的条件是当前点大于前一个点且大于后一个点且大于当前阈值。这三个条件同时满足才认为是一个有效峰值。第二步当检测到一个有效峰值时把这个峰值的幅值存入滑动窗口替换掉最旧的峰值。第三步重新计算滑动窗口内峰值的平均值更新阈值。第四步为了防止在同一个步态周期内重复检测设置一个最小步间隔时间。正常行走的步频最高大约3Hz对应最小步间隔约0.33秒。我设置的最小间隔是0.25秒这样既能防止重复检测又不会漏掉快速行走的步数。#define PEAK_WINDOW_SIZE 10 #define MIN_STEP_INTERVAL 12 // 50Hz采样率下12个采样点约0.24秒 typedef struct { float peak_values[PEAK_WINDOW_SIZE]; int peak_index; int peak_count; float threshold; int last_step_sample; int sample_counter; } StepDetector; void step_detector_init(StepDetector *sd) { memset(sd-peak_values, 0, sizeof(sd-peak_values)); sd-peak_index 0; sd-peak_count 0; sd-threshold 0.3f; // 初始阈值设低一点 sd-last_step_sample -MIN_STEP_INTERVAL; sd-sample_counter 0; } int step_detector_process(StepDetector *sd, float signal) { sd-sample_counter; int step_detected 0; // 峰值检测需要保存前一个采样点 static float prev_signal 0; static float prev_prev_signal 0; // 检测峰值当前点大于前一个点且前一个点大于前前个点 // 这里用简单的斜率变化来检测 if (prev_signal prev_prev_signal prev_signal signal prev_signal sd-threshold) { // 检查最小步间隔 if (sd-sample_counter - sd-last_step_sample MIN_STEP_INTERVAL) { // 有效步数 step_detected 1; sd-last_step_sample sd-sample_counter; // 更新峰值窗口 sd-peak_values[sd-peak_index] prev_signal; sd-peak_index (sd-peak_index 1) % PEAK_WINDOW_SIZE; if (sd-peak_count PEAK_WINDOW_SIZE) { sd-peak_count; } // 更新阈值 float sum 0; for (int i 0; i sd-peak_count; i) { sum sd-peak_values[i]; } float avg_peak sum / sd-peak_count; sd-threshold avg_peak * 0.55f; } } prev_prev_signal prev_signal; prev_signal signal; return step_detected; }注意上面的代码是简化版实际工程中还需要处理信号初始化阶段的问题。在算法刚启动时峰值窗口是空的阈值是初始值。这时候如果阈值设得太高可能前几步检测不到设得太低又可能误触发。我的做法是前2秒不输出步数只用这段时间来填充峰值窗口和初始化阈值。3.4 系数K的调参经验系数K决定了阈值相对于平均峰值的比例。K太大阈值太高容易漏计K太小阈值太低容易误触发。我试过0.3到0.8之间的多个值最终选了0.55。这个值是怎么定的我做了个实验用不同K值跑同一组测试数据统计准确率和误触发率。K0.55时准确率最高达到96.2%误触发率只有1.8%。K0.4时准确率95.1%但误触发率升到4.3%。K0.7时准确率降到91.5%漏计明显增多。不过K值不是一成不变的。如果你的应用场景主要是慢走可以把K调低到0.45左右提高灵敏度。如果主要是跑步可以把K调到0.65避免双峰误触发。这个需要根据具体场景来调。4. 完整算法流程与实测数据4.1 从传感器原始数据到步数的完整链路把前面的内容串起来完整的计步算法流程是这样的传感器以50Hz的采样率输出三轴加速度数据。第一步计算合加速度幅值。第二步用一阶低通滤波器截止频率0.1Hz提取重力分量合加速度减去重力分量得到步态信号。第三步用二阶巴特沃斯低通滤波器截止频率10Hz对步态信号进行滤波去除高频噪声。第四步用动态阈值峰值检测算法检测步态峰值每检测到一个有效峰值步数加一。整个算法的计算量很小。合加速度计算需要3次乘法、2次加法、1次开方。重力分量跟踪需要2次乘法、1次加法。低通滤波需要5次乘法、4次加法。峰值检测和阈值更新需要大约15次乘加运算。总计每采样点大约30次浮点运算。在50Hz采样率下每秒1500次浮点运算。对于Cortex-M0级别的MCU通常没有浮点单元用定点数实现也能轻松跑。4.2 实测数据与对比分析我用20名测试者10男10女年龄22到45岁体重50kg到90kg做了对比测试。每人分别以慢走约1.0m/s、正常走约1.4m/s、快走约1.8m/s三种速度各走400步。测试设备是同一款智能手环传感器是常见的三轴加速度传感器量程±8g分辨率12位。测试结果如下方案慢走准确率正常走准确率快走准确率平均准确率固定阈值48.2%76.5%92.1%72.3%动态阈值无滤波82.3%91.7%94.5%89.5%动态阈值滤波94.8%97.2%96.5%96.2%从数据可以看出动态阈值方案相比固定阈值方案在慢走场景下提升最明显从48.2%提升到82.3%。加上滤波之后又提升了12.5个百分点达到94.8%。正常走和快走的提升幅度相对小一些但也有明显改善。滤波对慢走的提升最大原因是慢走时步态信号幅值小高频噪声的相对影响更大。滤波之后信噪比提高峰值检测更准确。4.3 不同佩戴位置的适应性计步算法还有一个关键问题佩戴位置。手环戴在手腕上、手表戴在手腕上、手机放在裤兜里、计步器别在腰上传感器感受到的波形特征完全不同。手腕上的波形除了步态信号还叠加了手臂摆动的信号。手臂摆动的频率和步频一致但相位和幅值不同。我的算法在手腕场景下正常走的准确率是97.2%但慢走时降到91.3%。原因是慢走时手臂摆动幅度小步态信号和手臂摆动信号的区分度降低。裤兜里的波形主要反映大腿的上下运动步态信号幅值比手腕大但高频噪声也更多。我的算法在裤兜场景下正常走准确率96.8%慢走93.5%快走95.1%。腰部的波形最干净步态信号幅值大噪声小。我的算法在腰部场景下三种速度的准确率都在97%以上。实操心得如果你的产品需要支持多种佩戴位置建议在算法里加一个佩戴位置检测模块。我的做法是用一个简单的分类器根据信号的幅值范围、频率特征、三轴能量分布来判断当前佩戴位置然后切换对应的参数组。这个分类器不需要很复杂用几个阈值判断就能达到90%以上的分类准确率。5. 常见问题与排查技巧实录5.1 计步数偏多误触发怎么排查计步数偏多是最常见的问题。我遇到过的误触发原因主要有这么几种第一种是高频噪声导致的毛刺被误判为峰值。排查方法是把滤波后的信号打印出来看如果波形上有明显的毛刺说明滤波器截止频率设得太高或者滤波器阶数不够。解决办法是降低截止频率或者改用四阶滤波器。第二种是运动伪影导致的误触发。比如用户挥手、打字、挠头这些动作的加速度幅值可能超过阈值被误判为步态。排查方法是看误触发发生的时间点如果和用户的非步态动作时间吻合就是运动伪影。解决办法是加一个步态周期一致性检查连续两个峰值之间的时间间隔应该在0.25秒到1.5秒之间超出这个范围的峰值不计入步数。第三种是阈值太低导致的误触发。排查方法是看阈值和信号幅值的对比如果阈值明显低于信号的平均峰值说明K值设得太小。解决办法是增大K值或者增大滑动窗口长度。5.2 计步数偏少漏计怎么排查漏计的原因相对少一些但排查起来更麻烦。常见原因有第一种是阈值太高。如果阈值接近甚至超过信号峰值就会漏计。排查方法是看阈值和信号峰值的对比如果阈值大于信号峰值的80%说明K值设得太大。解决办法是减小K值。第二种是滤波过度。如果低通滤波器的截止频率设得太低步态信号的高次谐波被滤掉波形变得平滑峰值不明显。排查方法是看滤波后的波形如果波形接近正弦但幅值明显减小说明滤波过度。解决办法是提高截止频率。第三种是最小步间隔设得太大。如果用户步频很高比如跑步时步频达到3Hz以上步间隔只有0.33秒。如果最小步间隔设成0.4秒就会漏计。排查方法是看漏计发生的时间点如果和用户的高步频时段吻合就是最小步间隔的问题。解决办法是减小最小步间隔但要注意不能太小否则会误触发。5.3 常见问题速查表问题现象可能原因排查方法解决办法步数偏多高频噪声毛刺查看滤波后波形是否有毛刺降低截止频率或增加滤波器阶数步数偏多运动伪影对比误触发时间与用户动作加步态周期一致性检查步数偏多阈值太低对比阈值与信号峰值增大K值或窗口长度步数偏少阈值太高对比阈值与信号峰值减小K值步数偏少滤波过度查看滤波后波形幅值提高截止频率步数偏少最小步间隔太大对比漏计时间与步频减小最小步间隔步数不稳定滑动窗口太短观察阈值波动增大窗口长度步数不稳定重力分量跟踪太快观察基线漂移减小α值5.4 独家避坑技巧第一个技巧算法启动阶段要特殊处理。算法刚启动时峰值窗口是空的阈值是初始值。如果初始阈值设得不好前几步要么漏计要么误触发。我的做法是前2秒不输出步数只用这段时间来填充峰值窗口。2秒后窗口里至少有3到5个峰值阈值就比较准确了。第二个技巧用中值滤波代替均值滤波来计算阈值。均值滤波容易受异常峰值影响比如一个特别大的运动伪影峰值会把平均值拉高导致后续几步漏计。中值滤波对异常值更鲁棒。我后来的版本改用中值滤波准确率又提升了1.5个百分点。第三个技巧加一个步态节律检查。人行走时步间隔的变化是平滑的不会突然从0.5秒跳到1.5秒。如果检测到的步间隔变化超过50%说明可能是误触发。这个检查可以进一步降低误触发率。第四个技巧低功耗优化。计步算法通常需要24小时运行功耗很关键。我的做法是在MCU的DMA中断里做数据采集在主循环里做算法处理。算法处理可以降频运行比如每2个采样点处理一次这样计算量减半功耗也减半准确率只下降0.3个百分点。6. 算法扩展与进阶方向6.1 从计步到步态分析计步只是步态分析的最基础功能。基于同样的信号还可以做很多扩展步频检测、步幅估计、行走距离估算、甚至步态对称性分析。步频检测很简单就是统计单位时间内的步数。我的算法里已经维护了步间隔时间取倒数就是瞬时步频。步幅估计稍微复杂一些需要建立步频和步幅的回归模型。我用的模型是步幅 a × 步频 b其中a和b因人而异需要通过校准来确定。行走距离估算就是步数乘以步幅。这个功能的准确率取决于步幅估计的准确率。我的实测数据是在正常走场景下距离估算误差在5%以内慢走和快走场景下误差在8%到10%。6.2 基于机器学习的改进方案传统的动态阈值方案已经能达到96%以上的准确率但如果想进一步提升或者想支持更复杂的场景比如上下楼梯、跑步、骑行区分就需要引入机器学习方法。我试过用简单的决策树和随机森林来做步态分类特征包括信号幅值、步频、步间隔方差、三轴能量比等。在区分行走和跑步的场景下分类准确率能达到98%以上。但机器学习方法的计算量大在低功耗MCU上跑起来吃力。我的折中方案是用传统算法做实时计步用机器学习做后台的场景分类场景分类的结果用来调整传统算法的参数。6.3 多传感器融合的思路如果设备上还有陀螺仪可以把陀螺仪的数据也融合进来。陀螺仪测量的是角速度可以反映身体的旋转运动。行走时身体的旋转运动也有周期性特征。把加速度和角速度的周期性特征融合起来可以进一步提高计步的准确率尤其是在运动伪影干扰大的场景下。融合的方法可以用简单的加权平均也可以用卡尔曼滤波。我试过加权平均在挥手干扰场景下准确率从85%提升到93%。卡尔曼滤波的效果更好但计算量也更大。实操心得多传感器融合的前提是传感器之间的时间同步要做好。如果加速度和角速度的采样时间戳对不齐融合效果会大打折扣。我的做法是用同一个定时器触发两个传感器的采样确保时间戳一致。6.4 自适应参数调整的工程实现最后分享一个工程上的技巧自适应参数调整。不同用户的步态特征差异很大如果算法参数是固定的很难在所有用户上都达到最优。我的做法是在算法里加一个自适应模块根据用户前100步的步态特征自动调整K值、窗口长度、最小步间隔等参数。具体来说算法启动后的前100步用默认参数运行同时记录步态信号的峰值分布、步间隔分布、信噪比等特征。100步后根据这些特征查表得到最优参数组合。这个自适应过程对用户是无感的但能把准确率再提升2到3个百分点。这个自适应模块的实现不复杂核心就是一个特征提取加查表的过程。特征提取用滑动窗口统计查表用简单的if-else或者查表数组。计算量很小不会显著增加功耗。我在实际项目里把这个方案落地之后最直观的感受是用户投诉少了。以前经常有用户反馈“我走了1000步只记了800步”或者“我坐着没动步数自己在涨”加了动态阈值和滤波之后这类投诉基本消失了。当然算法没有完美的极端场景下还是会有偏差比如用户推着购物车走手臂不摆动手腕上的传感器就检测不到步态信号。这种场景目前没有特别好的解决方案只能靠多传感器融合或者佩戴位置检测来缓解。
返回列表