计步算法个人总结

时间:2021-09-27 15:40:41

在Android4.4 Kitkat 之后,谷歌新增了 Step Counter Sensor 以及 Step Detector Sensor 用于Android 计步。我在网上看了很多大牛的博客,现在计步都分为两个大方向,一是直接就使用上面自带的这个,二是自己造*,接下来我就讲讲,如何自己造*,也算是对自己近期学习成果的总结。

分析数据源

使用加速度传感器获取到手机加速度的变化,然后通过算法来检测步数。仔细想想平时走路的情景,人体重心的加速度由前向和纵向的两个分量合成,
计步算法个人总结

合成之后的加速度波形就很规律了,我们只需要检测波峰/波谷,再过滤掉其他的干扰,就能估算出步数了(绝对的加速度需要算上重力加速度,本文后面所说的都指相对的,人体行走时产生的加速度)。
计步算法个人总结

算法分析

检测波峰

既然目标是检测波峰,那就首先要知道,加速度传感器采样的频率比较高,一般大于30Hz,30Hz是什么概念,正常人走一步的时间内,可以采样大概二十次了,意思是,在一个上升波段中,检查到的加速度会连续增大好多次,同样在下降波段中加速度也会连续减小好多次。于是,从波形图看,检测波峰的方法可以概括为四个条件:
- 上一次波形上升状态
- 目前为下降状态
- 到波峰为止,连续上升多次
- 波峰/波谷值在一定范围内

翻译成代码便是下面的 boolean DetectorPeak(float newValue, float oldValue) 方法。

检测步数

分析数据源的时候就已提到,检测到波峰之后,需再滤掉干扰波,比如细微的震动等等,才能判定为一步。于是这里便生成了三个条件:
- DetectorPeak() return true
- 符合一定的时间差
- 大于一个动态生成的阈值

翻译成代码便是下面的 void detectorNewStep(float values) 方法。

源代码

我就直接上了,也可以在我的 GitHub 获取最新的版本。

/** * 自定义的加速度传感器监听 * 在此处理加速度事件 * 使用方法: * - 外部通过 setOnSensorChangeListener 方法传入回调对象 * - 外部通过复写接口 OnSensorChangeListener 的 onChange() 方法获取步数 * Created by XinZh on 2017/3/1. */

@RequiresApi(api = Build.VERSION_CODES.CUPCAKE)
public class MyStepDcretor implements SensorEventListener {
    private final String TAG = "StepDcretor";


    //加速度,用x、y、z轴的三个加速度分量计算出
    public static float acceleration = 0;
    //当前传感器的值
    //float gravityNew = 0;
    //上次的加速度
    float lastAcceleration = 0;

    //是否上升的标志位
    boolean isDirectionUp = false;
    //持续上升次数
    int continueUpCount = 0;
    //上一点的持续上升的次数,为了记录波峰的上升次数
    int continueUpFormerCount = 0;
    //上一点的状态,上升还是下降
    boolean lastStatus = false;

    //波峰值
    float peakOfWave = 0;
    //波谷值
    float valleyOfWave = 0;
    //此次波峰的时间
    long timeOfThisPeak = 0;
    //上次波峰的时间
    long timeOfLastPeak = 0;
    //系统当前的时间
    long timeOfNow = 0;

    //初始阈(yu)值
    final float initialThreshold = (float) 1.7;
    // 动态阈值需要动态的数据,这个值用于这些动态数据的阈值
    float threadThreshold = (float) 2.0;
    //用于存放计算阈值的波峰波谷差值
    final int valueNum = 5;
    float[] tempValue = new float[valueNum];
    int tempCount = 0;

    //初始范围
    float minValue = 11f;
    float maxValue = 19.6f;

    /** * 计步状态 * 0-未计步 1-预备计步,计时中 2-正常计步中,存储 */
    private int pedometerState = 0;
    //步数
    public static int CURRENT_SETP = 0;
    public static int TEMP_STEP = 0;
    private int lastStep = -1;

    private Timer timer;
    // 倒计时3.5秒,3.5秒内不会显示计步,用于屏蔽细微波动
    private long duration = 3500;
    private TimeCount timeCount;

    /** * 自定义的接口,实时向外传递步数 */
    OnSensorChangeListener onSensorChangeListener;

    public interface OnSensorChangeListener {
        //当步数改变时,通知外部更新UI
        void onStepsListenerChange(int steps);
        //当计步状态改变时,通知外部是否存储
        void onPedometerStateChange(int pedometerState);
    }

    //获取接口对象
    public OnSensorChangeListener getOnSensorChangeListener() {
        return onSensorChangeListener;
    }

    //设置监听,传入回调对象
    public void setOnSensorChangeListener(
            OnSensorChangeListener onSensorChangeListener) {
        this.onSensorChangeListener = onSensorChangeListener;
    }


    //构造函数
    public MyStepDcretor(int newSteps) {
        super();
        CURRENT_SETP = newSteps;
    }

    /** * @param event * 加速度传感器 */
    @Override
    public void onSensorChanged(SensorEvent event) {
        Sensor sensor = event.sensor;
        synchronized (this) {
            if (sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
                getAcceleration(event);
            }
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {

    }

    /** * 计算加速度 * @param event */
    synchronized private void getAcceleration(SensorEvent event) {
        //忽略加速度方向,取绝对值
        acceleration = (float) Math.sqrt(Math.pow(event.values[0], 2)
                + Math.pow(event.values[1], 2) + Math.pow(event.values[2], 2));
        detectorNewStep(acceleration);
    }

    /** * 检测步子,并开始计步 * 如果检测到了波峰,并且符合时间差以及阈值的条件,则判定为1步 * 符合时间差条件,波峰波谷差值大于initialValue,则将该差值纳入阈值的计算中 */
    public void detectorNewStep(float values) {
        if (lastAcceleration == 0) {
            lastAcceleration = values;
        } else {
            if (DetectorPeak(values, lastAcceleration)) {
                timeOfLastPeak = timeOfThisPeak;
                timeOfNow = System.currentTimeMillis();

                if (timeOfNow - timeOfLastPeak >= 200
                        && (peakOfWave - valleyOfWave >= threadThreshold) && (timeOfNow - timeOfLastPeak) <= 2000) {
                    timeOfThisPeak = timeOfNow;
                    //视为一步,更新界面的处理,不涉及到算法
                    preStep();
                }
                if (timeOfNow - timeOfLastPeak >= 200
                        && (peakOfWave - valleyOfWave >= initialThreshold)) {
                    timeOfThisPeak = timeOfNow;
                    threadThreshold = Peak_Valley_Thread(peakOfWave - valleyOfWave);
                }
            }
        }
        lastAcceleration = values;
    }

    /** * 检测波峰 * 以下四个条件判断为波峰: * 1.目前点为下降的趋势:isDirectionUp为false * 2.之前的点为上升的趋势:lastStatus为true * 3.到波峰为止,持续上升大于等于2次 * - 这是因为:加速度传感器采集的频率比较高,一般大于30Hz,2次还算少的了 * 4.波峰值大于1.2g,小于2g * 记录波谷值 * 1.观察波形图,可以发现在出现步子的地方,波谷的下一个就是波峰,有比较明显的特征以及差值 * 2.所以要记录每次的波谷值,为了和下次的波峰做对比 * @param newValue 本次的加速度 * @param oldValue 上次的加速度 */
    public boolean DetectorPeak(float newValue, float oldValue) {
        lastStatus = isDirectionUp;
        if (newValue >= oldValue) {//可以换成差值大于某一值也可
            isDirectionUp = true;
            continueUpCount++;
        } else {
            continueUpFormerCount = continueUpCount;
            continueUpCount = 0;
            isDirectionUp = false;
        }

       // Log.v(TAG, "oldValue:" + oldValue);
        if (!isDirectionUp && lastStatus
                && (continueUpFormerCount >= 2 && (oldValue >= minValue && oldValue < maxValue))) {
            peakOfWave = oldValue;
            return true;
        } else if (!lastStatus && isDirectionUp) {
            valleyOfWave = oldValue;
            return false;
        } else {
            return false;
        }
    }

    /** * 阈值的计算 * 1.通过波峰波谷的差值计算阈值 * 2.记录4个值,存入tempValue[]数组中 * 3.在将数组传入函数averageValue中计算阈值 */
    public float Peak_Valley_Thread(float value) {
        float tempThread = threadThreshold;
        if (tempCount < valueNum) { //存储过程
            tempValue[tempCount] = value;
            tempCount++;
        } else { //计算过程
            tempThread = averageValue(tempValue, valueNum);
            for (int i = 1; i < valueNum; i++) { 
                tempValue[i - 1] = tempValue[i];
            }
            tempValue[valueNum - 1] = value;
        }
        return tempThread;

    }

    /** * 梯度化阈值 * 1.计算数组的均值 * 2.通过均值将阈值梯度化在一个范围里 */
    public float averageValue(float value[], int n) {
        float ave = 0;
        for (int i = 0; i < n; i++) {
            ave += value[i];
        }
        ave = ave / valueNum;
        if (ave >= 8) {
            Log.v(TAG, "超过8");
            ave = (float) 4.3;
        } else if (ave >= 7 && ave < 8) {
            Log.v(TAG, "7-8");
            ave = (float) 3.3;
        } else if (ave >= 4 && ave < 7) {
            Log.v(TAG, "4-7");
            ave = (float) 2.3;
        } else if (ave >= 3 && ave < 4) {
            Log.v(TAG, "3-4");
            ave = (float) 2.0;
        } else {
            Log.v(TAG, "else");
            ave = (float) 1.7;
        }
        return ave;
    }

    private void preStep() {
        if (pedometerState == 0) {
            // 开启计时器,duration 秒倒计时,每次间隔700毫秒
            timeCount = new TimeCount(duration, 700);
            timeCount.start();
            pedometerState = 1;
            //通知外部计步预备中
            onSensorChangeListener.onPedometerStateChange(pedometerState);
            Log.v(TAG, "开启计时器");
        } else if (pedometerState == 1) {
            TEMP_STEP++;
            Log.v(TAG, "预备计步中 TEMP_STEP:" + TEMP_STEP);
        } else if (pedometerState == 2) {
            CURRENT_SETP++;
            if (onSensorChangeListener != null) {
                //调用接口向外传递信息
                onSensorChangeListener.onStepsListenerChange(CURRENT_SETP);
            }
        }
    }

    /** * 自定义计时器类,复写onFinish()和onTick()方法 */
    class TimeCount extends CountDownTimer {
        public TimeCount(long millisInFuture, long countDownInterval) {
            super(millisInFuture, countDownInterval);
        }

        @Override
        public void onFinish() {
            // 如果计时器正常结束,则开始计步
            timeCount.cancel();
            CURRENT_SETP += TEMP_STEP;
            lastStep = -1;
// CountTimeState = 2;
            Log.v(TAG, "计时正常结束");

            timer = new Timer(true);
            TimerTask task = new TimerTask() {
                public void run() {
                    if (lastStep == CURRENT_SETP) {
                        timer.cancel();
                        pedometerState = 0;
                        onSensorChangeListener.onPedometerStateChange(pedometerState);
                        lastStep = -1;
                        //<标签1:这里我自己的需求,会使得每一次计步过程后步数清零,可删>
                        CURRENT_SETP = 0;
                        //</标签1>
                        TEMP_STEP = 0;
                        Log.v(TAG, "停止计步:" + CURRENT_SETP);
                    } else {
                        lastStep = CURRENT_SETP;
                    }
                }
            };
            //0-调用后,多久开始一次次执行run()方法,2000-以后每次间隔多久调用run()
            timer.schedule(task, 0, 2000);
            pedometerState = 2;
            onSensorChangeListener.onPedometerStateChange(pedometerState);
        }

        //计时/预备计步过程,每700毫秒执行一次
        @Override
        public void onTick(long millisUntilFinished) {
            if (lastStep == TEMP_STEP) {
                Log.v(TAG, "onTick 计时停止");
                timeCount.cancel();
                pedometerState = 0;
                onSensorChangeListener.onPedometerStateChange(pedometerState);
                lastStep = -1;
                TEMP_STEP = 0;
            } else {
                lastStep = TEMP_STEP;
            }
        }

    }

}

想要简单使用这个类的计步功能,只需要在 Service 中实现 MyStepDcretor.OnSensorChangeListener 接口,建立它的对象并且注册加速度传感器就行了,注意把我代码中<标签1>中的那行按需处理。

myStepDcretor = new MyStepDcretor(stepsItemModel.getSteps());

                sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
                Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
                sensorManager.registerListener(myStepDcretor,sensor,SensorManager.SENSOR_DELAY_UI);

                myStepDcretor.setOnSensorChangeListener(PedometerService.this);

进阶使用

划分为过程

再来仔细分析代码,在 void preStep() 方法中,我用了一个 pedometerState 整型常量,这是因为,我把计步划分为一次次的过程单位,或者叫事务,这个过程有三个状态:
- 预备计步:临时步数++
- 正常计步:临时步数 + (新步数++),更新 UI
- 停止计步

每一次状态改变都会通过接口通知外部

每天所走的步数,都是由一个个的过程累加起来的,比如,我早上8点完成了一个计步过程,共100步,下午又有一次200步的,于是今天我便走了300步,为什么这么做,主要是为了后期统计数据的需求所致,这便要求我的计步能够返回一个当前计步状态,这便是这个整型常量 pedometerState.

为什么有预备计步?

这是因为在这个过程的概念下,需要进一步的过滤掉干扰,比如上面的例子中,我在中午敲代码的时候,去泡了杯咖啡,走了20步……你觉得这也要算做一次过程记录下来吗?当然不要。

整理下逻辑: 在算法检测到新的一步后,如果当前处于停止计步状态,会开启一个计时器,并进入预备计步状态,计时器每间隔一定时间检测是否有新的步数产生,如果没有,进入停止计步状态,如果在计时完成后还有新步数产生,那么进入正常进步状态,再开启新的计时器,每间隔一定时间判定是否有新步数,如果没有,进入停止计步状态。

{% img http://ol9mabyr6.bkt.clouddn.com/jibuliucheng.jpg %}

步数存储与恢复

在外部(Service 中),针对不同的计步状态可以有不同的存储方式,比较*,我的做法是:在正常计步状态中,每20步存储一次,在变为停止计步状态瞬间,存储一次。此外,在 Service 重启时,从存储中恢复数据,通过我这个类的构造方法传入。