[SwiftUI][AVFoundation] AVCaptureSession から写真を取得する方法

SwiftUI&AVFoundation

AVCaptureSession と AVCaptureVidePreviewView を使って、カメラのプレビューを表示できるようになったので、シャッターボタンを押された時に、プレビューに表示されている画像を取得する方法を説明します。

AVCapturePhotoOutputと取り出す手順

AVCaptureSession を使って表示している画像を取り出すためには、AVCapturePhotoOutput を使用します。

取り出すためには以下の手順が必要となります。

AVCaptureSession と Input を設定し、プレビューできるようにする
前回の記事で説明しました。
AVCapturePhotoOutput を準備して、AVCaptureSession の Output に設定する
AVCapturePhotoOutput をインスタンス化して、AVCaptureSession の Output に設定します。
AVCapturePhotoOutput#capturePhoto で画像取得リクエストを発行
画像データは、関数返り値ではなく、Delegate(AVCapturePhotoCaptureDelegate) に渡されます
AVCapturePhotoCaptureDelegate#photoOutput で取得し、処理する
引数の photo に AVCapturePhoto タイプのデータとして渡されますので、処理します

それでは、それぞれのステップをみていきます。

AVCaptureSession と Input を設定し、プレビューできるようにする

以前の記事で説明した通りです。

AVCapturePhotoOutput を準備して、AVCaptureSession の Output に設定する

以下のように、AVCapturePhotoOutput をインスタンス化し、AVCaptureSession の Output に設定します。

この段階で詳細な設定は不要です。

AVCapturePhotoOutput を AVCaptureSession に設定

  let photoOutput = AVCapturePhotoOutput()
  guard captureSession.canAddOutput(photoOutput) else { return }
  captureSession.sessionPreset = .photo
  captureSession.addOutput(photoOutput)

AVCapturePhotoOutput#capturePhoto で画像取得リクエストを発行

AVCaputurePhotoOutput#capturePhoto 関数によって、画像取得のリクエストを発行できます。

リクエスト時に、Delegate を渡して、処理されたデータはその Delegate に渡されてきます。

また、リクエスト時には撮影設定を、AVCapturePhotoSettings として渡す必要があります。

以下のようなコードとなります。

capturePhoto をコール

  let photoSetting = AVCapturePhotoSettings()
  photoSetting.flashMode = .auto
  photoSetting.isHighResolutionPhotoEnabled = false
  photoOutput.capturePhoto(with: photoSetting, delegate: self)

AVCapturePhotoCaptureDelegate#photoOutput で取得し、処理する

処理が終われば、Delegate の photoOutput がコールされますので、その中で処理します。

以下では、UIImage を作成しています。

AVCapturePhoto から UIImage

  public func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    let imageData = photo.fileDataRepresentation()
    self.image = UIImage(data: imageData!)
  }

サンプルコード

capturePhoto がAVsession の Output に設定されたオブジェクトの関数なので、キャプチャーする機能性をもつ外部オブジェクトとして AVCaptureModel というものを作成し、保持するようにしました。

アプリの動作としては、ボタンを押されると取得したUIImageをプレビューとは別に表示するようにしました。

beforeButtonPush

afterbuttonPush

画像の向きがおかしいのは、UIImageを表示する時に、デバイスの方向を考慮していないからです。

アプリケーションのコードは以下です。

AVCaptureModel.swift

//
//  AVCaputureModel.swift
//  CameraWithAVFoundation
//
//  Created by Tomoaki Yagishita on 2020/08/11.
//

import Foundation
import AVFoundation
import UIKit

public class AVCaptureModel : NSObject, AVCapturePhotoCaptureDelegate, ObservableObject {
    public var captureSession: AVCaptureSession
    public var videoInput: AVCaptureDeviceInput!
    public var photoOutput: AVCapturePhotoOutput
    @Published var image: UIImage?
    
    public override init() {
        self.captureSession = AVCaptureSession()
        self.photoOutput = AVCapturePhotoOutput()
    }
    
    public func setupSession() {
        captureSession.beginConfiguration()
        guard let videoCaputureDevice = AVCaptureDevice.default(for: .video) else { return }

        guard let videoInput = try? AVCaptureDeviceInput(device: videoCaputureDevice) else { return }
        self.videoInput = videoInput
        guard captureSession.canAddInput(videoInput) else { return }
        captureSession.addInput(videoInput)

        guard captureSession.canAddOutput(photoOutput) else { return }
        captureSession.sessionPreset = .photo
        captureSession.addOutput(photoOutput)
        
        captureSession.commitConfiguration()
    }
    
    public func updateInputOrientation(orientation: UIDeviceOrientation) {
        for conn in captureSession.connections {
            conn.videoOrientation = ConvertUIDeviceOrientationToAVCaptureVideoOrientation(deviceOrientation: orientation)
        }
    }
    
    
    public func takePhoto() {
        let photoSetting = AVCapturePhotoSettings()
        photoSetting.flashMode = .auto
        photoSetting.isHighResolutionPhotoEnabled = false
        photoOutput.capturePhoto(with: photoSetting, delegate: self)
        return
    }
    
    public func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        let imageData = photo.fileDataRepresentation()
        self.image = UIImage(data: imageData!)
    }
    
    func getImageFromSampleBuffer(sampleBuffer: CMSampleBuffer) ->UIImage? {
         guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
             return nil
         }
         CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
         let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
         let width = CVPixelBufferGetWidth(pixelBuffer)
         let height = CVPixelBufferGetHeight(pixelBuffer)
         let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
         let colorSpace = CGColorSpaceCreateDeviceRGB()
         let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue)
         guard let context = CGContext(data: baseAddress, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) else {
             return nil
         }
         guard let cgImage = context.makeImage() else {
             return nil
         }
         let image = UIImage(cgImage: cgImage, scale: 1, orientation:.right)
         CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
         return image
     }
}

public func ConvertUIDeviceOrientationToAVCaptureVideoOrientation(deviceOrientation: UIDeviceOrientation) -> AVCaptureVideoOrientation {
    switch deviceOrientation {
    case .portrait:
        return .portrait
    case .portraitUpsideDown:
        return .portraitUpsideDown
    case .landscapeLeft:
        return .landscapeRight
    case .landscapeRight:
        return .landscapeLeft
    default:
        return .portrait
    }
}
SwiftUIAVCaptureVidePreviewView.swift

//
//  SwiftUIAVCaptureVideoPreviewView.swift
//  CameraWithAVFoundation
//
//  Created by Tomoaki Yagishita on 2020/08/05.
//

import Foundation
import AVFoundation
import SwiftUI

public class UIAVCaptureVideoPreviewView: UIView {
    var captureSession: AVCaptureSession!
    var previewLayer: AVCaptureVideoPreviewLayer!

    public init(frame: CGRect, session: AVCaptureSession) {
        self.captureSession = session
        super.init(frame: frame)
    }
    
    public required init?(coder: NSCoder) {
        super.init(coder: coder)
        // no implementation
    }

    func setupPreview(previewSize: CGRect) {
        self.frame = previewSize

        self.previewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession)
        self.previewLayer.frame = self.bounds
        
        self.updatePreviewOrientation()

        self.layer.addSublayer(previewLayer)

        self.captureSession.startRunning()
    }
    
    func updateFrame(frame: CGRect) {
        self.frame = frame
        self.previewLayer.frame = frame
    }

    func updatePreviewOrientation() {
        switch UIDevice.current.orientation {
        case .portrait:
            self.previewLayer.connection?.videoOrientation = .portrait
        case .portraitUpsideDown:
            self.previewLayer.connection?.videoOrientation = .portraitUpsideDown
        case .landscapeLeft:
            self.previewLayer.connection?.videoOrientation = .landscapeRight
        case .landscapeRight:
            self.previewLayer.connection?.videoOrientation = .landscapeLeft
        default:
            self.previewLayer.connection?.videoOrientation = .portrait
        }
        return
    }
}

public struct SwiftUIAVCaptureVideoPreviewView: UIViewRepresentable {
    let previewFrame: CGRect
    let captureModel: AVCaptureModel

    public func makeUIView(context: Context) -> UIAVCaptureVideoPreviewView {
        let view = UIAVCaptureVideoPreviewView(frame: previewFrame, session: self.captureModel.captureSession)
        view.setupPreview(previewSize: previewFrame)
        return view
    }
    
    public func updateUIView(_ uiView: UIAVCaptureVideoPreviewView, context: Context) {
        print("in updateUIView")
        self.captureModel.updateInputOrientation(orientation: UIDevice.current.orientation)
        uiView.updateFrame(frame: previewFrame)
    }
}
ContentView.swift

//
//  ContentView.swift
//  Shared
//
//  Created by Tomoaki Yagishita on 2020/08/05.
//

import SwiftUI

struct ContentView: View {
    @ObservedObject var captureModel: AVCaptureModel
    @GestureState var scale: CGFloat = 1.0
    
    var body: some View {
        captureModel.setupSession()
        return VStack {
            Image(systemName: "camera")
                .resizable()
                .scaledToFit()
                .frame(height: 100)

            GeometryReader { geom in
                SwiftUIAVCaptureVideoPreviewView(previewFrame: CGRect(x: 0, y: 0, width: geom.size.width, height: geom.size.height),
                                                 captureModel: captureModel)
            }
            .border(Color.red)
            HStack {
                if captureModel.image != nil {
                    Image(uiImage: captureModel.image!)
                        .resizable()
                        .scaledToFit()
                        .frame(height:100)
                }
                Button("Button", action: {
                    captureModel.takePhoto()
                })
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(captureModel: AVCaptureModel())
    }
}

まとめ:AVSession を使ったプレビューから画像を取得するのは、非常に簡単です

画像を取得するのは非常に簡単なのですが、詳細なセッティングは、沼になっていそうです。

AVCapturePhotoSettings の詳細は、非常に多そうです。

説明は以上です。

コメントを残す

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