ARCoreアプリで撮影した写真をLogic App経由でTwitterに投稿する（顔検出とShared Access Signatureを添えて）

#はじめに
Unityで作成したAndroidアプリ上で撮影したAR写真をAzure経由でTwitterへ投稿してみようという試みです。ローカル保存→Twitterへ投稿という流れではなくあえてAzure経由にした理由としては、Azure上のCognitive Serviceとのコラボが期待できると感じたことが挙げられます。
(あとLogic Appに組み込まれているTwitter投稿の仕組みを利用すると、公式のAPI申請を待たなくても良いやつ)

何か間違っている点や改善案などがございましたらコメント等でお知らせいただけるとと幸いです。

##環境
- Unity2018.4.2
- VisualStudio2019 Enterprise
- ARCoreに対応したAndroid端末（今回はHuawei Mate20 lite）

##準備
- Azureサブスクリプション
- Unityで作成したARアプリ（使用するスクリプトはC#）
- AzureのStorage Account（[参考](https://docs.microsoft.com/ja-jp/azure/storage/)）
- Face APIのサービス（Portal上で作成してキーを入手出来ていたらOKです）

#作成したもの
今回は撮影したAR写真の中に顔が写っているかどうかを判定して、Twitterの投稿テキストを変えるという簡単なアプリを作成しました。動作の流れは以下の画像の通りで、簡単にはUnity→Logic App→Twitterという流れになっております。今回はFaceAPIの顔検出しか利用していませんが、顔識別やVisonAPIで物体認識をやってみても良いでしょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/161850/f9967f56-c49d-99a1-192d-2070fc013839.png)

画像投稿の結果はこのような感じになります。（左：顔検出なし、右：顔検出あり）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/161850/ef1d5a45-064b-5872-96e9-6db4943e9a91.png)


#簡単な解説
##はじめに考えるべきこと
###イベントトリガーについて
Logic Appには様々なコネクターが存在し、開発者はノンコーディングでイベントドリブンの処理を作成できることが売りです。今回の場合、撮影した画像をblobに保存しLogic Appでその写真を利用する形であるため、Logic Appの最初のトリガーとしては以下が挙げられます。

- HTTP RequestをUnityアプリ側から受け取る
- 「blobが追加または変更されたとき（プロパティのみ）」を利用してblobの変更を検知する

一見すると「blobが追加または変更されたとき（プロパティのみ）」が良さそうに見えます。しかし、このトリガーの詳細を見てみますと、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/161850/da05ac45-5e58-d92d-318f-fd747fb427d7.png)
変更をチェックする間隔を指定する設定があることがわかります。つまり、写真を撮影してもここで設定した頻度でしか処理が動かないことになります。今回は、写真を撮影したらリアルタイムに処理を行ってほしいのでHTTP Requestを利用することにします。（間隔を3秒とか短いのにすればよいのではという意見もありそうですが、その場合は写真を撮影していない時も常に処理が動くことになるので課金額が増えることになります。）

###セキュリティについて
Azure StorageのBlobストレージはデフォルトではストレージへの匿名アクセスを許可していません。つまり、ただURL(https://example.blob.core.windows.net/containernonamae/file.png) でアクセスしようとしても弾かれてしまいます。普段ならそれでよいのですが、FaceAPIを利用するためにはImage URLを与える必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/161850/6a88c72e-254e-531d-dcb4-d047ea76d317.png)
このままでは、Bad Requestが返ってくるため、対策を考える必要があります。ここで考えられる対策は以下の2点です。

- Blobストレージの匿名アクセスを許可する
- Shared Access Signature（SAS）を利用する

匿名アクセスを許可する方法はそのままの意味で、Blobストレージのアクセスポリシーを変更してあげるだけですべてが解決します。デメリットとしては、URLが漏洩した場合に誰もがデータにアクセスできてしまうことが挙げられます。これはかなり厳しいデメリットであるため、サービスSASを利用したアクセスを今回は採用します。（SASについては[公式ドキュメント](https://docs.microsoft.com/ja-jp/azure/storage/common/storage-sas-overview)を参照してください。）
そもそもLogic App上でImageURL指定以外の方法を用いてFaceAPIが利用可能（つまりFaceAPIから外部を経由せず直接Blobストレージへアクセスする方法）ならばそちらの方が良さそうな気がします。

##Unity側の実装
Unityでは大きく分けて以下の処理を行います。

- Storage Accountへの接続
- Blobのアップロード
- サービスSASの取得
- Logic Appで用いるメタデータを含めたJson作成
- Logic Appのエンドポイントをたたく

Storage Accountへの接続からblobアップロードまでのコード例（[公式のサンプル](https://docs.microsoft.com/ja-jp/azure/storage/blobs/storage-quickstart-blobs-dotnet#upload-blobs-to-a-container)参照）

```csharp
        //事前に StorageAccount = CloudStorageAccount.Parse(ConnectionString);を実行済み
        CloudBlobClient blobClient = StorageAccount.CreateCloudBlobClient();

        CloudBlobContainer container = blobClient.GetContainerReference(_blockBlobContainerName);
        try
        {
            await container.CreateIfNotExistsAsync();
        }
        catch (StorageException)
        {
            Debug.Log("Faild to connect Azure Storage (in blobContainer.CreateIfNotExistsAsync())");
            //例外処理はここに
            throw;
        }
        //blobへの参照作成
        CloudBlockBlob blockBlob = container.GetBlockBlobReference(filename);

#if WINDOWS_UWP
		Windows.Storage.StorageFolder storageFolder = await Windows.Storage.StorageFolder.GetFolderFromPathAsync(path);
		Windows.Storage.StorageFile sf = await storageFolder.GetFileAsync(filename);
		await blockBlob.UploadFromFileAsync(sf);
#else
        //画像のアップロード
        await blockBlob.UploadFromFileAsync(Path.Combine(path, filename));
#endif
```

ここでは単純にBlobストレージへファイルをアップロードする手順を踏んでいます。余談ですが、Androidでは単にアップロードしたいファイルのpathを`UploadFromFileAsync()`の引数に入れてあげればよい一方で、WindowsUWP(例えばHoloLens)の場合では引数としてStorageFileが指定されていることに注意します。（過去にここでめちゃくちゃハマった経験）

各blobのサービスSASを取得しLogic Appを呼び出すまでのコード例（SASは[公式のサンプル](https://docs.microsoft.com/ja-jp/azure/storage/blobs/storage-blob-service-sas-create-dotnet)参照）

```csharp
        //blobのサービスSASを作成
        var sharedAccessBlobPolicy = new SharedAccessBlobPolicy()
        {
            SharedAccessExpiryTime = DateTime.UtcNow.AddMinutes(10),
            Permissions = SharedAccessBlobPermissions.Read
        };
        var sasBlobToken = blockBlob.GetSharedAccessSignature(sharedAccessBlobPolicy);

        //LogicAppへ接続
        var picinfo = new PicInfo(StorageAccount.BlobEndpoint.ToString(), _blockBlobContainerName, filename, alternaName, sasBlobToken);
        var json = JsonConvert.SerializeObject(picinfo);

        //private static HttpClient client = new HttpClient();を宣言済み
        var content = new StringContent(json, Encoding.UTF8, "application/json");
        var response = await client.PostAsync(LogicAppEndpoint, content);
        
```

ここではアップロードするblob（今回は画像ファイル）に対して、`GetSharedAccessSignature()`を実行してあげることでサービスSASの取得を行っています。`SharedAccessBlobPolicy`を宣言する際の`SharedAccessExpiryTime`が該当するblobへアクセスできる期間(SASの有効期限)に該当し、`Permissions`がそのblobに対して行える操作権限付与に該当します。今回は、有効期限20分の読み取り専用SASを設定しています。
このサービスSASを設定することで、万が一blobへアクセスできるSASつきURLが流出した場合でもSAS自体に有効期限が設定されているため、第三者がアクセスしづらい状態を保てると思います。（これはあくまでもサービスSASを設定した場合）

Logic Appへの接続部分は、newtonfoft.jsonを利用してリクエストのbodyを作成し、`HttpClient()`でLogic Appのエンドポイントへリクエストを投げているだけです。

##Logic App部分の実装
大まかな流れは以下の通りです。（各API接続では指示の通りにキーやURLを入力してください）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/161850/9fdd0a58-d77d-fcef-d158-349f63c4147e.png)

HTTP Requestトリガーでは、受信する要求のJSONスキーマを設定することができ、それらを変数的に後の部分で利用できます。JSONのスキーマを設定することが面倒である場合は「サンプルのペイロードを使用してスキーマを生成する」を利用すると良いでしょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/161850/6660afc6-2a70-abfb-7d49-5cce6ba2f655.png)

先ほどのトリガーでスキーマを設定すると、blobコンテンツの取得やFaceAPIの部分でJSONに基づいた動的な値を入れることができます。ここで注意するべきところは、FaceAPIのImageURLを指定する部分でSASをURL内に入れておく必要があるところです。（もちろん匿名アクセスが可能な場合は必要ないです）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/161850/59038211-eb4e-9d33-917a-3d427d2e79cb.png)

あとは、FaceAPIの結果に応じてTwitterの投稿内容を変えるように設定すると完成です。今回の場合は、顔が存在するかどうかが判断基準なのでFaceAPIから帰ってくるJSONが空かそうでないかで条件分岐を行います。JSONが空の場合にForEachが回らないことを利用して条件分岐を行っていますが、正直ごり押し感がものすごいので要検討部分です…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/161850/5030837f-1de8-11c0-1cd3-35ba3fd57f09.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/161850/efef8fac-6d75-a7f0-4bd7-a6e5cd5ec918.png)

#おわりに
Logic Appで簡単に他サービスと連携できるとのことだったので単純な顔検出を組み込んだTwitter投稿アプリを作成しました。結局Twitterに投稿するのにStorageのセキュリティを気にするなど少し矛盾した内容であったかもしれませんが、最終的なアクションが投稿だけとは限らないので書き残しておきました。SASを紹介するにあたった経緯としましては、
FaceAPIでエラーが出まくる
↓
BlobストレージのアクセスポリシーがPrivateだった
↓
でも匿名アクセス可能にしたらまずくない？
↓
Azure先輩がPrivateのままアクセスできるようにするオプションを持っていないはずがない
↓
SASってものがあるやん
といった感じであったので、ハマってみるのも悪くないと感じる出来事でした…