Help us understand the problem. What is going on with this article?

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

作るもの

比較元の画像(base)に写っている人が、比較先の画像(target)に写っている人とどれだけ似ているかを表示してくれるコンソールアプリ。

書いてる内に妙に長くなってしまったので、①ではそれぞれの画像の顔の検出までを実装していきます。

完成版の実行結果

以下の画像を使用して、比較元画像の男性に対する、比較先画像の男女の類似度を判定させます。

↓比較元画像↓
test1.jpg

↓比較先画像↓
test3.jpg

アプリを実行すると、以下のように検出箇所と割り振られたFaceIDを確認できます。
キャプチャ.PNG
キャプチャ2.PNG

また、比較元画像のFaceIDとの類似度をコンソール上で表示します。

■FaceID:[0c8be41b-6d64-4a46-afcd-e4d70b78da12] との類似度判定
 7b05c4b3-4ffd-49aa-b0f2-14ca87dccff1 : 54.08%
 560bc9a3-6e68-442d-bc39-fcd2f21a32b1 : 8.98%

この場合、ちゃんと男性との類似度が高めに、女性との類似度が低めになっていますね。
(高めにとは言ってもそんなに数値高くないですが…)

これは完全に体感ですが、48%(APIからの戻り値が0.48)以上あれば、同一人物と判断してもよさそうです。
ちなみに、もし比較元画像から2人以上の顔が検出されれば、それぞれとの類似度が表示されることになります。

環境、使ったもの

  • Windows10
  • C# 7.0
  • .NET Framework 4.7.2
  • Visual Studio 2017 Community ※2019と行ったり来たりしてたけど
  • Microsoft Face API
  • OpenCVSharp 3
  • Json.NET 12.0.2

Face FindSimilarについて

今回はCognitive ServicesのFace APIから、Face FindSimilarという機能をアプリのメインとします。
Cognitive Servicesは、Microsoft Azureの提供するAIや機械学習に関するサービス群のことです。
Face FindSimilarは比較元となるFaceID(検出した顔情報に一意に割り振られるID)と、比較先となるFaceIDの配列や事前に作成した顔グループを渡すことで、比較先の顔が比較元とどれだけ似ているのかを返してくれる機能です。

ちなみにFaceIDを取得するにはFace Detect機能を使用する必要があるので、そちらも同時に実装していきます。
というか①ではそっちの実装がメインになります…

※Face Detect機能のみの単純な実装は、前回の【10分でできる】AzureのFace APIを使って画像から顔を検出するで紹介しています。
とりあえずFace APIを使ってみたいという方や、画像に写っている人の性別、年齢、特徴、感情などが検出できればいいという方はそちらを参照されることをお勧めします。
今回は画像の加工などもあるため、さすがに10分でとはいきませんでした( ;∀;)

Face APIへPOSTする際の各パラメータなど

Face Detect

  • リクエストヘッダ:
    • Content-Type:"application/octet-stream"
    • Ocp-Apim-Subscription-Key:Face APIキー
  • リクエストボディ:
    • url:画像のバイナリデータ
  • POST先のURL:エンドポイント + "/detect" + "?returnFaceId=true&returnFaceLandmarks=false"

Face FindSimilar

  • リクエストヘッダ:
    • Content-Type:"application/json"
    • Ocp-Apim-Subscription-Key:Face APIキー
  • リクエストボディ:
    • faceId:Face Detectで取得する一意のID。取得後24時間で期限切れとなります。
    • faceListId:ユーザー指定の一意の候補顔リスト。(今回は未使用)
    • LargefaceListId:ユーザー指定の一意の候補顔リスト。(今回は未使用)
    • faceIds:Face Detectで取得する一意のIDの配列。
    • maxNumOfCandidatesReturned(オプション):比較先となるfaceIdの内、上位何番目までの顔情報を返すか(デフォルトは20)
    • mode(オプション):判定方法。"matchPerson"または"matchFace"を指定。
  • POST先のURL:エンドポイント + "/findsimilars"

参考:
Face Detect APIリファレンス
Face FindSimilar APIリファレンス

作成手順

だいたいこんな感じ

  1. (準備)
  2. Face Detectから顔情報を取得
  3. Face Detectから戻り値の受け取り
  4. OpenCVSharpで検出箇所を画像に描画
  5. Face FindSimilarで類似度の判定

以下、この手順で実装を進めていきます。

1. 準備

コンソールアプリを新規作成

作りましょう。
名前はとりあえず「FindSimilar」とします。
キャプチャ1.PNG
キャプチャ2.PNG

ついでに「Program.cs」ファイルの名前も変えちゃいます。
ソリューションエクスプローラー上で「Program.cs」のファイル名を「FaceFindSimilar.cs」と変更すると、class名も「Program」から「FaceFindSimilar」となったかと思います。
キャプチャ3.PNG

設定ファイルの記述

APIキーと、APIにアクセスする為のエンドポイントをApp.configに持たせます。
appSettingsタグとその中身が追加した部分です。

App.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
    </startup>
    <appSettings>
      <add key="subscriptionKey" value="Your API Key" />
      <add key="requestUrl" value="https://westcentralus.api.cognitive.microsoft.com/face/v1.0/" />
    </appSettings>
</configuration>

"subscriptionKey"のvalueにAPIキーを、"requestUrl"のvalueにFace APIキー取得時に表示されたエンドポイントを記載してください。

2. Face Detectから顔情報を取得

OpenCVSharp、Face FindSimilarを使う上で必要な情報を、Face Detectから取得しましょう。

FaceDetect.csファイルの作成

すべての処理をFaceFindSimilar.csファイルに書くと読みにくいので、FaceDetect.csファイルを作成し、Face Detect機能による顔の検出処理はすべてそこに記述することにします。

「Ctrl」+「Shift」+「A」で「新しい項目の追加」ウィンドウを開きましょう。
キャプチャ4.PNG
「クラス」を選択し、名前は「FaceDetect.cs」とします。
作成できたら、以下のようになると思います。
キャプチャ5.PNG

戻り値はなにか

メインメソッドからFaceDetectクラスのメソッド(Detectメソッドとします)を呼び出して、検出した顔のIDやOpenCVSharpで枠を描画する為の座標を取得するわけですが、まずはその時Detectメソッドに渡す引数と、Detectメソッドから欲しい情報を整理してみます。

引数

Face DetectはAPIに対してURLだけ渡せばいいので、比較元画像のURLと比較先画像のURLを渡してあげようと思います。
ただし、URLをただの文字列や配列として渡すと比較元と比較先の区別がつかなくなるので、今回はdictionary型にして以下のように渡したいと思います。

  • Key: "base" (string) Value: "比較元URL" (string)
  • Key: "target" (string) Value: "比較先URL" (string)
戻り値

顔情報の検出後、FaceFindSimilarクラス側で必要な情報は以下になります。
検出箇所の座標は、OpenCVSharpで検出箇所を描画する為に必要になります。

  • 取得元: "base" or "target" (string)
  • 取得元URL: "比較元URL" or "比較先URL" (string)
  • 検出した顔のID (string)
  • 検出箇所の座標(x軸) (int)
  • 検出箇所の座標(y軸) (int)
  • 検出箇所の座標(左上からの幅) (int)
  • 検出箇所の座標(左上からの高さ) (int)

戻り値が多いので、今回は上記を含む「FaceInfo」クラス型を作成し、そのリストを返してあげることにしました。
namespace FaceFindSimilarの中に、以下のようにクラスとプロパティを追加します。

FaceDetect.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace FindSimilar
{
    public class FaceInfo
    {
        // 取得元: "base" or "target" (string)
        public string faceDivision { get; set; }

        // 取得元URL: "比較元URL" or "比較先URL" (string)
        public string sourceUrl { get; set; }

        // 検出した顔のID (string)
        public string faceId { get; set; }

        // 検出箇所の座標(x軸) (int)
        public int recLeft { get; set; }

        // 検出箇所の座標(y軸) (int)
        public int recTop { get; set; }

        // 検出箇所の座標(左上からの幅) (int)
        public int recWidth { get; set; }

        // 検出箇所の座標(左上からの高さ) (int)
        public int recHeight { get; set; }
    }

    class FaceDetect
    {
    }
}

Detectメソッドの実装

次に、実際にFindSimilarメソッドから呼び出されるDetectメソッドを、FaceDetectクラスの中に記述していきます。

FaceDetect.cs
    class FaceDetect
    {
        static public List<FaceInfo> Detect(Dictionary<string, string> imageUrls)
        {
            List<FaceInfo> faceInfoList = new List<FaceInfo>();

            try
            {
                //faceInfoList = ImageAnalysisRequest(imageUrls).Result;
            }
            catch (Exception e)
            {
                Console.WriteLine("例外:" + e.Message);
            }

            return faceInfoList;
        }
    }

Detectメソッドは戻り値となる顔情報を返すだけです。
実際にFace API Detectから顔情報を取得してくる処理は、Try~Catchの中で現在コメントアウトされているImageAnalysisRequestメソッドで行います。

ImageAnalysisRequestメソッド + α の実装

FaceDetectクラスの中に、Face API Detectの呼び出し処理を追加します。
また、APIに渡す画像はバイナリデータにする為、そのためのGetImageAsByteArrayメソッドも追加しています。

FaceDetect.cs
        // Face API Detectを使用して画像分析を行う
        static async Task<List<FaceInfo>> ImageAnalysisRequest(Dictionary<string, string> imageUrls)
        {
            List<FaceInfo> faceInfoList = new List<FaceInfo>();
            HttpClient client = new HttpClient();

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

            // リクエストパラメータ
            string requestParameters = "returnFaceId=true&returnFaceLandmarks=false";

            string uri = ConfigurationManager.AppSettings["requestUrl"] + "detect" + "?" + requestParameters;

            // 画像ファイルを一枚ずつ処理する
            foreach (KeyValuePair<string, string> item in imageUrls)
            {
                string contentString = string.Empty;

                byte[] byteData = GetImageAsByteArray(item.Value);

                // 画像から顔を検出
                using (ByteArrayContent content = new ByteArrayContent(byteData))
                {
                    // リクエストヘッダーの作成
                    content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");

                    // Face APIの呼び出し
                    HttpResponseMessage response = await client.PostAsync(uri, content);

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

            }

            return faceInfoList;
        }

        // 画像をバイナリデータに変換
        static byte[] GetImageAsByteArray(string imageFilePath)
        {
            using (FileStream fileStream =
                new FileStream(imageFilePath, FileMode.Open, FileAccess.Read))
            {
                BinaryReader binaryReader = new BinaryReader(fileStream);
                return binaryReader.ReadBytes((int)fileStream.Length);
            }
        }

これだけだと色々参照エラーが出ると思うので、参照に以下System.Configurationを追加して、
キャプチャ3.PNG
以下のusingも追加します。

using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Configuration;

何をやっているかというと、引数として受け取った画像ファイルのdictionaryを一つずつ取り出して、Face API Detectに渡したその実行結果、つまりFaceIDや検出箇所の座標を取得しています。
リクエストパラメータにもっと色々指定することで、検出した顔の推定年齢、感情や髪色その他までわかるのですが、今回は必要ないので必要最低限"returnFaceId=true&returnFaceLandmarks=false"だけ指定しています。

この時点ではまだアプリケーションを実行できませんが、もし比較先画像のファイルパスがitem.Valueに入っていた場合、以下のようなJSON文字列がcontentStringから取得できるはずです。

[
  {
    "faceId":"46086930-6e5b-4c54-956b-1d72de419266"
    ,"faceRectangle":{"top":260,"left":765,"width":148,"height":148}
  }
  ,{
    "faceId":"f8410c80-b7ff-4d05-bd1f-75a7f3cb2cc8"
    ,"faceRectangle":{"top":347,"left":545,"width":124,"height":124}
  }
]

faceIdが、検出できた顔に割り振られた一意のID。
faceRectangleの配列が、検出された位置の座標です。
この時点では2人分の顔が検出され、配列として返ってきています。

今はただのJSONなので、今度はこれを戻り値の型である独自クラスFaceInfo型のListに整形する必用があります。
整形するにあたってはJSONをstring型の配列として扱いたいので、ここではJSONをC#で扱う為のライブラリ「Json.NET」を使って整形したいと思います。

まず、Json.NETをNuGetから取得しましょう。
VisualStudioの「プロジェクト」メニューから「NuGetパッケージの管理」を選択します。
キャプチャ4.PNG
NuGetの管理画面が開くので、「参照」を選択し、検索窓に「Newtonsoft.Json」と入力。ロケットのアイコンのライブラリが表示されるので選択して、インストールボタンをクリックします。
現時点でのバージョンは12.0.2でした。
キャプチャ5.PNG
インストールが正常に完了すれば、ソリューションエクスプローラーの参照先に「Newtonsoft.Json」が追加されているはずです。
キャプチャ6.PNG
無事参照追加できていたら、FaceDetect.csファイルに処理を追加します。
「追加箇所 From(~To)」となっている箇所が新たに追記した部分です。
※このファイルの修正はここまでなので、全文を載せます。

FaceDetect.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Configuration;
// 追加箇所 From-----------------------------------------------------------------
using Newtonsoft.Json.Linq;
// 追加箇所 To-------------------------------------------------------------------

namespace FindSimilar
{
    public class FaceInfo
    {
        // 取得元: "base" or "target" (string)
        public string faceDivision { get; set; }

        // 取得元URL: "比較元URL" or "比較先URL" (string)
        public string sourceUrl { get; set; }

        // 検出した顔のID (string)
        public string faceId { get; set; }

        // 検出箇所の座標(x軸) (int)
        public int recLeft { get; set; }

        // 検出箇所の座標(y軸) (int)
        public int recTop { get; set; }

        // 検出箇所の座標(左上からの幅) (int)
        public int recWidth { get; set; }

        // 検出箇所の座標(左上からの高さ) (int)
        public int recHeight { get; set; }
    }

    class FaceDetect
    {
        static public List<FaceInfo> Detect(Dictionary<string, string> imageUrls)
        {
            List<FaceInfo> faceInfoList = new List<FaceInfo>();

            try
            {
                faceInfoList = ImageAnalysisRequest(imageUrls).Result;
            }
            catch (Exception e)
            {
                Console.WriteLine("例外:" + e.Message);
            }

            return faceInfoList;
        }

        // Face API Detectを使用して画像分析を行う
        static async Task<List<FaceInfo>> ImageAnalysisRequest(Dictionary<string, string> imageUrls)
        {
            List<FaceInfo> faceInfoList = new List<FaceInfo>();
            HttpClient client = new HttpClient();

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

            // リクエストパラメータ
            string requestParameters = "returnFaceId=true&returnFaceLandmarks=false";

            string uri = ConfigurationManager.AppSettings["requestUrl"] + "detect" + "?" + requestParameters;

            // 画像ファイルを一枚ずつ処理する
            foreach (KeyValuePair<string, string> item in imageUrls)
            {
                string contentString = string.Empty;

                byte[] byteData = GetImageAsByteArray(item.Value);

                // 画像から顔を検出
                using (ByteArrayContent content = new ByteArrayContent(byteData))
                {
                    // リクエストヘッダーの作成
                    content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");

                    // Face APIの呼び出し
                    HttpResponseMessage response = await client.PostAsync(uri, content);

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

                // 追加箇所 From-----------------------------------------------------------------
                // 戻り値となるリストへ顔情報を追加していく
                JArray jArray = JArray.Parse(contentString);
                foreach (JObject jObj in jArray)
                {
                    // 顔情報をFaceInfoクラス型に整形する
                    FaceInfo faceInfo = CreateFaceInfo(jObj, item);

                    faceInfoList.Add(faceInfo);
                }
                // 追加箇所 To-------------------------------------------------------------------

            }

            return faceInfoList;
        }

        // 追加箇所 From-----------------------------------------------------------------
        // FaceInfoクラス型のインスタンスを作成
        static FaceInfo CreateFaceInfo(JObject jObj, KeyValuePair<string, string> keyValue)
        {
            JValue jValFaceId = (JValue)jObj["faceId"];
            JValue jValRectLeft = (JValue)jObj["faceRectangle"]["left"];
            JValue jValRectTop = (JValue)jObj["faceRectangle"]["top"];
            JValue jValRectWidth = (JValue)jObj["faceRectangle"]["width"];
            JValue jValRectHeight = (JValue)jObj["faceRectangle"]["height"];

            FaceInfo faceInfo = new FaceInfo();
            faceInfo.faceDivision = keyValue.Key;
            faceInfo.sourceUrl = keyValue.Value;
            faceInfo.faceId = jValFaceId.ToString();
            faceInfo.recLeft = int.Parse(jValRectLeft.ToString());
            faceInfo.recTop = int.Parse(jValRectTop.ToString());
            faceInfo.recWidth = int.Parse(jValRectWidth.ToString());
            faceInfo.recHeight = int.Parse(jValRectHeight.ToString());

            return faceInfo;
        }
        // 追加箇所 To-------------------------------------------------------------------

        // 画像をバイナリデータに変換
        static byte[] GetImageAsByteArray(string imageFilePath)
        {
            using (FileStream fileStream =
                new FileStream(imageFilePath, FileMode.Open, FileAccess.Read))
            {
                BinaryReader binaryReader = new BinaryReader(fileStream);
                return binaryReader.ReadBytes((int)fileStream.Length);
            }
        }
    }
}

追加箇所ではFace Detectの結果JSONを入れていたcontentStringを、JArray型というJson.NET独自の配列にパースしています。
その後foreachで配列の数だけCreateFaceInfoメソッドを呼んでFaceInfo型に整形した後、faceInfoListというListにAddしていきます。

本当は上記のような方法ではなく、faceInfoList = JsonConvert.DeserializeObject<List<FaceInfo>>(contentString);として一文で書ければよかったのですが、今回は戻り値にどの画像から取得してきたのかという情報を付加したかったので、最終的にこの形に落ち着きました。
戻り値の型をList<FaceInfo>ではなくDictionary<string, List<FaceInfo>>としてKeyに"base" または "target"を指定してあげればよかったような気もしますが、そうすると受け取り側の処理がまた煩雑になりそうだったので、とりあえず今回はこれで。

ここまででFace Detect側の処理は終了です。
(長かった…)

最後に、FaceFindSimilar.cs側で値を受け取れるか確認して、①は終了したいと思います。
もう少しだけお付き合いくださいませ。

3. Face Detectから戻り値の受け取り

FaceFindSimilar.csファイルのFaceFindSimilarクラス内部を以下のように記述します。

FaceFindSimilar.cs
    class FaceFindSimilar
    {
        const string URL_BASE = "ローカル画像ファイルのパス(比較元)";
        const string URL_TARGET = "ローカル画像ファイルのパス(比較先)";
        const string DIVISION_BASE = "base"; // 区分:比較元
        const string DIVISION_TARGET = "target"; // 区分:比較先

        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);

            // Face Detectから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);
                }
            }
            else
            {
                Console.WriteLine("顔が検出できませんでした。");
            }

            Console.ReadLine();

        }
    }

定数URL_BASEURL_TARGETに対してローカル画像のパス(今回は冒頭の画像を指定しています)を指定して実行すると、以下のような値が取れるかと思います。

顔が検出できました。:
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}

Face Detectを使用して、その戻り値を取得できました。
次回②で、本題のFace FindSimilarを使用して類似度の判定を行いたいと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした