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?
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;
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
// ...
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.
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.
Related
I was faced with this question in one of my interviews and was completely stumped. The only solution I could think of was storing the currentAngle in a NSArray to calculate the next angle.
Question:
Move a 35px ball across the screen utilizing the iPhone's compass. Once the ball is in the center of the screen, let the user tap it to 'reset' the position. Once reset, the ball will go back to the Min position. Remember that the compass may start somewhere between 0-359, the task is to find the nearest capture angle and focus on that angle until the ball is aligned. Once the ball is aligned & reset, the iPhone will move to the next angle and so forth until the ball has been reset 18 times. 18 resets * 20 degree angles = 360.
Assigned Variables:
int currentAngle = (Ranging between 0-359) (Constant updates as the user twirls around)
int captureAngle = 20
int centerX = view.center.x (160) - 35 (size of ball)
int ballSize = 35 (ball.width/2)
The paper looked something like this:
Function so far:
-(void)testMotion{
motionQueue = [[NSOperationQueue alloc] init];
motionManager = [[CMMotionManager alloc] init];
motionManager.deviceMotionUpdateInterval = 1.0f / 60.0f;
if (([CMMotionManager availableAttitudeReferenceFrames] & CMAttitudeReferenceFrameXTrueNorthZVertical) != 0) {
[motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXTrueNorthZVertical
toQueue:motionQueue
withHandler:^(CMDeviceMotion *motion, NSError *error)
{
if (!error) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
CMAttitude *attitude = motion.attitude;
CMRotationMatrix rm = attitude.rotationMatrix;
// Get the heading.
double heading = M_PI + atan2(rm.m22, rm.m12);
heading = heading*180/M_PI;
int currentAngle = (int)heading;
NSLog(#"Current Angle: %d",currentAngle);
int captureAngle = 20; // 20 Degress Capture Angle
}];
}
}];
}
}
If I understood you, then it's something like this:
calculate the x and y movement from the angle
(see
https://en.wikipedia.org/wiki/Rotation_of_axes
http://keisan.casio.com/has10/SpecExec.cgi?id=system/2006/1223522781
)
Then move the ball according to those values, and if it moved by an angle of 20 - allow reset it or get out of the loop (for your choice)
while(1) {
x = r \cos(currentAngle)
y = r \sin(currentAngle)
//change the ball position,
ball.position.x += x*speed
ball.position.y += y*speed
//check if angel is +20 or -20
if (((currentAngle + 20) % 360) != captureAngle && (abs(currentAngle - 20) % 360) != captureAngle)) {
allow_reset_ball = true
break;
}
}
I've been trying to get my MKMapView to detect whether or not a tap was on a tile with alpha > 0. I'm quite new at ObjC and Xcode as well so this functionality is a bit over my head. All help will me greatly appreciated!
So far I've tried many different strategies but always come up short. We have custom classes to replace MKOverlay and MKOverlayView that implement each respectively so I've been trying to grab the tiles when they're created and save them to an array to later reference in the MKMapViewController when the map is touched.
- (NSArray *)tilesInMapRect:(MKMapRect)rect zoomScale:(MKZoomScale)scale
{
NSInteger z = zoomScaleToZoomLevel(scale);
// Number of tiles wide or high (but not wide * high)
NSInteger tilesAtZ = pow(2, z);
NSInteger minX = floor((MKMapRectGetMinX(rect) * scale) / TILE_SIZE);
NSInteger maxX = floor((MKMapRectGetMaxX(rect) * scale) / TILE_SIZE);
NSInteger minY = floor((MKMapRectGetMinY(rect) * scale) / TILE_SIZE);
NSInteger maxY = floor((MKMapRectGetMaxY(rect) * scale) / TILE_SIZE);
NSMutableArray *tiles = nil;
for (NSInteger x = minX; x <= maxX; x++) {
for (NSInteger y = minY; y <= maxY; y++) {
// As in initWithTilePath, need to flip y index to match the gdal2tiles.py convention.
NSInteger flippedY = abs(y + 1 - tilesAtZ);
NSString *tileKey = [[NSString alloc] initWithFormat:#"%d/%d/%d", z, x, flippedY];
if ([tilePaths containsObject:tileKey]) {
if (!tiles) {
tiles = [NSMutableArray array];
}
MKMapRect frame = MKMapRectMake((double)(x * TILE_SIZE) / scale,
(double)(y * TILE_SIZE) / scale,
TILE_SIZE / scale,
TILE_SIZE / scale);
NSString *path = [[NSString alloc] initWithFormat:#"%#/%#.png", tileBase, tileKey];
ImageTile *tile = [[ImageTile alloc] initWithFrame:frame path:path];
[tiles addObject:tile];
[myTiles addObject:tile];
[path release];
[tile release];
}
[tileKey release];
}
}
return tiles;
}
That's where I populate the array which is a "class variable". If I comment out the [tiles addObject:tile]; I get the background of the map drawn but no buildings so I think adding specifically those tiles is correct.
Then in the mapviewController gesture handler function I check if the touch is in the tile.frame which is is for 8 out of 32 (it can be 0 if you click far from the buildings and the total changes when you zoom around, but always gets bigger)which seems like an odd number. But pretending that that works correctly I check the alpha at that point using a modified version of this answerer's function: how to get the RGBA value of UIImage in the specific clicked point
but I don't know if that works for mapView's like it would for imageViews. I think I might need to translate the context but I've never worked with contexts before...
Sorry for so much text! Maybe this isn't even possible? I'll add more code if clarification is needed. Any input would help!
I'm trying to develop an App with an "Around Me"-like feature of a location list with small directional arrows on the side.
Bearing and offset to the different locations hadn't been a problem thanks to Stackoverflow and compensating the compass-lag did well with following tutorial:
http://www.sundh.com/blog/2011/09/stabalize-compass-of-iphone-with-gyroscope/
All the stuff works fine with only one location in that UITableView.
But when there are more than one location, the arrows won't turn smooth and it feels like my iPhone isn't fast enough for calculating the stuff and turning these multiple arrows but I don't know how to do that better.
At the moment I'm trying this (without the locations specific directional offset):
I'm saving all the UIImageViews of all the cells in an array
when getting a new yaw value I loop through the array an actualize all the Images Rotation
if(motionManager.isDeviceMotionAvailable) {
// Listen to events from the motionManager
motionHandler = ^ (CMDeviceMotion *motion, NSError *error) {
CMAttitude *currentAttitude = motion.attitude;
float yawValue = currentAttitude.yaw; // Use the yaw value
// Yaw values are in radians (-180 - 180), here we convert to degrees
float yawDegrees = CC_RADIANS_TO_DEGREES(yawValue);
currentYaw = yawDegrees;
// We add new compass value together with new yaw value
yawDegrees = newCompassTarget + (yawDegrees - offsetG);
// Degrees should always be positive
if(yawDegrees < 0) {
yawDegrees = yawDegrees + 360;
}
compassDif.text = [NSString stringWithFormat:#"Gyro: %f",yawDegrees]; // Debug
float gyroDegrees = (yawDegrees*radianConst);
// If there is a new compass value the gyro graphic animates to this position
if(updateCompass) {
[self setRotateArrow:gyroDegrees animated:YES];
[self commitAnimations];
updateCompass = 0;
} else {
[self setRotateArrow:gyroDegrees animated:NO];
[UIView commitAnimations];
}
};
and the setRotateArrow:animated method:
- (void) setRotateArrow:(float)degrees animated:(BOOL)animated{
UIImage *arrowImage = [UIImage imageNamed:#"DirectionArrow.png"];
for (int i = 0; i<arrowImageViews.count; i++) {
[(UIImageView *)[arrowImageViews objectAtIndex:i] setImage:arrowImage];
CGFloat arrowTransform = degrees;
//Rotate the Arrow
CGAffineTransform rotate = CGAffineTransformMakeRotation(arrowTransform);
[(UIImageView *)[arrowImageViews objectAtIndex:i] setTransform:rotate];
}
}
If anyone got an idea how to get the arrows rotation following smoothly the device rotation I would be very thankful.
In Android, the API provides the field of view angle:
Camera.Parameters.getHorizontalViewAngle()
Camera.Parameters.getVerticalViewAngle()
What's the equivalent in iOS?
I don't want to pre-write those values because it's not flexible.
I'm not entirely sure what "horizontal" and "vertical" mean in this context, but I think of two calculations, the rotation about the "z" axis (i.e. how level we are with the horizon in the photo), and how much it's tilted forward and backward (i.e. the rotation about the "x" axis, namely is it pointing up or down). You can do this using Core Motion. Just add it to your project and then you can do something like:
Make sure to import CoreMotion header:
#import <CoreMotion/CoreMotion.h>
Define a few class properties:
#property (nonatomic, strong) CMMotionManager *motionManager;
#property (nonatomic, strong) NSOperationQueue *deviceQueue;
Start the motion manager:
- (void)startMotionManager
{
self.deviceQueue = [[NSOperationQueue alloc] init];
self.motionManager = [[CMMotionManager alloc] init];
self.motionManager.deviceMotionUpdateInterval = 5.0 / 60.0;
[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;
// how much is it rotated around the z axis
CGFloat rotationAngle = atan2(y, x) + M_PI_2; // in radians
CGFloat rotationAngleDegrees = rotationAngle * 180.0f / M_PI; // in degrees
// how far it it tilted forward and backward
CGFloat r = sqrtf(x*x + y*y + z*z);
CGFloat tiltAngle = (r == 0.0 ? 0.0 : acosf(z/r); // in radians
CGFloat tiltAngleDegrees = tiltAngle * 180.0f / M_PI - 90.0f); // in degrees
}];
}];
}
When done, stop the motion manager:
- (void)stopMotionManager
{
[self.motionManager stopDeviceMotionUpdates];
self.motionManager = nil;
self.deviceQueue = nil;
}
I'm not doing anything with the values here, but you can save them in class properties which you can then access elsewhere in your app. Or you could dispatch UI updates back to the main queue right from here. A bunch of options.
Since this is iOS 5 and higher, if the app is supporting earlier versions you might also want to weakly link Core Motion then then check to see everything is ok, and if not, just realize that you're not going to be capturing the orientation of the device:
if ([CMMotionManager class])
{
// ok, core motion exists
}
And, in case you're wondering about my fairly arbitrary choice of twelve times per second, in the Event Handling Guide for iOS, they suggest 10-20/second if just checking the orientation of the device.
In iOS 7.0+, you can obtain FOV angle of a camera by reading this property.
https://developer.apple.com/documentation/avfoundation/avcapturedeviceformat/1624569-videofieldofview?language=objc
AVCaptureDevice *camera;
camera = ...
float fov = [[camera activeFormat] videoFieldOfView];
NSLog("FOV=%f(deg)", fov);
I need to calculate "facing" (it doesn't matter if it will be based on true north or magnetic one). As it can be seen on the iOS devices the CLHeading objects returned by the CLLocationManager gives us both the true and the magnetic heading by corresponding properties. Also, we can very easily see, that those values are related to the top of the device (the positive Y axis of the devices coordinate system) which is not good for my purposes.
What I actually need is to calculate the facing related to the screen of the device (Z axis) as I don't need the compass, but a king of AG application. The issue is when you rotate the device to landscape you get heading values to the left or to the right from your facing direction, which is what I need in the end.
As I know, I can get the magnetometer "raw" data (given to me in microtesla units with values from 128 to -128 for each device axis) along with the gyroscope "raw" data ( which comes in three types: Euler angels, Rotation matrix or Quaternion). What I need is to know, which calculations I need to apply to those to get the "facing" direction instead of "heading".
I've made it a while ago and because I see no answers, I've decided to put my solution here for those who'll search answer for the same question...
_motionManager = [[CMMotionManager alloc]init];
if (_motionManager.gyroAvailable) {
_motionManager.deviceMotionUpdateInterval = 1.0/20.0;
[_motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue]
withHandler:^(CMDeviceMotion *motion, NSError *error)
{
CMAcceleration gravity = motion.gravity;
CGPoint tiltVector = CGPointMake(-gravity.x, -gravity.y);
_tiltAngle = [self angleYAxisToVector:tiltVector];
CLLocationDirection heaqding = [[SVSession sharedSession] heading].trueHeading;
double newHeading = fmod(heaqding + _tiltAngle, 360.0);
self.azimuth = degreesToRadian(newHeading);
[self updateLocations]; //this function updates my ui for the new heading
}];
} else {
NSLog(#"No gyroscope on device.");
[_motionManager release],_motionManager = nil;
}
And here are some additional snippets that may help to understand this example:
-(double)angleYAxisToVector:(CGPoint)vector{
double dX = vector.x;
double dY = vector.y;
if(dY == 0){
if(dX > 0){
return 0.0;
}else{
if(dX < 0){
return 180.0;
}else{
return -1;
}
}
}
double beta = radiansToDegrees(atan(dX/dY));
double angle;
if(dX > 0){
if (dY < 0){
angle = 180 + beta;
}else{
angle = beta;
}
}else{
if (dY < 0){
angle = 180 + beta;
}else{
angle = 360 + beta;
}
}
// NSLog(#"angle = %f, normalized = %f",beta,angle);
return angle;
}
#define degreesToRadian(x) (M_PI * (x) / 180.0)
#define radiansToDegrees(x) ((x) * 180.0 / M_PI)
#define degreesToRadians(x) degreesToRadian(x)
#define radiansToDegree(x) radiansToDegrees(x)
Happy coding...