家庭用見守りロボットを作る（Raspberry Pi2+Windows10 IoT Core+Azure）第4回：ProjectOxford FaceAPIを用いてUWPアプリで家族の顔を識別して挨拶をさせる

家庭用見守りロボットを作る（Raspberry Pi2+Windows10 IoT Core+Azure）第4回です。

#今回のテーマ
いよいよ、Microsoft ProjectOxford FaceAPIを用いて、UWP（Universal Windows Platform）アプリで家族の顔を識別させます。
撮影した写真から家族の名前を呼んで、「◯◯ちゃんおはよう」とか、「◯◯ちゃんこんにちわ」とか、挨拶できるようになりました。
家族でない人がいると、「見知らぬ人がいます」と教えてくれます。
今回も無料でお試しできます。

# UWP（Universal Windows Platform）アプリとは？
Windows10にはUniversal Windows Platformというアプリの実行環境があり、1つのソースファイルで、
異なるデバイス用アプリをビルドして、実行できるようになります。
画面表示やキー入力、ネットワーク接続などについては、デバイス間でソースコードを変更する事無く、
利用できるようになります。
Raspberry PiにあるGPIOなどの入出緑端子など、デスクトップPCには無い機能は動作しませんが、
基本的な動作はデスクトップ上で確認できるようになります。

今回の実装でも、Raspberry Piを使わずに、デスクトップPC上で、
VisualStudioを使って実装ができましたので、プログラムの実装に集中することができました。
特にWebカメラ部分の確認は、デスクトップPC上でも確認できる部分ですので、
快適な実装を体験できました。

#参考になったサイト、書籍
以下のサイト、書式が参考になりました。
・MicrosoftのUWPの概要説明
[ユニバーサル Windows プラットフォーム (UWP) アプリとは](https://msdn.microsoft.com/library/windows/apps/dn726767.aspx)
・UWPの概要理解、UWPサンプルプログラムの提供
<a rel="nofollow" href="http://www.amazon.co.jp/gp/product/4839956790/ref=as_li_qf_sp_asin_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=4839956790&linkCode=as2&tag=noahloungecaf-22">Raspberry Pi 2とWindows 10ではじめるIoTプログラミング(Amazon)</a><img src="http://ir-jp.amazon-adsystem.com/e/ir?t=noahloungecaf-22&l=as2&o=9&a=4839956790" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />
・FaceAPIの概要理解
[空気を読んで年齢を答えてくれる例のあれの裏側にあるAPIがAzure Marketplaceに登場してきました。](http://hatsune.hatenablog.jp/entry/2015/05/02/143055)
・FaceAPIの仕様詳細
[ProjectOxford API Reference](https://dev.projectoxford.ai/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f30395249/console)
・JSONデータのPOST送信部分
[HttpClient を使って JSON データを POST する方法](http://blogs.msdn.com/b/japan_platform_sdkwindows_sdk_support_team_blog/archive/2013/12/10/httpclient-json-post.aspx)
・テキストの読み上げ
[Windows 8.1の新機能、テキスト読み上げを利用するには？](http://www.atmarkit.co.jp/ait/articles/1311/07/news088.html)

# 準備するもの
・[第1回](http://qiita.com/ma-watanabe/items/5cf62166cf69d21154e6#4%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83%E3%81%AE%E6%A7%8B%E7%AF%89)でVisualStudio2015を準備します。
・[第2回](http://qiita.com/ma-watanabe/items/c1bf739f1613506d65b9)でUWPアプリサンプルプログラムを準備します。
・[第3回](http://qiita.com/ma-watanabe/items/cb9477b1d79b05ba5311)でFaceAPIで家族の画像を学習させておきます。
・WebカメラのあるWindows10のデスクトップPCを用意します
※私のMacbookProのbootcamp環境のWindows10でも動作できましたので、Windows10が動くwebカメラのついたデスクトップPCであれば、ほぼ動作するのではないかと思われます。

# 作業の流れ
[第2回](http://qiita.com/ma-watanabe/items/c1bf739f1613506d65b9)で使ったサンプルプログラムを以下の流れで実装しています。
もっとスマートな実装がありましたらご指摘ください^^;

1.インポート、クラス定義
2.FaceAPIKeyの定数定義
3.FaceAPIの呼び出しメソッドの修正
4.その他のメソッドの追加
5.XAMLコードにMediaElementコントロールを配置
6.ビルドして結果を確認する。

# インポート、クラス定義
必要な型のインポートとクラス定義を追加しています。
顔検出した結果をFaceクラスに、顔識別の結果をPersonクラスにセットする方向で実装しています。
POST送信用のクラスも定義してます。

```csharp:MainPage.xaml.cs
// 追加
using Windows.Networking;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.IO;
using Windows.Media.SpeechSynthesis;


// JSON形式のシリアライズ・POST送信用クラス
public class PostItem
{
    [DataMember(Name = "personGroupId")]
    public String PersonGroupId { get; set; }

    [DataMember(Name = "faceIds")]
    public string[] FaceIds { get; set; }

    [DataMember(Name = "maxNumOfCandidatesReturned")]
    public int MaxNumOfCandidatesReturned { get; set; }
}

// 顔の画像上の位置をクラス定義 
public class FaceRectangle
{
    [DataMember(Name = "top")]
    public Double Top { get; set; }

    [DataMember(Name = "left")]
    public Double Left { get; set; }

    [DataMember(Name = "width")]
    public Double Width { get; set; }

    [DataMember(Name = "height")]
    public Double Height { get; set; }

}

public class Person
{
    // 名前
    [DataMember(Name = "personName")]
    public String PersonName { get; set; }


    // faceId
    [DataMember(Name = "faceId")]
    public String FaceID { get; set; }

    // personId
    [DataMember(Name = "personId")]
    public String PersonId { get; set; }

    // 顔の画像上の位置情報
    [DataMember(Name = "faceRectangle")]
    public FaceRectangle FaceRectangle { get; set; }
}
```

# FaceAPIKeyの定数定義
FaceAPIKeyを繰り返し使うため、定数として定義しています。
[第2回](http://qiita.com/ma-watanabe/items/c1bf739f1613506d65b9)で作成したFaceAPIKeyを使ってください。

```csharp:MainPage.xaml.cs
    public sealed partial class MainPage : Page
    {
        private MediaCapture capture;
        private StorageFile photoFile;
        private const string ServiceHost = "https://api.projectoxford.ai/face/v0";
        private Boolean isPreviewing;
        private const string FaceAPIKey = "XXXXXXXXXXXXXXXX";
```

# FaceAPIの呼び出しメソッドの修正
Makerequestメソッドを以下のように修正してます。
顔検出リクエストで写真にある顔データを取得して、顔識別リクエストで誰の顔なのか識別し、描画の処理と名前を読み上げる処理を入れてます。


```csharp:MainPage.xaml.cs
        /// <summary>
        /// FaceAPIにリクエストして家族の顔を識別する
        /// </summary>
        private async void Makerequest()
        {
            // 撮影した画像の顔検出をリクエスト送信する
            HttpResponseMessage detectResponse = await detectFaceRequest();

            //Responseデータ読み込み
            string detectResponseContent = await detectResponse.Content.ReadAsStringAsync();

            // 家族認識のリクエスト送信の為のfaceidリスト、
            // および家族の情報をセットするためのPersonリストを準備する
            var detectFaceArray = default(JsonArray);
            var isSuccess = JsonArray.TryParse(detectResponseContent, out detectFaceArray);

            // faceIDと顔の座標をPersonクラスの配列としてセットする
            var faceIdList = new List<string>();
            var personList = new List<Person>();

            if (isSuccess)
            {

                foreach (var face in detectFaceArray)
                {

                    var facedata = face.GetObject();
                    var faceId = facedata["faceId"];
                    faceIdList.Add(faceId.GetString());

                    var faceRectangle = new FaceRectangle();
                    var person = new Person();

                    //認識した顔の座標
                    var faceRect = facedata.GetNamedObject("faceRectangle");
                    faceRectangle.Top = faceRect["top"].GetNumber();
                    faceRectangle.Left = faceRect["left"].GetNumber();
                    faceRectangle.Width = faceRect["width"].GetNumber();
                    faceRectangle.Height = faceRect["height"].GetNumber();

                    //Person情報のセット
                    person.FaceID = faceId.GetString();
                    person.FaceRectangle = faceRectangle;
                    personList.Add(person);

                }
            }


            // 検出した顔が誰であるかを識別する
            HttpResponseMessage identifyResponse = await identifyFaceRequest(faceIdList);

            if (identifyResponse.IsSuccessStatusCode)
            {

                //Responseデータ読み込み
                string identifyResponseContent = await identifyResponse.Content.ReadAsStringAsync();

                //顔認識データを配列に取得
                var faceArray = default(JsonArray);
                isSuccess = JsonArray.TryParse(identifyResponseContent, out faceArray);
                String personNameLabel = "";

                //Canvasの消去
                canvas.Children.Clear();

                if (isSuccess)
                {
                    foreach (var face in faceArray)
                    {
                        var facedata = face.GetObject();

                        // faceIdの取得
                        var faceId = facedata["faceId"];

                        //認識する顔位置の初期値をセット
                        Double top = 0;
                        Double left = 0;
                        Double width = 0;
                        Double height = 0;
                        Rectangle rect = new Rectangle();

                        foreach (var person in personList)
                        {
                            if (person.FaceID == faceId.GetString())
                            {
                                top = person.FaceRectangle.Top;
                                left = person.FaceRectangle.Left;
                                width = person.FaceRectangle.Width;
                                height = person.FaceRectangle.Height;

                                JsonArray candidates = facedata["candidates"].GetArray();

                                if (candidates.Count != 0)
                                {
                                    person.PersonId = candidates.First().GetObject()["personId"].GetString();
                                    Double confidence = candidates.First().GetObject()["confidence"].GetNumber();
                                }

                                //personIdにより家族の名前を取得
                                person.PersonName = getPersonName(person.PersonId);
                                //画面表示用のラベルをセット
                                personNameLabel = person.PersonName;
                            }
                        }

                        rect.Height = height;
                        rect.Width = width;
                        rect.StrokeThickness = 2;
                        rect.Stroke = new SolidColorBrush(Colors.Red);

                        //名前表示
                        Viewbox vBox = new Viewbox
                        {
                            StretchDirection = StretchDirection.Both,
                            Stretch = Stretch.Uniform,
                            Width = width,
                            Height = height / 3
                        };

                        TextBlock tBlock = new TextBlock
                        {
                            Text = personNameLabel,
                            TextAlignment = TextAlignment.Center,
                            Width = width

                        };

                        tBlock.Foreground = new SolidColorBrush(Colors.White);

                        vBox.Child = tBlock;
                        vBox.HorizontalAlignment = HorizontalAlignment.Center;

                        //Canvasに描画
                        canvas.Children.Add(rect);
                        Canvas.SetLeft(rect, left);
                        Canvas.SetTop(rect, top);

                        canvas.Children.Add(vBox);
                        Canvas.SetLeft(vBox, left);
                        Canvas.SetTop(vBox, top + height);

                    }

                    // 名前読み上げ
                    foreach (var person in personList)
                    {
                        if (person.PersonName != "不明")
                        {
                            readText(greet() + person.PersonName);
                        }
                        else
                        {
                            readText("見知らぬ人がいます");
                        }

                        await Task.Delay(TimeSpan.FromSeconds(5));
                    }
                }

            }
        }
```

# その他のメソッドの追加

detectFaceRequestメソッド・・・写真から顔の検出をするメソッドです。

identifyFaceRequestメソッド・・・顔の識別をしてPersonIDを割り出しています。

getPersonNameメソッド・・・PersonIDから家族の名前を割り出してます。
[第3回](http://qiita.com/ma-watanabe/items/cb9477b1d79b05ba5311)で取得した家族それぞれのPersonIDをセットしてあげてください。

readTextメソッド・・・SpeechSynsesizer APIの読み上げ機能を使って家族の名前を呼べるようにしてます。
家族の場合は名前を呼び、見知らぬ人の場合は「見知らぬ人がいます」と呼ぶプログラムを作成します。

greetメソッド・・・名前だけだと物足りないので、時間によって「おはようございます」「こんにちは」「こんばんわ」と呼びかけるようにしてます。

popupsErrorMessageメソッド・・・エラーメッセージをポップアップで出せるようにしています。

```csharp:MainPage.xaml.cs
        /// <summary>
        /// 顔検出リクエスト
        /// <summary>
        private async Task<HttpResponseMessage> detectFaceRequest()
        {

            // 顔検出結果を取得する
            var client = new HttpClient();
            var detectFaceRequestUrl = "https://api.projectoxford.ai/face/v1.0/detect?returnFaceId=true&subscription-key=" + FaceAPIKey;
            var detectFaceRequest = new HttpRequestMessage(HttpMethod.Post, new Uri(ServiceHost));
            detectFaceRequest.RequestUri = new Uri(detectFaceRequestUrl);

            //画像ファイル読み込み
            StorageFile file = await KnownFolders.PicturesLibrary.GetFileAsync("pict.jpg");
            var stream = await file.OpenReadAsync();

            //画像データをリクエストBodyに設定
            detectFaceRequest.Content = new HttpStreamContent(stream);
            detectFaceRequest.Content.Headers.ContentType = new HttpMediaTypeHeaderValue("application/octet-stream");
            HttpResponseMessage detectResponse = new HttpResponseMessage();

            //POSTリクエスト
            detectResponse = await client.SendRequestAsync(detectFaceRequest);

            Debug.WriteLine(detectResponse.StatusCode);

            // 失敗している場合
            if (!detectResponse.IsSuccessStatusCode)
            {
                // 応答ステータスコードを表示します。 
                String failureMsg = "HTTP Status: " + detectResponse.StatusCode.ToString() + " - Reason: " + detectResponse.ReasonPhrase;
                popupsErrorMessage(failureMsg);
            }

            if (detectResponse.Content == null)            {
                popupsErrorMessage("detectResponse.Contentがnullです");
            };

            return detectResponse;
        }

        /// <summary>
        /// 顔識別リクエスト
        /// <summary>
        private async Task<HttpResponseMessage> identifyFaceRequest(List<string> faceIdList) {
            Uri theUri = new Uri("https://api.projectoxford.ai/face/v1.0/identify");

            //HttpClient を作成しヘッダーを設定します
            HttpClient aClient = new HttpClient();
            aClient.DefaultRequestHeaders.Host = new HostName(theUri.Host);
            aClient.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", FaceAPIKey);

            // JSON としてシリアライズし POST するデータ 
            PostItem postItem = new PostItem();

            //値をセット
            postItem.PersonGroupId = "watanabe-family";
            postItem.FaceIds = faceIdList.ToArray();
            postItem.MaxNumOfCandidatesReturned = 1;

            // 該当タイプ用の JsonSerializer を作成
            DataContractJsonSerializer jsonSer = new
            DataContractJsonSerializer(typeof(PostItem));

            // Serializer を使ってオブジェクトを MemoryStream に書き込み 
            MemoryStream ms = new MemoryStream();
            jsonSer.WriteObject(ms, postItem);
            ms.Position = 0;

            // StreamReader で StringContent (Json) をコンストラクト
            StreamReader sr = new StreamReader(ms);

            // StringContent コンストラクタの最初の引数として渡す
            HttpStringContent theContent = new HttpStringContent(sr.ReadToEnd(), UnicodeEncoding.Utf8, "application/json");

            // POST送信 
            HttpResponseMessage identifyResponse = await aClient.PostAsync(theUri, theContent);

            // 失敗している場合
            if (!identifyResponse.IsSuccessStatusCode)
            {
                // 応答ステータスコードを表示します。 
                String failureMsg = "HTTP Status: " + identifyResponse.StatusCode.ToString() + " - Reason: " + identifyResponse.ReasonPhrase;
                popupsErrorMessage(failureMsg);
            }
            if (identifyResponse.Content == null)
            {
                popupsErrorMessage("identifyResponse.Contentがnullです");
            };
            
            return identifyResponse;

        }

        /// <summary>
        /// 家族の名前を返す
        /// <summary>
        private string getPersonName(string personId)
        {
            string personName;

            //personIdにより名前をセット
            if (personId == "4407ed98-1c5f-4fbd-88cb-1607d747b9df")
            {
                personName = "あいみちゃん";
            }

            else if (personId == "3ba0b015-eb28-4562-b13d-5a271532226f")
            {
                personName = "まさとしさん";
            }
            else if (personId == "15836868-943f-4ea4-9aa4-da438eaf2d5d")
            {
                personName = "ひさこさん";
            }
            else if (personId == "6e03f5ec-838b-4378-9884-a497bbfe125f")
            {
                personName = "ゆうしんくん";
            }
            else
            {
                personName = "不明";
            }

            return personName;
        }


        /// <summary>
        /// 読み上げる
        /// <summary>
        private async void readText(string text)
        {

            // 合成した音声を受け取るストリーム
            Windows.Media.SpeechSynthesis.SpeechSynthesisStream stream;
            const string language = "ja-JP";
            var allVoices = SpeechSynthesizer.AllVoices;
            var voice = allVoices.FirstOrDefault(v =>
                          string.Equals(v.Language, language, StringComparison.OrdinalIgnoreCase)
                        );
            if (voice == null)
            {
                popupsErrorMessage("¥" + language + "¥を読めるSpeechSynthesizerが搭載されていません。");
            }
            using (var speech = new SpeechSynthesizer())
            {
                speech.Voice = voice;
                stream = await speech.SynthesizeTextToStreamAsync(text);

                // ストリームをMediaElementコントロールに渡して、再生させる
                this.mediaElement1.SetSource(stream, stream.ContentType);
                this.mediaElement1.Play();
            }
        }

        /// <summary>
        /// 挨拶する
        /// <summary>
        private string greet()
        {
            var time = DateTime.Now;
            int hour = time.Hour;
            if (5 <= hour && hour < 12)
            {
                return "おはようございます";
            }
            else if(12 <= hour && hour < 18)
            {
                    return "こんにちは";
            }
            else
            {
                return "こんばんわ";
             }
        }

        /// <summary>
        /// エラーメッセージをポップアップ表示する
        /// <summary>
        private async void popupsErrorMessage(string text)
        {
            await(new Windows.UI.Popups.MessageDialog(text)).ShowAsync();
        }

    }
}


```

# XAMLコードにMediaElementコントロールを配置

テキストを読み上げるためのMediaElementコントロール（Windows.UI.Xaml.Controls名前空間）を画面に配置します。
後半部分の</Grid>の前に追加しましょう。

```csharp:MainPage.xaml.cs
        <MediaElement x:Name="mediaElement1" Volume="100" />
    </Grid>
</Page>
```


#ビルドして結果を確認する。
実行するとアプリが立ち上がりますので、Previewに顔を合わせて、Take Pictureを実行してみましょう。
顔を認識すると名前のラベルが顔の下に表示されて、名前を読んでくれるようになりました！


![FaceAPI_UWP 2016_01_17 16_09_10.png](https://qiita-image-store.s3.amazonaws.com/0/33021/a55e8a7c-ae7d-0639-5c27-8e6c525a8ddc.png)

子どもたちの感想は、
ちょっと面白かったけどお話ができないのでまだまだだね、という辛口評価でしたが、
引き続きめげずに頑張っていこうと思います 笑。
声がちょっと怖いとも突っ込まれましたが、サードパーティなどを使って声を変えられる技術をお持ちの方いましたら、
お教えいただくと幸いです！

次回は、赤外線センサーを使って、家族が近づいた時に顔を認識して挨拶をして、
不審者がいるときはメール送信する実装をします。
Raspberry Pi2がようやく登場しますのでお楽しみに！

#音声がでない場合は
サウンドドライバのアンインストール、再インストールをお試しください。
参考）
[Windows 10にアップグレードした後、音が出なくなりました。](http://www.fmworld.net/cs/azbyclub/qanavi/jsp/qacontents.jsp?PID=2710-8244)
再インストール方法はPC取り扱いメーカーに確認するか、
[Realtekのサイト](http://www.realtek.com.tw/downloads/downloadsView.aspx?Langid=1&PNid=14&PFid=24&Level=4&Conn=3&DownTypeID=3&GetDown=false#1)から、Windows10対応のドライバをダウンロード＆インストールをお試しください。
「Software: Drivers & Utilities」-「High Definition Audio Codecs」- 「Vista, Windows7, Windows8, Windows8.1, Windows10 Driver (32/64bits) Driver only (Executable file)」をダウンロードします。