M5Stack COM.X Sigfoxを用いた「人流エッジAI計測」

M5Stack COM.X Sgifoxモジュールを用いた**エッジAIの組み込み実装**と，**エッジAIによる画像処理結果をSigfoxでクラウドへ送信する仕組み**のユースケース一例として，「人流エッジAI計測」を作ってみました。
<details><summary>【補足】ここで言うエッジAI</summary><div>
ここで言うエッジAIとは，一般にAIモデルがコンピュータリソースの豊富なクラウドサーバーにおいて実行されることが多いのに対して，現場で取得したデータのインプットをその場でタイムリーにAIモデルに与えて推論させ，取得したい特定の情報を得ることを意味しています。
</div></details>

#人流エッジAI計測
画像から人を検出する，ということを断続的に超短周期で実行し続けることで，カメラの前を通り過ぎる人の数をカウントします。
###ソリューションのポイントとメリット
デバイスを設置した現場で，エッジAIを用いて画像から人物の数の情報の取り出しを実行することにより…

- データ量を削減し、必要なデータのみSigfoxで送信可能になる
- プライバシーデータを送信することなく、集めたい必要なデータ収集が可能
- アップロード先のストレージサーバ容量の削減が可能
- AI推論専用の高価な解析サーバが不要に

これらのメリットが得られます。

#前提
Sigfoxについての前提理解は、下記サイトを確認してください。

- [Sigfoxクイックスタート](https://www.kccs-iot.jp/service/quickstart/)
- [Sigfox Custom Callbacks](https://www.kccs-iot.jp/20180601-technical/)
- [Sigfox IoT Agency Platform](https://www.kccs-iot.jp/20190121-technical/)　※IoT Agency Platformはサービス終了となっているため、その他プラットフォームをお使いいただくことをお奨めします
- [M5Stack COM.Xの使い方](https://www.kccs-iot.jp/20200918-technical/)

#使用デバイス

|デバイス|写真|
|:----|:---:|
|[M5Stack Gray](https://www.switch-science.com/catalog/3648/)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/f3041372-c2cb-91e7-bb5a-b09e5927ac10.png)|
|[UnitV AI Camera](https://www.switch-science.com/catalog/6212/)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/c93ab905-6d64-ad2e-4299-cda937388e67.png)|
|[M5Stack用Sigfoxモジュール](https://www.switch-science.com/catalog/6624/)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/00776a8f-bd7a-3690-04ca-98292ecefc6b.png)|

#処理概要
全体処理の流れは下記のようになります。
![system.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/98e0d489-71b5-cf9c-6f5b-a5cedce4c98b.png)

1. UnitVのカメラが映像(画像)を取得する
2. AIモデルが人を検出し移動人数をカウント，Grayに送る
3. GrayがSig moduleを通じてカウント数をSig Cloudへ送信．同時に簡易的な画面表示
4. Sig CloudからIot Agency Platformへ集計結果を送信，ダッシュボードを表示

#開発
AIモデル開発、エッジデバイス開発、デプロイの流れを記載します。
##AIモデル開発
YOLOv2をアーキテクチャとした検出モデルで，人物検出の学習を行います。
学習自体に特別なことはありませんが，最終的にエッジデバイス上で動かす`kmodel`形式にする必要がある為、

- 軽量である
- 変換可能なモデル(レイヤー)である

という制限が加わります。
これらの制限をクリアしてくれる学習環境として[aXeleRate](https://github.com/AIWintermuteAI/aXeleRate)を利用しました。

###AIモデル開発環境
Ubuntu16.04.5 LTS
###データセット
[COCOデータセット2017](https://cocodataset.org/#download)から以下の絞り込みを行っています．

1. `person`タグを持つ
2. 画像の中に写っている人が2人まで

2人までにしている理由は，人数が多い画像に含まれる人物は非常に小さく映っているものが多く，今回の検出対象(カメラの前を通り過ぎる人)として不適当と判断したことによります。
最終的に絞られたトレーニングデータ35562枚，テストデータ1481枚で学習と評価を行っています。

###学習結果
テストデータに対するmAPの学習推移です。
mAPは物体検出タスクにおいて使われる評価指標で大きい程良い事を示します。
![mAP.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/fc7b32e8-b418-d333-758e-84f419a4f85a.png)

また，`kmodel`への変換はaXeleRateが学習終了時に行ってくれています。
モデル自体はページ下部の付録にリンクを置いているので，自由に使って頂いて構いません。
<details><summary>変換処理を自分で行う場合の参考リンク</summary><div>
モデルの変換は，基本的にkeras(h5)→tflite→kmodelというステップで行われます。

- [変換モジュール(NNcase)](https://github.com/kendryte/nncase): aXeleRateも内部ではこれを使用
- [tflite→kmodel変換可能レイヤ](https://github.com/kendryte/nncase/blob/master/docs/tflite_ops.md)
- [kerasのPaddingについて](https://web.archive.org/web/20191130055915/https://bbs.sipeed.com/t/topic/682): kerasのデフォルトパディング形式はkmodel変換できない
- [日本語記事](https://www.shtsno24.tokyo/2020/03/tfkerasnncase-v020-beta-2kmodel_16.html)
</div></details>

##エッジデバイス開発
###UnitV
基本的に公式の[クイックスタート](https://docs.m5stack.com/#/en/quick_start/unitv/unitv_quick_start_maixpy)に則って準備します。

ファームウェア（※後に自作モデルを動かす為に別のパッケージを焼くことになるので，動作確認目的以外では不要です）は[EasyLoaderをダウンロード](https://m5stack.oss-cn-shenzhen.aliyuncs.com/EasyLoader/Windows/CORE/EasyLoader_M5StickV_v5.1.2.exe)し焼きます。

IDEは[MaixyPy IDE](http://dl.sipeed.com/MAIX/MaixPy/ide/)を使用しました。
コード全体はページ最下部の付録に載せています。
全体の大まかな流れは以下の通りです。

1. UART, KPU, sensor系の初期化
2. 画像の撮影
3. モデルへの画像入力と検出情報の出力
4. トラッカーへ検出情報の入力，前フレームとの差異情報の出力
5. カウンタへ差異情報の入力，条件に応じてカウント情報を更新
6. Grayへカウント情報と現フレームの検出数を送信，2.へ戻る
7. `kmodel`のロード

ベースは[aXeleRateのコード](https://github.com/AIWintermuteAI/aXeleRate/blob/master/example_scripts/k210/detector/racoon_detector.py)です．
####KPU初期化

```init_kpu.py
def init_kpu(threshold=0.3):
  classes = ["person"]
  task = kpu.load(0x300000) #change to "/sd/name_of_the_model_file.kmodel" if loading from SD card
  a = kpu.set_outputs(task, 0, 7,7, 30)   #the actual shape needs to match the last layer shape of your model(before Reshape)
  anchor = (0.57273, 0.677385, 1.87446, 2.06253, 3.33843, 5.47434, 7.88282, 3.52778, 9.77052, 9.16828)
  a = kpu.init_yolo2(task, threshold, 0.3, 5, anchor) #tweak the second parameter if you're getting too many false positives
  return task
```
<details><summary>`kpu.load()`</summary><div>
モデルの書き込まれてるアドレスを指定します(書き込みについては後述)。コメントにある通りSDカードを利用する場合はその保存パスに
</div></details>
<details><summary>`anchor`</summary><div>
モデル生成時に使用したアンカーボックスです。縦・横の繰り返しでボックスが記述されていて、ここには5つのアンカーボックスが書かれています。
</div></details>
<details><summary>`kpu.init_yolo2()`</summary><div>
引数は、kpuオブジェクト・検出の閾値(probability threshold)・絞り込みの閾値(nms threshold)・アンカーボックス数・アンカーボックスです。
第2引数が、コメントにあるように物体を検出したとみなすかどうかの閾値(確率値)になります。例えば0.3であれば，検出した区域の「物体らしさ」が0.3より小さければそれは破棄されます。
第3引数が、Non-Maximum Suprresion(nms)で使用するIoUの閾値(確率値)です。nmsを簡単に言うと、「同じクラスとして検出された候補群があった時、それらが示すものは一つの物体なのか複数の物体なのかをいい感じにまとめてor削ってくれるアルゴリズム」です。この閾が大きい程、一つの物体とみなされやすくなります．詳細はこちらの記事が非常に分かりやすく説明してくれています。
</div></details>
[参考ドキュメント](https://maixpy.sipeed.com/en/libs/Maix/kpu.html#kpu)

####UART制御

```init_uart.py
def init_uart():
    fm.register(35, fm.fpioa.UART1_TX, force=True)
    fm.register(34, fm.fpioa.UART1_RX, force=True)
    uart = UART(UART.UART1, 115200,8,0,0, timeout=1000, read_buf_len=4096)
    return uart
```
[参考ドキュメント](https://cn.maixpy.sipeed.com/en/libs/machine/uart.html?h=uart)

<details><summary>`fm.register()`</summary><div>
UnitVのuartで使用するピンは35と34番。
</div></details>
<details><summary>`UART()`</summary><div>
UARTオブジェクトの生成です．
</div></details>
<details><summary>トラッキング及びカウンタ</summary><div>
こちらは長いので，以下に図示しております。
![algorithm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/b427b428-7713-fb0c-29c7-c98e32d915d3.png)
</div></details>

###M5Stack Gray(+COM.X)
M5 Stack Grayの開発環境としてArudinoがメジャーですが、micropythonを使用する為UIflowを使用します。
こちらも[クイックスタート](https://docs.m5stack.com/#/en/quick_start/m5core/m5stack_core_get_started_MicroPython)に則って準備します。

同じmicropythonベースですが、UnitVとモジュールが色々異なる点に注意が必要です(UnitV側はMaixpy: micropython to k210)
[M5 Stack micropython](https://github.com/m5stack/M5Stack_MicroPython)
全体の大まかな流れは以下の通りです．

1. UART、アイコン生成部の初期化
2. UnitVからの信号を待つ
3. 信号を受信したら，カウント数と検出数を画面に描画し，Sigfox通信
4. 2に戻る

```uart_m5stack.py
## UnitV
VTX, VRX = (22, 21)
## Sig
STX, SRX = (17, 16)

def uart_init():
  # Sigfox
  uart1 = UART(1, tx=STX, rx=SRX)
  uart1.init(9600, bits=8, parity=None, stop=1)
  
  # UnitV
  uart2 = UART(2, tx=VTX, rx=VRX)
  uart2.init(115200, bits=8, parity=None, stop=1)
  return uart1, uart2
```

##デプロイ
####UnitV
AI開発部分で作ったモデルを動かす為に，`kfpkg`パッケージを作成し焼いていきます。
参考：https://maixpy.sipeed.com/en/get_started/upgrade_firmware.html

- ファームウェア`maixpy_v0.5.0_42_g458ed4e_m5stickv.bin`をダウンロード
https://dl.sipeed.com/MAIX/MaixPy/release/master/maixpy_v0.5.0_42_g458ed4e

- ベースとなるパッケージをダウンロード
https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/M5StickV_Firmware_v5.1.2.kfpkg

- `M5StickV_Firmware_v5.1.2.kfpkg`を`M5StickV_Firmware_v5.1.2.zip`とリネームし展開

- `maixypy.bin`を`maixpy_v0.5.0_42_g458ed4e_m5stickv.bin`に，`facedetect.kmodel`を自作モデル(ここでは`my_model.kmodel`)に置き換え

- 中にある`flash-list.json`を以下のように書き換え

```flash_list.json
{
"version": "0.1.0",
"files": [
 {
   "address": 0x00000000,
   "bin": "maixpy_v0.5.0_42_g458ed4e_m5stickv.bin",
   "sha256Prefix": true
 },
 {
   "address": 0x00D00000,
   "bin": "m5stickv_resources.img",
   "sha256Prefix": false
 },
 {
   "address": 0x00300000,
   "bin": "my_model.kmodel",
   "sha256Prefix": false
 }
]
}
```

- 4つのファイルをzipにまとめ，拡張子をkfpkgへと戻す

- パッケージの書き込み
参考：https://github.com/kendryte/kflash.py
kflashをインストールしUnitVに書き込みます．筆者はAnaconda環境にインストールしました。

```kflash.py
conda create -n kflash python=3.6
conda activate kflash
pip install kflash
kflash my_kfpkg.kfpkg
```

- コードの書き込み
IDE上のコードをブートコードとして書き込みます．

この時点で，電源を接続すると動くようになっています．(Grayに接続しないと動きは見えませんが．．)

####M5Stack Gray
UnitV同様，IDEからコードを書きこみます。
※表示がダウンロード(誤訳？)になっていることに注意

この時点で，こちらも電源を接続すると動くようになっています。

#動作確認
####UnitV
検出の様子をIDEのデバッガで表示したものです。
「2人の人物が左からやってきて，立ち止まった後，右に抜けていく」というシナリオです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/da90ba4a-fbcd-f473-7736-1b534ef88a4a.png)
矩形：人物検出領域
赤の数字：トラッカーによって振られたID
緑の数字：カウント
####M5Stack Gray(+COM.X)
動作画面です。
![grayDemo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/cff73c3f-30aa-1e9f-dedd-f7a0daedce4a.png)
画面上部の緑の数字が人数を、それに合わせ人型のアイコンを表示しています。定期的に計測した人数をSigfox通信で送信しています。
####ダッシュボード
人流カウントをダッシュボードで確認する事ができます
![sigBoard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/e4a2f5c8-2aa1-c369-bbfa-1e9266191452.png)
ある展示会での人流を計測したもので、凡そ、どの時間帯で来場者が多いかが見て取れるかと思います。

#付録
それぞれのデバイスで使用したコードの全体とモデルを置いておきます。
https://github.com/kccsairc/Edge_AIoT