LoginSignup
10
16

More than 5 years have passed since last update.

[iOS] バーストカメラ(連射カメラ)を実装してみる

Last updated at Posted at 2017-01-21

はじめに

以前AVFundationを利用したカメラの実装サンプルを書いた際、
バースト(連射)カメラの実装サンプルも書いてほしいと
要望がありましたので、纏めました。

完成版は、Githubを御覧ください。

開発環境

開発環境は、下記のとおりです。

category Version
XCode 8.2
iOS 10.0〜
Swift 3.0.2

登場人物

バースト(連射)カメラで利用する主なクラスは、下記のとおりです。

クラス名 説明
AVCaptureDeviceInput 入力デバイスとして、全面カメラ、背面カメラなどを設定する
AVCaptureSession カメラ撮影用のセッション??
AVCapturePhotoOutput 出力に、静止画を設定する
AVCaputreVideoOutput 出力に、動画フレームデータを設定する

※iOS10では、AVCaptureStillImageOutputが廃止になり、
AVCapturePhotoOutputを使います。

実装手順

  1. カメラの利用許可の設定
  2. 入出力の初期化および、接続
  3. セッションの開始/終了
  4. フレームごとに画像を取得
  5. 使ってみる

1. カメラの利用許可の設定

Info.plist
<key>NSCameraUsageDescription</key>
<string>カメラへアクセスするために必要です</string>

2. 入出力の初期化および、接続

2.1. セッションのインスタンス化

BurstCameraUtil.swift
    private let session = AVCaptureSession()

2.2. 入力デバイスの初期化

BurstCameraUtil.swift

    func findDevice(position: AVCaptureDevicePosition) -> AVCaptureDevice? {

        let device = AVCaptureDevice.defaultDevice(withDeviceType: .builtInWideAngleCamera,
                                                   mediaType: AVMediaTypeVideo,
                                                   position: position)

        device?.activeVideoMinFrameDuration = CMTimeMake(1, 30)
        return device
    }

2.3. 入力デバイスの接続

BurstCameraUtil.swift
    func createVideoPreviewLayer(device: AVCaptureDevice?) -> AVCaptureVideoPreviewLayer?{

        let videoInput = try! AVCaptureDeviceInput.init(device: device)

        if (session.canAddInput(videoInput)) {
            session.addInput(videoInput)
        }

        if (session.canAddOutput(photoOutput)) {
            session.addOutput(photoOutput)
        }

        return AVCaptureVideoPreviewLayer.init(session: session)
    }

2.4. 出力デバイスの初期化

BurstCameraUtil.swift

    private let photoOutput = AVCapturePhotoOutput()
    private let videoDataOutput = AVCaptureVideoDataOutput()

2.5. 出力デバイスの接続

BurstCameraUtil.swift
func createVideoDataOutput() {

        videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as AnyHashable : Int(kCVPixelFormatType_32BGRA)]
        videoDataOutput.alwaysDiscardsLateVideoFrames = true
        videoDataOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)

        session.addOutput(videoDataOutput)
        session.sessionPreset = AVCaptureSessionPreset1920x1080
    }

3. セッションの開始/終了

3.1. セッションの開始

BurstCameraUtil.swift

    func startRunning() {
        session.startRunning()
    }

3.2. セッションの終了

BurstCameraUtil.swift

    func stopRunning() {
        session.stopRunning()
    }

4. フレームごとに画像を取得

4.1. 写真を保存するための配列等を準備する

BurstCameraUtil.swift
    fileprivate var images:[UIImage] = []
    fileprivate var isShooting = false
    fileprivate var counter = 0

4.2. AVCaptureVideoDataOutputSampleBufferDelegate

BurstCameraUtil.swift
extension BurstCameraUtil: AVCaptureVideoDataOutputSampleBufferDelegate {

    func captureOutput(_ captureOutput: AVCaptureOutput!,
                       didOutputSampleBuffer sampleBuffer: CMSampleBuffer!,
                       from connection: AVCaptureConnection!) {

        if counter % 3 == 0 { // 1/10秒だけ処理する
            if isShooting {
                let image = imageFromSampleBuffer(sampleBuffer: sampleBuffer)
                images.append(image)
            }
        }
        counter += 1
    }

    private func imageFromSampleBuffer(sampleBuffer :CMSampleBuffer) -> UIImage {

        let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!

        CVPixelBufferLockBaseAddress(imageBuffer,
                                     CVPixelBufferLockFlags(rawValue: 0))

        let base = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0)!
        let bytesPerRow = UInt(CVPixelBufferGetBytesPerRow(imageBuffer))
        let width = UInt(CVPixelBufferGetWidth(imageBuffer))
        let height = UInt(CVPixelBufferGetHeight(imageBuffer))

        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitsPerCompornent = 8
        let bitmapInfo = CGBitmapInfo(rawValue: (CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) as UInt32)
        let newContext = CGContext(data: base,
                                   width: Int(width),
                                   height: Int(height),
                                   bitsPerComponent: Int(bitsPerCompornent),
                                   bytesPerRow: Int(bytesPerRow),
                                   space: colorSpace,
                                   bitmapInfo: bitmapInfo.rawValue)! as CGContext

        let imageRef = newContext.makeImage()!
        let image = UIImage(cgImage: imageRef,
                            scale: 1.0,
                            orientation: UIImageOrientation.right)

        CVPixelBufferUnlockBaseAddress(imageBuffer,
                                       CVPixelBufferLockFlags(rawValue: 0))
        return image
    }
}

4.3. 連射開始、終了、連射写真の画像リスト

BurstCameraUtil.swift
    func start() {
        images = []
        isShooting = true
    }

    func stop() {
        isShooting = false
    }

    func photos() -> [UIImage] {
        return images
    }

5. 使ってみる

UI周りは、割愛します。

ViewController.swift
import UIKit
import AVFoundation

final class ViewController: UIViewController {

    @IBOutlet weak var baseView: UIView!

    private var camera: CameraTakeable?

    override func viewDidLoad() {
        super.viewDidLoad()
        setupCameraView(camera: BurstCameraUtil())
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        camera?.startRunning()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        camera?.stopRunning()
        camera?.removeVideoInput()
    }

    private func setupCameraView(camera: CameraTakeable?) {

        self.camera = camera

        let device = camera?.findDevice(position: .back)
        camera?.createVideoDataOutput()

        if let videoLayer = camera?.createVideoPreviewLayer(device: device) {            
            videoLayer.frame = baseView.bounds
            videoLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
            baseView.layer.addSublayer(videoLayer)
        } else {
            fatalError("VideoLayer is Nil")
        }
    }

    //MARK:- Actions
    @IBAction func photoDidTapDown(_ sender: UIButton) {
        camera?.start()
    }

    @IBAction func photoDidTapUp(_ sender: UIButton) {
        camera?.stop()

        if let images = camera?.photos() {
            print("I took \(images.count) photos")
        }
    }
}

まとめ

完成版は、Githubを御覧ください。

カメラを利用して、様々な出力が出来ますね。

クラス名 説明
AVCaptureMovieFileOutput 動画ファイル
AVCaptureAudioFileOutput 音声ファイル
AVCaptureVideoDataOutput 動画フレームデータ
AVCaptureAudioDataOutput 音声データ
AVCapturePhotoOutput 静止画
AVCaptureMetadataOutput メタデータ
10
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
16