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

作为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

数据文件下载

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,717评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,501评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,311评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,417评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,500评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,538评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,557评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,310评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,759评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,065评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,233评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,909评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,548评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,172评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,420评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,103评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,098评论 2 352

推荐阅读更多精彩内容