AR顔認証打刻アプリを作る

#はじめに
皆さん、仕事の日は毎日打刻していると思いますが。
正直めんどくさいですよね。
それを解決するには、楽にできる方法を考えるのが普通だと思っていましたが。
[この記事](https://developers.freee.co.jp/entry/time-record-vr)を読んで、気分を上げる打刻という解決策があるということを教わり、便乗してVRがあるならARもありだろうということで、作ろうと思いました。

#作ったもの

動画はこちらです。

**追記**
ビルドすることができました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">やっとビルドできた <a href="https://t.co/oPHAs0zxuG">pic.twitter.com/oPHAs0zxuG</a></p>&mdash; Akome (@akomekagome) <a href="https://twitter.com/akomekagome/status/1283467044604669952?ref_src=twsrc%5Etfw">July 15, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

~~https://twitter.com/akomekagome/status/1283239374348406785~~

~~あれARは？と思ったと思いますが~~
~~Macが不調でスマホにビルドすることができませんでした...~~
~~後日、ビルド出来たら上げ直したいと思います。~~

コードをあげておきます。(github)

https://github.com/akomekagome/ARAttendanceManagementScript

##使用ライブラリ
FreeeForUnity
Unirx
Unitask
Zenject
Dotween
AWS for .net
OVRLipsync
OpenCV + Unity

#システムの流れ

1.カメラの画像からOpenCV + Unityで顔を検出
2.先ほどの画像をAWS Rekognitionで顔を照合
3.取得した顔情報から、Freeeの従業員データーと紐づけし、打刻を行う

#Freeeへの接続
接続は先ほどの記事を書いたtoofusanさんのコードを使わせていただきました。

https://github.com/toofusan/FreeeForUnity

使用するには、client_id, client_secret,code, access token, authorizationCode, employeeIdが必要になります。
ただ、employeeIdは初期のセットアップで取得できないので、個別で取得する必要があるのですが、一番簡単な方法として
[Free人事労務フリー](https://p.secure.freee.co.jp/)/勤怠/従業員名
でアクセスしたurlが

https://p.secure.freee.co.jp/employees#/employeeId/年/月/

となっているので、ここから取得できます。

#カメラの画像をOpenCV + Unityで顔を検出

AWS Rekognitionも無料ではないので、できるだけ費用を抑えるためカメラに顔が映ったときだけ顔照合しています。
OpenCVでデフォルトで入っているカスケード型分類器を使って、顔認識を行うのですが、Unityに持っていかないと使えないので、StreamingAssetsに持っていく必要があります。

```cs:OpenCvGateway.cs
public class OpenCvGateway
{
    public bool DetectFaceInImage(Mat mat, string cascadePath)
    {
        var cascade = new CascadeClassifier();
　　　　// ここでカスケード型分類器のパスを指定
        cascade.Load(cascadePath);
        
        var faces = cascade.DetectMultiScale(mat, 1.1, 3, 0, new Size(20, 20));
        
        Debug.Log(faces.Length);
        return faces.Length > 0;
    }
}
```

#AWS Rekognitionで顔認証　

通常UnityからAWSを使う場合は、AWS for Untiyから使用するのが普通だと思いますが、今回使うAWS Rekognitionが非対応だったため、やむを得ずaws for .netのnugetをunityに持ってきました。
ただ今思えば、AWS Lambaは使えるので、そちらの方でAWS Rekognitionは使ったほうがいい気がします。(未検証)
方法としては、nugetの拡張子をzipに変更し、中身から自分が欲しい.netのバージョンのdllを探し、UnityのPluginフォルダーに突っ込めば、使用できます。nugetは以下から取得できます。

https://www.nuget.org/packages/AWSSDK.Rekognition/3.5.0-beta

使い方は以下が参考になります。

https://docs.aws.amazon.com/ja_jp/sdk-for-net/v3/developer-guide/quick-start.html

AmazonRekognitionへの接続は以下のような形です。

```cs:AmazonRekognitionGateway.cs
    public class AmazonRekognitionGateway
    {
        AmazonRekognitionClient rekoClient;

        public AmazonRekognitionGateway(CognitoAWSCredentials credentials)
        {
            rekoClient = new AmazonRekognitionClient(credentials);
        }
        
        public AmazonRekognitionGateway(string keyId , string secretKey)
        {
            rekoClient = new AmazonRekognitionClient(keyId, secretKey, AwsSettings.Region);
        }

        public AmazonRekognitionGateway(AWSAuthData awsAuthData)
        {
            rekoClient = new AmazonRekognitionClient(awsAuthData.keyId, awsAuthData.secretKey);
        }
        
        public async UniTask<List<string>> AuthenticateFaceAsync(MemoryStream stream, string collectionId, CancellationToken token = default)
        {
            var image = new Image()
            {
                Bytes = stream
            };
            return await AuthenticateFaceAsync(image, collectionId, token);
        }

        private async UniTask<List<string>> AuthenticateFaceAsync(Image image, string collectionId, CancellationToken token = default)
        {
            var searchFacesByImageRequest = new SearchFacesByImageRequest()
            {
                CollectionId = collectionId,
                Image = image,
                FaceMatchThreshold = 90f,
                MaxFaces = 1
            };

            try
            {
                var searchFacesByImageResponse = await rekoClient.SearchFacesByImageAsync(searchFacesByImageRequest, token);
                
                var faceIds = searchFacesByImageResponse.FaceMatches
                    .Select(x => x.Face.FaceId)
                    .ToList();
            
                DebugExtensions.DebugShowList(faceIds);
            
                return faceIds;
            }
            catch (Exception e)
            {
                Debug.Log(e);
                return default;
            }
        }
    }
```

AWS for .netでAmazonRekognitionを紹介している記事が日本だとないので、知見を共有すると、カメラから取得したTexture2Dをjpgのbyte配列に変換し、それをMemoryStreamに変換することによって、Amazon S3を介することなく顔照合が行えます。(料金が浮く)

```cs:FaceRecognitionProvider.cs
       private async UniTask<List<string>> AuthenticateFace(Mat mat, CancellationToken token = default)
        {
            var tex = webCamClient.GetTexture2D();
            var jpgBytes = tex.EncodeToJPG();
            Destroy(tex);
            var stream = new MemoryStream(jpgBytes);
            
            return await rekoGate.AuthenticateFaceAsync(
                stream,
                AmazonRekognitionSettings.FaceCollectionId,
                token);
        }
```

#FreeeのAPIから打刻を行う

AmazonRekognitionは顔がFaceId(文字列)と結びついているので、employeeIdを紐づけしておき、打刻を行います。

以下のコードでまず、打刻可能なタイプを取得します

```cs:FreeeEventHandler.cs
public void GetTimeClocksAvailableTypes(int employeeId, Client.Callback callback = default) 
{
    var endpoint = "https://api.freee.co.jp/hr/api/v1/employees/" + employeeId +
                   "/time_clocks/available_types";
    var parameter = new Dictionary<string, string>
    {
        {"company_id", company_id.ToString()}
    };
    StartCoroutine(client.Get(endpoint, parameter, callback));
}
```

それをもとに打刻を行います。

```cs:FreeeEventHandler.cs
        public void PostTimeClocks(string type, int employeeId)
        {
            var endpoint = "https://api.freee.co.jp/hr/api/v1/employees/" + employeeId + "/time_clocks";
            var p = TimeClockRequestJson(type, DateTime.Now);
            StartCoroutine(client.Post(endpoint, p, OnPostTimeClocks));
        }

        void OnPostTimeClocks(bool success, string response)
        {
            if (!success)
            {
                var msg = JsonUtility.FromJson<Message>(response).message;
                return;
            }

            var tc = JsonUtility.FromJson<PostTimeClocksResponse>(response).employee_time_clock;
            _postTimeClockSubject.OnNext((FreeeType)Enum.Parse(typeof(FreeeType), tc.type));
        }
```

#OVRLipsyncでリップシンクする

Oculusが作成したOVRLipsyncを使用します。Vtuberとかがよく使ってる口パクできるやつです。
といっても使い方は、とても簡単で使用したいキャラクターにAudioSource, OVRLipSyncContext, OVRLipContextMorphTargetをアタッチするだけです。
BlendShapesがあるSkinned Mesh Renderが必要になります。

それぞれ
aa -> あ
E -> え
ih -> い
oh -> お
ou -> う
という感じに、紐ずいています

<img width="441" alt="2020-07-15 (2).png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667820/cd3bea6f-53ec-07c3-6e2d-bfcad18feafa.png">

#まとめ
中途半端な形になってしまい申し訳ないです。
原因を見つけ次第更新したいと思います。
作業中、Unityちゃんの「進捗どうですか？」がぐさぐさ刺さりました。