美文网首页
第四天. 基于离散傅里叶变换的计步器初探

第四天. 基于离散傅里叶变换的计步器初探

作者: 破旧的大卡车 | 来源:发表于2018-10-27 19:19 被阅读10次

作为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"}]
list-line-data
这里可以初略的过滤下数据, 例如你的记录中即有走又有跑, 会明显看出曲线的不同. 这里的n=2000即使如此得到的. 最原始的数据可在末尾下载.

离散傅里叶变换

接下来得到傅里叶变换后的图像

ListLinePlot[
 Select[Abs[Fourier[Sqrt[x^2 + y^2 + z^2]]], 30 > # &], 
 PlotRange -> All]
Stepcounter-Fourier

后面我过滤掉了一些数值(即能量小于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. 这也进一步验证了(x+y)/2=1/2即关于中心是对称的.

为了计算波峰的平均位置, 我根据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}

stepcnt-peak-pos

数据文件下载

这是我记录的一些数据, 可以供你参考. 下载

相关文章

网友评论

      本文标题:第四天. 基于离散傅里叶变换的计步器初探

      本文链接:https://www.haomeiwen.com/subject/hiodtqtx.html