AzureのFace APIを使って顔の類似度を判定する②

この記事は[AzureのFace APIを使って顔の類似度を判定する①](https://qiita.com/koob/items/b649e463842b1c16f297)の続きです。

前回はFace APIのFace Detectを使用して、まずはFaceIDなどの必要な情報を取得してきました。
今回はそれを使用して、Face FindSimilarによる類似度判定や、OpenCVSharpでの顔検出箇所の描画を行っていきます。

ちなみに前回Face Detectから取得した顔情報は、以下のような`List<FaceInfo>`型に整形してMainメソッドで受け取りました。
`FaceInfo`は検出時のFaceIDと検出箇所の座標、それが比較元と比較先画像のどちらから検出したものなのかという情報を持つ独自クラス型です。

```console
faceDivision: base , sourceUrl: 比較元画像のファイルパス
faceId: 206b61c0-6539-4ad8-bf01-2fd8c18537c1
Rectangle: {874, 241, 210, 210}

faceDivision: target , sourceUrl: 比較先画像のファイルパス
faceId: 68b07497-0502-41e9-9af1-290348848b1e
Rectangle: {765, 260, 148, 148}

faceDivision: target , sourceUrl: 比較先画像のファイルパス
faceId: 2df5c831-f10c-475e-a6c8-734a3cc8c04e
Rectangle: {545, 347, 124, 124}
```

## 4. OpenCVSharpで検出箇所を画像に描画する
今のままでは1枚の画像から複数人の顔が検出された際に誰が誰だかわからないので、最低限検出した箇所と、その際に割り当てられたFaceIDを描画したいと思います。
画像への描画には、画像処理・画像解析のライブラリであるOpenCVSharp（OpenCVのラッパー）を使用します。
#### OpenCVSharp3の追加
NuGetからインストールします。
VisualStudioのメニューから「プロジェクト」→「NuGetパッケージの管理」を選択してください。
管理画面が表示されたら、「参照」の検索窓から「opencvsharp」と検索し、「OpenCvSharp3-AnyCPU」をインストールしましょう。現時点での最新版は「4.0.0.20181129」でした。
<img width="726" alt="キャプチャ7.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403060/bac8ce2a-4fe6-70d0-e17a-4f32784e98a7.png">
インストールが成功すると、ソリューションエクスプローラー上で参照に色々追加されているのが確認できると思います。
<img width="190" alt="キャプチャ8.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403060/17bc36ba-ca57-221f-9a09-69f623d709f1.png">
ついでにusingにも`OpenCvSharp`を追加しておきましょう。

```c#:FaceFindSimilar.cs
using OpenCvSharp;
```

#### 描画用メソッドの実装
それでは、実際にOpenCVSharpを使用して描画する為のメソッドを追加します。
以下の2つのメソッドをFaceFindSimilarクラス内に追加してください。

```c#:FaceFindSimilar.cs
		static void dispImageByOpenCV(string urlBase, string urlTarget, List<FaceInfo> faceInfoList)
		{
			// FaceInfoクラスのリストを仕分け
			List<FaceInfo> faceInfoBaseList = faceInfoList.Where(x => x.faceDivision == DIVISION_BASE).ToList();
			List<FaceInfo> faceInfoTargetList = faceInfoList.Where(x => x.faceDivision == DIVISION_TARGET).ToList();

			// 比較元画像の表示
			ShowImage(urlBase, faceInfoBaseList);

			// 比較先画像の表示
			ShowImage(urlTarget, faceInfoTargetList);
		}

		static void ShowImage(string url, List<FaceInfo> faceInfoList)
		{
			// 画像の読み込み
			using (Mat image = new Mat(url))
			{
				foreach (FaceInfo faceInfo in faceInfoList)
				{
					// 顔の検出箇所に赤枠を表示
					Rect rect = new Rect(faceInfo.recLeft, faceInfo.recTop, faceInfo.recWidth, faceInfo.recHeight);
					Cv2.Rectangle(image, rect, new Scalar(0, 0, 255), 2);

					// FaceIDを描画
					Cv2.PutText(image, faceInfo.faceId, new Point(faceInfo.recLeft, faceInfo.recTop)
						, HersheyFonts.HersheyComplexSmall, 1, new Scalar(255, 0, 255), 1, LineTypes.AntiAlias);
				}

				Cv2.ImShow(url, image);
				Cv2.WaitKey(0);
			}
		}
```

dispImageByOpenCVメソッドではLinqで比較元画像の顔情報と比較先画像の顔情報を分けてあげて、ShowImageメソッドでそれぞれの画像に対して`Cv2.Rectangle`や`Cv2.PutText`で顔周りの四角い枠とFaceIDを描画しています。

#### Mainメソッドに呼び出し処理を追加
ではdispImageByOpenCVをMainメソッドから呼び出してみましょう。
dispImageByOpenCVメソッドはMainメソッドから引数として以下を受け取ります。

- 比較元画像のファイルパス
- 比較先画像のファイルパス
- Face Detectで取得した顔情報のリスト

```c#:FaceFindSimilar.cs
		static void Main(string[] args)
		{
			Dictionary<string, string> imageUrls = new Dictionary<string, string>();
			imageUrls.Add(DIVISION_BASE, URL_BASE);
			imageUrls.Add(DIVISION_TARGET, URL_TARGET);

			// FindSimilarのパラメータとなるfaceId、その他を取得
			List<FaceInfo> faceInfoList = FaceDetect.Detect(imageUrls);

			if (faceInfoList.Exists(x => x.faceDivision == DIVISION_BASE)
				&& faceInfoList.Exists(x => x.faceDivision == DIVISION_TARGET))
			{
				Console.WriteLine("顔が検出できました。:");

				foreach (FaceInfo faceInfo in faceInfoList)
				{
					Console.WriteLine("faceDivision: " + faceInfo.faceDivision + " , sourceUrl: " + faceInfo.sourceUrl
						+ Environment.NewLine + "faceId: " + faceInfo.faceId
						+ Environment.NewLine + "Rectangle: {" + faceInfo.recLeft + ", " + faceInfo.recTop + ", " + faceInfo.recWidth + ", " + faceInfo.recHeight + "}"
						+ Environment.NewLine);
				}

				// 追加箇所 From----------------------------------------
				dispImageByOpenCV(URL_BASE, URL_TARGET, faceInfoList);
				// 追加箇所 To------------------------------------------
			}
			else
			{
				Console.WriteLine("顔が検出できませんでした。");
			}

			Console.ReadLine();
			// 追加箇所 From----------------------------------------
			Cv2.DestroyAllWindows();
			// 追加箇所 To------------------------------------------

		}
```

`// 追加箇所 From`(~To)が呼び出し箇所です。
特に戻り値もないので、それぞれのファイルパスと顔情報のリストをそのまま渡しただけですね。
描画した画像ウィンドウを閉じる`Cv2.DestroyAllWindows();`も忘れずに記述しましょう。

ここまででF5実行すると、以下のように顔周りに赤い四角い枠とFaceIDが描画されると思います。
<img width="465" alt="キャプチャ9.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403060/08aca364-6e79-2b99-fd83-cdb6f26fa2c1.png">
<img width="681" alt="キャプチャ10.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403060/3b6268bc-062e-3130-1e02-d124ebd0bcb8.png">
以上で画像の描画処理は終了となります。

## 5. Face FindSimilarで類似度の判定
やっとという感じですが、Face FindSimilarを使用して類似度の判定を行いましょう。
まずは以下をコピペして、using4つとメソッド1つとクラス2つを追加してください。
書いといてなんですが、詰め込みすぎですね。すみません。
追加してください。（曲げない）

using

```c#:FaceFindSimilar
using System.Configuration;
using System.Net.Http;
using System.Net.Http.Headers;
using Newtonsoft.Json;
```

FindSimilarRequestメソッド

```c#:FaceFindSimilar
		static async void FindSimilarRequest(List<FaceInfo> faceInfoList)
		{
			var client = new HttpClient();

			// リクエストヘッダー
			client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", ConfigurationManager.AppSettings["subscriptionKey"]);

			string uri = ConfigurationManager.AppSettings["requestUrl"] + "findsimilars";

			HttpResponseMessage response;

			List<string> targetFaceIdList = new List<string>();

			// FindSimilarの第二引数（faceIds）作成
			foreach (FaceInfo faceInfoTarget in faceInfoList.FindAll(x => x.faceDivision == DIVISION_TARGET))
			{
				targetFaceIdList.Add(faceInfoTarget.faceId);
			}

			foreach (FaceInfo faceInfoBase in faceInfoList.FindAll(x => x.faceDivision == DIVISION_BASE))
			{
				Console.WriteLine("■FaceID：[" + faceInfoBase.faceId + "] との類似度判定");

				// リクエストボディをJSON形式にする
				MatchFaceSimilar matchFaceSimilar = new MatchFaceSimilar(faceInfoBase.faceId, targetFaceIdList.ToArray());
				var json = JsonConvert.SerializeObject(matchFaceSimilar);

				byte[] byteData = Encoding.UTF8.GetBytes(json);

				using (var content = new ByteArrayContent(byteData))
				{
					// リクエストヘッダーの作成
					content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
					response = await client.PostAsync(uri, content);

					// 実行結果からJSONの取得
					var contentString = await response.Content.ReadAsStringAsync();
					//Console.WriteLine(contentString);

					// JSONを整形して出力
					List<FaceConfidence> faceConfidence = JsonConvert.DeserializeObject<List<FaceConfidence>>(contentString);
					faceConfidence.ForEach((face) =>
					{
						decimal confidence = Math.Round(decimal.Parse(face.confidence) * 100, 2, MidpointRounding.AwayFromZero);
						Console.WriteLine("　" + face.faceId + " : " + confidence.ToString() + "％");
					});
				}
			}

			Console.WriteLine("処理終了");
		}
```

FaceConfidenceクラス、MatchFaceSimilarクラス

```c#:FaceFindSimilar
	public class FaceConfidence
	{
		public string faceId { get; set; }
		public string confidence { get; set; }
	}

	[JsonObject]
	public class MatchFaceSimilar
	{
		const int MAX_NUM = 20;
		const string MODE_MATCH_PERSON = "matchPerson";
		const string MODE_MATCH_FACE = "matchFace";

		[JsonProperty("faceId")]
		public string FaceId { get; private set; }

		[JsonProperty("faceIds")]
		public string[] FaceIds { get; private set; }

		[JsonProperty("maxNumOfCandidatesReturned")]
		public int MaxNumOfCandidatesReturned { get; private set; }

		[JsonProperty("mode")]
		public string Mode { get; private set; }

		public MatchFaceSimilar(
			string faceId
			, string[] faceIds
			, int maxNumOfCandidatesReturned = MAX_NUM
			, string mode = MODE_MATCH_FACE
			)
		{
			this.FaceId = faceId;
			this.FaceIds = faceIds;
			this.MaxNumOfCandidatesReturned = maxNumOfCandidatesReturned;
			this.Mode = mode;
		}
	}
```

流れとしてはMainメソッドからFindSimilarRequestメソッドを呼び出し、その中でFace FindSimilarへ投げる際のリクエストヘッダ、リクエストボディを整形して、各類似度を取得しています。

Face FindSimilarに投げる際のリクエストボディはJSONにして渡さないといけないので、MatchFaceSimilar独自クラスのインスタンスを作成後、Json.NETの`JsonConvert.SerializeObject()`を使用してシリアライズしています。

ちなみに[公式ドキュメント](https://westus.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f30395237)によると、以下のような形式のJSONにする必要があります。（一部加工しています）

```json
{
    "faceId": "比較元画像のFaceID",
    "faceIds": ["比較先画像のFaceID_1","比較先画像のFaceID_2","比較先画像のFaceID_3",...],
    "maxNumOfCandidatesReturned": 20,
    "mode": "matchFace"
}
```

MatchFaceSimilarクラス内の`JsonProperty`には上記のキー項目名を設定しています。

また、FaceConfidenceクラスはFace FindSimilarからの結果JSONを受け取るための独自クラスです。FindSimilarRequestメソッド内で、Face FindSimilar実行後に`contentString`に入った結果JSONを`JsonConvert.DeserializeObject()`でデシリアライズし、FaceConfidenceクラスのインスタンスを作成しています。

#### FaceFindSimilarの実行
最後に、Mainメソッドで`dispImageByOpenCV(URL_BASE, URL_TARGET, faceInfoList);`としていた部分を以下のコードで上書きして、実行してみましょう。

```c#:FaceFindSimilar.cs
				try
				{
					dispImageByOpenCV(URL_BASE, URL_TARGET, faceInfoList);
					FindSimilarRequest(faceInfoList);
				}
				catch (Exception e)
				{
					Console.WriteLine("例外：" + e.Message);
				}
```

ここでは以下の画像を使用して試してみます。

比較元：[ちょいワルになりきれない男たちのフリー画像（写真）](https://www.pakutaso.com/201410082832-1.html)
![OYtantei0I9A9515_TP_V.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403060/d0dbf5dd-1305-edc7-d27d-4549579e3685.jpeg)

比較先：[社員の不正を暴く監査役のフリー画像（写真）](https://www.pakutaso.com/20141018279post-4660.html)
![AL206_kinshi220140810213504_TP_V.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403060/b12d7297-c4a8-5295-bded-e36aeaeeea1b.jpeg)


#### 実行結果
```console
顔が検出できました。:
faceDivision: base , sourceUrl: {"比較元画像ファイルパス"}
faceId: dea8b4c8-550a-46a9-b289-327fd8b9eaeb
Rectangle: {885, 216, 150, 150}

faceDivision: base , sourceUrl: {"比較元画像ファイルパス"}
faceId: 97812fab-a7b8-4fc6-a337-022b74ca3f3f
Rectangle: {472, 197, 139, 139}

faceDivision: target , sourceUrl: {"比較先画像ファイルパス"}
faceId: 63b697e5-3043-419b-9c10-95e59a1641c0
Rectangle: {786, 558, 105, 105}

faceDivision: target , sourceUrl: {"比較先画像ファイルパス"}
faceId: 28cb05e9-68bf-4a43-92e4-1d4c31ad5b2a
Rectangle: {1054, 242, 103, 103}

■FaceID：[dea8b4c8-550a-46a9-b289-327fd8b9eaeb] との類似度判定
　28cb05e9-68bf-4a43-92e4-1d4c31ad5b2a : 27.62％
　63b697e5-3043-419b-9c10-95e59a1641c0 : 9.75％
■FaceID：[97812fab-a7b8-4fc6-a337-022b74ca3f3f] との類似度判定
　28cb05e9-68bf-4a43-92e4-1d4c31ad5b2a : 61.75％
　63b697e5-3043-419b-9c10-95e59a1641c0 : 13.41％
処理終了
```

<img width="791" alt="キャプチャ11.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403060/2abb135c-3483-3876-eca9-a89b97439372.png">
<img width="441" alt="キャプチャ12.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403060/2c1875fe-585f-f5a8-94bc-5c443d9728c9.png">

比較元画像から2人分の顔が検出され、比較先画像で検出された2人分の顔とそれぞれ比較、類似度が判定されていますね。

以上で終了になります。

# おわり
.NET Framework（C#, VB）を用いてFace FindSimilarの類似判定をしている記事が意外に見つけられなかったので、自分でやってみました。

SDKでNuGetに「Microsoft.Azure.CognitiveServices.Vision.Face」とかいうすごく便利そうなものがあると知ったのは作った後のこと。泣きそう…

次はちゃんとこの辺活用したコード書きます！