1. 从“数步子”到“算步子”:三轴加速度计步算法入门
大家好,我是老张,在智能硬件和算法领域摸爬滚打了十几年。今天想和大家聊聊一个既基础又充满挑战的话题:如何用三轴加速度传感器精准地“数步子”。你可能觉得,现在手环、手表计步不是挺准的嘛?但如果你自己动手做过,或者想优化现有产品的计步精度,就会发现这里面的水其实挺深的。尤其是在不同佩戴位置(口袋、手腕、背包)、不同运动状态(快走、慢跑、上下楼)下,如何让算法依然保持高精度和强鲁棒性,是每个工程师都要面对的难题。
简单来说,计步算法的核心任务,就是从传感器连续不断的、充满噪声的加速度数据流中,准确地识别出“一步”发生的那个瞬间。这听起来像是个模式识别问题,但难点在于,这个“模式”不是固定的。你走路、跑步、上下楼梯时,身体产生的加速度波形差异巨大;传感器放在口袋和绑在手臂上,数据也完全不同。所以,一个“死板”的、用固定阈值判断的算法,在实际场景中往往会“翻车”——要么漏计,要么多计,误差大到让人无法接受。
因此,今天的重点不是复述一个简单的计步原理,而是深入探讨如何通过动态阈值调整和高效滤波技术这两个核心手段,来优化算法,让它变得更聪明、更稳定。我会结合大量工程实践中的代码和踩坑经验,带你一步步构建一个鲁棒性更强的计步方案。无论你是嵌入式开发者、移动应用工程师,还是对算法优化感兴趣的爱好者,这篇文章都能给你带来实实在在的收获。
2. 基石:为什么简单的均值滤波远远不够?
在讨论高级优化之前,我们必须先打好地基。原始文章里提到了使用均值滤波进行初步平滑,这确实是第一步。但根据我多年的经验,仅仅做均值滤波是远远不够的,甚至可能引入新的问题。
2.1 均值滤波的局限性与改进
原始代码中,对连续4组(FILTER_CNT=4)的X、Y、Z轴数据分别求平均。这个方法简单直接,能快速平滑掉一些高频毛刺。但它的缺点也很明显:滞后性。假设我们的采样频率是50Hz(即每20ms一个点),4点均值滤波会带来80ms的延迟。在快速跑步时,这个延迟可能会影响我们对步伐峰值的及时判断。
更关键的是,均值滤波对脉冲噪声(比如突然的撞击或抖动)的抑制效果有限。一个异常大的噪声点会显著拉高或拉低这4个点的平均值,导致输出失真。在实际项目中,我遇到过因为手部突然甩动,导致滤波后数据出现一个“假峰值”,从而误判为一步的情况。
所以,我通常会采用滑动窗口均值滤波结合中值滤波的策略。中值滤波能有效剔除孤立的脉冲噪声,而滑动窗口均值则提供平滑。下面是一个改进版的滤波函数示例:
#define WINDOW_SIZE 5
typedef struct {
short buffer[WINDOW_SIZE];
int index;
short sorted[WINDOW_SIZE]; // 用于中值滤波的排序数组
} axis_filter_t;
static short axis_median_filter(axis_filter_t *filter, short new_sample) {
// 1. 更新环形缓冲区
filter->buffer[filter->index] = new_sample;
filter->index = (filter->index + 1) % WINDOW_SIZE;
// 2. 复制数据到排序数组
for (int i = 0; i < WINDOW_SIZE; i++) {
filter->sorted[i] = filter->buffer[i];
}
// 3. 简单冒泡排序找中值(对于小窗口,效率可接受)
for (int i = 0; i < WINDOW_SIZE - 1; i++) {
for (int j = 0; j < WINDOW_SIZE - i - 1; j++) {
if (filter->sorted[j] > filter->sorted[j + 1]) {
short temp = filter->sorted[j];
filter->sorted[j] = filter->sorted[j + 1];
filter->sorted[j + 1] = temp;
}
}
}
// 返回中值
return filter->sorted[WINDOW_SIZE / 2];
}
static short axis_moving_average(axis_filter_t *filter) {
long sum = 0;
for (int i = 0; i < WINDOW_SIZE; i++) {
sum += filter->buffer[i];
}
return (short)(sum / WINDOW_SIZE);
}
// 综合滤波:先中值,再对中值结果做滑动平均(可选)
static short combined_filter(axis_filter_t *filter, short new_sample) {
short median_val = axis_median_filter(filter, new_sample);
// 这里可以再对 median_val 进行一次小窗口的滑动平均,进一步平滑
// 为了清晰,我们直接返回中值滤波结果
return median_val;
}
这个改进方案先用中值滤波(窗口为5)干掉明显的异常点,然后再进行后续处理。实测下来,对于抑制手抖、设备碰撞等突发干扰,效果比单纯均值滤波好很多。
2.2 向量模长:从三轴到一维的关键转换
原始算法中,步伐判断依赖于找出“最活跃轴”。这个思路没问题,但在某些特殊姿态下(比如手机斜放在裤袋里),可能没有一个轴的变化特别明显,导致“最活跃轴”判断失效。
一个更鲁棒的做法是计算三轴加速度的向量模长(Magnitude)。无论设备如何旋转,人体运动产生的合加速度大小变化规律是相对稳定的。计算模长可以消除方向的影响,将三维问题简化为一维问题,这是很多成熟计步算法的第一步。
static short calculate_magnitude(axis_info_t *sample) {
// 注意:这里使用整数运算,避免浮点开销。实际值可能很大,用 long 类型。
long x = (long)sample->x;
long y = (long)sample->y;
long z = (long)sample->z;
// 计算平方和,为了减少计算量,有时会使用近似算法或查表法
long sq_sum = x*x + y*y + z*z;
// 开方运算在资源受限的设备上代价高,可以考虑使用快速整数开方算法
// 这里为了清晰,使用标准库 sqrt,实际项目可能需要优化
return (short)sqrt(sq_sum);
}
计算模长后,我们后续的动态阈值、峰值检测等操作,都可以基于这个一维的模长信号进行,逻辑会清晰和统一很多。当然,这增加了计算量,需要根据你的MCU性能进行权衡。在我的很多项目中,尤其是对功耗敏感的可穿戴设备,使用模长信号是利大于弊的。
3. 核心进化:从“静态”到“动态”的阈值艺术
静态阈值是计步算法新手最容易犯的错误。你可能会想:“我


290

被折叠的 条评论
为什么被折叠?



