ALGYANというグループはご存知でしょうか。
IoT界隈で楽しい事をいろいろやられてるなんだかエネルギッシュな集団です。
12/18に 【プレゼント有】『Azure Percept』紹介セミナー! というイベントがあるのですが、それのLT枠に応募させていただきまして、見事に当選。
Azure Perceptというデバイス(なんと合計税込¥59,873相当)を頂戴いたしました次第です。
LT頑張るぞー。
前置き
そんなわけでして、この記事は前述のLTに向けて作成している記事です。
少し余計な説明もあるかもしれませんがご容赦ください。
ついでにAWS Azure Advent Calendar 2021に参加してみました。ちょうど12/18が空いてたので。
なにをやろう?
かなり以前のことなのですが、リクガメの水槽をRasPiで撮り、Aws IoTに垂れ流し、ストリーミング配信するってのをやりました。
その時は、画像を解析し、我が家のリクガメの行動をモニタリングしてみたいなという所まで考えてまして、結果はいまいちだったけど、ObjectDetectionなんかも試してました。
・・が、RasPiのSDカードが死んでしまい・・まさに「ぼうけんのしょ は きえてしまいました」状態です。。
モチベーションがなくなってしまってそのままです。
・・良いきっかけなのでこれをやり直してみますか?
あなたは誰?
- みなみじま(40代♂)
- バックエンドエンジニア(インフラ寄り)
- AWSエンジニア(12冠) 11冠の時の記事
- Azureわからない
目次
- Azure Percept DKを使ってみる
- セットアップ
- チュートリアル
- お試し開発 Azure functionsを作る
- モデルを作り直す
- cosmos DBにクエリをかけてslackに通知したい
- 感想
Azure Percept DKを使ってみる
Azure Percept DKの組み立て
Wifiアンテナを差したり、ケーブルをつないでみたりした程度で完了です。お手軽。
デバイスのセットアップ
こちらを手順通りに進めてセットアップしていきます。
Azureのアカウントの作成がまだだったのでアカウントの作成から進めていきました。
無料サブスクリプションの登録やら、デバイスのアクセスポイントに繋いでのデバイスやIoT Hubのセットアップやらを行う。
が、ここでさっそくトラブル。。
Complete the authorization processって所で・・
Login to Azure 押しても先に進まない・・
って思ったんだけど、デバイスのアクセスポイントじゃなく、192.168.21.xxxに家のwifiアクセスポイント経由繋いだら同じセットアップ画面(日本語)が出てきて解決した。
ほんとは別ウインドウが開く予定だったのね。たぶん、http://10.1.1.1の方をブラウザで開いてもいけたと思う。
自動的に開いたモーダル的なやつがあかんかった(mac)
別ウインドウ(タブ)で認証が進み、ここまでやったら完了。
正規ルートに戻ります
そんなわけでセットアップ終了したみたいです
Azure Percept Audio デバイスを設定するっていう工程もありますが、ケーブルつなぐだけです。
Azure Percept Studio でコードなしのビジョン ソリューションを作成する
いよいよ、チュートリアル開始です。
Azure Percept Studio では、コーディングを必要とせずに、カスタム コンピューター ビジョン ソリューションを構築してデプロイできます。
ビジョン プロトタイプの作成
Azure Percept Studio上でポチポチやってく感じです。
1. Visionプロトタイプの作成
なお、チュートリアルでは
[デバイスの種類] ドロップダウン メニューで [Azure Percept DK] を選択します。
ってのがあったのですが、その項目がなくて選択できませんでした。が、問題ありませんでした。
Azure Percept Studio上とは関係ないのですが、デバイスのIPアドレスにブラウザでアクセスしたらカメラの映像がストリーミングされてました。
トレーニング用の画像集め
Studio上のボタンで画像をキャプチャできるので、それで30枚ほど適当な画像を集めます。
customvision.aiとかいう所で撮った画像は確認できます。
タグ付け
最初はこの3つ以外にもタグ付けてたのですが、一つのタグにつき最低15枚は画像が必要っていわれて、タグは3つだけにしました。
トレーニング
QuickとAdvancedとありましたが、Quickにしときました。
完了
モデルのデプロイ
モデルのデプロイ完了
無事、トレーニング結果が反映されたようです。
トレーニングとデプロイがブラウザで完結するので思ったよりスムーズにできました。
Azure Percept DK のモデル推論のテレメトリを表示する
映像を読み取って、解析後の情報がリアルタイムで取得できてる。
これ、なんか活用できないですかねぇ。awsだったらLambdaやkinesisに垂れ流してって感じなんですが、Azureだとどうなるんだろうか?
と思ったら素敵な記事が。
これを参考にしつつ、少し頑張ってみよう。
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つをタグ付けしました
- カメクサ(3歳♂)
- 温湿度計
- カメハウス
温湿度計とカメハウスは固定のほぼ固定の角度からしか撮ってないのでブレませんでしたが、亀の方はいろんな角度から撮ったので再現率(Recall)は83.3%と、精度が落ちました。
モデルのテスト
deployしてチェックします。
検知率がちょい低いんだけど、、対象が複雑なのでこんなものか?
Stream Analytics ジョブを触ってみる。
入力
なんやわからんけど、IoT Hubから入力したいですねん。
直感でいけるか?
出力
お次は出力です。
Stream Analyticsからcosmos DB に吐き出せるらしい。
cosmos dbのSQL APIってのを使うのか。
cosmos db
cosmos dbを作っておいてみます。
serverlessってのにしてみたけど、これだとDynamoDB的な感じですかね?
とりあえずなので、一旦は適当に。
作成後、サンプルプログラムをDLして実行してみたら以下の感じになりました。
ローカルからでも書き込めてるみたい。App.configってファイルのエンドポイントとかが書かれてるのか。
% 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.
適当なデータベースとコンテナ(?)作成してみました。
一旦、Stream Analyticsに戻ってみましょう。
ここで残念なお知らせ。
cosmosDBがserverlessだとanalyticsからの出力に使えないっぽい??
クラウド(つまりサーバー有)にすると出力の項目にcosmosが出てきました。
イベントハブとか介して上手くやればできるのかもですが・・素人なので大人しくserverlessを諦めます。。
テレメトリデータの形
{
"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"
}
クエリを作ってみる
(これでいいのか? このへんをみました)
カメは家に引きこもってるのでデータの取りやすい温湿度計の方をクエリにかけてます。
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
クエリのテスト
stream ジョブスタート
入ってきた!
Azure functionからcosmos DBにクエリをかけてslackに通知したい
これでEdgeからテレメトリーデータが入ってくるようになったのですが、お次は、
- カメが家から出てきてる(=検知してる)
- 移動してる(bboxの位置が変化している)
ってあたりを条件にしつつ、slackに通知するようにしてみたいと思います。
slackに通知するfunction作成
先に通知だけやっておきたいです。Hello world的なやつ。
↓これだけでいけちゃうのかな?
function App
実はいろいろ試行錯誤したのですが、Visual Studio 2022 for Macを使うことにしました。
Azure functionのテンプレートがあったのでお手軽でした。
TimeTriggerってテンプレートにしつつ、とりあえず15分刻みで実行するfunctionを作成してみています。
前述のQiitaの記事を最大限にインスパイアさせていただいております。
slackの設定等については割愛。incomming webhookを使ってます。
//#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
通知できました。
(なお、上記の画像は別件で、ESP32デバイスから定期的に投稿してるやつ)
db連携も含めたプロトタイプfunction開発
移動したかどうか
プロタイプということもあり、とりあえずはアバウトに設定します。
温湿度計の位置を取ってるのは基準点てきな意味合いが強いのですが、とりあえず、これのサイズ感と座標の数値から、0.1=1cm移動としておきます。
検知の条件としては、
- 15分に一度DBから値を取得(selectの条件はnow - 15分)
- 連続した10回の計測点の平均の座標を取る(A)
- 前後のAを比較し、xとyの増減の合計が0.05以上であれば動いたものとして計測。
- cmというのも味気ないので3cmを1歩として計算してみましょう。(実際に一歩はもうちょい大きい気もする)
これらに合致した時だけslackに通知します。
cosmos dbと繋いでみるのは、このあたりを参考にしてみます。
最終的なコード
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; }
}
}
//#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)));
}
}
}
でけたよ!!!
コードはこちらに上げてみています。
https://github.com/ikegam1/kamekusaStrmPushSlack
お初なことが多く、ツッコミどころは多そうですがご容赦を。
やってみて
- 画像検出がトレーニングも含めてブラウザで完結して簡単
- Azureの設定も簡単ですぐ使える
- ちょっとゴツいので、もう少しお手軽なレベルのやつがあったら用途が広がりそう
- 音声も試してみたかったが、カメはしゃべらないからなぁ。。犬とかだと面白い使い方あるのかも?
- けっこうお金かかるかも?
- Iot Hubへのデータ送信量(フレームレート?)減らしたいけどどこで設定すれば?
- イメージトレーニングに使える、設定済みのDocker imageとかないのかなぁ
- 1日の運動量とか出して日次グラフにしてみたい。
- 画像を一緒にpostしたかったりもするけど、それは別で組み込むべきなんだろうな。