Here is a partial answer based on my understanding of your question (which differs from the other answers you have had).
You have the app locked to portrait orientation. So the status bar is always at the portrait top of the phone regardless of the phone's orientation. This successfully locks your interface, including your AVCapture interface. But you want to also lock the raw image feed from the camera so that the image horizon is always parallel with the status bar.
This will ideally need to be done continuously - so that if you have the camera at a 45degree angle the image will be counter-rotated 45 degrees. Otherwise, most of the time, the image will not be aligned correctly (the alternative is that it is always out of line until your 90degree orientation switch updates, which would swivel the image 90 degrees).
To do this you need to use Core Motion and the accelerometer. You want to get angle of the phone's Y-axis to true vertical and rotate the image accordingly. See here for geometry details:
iPhone orientation -- how do I figure out which way is up?
Using Core Motion, trigger this method from viewDidLoad
- (void)startAccelerometerUpdates {
self.coreMotionManager = [[CMMotionManager alloc] init];
if ([self.coreMotionManager isAccelerometerAvailable] == YES) {
CGFloat updateInterval = 0.1;
// Assign the update interval to the motion manager
[self.coreMotionManager setAccelerometerUpdateInterval:updateInterval];
[self.coreMotionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue]
withHandler: ^(CMAccelerometerData *accelerometerData, NSError *error) {
CGFloat angle = -atan2( accelerometerData.acceleration.x,
accelerometerData.acceleration.y)
+ M_PI ;
CATransform3D rotate = CATransform3DMakeRotation(angle, 0, 0, 1);
self.previewLayer.transform = rotate;
}];
}
}
a b c
phone held (a) portrait; (b) rotated ~30deg; (c) landscape
.
You may find this is a little jumpy, and there is a bit of a lag between the device movement and the view. You can play with the updateInterval, and get in deeper with other Core Motion trickery to dampen the movement. (I have not treated the case of the phone being exactly upside down, and if you hold the camera face down or face up, the result is undefined fixed with updated code/ use of atan2
).
Now orientation is reasonably correct, but your image does not fit your view. There is not a lot you can do about this as the format of the raw camera feed is fixed by the physical dimensions of it's sensor array. The workaround is to zoom the image so that you have enough excess image data at all angles to enable you to crop the image to fit the portrait format you want.
Either in Interface Builder:
- set your previewLayer's view to square centered on it's superview, with width and height equal to the diagonal of the visible image area (sqrt (width2+height2)
Or in code:
- (void)resizeCameraView
{
CGSize size = self. videoPreviewView.bounds.size;
CGFloat diagonal = sqrt(pow(size.width,2)+pow(size.height,2));
diagonal = 2*ceil(diagonal/2); //rounding
self.videoPreviewView.bounds = (CGRect){0,0,diagonal,diagonal};
}
If you do this in code, resizeCameraView
should work if you call it from your viewDidLoad
. Make sure that self.videoPreviewView
is your IBOutlet reference to the correct view.
Now when you take a photo, you will capture the whole of the 'raw' image data from the camera's array, which will be in landscape format. It will be saved with an orientation flag for display rotation. But what you may want is to save the photo as seen onscreen. This means that you will have to rotate and crop the photo to match your onscreen view before saving it, and remove it's orientation metadata. That's for you to work out (the other part of the 'partial answer'): I suspect you might decide that this whole approach doesn't get you what you want (I think what you'd really like is a camera sensor that hardware-rotates against the rotation of the device to keep the horizon stable).
update
changed startAccelerometerUpdates
to get angle from atan2
instead of acos
, smoother and takes account of all directions without fiddling
update 2
From your comments, it seems your rotated preview layer is getting stuck? I cannot replicate your error, it must be some other place in your code or settings.
So that you can check with clean code, I have added my solution into Apple's AVCam project, so you can check it against that. Here is what to do:
- add the Core Motion framework to AVCam.
In AVCamViewController.m
#import <CoreMotion/CoreMotion.h>
- add my
startAccelerometerUpdates
method
- add my
resizeCameraView
method (stick both of these methods near the top of the class file or you may get confused, there are more than one @implementation
s in that file)
- add the line:
[self resizeCameraView];
to viewDidLoad
(it can be the first line of the method)
- add the property
@property (strong, nonatomic) CMMotionManager* coreMotionManager
to the @interface (it doesn't need to be a property, but my method assumes it exists, so if you don't add it you will have to modify my method instead).
In startAccelerometerUpdates
change this line:
self.previewLayer.transform = rotate;
to:
self.captureVideoPreviewLayer.transform = rotate;
also, in the Objects list in AVCamViewController.xib
, move the videoPreview View above the ToolBar (otherwise when you enlarge it you cover the controls)
Be sure to disable rotations - for iOS<6.0, that is already true, but for 6.0+ you need to select just portrait in supported orientations in the target summary.
I think that is a complete list of changes I made to AVCam, and the rotation/orientation is all working very well. I suggest you try doing the same. If you can get this to work smoothly, you know there is some other glitch in your code somewhere. If you still find your rotations stick, I would be curious to know more about your hardware and software environment such as which devices are you testing on.
I am compiling on XCode 4.6/OSX10.8.2, and testing on:
- iPhone4S / iOS5.1
- iPhone3G / iOS6.1
- iPad mini / iOS6.1
All results are smooth and accurate.