Swift + OpenCVでリアルタイムに顔認識してみた2

前回に引き続き、今回はOpenCVを使って顔認識を行います。

分類器

画像データの中から顔を検出するための分類器となるファイルを用意します。
Githubからこちらをダウンロードします。
ダウンロードしたデータの中から、data/haarcascades/haarcascade_frontalface_alt.xmlファイルを取り出し、プロジェクトに追加します。

顔認識

OpenCVの処理はC++で書く必要があります。そのためにObjective-Cでラップしてあげます。
まずXcodeで「New File」から「Objective-C」のファイルを追加します。すると「Would you like to configure an Objective-C bridging header?」と聞かれるのでYesとします。 作成された.mファイルの名前を変更して.mmファイルとするとC++が使用できます。

では処理を書いていきます。

OpenCVSample-Bridging-Header.h

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

@interface Detector: NSObject

- (id)init;
- (UIImage *)recognizeFace:(UIImage *)image;

@end

Detector.mm


#import "OpenCVSample-Bridging-Header.h"
#import <opencv2/opencv.hpp>
#import <opencv2/highgui/ios.h>

@interface Detector()
{
    cv::CascadeClassifier cascade;
}
@end

@implementation Detector: NSObject

- (id)init {
    self = [super init];
    
    // 分類器の読み込み
    NSBundle *bundle = [NSBundle mainBundle];
    NSString *path = [bundle pathForResource:@"haarcascade_frontalface_alt" ofType:@"xml"];
    std::string cascadeName = (char *)[path UTF8String];
    
    if(!cascade.load(cascadeName)) {
        return nil;
    }
    
    return self;
}

- (UIImage *)recognizeFace:(UIImage *)image {
    // UIImage -> cv::Mat変換
    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 |
                                                    kCGBitmapByteOrderDefault);
    
    CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), image.CGImage);
    CGContextRelease(contextRef);
    
    // 顔検出
    std::vector<cv::Rect> faces;
    cascade.detectMultiScale(mat, faces,
                             1.1, 2,
                             CV_HAAR_SCALE_IMAGE,
                             cv::Size(30, 30));

    // 顔の位置に丸を描く
    std::vector<cv::Rect>::const_iterator r = faces.begin();
    for(; r != faces.end(); ++r) {
        cv::Point center;
        int radius;
        center.x = cv::saturate_cast<int>((r->x + r->width*0.5));
        center.y = cv::saturate_cast<int>((r->y + r->height*0.5));
        radius = cv::saturate_cast<int>((r->width + r->height));
        cv::circle(mat, center, radius, cv::Scalar(80,80,255), 3, 8, 0 );
    }
    
    
    // cv::Mat -> UIImage変換
    UIImage *resultImage = MatToUIImage(mat);
    
    return resultImage;
}

@end

イニシャライザで分類器を読み込んでおきます。
recognizeFaceで受け取ったUIImageをOpenCVで扱うデータ構造のcv::Matへ変換し、顔検出の処理を行ったあと、最後にまたUIImageへ変換して返しています。

実は、UIImage->cv::Mat変換ではcvMatFromUIImageメソッドがあり、簡単に変換できるようなのですが、何度か繰り返しこのメソッドを実行していくと不意に実行時エラーとなってしまいました。原因不明だったので、その処理を置き換えています。

最後にカメラからの画像データを変換する処理を次のように変更して終わりです。

    // 顔検出オブジェクト
    let detector = Detector()

   // ---------- 省略 ---------- //


    // 毎フレーム実行される処理
    func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!)
    {
        dispatch_async(dispatch_get_main_queue(), {
            // UIImageへ変換
            let image = CameraUtil.imageFromSampleBuffer(sampleBuffer)
            
            // 顔認識
            let faceImage = self.detector.recognizeFace(image)
            
            // 表示
            self.imageView.image = faceImage
        })
    }

これで実行すると、カメラに写した顔の位置に丸が描かれるはずです。
(何にも場所に反応されるとちょっとビビります・・・)

たぶん顔認識の処理のところで工夫すると、パフォーマンスをあげられるんだろうけど良く分かりません!!
どなたか教えてくださいませませ。

Githubにソースを上げておきます。

参考

OpenCV iOS - Image Processing

2014/11/30 追記

上記のコードにはメモリ処理上の問題があることを教えていただきました!
[自在]OpenCVかCIDetectorを使ってiOSで顔認識してみよう!

dispatch_asyncをdispatch_syncに変更で、明らかにメモリ使用量が改善します。

感謝感謝!