/
PedometerProcessingNode.ts
271 lines (250 loc) · 10.8 KB
/
PedometerProcessingNode.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
import {
Acceleration,
LinearVelocity,
SerializableArrayMember,
SerializableMember,
SerializableObject,
Euler,
LinearVelocityUnit,
Vector3,
LengthUnit,
ProcessingNode,
ProcessingNodeOptions,
DataFrame,
AbsoluteOrientationSensor,
LinearAccelerationSensor,
} from '@openhps/core';
/**
* Pedometer processing node
*
* Based on:
* @see {@link https://github.com/MaximilianBuegler/node-pedometer/blob/master/src/pedometer.js}
* @see {@link https://github.com/MaximilianBuegler/node-kinetics/blob/master/src/kinetics.js}
* @author Maximilian Bügler
*/
export class PedometerProcessingNode<InOut extends DataFrame> extends ProcessingNode<InOut> {
protected options: PedometerOptions;
constructor(options?: PedometerOptions) {
super(options);
// Default options
this.options.windowSize = this.options.windowSize || 1;
this.options.minPeak = this.options.minPeak || 2;
this.options.maxPeak = this.options.maxPeak || 8;
this.options.minStepTime = this.options.minStepTime || 0.3;
this.options.peakThreshold = this.options.peakThreshold || 0.5;
this.options.maxStepTime = this.options.maxStepTime || 0.8;
this.options.meanFilterSize = this.options.meanFilterSize || 1;
this.options.minConsecutiveSteps = this.options.minConsecutiveSteps || 3;
this.options.stepSize = this.options.stepSize || 0.7;
}
public process(frame: DataFrame): Promise<DataFrame> {
return new Promise((resolve, reject) => {
// Get node data for this source object
let pedometerData: PedometerData;
this.getNodeData(frame.source)
.then((data: PedometerData) => {
if (!data) {
data = new PedometerData();
}
// Add the frame information
data.add(frame);
const windowSize = Math.floor(this.options.windowSize * data.frequency);
if (data.accelerometerData.length > 4 * windowSize) {
data.shift();
}
pedometerData = data;
return this.processPedometer(pedometerData);
})
.then((steps) => {
// Do not double count steps
const previousStep = steps.indexOf(pedometerData.lastStepIndex);
if (previousStep !== -1) {
steps = steps.slice(previousStep + 1);
}
if (steps.length > 0) {
pedometerData.lastStepIndex = steps[steps.length - 1];
}
const stepCount = steps.length;
// Distance travelled in windowSize
const distance = this.options.stepSize * stepCount;
const position = frame.source.getPosition();
position.timestamp = frame.createdTimestamp;
position.linearVelocity = new LinearVelocity(
distance / this.options.windowSize,
0,
0,
LinearVelocityUnit.METER_PER_SECOND,
);
const orientationSensor = frame.getSensor(AbsoluteOrientationSensor);
const orientation =
(orientationSensor ? orientationSensor.value : undefined) || position.orientation;
if (orientation) {
const relativePosition = Vector3.fromArray([distance / this.options.windowSize, 0, 0]);
const eulerOrientation = orientation.toEuler();
eulerOrientation.x = 0;
eulerOrientation.y = 0;
position.fromVector(
position.toVector3(LengthUnit.METER).add(relativePosition.applyEuler(eulerOrientation)),
);
}
return this.setNodeData(frame.source, pedometerData);
})
.then(() => {
resolve(frame);
})
.catch(reject);
});
}
public processPedometer(data: PedometerData): Promise<number[]> {
return new Promise((resolve) => {
// Factor in the sampling time
const windowSize = Math.floor(this.options.windowSize * data.frequency);
const taoMin = this.options.minStepTime * data.frequency;
const taoMax = this.options.maxStepTime * data.frequency;
// Extract verical component from input signals
const verticalComponent = this._extractVerticalComponents(data.accelerometerData, data.attitudeData);
if (verticalComponent.length < windowSize) {
return resolve([]);
}
let smoothedVerticalComponent = verticalComponent;
if (this.options.meanFilterSize > 1) {
smoothedVerticalComponent = this._meanFilter(verticalComponent, this.options.meanFilterSize);
}
// Offset is half window size first and last half can not be used
const window: number[] = verticalComponent.slice(0, windowSize);
// Max and sum peak of window and settings
let windowMax = Math.max(this.options.minPeak, Math.min(this.options.maxPeak, Math.max(...window)));
let windowSum = window.reduce((a, b) => a + b);
const windowAvg = windowSum / windowSize;
const offset = Math.ceil(windowSize / 2);
let steps: number[] = [];
let lastPeak = data.lastStepIndex;
for (let i = offset; i < verticalComponent.length - offset - 1; i++) {
// If the current value minus the mean value of the current window is larger than the thresholded maximum
// and the values decrease after i, but increase prior to i
// and the last peak is at least taoMin steps before
if (
verticalComponent[i] >
Math.max(this.options.minPeak, this.options.peakThreshold * windowMax + windowAvg) &&
smoothedVerticalComponent[i] >= smoothedVerticalComponent[i - 1] &&
smoothedVerticalComponent[i] > smoothedVerticalComponent[i + 1] &&
lastPeak < i - taoMin
) {
// Add the current index to the steps array and note it down as last peak
if (verticalComponent[i] < this.options.maxPeak) steps.push(i);
lastPeak = i;
}
// Push next value to the end of the window
window.push(verticalComponent[i + offset]);
// remove value from the start of the window
const removed = window.shift();
// Update sum of window by substracting the removed and adding the added value
windowSum += verticalComponent[i + offset] - removed;
// If the removed value was the maximum or the new value exceeds the old maximum, we recheck the window
if (removed >= windowMax || verticalComponent[i + offset] > windowMax) {
windowMax = Math.max(this.options.minPeak, Math.min(this.options.maxPeak, Math.max(...window)));
}
}
// Remove steps that do not fulfile the minimum consecutive steps requirement
if (this.options.minConsecutiveSteps > 1) {
let consecutivePeaks = 1;
let i = steps.length;
while (i--) {
if (i === 0 || steps[i] - steps[i - 1] < taoMax) {
consecutivePeaks++;
} else {
if (consecutivePeaks < this.options.minConsecutiveSteps) {
steps.splice(i, consecutivePeaks);
}
consecutivePeaks = 1;
}
}
if (steps.length < this.options.minConsecutiveSteps) {
steps = [];
}
}
resolve(steps);
});
}
private _extractVerticalComponents(accelerometerData: Acceleration[], attitudeData: Euler[]): number[] {
return accelerometerData.map((acceleration, i) => {
const attitude = attitudeData[i].clone();
attitude.z = 0;
return acceleration.clone().applyEuler(attitude).getComponent(2);
});
}
private _meanFilter(arr: number[], size: number): number[] {
const window: number[] = [];
return arr.map((val) => {
if (window.length >= size) window.shift();
window.push(val);
return window.reduce((a, b) => a + b) / arr.length;
});
}
}
@SerializableObject()
export class PedometerData {
@SerializableArrayMember(Acceleration)
accelerometerData: Acceleration[] = [];
@SerializableArrayMember(Euler)
attitudeData: Euler[] = [];
@SerializableMember()
frequency: number;
@SerializableMember()
lastStepIndex = -Infinity;
public add(frame: DataFrame): this {
const linearAccelerometer = frame.getSensor(LinearAccelerationSensor);
const absoluteOrientation = frame.getSensor(AbsoluteOrientationSensor);
if (!linearAccelerometer || !absoluteOrientation) {
throw new Error(`No linear accelerometer sensor or absolute orientation sensors in data frame!`);
}
this.accelerometerData.push(linearAccelerometer.value);
this.attitudeData.push(absoluteOrientation.value.toEuler('ZYX'));
this.frequency = linearAccelerometer.frequency;
return this;
}
public shift(): this {
this.lastStepIndex--;
this.accelerometerData.shift();
this.attitudeData.shift();
return this;
}
}
export interface PedometerOptions extends ProcessingNodeOptions {
/**
* Length of the window in seconds
*/
windowSize?: number;
/**
* Minimum magnitude of a steps largest positive peak
*/
minPeak?: number;
/**
* Maximum magnitude of a steps largest postive peak
*/
maxPeak?: number;
/**
* Minimum time in seconds between two steps
*/
minStepTime?: number;
/**
* Minimum ratio of the current window's maximum to be considered a step
*/
peakThreshold?: number;
/**
* Minimum number of consecutive steps to be counted
*/
minConsecutiveSteps?: number;
/**
* Maximum time between two steps to be considered consecutive
*/
maxStepTime?: number;
/**
* Amount of smoothing
*/
meanFilterSize?: number;
/**
* Step size in meters
*/
stepSize?: number;
}