38

Suppose you are holding an iphone/ipad vertically in front of you with the screen facing you, in portrait orientation. You tilt the device to one side, keeping the screen facing you. How do you measure that static tilt angle using CMMotionManager? It seems a simple question which should have a simple answer, yet I cannot find any method that does not disappear into quaternions and rotation matrices.

Can anyone point me to a worked example?

millport
  • 2,411
  • 5
  • 23
  • 19
  • "foundry" answered your question in the best and only way it could be answered. You should mark his answer as regret accepted one. – James Bush Jan 18 '17 at 05:52
  • Here's my own implementation, which works better than most: https://bitbucket.org/snippets/theoknock/74XxB/untitled-snippet – James Bush Jan 23 '17 at 18:55

4 Answers4

57

Look at gravity:

self.deviceQueue = [[NSOperationQueue alloc] init];
self.motionManager = [[CMMotionManager alloc] init];
self.motionManager.deviceMotionUpdateInterval = 5.0 / 60.0;

// UIDevice *device = [UIDevice currentDevice];

[self.motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXArbitraryZVertical
                                                        toQueue:self.deviceQueue
                                                    withHandler:^(CMDeviceMotion *motion, NSError *error)
{
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        CGFloat x = motion.gravity.x;
        CGFloat y = motion.gravity.y;
        CGFloat z = motion.gravity.z;
    }];
}];

With this reference frame (CMAttitudeReferenceFrameXArbitraryZVertical), if z is near zero, you're holding it on a plane perpendicular with the ground (e.g. as if you were holding it against a wall) and as you rotate it on that plane, x and y values change. Vertical is where x is near zero and y is near -1.


Looking at this post, I notice that if you want to convert this vector into angles, you can use the following algorithms.

If you want to calculate how many degrees from vertical the device is rotated (where positive is clockwise, negative is counter-clockwise), you can calculate this as:

// how much is it rotated around the z axis

CGFloat angle = atan2(y, x) + M_PI_2;           // in radians
CGFloat angleDegrees = angle * 180.0f / M_PI;   // in degrees

You can use this to figure out how much to rotate the view via the Quartz 2D transform property:

self.view.layer.transform = CATransform3DRotate(CATransform3DIdentity, -rotateRadians, 0, 0, 1);

(Personally, I update the rotation angle in the startDeviceMotionUpdates method, and update this transform in a CADisplayLink, which decouples the screen updates from the angle updates.)

You can see how far you've tilted it backward/forward via:

// how far it it tilted forward and backward

CGFloat r = sqrtf(x*x + y*y + z*z);
CGFloat tiltForwardBackward = acosf(z/r) * 180.0f / M_PI - 90.0f;
Rob
  • 415,655
  • 72
  • 787
  • 1,044
10

It is kind of a late answer but you can found a working example on github and the blog article that comes with it.

To summarize the article mentioned above, you can use quaternions to avoid the gimbal lock problem that you are probably facing when holding the iPhone vertically.

Here is the coding part that compute the tilt (or yaw) :

CMQuaternion quat = self.motionManager.deviceMotion.attitude.quaternion;
double yaw = asin(2*(quat.x*quat.z - quat.w*quat.y));

// use the yaw value
// ...

You can even add a simple Kalman filter to ease the yaw :

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
// ...
dulaccc
  • 1,148
  • 11
  • 12
  • 1
    Why go the extra mile and use an overly complex Kalman Filter? Why not a simple low-pass filter? – openfrog Nov 29 '13 at 16:27
  • 1
    That's a very good point, I was influenced by articles on signal processing and kept in mind that a KF is a very good tool. I'm clearly not an expert on this field. Basically I tried to configure the Kalman filter to behave like a low pass filter, so yes why not simply use a low pass filter :) If you have a better implementation, go for it, and I'll be glad to update the answer with that. – dulaccc Nov 30 '13 at 13:22
  • 5
    Just wanted to mention, that of all the solutions above this one with the quaternions was the only one that reliably worked and avoided the Gimbal lock problem - thank you kind Sir! – hreimer Feb 26 '14 at 19:06
  • The problem w/this is that b/c the range of arcsin is [-90, 90] degrees, this can't tell when you've rotated more than 90 degrees clockwise or counterclockwise. – kevlar Apr 27 '17 at 03:25
7

Here is an example that rotates a UIView self.horizon to keep it level with the horizon as you tilt the device.

- (void)startDeviceMotionUpdates 
{
    CMMotionManager* coreMotionManager = [[CMMotionManager alloc] init];
    NSOperationQueue* motionQueue = [[NSOperationQueue alloc] init]
    CGFloat updateInterval = 1/60.0;
    CMAttitudeReferenceFrame frame = CMAttitudeReferenceFrameXArbitraryCorrectedZVertical;
    [coreMotionManager setDeviceMotionUpdateInterval:updateInterval];
    [coreMotionManager startDeviceMotionUpdatesUsingReferenceFrame:frame
                            toQueue:motionQueue
                            withHandler:
     ^(CMDeviceMotion* motion, NSError* error){
         CGFloat angle =  atan2( motion.gravity.x, motion.gravity.y );
         CGAffineTransform transform = CGAffineTransformMakeRotation(angle);
         self.horizon.transform = transform;
     }];
}

This is a little oversimplified - you should be sure to have only one instance of CMMotionManager in your app so you want to pre-initialise this and access it via a property.

foundry
  • 31,615
  • 9
  • 90
  • 125
  • Many thanks, it was that atan2 function that I needed (though I question the need to add M_PI to the angle?). I have used the previous answer to guide me in warning the user when the z value gets too high or too low, to make sure he/she holds the device vertically. I also appreciate the reminder to have only one CMMotionManager created within the app. – millport Mar 27 '13 at 14:45
  • 1
    @millport, you're right, it works fine without M_PI - I was using this in a context where I wanted to keep my numbers above zero. I've fixed the answer. – foundry Mar 27 '13 at 15:18
  • @foundry Bravo! This is really the only way it should be done (i.e., atan2...); that formula is what Apple uses for applying the preferred rotation transform to the coordinate position of OpenGL shader output. Numerical values generated by anything other than the GPU that are passed to an OpenGL shader must be as precise as those generated by the GPU to render expected results (close doesn't cut it). The only formula of all the above to achieve perfect precision for the purposes of OpenGL is the one you provided. – James Bush Jan 18 '17 at 05:48
3

Since iOS8 CoreMotion also returns you a CMAttitude object, which contains pitch, roll and yaw properties, as well as the quaternion. Using this will mean you don't have to do the manual maths to convert acceleration to orientation.

rennarda
  • 258
  • 2
  • 8