利用AVFoundation自定义相机

如果只是简单的调用相机来拍照的话苹果为我们提供了UIImagePickerController这个简单的拍照功能的实现。但是它的界面是固定的。当你想自定义拍照界面以及使用其他更高级的功能的时候就需要用到AVFoundation这个框架。

AVFoundation 相关类

AVFoundation 框架基于以下几个类实现图像捕捉 ,通过这些类可以访问来自相机设备的原始数据并控制它的组件。

  • AVCaptureDevice 是关于相机硬件的接口。它被用于控制硬件特性,诸如镜头的位置、曝光、闪光灯等。
  • AVCaptureDeviceInput 提供来自设备的数据。
  • AVCaptureOutput 是一个抽象类,描述 capture session 的结果。以下是三种关于静态图片捕捉的具体子类:
    AVCaptureStillImageOutput 用于捕捉静态图片
    AVCaptureMetadataOutput 启用检测人脸和二维码
    AVCaptureVideoDataOutput (原文显示为AVCaptureVideoOutput,但是我用到的是这个)为实时预览图提供原始帧
  • AVCaptureSession 管理输入与输出之间的数据流,以及在出现问题时生成运行时错误。
  • AVCaptureVideoPreviewLayer是 CALayer的子类,可被用于自动显示相机产生的实时图像。它还有几个工具性质的方法,可将 layer 上的坐标转化到设备上。它看起来像输出,但其实不是。另外,它拥有 session (outputs 被 session 所拥有)。

以上引用自这篇文章

使用

初始化

  1. 如上文所说AVCaptureSession是管理输入输出的类。担任管理调度的角色。因此需要先创建它
_session = [[AVCaptureSession alloc] init];
_session.sessionPreset = AVCaptureSessionPresetPhoto;

使用AVCaptureSessionPresetPhoto会自动设置为最适合的拍照配置。比如它可以允许我们使用最高的感光度 (ISO) 和曝光时间,基于相位检测的自动对焦, 以及输出全分辨率的 JPEG 格式压缩的静态图片。

  1. AVCaptureDevice是用来控制硬件的接口。在拍照的时候我们需要一个摄像头的设备。因此我们需要遍历所有设备找到相应的摄像头。
// 用来返回是前置摄像头还是后置摄像头
-(AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition) position {
    // 返回和视频录制相关的所有默认设备
    NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    // 遍历这些设备返回跟position相关的设备
    for (AVCaptureDevice *device in devices) {
        if ([device position] == position) {
            return device;
        }
    }
    return nil;
}
  1. AVCaptureDeviceInput,找到相应的摄像头之后就能通过这个获取来自硬件的数据。
// 后置摄像头输入
-(AVCaptureDeviceInput *)backCameraInput {
    if (_backCameraInput == nil) {
        NSError *error;
        _backCameraInput = [[AVCaptureDeviceInput alloc] initWithDevice:[self backCamera] error:&error];
        if (error) {
            NSLog(@"获取后置摄像头失败~");
        }
    }
    return _backCameraInput;
}

获得到AVCaptureDeviceInput后加入到AVCaptureSession上

   // 添加后置摄像头的输入
      if ([_session canAddInput:self.backCameraInput]) {
          [_session addInput:self.backCameraInput];
          self.currentCameraInput = self.backCameraInput;
        }
  1. AVCaptureOutput获取输出数据的类。拍照的时候用的是AVCaptureStillImageOutput。它是用来捕获静态图片的类。
// 静态图像输出
-(AVCaptureStillImageOutput *)stillImageOutput
{
    if (_stillImageOutput == nil) {
        _stillImageOutput = [[AVCaptureStillImageOutput alloc] init];
        NSDictionary *outputSettings = [[NSDictionary alloc] initWithObjectsAndKeys:AVVideoCodecJPEG,AVVideoCodecKey,nil];
        _stillImageOutput.outputSettings = outputSettings;
    }
    return _stillImageOutput;
}
// 添加静态图片输出(拍照)
        if ([_session canAddOutput:self.stillImageOutput]) {
            [_session addOutput:self.stillImageOutput];
        }

AVCaptureMetadataOutput用来进行人脸或者二维码一维码的识别。不同于AVCaptureStillImageOutput,它需要在加载如session中之后才能进行设置,不然会报错。

// 添加元素输出(识别)
        if ([_session canAddOutput:self.metaDataOutput]) {
            [_session addOutput:self.metaDataOutput];
            // 人脸识别
            [_metaDataOutput setMetadataObjectTypes:@[AVMetadataObjectTypeFace]];
            // 二维码,一维码识别
            //        [_metaDataOutput setMetadataObjectTypes:@[AVMetadataObjectTypeCode39Code,AVMetadataObjectTypeCode128Code,AVMetadataObjectTypeCode39Mod43Code,AVMetadataObjectTypeEAN13Code,AVMetadataObjectTypeEAN8Code,AVMetadataObjectTypeCode93Code]];
            [_metaDataOutput setMetadataObjectsDelegate:self queue:self.sessionQueue];
        }

AVCaptureVideoDataOutput用来录制视频或者从输出数据流捕捉单一的图像帧。比如进行身份证和手机号识别的过程中就需要不断从数据流中获取图像,这个时候需要用到它。

// 视频输出
-(AVCaptureVideoDataOutput *)videoDataOutput {
    if (_videoDataOutput == nil) {
        _videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
        [_videoDataOutput setSampleBufferDelegate:self queue:self.sessionQueue];
        NSDictionary* setcapSettings = [NSDictionary dictionaryWithObjectsAndKeys:
                                        [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange], kCVPixelBufferPixelFormatTypeKey,
                                        nil];
        _videoDataOutput.videoSettings = setcapSettings;
    }
    return _videoDataOutput;
}

操作

  1. 启动相机
    在session和相机设备中完成的操作都利用block来调用。因此这些操作都建议分配到后台串行队列中。
dispatch_async(self.sessionQueue, ^{
        [self.session startRunning];
    });
  1. 拍照
    利用AVCaptureStillImageOutput来进行拍照,开启闪光灯的话会在拍照后关闭,有快门声音。注意,通过拍照方法获取的照片旋转了90度,并且其大小并不是预览窗口的大小,需要进行截取。
#pragma mark - 拍照
-(void)takePhotoWithImageBlock:(void (^)(UIImage *, UIImage *, UIImage *))block
{
    __weak typeof(self) weak = self;
    [self.stillImageOutput captureStillImageAsynchronouslyFromConnection:[self imageConnection] completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {
        if (!imageDataSampleBuffer) {
            return ;
        }
        NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
        UIImage *originImage = [[UIImage alloc] initWithData:imageData];
        
        CGFloat squareLength = weak.previewLayer.bounds.size.width;
        CGFloat previewLayerH = weak.previewLayer.bounds.size.height;
        CGSize size = CGSizeMake(squareLength * 2, previewLayerH * 2);
        UIImage *scaledImage = [originImage resizedImageWithContentMode:UIViewContentModeScaleAspectFill bounds:size interpolationQuality:kCGInterpolationHigh];

        CGRect cropFrame = CGRectMake((scaledImage.size.width - size.width) / 2, (scaledImage.size.height - size.height) / 2, size.width, size.height);
        UIImage *croppedImage = [scaledImage croppedImage:cropFrame];

        UIDeviceOrientation orientation = [UIDevice currentDevice].orientation;
        if (orientation != UIDeviceOrientationPortrait) {
            CGFloat degree = 0;
            if (orientation == UIDeviceOrientationPortraitUpsideDown) {
                degree = 180;// M_PI;
            } else if (orientation == UIDeviceOrientationLandscapeLeft) {
                degree = -90;// -M_PI_2;
            } else if (orientation == UIDeviceOrientationLandscapeRight) {
                degree = 90;// M_PI_2;
            }
            croppedImage = [croppedImage rotatedByDegrees:degree];
            scaledImage = [scaledImage rotatedByDegrees:degree];
            originImage = [originImage rotatedByDegrees:degree];
        }
        if (block) {
            block(originImage,scaledImage,croppedImage);
        }
    }];
}
  1. 识别
    利用AVCaptureMetadataOutputObjectsDelegate方法筛选相应的元素对象
#pragma mark - AVCaptureMetadataOutputObjectsDelegate
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    if (self.faceRecognition) {
        for(AVMetadataObject *metadataObject in metadataObjects) {
            if([metadataObject.type isEqualToString:AVMetadataObjectTypeFace]) {
                AVMetadataObject *transform = [self.previewLayer transformedMetadataObjectForMetadataObject:metadataObject];
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self showFaceImageWithFrame:transform.bounds];
                });
            }
        }
    }
}
  1. 从输出数据流捕捉单一的图像帧
    利用AVCaptureVideoDataOutputSampleBufferDelegate获取相应的数据流,然后获取某一帧。与拍照一样,这种方式获取的图片依然角度大小不正确,要进行相应的处理。
#pragma mark - 从输出数据流捕捉单一的图像帧
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
    if (self.isStartGetImage) {
        UIImage *originImage = [self imageFromSampleBuffer:sampleBuffer];
        CGFloat squareLength = self.previewLayer.bounds.size.width;
        CGFloat previewLayerH = self.previewLayer.bounds.size.height;
        CGSize size = CGSizeMake(squareLength*2, previewLayerH*2);
        UIImage *scaledImage = [originImage resizedImageWithContentMode:UIViewContentModeScaleAspectFill bounds:size interpolationQuality:kCGInterpolationHigh];
        CGRect cropFrame = CGRectMake((scaledImage.size.width - size.width) / 2, (scaledImage.size.height - size.height) / 2, size.width, size.height);
        UIImage *croppedImage = [scaledImage croppedImage:cropFrame];
        UIDeviceOrientation orientation = [UIDevice currentDevice].orientation;
        if (orientation != UIDeviceOrientationPortrait) {
            CGFloat degree = 0;
            if (orientation == UIDeviceOrientationPortraitUpsideDown) {
                degree = 180;// M_PI;
            } else if (orientation == UIDeviceOrientationLandscapeLeft) {
                degree = -90;// -M_PI_2;
            } else if (orientation == UIDeviceOrientationLandscapeRight) {
                degree = 90;// M_PI_2;
            }
            croppedImage = [croppedImage rotatedByDegrees:degree];
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            if (self.getimageBlock) {
                self.getimageBlock(croppedImage);
                self.getimageBlock = nil;
            }
        });
        self.isStartGetImage = NO;
    }
}
  1. 对焦
    通过设置AVCaptureDevice的AVCaptureFocusMode来进行设置对焦模式。

AVCaptureFocusMode
是个枚举,描述了可用的对焦模式:
Locked 指镜片处于固定位置
AutoFocus指一开始相机会先自动对焦一次,然后便处于 Locked
模式。
ContinuousAutoFocus 指当场景改变,相机会自动重新对焦到画面的中心点。

可以通过变换 “感兴趣的点 (point of interest)” 来设定另一个区域。这个点是一个 CGPoint,它的值从左上角{0,0}到右下角 {1,1},{0.5,0.5} 为画面的中心点。通常可以用视频预览图上的点击手势识别来改变这个点,想要将 view 上的坐标转化到设备上的规范坐标,我们可以使用[self.previewLayer captureDevicePointOfInterestForPoint:devicePoint]转换view上的坐标到感兴趣的点。(在进行二维码识别的时候也可以通过设置这个调整识别的重点位置)

总结

总的来说自定义相机能做的事情还是挺多的,还能够对曝光、白平衡进行调节。项目中暂时没用到,用到再进行补充。
注意点
1. 通过拍照方法获取的照片旋转了90度,并且其大小并不是预览窗口的大小,需要进行裁剪
2. 所有对相机进行的操作建议都放到后台进行,包括切换相机之类
3. 更改相机配置时需要先锁定相机,更改完成后再解开锁定

我是demo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容