見出し画像

iOSのビデオキャプチャ

機械学習モデルでビデオキャプチャ使う機会が多いので、簡単にビデオキャプチャできるクラスを作って(Appleのサンプルの改変版)、その使い方をまとめました。

1. VideoCaptureクラスの仕様

VideoCaptureクラスの仕様は、次のとおり。

◎ initCapture(cameraPosition:,preset:, fps:, completion:)
キャプチャの初期化。キャプチャ開始前に呼びます。
・cameraPosition: AVCaptureDevice.Position    カメラ位置 (.front / .back)
・preset: AVCaptureSession.Preset    画面サイズ (.vga640x480 など)
・fps: Int    FPS
・completion: (Error?) -> Void    完了コールバック

◎ startCapture(completion:)
キャプチャの開始。
・completion: () -> Void    完了コールバック

◎ stopCapture(completion:)
キャプチャの停止。
・completion: () -> Void    完了コールバック

◎ flipCamera(completion:)
前面カメラと背面カメラの切り替え。
・completion: (Error?) -> Void    完了コールバック

◎ delegateプロパティ
キャプチャ画像を受信するデリゲート。キャプチャ開始前に設定。

videoCapture(_ videoCapture: VideoCapture, didCaptureFrame image: CGImage?)
キャプチャ画像受信時に呼ばれる
・videoCapture: VideoCapture    ビデオキャプチャ
・image: CGImage    キャプチャ画像

2. VideoCaptureクラスの使用例

VideoCaptureクラスの使用例は、次のとおり。
縦画面では480x640、横画面では640x480の画像を、秒間10フレームで受信しています。

・ViewController.swift

import UIKit

class ViewController: UIViewController {
    // UI
    @IBOutlet private var imageView: UIImageView!
    
    // ビデオキャプチャ
    private let videoCapture = VideoCapture()   

    // ビューロード時に呼ばれる
    override func viewDidLoad() {
        super.viewDidLoad()
       
        // キャプチャの開始
        startCapture()
    }
   
    // ビュー非表示時に呼ばれる
    override func viewWillDisappear(_ animated: Bool) {
        // キャプチャの停止
        self.videoCapture.stopCapture {
            super.viewWillDisappear(animated)
        }
    }
   
    // ビュー回転時に呼ばれる
    override func viewWillTransition(to size: CGSize,
        with coordinator: UIViewControllerTransitionCoordinator) {
        // キャプチャの開始 (ビュー回転時はカメラ再起動)
        startCapture()
    }
   
    // キャプチャの開始
    private func startCapture() {
        // キャプチャの初期化
        self.videoCapture.initCapture(
            cameraPosition: .back, preset: .vga640x480, fps: 10) { error in
            if let error = error {
                print("Failed to setup camera with error \(error)")
                return
            }
           
            // キャプチャの開始
            self.videoCapture.delegate = self
            self.videoCapture.startCapture()
        }
    }

    // フリップボタン押下時に呼ばれる
    @IBAction func onFlip(sender: UIButton) {
        // 前面カメラと背面カメラの切り替え
        self.videoCapture.flipCamera() { error in
            if let error = error {
                print("Failed to flip camera with error \(error)")
                return
            }
        }
    }
}

// ViewControllerの拡張 - VideoCaptureDelegate
extension ViewController: VideoCaptureDelegate {
    // キャプチャ画像の受信時に呼ばれる
    func videoCapture(_ videoCapture: VideoCapture,
        didCaptureFrame capturedImage: CGImage?) {
        // captureImageの取得
        guard let capturedImage = capturedImage else {
            fatalError("Captured image is null")
        }
       
        // イメージビューの更新
        self.imageView.image = UIImage(cgImage: capturedImage)
    }
}

3. VideoCaptureクラスのコード

VideoCaptureクラスのコードは、次のとおり。

・VideoCapture.swift

import AVFoundation
import CoreVideo
import UIKit
import VideoToolbox

// AVCaptureVideoOrientationの拡張
extension AVCaptureVideoOrientation {
    // UIDeviceOrientation → AVCaptureVideoOrientation
    init(deviceOrientation: UIDeviceOrientation) {
        switch deviceOrientation {
        case .landscapeLeft:
            self = .landscapeLeft
        case .landscapeRight:
            self = .landscapeRight
        case .portrait:
            self = .portrait
        case .portraitUpsideDown:
            self = .portraitUpsideDown
        default:
            self = .portrait
        }
    }
}

// ビデオキャプチャデリゲート
protocol VideoCaptureDelegate: AnyObject {
    // キャプチャ画像受信時に呼ばれる
    func videoCapture(_ videoCapture: VideoCapture, didCaptureFrame image: CGImage?)
}

// ビデオキャプチャ
class VideoCapture: NSObject {
    // エラー定数
    enum VideoCaptureError: Error {
        case captureSessionIsMissing
        case invalidInput
        case invalidOutput
        case unknown
    }

    // システム
    weak var delegate: VideoCaptureDelegate? // ビデオキャプチャデリゲート
    private let sessionQueue = DispatchQueue(label: "sessionqueue") // ディスパッチキュー
    private let captureSession = AVCaptureSession() // キャプチャセッション
    private let videoOutput = AVCaptureVideoDataOutput() // キャプチャ出力
   
    // 設定
    private var cameraPosition = AVCaptureDevice.Position.back // カメラ位置(front/back)
    private var preset: AVCaptureSession.Preset = .vga640x480 // 画面解像度
    private var fps: Int32 = -1 // FPS

    // カメラ位置(front/back)の切り替え
    public func flipCamera(completion: @escaping (Error?) -> Void) {
        sessionQueue.async {
            do {
                // カメラ位置(front/back)の切り替え
                self.cameraPosition = self.cameraPosition == .back ? .front : .back
 
                // カメラの再開
                self.captureSession.beginConfiguration()
                try self.setCaptureSessionInput()
                try self.setCaptureSessionOutput()
                self.captureSession.commitConfiguration()

                // 通知
                DispatchQueue.main.async {
                    completion(nil)
                }
            } catch {
                // 通知
                DispatchQueue.main.async {
                    completion(error)
                }
            }
        }
    }

    // キャプチャの初期化
    public func initCapture(
        cameraPosition: AVCaptureDevice.Posi
        preset: AVCaptureSession.Preset = .vga640x480,tion = .back,
        fps: Int = -1,
        completion: @escaping (Error?) -> Void) {
        self.cameraPosition = cameraPosition
        self.preset = preset
        self.fps = Int32(fps)
        self.sessionQueue.async {
            do {
                try self.initCapture()
                DispatchQueue.main.async {
                    completion(nil)
                }
            } catch {
                DispatchQueue.main.async {
                    completion(error)
                }
            }
        }
    }

    // キャプチャの初期化(同期)
    private func initCapture() throws {
        if self.captureSession.isRunning {
            self.captureSession.stopRunning()
        }
        self.captureSession.beginConfiguration()
        self.captureSession.sessionPreset = self.preset
        try setCaptureSessionInput()
        try setCaptureSessionOutput()
        self.captureSession.commitConfiguration()
    }

    // セッション入力のセットアップ
    private func setCaptureSessionInput() throws {
        // キャプチャデバイスの取得
        guard let captureDevice = AVCaptureDevice.default(
            .builtInWideAngleCamera,
            for: AVMediaType.video,
            position: cameraPosition) else {
                throw VideoCaptureError.invalidInput
        } 

        // 既存のキャプチャ入力の削除
        captureSession.inputs.forEach { input in
            captureSession.removeInput(input)
        }

        // キャプチャ入力の追加
        guard let videoInput = try? AVCaptureDeviceInput(device: captureDevice) else {
            throw VideoCaptureError.invalidInput
        }
        guard captureSession.canAddInput(videoInput) else {
            throw VideoCaptureError.invalidInput
        }
        captureSession.addInput(videoInput)
       
        // FPSの指定
        if (self.fps > 0) {
            try captureDevice.lockForConfiguration()
            captureDevice.activeVideoMinFrameDuration = CMTimeMake(value: 1, timescale: self.fps)
            captureDevice.activeVideoMaxFrameDuration = CMTimeMake(value: 1, timescale: self.fps)
            captureDevice.unlockForConfiguration()
        }
    }

    // セッション出力のセットアップ
    private func setCaptureSessionOutput() throws {
        // 既存のキャプチャ出力の削除
        captureSession.outputs.forEach { output in
            captureSession.removeOutput(output)
        }
 
        // ピクセル種別の生成
        let settings: [String: Any] = [
            String(kCVPixelBufferPixelFormatTypeKey): kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
        ]
 
        // キャプチャ出力の追加
        videoOutput.videoSettings = settings // ピクセル種別
        videoOutput.alwaysDiscardsLateVideoFrames = true // ビジー時のフレーム破棄
        videoOutput.setSampleBufferDelegate(self, queue: sessionQueue)
        guard captureSession.canAddOutput(videoOutput) else {
            throw VideoCaptureError.invalidOutput
        }
        captureSession.addOutput(videoOutput)

        // キャプチャ出力の向きの更新
        if let connection = videoOutput.connection(with: .video),
            connection.isVideoOrientationSupported {
            connection.videoOrientation =
                AVCaptureVideoOrientation(deviceOrientation: UIDevice.current.orientation)
            connection.isVideoMirrored = cameraPosition == .front
            if connection.videoOrientation == .landscapeLeft {
                connection.videoOrientation = .landscapeRight
            } else if connection.videoOrientation == .landscapeRight {
                connection.videoOrientation = .landscapeLeft
            }
        }
    }

    // キャプチャの開始
    public func startCapture(completion completionHandler: (() -> Void)? = nil) {
        sessionQueue.async {
            if !self.captureSession.isRunning {
                self.captureSession.startRunning()
            }
            if let completionHandler = completionHandler {
                DispatchQueue.main.async {
                    completionHandler()
                }
            }
        }
    }

    // キャプチャの停止
    public func stopCapture(completion completionHandler: (() -> Void)? = nil) {
        sessionQueue.async {
            if self.captureSession.isRunning {
                self.captureSession.stopRunning()
            }
            if let completionHandler = completionHandler {
                DispatchQueue.main.async {
                    completionHandler()
                }
            }
        }
    }
}

// ビデオキャプチャの拡張
extension VideoCapture: AVCaptureVideoDataOutputSampleBufferDelegate {
    // キャプチャ画像受信時に呼ばれる
    public func captureOutput(_ output: AVCaptureOutput,
        didOutput sampleBuffer: CMSampleBuffer,
        from connection: AVCaptureConnection) {
        // deleateの取得
        guard let delegate = delegate else { return }
 
        if let pixelBuffer = sampleBuffer.imageBuffer {
            // ImageBuffer → CGImage
            guard CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) == kCVReturnSuccess else {
                return
            }
            var image: CGImage?
            VTCreateCGImageFromCVPixelBuffer(pixelBuffer, options: nil, imageOut: &image)
            CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
 
            // 通知
            DispatchQueue.main.sync {
                delegate.videoCapture(self, didCaptureFrame: image)
            }
        }
    }
}



この記事が気に入ったらサポートをしてみませんか?