ミライスタート TECH系ブログ

株式会社ミライスタートのエンジニア達が気になったTECH系の記事等をアップしています!

iOS swift カメラ 2. AVCaptureStillImageOutput

こんにちわ。ミライスタートエンジニアの横田です。
今回は、AVFoundationのAVCaptureStillImageOutputを使って静止画像を取得する処理についてです。
f:id:miraistart:20160724193933p:plain

このような画面を作り、image Viewにカメラからキャプチャしたプレビューを表示させ、撮影ボタンを押した時にキャプチャと同じ写真を保存するプログラムを書きます。

import UIKit
import AVFoundation

class ViewController: UIViewController {
    //バックカメラ
    let cameraMode = AVCaptureDevicePosition.Back
    //フロントカメラ
    //let cameraMode = AVCaptureDevicePosition.Front
    
    //アウトプット作成
    let imageOutput = AVCaptureStillImageOutput()
    //ビデオレイヤー
    var videoLayer : AVCaptureVideoPreviewLayer!

    @IBOutlet weak var previewImageView: UIImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        //デバイス一覧取得
        let devices = AVCaptureDevice.devices()
        //カメラ取得
        var cameraDevice : AVCaptureDevice!
        for device in devices{
            if(device.position == cameraMode){
                cameraDevice = device as! AVCaptureDevice
            }
        }
        //セッション作成
        let session = AVCaptureSession()
        //セッションのインプットにカメラを追加
        let cameraVideoInput = try! AVCaptureDeviceInput(device: cameraDevice)
        session.addInput(cameraVideoInput)
        //セッションのアウトプットを追加
        session.addOutput(imageOutput)
        //ビデオレイヤーを作成し、カメラからのキャプチャーをプレビュー表示する
        self.videoLayer = AVCaptureVideoPreviewLayer(session: session)
        //viewDidLoadのタイミングでは通常はAutolayoutが未反映。layoutIfNeededを呼ぶと強制的に反映させることが出来る
        self.view.layoutIfNeeded()
        //プレビュー表示imageViewに重なるようvideoLayerのframeを調整
        self.videoLayer.frame = self.previewImageView.bounds
        //カメラからのキャプチャがvideoLayerの表示域からははみ出た分は非表示にする。
        self.videoLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
        self.previewImageView.layer.addSublayer(self.videoLayer)
        //プレビュー開始
        session.startRunning()

    }

    @IBAction func captureButton(sender: AnyObject) {
        //コネクション作成
        let videoConnection = imageOutput.connectionWithMediaType(AVMediaTypeVideo)
        
        imageOutput.captureStillImageAsynchronouslyFromConnection(videoConnection, completionHandler: { (imageDataBuffer, error) -> Void in
            // 取得したImageのDataBufferをJpegに変換.
            let imageData : NSData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(imageDataBuffer)
            // JpegからUIIMageを作成.
            let image : UIImage = UIImage(data: imageData)!
            
            // 取得した画像に対する処理
            AppDelegate.capturedImage = image
            
            self.performSegueWithIdentifier("captured",sender: nil)
            
        })
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


}

動かしてみますと、
f:id:miraistart:20160724194749j:plain
カメラからキャプチャした映像がプレビューとして表示されます。
当ブログ以外に同様サンプルがネット上に上がっていたのを参考に書いたのですが、数カ所ハマったところがあったので、そこだけ少し解説します。
まずself.view.layoutIfNeeded()ですが、AutolayoutでViewのサイズを指定している場合、viewDidLoadの中では通常Autolayout反映後のサイズを得られないようです。self.view.layoutIfNeeded()をpreviewImageViewのboundsを取得する前に呼び出すと上手く行きました。

また、このプレビュー状態で撮影を行った場合の写真を見てみたところ、
f:id:miraistart:20160724194754j:plain
出力された写真が細長くなってしまいました。
これは、videoLayer.videoGravity = AVLayerVideoGravityResizeAspectFillを指定しているためです。
実際のカメラのキャプチャ領域は縦長なのですが、previewImageViewの形に合わせて枠に収まらない分を切り捨ててプレビュー表示していたのです。何も調整せずにimageOutputからそのまま取得した画像だと、この切り捨てた分も含めた画像が生成されます。

ですので取得した画像を使う前に、画像でも余計な領域を切り捨てるコードを追加します。

// uiimageを表示域に合わせて切り取る
            let outputRect : CGRect = self.videoLayer.metadataOutputRectOfInterestForRect(self.videoLayer.bounds)
            let takenCGImage : CGImageRef = image.CGImage!
            
            let width : Int = CGImageGetWidth(takenCGImage);
            let height : Int = CGImageGetHeight(takenCGImage);
            let cropRect : CGRect = CGRectMake(outputRect.origin.x * CGFloat(width), outputRect.origin.y * CGFloat(height), outputRect.size.width * CGFloat(width), outputRect.size.height * CGFloat(height));
            let cropCGImage : CGImageRef = CGImageCreateWithImageInRect(takenCGImage, cropRect)!;
            let cropUIImage : UIImage = UIImage(CGImage: cropCGImage, scale: 1.0, orientation: .Right)

このようなコードを追加することで、プレビューと同じ縦横比の写真が取得できます。

f:id:miraistart:20160724194336j:plain