1. 程式人生 > >Android計步器演算法實現(2)

Android計步器演算法實現(2)

前言

之前的那套演算法自己平時玩玩其實還是沒問題的,但是在大工程裡面運用遇到了一些不得不優化的問題:

  • 定時器太耗資源
  • 每次計算的時間間隔短(幾十毫秒),但計算量大

因此針對這兩個問題需要再一次修改演算法。新演算法相比過去優點在於:

  • 大大降低計算量
  • 不需要使用計時器,減小系統負擔

演算法實現的意義

現實原因

對於大部分的開發者來說,自己寫一套計步器演算法是非常沒有必要的,呼叫系統自帶的計步器即可。那麼我又為什麼要自己動手呢?實際是有幾個原因的:

  • 自己實現的演算法有利於做步長估計
  • 鑑於國內Android手機碎片化嚴重,不同廠商的手機計步表現差異(下面說明)很大,自己實現的演算法便於控制差異性

不同廠商的手機計步器主要差異在這幾個方面:

  • 計步器的敏感度,較敏感的演算法會把手部的輕微抖動也算成走步,敏感度低的演算法則計步丟失會比較嚴重
  • 計步器每次返回的步數,因為Android手機用的是系統回撥的方式而不是查詢的方式,因此有的手機是走一步就回調(大部分Android手機),而有的則是走四步回撥(蘋果手機和少部分Android手機),還有些非固定數值返回的手機就不提了
  • 計步器判決人在行走的時間長短,所有系統級演算法都會等人走了一定步數後才能確定人確實在行走,然後才開始回撥(此時會把之前走的步數一起返回)。大部分手機演算法需要等十步左右才開始,華為手機一般要等二三十步

優缺點

自己實現演算法的意義就是希望避開上面說的差異性問題,實現演算法上的統一。個人認為優點在於:

  • 多了步長估計
  • 每走一步就提示步態變化

缺點在於:

  • 精度略微降低(每百步差一步左右)
  • 沒有步行的預判時間

演算法原理

運動狀態判斷

判斷人的運動狀態。簡單做,利用滑動窗求方差,方差超過一定門限就判斷在運動中。

計步原理

計步演算法原理實際上相當簡單。我們來看走路時候的波形(加速度資料):
加速度訊號
這個是處理過的訊號,但是真實訊號實際上就是這麼簡單。我們只要把每個峰值從中抓出來然後加點判決門限就大功告成了。

步長計算原理

這裡用到了兩個引數H和T,見圖:
在這裡插入圖片描述

H對應峰谷值的差值,T對應兩個波谷之間的時間差。帶入公式得到步長:
length=0.370.000155T+0.1638H;length=0.37-0.000155T+0.1638\sqrt{H};


公式還不是最優的,需要驗證改進。

Java實現

public class StepController {

    //狀態:靜止
    public static int STAY=0;
    
    //狀態:運動
    public static int MOVE=1;

    //運動狀態判斷陣列
    private float[] stateValue=new float[20];

    //加速度陣列
    private float[] Accs=new float[100];

    //每次傳入加速度的時間,省去定時器
    private long[] time=new long[100];

    //陣列下標
    private int count=0;

    //行走步數
    private int Step=0;

    //步長
    private float Length=0;

    //行進距離
    private float Distance=0;
    
    //狀態
    private int State=STAY;

    //回撥
    public interface StepCallback{
        void refreshStep(int step,float stepLength,float distance);
    }
    private StepCallback callback;

    public StepController(StepCallback callback){
        this.callback=callback;
    }

    //傳入加速度和時間
    public void refreshAcc(float[] values,long timestamp){
        Accs[count]=(float) Math.sqrt(values[0]*values[0]+
                values[1]*values[1]+
                values[2]*values[2]);
        time[count]=timestamp;

        //檢查運動狀態
        stateValue[count%20]=Accs[count];
        checkState();
        
        //設定檢測點,檢測點的作用是來減少計算量
        final int ckpoint = Accs.length / 5;
        
        //運動狀態判斷加上判斷檢測點是否有波谷存在
        if (State==MOVE&&Accs[(count-ckpoint+Accs.length)%Accs.length]<Accs[(count-ckpoint+Accs.length+1)%Accs.length]
                &&Accs[(count-ckpoint+Accs.length)%Accs.length]<Accs[(count-ckpoint+Accs.length-1)%Accs.length]) {
            //求均值
            float ave = Utils.ave(Accs);
            for (int i = 0; i < Accs.length; ++i) {
                ave += Accs[i];
            }
            ave /= Accs.length;

            //調整陣列順序,把新資料放在前面,同時把資料減去均值
            float[] data = new float[Accs.length];
            for (int i = 0, j = count; i < data.length; ++i, --j) {
                if (j < 0) j += data.length;
                data[i] = Accs[j] - ave;
            }
            
            //尋找波峰波谷
            float[] sign = new float[Accs.length];
            for (int i = 1; i < Accs.length - 1; ++i) {
                if (Math.abs(data[i]) > 0.8 && Math.abs(2 * data[i]) > Math.abs((data[i - 1] + data[i + 1]))) {
                    if (data[i] > 0) {
                        sign[i] = 1;
                    } else {
                        sign[i] = -1;
                    }
                }
            }

            //取相鄰波峰中的最大值和相鄰波谷中的最小值
            for (int i = 1; i < sign.length - 1; ) {
                int index = i;
                while (++i < sign.length - 1 && (sign[i] == 0 || sign[i] == sign[index])) {
                    if (sign[i] != 0 && Math.abs(data[i]) > Math.abs(data[index])) {
                        sign[index] = 0;
                        index = i;
                    } else {
                        sign[i] = 0;
                    }
                }
            }

            //再次判斷檢測點是否是波谷
            if (sign[ckpoint] < 0) {
                int index = ckpoint;
                
                //尋找下個波峰
                while (++index < sign.length && sign[index] == 0) ;

                if (index < sign.length && sign[index] > 0) {
                    int peak = index;
                    
                    //尋找下個波谷
                    while (++index < sign.length && sign[index] == 0) ;

                    if (index < sign.length && sign[index] < 0) {
                        int valley = index;
                        
                        //計算H和T
                        float H = data[peak] - 0.5f * data[ckpoint] - 0.5f * data[valley];
                        long T = time[(count - ckpoint + time.length) % time.length] - time[(count - valley + time.length) % time.length];

                        //門限判決
                        if (H > 3 && T > 300 && T < 1400) {
                            //步長計算
                            DetectStepLength((int) T, H);
                            ++Step;
                            callback.refreshStep(Step,Length,Distance);
                        }
                    }
                }
            }
        }

        if (++count==Accs.length) count=0;
    }

    //運動狀態判斷
    private void checkState(){
        float ave=0;
        float var=0;
        
        //求均值
        for (int i=0;i<stateValue.length;++i){
            ave+=stateValue[i];
        }
        ave/=stateValue.length;

        //求方差
        for (int i=0;i<stateValue.length;++i){
            var+=(stateValue[i]-ave)*(stateValue[i]-ave);
        }
        var/=stateValue.length;
        
        //狀態判決
        State=var>0.5?MOVE:STAY;
    }
    
    //步長計算,該公式利用最小二乘法推匯出,有一定可信性
    private void DetectStepLength(int time,float f){
        float steplength=0.35f-0.000155f*time+0.1638f*(float) Math.sqrt(f);
        this.Length=(this.Length+steplength)/2;
        Distance+=steplength;
    }
}