iOS
avfoundation
カメラ
UIKit
Swift
SwiftDay 1

iOS10でのAVCapturePhotoOutputを用いたカメラ実装📷

More than 1 year has passed since last update.

はじめに

iOS10から写真データ取得に用いていたAVCaptureStillImageOutputがdeprecatedになり、代わりに追加されたAVCapturePhotoOutputを使うようになりました。
ですが、既存のライブラリを見ても未だAVCaptureStillImageOutputを使ってるものがほとんどです。
これらを書き直そうと思いぐぐったところ、AVCapturePhotoOutputについては現状あまり知見がありませんでした(10/27現在)。

そこで、
"知見がないなら作ればいいんやで🤔"
という気持ちで、AVCapturePhotoOutputを用いた実装法をご紹介したいと思います!

現状考えられるカメラの実装方法とメリット・デメリット

(1)AVCaptureStillImageOutput(iOS10からdeprecated)

(2) AVCapturePhotoOutput

  • API Reference - AVCapturePhotoOutput
  • LivePhotoなどの撮影が可能
  • AVCaptureStillImageOutputの後継と考えられるが、セッティング周りはAVCapturePhotoSettingsに分けられているなど、AVCaptureStillImageOutputとは勝手が異なる。
  • 知見があまり無い
  • UIImagePickerControllerと比べると実装が複雑

(3) UIImagePickerController

以上の3つが現状考えられるカメラの実装方法とメリット・デメリットです。

では、上記3種類の実装・比較を行っていきます。

環境

macOS Sierra 10.12.1
Xcode8.1
Swift3.0
iOS10.1.1 (iPhoneSE)

実装

まず3つのソースコード共通で、StoryboardにUIImageView、UIButtonを配置する。

image

次に(忘れる前に)Info.plistに以下の2つの要素を追加する。

image

参照︰[iOS 10] ユーザーのプライバシー情報にアクセスするときは理由を書こう

ここから3種類コードを書きます↓

(1)AVCaptureStillImageOutputの場合

AVCaptureStillImageOutput.swift
import UIKit
import AVFoundation

class ViewController: UIViewController {
    @IBOutlet weak var imageView: UIView!

    var captureSession: AVCaptureSession!
    var cameraDevices: AVCaptureDevice!
    var imageOutput: AVCaptureStillImageOutput!

    @IBAction func takePhoto(_ sender: Any) {
        let captureVideoConnection = imageOutput.connection(withMediaType: AVMediaTypeVideo)
        self.imageOutput.captureStillImageAsynchronously(from: captureVideoConnection) { (imageDataBuffer, error) -> Void in
            let capturedImageData: NSData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(imageDataBuffer) as NSData
            let Image: UIImage = UIImage(data: capturedImageData as Data)!
            UIImageWriteToSavedPhotosAlbum(Image, self, nil, nil)
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        captureSession = AVCaptureSession()

        let devices = AVCaptureDevice.devices()
        for device in devices! {
            if (device as AnyObject).position == AVCaptureDevicePosition.back {
                cameraDevices = device as! AVCaptureDevice
            }
        }

        let videoInput: AVCaptureInput!
        do {
            videoInput = try AVCaptureDeviceInput.init(device: cameraDevices)
        } catch {
            videoInput = nil
        }

        captureSession.addInput(videoInput)
        imageOutput = AVCaptureStillImageOutput()
        captureSession.addOutput(imageOutput)
        let captureVideoLayer: AVCaptureVideoPreviewLayer = AVCaptureVideoPreviewLayer.init(session: captureSession)
        captureVideoLayer.frame = self.imageView.bounds
        captureVideoLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
        self.imageView.layer.addSublayer(captureVideoLayer)
        captureSession.startRunning()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

今までのコードの書き方だとこんな感じだと思います。
大抵のライブラリもまだこの書き方が多いイメージ🤔

AVCaptureStillImageOutputのサンプル

(2) AVCapturePhotoOutputの場合

AVCapturePhotoOutput.swift
import UIKit
import AVFoundation

class ViewController: UIViewController,AVCapturePhotoCaptureDelegate {
    @IBOutlet weak var imageView: UIImageView!

    var captureSesssion: AVCaptureSession!
    var stillImageOutput: AVCapturePhotoOutput!
    //    ↑ 書き換え AVCaptureStillImageOutput → AVCapturePhotoOutput

    @IBAction func takePhoto(_ sender: Any){
        let settingsForMonitoring = AVCapturePhotoSettings()
        settingsForMonitoring.flashMode = .auto
        settingsForMonitoring.isAutoStillImageStabilizationEnabled = true
        settingsForMonitoring.isHighResolutionPhotoEnabled = false
        stillImageOutput?.capturePhoto(with: settingsForMonitoring, delegate: self)
    }
    //    AVCapturePhotoSettingsという新しいClassがAVCapturePhotoOutputと一緒に追加された。
    //    フラッシュなどの細かい設定はAVCapturePhotoSettingsで行う

    override func viewDidLoad() {
        super.viewDidLoad()
        captureSesssion = AVCaptureSession()
        captureSesssion.sessionPreset = AVCaptureSessionPreset1920x1080
        stillImageOutput = AVCapturePhotoOutput()

        let device = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)

//        今回は📷のFront、Back、Dualの指定はしていないが、するとしたらこんな感じ
//        do {
//            var defaultVideoDevice: AVCaptureDevice?
//            defaultVideoDevice = dualCameraDevice
//        }
//            else if let backCameraDevice = AVCaptureDevice.defaultDevice(withDeviceType: .builtInWideAngleCamera, mediaType: AVMediaTypeVideo, position: .back) {
//                defaultVideoDevice = backCameraDevice
//            }
//            else if let frontCameraDevice = AVCaptureDevice.defaultDevice(withDeviceType: .builtInWideAngleCamera, mediaType: AVMediaTypeVideo, position: .front) {
//               defaultVideoDevice = frontCameraDevice
//            }
//            
//            let videoDeviceInput = try AVCaptureDeviceInput(device: defaultVideoDevice)
//            
//            if session.canAddInput(videoDeviceInput) {
//                session.addInput(videoDeviceInput)
//                self.videoDeviceInput = videoDeviceInput
//                
//                DispatchQueue.main.async {
//                    let statusBarOrientation = UIApplication.shared.statusBarOrientation
//                    var initialVideoOrientation: AVCaptureVideoOrientation = .portrait
//                    if statusBarOrientation != .unknown {
//                        if let videoOrientation = statusBarOrientation.videoOrientation {
//                            initialVideoOrientation = videoOrientation
//                        }
//                    }
//                    
//                    self.previewView.videoPreviewLayer.connection.videoOrientation = initialVideoOrientation
//                }
//            }
//            else {
//                setupResult = .configurationFailed
//                session.commitConfiguration()
//                return
//            }
//        }

        do {
            let input = try AVCaptureDeviceInput(device: device)
            if (captureSesssion.canAddInput(input)) {
                captureSesssion.addInput(input)
                if (captureSesssion.canAddOutput(stillImageOutput)) {
                    captureSesssion.addOutput(stillImageOutput)
                    captureSesssion.startRunning()
                    let captureVideoLayer: AVCaptureVideoPreviewLayer = AVCaptureVideoPreviewLayer.init(session: captureSesssion)
                    captureVideoLayer.frame = self.imageView.bounds
                    captureVideoLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
                    self.imageView.layer.addSublayer(captureVideoLayer)
                }
            }
        }
        catch {
            print(error)
        }
    }

    func capture(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingPhotoSampleBuffer photoSampleBuffer: CMSampleBuffer?, previewPhotoSampleBuffer: CMSampleBuffer?, resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Error?) {
        if let photoSampleBuffer = photoSampleBuffer {
            let photoData = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: photoSampleBuffer, previewPhotoSampleBuffer: previewPhotoSampleBuffer)
            let image = UIImage(data: photoData!)
            UIImageWriteToSavedPhotosAlbum(image!, nil, nil, nil)
        }
    }
}

これがiOS10でデフォルトになる書き方。
Swift3.0でスッキリかけるようになったのにAVCapturePhotoOutputとAVCapturePhotoSettingsに別れたりちとめんどくささが増してる…👿(慣れたらこっちのほうが別れてる分書きやすいかもしれない)

AVCapturePhotoOutputのサンプル

(3) UIImagePickerControllerの場合

※UIImagePickerControllerのみ、ボタンを押す → 写真を撮る → 取った写真をImageViewに表示という流れになっています。

UIImagePickerController.swift
import UIKit

class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

    @IBOutlet weak var imageView: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func Btn(_ sender: Any) {
        //カメラが使えるか調べる
        if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera) {

            //UIImagePickerController型の変数を作成
            let picker = UIImagePickerController()

            //sourceTypeでカメラを指定
            picker.sourceType = UIImagePickerControllerSourceType.camera
            //picker.sourceType = UIImagePickerControllerSourceType.photoLibrary

            picker.delegate = self

            //カメラ起動
            present(picker, animated: true, completion: nil)

        }

    }

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {

        let image = info[UIImagePickerControllerOriginalImage] as! UIImage
        imageView.image = image 
        //撮影画像保持

        //カメラロールに保存
        UIGraphicsBeginImageContext(imageView.bounds.size)
        imageView.layer.render(in: UIGraphicsGetCurrentContext()!)
        UIImageWriteToSavedPhotosAlbum(UIGraphicsGetImageFromCurrentImageContext()!, self, nil, nil)
        UIGraphicsEndImageContext()
        dismiss(animated: true, completion: nil)
    }
}

で、おなじみUIPickerController。
やはり、ミニマムの実装だとUIImagePickerControllerが一番ラク。

UIPickerControllerのサンプル

所感

・AVCaptureStillImageOutputを用いた時とAVCapturePhotoOutputを用いたときとでは勝手が大いに異なると(個人的に)感じた。
・ライブラリを書き直すとなると、骨が折れる(骨が折れた
・この記事を書いた後にAppleのとてもいいサンプルを発見したので、そちらを見ていただくとより参考になるかもしれない。(参照に添付)

参考

AVCam-iOS: Using AVFoundation to Capture Images and Movies
iOS10カスタムカメラ - Swift 3
iOS 10の新機能のサンプルコード集「iOS 10 Sampler」
(順不同)