戴西是一名职业工程师,一名激情企业家,本质上是一名开发人员。她目前是Nush Rewards的首席技术官和联合创始人。当她不忙着和团队一起开发产品时,你可以在dessydaskalov.com和@dess_e上看到她教别人编程、参加或主持多伦多科技活动的身影。
许多软件工程师回想起他们所受的培训,都会记得生活在一个非常完美的世界里的乐趣。我们被教导要在理想化的领域中解决定义明确的问题。
然后我们被扔进了现实世界,面对着它所有的复杂性和挑战。它是凌乱的,这让它变得更加令人兴奋。当你能解决现实生活中的所有问题时,你就能构建出真正帮助人们的软件。
在这一章中,我们将考察一个表面上看起来很简单的问题,但当现实世界和真实的人被混为一谈时,这个问题很快就会变得扑朔迷离。
我们将共同打造一个基本的计步器。我们将从讨论计步器背后的理论开始,并在代码之外创建一个步数计算解决方案。然后,我们将用代码实现我们的解决方案。最后,我们将在代码中添加一个web层,这样我们就有了一个友好的界面供用户使用。
移动设备的兴起带来了一种趋势,即收集越来越多的日常生活数据。很多人收集的一种数据是他们在一段时间内走了多少步。这些数据可以用于健康跟踪,体育赛事的训练,或者,对于我们这些痴迷于收集和分析数据的人来说,只是为了好玩。步数可以使用计步器来计算,计步器通常使用硬件加速度计的数据作为输入。
加速度计是一种硬件,可以测量x、y和z方向的加速度。许多人无论走到哪里都随身携带加速度计,因为目前市场上几乎所有的智能手机都内置了加速度计。\(x\)、\(y\)和\(z\)方向与电话相关。
加速度计返回三维空间中的信号。信号是一组随时间记录的数据点。信号的每个分量都是一个时间序列,表示在x、y或z方向中的一个方向上的加速度。时间序列中的每个点都是特定时间点在该方向上的加速度。加速度的测量单位是重力,或称g。1 g等于9.8(m/s^2),即地球上由于重力而产生的平均加速度。
加速计的采样率通常是可以校准的,它决定了每秒测量的次数。例如,采样率为100的加速度计每秒返回每个(x\)、\(y\)和\(Z)时间序列的100个数据点。
当一个人走路时,他们每走一步都会略微反弹。当一个人从你身边走开时,只要注意他的头顶就行了。它们的头部、躯干和臀部以平滑的弹跳运动同步。虽然人们不会弹跳很远,只有一两厘米,但它是一个人行走加速信号中最清晰、最恒定、最容易辨认的部分之一。
一个人每走一步,都会在垂直方向上上下跳跃。如果你在地球上行走(或在太空中漂浮的另一个大质量球),反弹的方向很方便,与重力方向相同。
我们要用加速度计来计算上下跳动的步数。由于手机可以向任何方向旋转,我们将利用重力来确定向下的方向。计步器可以通过计算重力方向上的反弹次数来计算步数。
让我们来看看一个人,他或她的衬衫口袋里放着一部装有加速度计的智能手机(图16.2)。
在我们的完美世界中,阶跃反弹产生的加速度将在y方向上形成一个完美的正弦波。正弦波中的每个峰值正好是一个台阶。计算步数变成了计算这些完美峰值的问题。
啊,完美世界的快乐,我们只有在这样的文本中才能体验到。不要着急,事情会变得更混乱,更令人兴奋。让我们为我们的世界增添一点现实。
重力引起向重力方向的加速度,我们称之为重力加速度。这种加速度是唯一的,因为它总是存在的,并且在本章中是恒定的,为9.8\(m/s^2\)。
假设一部智能手机正面朝上躺在一张桌子上。在这个方向上,我们的坐标系统是这样的,负(Z)方向就是重力作用的方向。重力会将我们的手机拉向负(Z)方向,所以我们的加速度计,即使完全静止,也会记录到9.8(m/s^2)的负(Z)方向的加速度。我们手机在这个方向上的加速度计数据如图16.3所示。
请注意,x(T)和y(T)在0处保持不变,而z(T)在-1g处保持不变。我们的加速度计记录所有加速度,包括重力加速度。
每个时间序列都测量该方向的总加速度。总加速度是用户加速度和重力加速度之和。
用户加速度是设备因用户移动而产生的加速度,当手机完全静止时,用户加速度为0。然而,当用户随着设备移动时,用户的加速度很少是恒定的,因为人很难以恒定的加速度移动。
为了计算步数,我们对用户在重力方向上产生的弹跳很感兴趣。这意味着我们有兴趣将描述用户重力方向加速度的一维时间序列从我们的三维加速度信号中分离出来(图16.4)。
在我们的简单例子中,重力加速度在x(T)和z(T)中为0,在y(T)中为9.8(m/s^2)常数。因此,在我们的总加速度图中,x(T)和z(T)在0附近波动,而y(T)在-1g附近波动。在我们的用户加速图中,我们注意到-因为我们去掉了重力加速度-所有三个时间序列都在0附近波动。请注意\(y_{u}(T)\)中的明显峰值。这些都是由于台阶反弹造成的!在最后一张图中,重力加速度y_{g}(T)在-1g处不变,x_{g}(T)和z_{g}(T)在0处不变。
因此,在我们的例子中,我们感兴趣的重力时间序列方向上的一维用户加速度是\(y_{u}(T)\)。虽然(y_{u}(T)\)不像我们理想的正弦波那样平滑,但我们可以识别峰值,并使用这些峰值来计算步数。到现在为止还好。现在,让我们为我们的世界增添更多的现实。
如果一个人把手机装在包里扛在肩上,而手机放在一个更摇摇晃晃的位置,那该怎么办?更糟糕的是,如果手机在行走过程中在包中旋转,如图16.5所示?
哎呀。现在我们的所有三个分量都有一个非零的重力加速度,所以用户在重力方向上的加速度现在被分割到所有三个时间序列中。要确定用户在重力方向上的加速度,我们首先必须确定重力在哪个方向上起作用。要做到这一点,我们必须将三个时间序列中的每个时间序列中的总加速度分为用户加速时间序列和重力加速时间序列(图16.6)。
然后,我们可以在每个分量中分离出重力方向上的用户加速度部分,从而只得到重力方向上的用户加速度时间序列。
我们可以使用一种名为过滤器的工具将总加速时间序列划分为用户加速时间序列和重力加速时间序列。
滤波器是在信号处理中使用的一种工具,用于从信号中去除不需要的分量。
低通滤波器允许低频信号通过,同时衰减高于设定阈值的信号。相反,高通滤波器允许高频信号通过,同时将信号衰减到设定阈值以下。以音乐为类比,低通滤波器可以消除高音,高通滤波器可以消除低音。
在我们的情况下,以赫兹为单位测量的频率表示加速度变化的速度有多快。恒定加速度的频率为0 Hz,而非恒定加速度的频率为非零。这意味着我们的恒定重力加速度是0 Hz信号,而用户加速度不是。
对于每个分量,我们可以通过一个低通滤波器传递总加速度,我们将只得到重力加速度时间序列。然后我们可以从总加速度中减去重力加速度,我们就得到了用户加速时间序列(图16.7)。
过滤器有很多种。我们将使用的一种叫做无限脉冲响应(IIR)滤波器。我们选择了IIR过滤器,因为它开销低,易于实现。我们选择的IIR过滤器使用以下公式实现:
数字滤波器的设计超出了本章的范围,但有必要进行非常简短的预告性讨论。这是一个经过充分研究、引人入胜的话题,有着无数的实际应用。可以设计数字滤波器来消除所需的任何频率或频率范围。公式中的\(\α\)和\(\β\)值是根据截止频率和我们希望保留的频率范围设置的系数。
我们想要消除除恒定重力加速度之外的所有频率,所以我们选择了衰减频率高于0.2赫兹的系数。请注意,我们设置的阈值略高于0赫兹。虽然重力确实产生了真正的0赫兹加速度,但我们真实的、不完美的世界有真实的、不完美的加速度计,所以我们在测量中允许有轻微的误差。
我们将把重力加速度的前两个值初始化为0,这样公式就有了初始值。
\(x_{g}(T)\)和\(z_{g}(T)\)在0附近徘徊,而\(y_{g}(T)\)很快下降到\(-1g)。\(y_{g}(T)\)中的初始0值来自公式的初始化。
\[x_{u}(T)=x(T)-x_{g}(T)\]\[y_{u}(T)=y(T)-y_{g}(T)\]\[z_{u}(T)=z(T)-z_{g}(T)\]。
结果是如图16.9所示的时间序列。我们已经成功地将总加速分为用户加速和重力加速!
\(x_{u}(T)\)、\(y_{u}(T)\)和\(z_{u}(T)\)包括用户的所有移动,而不仅仅是重力方向的移动。我们的目标是最终得到一维时间序列,表示用户在重力方向上的加速度。这将包括每个方向的部分用户加速。
让我们开始吧。首先是一些线性代数的基础知识。别急着摘下数学家的帽子!
在使用坐标时,在介绍点积之前,您不会走得很远,点积是比较\(x\)、\(y\)和\(Z)坐标的大小和方向时使用的基本工具之一。
点积将我们从3维空间带到1维空间(图16.10)。当我们把用户加速度和重力加速度这两个时间序列的点积相乘时,这两个时间序列都在3维空间中,我们会得到1维空间中的单个时间序列,表示用户加速度在重力方向上的那部分。我们会随意地称这个新的时间序列为\(a(T)\),因为,好吧,每个重要的时间序列都应该有一个名字。
我们可以使用公式\(a(T)=x_{u}(T)x_{g}(T)+y_{u}(T)y_{g}(T)+z_{u}(T)z_{g}(T)\)实现前面示例的点积,从而得到一维空间中的\(a(T)\)(图16.11)。
现在,我们可以直观地找出步骤在\(a(T)\)中的位置。这个Dot产品非常强大,但却非常简单。
我们看到,当我们面对现实世界和现实人的挑战时,我们看似简单的问题很快就变得复杂起来。然而,我们越来越接近计算步数,我们可以看到(a(T))开始与我们理想的正弦波相似。但是,只有在某种程度上,才开始出现这种情况。我们仍然需要使我们混乱的(a(T)\)时间序列更加平滑。在当前状态下,\(a(T)\)存在四个主要问题(图16.12)。让我们逐一检查一下。
(a(T)\)非常紧张,因为手机每走一步都会抖动,给我们的时间序列增加了一个高频成分。(a(T))(a(T))非常紧张,因为手机每走一步都会抖动,这会给我们的时间序列增加一个高频成分。这种跳跃称为噪音。通过研究大量的数据集,我们已经确定步幅加速度最大为5赫兹。我们可以使用低通IIR滤波器来去除噪声,选择\(\α\)和\(\β\)来衰减所有高于5 Hz的信号。
当采样率为100时,(a(T))显示的慢峰跨度为1.5秒,太慢而不是一个台阶。在研究了足够多的数据样本后,我们已经确定,我们能采取的最慢的步骤是在1赫兹的频率上。较慢的加速是由于低频分量,我们可以再次使用高通IIR滤波器将其去除,设置\(\α\)和\(\β\)以消除低于1 Hz的所有信号。
当一个人在使用应用程序或打电话时,加速度计会记录重力方向上的微小移动,在我们的时间序列中显示为短峰值。我们可以通过设置一个最小阈值来消除这些短峰值,并在每次(a(T))在正方向上超过该阈值时计数一步。
我们的计步器应该可以容纳许多不同走路的人,所以我们根据大量的人和走路的样本设置了最小和最大步频。这意味着我们有时可能会过滤得太多或太少。虽然我们通常会有相当平坦的山峰,但偶尔也会出现比较崎岖的山峰。图16.12放大了一个这样的山峰。
当我们的门槛处出现颠簸时,我们可能会错误地将太多的步数计算为一个高峰。我们将使用一种叫做滞后的方法来解决这个问题。滞后指的是产出对过去投入的依赖性。我们可以计算正方向上的阈值交叉,以及负方向上的0个交叉。然后,我们只计算在0跨越阈值之后发生的步数,确保每一步只计数一次。
在考虑到这四种情况时,我们已经设法使混乱的\(a(T)\)相当接近我们理想的正弦波(图16.13),使我们能够计算步数。
乍一看,这个问题看起来很简单。然而,现实世界和现实中的人们向我们扔了几个曲线球。让我们回顾一下我们是如何解决这个问题的:
我们使用低通滤波器将总加速度分解为用户加速度和重力加速度,分别为((x_{u}(T),y_{u}(T),z_{u}(T))和\((x_{g}(T),y_{g}(T),z_{g}(T)。
我们取((x_{u}(T),y_{u}(T),z_{u}(T))和\(x_{g}(T),y_{g}(T),z_{g}(T))的点积得到用户在重力方向上的加速度,\(a(T)\)。
我们再次使用低通滤波器去除了(a(T))的高频分量,去除了噪声。
我们用高通滤波器消除了(a(T))的低频成分,去掉了慢峰。
作为培训或学术环境中的软件开发人员,我们可能已经得到了一个完美的信号,并被要求编写代码来计算该信号中的步骤。虽然这可能是一个有趣的编码挑战,但它不是我们可以在现实生活中应用的东西。我们看到,在现实中,随着重力和人的加入,问题变得更加复杂。我们使用数学工具来解决复杂性,并能够解决现实世界中的问题。是时候把我们的解决方案转换成代码了。
本章的目标是用Ruby创建一个Web应用程序,它接受加速度计数据,解析、处理和分析数据,并返回所走的步数、行驶的距离和经过的时间。
我们的解决方案要求我们对时间序列进行多次过滤。创建一个负责过滤的类是有意义的,而不是在我们的程序中散布过滤代码,如果我们需要增强或修改它,我们只需要更改这个类。这种策略被称为关注点分离,这是一种常用的设计原则,它提倡将程序拆分成不同的部分,其中每个部分都有一个主要的关注点。这是一种编写干净、可维护且易于扩展的代码的好方法。在这一章中,我们将多次回顾这个想法。
类过滤系数_LOW_0_HZ={阿尔法:[1,-1.979133761292768,0.979521463540373],β:[0.000086384997973502,0.000172769995947004,0.000086384997973502]}COMERS_LOW_5_HZ={阿尔法:[1,-1.80898117793047,0.827224480562408],β:[0.095465967120306,-0.172688631608676,0.095465967120306]}COMERS_HIGH_1_HZ={阿尔法:[1,-1.905384612118461,0.910092542787947],测试版:[0.953986986993339,-1.907503180919730,0.953986986993339]}def self.low_0_hz(Data)filter(data,coates_low_0_hz)end def self.low_5_hz(Data_5_Hz)end def self.high_1_Hz(Data_1_Hz)end Private def self.filter(data,coates_low_0_hz)end Private def self.filter(data,coates)filter_data=[0,0](2..data.length-1)。每个Do|i|Filtered_Data<;<;coducts[:Alpha][0]*(data[i]*coducts[:beta][0]+data[i-1]*coducts[:beta][1]+data[i-2]*coducts[:beta][2]-filtered_data[i-1]*coducts[:Alpha][1]-filtered_data[i-2]*coates[:Alpha][2])end filtered_data end。
无论何时我们的程序需要过滤时间序列,我们都可以调用Filter中的一个类方法来过滤我们需要过滤的数据:
每个类方法调用Filter,后者实现IIR过滤器并返回结果。如果我们希望将来添加更多的过滤器,我们只需要更改这一个类。请注意,所有幻数都是在顶部定义的。这使得我们的课更容易阅读和理解。
我们的输入数据来自Android手机和iPhone等移动设备。如今市场上的大多数手机都内置了加速计,能够记录总加速度。让我们将记录总加速度的输入数据格式称为组合格式。许多(但不是全部)设备还可以分别记录用户加速度和重力加速度。让我们称这种格式为分隔格式。能够以分离格式返回数据的设备必须具有以组合格式返回数据的能力。然而,反之亦然。有些设备只能以组合格式记录数据。合并格式的输入数据需要通过低通滤波器将其转换为分离格式。
我们希望我们的程序能够处理市场上所有带有加速计的移动设备,因此我们需要接受这两种格式的数据。让我们来看看我们将分别接受的两种格式。
组合格式的数据是在一段时间内在\(x\)、\(y\)和\(z\)方向上的总加速度。\(X\)、\(y\)和\(z\)值将用逗号分隔,单位时间的样本将用分号分隔。
分离格式返回用户在一段时间内在\(x\)、\(y\)和\(z\)方向上的加速度和重力加速度。用户加速度值将通过管道与重力加速度值分开。
处理多种输入格式是常见的编程问题。如果我们。
.