(注意) 追加したframework が認識されない場合は、プロジェクトの "Build Settings" -> "Search Paths" -> "Framework Search Paths" の "Debug" と "Release" の両方に opencv2.framework が存在するパスを付け加えましょう。(例では /Users/nitta/doc/iApp/2016 )
iOS -> Source -> Cocoa Touch Class -> Next を選ぶとクラス名の入力になります。ここではクラス名は ObjCWrapper、Subclass ofに NSObject, LauguageにObjective-C を選択しました。
このあと「SwiftとObjective-Cをブリッジ(橋渡し)するHeaderを作るか」と確認を求められるので "Create Bridging Header" を選択します。 ブリッジングヘッダーファイルの名前は「プロジェクト名-Bridging-Header.h」となります。
ObjCWrapper.h, ObjCWrapper.m, SwiftAVFOpenCV-Bridging-Header.h が新たに生成されました。
SwiftAVFOpenCV-Bridging-Header.hに追加するコード(赤字部分) |
#import "ObjCWrapper.h" |
ObjCWrapper.hに追加するコード(赤字部分) |
#import <Foundation/Foundation.h> #import <UIKit/UIKit.h> @interface ObjCWrapper : NSObject - (bool) isActive; - (bool) setXML: (NSString *) name; - (int) detect: (UIImage *) image founds: (NSMutableArray *) arr; @end |
ObjCWrapper.mmに追加するコード(赤字部分) |
#import "ObjCWrapper.h" #import <opencv2/opencv.hpp> #import <opencv2/highgui/ios.h> using namespace std; @implementation ObjCWrapper cv::CascadeClassifier cascade; bool active; - (bool) isActive { return active; } - (bool) setXML: (NSString *) name { NSBundle *bundle = [NSBundle mainBundle]; NSString *path = [bundle pathForResource: name ofType:@"xml"]; string cascadeName = (char *) [path UTF8String]; if (!cascade.load(cascadeName)) { return active = false; } return active = true; } - (int) detect: (UIImage *) image founds: (NSMutableArray *) arr { CGColorSpaceRef colorSpace = CGImageGetColorSpace(image.CGImage); CGFloat cols = image.size.width; CGFloat rows = image.size.height; cv::Mat mat(rows,cols,CV_8UC4); CGContextRef contextRef = CGBitmapContextCreate(mat.data, cols, rows, 8, mat.step[0], colorSpace, kCGImageAlphaNoneSkipLast); CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), image.CGImage); CGContextRelease(contextRef); vector<cv::Rect> founds; cascade.detectMultiScale(mat, founds, 1.1, 2, CV_HAAR_SCALE_IMAGE,cv::Size(30,30)); for (int i=0; i<founds.size(); i++) { cv::Rect rect = founds[i]; [arr addObject: [NSNumber numberWithInteger: rect.x]]; [arr addObject: [NSNumber numberWithInteger: rect.y]]; [arr addObject: [NSNumber numberWithInteger: rect.width]]; [arr addObject: [NSNumber numberWithInteger: rect.height]]; } return (int) [arr count]; } @end |
ViewController.swiftに追加するコード(赤字と緑字部分) |
import UIKit import AVFoundation class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate { var detector: ObjCWrapper! var mySession: AVCaptureSession! var myCamera: AVCaptureDevice! var myVideoInput: AVCaptureDeviceInput! var myVideoOutput: AVCaptureVideoDataOutput! var detectFlag: Bool = false @IBOutlet weak var myImageView: UIImageView! @IBAction func tapButton(sender: AnyObject) { mySession.startRunning() } @IBAction func tapStop(sender: AnyObject) { mySession.stopRunning() } @IBAction func tapDetect(sender: AnyObject) { detectFlag = !detectFlag } func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!) { print("captureOutput:didOutputSampleBuffer:fromConnection)") if connection.supportsVideoOrientation { connection.videoOrientation = AVCaptureVideoOrientation.Portrait } dispatch_async(dispatch_get_main_queue(), { let image = self.imageFromSampleBuffer(sampleBuffer) if self.detectFlag { self.myImageView.image = self.detectFace(image) } else { self.myImageView.image = image } }) } func imageFromSampleBuffer(sampleBuffer: CMSampleBufferRef) -> UIImage { let imageBuffer: CVImageBufferRef = CMSampleBufferGetImageBuffer(sampleBuffer)! CVPixelBufferLockBaseAddress(imageBuffer, 0) // Lock Base Address let baseAddress = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0) // Get Original Image Information let bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer) let width = CVPixelBufferGetWidth(imageBuffer) let height = CVPixelBufferGetHeight(imageBuffer) let colorSpace = CGColorSpaceCreateDeviceRGB() // RGB ColorSpace let bitmapInfo = (CGBitmapInfo.ByteOrder32Little.rawValue | CGImageAlphaInfo.PremultipliedFirst.rawValue) let context = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, colorSpace, bitmapInfo) let imageRef = CGBitmapContextCreateImage(context) // Create Quarts image CVPixelBufferUnlockBaseAddress(imageBuffer, 0) // Unlock Base Address let resultImage: UIImage = UIImage(CGImage: imageRef!) return resultImage } func prepareVideo() { mySession = AVCaptureSession() mySession.sessionPreset = AVCaptureSessionPresetHigh let devices = AVCaptureDevice.devices() for device in devices { if (device.position == AVCaptureDevicePosition.Back) { myCamera = device as! AVCaptureDevice } } do { myVideoInput = try AVCaptureDeviceInput(device: myCamera) if (mySession.canAddInput(myVideoInput)) { mySession.addInput(myVideoInput) } else { print("cannot add input to session") } myVideoOutput = AVCaptureVideoDataOutput() myVideoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey : Int(kCVPixelFormatType_32BGRA)] myVideoOutput.setSampleBufferDelegate(self,queue:dispatch_get_main_queue()) myVideoOutput.alwaysDiscardsLateVideoFrames = true if (mySession.canAddOutput(myVideoOutput)) { mySession.addOutput(myVideoOutput) } else { print("cannot add output to session") } /* // preview background let myVideoLayer = AVCaptureVideoPreviewLayer(session: mySession) myVideoLayer.frame = view.bounds myVideoLayer.videoGravity = AVLayerVideoGravityResizeAspectFill view.layer.insertSublayer(myVideoLayer,atIndex:0) */ } catch let error as NSError { print("cannot use camera \(error)") } } func detectFace(image: UIImage) -> UIImage { if (detector.isActive()) { let arr = NSMutableArray() detector.detect(image,founds: arr) let count:Int = arr.count; print(count) UIGraphicsBeginImageContext(image.size); image.drawInRect(CGRectMake(0,0,image.size.width,image.size.height)) let context: CGContextRef = UIGraphicsGetCurrentContext()! CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0) CGContextSetLineWidth(context, 5.0); for i in 0..<(count/4) { let x:Int = arr[i * 4 + 0] as! NSNumber as Int let y:Int = arr[i * 4 + 1] as! NSNumber as Int let w:Int = arr[i * 4 + 2] as! NSNumber as Int let h:Int = arr[i * 4 + 3] as! NSNumber as Int print("\(i): \(x) \(y) \(w) \(h)") CGContextAddRect(context, CGRectMake(CGFloat(x),CGFloat(y),CGFloat(w),CGFloat(h))); } CGContextStrokePath(context) let img = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return img; } return image; } override func viewDidLoad() { super.viewDidLoad() detector = ObjCWrapper(); detector.setXML("haarcascade_frontalface_alt") prepareVideo() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } } |
ウィンドウ左のProject Navigator でプロジェクトを選択して、マウスの右クリックで "Add Files to プロジェクト名"を選択します。 ファイル選択の画面ではまず Option をクリックして "Copy items if needed" にチェックをいれてから、Addします。
下に実行画面を示します。 左は Start ボタンをタップして画像のキャプチャを始めた画面です。 真ん中は Detect ボタンをタップして認識を開始した画面で、顔が7個認識されています。 右の画面では顔が6個認識されていますが、うち1個は誤認識です。
この実行例は、 素材フリーのサイト の Webページ を利用させていただきました。
iOSでは、処理をキューに登録することでマルチスレッド動作をプログラミングできるようにしています (GCD, Grand Central Dispatch)。 dispatch_sync や dispatch_async を用いると実行すべき処理をクロージャで記述できるので、マルチスレッド動作の プログラムがとても記述しやすくなっています。
上記の例では、「ビデオをフレーム毎に処理をするスレッドを登録するキュー」も、 「画像をUIImageに変換してOpenCVの顔認識処理を行うスレッドを登録するキュー」も、 同じメイン・キュー dispatch_get_main_queue() を使っています。 GUIを操作するスレッドはメイン・キュー上で動作する必要があるので後者は仕方がないのですが、 「ビデオをフレーム毎に処理をするスレッド」は新たに作成した別のキューに登録するのが一般的のようです。 ただし、その場合は、「画像をUIImageに変換してOpenCVの顔認識処理を行うスレッド」の呼び出しを dispatch_async ではなくdispatch_sync で行わないと、 顔認識の重い処理が間に合わずどんどん溜っていくのでやたらと実行が遅くなってしまいます。
以上の点を考慮してViewController.swift を書き直すとすると、次のような変更になるでしょう。 私の環境では変更後の方が若干動作が速い気がします。 が、どちらがよいかは各自で試してみて下さい。
ViewController.swiftの変更点 (diff -c の出力) |
*** ViewController.swift.org 2016-06-21 08:59:25.000000000 +0900 --- ViewController.swift 2016-06-21 08:59:43.000000000 +0900 *************** *** 38,44 **** if connection.supportsVideoOrientation { connection.videoOrientation = AVCaptureVideoOrientation.Portrait } ! dispatch_async(dispatch_get_main_queue(), { let image = self.imageFromSampleBuffer(sampleBuffer) if self.detectFlag { self.myImageView.image = self.detectFace(image) --- 38,44 ---- if connection.supportsVideoOrientation { connection.videoOrientation = AVCaptureVideoOrientation.Portrait } ! dispatch_sync(dispatch_get_main_queue(), { let image = self.imageFromSampleBuffer(sampleBuffer) if self.detectFlag { self.myImageView.image = self.detectFace(image) *************** *** 87,93 **** myVideoOutput = AVCaptureVideoDataOutput() myVideoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey : Int(kCVPixelFormatType_32BGRA)] ! myVideoOutput.setSampleBufferDelegate(self,queue:dispatch_get_main_queue()) myVideoOutput.alwaysDiscardsLateVideoFrames = true if (mySession.canAddOutput(myVideoOutput)) { mySession.addOutput(myVideoOutput) --- 87,93 ---- myVideoOutput = AVCaptureVideoDataOutput() myVideoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey : Int(kCVPixelFormatType_32BGRA)] ! myVideoOutput.setSampleBufferDelegate(self,queue:dispatch_queue_create("myqueue",nil)) myVideoOutput.alwaysDiscardsLateVideoFrames = true if (mySession.canAddOutput(myVideoOutput)) { mySession.addOutput(myVideoOutput) |