ちょっと話題の記事

OpenCVを利用したリアルタイムフィルタリングの基本

2013.04.07

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

お久しぶりです。CM iOS部隊所属の平井です。今回は久しぶりにOpenCVネタをやっていこうと思います。

iOS6ではCore Imageが強化され色々使えるフィルタも増えたようですが、実際業務で使おうとするとiOSのバージョンだったりで使えないことがほとんど。なのでやっぱりまだOpenCVは手放せそうにないですね。

で、今回はせっかくなので撮影中のカメラの映像をリアルタイムでフィルタするサンプルを作りたいと思います。では早速。

ちなみに今回は以下の環境を前提に説明します。

Mac OS X 10.8 Moutain lion
Xcode 4.6.1
iOS SDK 6.1
iPhone 5

サンプルプロジェクトのダウンロード

今回紹介するiOSアプリのソースコードをGitHubにあげてあるのでダウンロードしてください。
hirai-yuki/RealTimeProcessing

実機につないでプロジェクトを実行すると、以下のように白黒画像とカメラボタンが表示されるはず。

実行例

次回以降はこのサンプルプロジェクトをいじっていく予定です。今回は要点だけ。

必要なフレームワーク・ライブラリの設定

今回必要なフレームワークは以下のようになります。

  • AVFoundation.framework
  • CoreVideo.framework
  • CoreMedia.framework
  • AudioToolbox.framework
  • QuartzCore.framework
  • opencv2.framework

iOS SDKのフレームワークをプロジェクトに追加

このうちopencv2.framework以外はiOS SDKに入ってますので、まずはこいつらをプロジェクトで使用するように設定しちゃってください。ちなみにAudioToolbox.frameworkはシャッター音を出すの使うだけなので、実質あんまり関係ないです。

opencv2.frameworkのダウンロードと追加

SourceForgeからOpenCVのフレームワークをダウンロードします。ダウンロードするバージョンは2.4.5です。以下のURLからダウンロード、解凍してプロジェクトに追加します。 てか、知らないうちに2.4.5になってる・・・。

OpenCV - Browse /opencv-ios/2.4.5 at SourceForge.net

フレームワークの設定

プロジェクトの設定はこれだけです。OpenCVを使うまでがすごく簡単になりましたね!

主なクラス

今回のサンプルで使用する主なクラスは以下の通りです。

AVFoundationUtilクラス
AVFoundation.framework用ユーティリティクラス
OpenCVUtilクラス
OpenCV用ユーティリティクラス
ViewControllerクラス
メイン画面
MonochromeFilterクラス
白黒に加工するだけ(今回は簡単にいきましょう!)

AVFoundationUtilクラス

AVFoundation.framework用ユーティリティクラスです。サンプルバッファからCGImageを生成したり、カメラの向きを判別したりするメソッドを定義します。

AVFoundationUtil.h
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import <CoreVideo/CoreVideo.h>

/**
 AVFoundation.framework用ユーティリティクラス
 */
@interface AVFoundationUtil : NSObject

/**
 サンプルバッファのデータから`UIImage`インスタンスを生成する
 
 @param     sampleBuffer       サンプルバッファ
 @return    生成した`UIImage`インスタンス
 */
+ (UIImage *)imageFromSampleBuffer:(CMSampleBufferRef)sampleBuffer;

/**
 デバイスの向きからカメラAPIの向きを判別する
 
 @param     deviceOrientation   デバイスの向き
 @return    カメラの向き
 */
+ (AVCaptureVideoOrientation)videoOrientationFromDeviceOrientation:(UIDeviceOrientation)deviceOrientation;

@end
AVFoundationUtil.m
#import "AVFoundationUtil.h"

@implementation AVFoundationUtil

+ (UIImage *)imageFromSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    
    // ピクセルバッファのベースアドレスをロックする
    CVPixelBufferLockBaseAddress(imageBuffer, 0);
    
    // Get information of the image
    uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);
    
    size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
    size_t width = CVPixelBufferGetWidth(imageBuffer);
    size_t height = CVPixelBufferGetHeight(imageBuffer);
    
    // RGBの色空間
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    
    CGContextRef newContext = CGBitmapContextCreate(baseAddress,
                                                    width,
                                                    height,
                                                    8,
                                                    bytesPerRow,
                                                    colorSpace,
                                                    kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
    
    CGImageRef imageRef = CGBitmapContextCreateImage(newContext);
    UIImage *ret = [UIImage imageWithCGImage:imageRef];
    
    CGImageRelease(imageRef);
    CGContextRelease(newContext);
    CGColorSpaceRelease(colorSpace);
    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
    
    return ret;
}

+ (AVCaptureVideoOrientation)videoOrientationFromDeviceOrientation:(UIDeviceOrientation)deviceOrientation
{
    AVCaptureVideoOrientation orientation;
    switch (deviceOrientation) {
        case UIDeviceOrientationUnknown:
            orientation = AVCaptureVideoOrientationPortrait;
            break;
        case UIDeviceOrientationPortrait:
            orientation = AVCaptureVideoOrientationPortrait;
            break;
        case UIDeviceOrientationPortraitUpsideDown:
            orientation = AVCaptureVideoOrientationPortraitUpsideDown;
            break;
        case UIDeviceOrientationLandscapeLeft:
            orientation = AVCaptureVideoOrientationLandscapeRight;
            break;
        case UIDeviceOrientationLandscapeRight:
            orientation = AVCaptureVideoOrientationLandscapeLeft;
            break;
        case UIDeviceOrientationFaceUp:
            orientation = AVCaptureVideoOrientationPortrait;
            break;
        case UIDeviceOrientationFaceDown:
            orientation = AVCaptureVideoOrientationPortrait;
            break;
    }
    return orientation;
}

@end

OpenCVUtilクラス

OpenCV用ユーティリティクラスです。CGImageとOpenCV画像(IplImageインスタンス)の相互変換用メソッドを定義します。実装ファイルの拡張子を.mmにすることを忘れずに!!

OpenCVUtil.h
#import <Foundation/Foundation.h>
#import <opencv2/opencv.hpp>

/**
 OpenCV用ユーティリティクラス
 */
@interface OpenCVUtil : NSObject

/**
 `UIImage`インスタンスをOpenCV画像データに変換するメソッド
 
 @param     image       `UIImage`インスタンス
 @return    `IplImage`インスタンス
 */
+ (IplImage *)IplImageFromUIImage:(UIImage *)image;

/**
 OpenCV画像データを`UIImage`インスタンスに変換するメソッド
 
 @param     image `IplImage`インスタンス
 @return    `UIImage`インスタンス
 */
+ (UIImage *)UIImageFromIplImage:(IplImage*)image;

@end
OpenCVUtil.mm
#import "OpenCVUtil.h"

@implementation OpenCVUtil

+ (IplImage *)IplImageFromUIImage:(UIImage *)image
{
    CGImageRef imageRef = image.CGImage;
    
    // RGB色空間を作成
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    
    // 一時的なIplImageを作成
    IplImage *iplimage = cvCreateImage(cvSize(image.size.width,image.size.height), IPL_DEPTH_8U, 4);
    
    // CGBitmapContextをIplImageのビットマップデータのポインタから作成
    CGContextRef contextRef = CGBitmapContextCreate(
                                                    iplimage->imageData,
                                                    iplimage->width,
                                                    iplimage->height,
                                                    iplimage->depth,
                                                    iplimage->widthStep,
                                                    colorSpace,
                                                    kCGImageAlphaPremultipliedLast|kCGBitmapByteOrderDefault);
    
    // CGImageをCGBitmapContextに描画
    CGContextDrawImage(contextRef,
                       CGRectMake(0, 0, image.size.width, image.size.height),
                       imageRef);
    
    // ビットマップコンテキストと色空間を解放
    CGContextRelease(contextRef);
    CGColorSpaceRelease(colorSpace);
    
    // 最終的なIplImageを作成
    IplImage *ret = cvCreateImage(cvGetSize(iplimage), IPL_DEPTH_8U, 3);
    
    // 一時的なIplImageを解放
    cvCvtColor(iplimage, ret, CV_RGBA2BGR);
    cvReleaseImage(&iplimage);
    
    return ret;
}

+ (UIImage *)UIImageFromIplImage:(IplImage*)image
{
    CGColorSpaceRef colorSpace;
    if (image->nChannels == 1) {
        colorSpace = CGColorSpaceCreateDeviceGray();
    } else {
        colorSpace = CGColorSpaceCreateDeviceRGB();
        //BGRになっているのでRGBに変換
        cvCvtColor(image, image, CV_BGR2RGB);
    }
    
    // IplImageのビットマップデータのポインタアドレスからNSDataを作成
    NSData *data = [NSData dataWithBytes:image->imageData length:image->imageSize];
    
    CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
    
    // CGImageを作成
    CGImageRef imageRef = CGImageCreate(image->width,
                                        image->height,
                                        image->depth,
                                        image->depth * image->nChannels,
                                        image->widthStep,
                                        colorSpace,
                                        kCGImageAlphaNone|kCGBitmapByteOrderDefault,
                                        provider,
                                        NULL,
                                        false,
                                        kCGRenderingIntentDefault
                                        );
    
    // UIImageを生成
    UIImage *ret = [UIImage imageWithCGImage:imageRef];
    
    CGImageRelease(imageRef);
    CGDataProviderRelease(provider);
    CGColorSpaceRelease(colorSpace);
    
    return ret;
}

@end

ViewControllerクラス

撮影中の映像をリアルタイムに加工したものを表示したり、撮影したりする画面です。このクラスの特に重要なメソッドだけのせておきます。

ViewController.m

- (void)setupAVCapture { // 入力と出力からキャプチャーセッションを作成 self.session = [[AVCaptureSession alloc] init];

if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { self.session.sessionPreset = AVCaptureSessionPreset640x480; } else { self.session.sessionPreset = AVCaptureSessionPresetPhoto; }

// カメラからの入力を作成 AVCaptureDevice *device;

// フロントカメラを検索 for (AVCaptureDevice *d in [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) { if ([d position] == AVCaptureDevicePositionFront) { device = d; self.isUsingFrontFacingCamera = YES; break; } } // フロントカメラがなければデフォルトのカメラ(バックカメラ)を使用 if (!device) { self.isUsingFrontFacingCamera = NO; device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; }

// カメラからの入力を作成 NSError *error = nil; AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];

if (error) { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle: [NSString stringWithFormat:@"Failed with error %d", (int)[error code]] message:[error localizedDescription] delegate:nil cancelButtonTitle:@"Dismiss" otherButtonTitles:nil]; [alertView show]; [self teardownAVCapture]; return; }

// キャプチャーセッションに追加 if ([self.session canAddInput:deviceInput]) { [self.session addInput:deviceInput]; }

// 画像への出力を作成 self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];

self.videoDataOutput.alwaysDiscardsLateVideoFrames = YES;

// ビデオへの出力の画像は、BGRAで出力 self.videoDataOutput.videoSettings = @{(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA)};

// ビデオ出力のキャプチャの画像情報のキューを設定 self.videoDataOutputQueue = dispatch_queue_create("VideoDataOutputQueue", DISPATCH_QUEUE_SERIAL); [self.videoDataOutput setSampleBufferDelegate:self queue:self.videoDataOutputQueue];

// キャプチャーセッションに追加 if ([self.session canAddOutput:self.videoDataOutput]) { [self.session addOutput:self.videoDataOutput]; }

// ビデオ入力のAVCaptureConnectionを取得 AVCaptureConnection *videoConnection = [self.videoDataOutput connectionWithMediaType:AVMediaTypeVideo]; videoConnection.videoOrientation = [AVFoundationUtil videoOrientationFromDeviceOrientation:[UIDevice currentDevice].orientation];

// 1秒あたり64回画像をキャプチャ videoConnection.videoMinFrameDuration = CMTimeMake(1, 64);

// 開始 [self.session startRunning]; }

// キャプチャー情報をクリーンアップ - (void)teardownAVCapture { self.videoDataOutput = nil; if (self.videoDataOutputQueue) { #if __IPHONE_OS_VERSION_MIN_REQUIRED < 60000 dispatch_release(self.videoDataOutputQueue); #endif } } // 画像の加工処理 - (void)process:(UIImage *)image { // 画像を白黒に加工 UIImage *processedImage = [MonochromeFilter doFilter:image]; // 加工した画像をプレビューレイヤーに追加 self.previewLayer.contents = (__bridge id)(processedImage.CGImage); // フロントカメラの場合は左右反転 if (self.isUsingFrontFacingCamera) { self.previewLayer.affineTransform = CGAffineTransformMakeScale(-1.0f, 1.0f); } } #pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate methods // AVCaptureVideoDataOutputSampleBufferDelegateプロトコルのメソッド。 // 新しいキャプチャの情報が追加されたときに呼び出される。 - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { // キャプチャしたフレームからCGImageを作成 UIImage *image = [AVFoundationUtil imageFromSampleBuffer:sampleBuffer]; // 画像を画面に表示 dispatch_async(dispatch_get_main_queue(), ^{ [self process:image]; }); } [/c]

いろんなフィルターを試してみよう!

これで、リアルタイムに動画を加工するための土台ができました。次回は色々なフィルターで試して見ましょう!