在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 重启时,从存储中恢复数据,通过我这个类的构造方法传入。