利用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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容