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)
}
}
}
}
この記事が気に入ったらサポートをしてみませんか?