Simple step counting with 3-axis acceleration sensor

This is a memo on how to record data from the 3-axis accelerometer with phyphox (Physical Phone Experiments) and estimate the number of steps walked while recording the data.

1. You can refer this procedure to record 3-axis accelerometers data with phyphox.

2. Acceleration sensor data were recorded about every 0.01 seconds with phyphox. The data were recorded for about 14 seconds. The recorded data may be thinned out at specified intervals. In the following figure, the data were not thinned out.

3. A moving average of the acquired data has been calculated to suppress the effects of small movements and noise. The figure below shows the result of taking the moving average for about 0.2 seconds. The average of the sensor values for the past 0.2 seconds is used as the estimated acceleration value of the pedestrian’s center of gravity at each time. The data for about 2 seconds just after the start of the measurement and about 3 seconds just before the end of the measurement were removed this time because the user was manipulating the smartphone during these periods.

4. To estimate the direction of gravity from the 3-axis acceleration data, a moving average of about 2 seconds was taken. The direction of the average vector of the 3-axis acceleration sensor values for the past 2 seconds is used as the estimated direction of gravity.

5. Project the estimated acceleration value of the pedestrian’s center of gravity, which is calculated in 3., to the direction of gravity calculated in 4. (The values calculated in 4. include the influence of the data for 2 seconds after the start of measurement because they are the average values for the past 2 seconds. Therefore, I decided to remove the data for 4 seconds after the start of measurement.)

The value of the projection of the 3-dimensional acceleration vector in the direction of gravity is calculated by the following code.

        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. In order to suppress the effects of fine motion and noise, a moving average of about 0.2 seconds has been applied to the previous data.

7. The gravitational component of the pedestrian’s acceleration oscillates with the period of walking. Whether you are walking with your right or left foot, it becomes larger or smaller around the gravitational acceleration of 9.80665[m/s^2]. The line named “mean” is the mean value of the given data. The gravitational acceleration, 9.80665[m/s^2], is also plotted for comparison. Although “mean” is slightly different from the gravitational acceleration, the pedestrian’s acceleration oscillates with the period of walking around the gravitational acceleration.

8. The figure below shows the estimated number of steps.

The number of steps is estimated by the following code. The code calculates the difference between the gravitational component of the pedestrian’s acceleration and the gravitational acceleration at each time. Then it adds up the differences. The threshold is adjusted so that the time at which the summation of differences exceeds a certain positive threshold and the time at which it falls below a certain negative threshold are found once for each step. (Adjust the size of the threshold so that it does not count up when the gravitational component of the pedestrian’s acceleration swings by a small amount in a cycle shorter than walking, but can count up when the user takes a step.)

    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);
    }

This example is for recorded data, but the step estimation method described here can also be applied to pedometers that process measured data in real time.


The following JavaScript was used in this post.

<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() {
    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>

Leave a Reply

Your email address will not be published. Required fields are marked *

CAPTCHA