[iOS] AVFoundation+OpenCVで劇画調カメラを作ってみた

ios

1 はじめに

AVFoundationで、AVCaptureVideoDataOutputを出力に指定すると、1/30フレームなどの画像を取得可能です。

今回は、取得した画像データをフィルターで白黒の2値画像にすることで、劇画調の写真を撮るカメラを作成してみました。

iOSでフィルターを掛ける方法は、CIFilterなど、方法が色々ありますが、今回は、比較的自由に調整が可能なOpenCV(Ver3.x)を使用しました。

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

2 入出力

劇画調カメラは、AVCaptudeSessionの入力として、カメラ、出力に、動画データであるAVCaptureVideoDataOutputを繋ぎます。 そして、この出力をプレビュー(UIImageVIew)に表示する前に、OpenCVでフィルターを掛けています。

003

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

<br />// セッションのインスタンス生成
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では、AVCaptureVideoDataOutputSampleBufferDelegateプロトコルを実装したクラスのcaptureOutput(_:didOutputMetadataObjects:from:)でフレーム毎のデータを受け取ることが出来ます。

今回は、ここで受け取ったデータを、一旦UIImageに変換してから、OpenCVでフィルターを掛け、それをプレビュー画面であるUIImageViewにに表示しています。(AVCaptureSessionにプレビューは設定していません)

// 新しいキャプチャの追加で呼ばれる
func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {

    // UIImageへの変換
    let image = imageFromSampleBuffer(sampleBuffer: sampleBuffer)
    // OpenCVでフィルタしてプレビューに表示
    imageView.image = openCv.filter(image)

}

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 OpenCV

(1) CocoaPods

OpenCVは、CococaPodsでインストール可能です。

Podfile

pod 'OpenCV'

2017.02.19現在の最新は、3.1.0.1です。

(2) Bridgingファイルの作成

C++のライブラリであるOpenCVをSwiftから使用するには、Bridgingファイルが必要です。今回は、SwiftのプロジェクトにObjective-Cのファイルを追加した際に起動する自動生成を利用しました。

File > New > File から Objective-C Fileを選択し、Objective-Cのファイルを追加します。(ここでは名前をOpenCv.mとしました)

001

ここで表示されるポップアップでCreate Bridging Headerを選択します。

002

Bridgingファイルは、プロジェクト名-Bridging-Header.hという名前で生成されます。 その後、ファイルの拡張子を.mmに変更しています。(OpenCv.mm

(3) フィルタークラスの実装

作成した、OpenCVによるフィルターの実装は、次のようなものです。

最初に、受け取ったUIImageの縦横を調整した後、Mat形式に変換してグレースケールにしています。 (グレースケールにしている理由は、以降のフィルターがグレースケールにしか適用できないためです。)

フィルタの種類は、「GaussianBlur」「treshold」「adaptiveThreshold」の3種類です。

アプリからの設定値に基づいてフィルターを掛け、最終的にUIImageに戻しています。

OpenCv.mm

@implementation OpenCv : NSObject

-(UIImage *)Filter:(UIImage *)image {

    // 縦横を修正
    UIGraphicsBeginImageContext(image.size);
    [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
    image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    //UIImageをcv::Matに変換
    cv::Mat mat;
    UIImageToMat(image, mat);
    //グレースケールに変更
    cv::cvtColor(mat,mat,CV_BGR2GRAY);

    //Blur ぼかし
    if(_useBlur) {
        // kSizeは奇数のみ
        int kSize = _blur0;
        if(kSize % 2 == 0) {
            kSize += 1;
        }
        cv::GaussianBlur(mat, mat, cv::Size(kSize,kSize), _blur1);
    }

    // 閾値
    if(_useTreshold) {
        cv::threshold(mat, mat, 0, 255, cv::THRESH_BINARY|cv::THRESH_OTSU);
    }

    // 適応閾値
    if(_useAdaptiveTreshold) {

        // blockSizeは奇数のみ
        int blockSize = _adaptiveThreshold0;
        if(blockSize % 2 == 0) {
            blockSize += 1;
        }
        cv::adaptiveThreshold(mat, mat, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY, blockSize, _adaptiveThreshold1);
    }

    return MatToUIImage(mat);
}

@end

これをSwiftから利用するためのBridgingファイルは、以下のとおりです。

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface OpenCv : NSObject

- (UIImage *)Filter:(UIImage *)image;

@property bool useBlur;
@property int blur0;
@property int blur1;
@property bool useTreshold;
@property bool useAdaptiveTreshold;
@property int adaptiveThreshold0;
@property int adaptiveThreshold1;

@end

Swiftから利用する場合は、ファンクション名が、lower camel caseになっている事に注意が必要です。

imageView.image = openCv.filter(image)
@IBAction func changeBlurSwitch(_ sender: UISwitch) {
    openCv.useBlur = sender.isOn
    blur0Slider.isEnabled = sender.isOn
    blur1Slider.isEnabled = sender.isOn
}

@IBAction func changeBlur0Slider(_ sender: UISlider) {
    openCv.blur0 = Int32(sender.value)
}

5 動作確認

サンプルアプリを実行している様子です。

プレビュー画面をタップすると、フィルターの設定値を変更するビューが表示されます。ここでの変更は、リアルタイムで画面で確認できます。 画面下部のシャッターを押すと、その時のUIImageが、カメラロールに保存されます。

6 最後に

今回は、OpneCVでリアルタイムにフィルターを掛けることで、色々な設定値を試しながら、劇画調の写真を撮影するカメラを作成してみました。 OpenCVを利用することで、画像を自由な操作できますので、色々面白そうなものが作れそうな予感がします。

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

004 005 006 007

参考リンク


OpenCVを利用したリアルタイムフィルタリングの基本
API Reference AVCaptureVideoDataOutput
[iOS] AVFundationを使用して、「ビデオ録画」や「連写カメラ」や「QRコードリーダー」や「バーコードリーダー」を作ってみた