Simple step counting with 3-axis acceleration sensor (walking up and down the stairs)

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. I walked up and down the stairs this time.

I used the same method to estimate the number of steps as this page. I used the same JavaScript. (Only the URL of the uploaded data has been changed.)

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 about 43 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 moving average of 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. The initial 2 seconds and the last 3 seconds of the measurement were removed. I started walking 4 seconds after I pressed the start button. I pressed the stop button 3 seconds after I stopped walking.

4. I calculated 2 seconds moving average of the acceleration data to estimate the direction of the gravity. (I regarded the moving average of the past 2 seconds acceleration data as the gravitational direction with regard to the smartphone direction.)

5. I projected the pedestrian’s acceleration vector that is estimated in 3. to the gravitational direction that is estimated in 4. (I removed the calculated data in first 4 seconds.)

I calculated the pedestrian’s acceleration vector projected to the gravitational direction with 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. I applied the 0.2 seconds moving average to the results in figure 5 again. The purpose is to remove the effects of fine motion and noise.

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. I added the process to estimate the number of steps and plotted the estimated result. The stairs were 15 steps with two landings. 2 steps were taken on each of the two landings. I walked up with 17 steps and walked down with 17 steps. On the way down, the upward and downward shifts of the second steps were small on the landings and the second steps were not counted. After I walked up all steps, I made a U-turn on the spot with small steps. The number of steps I took during U-turn was estimated as one.

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/1LPud6SRVzwOSsTKrg2OmTvQw3Khau8vz5ofZcRDmNfU/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