作为一个刚学习iPhone编程一周的人来说,其实写这篇文章的目的不是传道授业解惑,而是为了知识的总结。如果能吸引到各位为我传道授业解惑那再好不过了。文章基本的内容,整个流程是参照WWDC 2010上的session 423: Sensing Device Motion in iOS4写的,同时参考了开发文档Event Handling Guide for iPhone OS: Motion Events。当然,我在介绍的时候会添加一些个人的总结和原理的补充。文章可能有错误的叙述,发现后请指出,谢谢。
我之前的文章:移动设备智能化的基石–从iPhone4的传感器谈起曾经写过,iPhone4增加的陀螺仪弥补了很多现有motion sensor的不足,也介绍了很多背景知识。加速度计最大的缺陷,就是无法检测沿着重力加速度轴方面的旋转变化,而且,如果仅仅有加速度计,无法避免重力的干扰。传统的方法是怎么做的呢?先高通滤波,把近似于直流分量的重力加速度隔离出来,再低通滤波,把因为手机颤抖产生的高频噪声去掉。大量的滤波,不仅会影响到原本的加速度信号,还会严重减缓处理速度,并随之影响程序相应速度。电子罗盘在这方面毫无帮助,这玩意本身的读数都需要很长的稳定时间,还特别容易受环境干扰。所以,陀螺仪的引入,解决了这几个大问题,我们可以用它来测量沿重力加速度的旋转,也可以测量快速的旋转,大大增强了motion相关的处理工作,更重要的是,通过陀螺仪可以判断手机当前的摆放位置和姿势,然后通过这个信息能够得到相当准确的重力加速度分量,而不是从加速计采集的包含很多噪声的加速度值中提取。所以说,陀螺仪对于motion sensing方面的工作,是最关键的一个部件。陀螺仪的具体原理,请参考之前那篇文章。
在iOS4之前,加速度计由UIAccelerometer类来负责采集工作,而电子罗盘则由Core Location接管。而iPhone4的推出,由于加速度计的升级(有消息说使用的是这款芯片)和陀螺仪的引入,与motion相关的编程成为重头戏,所以,苹果在iOS4中增加一个一个专门负责该方面处理的框架,就是Core Motion Framework。这个Core Motion有什么好处呢?简单来说,它不仅仅提供给你获得实时的加速度值和旋转速度值,更重要的是,苹果在其中集成了很多算法,可以直接给你输出把重力加速度分量剥离的加速度,省去你的高通滤波操作,以及提供给你一个专门的设备的三维attitude信息!
有这么一个好东西,我们自然就要好好利用了。下面就介绍一下,如何利用Core Motion Framework,来获得对应的motion信息。
Core Motion在iOS4.0主要负责三种数据:加速度值,陀螺仪值,设备motion值。实际上,这个设备motion值就是通过加速度和旋转速度进行fusing变换算出来的,基本原理后面会介绍。Core Motion在系统中以单独的后台线程的方式去获得原始数据,并同时执行一些motion算法来提取更多的信息,然后呈献给应用层做进一步处理。Core Motion框架包含有一个专门的Manager类,CMMotionManager,然后由这个manager去管理三种和运动相关的数据封装类,而且,这些类都是CMLogItem类的子类,所以相关的motion数据都可以和发生的时间信息一起保存到对应文件中,有了时间戳,两个相邻数据之间的实际更新时间就很容易得到了。这个东西是非常有用的,比如有些时候,你得到的是50Hz的采样数据,但希望知道的是每一秒加速度的平均值。
从Core Motion中获取数据主要是两种方式,一种是Push,就是你提供一个线程管理器NSOperationQueue,再提供一个Block(有点像C中的回调函数),这样,Core Motion自动在每一个采样数据到来的时候回调这个Block,进行处理。在这中情况下,block中的操作会在你自己的主线程内执行。另一种方式叫做Pull,在这个方式里,你必须主动去像Core Motion Manager要数据,这个数据就是最近一次的采样数据。你不去要,Core Motion Manager就不会给你。当然,在这种情况下,Core Motion所有的操作都在自己的后台线程中进行,不会有任何干扰你当前线程的行为。
那接下来的问题就是,我在什么时候选择什么方式呢?苹果官方推荐了一个使用指南,比较了两种方式的优劣,并做出了使用场景的推荐。如下图所示。应该说,两种方式各自的优缺点还是很鲜明的,使用场景也大不一样,很好区分。
Core Motion的大体介绍就是这些。下面说说Core Motion具体负责的采集,计算和处理。Core Motion的使用就是一三部曲:初始化,获取数据,处理后事。
在初始化阶段,不管你要获取的是什么数据,首先需要做的就是
motionManager = [[CMMotionManager alloc] init];
所有的操作都会由这个manager接管。后面的初始化操作相当直观,以加速度的pull方式为例
if (!motionManager.accelerometerAvailable) {
// fail code // 检查传感器到底在设备上是否可用
}
motionManager.accelerometerUpdateInterval = 0.01; // 告诉manager,更新频率是100Hz
[motionManager startAccelerometerUpdates]; // 开始更新,后台线程开始运行。这是pull方式。
如果是push方式,更新的代码可以写成这样
[motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:^(CMAccelerometerData *latestAcc, NSError *error)
{
// Your code here
}];
接下来就是获取数据了。Again,很简单的代码
CMAccelerometerData *newestAccel = motionManager.accelerometerData;
filteredAcceleration[0] = newestAccel.acceleration.x;
filteredAcceleration[1] = newestAccel.acceleration.y;
filteredAcceleration[2] = newestAccel.acceleration.z;
通过定义的CMAccelerometerData变量,获取CMAcceleration信息。和以前的UIAccelerometer类的使用方式一样,CMAcceleration在Core Motion中是以结构体形式定义的
typedef struct {
double x;
double y;
double z;
}
对应的motion信息,比如加速度或者旋转速度,就可以直接从这三个成员变量中得到。
最后是处理后事,就是在你不需要Core Motion进行处理的时候,释放资源
[motionManager stopAccelerometerUpdates];
//[motionManager stopGyroUpdates];
//[motionManager stopDeviceMotionUpdates];
[motionManager release];
你看,就是这么简单。当然,如果这么Core Motion这么简单,就太无趣了。实际上,Core Motion最好玩的地方,既不是加速度,也不是角速度,而是经过sensor fusing算法处理的Device Motion信息的提供。Core Motion里面提供了一个叫做CMDeviceMotion的类,用来把下图所示的这些数据封装成Device Motion信息:
我们来看看这些被封装数据的介绍。
第一个attitude,就是刚才说到的三维attitude,通俗来讲,就是告诉你手机在当前空间的位置和姿势。
第二个是重力信息,其本质是重力加速度矢量在当前设备的参考坐标系中的表达,开发不再需要通过滤波来提取这个信息了,因为Core Motion已经给你了。
第三个是加速度信息。同样,滤波在这里不再需要(根据程序需求而加的滤波算法自然是可以保留的)。
第四个是即时的旋转速率,也就是rotation rate,是陀螺仪的输出。
下面就来详细介绍一下这四种数据。
1. Attitude。在CMDeviceMotion对象中,attitude是以
@property (readonly, nonatomic) CMAttitude *attitude;
属性定义的。一个CMAttitude的实例,封装了关于当前设备在空间中的姿态信息。这个信息是由下面集中数学表达式定义的:
四元数是一种Attitude Determination System经常使用的数据保存形式,我不是很清楚。而欧拉角和变换矩阵则是相辅相成的,两者之间可以相互推导。所以这里主要介绍一下对虚拟现实或者游戏都大有帮助的变换矩阵。
这个rotation变换矩阵究竟有什么用呢?我们来看看下面这张图
本质上讲,变换矩阵给我们阐述了从一个向量空间到另一个向量空间的映射关系。举个例子,在很多应用中都需要对加速度信息进行判断,但是,用户在使用手机的过程中,姿势是不断变换的,我们可以采集到某个设备在t1时间点的加速度以及重力信息,也可以采集到t2时间点的信息,我们却不能直接拿他们做运算。为什么?因为由于手机各个轴方向的变化,加速度和重力信息在t1时间点属于一个向量空间,在t2时间点,就属于另一个向量空间了,如果你硬拿acc.x1和acc.x2求设备的运动模式,自然不可能准的。
所以,现在的问题是,我们要找到两个三维空间的线性变换T,让这个变换关系帮我们把某个空间的值变换到另一个空间去,这样就可以在同一个空间做比较或者任何计算了。Core Motion如何解决这个问题呢?它首先让你可以在程序开始的初始时间点t1(比如你画第一祯的时候)采集一个attitude的值作为参照坐标系,我们假定这个向量是v_ref。在任何时间点,比如t2,采集一个attitude的值,假定这个向量是v_dev,位于当前设备的坐标系,那么我们有以下关系:
其中R就是rotation matrix。由于v_ref是正交基向量,所以
刚才说到了,v_ref和v_dev都是其对应向量空间的正交基,而这个R矩阵正好是正交矩阵,所有的列向量线性独立。所以,R所对应的变换,正是我们要找的这两个空间的线性变换,而且这是一对一变换。
好,上面这个结论告诉我们什么呢?你在当前时刻t2采集的当前坐标系下的加速度信息,不仅在t1时刻的参照坐标系下有对应的向量,而且仅有一个对应向量!如果我们定义a_dev是当前的加速度向量,那么它在参照坐标系里面对应的加速度向量只有一个,而且肯定可以由下面式子求出
这个式子不存在无解的情况,因为正交矩阵永远都是有逆矩阵的。经此变换,你就可以随意比较和计算不同时间点的加速度和重力信息,从而得出精确的用户运动模式了。
比较有趣的一点是,R变换矩阵的表达形式,正好表明了R和rotation rate的关系: 当前时间点的坐标系和参照坐标系的变换矩阵,是由陀螺仪提供的yaw, pitch和roll三个轴上角度信息推断的。于是,再一次,我们感受到了新加的陀螺仪强大的地方。更强大的地方在于,Core Motion直接就把R矩阵提供给开发者了,省去了开发者很多易错而繁琐的工作。
说了这么多铺垫,还是简单介绍一下获得当前时间点的R矩阵信息的步骤吧:
首先,获得参考矩阵信息
if (motionManager != nil) {
CMDeviceMotion *deviceMotion = motionManager.deviceMotion;
referenceAttitude = [deviceMotion.attitude retain];
}
然后在希望得到R矩阵的时候,执行下列操作:
CMRotationMatrix rotation;
CMDeviceMotion *deviceMotion = motionManager.deviceMotion;
CMAttitude *attitude = deviceMotion.attitude;
if (referenceAttitude != nil) {
[attitude multiplyByInverseOfAttitude:referenceAttitude];
}
rotation = attitude.rotationMatrix;
很简单,也很直观,一个multiplyByInverseOfAttitude的调用,正好反应了我们刚才推导的矩阵运算关系。至此,rotationMatrix被我们拿到,接下来的事情就只有想不到,没有做不到了。
2. Gravity和UserAcceleration。之所以把他们放在一起讲,是因为他们本质上比较类似,而且原始的加速度(就是通过[motionManager startAccelerometerUpdates]获得的那个值)本来就是他们的叠加和,换句话说,将原始加速度分解就得到了他们俩,只不过现在苹果帮你把这个滤波分解给做了。他俩在Core Motion中的属性定义是
@property (readonly, nonatomic) CMAcceleration gravity;
@property (readonly, nonatomic) CMAcceleration UserAcceleration;
都是CMAcceleration所包装的结构体。而且,两者的参考坐标系都是一样的,以设备的外框架为准:
得到这两种数据的方式比较简单,就是直接通过读取motionManager.deviceMotion.userAcceleration/gravity的三个成员变量即可。
3. Rotation rate。旋转速率是通过叫CMRotationRate的结构体封装的,其内部变量定义和CMAcceleration一模一样。正负的确定,由右手法则判断。看到这里,不少朋友可能会有问题:这个数据,和之前介绍的直接通过motionManager获得的CMGyroData有什么区别呢?通过Device Motion封装处理后的Rotation rate,去掉了原始的CMGyroData所有的bias。举个例子,如果我们把设备放在桌上静止不动,理想情况下,陀螺仪的输出应该是0。问题在于,你直接从陀螺仪获得的原始数据并不是0,而是由很多不确定因素导致的非0值,这其中就包括了很多的漂移误差等等,比如陀螺仪温漂,就会影响到我们的读数。Core Motion经过一些算法的处理,帮开发者消除了这种bias,极大方便了motion相关的开发工作。
说到Rotation rate,要讲一下这个输出数值的特点。如果你写一个简单的测试程序,把三个轴的数值都输出到屏幕上来看,会发现一个很有意思的现象:pitch和roll的值,和你读数时候的手机的attitude完全对应,而yaw的值,则是从0开始显示,手机的attitude在之后变了的话,yaw的值才有对应的变化。这是因为,对于pitch和roll来讲,他们都有明确的参照面,就是水平面,而且这个值肯定是在出厂之前就校正过。但yaw呢?没有,用户在刚打开app的时候,可能会朝向任何不同的方向。所以此时,Core Motion干脆就给你输出相对的初始值0,之后你再根据yaw方向上的相对变化来判断设备的位置变化。
另外一点要补充的是,对于设备的旋转,如果三个轴上都有变化,那么默认角度计算的顺序是先roll,再pitch,最后yaw。
讲到现在,基本的Core Motion知识就都总结完了。在智能手机出现之前,我们都说,手机就是用来打电话的。智能手机改变了这一切。用户总是有各种各样的需求等待开发者满足,而关键就在于,开发者能不能理解用户,认识用户,做好个人化。Gyroscope的集成和Core Motion的推出,让很多以前在智能手机上无法实现或者难以实现的应用成为了可能。这是机遇,想象力的机遇,也是挑战,执行能力的挑战。