iOSプログラミング with Swift 2


2016.06.20: created by

SwiftでOpenCVを利用して顔認識する(AVFoundationの例)

SwiftでCamera を使う(AVFoundation 経由)」 と 「SwiftでOpenCVを利用して顔認識する(もっとも単純な例)」 を組み合わせて、リアルタイムで顔認識をする Swift のプログラムを作成してみましょう。
  1. OpenCV のiOS用バイナリ を用意します。今回は http://opencv.org/ で公開されている OpenCV for iOS から opencv2.framework.zip をダウンロードします。 学内からはこちらからが速くダウンロードできます。 展開して opencv2.framework フォルダを生成します。
  2. Xcode を起動して "Create a new Xcode project" で "Single View Application" として新しいプロジェクトを開きます。 ここではプロジェクト名を SwiftAVFOpenCV としています。



  3. プロジェクトの最初に表示される画面は、ウィンドウの左側に表示されている "Show the project navigator" アイコンをクリックしても表示されます。 "General" -> "Linked Frameworks and Libraries" ->"+"ボタン を押して Add で AVFoundation.framework を追加します。












  4. さらに同じ画面で、"General" -> "Linked Frameworks and Libraries" ->"+"ボタン を押して Add Other で OpenCV2.framework を追加します。









  5. (注意) 追加したframework が認識されない場合は、プロジェクトの "Build Settings" -> "Search Paths" -> "Framework Search Paths" の "Debug" と "Release" の両方に opencv2.framework が存在するパスを付け加えましょう。(例では /Users/nitta/doc/iApp/2016 )




  6. ウィンドウ左の Project Navigator のプロジェクト名の上で、マウスを左クリックします。 メニューの中から "New File..." を選択します。



  7. 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 が新たに生成されました。




  8. ObjCWrapper.m のファイル名をObjCWrapper.mm に変更します。 この変更によって ObjCWrapper.mm の中で C++ が使えるようになります。



  9. "プロジェクト名-Bridging-Header.h" (この例では "SwiftAVFOpenCV-Bridging-Header.h") に Objective-C のヘッダファイルを imporotする記述を追加します。この記述によって、このプロジェクト内の Swift プログラムから Objective-C のクラスが参照できるようになります。
  10. SwiftAVFOpenCV-Bridging-Header.hに追加するコード(赤字部分)
    #import "ObjCWrapper.h"
    
  11. ObjCWrapperクラスにプロパティやメソッドの定義を追加します。
  12. ObjCWrapper.h はブリッジヘッダにincludeされて Swift のプログラムから参照されるので、 ObjCWrapper.h に opencv2/opencv.hpp をincludeしてはいけないことに注意しましょう。 Foundation/Foundation.h だけが importされていますので UIKit/UIKit.h もimport します。
    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
    
  13. Main.storyboard のViewController 上に Button を3個と Image Viewを配置します。 Buttonの表記はそれぞれ "Start", "Stop", "Detect" に変えておきます。



  14. Main.storyboard上を3個のButtonをそれぞれViewController.swift中に Action (Touch Up Inside) で, Image View はOutlet で connect します。



  15. ViewController.swift を変更します。
  16. 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()
        }
    
    }
    
  17. プロジェクトに、OpenCVの認識用 xml ファイル haarcascade_frontalface_alt.xml を追加します。
  18. ウィンドウ左のProject Navigator でプロジェクトを選択して、マウスの右クリックで "Add Files to プロジェクト名"を選択します。 ファイル選択の画面ではまず Option をクリックして "Copy items if needed" にチェックをいれてから、Addします。













  19. 実行すると Button が3個表示され、Image Viewは真っ白なままです。 ここでStart Buttonを押すと Video 画像の表示が始まり、Detect Buttonで顔認識をするかどうか切り替えます。 顔認識した領域は赤い枠で囲まれて表示されます。 顔認識を開始するとフレームの更新がかなり遅くなります。
  20. 下に実行画面を示します。 左は Start ボタンをタップして画像のキャプチャを始めた画面です。 真ん中は Detect ボタンをタップして認識を開始した画面で、顔が7個認識されています。 右の画面では顔が6個認識されていますが、うち1個は誤認識です。


    --> -->

    この実行例は、 素材フリーのサイト Webページ を利用させていただきました。

  21. サンプルのプロジェクトはこちら。(Xcode 7.3.1版)。 (注意) ダウンロードしたプロジェクトを各自の環境で動かすには、 プロジェクトの Bulding Settings の Search Framework Paths に opencv2.framework の親フォルダへのパスを設定し直す必要があると思われます。
  22. OpenCV-2.4と同時に配布されている認識用のXMLファイルの一覧はこちら。 また、画像ファイル例はこちら
  23. マルチ・スレッド動作について(2016/06/21 追記)
  24. 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)
    
    


http://nw.tsuda.ac.jp/