[Swift]iPhoneのカメラに映ってる画像にカスタムフィルタかけてみる(前編)

2014.10.29

はじめに

iPhoneのカメラに写っている画像にリアルタイムでフィルタをかけてみたいと思います。 長くなったので2回に分け、前半はカメラ機能、後半はフィルタ部分を記載します。 今回は、AVCaptureVideoDataOutputを使用し、カメラに映った画像をiPhoneの画面に表示するまでになります。

実装

プロジェクトはSingleViewApplicationで作成しました。Storyboardは使用してません。 また、カメラ機能を使用するので、実機で動かす必要があります。

ソースコード

ViewController.swift

import UIKit
import GLKit
import AVFoundation

class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate
{
    var videoDisplayView: GLKView!
    var videoDisplayViewRect: CGRect!
    var renderContext: CIContext!
    var cpsSession: AVCaptureSession!
    
    override func viewDidLoad()
    {
        super.viewDidLoad()
    }
    override func viewWillAppear(animated: Bool)
    {
        //画面の生成
        self.initDisplay()
        
        // カメラの使用準備.
        self.initCamera()
    }
    override func viewDidDisappear(animated: Bool)
    {
        // カメラの停止とメモリ解放.
        self.cpsSession.stopRunning()
        for output in self.cpsSession.outputs
        {
            self.cpsSession.removeOutput(output as AVCaptureOutput)
        }
        for input in self.cpsSession.inputs
        {
            self.cpsSession.removeInput(input as AVCaptureInput)
        }
        self.cpsSession = nil
    }
    func initDisplay()
    {
        videoDisplayView = GLKView(frame: view.bounds, context: EAGLContext(API: .OpenGLES2))
        videoDisplayView.transform = CGAffineTransformMakeRotation(CGFloat(M_PI_2))
        videoDisplayView.frame = view.bounds
        view.addSubview(videoDisplayView)
        
        renderContext = CIContext(EAGLContext: videoDisplayView.context)
        videoDisplayView.bindDrawable()
        videoDisplayViewRect = CGRect(x: 0, y: 0, width: videoDisplayView.drawableWidth, height: videoDisplayView.drawableHeight)
    }
    func initCamera()
    {
        //カメラからの入力を作成
        var device: AVCaptureDevice!
        
        //背面カメラの検索
        for device: AnyObject in AVCaptureDevice.devices()
        {
            if device.position == AVCaptureDevicePosition.Back
            {
                device = device as AVCaptureDevice
            }
        }
        
        //入力データの取得
        var deviceInput: AVCaptureDeviceInput = AVCaptureDeviceInput.deviceInputWithDevice(device, error: nil) as AVCaptureDeviceInput
        
        //出力データの取得
        var videoDataOutput:AVCaptureVideoDataOutput = AVCaptureVideoDataOutput()
        
        //カラーチャンネルの設定
        videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey : kCVPixelFormatType_32BGRA]
        
        //画像をキャプチャするキューを指定
        videoDataOutput.setSampleBufferDelegate(self, queue: dispatch_get_main_queue())
        
        //キューがブロックされているときに新しいフレームが来たら削除
        videoDataOutput.alwaysDiscardsLateVideoFrames = true
        
        //セッションの使用準備
        self.cpsSession = AVCaptureSession()
        
        //Input
        if(self.cpsSession.canAddInput(deviceInput))
        {
            self.cpsSession.addInput(deviceInput as AVCaptureDeviceInput)
        }
        //Output
        if(self.cpsSession.canAddOutput(videoDataOutput))
        {
            self.cpsSession.addOutput(videoDataOutput)
        }
        //解像度の指定
        self.cpsSession.sessionPreset = AVCaptureSessionPresetMedium
        
        self.cpsSession.startRunning()
    }
    func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!)
    {
        //SampleBufferから画像を取得
        let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
        let opaqueBuffer = Unmanaged<CVImageBuffer>.passUnretained(imageBuffer).toOpaque()
        let pixelBuffer = Unmanaged<CVPixelBuffer>.fromOpaque(opaqueBuffer).takeUnretainedValue()
        let outputImage = CIImage(CVPixelBuffer: pixelBuffer, options: nil)
        
        //補正
        var drawFrame = outputImage.extent()
        let imageAR = drawFrame.width / drawFrame.height
        let viewAR = videoDisplayViewRect.width / videoDisplayViewRect.height
        if imageAR > viewAR {
            drawFrame.origin.x += (drawFrame.width - drawFrame.height * viewAR) / 2.0
            drawFrame.size.width = drawFrame.height / viewAR
        } else {
            drawFrame.origin.y += (drawFrame.height - drawFrame.width / viewAR) / 2.0
            drawFrame.size.height = drawFrame.width / viewAR
        }
        
        //出力
        videoDisplayView.bindDrawable()
        if videoDisplayView.context != EAGLContext.currentContext() {
            EAGLContext.setCurrentContext(videoDisplayView.context)
        }
        renderContext.drawImage(outputImage, inRect: videoDisplayViewRect, fromRect: drawFrame)
        videoDisplayView.display()
        
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

func initDisplay()で表示用の画面を生成します。 videoDisplayViewRectにvideoDisplayViewのrectを保持しているのは出力時の補正で使用するためです。

func initCamera()でカメラの設定をします。 今回は背面カメラ(デフォルト)を使用してますが、前面カメラを使用する時は、 57行目のAVCaptureDevicePosition.Backの部分をAVCaptureDevicePosition.Frontに変更すればOKです。

self.cpsSession.sessionPresetで設定できる解像度は下記の種類があります。

AVCaptureSessionPresetPhoto 高解像度の写真画質の出力に適した設定
AVCaptureSessionPresetHigh 高品質のビデオとオーディオ出力に適した設定
AVCaptureSessionPresetMedium ビデオ出力をWiFi経由で共有するために適した設定
AVCaptureSessionPresetLow ビデオ出力を3G経由で共有するために適した設定
AVCaptureSessionPreset320x240 320×240ピクセルのビデオ出力
AVCaptureSessionPreset352x288 CIF品質(352×288ピクセル)の映像出力
AVCaptureSessionPreset640x480 VGA画質(640×480ピクセル)の映像出力
AVCaptureSessionPreset960x540 HD画質(960x540のピクセル)の映像出力
AVCaptureSessionPreset1280x720 720pの画質(1280×720ピクセル)の映像出力
AVCaptureSessionPresetiFrame960x540 960x540のiFrame H.264ビデオ ※編集アプリケーションに最適
AVCaptureSessionPresetiFrame1280x720 1280×720iFrame H.264ビデオ ※編集アプリケーションに最適

func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!)でキャプチャされたデータを変換して補正して表示します。 今回はそのまま表示しますが、後編では出力前にフィルターをかけてみたいと思います。

参考

Swiftでカメラを使う|vaguely