OpenCVを利用したリアルタイムフィルタリングの基本
お久しぶりです。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]
いろんなフィルターを試してみよう!
これで、リアルタイムに動画を加工するための土台ができました。次回は色々なフィルターで試して見ましょう!