作为Android开发的一个实例, 我将演示如何实时显示手机内置加速器的数字以及当前时间. 然后将这些数据实时地写入到一个文本文件, 最后对这些数据利用离散傅里叶变换进行分析.
初步的分析结果表明, 人的走路方式有两个明显的波峰, 它们的周期呈现倍数关系. 这相当于将比较复杂的运动过程, 提取出了该运动的特征. 下一步是怎么运用该特征, 这有待于进一步研究.
现行的计步算法
目前大多数是基于滤波器的算法. 例如文章FootPath: Accurate map-based indoor navigation using smartphones中讨论的. 其算法可以参考xfmax的项目BasePedo
以及Liyachao.
下面作为实验, 我们来看看如何抓取传感器数据.
传感器数据的抓取
AndroidManifest.xml
文件的修改
由于我们最终需要把数据放到一个文本文件, 故需要在AndroidManifest.xml
中加入权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
activity_main.xml
文件的修改
我们将加速度传感器的坐标以及当前时间实时的显示到频幕, 故加入四个TextView:
<LinearLayout
android:id="@+id/view"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="false">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="New Text"
android:id="@+id/textViewx" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="New Text"
android:id="@+id/textViewy" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="New Text"
android:id="@+id/textViewz" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="New Text"
android:id="@+id/textViewt" />
</LinearLayout>
MainActivity.java
文件的修改
最后, 我们重写MainActivity
如下
public class MainActivity extends AppCompatActivity implements SensorEventListener {
TextView textViewx, textViewy, textViewz, textViewt;
Calendar mCalendar;
long t0 = 0;
private static final String TAG = "";
//首先注册传感器以及是个view
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LinearLayout view = (LinearLayout) findViewById(R.id.view);
//sensor
SensorManager mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
Sensor mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
if (!(mSensorManager == null)) {
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);
}
textViewx = (TextView) findViewById(R.id.textViewx);
textViewy = (TextView) findViewById(R.id.textViewy);
textViewz = (TextView) findViewById(R.id.textViewz);
textViewt = (TextView) findViewById(R.id.textViewt);
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// TODO Auto-generated method stub
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
float x = (float) event.values[0];
float y = (float) event.values[1];
float z = (float) event.values[2];
mCalendar = Calendar.getInstance();
//update pre 0.1 sec and recount after 100 sec
long t1 = mCalendar.getTimeInMillis() / 100;
if (t1-t0>1000) {
t0 = t1;
}
//output to views
textViewt.setText("T: " + String.valueOf(t1 - t0));
textViewx.setText("X: " + x);
textViewy.setText("Y: " + y);
textViewz.setText("Z: " + z);
//output to device
File root = new File(Environment.getExternalStorageDirectory().toString() + "/stepcnt");
if (!root.mkdirs()) {
String LOG_TAG = "";
Log.e(LOG_TAG, "Directory not created");
}
//output filename based on time, maybe not necessary
File data = new File(root, "data"+String.valueOf(t0)+".txt");
try {
FileOutputStream stream = new FileOutputStream(data, true);
String str = "{" + String.valueOf(x) + "," + String.valueOf(y) + "," + String.valueOf(z) + "," + String.valueOf(t1) + "}\n";
stream.write(str.getBytes());
stream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
Log.i(TAG, "data.txt not found");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这样, 大约0.2秒会抓取一个数据{x,y,z,t}
到文本文件data*.txt
.
数据的分析
我是基于mma来分析这些数据的.
手机数据的导出
首先将手机根目录下的文件夹stepcnt
下的所有txt数据文件copy到笔记本桌面位置:D:\ Users\ThinkPad\Desktop\stepcnt
.
这里要注意, 直接连上手机, 在电脑上好像不能发现手机目录stepcnt
下的文件, 我是将其copy到手机根目录, 然后就可以复制到电脑了. 这应该是权限不够的原因.
导入数据到mma
下面的代码首先设置工作目录, 然后得到工作目录下所有的txt
文件. 最后用Join
把每个文件的数据合并到一起. 注意这里Import
的类型是List
但是直接导入的并不是数组而是字符串, 故需要ToExpression
转换下. 最后列出数组的长度. 并画出连线图.
SetDirectory["D:\\Users\\ThinkPad\\Desktop\\stepcnt\\"];
files = FileNames["*.txt"];
data = Join[
Sequence @@
Table[Import[files[[i]], "List"] // ToExpression, {i,
Length[files]}]];
data // Length
画出连线图,
n=2000;
x = data[[1 ;;n , 1]];
y = data[[1 ;;n , 2]];
z = data[[1 ;;n , 3]];
ListLinePlot[{x, y, z}, PlotLegends -> {"x", "y", "z"}]
这里可以初略的过滤下数据, 例如你的记录中即有走又有跑, 会明显看出曲线的不同. 这里的
n=2000
即使如此得到的. 最原始的数据可在末尾下载.
离散傅里叶变换
接下来得到傅里叶变换后的图像
ListLinePlot[
Select[Abs[Fourier[Sqrt[x^2 + y^2 + z^2]]], 30 > # &],
PlotRange -> All]
后面我过滤掉了一些数值(即能量小于30的数据才取出来), 这基本不影响我们的分析.
粗略的结论
观察注意到如下结论, 我们将在后面进一步验证.
- 图像关于
x=1000
对称, 这并不奇怪, 因为标准的周期函数sin[2*Pi*x*30/200]
的离散傅里叶变换也有两个波峰, 他们它们在x
轴的位置之和恰为数据的长度. 请参考mma文档:离散傅里叶变换. - 从图中可以看出有四种波峰
- 进一步分析会发现, 它们对应的
x
轴的坐标间隔基本一致大约都是0.09*2000
的倍数.
数据的进一步验证
首先我们定义傅里叶变换的数据dxyz
, 它是传感器各个方向的欧氏模长, 这去掉了手机本身的放置状态.
其次, 我定义了一个fliter
以及误差e
. 它们是通过波峰的位置得到的, 其实也可参考后面的图形进行进一步调整. 应该注意, 对不同的人, 这些数值是不一样的.
然后我选出波峰, 判断的标准是高度在8--20
之间, 并按从大到小排列. 接着得到上面选出的波峰对应的x
-轴的位置. 由于前面的分析, 波峰应该关于某个轴对称, 故我们还过滤掉了那些单峰最终得到的波峰位置数据为pos
.
得到了pos
首先我们可以拟合这些数据, 输出结果表明它们满足方程y=1-x
. 这也进一步验证了即关于中心是对称的.
为了计算波峰的平均位置, 我根据pos
的图像设计了误差e
, 然后用Select
选出pos
的第一个坐标与fliter
预先给出的值小于误差的数据res
. 并打印出这样的数据的个数以及平均值.
最后, 画出pos
的图像.
dxyz = Select[Abs[Fourier[Sqrt[x^2 + y^2 + z^2]]], 30 > # &];
fliter = {0.16, 0.27, 0.36, 0.45};
listfit[d_, n_, fliter_] :=
Module[{mx, tab, pos, e = 0.04,(*the width of x-coord*)},
mx = Sort[Select[d, 8 < # < 20 &], Greater];
tab = Table[Flatten[Position[d, mx[[i]]]]/n, {i, Length[mx]}];
pos = Cases[tab, {_, _}];
Print["方程: ", Fit[pos, {1, s}, s]];
Table[res = Select[pos/1., Abs[#[[1]] - i] < e &];
Print["个数: ", Length[res], " 平均值: ", Total[res]/Length[res]], {i,
fliter}];
ListPlot[pos, AxesOrigin -> {0, 0}, AspectRatio -> 1]]
listfit[dxyz, n, fliter]
输出结果:
方程: 1. -1. s
个数: 4 平均值: {0.181,0.819}
个数: 12 平均值: {0.272583,0.727417}
个数: 22 平均值: {0.365636,0.634364}
个数: 16 平均值: {0.452438,0.547563}
数据文件下载
这是我记录的一些数据, 可以供你参考. 下载