Azure Percept DKを使ってみる、そして亀が動いたらslackに通知したかった話

ALGYANというグループはご存知でしょうか。
IoT界隈で楽しい事をいろいろやられてるなんだかエネルギッシュな集団です。
12/18に **【プレゼント有】『Azure Percept』紹介セミナー！** というイベントがあるのですが、それのLT枠に応募させていただきまして、見事に当選。
Azure Perceptというデバイス(なんと合計税込¥59,873相当:exclamation:)を頂戴いたしました次第です。
LT頑張るぞー。

https://algyan.connpass.com/event/228065/

-----
## 前置き

そんなわけでして、この記事は前述のLTに向けて作成している記事です。
少し余計な説明もあるかもしれませんがご容赦ください。

ついでに[AWS Azure Advent Calendar 2021](https://qiita.com/advent-calendar/2021/azure)に参加してみました。ちょうど12/18が空いてたので。

-----
## なにをやろう？

かなり以前のことなのですが、リクガメの水槽をRasPiで撮り、Aws IoTに垂れ流し、ストリーミング配信するってのをやりました。
その時は、画像を解析し、我が家のリクガメの行動をモニタリングしてみたいなという所まで考えてまして、結果はいまいちだったけど、[ObjectDetectionなんかも試してました](https://qiita.com/ikegam1/items/2b13ecf18354bbb6d42c)。


・・が、RasPiのSDカードが死んでしまい・・まさに「ぼうけんのしょ は きえてしまいました」状態です。。
モチベーションがなくなってしまってそのままです。

・・良いきっかけなのでこれをやり直してみますか？

-----
## あなたは誰？

- みなみじま(40代♂)
- バックエンドエンジニア(インフラ寄り)
- AWSエンジニア(12冠) [11冠の時の記事](https://qiita.com/ikegam1/items/eb8d67e032f1064832ad)
- Azureわからない

-----
# 目次
- Azure Percept DKを使ってみる
    - セットアップ
    - チュートリアル
- お試し開発　Azure functionsを作る
    - モデルを作り直す
    - cosmos DBにクエリをかけてslackに通知したい
- 感想


-----
# Azure Percept DKを使ってみる
## Azure Percept DKの組み立て

Wifiアンテナを差したり、ケーブルをつないでみたりした程度で完了です。お手軽。

![IMG20211120181558.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/6a96d2e9-6349-804e-fa13-78fd86a14952.jpeg)

-----
## デバイスのセットアップ

こちらを手順通りに進めてセットアップしていきます。
Azureのアカウントの作成がまだだったのでアカウントの作成から進めていきました。
無料サブスクリプションの登録やら、デバイスのアクセスポイントに繋いでのデバイスやIoT Hubのセットアップやらを行う。

https://docs.microsoft.com/ja-jp/azure/azure-percept/quickstart-percept-dk-set-up

-----
### が、ここでさっそくトラブル。。

Complete the authorization processって所で・・

![スクリーンショット 2021-11-20 19.10.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/c6bf37ed-4a4b-c71a-5f36-f077cc49cf56.png)

![スクリーンショット 2021-11-20 19.10.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/264c0dc4-f8c2-5b80-8aa7-8e944124f2da.png)

**Login to Azure** 押しても先に進まない・・

-----

って思ったんだけど、デバイスのアクセスポイントじゃなく、192.168.21.xxxに家のwifiアクセスポイント経由繋いだら同じセットアップ画面(日本語)が出てきて解決した。
ほんとは別ウインドウが開く予定だったのね。たぶん、http://10.1.1.1の方をブラウザで開いてもいけたと思う。
自動的に開いたモーダル的なやつがあかんかった（mac)

別ウインドウ(タブ)で認証が進み、ここまでやったら完了。

![スクリーンショット 2021-11-20 19.10.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/59206dc0-653d-8eed-aa3a-214d29d26ce8.png)

-----

正規ルートに戻ります

![スクリーンショット 2021-11-20 19.20.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/c0f3b565-358a-abab-82fb-7fb082e44554.png)

-----
そんなわけでセットアップ終了したみたいです :congratulations: 

![スクリーンショット 2021-11-20 19.34.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/ed0860d8-18f1-1aa7-6095-33341702521c.png)

**Azure Percept Audio デバイスを設定する**っていう工程もありますが、ケーブルつなぐだけです。

-----
## Azure Percept Studio でコードなしのビジョン ソリューションを作成する

いよいよ、チュートリアル開始です。

> Azure Percept Studio では、コーディングを必要とせずに、カスタム コンピューター ビジョン ソリューションを構築してデプロイできます。

https://docs.microsoft.com/ja-jp/azure/azure-percept/tutorial-nocode-vision

-----
### ビジョン プロトタイプの作成

Azure Percept Studio上でポチポチやってく感じです。

https://portal.azure.com/#blade/AzureEdgeDevices/Main/overview

![スクリーンショット 2021-11-20 19.48.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/2af91063-1394-2a7e-86c1-c3dcbbe33c4a.png)

-----
#### 1. Visionプロトタイプの作成


なお、チュートリアルでは
> [デバイスの種類] ドロップダウン メニューで [Azure Percept DK] を選択します。

ってのがあったのですが、その項目がなくて選択できませんでした。が、問題ありませんでした。

-----

Azure Percept Studio上とは関係ないのですが、デバイスのIPアドレスにブラウザでアクセスしたらカメラの映像がストリーミングされてました。

http://192.168.21.xxx:3000/

![スクリーンショット 2021-11-20 20.03.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/a8dfcd08-6854-c8cf-87c8-70702167821d.png)

----
##### トレーニング用の画像集め

Studio上のボタンで画像をキャプチャできるので、それで30枚ほど適当な画像を集めます。

![スクリーンショット 2021-11-20 20.11.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/68dca78f-c1a1-0394-b184-9d7cfc06bebb.png)

customvision.aiとかいう所で撮った画像は確認できます。

![スクリーンショット 2021-11-20 20.16.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/dc3e7593-d143-3ac5-ee08-6cf67ad6e8a3.png)

-----
##### タグ付け

最初はこの３つ以外にもタグ付けてたのですが、一つのタグにつき最低15枚は画像が必要っていわれて、タグは3つだけにしました。

![スクリーンショット 2021-11-20 20.19.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/0c5c1c99-6193-1c91-6172-df5dd36b81cb.png)

-----
##### トレーニング
QuickとAdvancedとありましたが、Quickにしときました。

![スクリーンショット 2021-11-20 20.38.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/fe35c867-f543-43cc-1e99-0c2e1a5c0462.png)

-----
**完了 :exclamation:**

![スクリーンショット 2021-11-20 20.51.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/93866488-2476-e360-bc4c-a0544a2b09ff.png)

-----
##### モデルのデプロイ

![スクリーンショット 2021-11-20 20.52.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/fda99e14-567b-aaf8-19be-1532aec4d2c6.png)

![スクリーンショット 2021-11-20 20.54.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/16c61ed8-1f0a-fbbf-730a-bce148ad8977.png)

-----
### モデルのデプロイ完了

無事、トレーニング結果が反映されたようです。
トレーニングとデプロイがブラウザで完結するので思ったよりスムーズにできました。

![スクリーンショット 2021-11-20 20.55.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/6f2c4c2f-d956-8d9a-3ea7-893a9d3fd54c.png)

-----
## Azure Percept DK のモデル推論のテレメトリを表示する

映像を読み取って、解析後の情報がリアルタイムで取得できてる。
これ、なんか活用できないですかねぇ。awsだったらLambdaやkinesisに垂れ流してって感じなんですが、Azureだとどうなるんだろうか？

https://docs.microsoft.com/ja-jp/azure/azure-percept/how-to-view-telemetry

-----

と思ったら素敵な記事が。
これを参考にしつつ、少し頑張ってみよう。
Percept DKデバイスからテレメトリを送り、それがCosmos DBに入り、それをAzure functionsから参照できるって所をイメージ。LTまでにこれができれば100点。（・・70点ぐらいを目指します、、)

https://qiita.com/mstakaha1113/items/1c9b8f61452147a87640
https://docs.microsoft.com/ja-jp/azure/stream-analytics/stream-analytics-get-started-with-azure-stream-analytics-to-process-data-from-iot-devices

-----
# お試し開発
## モデルを作り直す

そんなわけですが、モデルを実物ベースで作り直します。
以下の３つをタグ付けしました

- カメクサ(3歳♂)
- 温湿度計
- カメハウス

![スクリーンショット 2021-11-21 19.27.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/c1559f48-812f-0717-f5d5-6002c4994ef9.png)

-----
温湿度計とカメハウスは固定のほぼ固定の角度からしか撮ってないのでブレませんでしたが、亀の方はいろんな角度から撮ったので再現率（Recall）は83.3%と、精度が落ちました。

![スクリーンショット 2021-11-21 19.39.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/3b92cf70-2b18-8344-d5bc-0e1d1b9b5fcd.png)


-----
### モデルのテスト
deployしてチェックします。
検知率がちょい低いんだけど、、対象が複雑なのでこんなものか？

![kamestoream.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/80f62f08-77d5-264f-5808-3b546b73f852.gif)


-----
## Stream Analytics ジョブを触ってみる。

Azureのコンソールからジョブを作ってみます。
![スクリーンショット 2021-11-23 14.39.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/19daf990-d120-0868-0ab1-78c1cb672a37.png)

-----
### 入力
なんやわからんけど、IoT Hubから入力したいですねん。
直感でいけるか？

![スクリーンショット 2021-11-23 14.41.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/ffd313c7-3d8f-ebb0-a2d0-85f4c9d38856.png)

![スクリーンショット 2021-11-23 14.43.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/5ffcf418-7666-d1cc-c85d-0324da7d6ba1.png)

-----
### 出力
お次は出力です。
Stream Analyticsからcosmos DB に吐き出せるらしい。
cosmos dbのSQL APIってのを使うのか。

https://docs.microsoft.com/ja-jp/azure/stream-analytics/stream-analytics-documentdb-output 

-----
### cosmos db
cosmos dbを作っておいてみます。
serverlessってのにしてみたけど、これだとDynamoDB的な感じですかね？
とりあえずなので、一旦は適当に。

![スクリーンショット 2021-11-23 15.06.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/74d0d120-c5ec-3dde-c10a-7ae6dcdf0afc.png)

-----
作成後、サンプルプログラムをDLして実行してみたら以下の感じになりました。
ローカルからでも書き込めてるみたい。App.configってファイルのエンドポイントとかが書かれてるのか。

```bash
% dotnet run --framework netcoreapp5.0
Beginning operations...

Created Database: ToDoList

Created Container: Items

Cannot read container throuthput.
Reading or replacing offers is not supported for serverless accounts.
ActivityId: daf439ff-1f36-46d4-9bdc-090d0f4d261a, Microsoft.Azure.Documents.Common/2.14.0, Please see CosmosDiagnostics, Darwin/10.16 cosmos-netstandard-sdk/3.11.4
Created item in database with id: Andersen.1 Operation consumed 12.95 RUs.

Created item in database with id: Wakefield.7 Operation consumed 14.86 RUs.

Running query: SELECT * FROM c WHERE c.PartitionKey = 'Andersen'

Updated Family [Wakefield,Wakefield.7].
 	Body is now: {"id":"Wakefield.7","partitionKey":"Wakefield","LastName":"Wakefield","Parents":[{"FamilyName":"Wakefield","FirstName":"Robin"},{"FamilyName":"Miller","FirstName":"Ben"}],"Children":[{"FamilyName":"Merriam","FirstName":"Jesse","Gender":"female","Grade":6,"Pets":[{"GivenName":"Goofy"},{"GivenName":"Shadow"}]},{"FamilyName":"Miller","FirstName":"Lisa","Gender":"female","Grade":1,"Pets":null}],"Address":{"State":"NY","County":"Manhattan","City":"NY"},"IsRegistered":true}

Deleted Family [Wakefield,Wakefield.7]

Deleted Database: ToDoList

End of demo, press any key to exit.
```

-----
適当なデータベースとコンテナ(?)作成してみました。

![スクリーンショット 2021-11-23 16.27.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/7cf74848-1bba-6620-ba64-b33763105665.png)

一旦、Stream Analyticsに戻ってみましょう。

-----
### ここで残念なお知らせ。
cosmosDBがserverlessだとanalyticsからの出力に使えないっぽい？？
クラウド(つまりサーバー有）にすると出力の項目にcosmosが出てきました。
イベントハブとか介して上手くやればできるのかもですが・・素人なので大人しくserverlessを諦めます。。

![スクリーンショット 2021-11-23 16.40.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/0eefac3a-f7d7-3d77-6304-41cb11b1d8ce.png)


![スクリーンショット 2021-11-23 16.38.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/9790c72b-7406-ba89-8825-2a49037ebd27.png)

-----
### テレメトリデータの形

```json
{
  "body": {
    "NEURAL_NETWORK": [
      {
        "bbox": [
          0.335,
          0.435,
          0.468,
          0.542
        ],
        "label": "kamekusa",
        "confidence": "0.628292",
        "timestamp": "1637652280700386238"
      }
    ]
  },
  "enqueuedTime": "2021-11-23T07:24:41.247Z"
}
```

-----
### クエリを作ってみる
（これでいいのか？ [このへんをみました](https://docs.microsoft.com/ja-jp/azure/stream-analytics/stream-analytics-parsing-json))
カメは家に引きこもってるのでデータの取りやすい温湿度計の方をクエリにかけてます。

```sql
SELECT
    object01.arrayvalue.timestamp as id,
    object01.arrayvalue.label as label,
    cast(object01.arrayvalue.confidence as float) as confidence,
    object01.arrayvalue.timestamp as timestamp,
    object01.arrayvalue.bbox as bbox
INTO
    [output-cosmos]
FROM
    [kamekusa-analytics]
CROSS APPLY GetArrayElements(NEURAL_NETWORK) AS object01
WHERE
  object01.arrayvalue.label = 'kamekusa'
AND
  cast(object01.arrayvalue.confidence as float) > 0.4
```

-----
クエリのテスト

![スクリーンショット 2021-12-11 17.36.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/e957a853-5ca6-8bc8-bbc4-1b7972574f3a.png)

-----
### stream ジョブスタート

![スクリーンショット 2021-11-23 17.20.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/360e559f-2b0c-ba26-d30c-57bc8aadbc28.png)

-----
入ってきた！

![スクリーンショット 2021-11-23 17.41.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/43a3cdc6-e9a7-b015-f1c8-83b077223df7.png)

-----
## Azure functionからcosmos DBにクエリをかけてslackに通知したい

これでEdgeからテレメトリーデータが入ってくるようになったのですが、お次は、

- カメが家から出てきてる(=検知してる)
- 移動してる(bboxの位置が変化している)

ってあたりを条件にしつつ、slackに通知するようにしてみたいと思います。

-----
## slackに通知するfunction作成

先に通知だけやっておきたいです。Hello world的なやつ。
↓これだけでいけちゃうのかな？

https://qiita.com/oyngtmhr/items/1d8a443276d9b1eaf4de

-----
### function App

実はいろいろ試行錯誤したのですが、Visual Studio 2022 for Macを使うことにしました。
Azure functionのテンプレートがあったのでお手軽でした。
TimeTriggerってテンプレートにしつつ、とりあえず15分刻みで実行するfunctionを作成してみています。

https://docs.microsoft.com/ja-jp/visualstudio/mac/azure-functions?view=vsmac-2019

-----
前述のQiitaの記事を最大限にインスパイアさせていただいております。
slackの設定等については割愛。incomming webhookを使ってます。

```c#
//#r "Newtonsoft.Json"

using System;
using System.Text;
using System.Net;
using Newtonsoft.Json;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.Logging;

namespace kamekusaStrmPushSlack
{
    public static class TimerTriggerEvery15min
    {
        [FunctionName("TimerTriggerEvery15min")]
        public static void Run([TimerTrigger("0 */15 * * * *")] TimerInfo myTimer, ILogger log)
        {
            log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");

            string slack_text = "はろー! Azure！";
            var wc = new WebClient();

            var WEBHOOK_URL = "https://hooks.slack.com/services/xxxxxxxx"; //incoming hookのURL
            var data = JsonConvert.SerializeObject(new
            {
                text = slack_text,
            });
            log.LogInformation("json=" + data);
            wc.Headers.Add(HttpRequestHeader.ContentType, "application/json;charset=UTF-8");
            wc.Encoding = Encoding.UTF8;
            wc.UploadString(WEBHOOK_URL, data);

        }
    }
}
```

-----
deploy

![スクリーンショット 2021-11-28 22.20.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/b2d09399-60a2-1682-de5d-a7854ff1e6d9.png)

-----
通知できました。
（なお、上記の画像は別件で、ESP32デバイスから定期的に投稿してるやつ）

![スクリーンショット 2021-11-28 22.38.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/c7499019-73ea-6dae-9cc6-87d7ba9d856e.png)

-----
## db連携も含めたプロトタイプfunction開発
### 移動したかどうか
プロタイプということもあり、とりあえずはアバウトに設定します。
温湿度計の位置を取ってるのは基準点てきな意味合いが強いのですが、とりあえず、これのサイズ感と座標の数値から、0.1=1cm移動としておきます。

![bbox.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/acee682a-924d-00ad-7723-e4bf1bc7cec9.png)

-----
検知の条件としては、

- 15分に一度DBから値を取得(selectの条件はnow - 15分)
- 連続した10回の計測点の平均の座標を取る(A)
- 前後のAを比較し、xとyの増減の合計が0.05以上であれば動いたものとして計測。
- cmというのも味気ないので3cmを1歩として計算してみましょう。（実際に一歩はもうちょい大きい気もする）

これらに合致した時だけslackに通知します。

-----
cosmos dbと繋いでみるのは、このあたりを参考にしてみます。

https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-cosmosdb-v2-input?tabs=csharp#http-trigger-get-multiple-docs-using-documentclient

-----
### 最終的なコード

```C#:Analytics.cs
using System;
using Newtonsoft.Json;

namespace kamekusaStreamPushSlack
{
    public class AnalyticsItem
    {
        [JsonProperty("id")]
        public string Id { get; set; }

        [JsonProperty("partitionKey")]
        public string PartitionKey { get; set; }

        [JsonProperty("label")]
        public string Label { get; set; }

        [JsonProperty("timestamp")]
        public ulong Timestamp { get; set; }

        [JsonProperty("_ts")]
        public uint Ts { get; set; }

        [JsonProperty("bbox")]
        public float[] BBox { get; set; }
    }
}
```

-----
```C#:TimerTriggerEvery15min.cs
//#r "Newtonsoft.Json"

using System;
using System.Text;
using System.Net;
using Newtonsoft.Json;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos;
using System.Collections.Generic;

namespace kamekusaStrmPushSlack
{
    public static class TimerTriggerEvery15min
    {
        [FunctionName("TimerTriggerEvery15min")]
        public static async Task RunAsync([TimerTrigger("0 */15 * * * *")] TimerInfo myTimer,
            ILogger log)
        {
            const string WEBHOOK_URL = "https://hooks.slack.com/services/xxxxxxxx";
            const string EndpointUri = "https://xxxxxxxx.documents.azure.com:443/";
            const string PrimaryKey = "xxxxxxxx";
            const string databaseId = "xxxxxxxx";
            const string containerId = "iot";

            var wc = new WebClient();
            string slack_text = "[kame-moving] ts:";

            wc.Headers.Add(HttpRequestHeader.ContentType, "application/json;charset=UTF-8");
            wc.Encoding = Encoding.UTF8;

            Int32 unixTimestamp = (Int32)(DateTime.Now.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
            //cosmosdbのタイムゾーンとズレてたのでとりあえず9時間も追加で引く
            //Int32 before15Min = unixTimestamp - 15 * 60 - (60 * 60 * 9);
            Int32 before15Min = unixTimestamp - 15 * 60; //勘違い？合ってた。
            slack_text += unixTimestamp.ToString();
            slack_text += "\n";

            CosmosClient cosmosClient;
            Microsoft.Azure.Cosmos.Database database;
            Container container;
            cosmosClient = new CosmosClient(EndpointUri, PrimaryKey);
            database = await cosmosClient.CreateDatabaseIfNotExistsAsync(databaseId);
            container = await database.CreateContainerIfNotExistsAsync(containerId, "/_partitionKey");

            var sqlQueryText = "SELECT * FROM c WHERE c.label = 'kamekusa' AND c._ts >= " + before15Min.ToString();
            sqlQueryText += " ORDER BY c._ts desc OFFSET 0 LIMIT 100000";

            log.LogInformation("Running query: {sqlQueryText}\n");

            QueryDefinition queryDefinition = new QueryDefinition(sqlQueryText);
            FeedIterator<kamekusaStreamPushSlack.AnalyticsItem> queryResultSetIterator =
                   container.GetItemQueryIterator<kamekusaStreamPushSlack.AnalyticsItem>(queryDefinition);

            List<kamekusaStreamPushSlack.AnalyticsItem> analyticsItems = new List<kamekusaStreamPushSlack.AnalyticsItem>();
            List<float> avgX = new List<float>();
            List<float> avgY = new List<float>();

            const int SAMPLING_UNIT = 10; //連続したポイントの平均値を取る単位
            while (queryResultSetIterator.HasMoreResults)
            {
                Microsoft.Azure.Cosmos.FeedResponse<kamekusaStreamPushSlack.AnalyticsItem> currentResultSet = await queryResultSetIterator.ReadNextAsync();
                float x = 0, y = 0, w = 0, h = 0;
                int i = 0;
                foreach (kamekusaStreamPushSlack.AnalyticsItem analyticsItem in currentResultSet)
                {
                    analyticsItems.Add(analyticsItem);
                    log.LogInformation("\tRead " + analyticsItem.Ts + "");

                    x += analyticsItem.BBox[0];
                    y += analyticsItem.BBox[1];
                    w += analyticsItem.BBox[2];
                    h += analyticsItem.BBox[3];


                    if (i % SAMPLING_UNIT == 0)
                    {
                        avgX.Add((float)((x + w) / 2) / SAMPLING_UNIT);
                        avgY.Add((float)((y + h) / 2) / SAMPLING_UNIT);
                        x = analyticsItem.BBox[0];
                        y = analyticsItem.BBox[1];
                        w = analyticsItem.BBox[2];
                        h = analyticsItem.BBox[3];
                    }

                    i += 1;
                }
            }

            float prevX = 0;
            float prevY = 0;
            float moved = 0;
            for (int i = 0; i < avgX.Count; i++)
            {
                Console.WriteLine("\tMoved {0}\n", Math.Abs((prevX - avgX[i])) + Math.Abs((prevY - avgY[i])));
                if (prevX > 0 && Math.Abs((prevX - avgX[i])) + Math.Abs((prevY - avgY[i])) > 0.05)
                {
                    moved += Math.Abs((prevX - avgX[i])) + Math.Abs((prevY - avgY[i]));
                    log.LogInformation("\tMoved " + moved);
                    log.LogInformation("\tMoved " + prevX);
                    log.LogInformation("\tMoved " + avgX[i]);
                    log.LogInformation("\tMoved " + prevY);
                    log.LogInformation("\tMoved " + avgY[i]);
                    //slack_text = $"{slack_text}moved: {string.Format("{0:F1}", (moved / 0.3))}";
                }
                prevX = avgX[i];
                prevY = avgY[i];
            }

            if (moved >= 0.3)
            {
                slack_text = $"{slack_text}:turtle:が{string.Format("{0:F1}", (moved / 0.3))}歩、歩いたよ！";
                var data = JsonConvert.SerializeObject(new
                {
                    text = slack_text,
                });
                wc.UploadString(WEBHOOK_URL, data);
            }
        }

        private static object GetDistance(float x, float y, float w, float h)
        {
            return (float)Math.Sqrt((Math.Pow(x - w, 2) + Math.Pow(y - h, 2)));
        }
    }
}
```

-----
### でけたよ！！！

![スクリーンショット 2021-12-11 17.31.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/c39d2d97-5134-fd5d-ea22-8c817a0aa04b.png)

コードはこちらに上げてみています。
https://github.com/ikegam1/kamekusaStrmPushSlack

お初なことが多く、ツッコミどころは多そうですがご容赦を。


-----
## やってみて

- 画像検出がトレーニングも含めてブラウザで完結して簡単
- Azureの設定も簡単ですぐ使える
- ちょっとゴツいので、もう少しお手軽なレベルのやつがあったら用途が広がりそう
- 音声も試してみたかったが、カメはしゃべらないからなぁ。。犬とかだと面白い使い方あるのかも？
- けっこうお金かかるかも？
- Iot Hubへのデータ送信量(フレームレート?)減らしたいけどどこで設定すれば？
- イメージトレーニングに使える、設定済みのDocker imageとかないのかなぁ
- 1日の運動量とか出して日次グラフにしてみたい。
- 画像を一緒にpostしたかったりもするけど、それは別で組み込むべきなんだろうな。