家庭用見守りロボットを作る(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) アプリとは
・UWPの概要理解、UWPサンプルプログラムの提供
Raspberry Pi 2とWindows 10ではじめるIoTプログラミング(Amazon)
・FaceAPIの概要理解
空気を読んで年齢を答えてくれる例のあれの裏側にあるAPIがAzure Marketplaceに登場してきました。
・FaceAPIの仕様詳細
ProjectOxford API Reference
・JSONデータのPOST送信部分
HttpClient を使って JSON データを POST する方法
・テキストの読み上げ
Windows 8.1の新機能、テキスト読み上げを利用するには?
準備するもの
・第1回でVisualStudio2015を準備します。
・第2回でUWPアプリサンプルプログラムを準備します。
・第3回でFaceAPIで家族の画像を学習させておきます。
・WebカメラのあるWindows10のデスクトップPCを用意します
※私のMacbookProのbootcamp環境のWindows10でも動作できましたので、Windows10が動くwebカメラのついたデスクトップPCであれば、ほぼ動作するのではないかと思われます。
作業の流れ
第2回で使ったサンプルプログラムを以下の流れで実装しています。
もっとスマートな実装がありましたらご指摘ください^^;
1.インポート、クラス定義
2.FaceAPIKeyの定数定義
3.FaceAPIの呼び出しメソッドの修正
4.その他のメソッドの追加
5.XAMLコードにMediaElementコントロールを配置
6.ビルドして結果を確認する。
インポート、クラス定義
必要な型のインポートとクラス定義を追加しています。
顔検出した結果をFaceクラスに、顔識別の結果をPersonクラスにセットする方向で実装しています。
POST送信用のクラスも定義してます。
// 追加
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回で作成したFaceAPIKeyを使ってください。
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メソッドを以下のように修正してます。
顔検出リクエストで写真にある顔データを取得して、顔識別リクエストで誰の顔なのか識別し、描画の処理と名前を読み上げる処理を入れてます。
/// <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回で取得した家族それぞれのPersonIDをセットしてあげてください。
readTextメソッド・・・SpeechSynsesizer APIの読み上げ機能を使って家族の名前を呼べるようにしてます。
家族の場合は名前を呼び、見知らぬ人の場合は「見知らぬ人がいます」と呼ぶプログラムを作成します。
greetメソッド・・・名前だけだと物足りないので、時間によって「おはようございます」「こんにちは」「こんばんわ」と呼びかけるようにしてます。
popupsErrorMessageメソッド・・・エラーメッセージをポップアップで出せるようにしています。
/// <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名前空間)を画面に配置します。
後半部分のの前に追加しましょう。
<MediaElement x:Name="mediaElement1" Volume="100" />
</Grid>
</Page>
#ビルドして結果を確認する。
実行するとアプリが立ち上がりますので、Previewに顔を合わせて、Take Pictureを実行してみましょう。
顔を認識すると名前のラベルが顔の下に表示されて、名前を読んでくれるようになりました!
子どもたちの感想は、
ちょっと面白かったけどお話ができないのでまだまだだね、という辛口評価でしたが、
引き続きめげずに頑張っていこうと思います 笑。
声がちょっと怖いとも突っ込まれましたが、サードパーティなどを使って声を変えられる技術をお持ちの方いましたら、
お教えいただくと幸いです!
次回は、赤外線センサーを使って、家族が近づいた時に顔を認識して挨拶をして、
不審者がいるときはメール送信する実装をします。
Raspberry Pi2がようやく登場しますのでお楽しみに!
#音声がでない場合は
サウンドドライバのアンインストール、再インストールをお試しください。
参考)
Windows 10にアップグレードした後、音が出なくなりました。
再インストール方法はPC取り扱いメーカーに確認するか、
Realtekのサイトから、Windows10対応のドライバをダウンロード&インストールをお試しください。
「Software: Drivers & Utilities」-「High Definition Audio Codecs」- 「Vista, Windows7, Windows8, Windows8.1, Windows10 Driver (32/64bits) Driver only (Executable file)」をダウンロードします。