RxとOpenCVSharpで顔認識を簡単に作成する

#はじめに
　[前回](http://qiita.com/kattoshi/items/d65c231865fbfddf82fa)の投稿では、カメラキャプチャアプリを作成しました。`Rx`と`OpenCVSharp`を組み合わせると、簡単にできる事が分かりました。
　ここまでくると欲を出したくなる物で、OpenCVには顔認証ができる仕組みが準備されています。今回はそれを使って、顔認識をさせようという物です。

#画面にタイムスタンプを入れてみよう
顔認識の前に画面の左上にタイムスタンプを入れてみよう。

###実装
#####1. フォントの定義

```C#:(変更前)
/// <summary>
/// 初期化
/// </summary>
public void Initialize()
{
    // キャプチャーデバイス
    var capture = new CvCapture(0);
```
↓

```C#:(変更後)
/// <summary>
/// 初期化
/// </summary>
public void Initialize()
{
    // 画面にタイムスタンプを描画するためのフォント定義
    var font = default(CvFont);
    Cv.InitFont(out font, FontFace.HersheyScriptComplex, 0.5, 0.5);

    // キャプチャーデバイス
    var capture = new CvCapture(0);
```

#####2.Rxのシーケンスの途中にテキスト描画処理を追加
```C#:(変更前)
var capture = new CvCapture(0);
Observable.Interval(TimeSpan.FromMilliseconds(1), DispatcherScheduler.Current)
    .Select(_ => Cv.QueryFrame(capture))
    .Where(frame => frame != null)
    .Subscribe(frame =>
    {
```
↓

```C#:(変更後)
var capture = new CvCapture(0);
Observable.Interval(TimeSpan.FromMilliseconds(1), DispatcherScheduler.Current)
    .Select(_ => Cv.QueryFrame(capture))
    .Where(frame => frame != null)
    .Do(frame => frame.PutText(string.Format("{0:MM/dd HH:mm:ss}", DateTime.Now), new CvPoint(0, 15), font, Cv.RGB(255, 255, 255)))
    .Subscribe(frame =>
    {
```
`Do()`を使用して、キャプチャーした画像の`frame`の左上にタイムスタンプを表示します。

###実行
実行すると、左上にタイムスタンプが表示します。

#顔認識をさせる
さて、顔認識をさせましょう。
OpenCVでは「[カスケード型分類器](http://opencv.jp/opencv-2.2/py/objdetect_cascade_classification.html"カスケード型分類器")」と呼ばれる物が実装されていて、それを利用するとﾁｮｰ簡単に顔認識ができます。

###実装
#####1.顔認識分類器の作成
```C#:(変更前)
/// <summary>
/// 初期化
/// </summary>
public void Initialize()
{
    // 画面にタイムスタンプを描画するためのフォント定義
    var font = default(CvFont);
    Cv.InitFont(out font, FontFace.HersheyScriptComplex, 0.5, 0.5);

    // キャプチャーデバイス
    var capture = new CvCapture(0);
```
↓

```C#:(変更後)
/// <summary>
/// 初期化
/// </summary>
public void Initialize()
{
    // 画面にタイムスタンプを描画するためのフォント定義
    var font = default(CvFont);
    Cv.InitFont(out font, FontFace.HersheyScriptComplex, 0.5, 0.5);

    // 顔の分類器作成
    var faceDetector = Cv.Load<CvHaarClassifierCascade>("haarcascade_frontalface_default.xml");

    // キャプチャーデバイス
    var capture = new CvCapture(0);
```

顔の分類器の作成で指定されている`"haarcascade_frontalface_default.xml"`は、OpenCVSharpには同梱されていません。OpenCV本体をダウンロードして、解凍後、自力でプロジェクトに追加します。

* [OpenCV](http://sourceforge.net/projects/opencvlibrary/)の本体をダウンロードし、解凍する。
* 解凍先のフォルダ`opencv\sources\data\haarcascades`に`haarcascade_frontalface_default.xml`があるので、プロジェクトに取り込む。
* 取り込み方法は、VS2013の「ソリューションエクスプローラ」で「LivetWPFApplication1」を選択し、右クリック\[追加]-\[既存の項目]を選択。ファイル選択エクスプローラが開くので、上記xmlファイルを選択して下さい。
* 取り込み後、プロジェクトの中に`haarcascade_frontalface_default.xml`が追加されるので、プロパティを表示し、「ビルドアクション」は「なし」、「出力ディレクトリーにコピー」も「新しい場合にコピー」を選択して下さい。<p/>![10.png](https://qiita-image-store.s3.amazonaws.com/0/28247/de32e2f9-b71e-4c85-3bad-cae1d8648359.png)<p/>

#####2. 顔認識処理を追加
顔認識を行い、認識した場所に四角の矩形枠を描画するメソッドを追加します

```C#:(追加メソッド)
/// <summary>
/// 顔認識を行い、矩形枠を描画する
/// </summary>
/// <param name="frame">キャプチャーした画像</param>
/// <param name="faceDetector">顔の分類器</param>
private void ImageProcess(IplImage frame, CvHaarClassifierCascade faceDetector)
{
    using (var frameMatImage = Cv.GetMat(frame))
    {
        using (var grayMatImage = new CvMat(frameMatImage.Rows, frameMatImage.Cols, MatrixType.U8C1))
        {
            // グレースケールへ変換
            frameMatImage.CvtColor(grayMatImage, ColorConversion.BgraToGray);
            var grayImage = Cv.GetImage(grayMatImage);
            //正規化する
            grayImage.EqualizeHist(grayImage);
            using (var cvMem = new OpenCvSharp.CvMemStorage())
            {
                //検出を行う
                Cv.HaarDetectObjects(
                                    grayImage,
                                    faceDetector,
                                    cvMem,
                                    1.1,
                                    10,
                                    0,
                                    new CvSize(10, 10))
                .Where(face => face.HasValue)
                .Select(face => face.Value.Rect)
                .ToList()
                .ForEach(faceRect =>
                {
                    //検出場所へ矩形表示
                    frame.DrawRect(faceRect, Cv.RGB(255, 0, 0), 2);
                });
            }
        }
    }
}

```

####3. Rxのシーケンスに追加する
追加したメソッド(`ImageProcess`)を、Rxのシーケンスに挿入します。

```C#:(変更前)
var capture = new CvCapture(0);
Observable.Interval(TimeSpan.FromMilliseconds(1), DispatcherScheduler.Current)
    .Select(_ => Cv.QueryFrame(capture))
    .Where(frame => frame != null)
    .Do(frame => frame.PutText(string.Format("{0:MM/dd HH:mm:ss}", DateTime.Now), new CvPoint(0, 15), font, Cv.RGB(255, 255, 255)))
    .Subscribe(frame =>
    {
```
↓

```C#:(変更後)
var capture = new CvCapture(0);
Observable.Interval(TimeSpan.FromMilliseconds(1), DispatcherScheduler.Current)
    .Select(_ => Cv.QueryFrame(capture))
    .Where(frame => frame != null)
    .Do(frame => frame.PutText(string.Format("{0:MM/dd HH:mm:ss}", DateTime.Now), new CvPoint(0, 15), font, Cv.RGB(255, 255, 255)))
    .Do(frame => ImageProcess(frame, faceDetector))
    .Subscribe(frame =>
    {
```

###実行
実行して自分の顔へカメラを向けて下さい。顔の周りに矩形枠が表示されるはずです。
`OpenCV`に同梱されているカスケードファイルは精度が低いので、真正面じゃないと認識しないと思います。

また、顔以外を認識するカスケードファイルは、[「顔以外」のものを画像認識する](http://qiita.com/shu223/items/ffd2202eaf92d342f83d)を参照して下さい。

#ViewModelソース
最後に、ViewModel側のソースコード全てを掲載します。View側は変更無しです。
長い説明となりましたが、最後まで読んでいただきありがとうございました。

```C#:MainWindowViewModel.cs
public class MainWindowViewModel : ViewModel
{
    #region ImageSource 表示画像
    private BitmapSource _CaptureImageSource;
    /// <summary>
    /// 表示画像
    /// </summary>
    public BitmapSource CaptureImageSource
    {
        get
        { return _CaptureImageSource; }
        set
        {
            if (_CaptureImageSource == value)
                return;
            _CaptureImageSource = value;
            RaisePropertyChanged();
        }
    }
    #endregion
    /// <summary>
    /// 初期化
    /// </summary>
    public void Initialize()
    {
        // 画面にタイムスタンプを描画するためのフォント定義
        var font = default(CvFont);
        Cv.InitFont(out font, FontFace.HersheyScriptComplex, 0.5, 0.5);

        // 顔の分類器作成
        var faceDetector = Cv.Load<CvHaarClassifierCascade>("haarcascade_frontalface_default.xml");

        // キャプチャーデバイス
        var capture = new CvCapture(0);
        Observable.Interval(TimeSpan.FromMilliseconds(1), DispatcherScheduler.Current)
            .Select(_ => Cv.QueryFrame(capture))
            .Where(frame => frame != null)
            .Do(frame => frame.PutText(string.Format("{0:MM/dd HH:mm:ss}", DateTime.Now), new CvPoint(0, 15), font, Cv.RGB(255, 255, 255)))
            .Do(frame => ImageProcess(frame, faceDetector))
            .Subscribe(frame =>
            {
                var bitmapSource = frame.ToBitmapSource();
                frame.Dispose();
                this.CaptureImageSource = bitmapSource;
            });
    }
    /// <summary>
    /// 顔認識を行い、矩形枠を描画する
    /// </summary>
    /// <param name="frame">キャプチャーした画像</param>
    /// <param name="faceDetector">顔の分類器</param>
    private void ImageProcess(IplImage frame, CvHaarClassifierCascade faceDetector)
    {
        using (var frameMatImage = Cv.GetMat(frame))
        {
            using (var grayMatImage = new CvMat(frameMatImage.Rows, frameMatImage.Cols, MatrixType.U8C1))
            {
                // グレースケールへ変換
                frameMatImage.CvtColor(grayMatImage, ColorConversion.BgraToGray);
                var grayImage = Cv.GetImage(grayMatImage);
                //正規化する
                grayImage.EqualizeHist(grayImage);
                using (var cvMem = new OpenCvSharp.CvMemStorage())
                {
                    //検出を行う
                    Cv.HaarDetectObjects(
                                        grayImage,
                                        faceDetector,
                                        cvMem,
                                        1.1,
                                        10,
                                        0,
                                        new CvSize(10, 10))
                    .Where(face => face.HasValue)
                    .Select(face => face.Value.Rect)
                    .ToList()
                    .ForEach(faceRect =>
                    {
                        //検出場所へ矩形表示
                        frame.DrawRect(faceRect, Cv.RGB(255, 0, 0), 2);
                    });
                }
            }
        }
    }
}
```