[iOS] AVFoundation(AVCaptureVideoDataOutput)で連写カメラを作ってみた

2017.01.04

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

1 はじめに

本記事は、AVFoundationを使用した連写カメラを紹介するものです。

AVFoundationを使用する場合の共通的な処理については下記に纏めましたので、是非、ご参照下さい。
[iOS] AVFundationを使用して、「ビデオ録画」や「連写カメラ」や「QRコードリーダー」や「バーコードリーダー」を作ってみた

2 入出力

連写カメラを実装する場合、入力として、カメラ(今回は背面)を使用し、出力として、動画データであるAVCaptureVideoDataOutputを設定します。 001

下記のコードは、セッションを生成して、上記のとおり入出力をセットしている例です。

// セッションのインスタンス生成
let captureSession = AVCaptureSession()

// 入力(背面カメラ)
let videoDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)
let videoInput = try! AVCaptureDeviceInput.init(device: videoDevice)
captureSession.addInput(videoInput)

// 出力(動画データ)
let videoDataOutput = AVCaptureVideoDataOutput()
captureSession.addOutput(videoDataOutput)

3 AVCaptureVideoDataOutput

AVCaptureVideoDataOutputでは、フレーム毎に映像を取得することができ、本サンプルでは、シャッターボタンを押している間に来たフレームを、UIImageに変換して保存しています。

setSampleBufferDelegate(:queue:) を使用して、 AVCaptureVideoDataOutputSampleBufferDelegate プロトコルを実装したデリゲートを設定することが出来、このデリゲートでは、フレームごとに captureOutput(:didOutputSampleBuffer:from:) メソッドが呼ばれます。

本記事のサンプルでは、activeVideoMinFrameDurationで、フレームを1/30秒に設定し、captureOutput(_:didOutputSampleBuffer:from:)で、1/10秒に一回、取得したデータをUIImageに変換して、スクロールビューに追加しています。

最初から、フレームを1/10秒に設定していない理由は、設定できるフレーム数は、デバイス毎決まっており、手元のiPhone6では、1/30若しくは1/60になっていたためです。

videoDevice?.activeVideoMinFrameDuration = CMTimeMake(1, 30)// 1/30秒 (1秒間に30フレーム)
// 新しいキャプチャの追加で呼ばれる(1/30秒に1回)
func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
    if counter % 3 == 0 { // 1/10秒だけ処理する
        if isShooting {
            let image = imageFromSampleBuffer(sampleBuffer: sampleBuffer)
            addImage(image: image) // スクロールビューへの追加
        }
    }
    counter += 1
}

4 その他の設定

AVCaptureVideoDataOutputには、色情報や、フレームデータの扱いに関する設定が可能です。

// ピクセルフォーマット(32bit BGRA)
videoDataOutput?.videoSettings = [kCVPixelBufferPixelFormatTypeKey as AnyHashable : Int(kCVPixelFormatType_32BGRA)]
// キューのブロック中に新しいフレームが来たら削除する
videoDataOutput?.alwaysDiscardsLateVideoFrames = true

また、画像の品質に関しては、AVCaptureSessionの方で設定します。

// クオリティ(1920x1080ピクセル)
captureSession.sessionPreset = AVCaptureSessionPreset1920x1080

5 UIImageへの変換

次のコードは、captureOutput(_:didOutputSampleBuffer:from:)メソッドで取得できるCMSampleBufferからUIImageを生成しているコードですで。

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
}

6 シャッター音

今回の実装では、ボタンを押している間は、どんどん写真が撮れるわけですが、シャッターの操作感覚の向上のために、シャッター音を鳴らしてみました。

1枚の写真ごとに音を再生すると、ちょっと間に合わなかったので、「連写の音」を連続再生するようにしました。

let audioPath = Bundle.main.path(forResource: "nc66359", ofType:"wav")!
let audioUrl = URL(fileURLWithPath: audioPath)
let audioPlayer = try! AVAudioPlayer(contentsOf: audioUrl)
audioPlayer.delegate = self
audioPlayer.currentTime = 0

AVAudioPlayerでは、AVAudioPlayerDelegateに、再生完了時のメソッドが有りますので、連写中は、再び再生することで継続しています。

<br />@IBAction func touchUpButton(_ sender: UIButton) {
    isShooting = false // 連写終了
    audioPlayer.stop()
}

@IBAction func touchDownButton(_ sender: UIButton) {
    isShooting = true // 連写中
    audioPlayer.play()
}

// 連写音の連続再生
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
    if isShooting {
        audioPlayer.play()
    }
}

7 動作確認

使用している様子は、次のとおりです。

現在、連写したデータは、UIImageのまま、スクロールビューに表示しているだけですが、この中から、保存する写真を選択するなどのUIを作れば、ペットの写真など、タイミングが難しい場面で威力を発揮するカメラも作成できそうです。

8 最後に

今回は、カメラから連続して出力されるフレームデータを保存することで、連写カメラを作成してみました。

一応、この方法を使用すれば、デバイスで指定できる最大フレーム数まで、高速な連写カメラを作成することが可能だと言うことになると思います。

コードは下記に置いています。気になるところが有りましたら、ぜひ教えてやってください。
github [GitHub] https://github.com/furuya02/AVCaptureVideoDataOutputSample

シャッターの連写音は、下記のものを使用させて頂きました。
ニコニ・コモンズ http://commons.nicovideo.jp/

9 参考資料


API Reference AVCaptureVideoDataOutput
[iOS] AVFundationを使用して、「ビデオ録画」や「連写カメラ」や「QRコードリーダー」や「バーコードリーダー」を作ってみた