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" > google.charts.load( 'current' , {packages:[ 'corechart' ]}); google.charts.setOnLoadCallback(Spreadsheet); function Spreadsheet() { var query = new google.visualization.Query( 'https://docs.google.com/spreadsheets/d/1n4a49yyAQei82GNIxyD7pSOyWRciq76RiZOtBLiQMqc/edit?usp=sharing' ); 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> |