Categories
SugiBlog ホームページ制作・システム開発

カメラで写真を撮影する AVFoundation

今回はシステムのカメラUIImagePickerControllerではなくAVFoundationフレームワークを
使った方法をご紹介します。
簡単に実装するならUIImagePickerControllerが楽ですが、AVFoundationのほうが柔軟なアプリの作成が可能になります。

ネット上でも様々な情報がありますが、今回の肝になるのがデバイスの向きと
撮影した写真の向きの問題です。
例えば、iPhoneのみ対応のアプリで縦画面(portrait)に完全固定であれば
そう難しくないのですが、私が作ろうとしていたのが、iPhoneでは縦固定、iPadでは横固定で、
且つNavigationControllerを使用していたため、結構頭を悩ませました。

NavigationControllerを使っていると、カメラのシーンだけ縦に固定することができないのです。
通常はViewControllerに対してshouldAutorotateをoverrideすれば特定のシーンのみ
固定することが可能なのですが、NavigationControllerを使用している場合、
ViewControllerはNavigationControllerに包括されているのでViewControllerのshouldAutorotateをoverrideしても効果がないのです。
そしてその場合はNavigationControllerのshouldAutorotateをoverrideすることになりますが、
そうしてしまうと包括されている全てのシーンに適用されてしまいます。

また、iPhoneのみ対応で縦画面固定だったとしても、適切に処理していないと、
“最初から端末を横にして”カメラを起動すると写真が横にならないという事態になってしまいます。
これは私が参考にさせて頂いた書籍でもそこまでは対応していませんでした。

今回、意識するポイントとしては2種類、それぞれ3つあります。

まず1つ目

  • デバイスの向き
  • プレビューの向き
  • 保存時の出力の向き

これはiPhoneでは縦、iPadでは横とする場合のポイントです。

次に2つ目

  • インターフェースの向き
  • プレビューの向き
  • レイヤーの向き

これは画面の回転に対応する場合に気を付けるポイントです。

それでは、以上を踏まえた上で先に進みましょう。
まずはiPhoneでは縦固定、iPadでは横固定の場合で作成します。

カメラ利用の許可

まずはカメラを利用するためInfo.plistにカメラの利用許可を追加します。
また、写真を撮影するということは保存するためにフォトライブラリーにアクセスしますので、
そちらも一緒に追加します。

Privacy - Camera Usage Description
Privacy - Photo Library Usage Description

Valueには「写真を撮影します。」「写真を保存します。」等と設定します。

Viewの配置

StoryBoardでViewControllerにViewとButtonを配置します。

配置したViewとButtonをそれぞれ「previewView」「shutterButton」としてOutlet接続します。

@IBOutlet weak var previewView: UIView!
@IBOutlet weak var shutterButton: UIButton!

shutterButtonはtakePhotoメソッドを実行するようにAction接続もしておきます。

@IBAction func takePhoto(_ sender: Any)

previewViewは画面いっぱいになるようオートレイアウトを設定。

shutterButtonには背景に画像を設定しラベルテキストは空白にします。
ここでは大きさを120×120、上(top)と左(leading)を0に設定します。
また表示を一旦非表示(hidden)にしておきます。
※位置を左上にしたのは、横位置は中心で縦位置を下のほうに設置した際、
Position is ambiguousと警告が出るためであることと、どうせ後で位置変更するためです。

配置した後のドキュメントアウトラインはこのようになります。

コーディング

UIの作成が出来ましたので、後はコーディングをしていきます。
ある程度ブロックに分けて書いていきます。

まずはAVFoundationのインポートを忘れずに

import AVFoundation

必要な変数等の宣言

// インスタンスの作成
var session = AVCaptureSession()
var photoOutput = AVCapturePhotoOutput() //出力先

// 通知センターを作る
let notification = NotificationCenter.default

// プライバシーと入出力のステータス
var authStatus:AuthorizedStatus = .authorized
var inOutStatus:InputOutputStatus = .ready

// 認証のステータス
enum AuthorizedStatus {
    case authorized
    case notAuthorized
    case failed
}

// 入出力のステータス
enum InputOutputStatus {
    case ready
    case notReady
    case failed
}

ビューが表示された直後に実行される処理を記述

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    // セッション実行中なら中断
    if session.isRunning {
        return
    }

    // カメラのプライバシー認証確認
    cameraAuth()

    // 入出力の設定
    setupInputOutput()

    // カメラの準備ができているかどうか
    if (authStatus == .authorized)&&(inOutStatus == .ready) {
        // プレビューレイヤの設定
        setPreviewLayer()
        // セッション開始
        session.startRunning()
        shutterButton.isEnabled = true
    } else {
        // カメラの利用が許可されていない時(初回起動時は無効)
        shutterButton.isEnabled = false
    }

    // デバイスが回転したときに通知するイベントハンドラを設定する
    UIDevice.current.beginGeneratingDeviceOrientationNotifications()
    notification.addObserver(self,
                             selector: #selector(self.changedDeviceOrientation(_:)),
                             name: NSNotification.Name.UIDeviceOrientationDidChange,
                             object: nil)
}

シャッターボタンで実行する処理

@IBAction func takePhoto(_ sender: Any) {
    let captureSetting = AVCapturePhotoSettings()
    captureSetting.flashMode = .auto
    captureSetting.isAutoStillImageStabilizationEnabled = true
    captureSetting.isHighResolutionPhotoEnabled = false

    // キャプチャのイメージ処理はデリゲートに任せる
    photoOutput.capturePhoto(with: captureSetting, delegate: self)
}

カメラのプライバシー認証画面

func cameraAuth() {
    let status = AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo)
    switch status {
    case .notDetermined:
        // 初回起動時
        AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo,
                                      completionHandler: { [unowned self] authorized in
                                        print("初回", authorized.description)
                                        if authorized {
                                            self.authStatus = .authorized
                                        } else {
                                            self.authStatus = .notAuthorized
                                        }})
    case .restricted, .denied:
        authStatus = .notAuthorized
    case .authorized:
        authStatus = .authorized
    }
}

入出力の設定

func setupInputOutput() {
    // 解像度の設定
    session.sessionPreset = AVCaptureSessionPresetPhoto

    // 入出力
    do {
        // デバイスの取得
        let device = AVCaptureDevice.defaultDevice(
            withDeviceType: AVCaptureDeviceType.builtInWideAngleCamera,
            mediaType: AVMediaTypeVideo,
            position: .back)

        // 入力元
        let input = try AVCaptureDeviceInput(device: device)
        if session.canAddInput(input) {
            session.addInput(input)
        } else {
            inOutStatus = .notReady
            print("セッションに入力を追加できませんでした。")
            return
        }

    } catch let error as NSError {
        inOutStatus = .notReady
        print("カメラがありません \(error)")
        return
    }

    // 出力先
    if session.canAddOutput(photoOutput) {
        session.addOutput(photoOutput)
    } else {
        inOutStatus = .notReady
        print("セッションに出力を追加できませんでした。")
        return
    }
}

プレビューレイヤの設定

func setPreviewLayer() {

    // プレビューレイヤを作る
    let previewLayer = AVCaptureVideoPreviewLayer(session: session)
    guard let videoLayer = previewLayer else {
        print("プレビューレイヤを作成できませんでした。")
        shutterButton.isEnabled = false
        return
    }

    videoLayer.frame = view.bounds
    videoLayer.masksToBounds = true
    videoLayer.videoGravity = AVLayerVideoGravityResizeAspectFill


    // iPhone/iPadによってボタン配置位置を設定
    switch UIDevice.current.model {
    case "iPhone":
        portraitShutterButton()
    case "iPad":
        landscapeShutterButton()
    default:
        portraitShutterButton()
    }

    // レイヤーの向きを設定
    if UIDevice.current.model == "iPhone" {
        videoLayer.connection.videoOrientation = .portrait
    } else {
        videoLayer.connection.videoOrientation = .landscapeRight
    }

    // 出力の向きを設定
    if let photoOutputConnection = self.photoOutput.connection(withMediaType: AVMediaTypeVideo) {
        switch UIDevice.current.orientation {
        case .portrait:
            photoOutputConnection.videoOrientation = .portrait
        case .portraitUpsideDown:
            photoOutputConnection.videoOrientation = .portraitUpsideDown
        case .landscapeLeft:
            photoOutputConnection.videoOrientation = .landscapeRight
        case .landscapeRight:
            photoOutputConnection.videoOrientation = .landscapeLeft
        default:
            break
        }
    }

    // previewViewに追加する
    previewView.layer.addSublayer(videoLayer)
}

デバイスの向きが変わったときに呼び出すメソッド

func changedDeviceOrientation(_ notification :Notification) {

    // photoOutput.connectionの回転向きをデバイスと合わせる
    if let photoOutputConnection = self.photoOutput.connection(withMediaType: AVMediaTypeVideo) {
        switch UIDevice.current.orientation {
        case .portrait:
            photoOutputConnection.videoOrientation = .portrait
        case .portraitUpsideDown:
            photoOutputConnection.videoOrientation = .portraitUpsideDown
        case .landscapeLeft:
            photoOutputConnection.videoOrientation = .landscapeRight
        case .landscapeRight:
            photoOutputConnection.videoOrientation = .landscapeLeft
        default:
            break
        }
    }
}

iPhone用のshutterButton位置設定

func portraitShutterButton() {
    // Constraintsを一度削除する
    for constraint in self.view.constraints {
        let firstItem: UIButton? = constraint.firstItem as? UIButton
        if firstItem == self.shutterButton {
            self.view.removeConstraint(constraint)
        }
    }
    // Constraintsを追加
    self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: NSLayoutAttribute.centerX, relatedBy: NSLayoutRelation.equal, toItem: shutterButton, attribute: NSLayoutAttribute.centerX, multiplier: 1.0, constant: 0.0))
    self.view.addConstraint(NSLayoutConstraint(item: self.bottomLayoutGuide, attribute: NSLayoutAttribute.top, relatedBy: NSLayoutRelation.equal, toItem: shutterButton, attribute: NSLayoutAttribute.bottom, multiplier: 1.0, constant: 20.0))

    shutterButton.isHidden = false
}

iPad用のshutterButton位置設定

func landscapeShutterButton() {
    // Constraintsを一度削除する
    for constraint in self.view.constraints {
        let firstItem: UIButton? = constraint.firstItem as? UIButton
        if firstItem == self.shutterButton {
            self.view.removeConstraint(constraint)
        }
    }
    // Constraintsを追加
    self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: NSLayoutAttribute.centerY, relatedBy: NSLayoutRelation.equal, toItem: shutterButton, attribute: NSLayoutAttribute.centerY, multiplier: 1.0, constant: 0.0))
    self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: NSLayoutAttribute.trailing, relatedBy: NSLayoutRelation.equal, toItem: shutterButton, attribute: NSLayoutAttribute.trailing, multiplier: 1.0, constant: 20.0))

    shutterButton.isHidden = false
}

後処理として不要になった通知を終了させます。

override func viewDidDisappear(_ animated: Bool) {
    UIDevice.current.endGeneratingDeviceOrientationNotifications()
}

ステータスバーを非表示にしておきます。

override var prefersStatusBarHidden: Bool {
    return true
}

撮影した画像を保存するキャプチャー処理部分はextensionで別ファイルに記述します。

import Photos

// デリゲート部分を拡張する
extension CameraViewController:AVCapturePhotoCaptureDelegate {
    // 映像をキャプチャする
    func capture(_ captureOutput: AVCapturePhotoOutput,
                 didFinishProcessingPhotoSampleBuffer photoSampleBuffer: CMSampleBuffer?,
                 previewPhotoSampleBuffer: CMSampleBuffer?,
                 resolvedSettings: AVCaptureResolvedPhotoSettings,
                 bracketSettings: AVCaptureBracketedStillImageSettings?,
                 error: Error?) {

        // バッファからjpegデータを取り出す
        let photoData = AVCapturePhotoOutput.jpegPhotoDataRepresentation(
            forJPEGSampleBuffer: photoSampleBuffer!,
            previewPhotoSampleBuffer: previewPhotoSampleBuffer)
        // photoDataがnilでないときUIImageに変換する
        if let data = photoData {
            if let stillImage = UIImage(data: data) {
                // アルバムに追加する
                UIImageWriteToSavedPhotosAlbum(stillImage, self, nil, nil)
            }
        }
    }

}

画面の回転に対応する

オートレイアウトだとうまくいかないので、直接配置する方法を取っています。
シャッターボタン位置が常に同じ場所に来るよう配置しています。

プレビューレイヤの設定

func setPreviewLayer() {

    // プレビューレイヤを作る
    let previewLayer = AVCaptureVideoPreviewLayer(session: session)
    guard let videoLayer = previewLayer else {
        print("プレビューレイヤを作成できませんでした。")
        shutterButton.isEnabled = false
        return
    }

    videoLayer.frame = view.bounds
    videoLayer.masksToBounds = true
    videoLayer.videoGravity = AVLayerVideoGravityResizeAspectFill

    switch UIDevice.current.orientation {
    case .portrait:
        videoLayer.connection.videoOrientation = .portrait
        portraitShutterButton()
    case .portraitUpsideDown:
        videoLayer.connection.videoOrientation = .portraitUpsideDown
        portraitUpsideDownShutterButton()
    case .landscapeLeft:
        videoLayer.connection.videoOrientation = .landscapeRight
        landscapeLeftShutterButton()
    case .landscapeRight:
        videoLayer.connection.videoOrientation = .landscapeLeft
        landscapeRightShutterButton()
    default:
        print("unknown")
    }

    if let photoOutputConnection = self.photoOutput.connection(withMediaType: AVMediaTypeVideo) {
        switch UIDevice.current.orientation {
        case .portrait:
            photoOutputConnection.videoOrientation = .portrait
        case .portraitUpsideDown:
            photoOutputConnection.videoOrientation = .portraitUpsideDown
            break
        case .landscapeLeft:
            photoOutputConnection.videoOrientation = .landscapeRight
        case .landscapeRight:
            photoOutputConnection.videoOrientation = .landscapeLeft
        default:
            break
        }
    }

    // previewViewに追加する
    previewView.layer.addSublayer(videoLayer)
}

デバイスの向きが変わったときに呼び出すメソッドを書き換えます。

func changedDeviceOrientation(_ notification :Notification) {

    let videoLayer: AVCaptureVideoPreviewLayer = previewView.layer.sublayers?[0] as! AVCaptureVideoPreviewLayer
    videoLayer.frame = view.bounds

    switch UIDevice.current.orientation {
    case .portrait:
        videoLayer.connection.videoOrientation = .portrait
        portraitShutterButton()
    case .portraitUpsideDown:
        videoLayer.connection.videoOrientation = .portraitUpsideDown
        portraitUpsideDownShutterButton()
    case .landscapeLeft:
        videoLayer.connection.videoOrientation = .landscapeRight
        landscapeLeftShutterButton()
    case .landscapeRight:
        videoLayer.connection.videoOrientation = .landscapeLeft
        landscapeRightShutterButton()
    default:
        print("unknown")
    }

    // photoOutput.connectionの回転向きをデバイスと合わせる
    if let photoOutputConnection = self.photoOutput.connection(withMediaType: AVMediaTypeVideo) {
        switch UIDevice.current.orientation {
        case .portrait:
            photoOutputConnection.videoOrientation = .portrait
        case .portraitUpsideDown:
            photoOutputConnection.videoOrientation = .portraitUpsideDown
            break
        case .landscapeLeft:
            photoOutputConnection.videoOrientation = .landscapeRight
        case .landscapeRight:
            photoOutputConnection.videoOrientation = .landscapeLeft
        default:
            break
        }
    }
}

ボタンの配置を行うメソッド

func portraitShutterButton() {
    shutterButton.center.x = self.view.center.x
    shutterButton.frame.origin.y = self.view.frame.height - shutterButton.frame.height - 20
    shutterButton.isHidden = false
}

func landscapeLeftShutterButton() {
    shutterButton.frame.origin.x = self.view.frame.size.width - shutterButton.frame.size.width - 20
    shutterButton.center.y = self.view.center.y
    shutterButton.isHidden = false
}

func landscapeRightShutterButton() {
    shutterButton.frame.origin.x = 20
    shutterButton.center.y = self.view.center.y
    shutterButton.isHidden = false
}

func portraitUpsideDownShutterButton() {
    shutterButton.center.x = self.view.center.x
    shutterButton.frame.origin.y = 20
    shutterButton.isHidden = false
}

Xcode: 8.3.2
Swift: 3.1
OS: Sierra 10.12

78 views

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

*