dlibを使用してiOSで顔の輪郭検出、オブジェクト候補領域抽出を行う

[dlib](https://github.com/davisking/dlib)は、OpenCVのようなコンピュータビジョン系の機能や機械学習のツールなどを含むライブラリです。dlibを使って、iOS上で顔の輪郭検出とオブジェクトの候補領域抽出をテストしたのでメモとして残します。

![dlib_testapp.jpg](https://qiita-image-store.s3.amazonaws.com/0/9042/c1fb73fb-2589-60e2-6f2b-c4ce58fe5b7d.jpeg "dlib test app")

dlibの特徴については[公式サイト](http://dlib.net)の冒頭で次のように説明されています。

>Dlib is a modern C++ toolkit containing machine learning algorithms and tools for creating complex software in C++ to solve real world problems. It is used in both industry and academia in a wide range of domains including robotics, embedded devices, mobile phones, and large high performance computing environments. Dlib's open source licensing allows you to use it in any application, free of charge.

筆者環境はXcode 8.1 + iPhone 7で、テストアプリはSwiftプロジェクトにObjective C++を混在させるかたちで開発しています。一応、下記記事の続きといえば続きです。
[TensorFlowをiOSアプリ（Swiftプロジェクト）に実装する](http://qiita.com/hirokimu/items/ae5100ac4dc361ee0fcd)


**開発手順**
1. dlibをiOS用にビルドする
2. iOSプロジェクトへの組み込み
3. Objective C++での開発

#1. dlibをiOS用にビルドする

iOSでdlibを使うには、ライブラリをビルドして用意する必要があります。まず、Stack Overflowの[こちらの投稿](http://stackoverflow.com/questions/34591254/how-to-build-dlib-for-ios/35058969#35058969)の手順を行います。

cmakeによるビルドが完了すると、dlib-master/examples/build/dlib_build にXcodeプロジェクト（dlib.xcodeproj）が生成されます。このプロジェクトのdlibターゲットはプラットフォームやアーキテクチャの設定がMac OS用になっているので、Build Settingsの各所を自分の望むiOS用の設定に変更します。

変更してターゲットのビルドを行うと、dlib-master/examples/build/dlib_build/Release-iphoneos（リリースビルドの場合）にlibdlib.a が生成されます。

#2. iOSプロジェクトへの組み込み

##ライブラリの追加

libdlib.a をプロジェクトに追加し、Build SettingsのHeader Search Pathには**dlibディレクトリの上の階層**を指定します（例えばdlib-master/dlib ではなく dlib-master を指定）。
また、Processor Macrosに次のマクロを追加します。

+ DLIB_USE_LAPACK
+ DLIB_USE_BLAS
+ DLIB_NO_GUI_SUPPORT
+ DLIB_JPEG_SUPPORT
+ NDEBUG

##OpenCVの追加

dlibにはOpenCV連携が実装されていて、cv::Matなどを扱う場面が出てきます。このため、OpenCVもプロジェクトに追加しておきます。CocoaPodsで追加すれば大丈夫です。

#3. Objective C++での開発

##OpenCV用のカテゴリの追加

UIImageからcv::Matを生成するためのメソッドを追加しておきます。

```objc++:UIImage+OpenCV.mm
#import "UIImage+OpenCV.h"

@implementation UIImage (OpenCV)

- (cv::Mat)createCVMat {
    
    CGColorSpaceRef colorSpace = CGImageGetColorSpace(self.CGImage);
    CGFloat cols = self.size.width * self.scale;
    CGFloat rows = self.size.height * self.scale;
    
    cv::Mat cvMat(rows, cols, CV_8UC4);
    
    CGContextRef contextRef = CGBitmapContextCreate(cvMat.data,
                                                    cols,
                                                    rows,
                                                    8,
                                                    cvMat.step[0],
                                                    colorSpace,
                                                    kCGImageAlphaNoneSkipLast |
                                                    kCGBitmapByteOrderDefault);
    
    CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), self.CGImage);
    CGContextRelease(contextRef);
    
    return cvMat;
}
```


##顔の輪郭検出

dlibのface detectorを使うと、顔の領域だけでなく輪郭やパーツの位置まで検出することができます。
使用するためにはまず、判定に使う学習済みモデルを読み込ませておく必要があります。これは frontal_face_detector.h ファイルに記載のある下記URLからダウンロードが可能です。入手してプロジェクトに追加します。
http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2

次のような感じで検出を行います。detectFacesメソッドにUIImageを渡すと、NSValueの配列で座標データが返されるようにしています。その結果を描画したのが記事冒頭の左側のスクリーンショットです。

```objc++:DLWrapper.mm
#import <opencv2/opencv.hpp>
#import "DLWrapper.h"
#import "UIImage+OpenCV.h"
#import "dlib/opencv.h"
#import "dlib/image_processing/frontal_face_detector.h"
#import "dlib/image_processing.h"
#import "dlib/image_transforms/segment_image.h"
#import "dlib/image_transforms/segment_image_abstract.h"
#import "dlib/matrix/matrix_exp_abstract.h"

using namespace std;

@implementation DLWrapper {
    dlib::frontal_face_detector detector;
    dlib::shape_predictor predictor;
}

static DLWrapper *_instance = nil;

+ (instancetype)instance {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[DLWrapper alloc] init];
    });
    return _instance;
}

- (id)init {
    if (!(self = [super init])) return nil;

    detector = dlib::get_frontal_face_detector();    
    NSString *path = [[NSBundle mainBundle] pathForResource:@"shape_predictor_68_face_landmarks" ofType:@"dat"];
    const char *cpath = [path UTF8String];
    dlib::deserialize(cpath) >> predictor;

    return self;
}

- (nonnull NSArray<NSArray<NSValue *> *> *)detectFaces:(nonnull UIImage *)image {

    CGFloat w = image.size.width * image.scale;
    CGFloat h = image.size.height * image.scale;

    cv::Mat rgbaMat = [image createCVMat];
    cv::Mat rgbMat = cv::Mat(w, h, CV_8UC3);
    cv::cvtColor(rgbaMat, rgbMat, CV_RGBA2RGB, 3);
    
    dlib::cv_image<dlib::bgr_pixel> cvImage(rgbMat);

    vector<dlib::rectangle> faces = detector(cvImage);

    NSMutableArray<NSArray *> *allFaces = [NSMutableArray array];
    
    for (auto face: faces) {

        NSMutableArray<NSValue *> *values = [NSMutableArray array];
        
        // facial rect
        auto shape = predictor(cvImage, face);
        auto r = shape.get_rect();
        [values addObject:[NSValue valueWithCGRect:CGRectMake(r.left(), r.top(), r.width(), r.height())]];
        
        // facial shape
        for (auto i = 0; i < shape.num_parts(); i++) {
            auto r = shape.part(i);
            [values addObject:[NSValue valueWithCGPoint:CGPointMake(r.x(), r.y())]];
        }
        
        [allFaces addObject:values];
    }

    return allFaces;
}
```


##オブジェクトの領域候補抽出
ディープラーニング（CNN）を使用した判定器を使ってオブジェクトを検出・判定したいような場合、画像の全領域をくまなく探し回るよりも、オブジェクトと思しき領域の候補をまず抽出して絞り込めれば理想的です。
dlibには領域候補抽出を行う関数 find_candidate_object_locations があるので、これを使用します。

第3〜第5引数は省略可能ですが、特に第4引数（min_size）がデフォルト値のままだと膨大な数の領域が抽出されてしまうので注意が必要です。引数については[ドキュメント](http://dlib.net/python/#dlib.find_candidate_object_locations)を参照してください。
次のコードのテスト結果を描画したものが、記事冒頭の右側のスクリーンショットです。


```objc++:DLWrapper.mm
- (nonnull NSArray<NSValue *> *)detectCandidates:(nonnull UIImage *)image {
    
    CGFloat w = image.size.width * image.scale;
    CGFloat h = image.size.height * image.scale;

    cv::Mat rgbaMat = [image createCVMat];
    cv::Mat rgbMat = cv::Mat(w, h, CV_8UC3);
    cv::cvtColor(rgbaMat, rgbMat, CV_RGBA2RGB, 3);
    
    dlib::cv_image<dlib::bgr_pixel> cvImage(rgbMat);
    
    vector<dlib::rectangle> rects;
    
    dlib::find_candidate_object_locations(cvImage, rects, dlib::linspace(100, 250, 4), 10000, 5);

    NSMutableArray<NSValue *> *allRects = [NSMutableArray array];

    for (auto r: rects) {
        [allRects addObject:[NSValue valueWithCGRect:CGRectMake(r.left(), r.top(), r.width(), r.height())]];
    }
    return allRects;
}
```

領域の抽出に関しては、OpenCVのContributionにある[BING](https://github.com/opencv/opencv_contrib/tree/master/modules/saliency)を使いたかったのですが、OpenCV 3.1をContribution付きでビルドして試用したところ、筆者環境では正常に動作させることができなかったので（computeSaliencyメソッドでエラーになる）、代わりにdlibをテストしてみました。

#参考資料
+ Face Landmarking on iPhone
https://github.com/zweigraf/face-landmarking-ios
+ dlibで画像を認識させて、遊んでみた。
http://nonbiri-tereka.hatenablog.com/entry/2016/07/15/110000
+ dlibを用いたselective search
http://kivantium.hateblo.jp/entry/2015/07/25/184346