3軸加速度センサーデータを用いたシンプルな歩数推定

phyphox(Physical Phone Experiments)で歩行中の3軸加速度センサーのデータを記録し、データ記録中に歩いた歩数を推定する手順のメモになります。

1. こちらの手順で歩行中の3軸加速度センサーのデータをphyphoxを使って記録します。

2. phyphoxで記録した3軸加速度のデータは約0.01秒ごとに記録されています。下図のデータは約14秒間のデータになります。記録したデータを指定した間隔で間引きすることもありますが、今回は間引きしませんでした。

3. 取得したデータの移動平均を計算し、細かな動きやノイズの影響を抑えます。下の図は約0.2秒間の移動平均を取った結果です。過去約0.2秒間のセンサーの値の平均値を各時刻の歩行者の重心の加速度の推定値とします。スマートフォンを操作していた測定開始直後の約2秒間と測定終了直前の約3秒間のデータは、今回は取り除くことにしました。

4. 3軸加速度のデータから重力方向を推定するため、約2秒間の移動平均を取りました。過去約2秒間の3軸加速度センサーの値の平均値ベクトルの向きをその時刻のスマートフォンの向きに対する重力方向の推定値とします。

5. 先の3.で計算した細かな動きやノイズの影響を抑えた各時刻の3軸加速度のセンサー値を、4.で計算した重力方向に射影します。歩行時の重心の動き(加速度)の推定値の重力方向の成分を得ることができます。(4.で計算した値は過去約2秒間の平均値なため、測定開始後約2秒間のデータの影響が含まれています。そのため、測定開始後4秒後以降のデータを対象とすることにしました。)

3軸加速度センサーで取得した加速度の3次元ベクトルを重力方向に射影した値は、下記のコードで計算しています。

1
2
3
4
5
6
7
8
9
10
const ax = data_short_moving_average.getValue(rowIndex, 1);
const ay = data_short_moving_average.getValue(rowIndex, 2);
const az = data_short_moving_average.getValue(rowIndex, 3);
 
const gx = data_long_moving_average.getValue(rowIndex, 1);
const gy = data_long_moving_average.getValue(rowIndex, 2);
const gz = data_long_moving_average.getValue(rowIndex, 3);
const absolute_value_of_g = Math.sqrt(gx * gx + gy * gy + gz * gz);
 
const accleration_projected_to_garavitational_direction = (ax * gx + ay * gy + az * gz) / absolute_value_of_g;

6. 細かな動きやノイズの影響を抑えるため、5.で計算した重心の動きの重力方向の推定値の約0.2秒間の移動平均を計算します。

7. 重心の加速度の重力方向の成分は、右足でも左足でも一歩ごとに歩く周期で振動します。重力加速度 9.80665[m/s^2] を中心として、大きくなったり小さくなったりします。先の6.の図にプロットした約8秒間のデータの平均を計算し、プロットしました。meanという名前の直線が平均値になります。比較のため重力加速度 9.80665[m/s^2] もプロットしました。データの平均値(mean)と重力加速度の値は少しずれていますが、重心加速度の重力方向成分はほぼ重力加速度を中心として歩行の周期で振動しています。

8. 歩数を推定する処理を追加してプロットしました。

歩数は下記のようなコードで計算しています。各時刻の重心加速度の重力方向成分と重力加速度の差を計算し、それを加算していきます。加算していった値があるプラスの閾値を超える時刻とマイナスの閾値を下回る時刻が、一歩ごとに一回ずつ見つかるように閾値を調節します。(歩行の周期より短い周期で重心加速度の重力方向成分が小さく揺れた場合はカウントアップせず、歩いたときにはカウントアップできるように閾値の大きさを調節します。)

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
let steps = 0;
let sum = 0;
let is_positive = false;
let is_negative = false;
 
for (let rowIndex = 0; rowIndex < numRows; rowIndex++) {
 
    const diff = data.getValue(rowIndex, 1) - 9.80665;
 
    if ( (diff > 0 && is_positive == false) || (diff < 0 && is_negative == false) ) {
        sum += diff;
    }
 
    if (sum > positive_threshold) {
        steps++;
        is_positive = true;
        is_negative = false;
        sum = 0;
    }
 
    if (sum < negative_threshold) {
        is_positive = false;
        is_negative = true;
        sum = 0;
    }
 
    data.setCell(rowIndex, 4, steps);
}

この例では記録済みのデータを対象としていますが、こちらに記載した歩数推定方法は計測したデータをリアルタイムで処理する歩数計にも適用することができます。


補足:この投稿には下記のJavaScriptを使用しました。

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
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript">
 
google.charts.load('current', {packages:['corechart']});
google.charts.setOnLoadCallback(Spreadsheet);
 
function Spreadsheet() {
    query.send(drawChart);
}
 
const skip_length = 1;
const time_interval = 0.01;
 
function drawChart(response) {
    const data = response.getDataTable();
    const numRows = data.getNumberOfRows();
    const numThinnedRows = Math.floor(numRows / skip_length);
    const remainder = numRows % skip_length;
 
    for (let rowIndex = 0; rowIndex < numThinnedRows; rowIndex++) {
        data.removeRows(rowIndex, skip_length - 1);
    }
 
    if (remainder != 0) {
        data.removeRows(numThinnedRows, remainder);
    }
 
    // data thinning
    const options = {title: 'phyphox 3-axis acceleration sensor data (after data thinning)',
                     hAxis: {title: 'time[s]'},
                     vAxis: {title: 'acceleration [m/s^2]'}};
    const chart = new google.visualization.LineChart(document.getElementById('after_data_thinning'));
    chart.draw(data, options);
 
    // short moving average
    let title = 'phyphox 3-axis acceleration sensor data (short moving average)';
    let term = 0.2; // seconds
    let moving_average_length = Math.floor(term / time_interval / skip_length);
    let opening_term = 2.0; // seconds
    let closing_term = 3.0; // seconds
    const data_short_moving_average = data.clone();
    drawMovingAverage(data_short_moving_average, moving_average_length, 'short_moving_average', title, opening_term, closing_term);
 
    // long moving average : estimate direction of gravity
    title = 'phyphox 3-axis acceleration sensor data (long moving average)';
    term = 2.0; // seconds
    moving_average_length = Math.floor(term / time_interval / skip_length);
    opening_term = 2.0; // seconds
    closing_term = 3.0; // seconds
    const data_long_moving_average = data.clone();
    drawMovingAverage(data_long_moving_average, moving_average_length, 'long_moving_average', title, opening_term, closing_term);
 
    // acceleration in the direction of gravity
    title = 'acceleration in the direction of gravity';
    opening_term = 2.0; // seconds
    drawAccelerationProjectedToGravitaionalDirection(data_short_moving_average, data_long_moving_average,
                                                     'acceleration_gravitaional_direction', title, opening_term);
 
    // acceleration in the direction of gravity (short moving average)
    title = 'acceleration in the direction of gravity (short moving average)';
    term = 0.2; // seconds
    moving_average_length = Math.floor(term / time_interval / skip_length);
    opening_term = 0.2; // seconds
    closing_term = 0; // seconds
    drawMovingAverage(data_short_moving_average, moving_average_length, 'acceleration_gravitaional_direction_moving_average',
                      title, opening_term, closing_term);
 
    // acceleration in the direction of gravity (short moving average) : with mean line
    title = 'acceleration in the direction of gravity (short moving average with mean line)';
    drawWithMeanLine(data_short_moving_average, 'acceleration_gravitaional_direction_moving_average_with_mean_line', title);
 
    // acceleration in the direction of gravity (short moving average) : with mean line and step counting
    title = 'step counting';
    const positive_threshold = 0.05 / time_interval / skip_length;
    const negative_threshold = -0.05 / time_interval / skip_length;
    drawWithStepCounting(data_short_moving_average, 'step_counting', title, positive_threshold, negative_threshold);
}
 
 
 
function drawMovingAverage(data, moving_average_length, graph_id, title, opening_term, closing_term) {
 
    const numColumns = data.getNumberOfColumns();
    const numRows = data.getNumberOfRows();
 
    for (let columnIndex = 1; columnIndex < numColumns; columnIndex++) {
        const data_array = [];
 
        for (let rowIndex = 0; rowIndex < numRows; rowIndex++) {
            data_array.push(data.getValue(rowIndex, columnIndex));
 
            if (data_array.length == moving_average_length) {
 
                let sum = 0;
                for (let i = 0; i < moving_average_length; i++) {
                    sum += data_array[i];
                }
                const moving_average = sum / moving_average_length;
 
                // failed to update tooltip values
                // data.setValue(rowIndex, columnIndex, moving_average);
 
                // set 4th parameter (formattedValue) to update tooltip values
                data.setCell(rowIndex, columnIndex, moving_average, moving_average);
 
                data_array.shift();
            } else {
 
                // set 4th parameter (formattedValue) to update tooltip values
                data.setCell(rowIndex, columnIndex, 0, 0);
            }
        }
    }
 
    // remove data written during first 2 seconds and last 3 seconds
    const opening_rows = Math.floor(opening_term / time_interval / skip_length);
    const closing_rows = Math.floor(closing_term / time_interval / skip_length);
 
    data = remove_opening_and_closing_data(data, opening_rows, closing_rows);
 
    const options = {title,
                     hAxis: {title: 'time[s]'},
                     vAxis: {title: 'acceleration [m/s^2]'}};
    const chart = new google.visualization.LineChart(document.getElementById(graph_id));
    chart.draw(data, options);
}
 
 
 
function remove_opening_and_closing_data(data, opening_rows, closing_rows) {
 
    data.removeRows(0, opening_rows);
 
    const numRows = data.getNumberOfRows();
    const rowIndex_StartClosing = numRows - closing_rows;
 
    data.removeRows(rowIndex_StartClosing, closing_rows);
 
    return data;
}
 
 
 
function drawAccelerationProjectedToGravitaionalDirection(data_short_moving_average, data_long_moving_average, graph_id, title, opening_term) {
 
    const numRows = data_short_moving_average.getNumberOfRows();
 
    for (let rowIndex = 0; rowIndex < numRows; rowIndex++) {
        const ax = data_short_moving_average.getValue(rowIndex, 1);
        const ay = data_short_moving_average.getValue(rowIndex, 2);
        const az = data_short_moving_average.getValue(rowIndex, 3);
 
        const gx = data_long_moving_average.getValue(rowIndex, 1);
        const gy = data_long_moving_average.getValue(rowIndex, 2);
        const gz = data_long_moving_average.getValue(rowIndex, 3);
        const absolute_value_of_g = Math.sqrt(gx * gx + gy * gy + gz * gz);
 
        const accleration_projected_to_garavitational_direction = (ax * gx + ay * gy + az * gz) / absolute_value_of_g;
 
        // set 4th parameter (formattedValue) to update tooltip values
        data_short_moving_average.setCell(rowIndex, 1, accleration_projected_to_garavitational_direction,
                                          accleration_projected_to_garavitational_direction);
    }
 
    // set new column label
    data_short_moving_average.setColumnLabel(1, "Gravitational Direction Component");
 
    // remove other columns
    data_short_moving_average.removeColumns(2, 3);
 
    // remove data written during first 2 seconds
    const opening_rows = Math.floor(opening_term / time_interval / skip_length);
    data_short_moving_average = remove_opening_and_closing_data(data_short_moving_average, opening_rows, 0);
 
    const options = {title,
                     hAxis: {title: 'time[s]'},
                     vAxis: {title: 'acceleration [m/s^2]'}};
    const chart = new google.visualization.LineChart(document.getElementById(graph_id));
    chart.draw(data_short_moving_average, options);
}
 
 
 
function drawWithMeanLine(data, graph_id, title) {
 
    const numRows = data.getNumberOfRows();
 
    let sum = 0;
    for (let rowIndex = 0; rowIndex < numRows; rowIndex++) {
        sum += data.getValue(rowIndex, 1);
    }
    const mean = sum / numRows;
 
    data.addColumn('number', 'mean');
    for (let rowIndex = 0; rowIndex < numRows; rowIndex++) {
        data.setCell(rowIndex, 2, mean);
    }
 
    data.addColumn('number', 'standard gravity (9.80665)');
    for (let rowIndex = 0; rowIndex < numRows; rowIndex++) {
        data.setCell(rowIndex, 3, 9.80665);
    }
 
    const options = {title,
                     hAxis: {title: 'time[s]'},
                     vAxis: {title: 'acceleration [m/s^2]'}};
    const chart = new google.visualization.LineChart(document.getElementById(graph_id));
    chart.draw(data, options);
}
 
 
 
function drawWithStepCounting(data, graph_id, title, positive_threshold, negative_threshold) {
 
    const numRows = data.getNumberOfRows();
 
    data.addColumn('number', 'steps'); // columnIndex 4
 
    let steps = 0;
    let sum = 0;
    let is_positive = false;
    let is_negative = false;
 
    for (let rowIndex = 0; rowIndex < numRows; rowIndex++) {
 
        const diff = data.getValue(rowIndex, 1) - 9.80665;
 
        if ( (diff > 0 && is_positive == false) || (diff < 0 && is_negative == false) ) {
            sum += diff;
        }
 
        if (sum > positive_threshold) {
            steps++;
            is_positive = true;
            is_negative = false;
            sum = 0;
        }
 
        if (sum < negative_threshold) {
            is_positive = false;
            is_negative = true;
            sum = 0;
        }
 
        data.setCell(rowIndex, 4, steps);
    }
 
 
    const options = {title,
                     hAxis: {title: 'time[s]'},
                     vAxes: {
                         0: {title: 'acceleration [m/s^2]'},
                         1: {title: 'steps'}
                     },
                     series: {
                         0: {targetAxisIndex: 0},
                         1: {targetAxisIndex: 0},
                         2: {targetAxisIndex: 0},
                         3: {targetAxisIndex: 1}
                     }};
 
    const chart = new google.visualization.LineChart(document.getElementById(graph_id));
    chart.draw(data, options);
}
 
</script>

返信を残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA