禁止在 AVFoundation 中除竖屏以外其他方向拍摄图像

12

我正在使用 AVFoundation 来展示相机。

我希望防止相机本身旋转,以便观看者只能在纵向模式下看到相机,并且图像只能以纵向模式拍摄。

我定义了 Supported Interface Orientation 仅支持纵向模式,视图本身也仅以纵向模式显示,但是相机会随着设备方向而旋转。

如何强制 AVFoundation 相机只以纵向模式显示和捕获图像,就像UIViewController一样?

我的相机设置代码:

AVCaptureVideoPreviewLayer* lay = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.sess];
UIView *view = [self videoPreviewView];
CALayer *viewLayer = [view layer];
[viewLayer setMasksToBounds:YES];
CGRect bounds = [view bounds];
[lay setFrame:bounds];
if ([lay respondsToSelector:@selector(connection)])
 {
   if ([lay.connection isVideoOrientationSupported])
   {
      [lay.connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
    }
 }
 [lay setVideoGravity:AVLayerVideoGravityResizeAspectFill];
 [viewLayer insertSublayer:lay below:[[viewLayer sublayers] objectAtIndex:0]];
 self.previewLayer = lay;

不确定你的问题。没有什么能强制用户不旋转他的iPhone。所以,你想在界面为竖屏时允许录制视频吗?或者更好的方式是:设备只能处于垂直位置? - Tricertops
不 - 我希望相机不要旋转到纵向 - 就像我可以强制UiViewcontroller保持纵向位置一样。 - Dejell
我不理解问题所在。在iOS6中,如果我将支持的界面方向设置为仅竖屏(在目标摘要中),我的相机预览会保持不变,与界面的其余部分一样,无论设备的方向如何。你的相机预览不是这样吗?显然,相机预览图像会随着设备旋转而旋转,以使地平线保持水平 - 这是您想要禁用的吗,以便地平线始终按照纵向视图的方式固定,无论方向如何? - foundry
是的,这正是我想要做的。 - Dejell
@HeWas,而且拍摄的照片只能是竖屏。 - Dejell
3个回答

10

根据我对您问题的理解(与其他回答有所不同),以下是部分答案。

您已将应用程序锁定为纵向方向。因此,状态栏始终位于手机的纵向顶部,而不管手机的方向如何。这成功地锁定了您的界面,包括您的AVCapture接口。但是,您还希望锁定从相机获取的原始图像数据流,以便图像水平线始终平行于状态栏。

这最好需要 连续地 进行 - 这样,如果您将相机放在45度角,图像将被逆时针旋转45度。否则,大多数情况下,图像将无法正确对齐(另一种选择是图像始终不对齐,直到您90度的方向切换更新,这将使图像旋转90度)。

为此,您需要使用Core Motion和加速度计。您想要获取手机Y轴相对于真正垂直的角度,并相应地旋转图像。有关几何详细信息,请参见此处:

iPhone orientation -- how do I figure out which way is up?

使用Core Motion,从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 30度旋转 c 横屏

手机持有方式 (a) 竖屏; (b) 旋转 ~30度; (c) 横屏
.

你可能会发现图像变化有点跳动,并且设备移动和视图之间存在一定的延迟。您可以调整updateInterval,深入了解其他Core Motion技巧以减缓移动。(我没有处理手机完全颠倒的情况,如果您将相机正面朝下或者正面朝上,结果是未定义的 使用更新的代码/使用atan2已经修复此问题)。

现在方向已经相当正确,但是您的图像不适合您的视图。这样做的原因是相机原始数据的格式由其传感器阵列的物理尺寸确定。解决方法是缩放图像,使您在所有角度都有足够的多余图像数据,以使您能够裁剪图像以适合所需的竖屏格式。

在界面构建器中:

  • 将预览图层的视图设置为正方形,居中于其父视图,宽度和高度等于可见图像区域的对角线(sqrt(width2+height2))

或者在代码中:

- (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};
}
如果您在代码中这样做,resizeCameraView应该可以在viewDidLoad中调用。确保self.videoPreviewView是正确视图的IBOutlet引用。

现在当您拍照时,您将捕获来自相机阵列的整个“原始”图像数据,它将以横向格式保存。它将保存一个方向标志以进行显示旋转。但是,您可能希望将照片保存为屏幕上看到的内容。这意味着您需要旋转和裁剪照片以匹配您的屏幕视图,然后删除其方向元数据。这就是要解决的问题('partial answer'的另一部分):我怀疑您可能会发现这种方法并不能完全满足您的需求(我认为您真正想要的是一个相机传感器,可以针对设备旋转而旋转以保持地平线稳定)。

更新
更改startAccelerometerUpdates,从acos获取角度改为从atan2获取,更平滑,考虑了所有方向而无需微调

更新2
根据您的评论,似乎您旋转的预览层卡住了?我无法复制您的错误,这一定是代码或设置中的其他地方。为了让您可以检查干净的代码,我已将我的解决方案添加到Apple的AVCam项目中,因此您可以与其进行比较。以下是要做的内容:

  • 向AVCam添加Core Motion框架。


在AVCamViewController.m中:

  • #import <CoreMotion/CoreMotion.h>
  • 添加我的startAccelerometerUpdates方法
  • 添加我的resizeCameraView方法(将这两个方法都放在类文件的顶部,否则您可能会感到困惑,因为该文件中有多个@implementation
  • viewDidLoad中添加: [self resizeCameraView]; (它可以是该方法的第一行)
  • 在@interface中添加属性
    @property (strong, nonatomic) CMMotionManager* coreMotionManager
    (它不需要是属性,但是我的方法假设它存在,因此如果您不添加它,则必须修改我的方法)。


startAccelerometerUpdates中更改此行:
    self.previewLayer.transform = rotate;  
To:
    self.captureVideoPreviewLayer.transform = rotate;

此外,在AVCamViewController.xib的对象列表中,将videoPreview视图移动到ToolBar之上(否则当您放大它时,会覆盖控件)。

确保禁用旋转 - 对于iOS<6.0,已经是这样,但对于6.0+,您需要在目标摘要中选择仅支持纵向。

我认为这是我对AVCam所做的完整更改列表,并且旋转/方向都非常顺利地工作。我建议你尝试做同样的事情。如果您可以使其顺畅地工作,那么您就知道代码中还有其他故障。如果您仍然发现旋转固定不变,我很想了解更多关于您的硬件和软件环境的信息,例如您正在测试哪些设备。

我正在XCode 4.6 / OSX10.8.2上进行编译,并进行以下测试:

- iPhone4S  /  iOS5.1  
- iPhone3G  /  iOS6.1  
- iPad mini /  iOS6.1 

所有结果都是流畅和准确的。


我再次尝试了您的解决方案。我更新了我的答案,展示了在尝试后我的屏幕看起来是什么样子,以及当我将iPhone向右旋转但保持直立时的情况。我没有在AVCAM上尝试过,但我的代码中没有太多防止它的内容。 - Dejell
@Odelya,看起来你只是没有扩大相机视图(resizeCameraView方法没有被正确调用)。 - foundry
如果我拿着手机时没有向右旋转,它就不会填充正方形。这是我需要通过通知调用的东西吗?(只有在我向右旋转时才会发生) - Dejell
@Odelya,在 resizeCameraView 函数中,你是否已经将我的代码中的属性名称 - self.cameraPreview - 更改为你的代码使用的 - self.videoPreviewView - foundry
1
你的意思是用resizeCameraView方法替换我在问题中设置图像的代码? - Dejell
显示剩余2条评论

2
我猜你需要使用这种方法来限制相机旋转。
AVCaptureConnection *videoConnection = [CameraVC connectionWithMediaType:AVMediaTypeVideo fromConnections:[imageCaptureOutput connections]];
if ([videoConnection isVideoOrientationSupported])
{
    [videoConnection setVideoOrientation:[UIDevice currentDevice].orientation];
}

假设您的预览层被定义为属性,可以使用以下方法:
[self.previewLayer setOrientation:[[UIDevice currentDevice] orientation]];

在您的情况下,您可以将[[UIDevice currentDevice] orientation]替换为UIInterfaceOrientationPortrait

编辑过的

尝试在实际需要时添加预览层。 例如:

preview = [[self videoPreviewWithFrame:CGRectMake(0, 0, 320, 480)] retain];
[self.view addSubview:preview];

视频预览功能,使用“videoPreviewWithFrame”函数实现。

- (UIView *) videoPreviewWithFrame:(CGRect) frame 
{
  AVCaptureVideoPreviewLayer *tempPreviewLayer = [[AVCaptureVideoPreviewLayer alloc]initWithSession:[self captureSession]];
  [tempPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
  tempPreviewLayer.frame = frame;

  UIView* tempView = [[UIView alloc] init];
  [tempView.layer addSublayer:tempPreviewLayer];
  tempView.frame = frame;

  [tempPreviewLayer autorelease];
  [tempView autorelease];
  return tempView;
}

我编辑了我的问题。1. 我已经在视频连接中将方向设置为纵向。关于预览层 - setOrientation已被弃用,所以我需要编写:self.previewLayer.connection,这与我已经在做的相同,对吗? - Dejell
我已经编辑了我的答案。请尝试实现我在编辑行后所写的内容。 - Pushpak Narasimhan
如果 ([captureVideoPreviewLayer respondsToSelector:@selector(connection)]) { 如果 ([captureVideoPreviewLayer.connection isVideoOrientationSupported]) { [captureVideoPreviewLayer.connection setVideoOrientation:self.interfaceOrientation]; } } 否则 { // 在6.0中已弃用;这里是为了向后兼容 如果 ([captureVideoPreviewLayer isOrientationSupported]) { [captureVideoPreviewLayer setOrientation:self.interfaceOrientation]; }
}
- Pushpak Narasimhan
您正在设置videoOrientation,这只适用于iOS6,因此为了向后兼容,请在else部分使用setOrientation。 - Pushpak Narasimhan
看到He Was在我的问题下发表了评论 - 我想禁用相机在用户旋转设备时的旋转。 - Dejell
显示剩余2条评论

2
假设您的预览层添加到视图控制器视图中。在 viewDidLoad 中执行以下操作:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationChanged:) name:UIDeviceOrientationDidChangeNotification object:nil];

并将选择器定义为:
- (void)orientationChanged:(NSNotification*)notification {
    UIInterfaceOrientation interfaceOrientation = [UIApplication sharedApplication].statusBarOrientation;
    if ([self.previewlayer respondsToSelector:@selector(orientation)]) {
        //for iOS5
        if (interfaceOrientation != UIInterfaceOrientationPortrait) {
            self.previewlayer.orientation = (AVCaptureVideoOrientation)UIInterfaceOrientationPortrait;
        }
    } else {
        //for iOS6
        if (interfaceOrientation != UIInterfaceOrientationPortrait) {
            self.previewlayer.connection.videoOrientation = (AVCaptureVideoOrientation)UIInterfaceOrientationPortrait;
        }
    }
}
注意:tempPreviewLayer放入属性self.previewlayer中。

这将在设备方向更改时强制预览图层处于纵向位置。

编辑

您还可以在视图控制器的`shouldAutoRotate`方法中添加此内容。

    - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
    {
        // Return YES for supported orientations
        if ([self.previewlayer respondsToSelector:@selector(orientation)]) {
            if (interfaceOrientation != UIInterfaceOrientationPortrait) {
                self.previewlayer.orientation = (AVCaptureVideoOrientation)UIInterfaceOrientationPortrait;
            }
        } else {
            if (interfaceOrientation != UIInterfaceOrientationPortrait) {
                self.previewlayer.connection.videoOrientation = (AVCaptureVideoOrientation)UIInterfaceOrientationPortrait;
            }
        }

        return (interfaceOrientation == UIInterfaceOrientationPortrait);
    }

针对iOS6及以上版本,请重写这两个并进行检查。

-(NSUInteger)supportedInterfaceOrientations {
    //UIInterfaceOrientation interfaceOrientation = [UIApplication sharedApplication].statusBarOrientation;
    //return (
    //interfaceOrientation == UIInterfaceOrientationLandscapeLeft |
    //interfaceOrientation == UIInterfaceOrientationLandscapeRight);
    return UIInterfaceOrientationMaskLandscape;//(UIInterfaceOrientationLandscapeLeft | UIInterfaceOrientationLandscapeRight);
}

- (BOOL)shouldAutorotate {
    return YES;
}

在这两个方法中的 return 前添加代码,并查看给出的通知是否在旋转设备时被调用。

你试过了吗?在添加通知之前加上[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];。这对我来说完美地解决了问题,它锁定了我想要的预览层方向。 - devluv
将tempPreviewLayer放入属性中的注释是什么?你是什么意思? - Dejell
在你的 videoPreviewWithFrame 方法中,在 tempPreviewLayer.frame = frame; 这一行之后,使用 self.previewlayer = tempPreviewLayer 来获取属性 @property (nonatomic, retain) AVCaptureVideoPreviewLayer *previewlayer;,然后在接下来的代码中到处使用 self.previewlayer - devluv
是的 - 我尝试了调用该方法的代码,但预览层没有锁定在竖屏模式。我正在使用iOS6。 - Dejell
self.previewlayer = tempPreviewLayer? tempPreviewLayer是什么? - Dejell
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接