Computing the iOS device tilt

Mar 3, 2013 • Use quaternion mathematic properties to efficiently and easily compute the tilt of an iPhone.

The tilt of a device is probably the most useful information you can get from the acceleration data of your iPhone, iPad or iPod touch. Besides, a lot of games are using it. The question is, how to get this tilt value ?

First thought

I’ve looked into the Core Motion documention and found out that Apple is computing some values for us, which is available within a CMAttitude class instance.

An instance of the CMAttitude class represents a measurement of the device’s attitude at a point in time.

CMAttitude reference

Well, Apple isn’t talking about a ballerina attitude but more of a flight-type attitude. Indeed, the iPhone orientation can be described just like an airplane by its roll, pitch and yaw.

And you have guessed, the yaw value is the rotation against the red axis. It seems pretty straight forward so let’s implement that.

- (void)viewDidLoad {
    [super viewDidLoad];

    self.motionManager = [[CMMotionManager alloc] init];
    self.motionManager.deviceMotionUpdateInterval = 0.02;  // 50 Hz

    self.motionDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(motionRefresh:)];
    [self.motionDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

    if ([self.motionManager isDeviceMotionAvailable]) {
        // to avoid using more CPU than necessary we use `CMAttitudeReferenceFrameXArbitraryZVertical`
        [self.motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXArbitraryZVertical];
    }
}

- (void)motionRefresh:(id)sender {
    double yaw = self.motionManager.deviceMotion.attitude.yaw;

    // use the yaw value
    // ...
}

And I thought “easy, problem solved”. In fact, it was terrible and unusable because the yaw value was impacted by the iPhone roll and pitch. I mean, if we keep the iPhone vertical and twist it against the blue axis, it will modify the yawBad!

Check out the gimbal lock problem if you want to understand more about it.

So back to square one, I had to find a way to compute the yaw by myself and I felt that math may rescue me!

The beauty of Quaternions

If you don’t know what a quaternion is yet, please don’t freak out by this strange word that seems coming right out of Star Trek.

Quaternions were first described by Hamilton in 1843 and applied to mechanics in three-dimensional space.

Wikipedia

It eases the way we deal with the orientation of a body in a 3D space, and is better suited than the Euler angles that Apple is computing for us because of three reasons :

  • it’s easier to compose rotations or to extract values from it.
  • it avoids the gimbal lock problem.
  • and Apple provides a quaternion in the CMAttitude class instance.

And because we only want to compute the yaw we do not have to worry about the gimbal lock problem, since our goal is not to describe the complete iPhone orientation in the 3D space but only the tilt of it.

There is a very simple formula to compute yaw from a quaternion :

So, the motionRefresh: method described above become :

- (void)motionRefresh:(id)sender {
    CMQuaternion quat = self.motionManager.deviceMotion.attitude.quaternion;
    double yaw = asin(2*(quat.x*quat.z - quat.w*quat.y));

    // use the yaw value
    // ...
}

The icing on the cake

We can improve the code a bit to have a perfectly smooth yaw signal, or to have some kind of internia in the tilt movement (just like I needed in my DPMeterView project).

In order to do that, we need a very simple one dimensional Kalman-filter. I’m not discussing the details of how it works because it’s not the purpose of the article. However, you can experiment by yourself the impact of changing some of those values.

- (void)motionRefresh:(id)sender {
    CMQuaternion quat = self.motionManager.deviceMotion.attitude.quaternion;
    double yaw = asin(2*(quat.x*quat.z - quat.w*quat.y));

    if (self.motionLastYaw == 0) {
        self.motionLastYaw = yaw;
    }

    // kalman filtering
    static float q = 0.1;   // process noise
    static float r = 0.1;   // sensor noise
    static float p = 0.1;   // estimated error
    static float k = 0.5;   // kalman filter gain

    float x = self.motionLastYaw;
    p = p + q;
    k = p / (p + r);
    x = x + k*(yaw - x);
    p = (1 - k)*p;
    self.motionLastYaw = x;

    // use the x value as the "updated and smooth" yaw
    // ...
}

Fork it !

If you have some ideas of improvement, or just want to play with a working example, don’t hesitate to fork the DPMeterView project hosted on GitHub.

iPhone 5 model created by Pixeden.

comments powered by Disqus