ラズパイでGreengrass v1、Lambda、DLRを使ってエッジで画像分類やってみた 

こんにちは、皆川です。AWS IoTからMQTTでメッセージを送って、エッジデバイスであるラズパイでカメラの画像から画像分類をして結果を返すような仕組みを作ります。

家にはこんな感じでラズパイが設置してあって、会社からIoT CoreにMQTT送信すると推論結果をMQTTでIoT Coreに返してくれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001699/3ecfc285-0cba-766b-bd9c-6a7281434d1f.png)
こんな感じ。AI/IoTの基本という感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001699/76dcc8fe-f93c-b70b-00e0-468b22657421.png)

※ちなみにGreengrassのバージョンは1、LambdaはGreengrassコンテナなしで動かします。

# はじめに
本記事は基本的にはAWSの公式ドキュメント（https://docs.aws.amazon.com/ja_jp/greengrass/v1/developerguide/ml-dlc-console.html ）をなぞる内容になっていますが、2021年5月31日現在ドキュメントに不備が多くその通りにやると複数の箇所で詰まってしまうので改良してあります。

早速始めていきましょう。

# 必要なもの
### ラズパイモデル3B
### ラズパイカメラモジュール
https://www.amazon.com/Raspberry-Pi-Camera-Module-Megapixel/dp/B01ER2SKFS
(USBカメラでできるかは未確認です)

# 必要だけど本記事で説明していないもの
### ラズパイの初期セットアップ
OSはRaspberryPi OS（Raspbianから名前が変わった）を使用しました。
https://www.raspberrypi.org/software/
インターネットへの接続、SSH接続及びVNC接続の許可が必要です。

### Greengrassグループ設定
[こちら]()を参照してください。

### ラズパイへのMacからのSSH接続とVNC接続
筆者はどちらもremote.itで行った。SSHもVNCも簡単なGUIで接続できるようになるので便利（弊社で扱っている商品なので使い始めたが、使用感は悪くない）。
https://qiita.com/masa-e/items/321d2962886931593ad4


# 手順
## OSのアップデート
20分ほどで完了します。

```
sudo apt-get update
sudo apt-get dist-upgrade
```

## 必要なPythonライブラリーのインストール

```
sudo apt-get install -y python3-dev \
python3-setuptools python3-pip python3-picamera
```
## pipのインストール

```
# pipのインストール
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python get-pip.py

# パスを通す。これでpipコマンドが使えるようになる。
export PATH=$PATH:/home/pi/.local/bin

# お掃除
sudo rm get-pip.py
```

## DLRのインストール
今回はラズパイモデル3Bなので以下のコマンドでDLRをインストールします。

```
pip3 install \
https://neo-ai-dlr-release.s3-us-west-2.amazonaws.com/v1.6.0/rasp3b/dlr-1.6.0-py3-none-any.whl
```

```
python3
>>>import dlr
```

でNo Moduleエラーが出ないことを確認できたらオッケーです(urllib3とchardetのバージョンがサポートされていませんというWarningは無視します)。

### ※他のハードウェアにDLRをインストールする場合
x86 CPUへは普通にpip install dlr でいけるようです。
Jetsonやラズパイはハードウェア別のWheelファイルのURLをpipで引いてくればいけます。詳しくは
https://github.com/neo-ai/neo-ai-dlr/releases を参照してください。ただ、v1.7以降はdlr.soというダイナミックリンクファイルがついてこないためv1.6をインストールしてください。

## PATH問題を解決する(重要)
pipでdlrをインストールすると~/.local・・・にインストールされますが、Lambdaはそこを探してくれない（PATHが通っていない）のでLambdaも探してくれる場所にコピーします

```
cd ~/.local/lib/python3.7/site-packages
sudo cp -r dlr* /usr/lib/python3.7
```

```
# libdlr.soファイルもLambdaが探さないところ（~/.local/dlr/）にあるのでPATHがLambdaが探すところ（/usr/lib/python3.7/dlr/）にコピーする

sudo cp ~/.local/dlr/libdlr.so /usr/lib/python3.7/dlr/
```

## 今回使うMLモデルをダウンロード

```
sudo wget https://d1onfpft10uf5o.cloudfront.net/greengrass-ml-samples/dlr/dlr-py3-armv7l.tar.gz

# 解凍する
sudo tar -xvzf dlr-py3-armv7l.tar.gz
```
参考：
https://docs.aws.amazon.com/ja_jp/greengrass/v1/developerguide/what-is-gg.html#gg-ml-samples

```
# 後々Lambdaで参照するML用のディレクトリを作る
sudo mkdir /ml_model

# コンパイルされたMLモデル（model.json, model.params, model.so）とアノテーションファイル（synset.txt）を作成したディレクトリのルート（直接配下）へコピー（`mv`でも大丈夫です）
cd dlr-py3-armv7l/models/resnet50
sudo cp * /ml_model/

# いらないファイルを消す
cd
sudo rm -rf dlr*

```

## Lambdaの作成

Macでもターミナルを開いて必要なLambdaを持ってきます。

```
sudo wget https://d1onfpft10uf5o.cloudfront.net/greengrass-ml-samples/dlr/dlr-py3-armv7l.tar.gz

tar -xvzf dlr-py3-armv7l.tar.gz

```

`dlr-py3-armv7l/`にある`example`フォルダをフォルダごとzipファイルにします。その後AWSコンソールのLambdaに行って、登録していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001699/06add57f-e73a-3207-b5cc-99c68028a7a0.png)
￼.zipファイルを選んで今圧縮したzipファイルを登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001699/2282a95a-4ee5-8a56-6ce4-065243c4f8a8.png)
￼
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001699/d59941bf-158f-013e-280f-093fe6e817bd.png)
その後ハンドラーの設定やエイリアスの設定をします。詳しくは[こちら](https://qiita.com/hdmn54321/items/e10990014620d674d3ec#2-lambda%E3%81%AE%E4%BD%9C%E6%88%90)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001699/e33b85ad-00e9-1be2-ccb7-d97698b5fad5.png)

## Lambdaのコード上の注意点

Lambdaのinference.pyの中で`model_resource_path`でコンパイルしたモデルとそのラベルファイル（model.json, model.params, model.so, synset.txt）が存在するフォルダへのファイルパスを指定してやる必要があります。ハードコードしても良いですが、GGGのリソース追加でS3からモデルを引っ張ってくるときは以下のコードを参考にしてください（ちなみにモデルはIoT CoreコンソールからGreengrassGroupのMLリソース追加から`/trained_models`にモデルを追加してLambdaと紐づけてあると仮定します）。

※GreengrassGroupの設定時にS3からモデルを追加するとGGデバイス上の/greengrass/ggc/core/deployment/…/mlmodel/もしくは/greengrass/ggc/core/deployment/…/mlmodel_public/配下にモデルがインストールされます。

```inference.pyから一部抜粋
#　LambdaをGGコンテナ内で動かす設定のとき
model_resource_path = os.environ.get('MODEL_PATH', '/trained_models')

#　LambdaをGGコンテナなしで動かす設定のとき
model_resource_path = os.getenv("AWS_GG_RESOURCE_PREFIX") + "/trained_models"
```

※LambdaをGGコンテナ内で動かすときは同じくリソースの追加から`/dev/vcsm`と`/dev/vchiq`を追加してLambdaを紐づけてやる必要があります。また、LambdaをGGコンテナなしで動かすときは上記のデバイスリソースの追加をするとデプロイに失敗するので注意が必要です。


## Greengrass Groupのデプロイ
以下のように、Groupから自分のGroupに行ってLambdaのタブから先ほどつくったLambdaを登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001699/46fa5f20-3b70-02f8-fa77-6f045e125de0.png)
Lambdaの設定は以下です。UID,GIDを`1000`でデフォルトのpiユーザーで実行します(ggc_userでもいいと思います)。コンテナは`なし`を選択。タイムアウトは念の為`10秒`に引き上げました。Lambdaのライフサイクルはオンデマンド（MQTTを受け取った時だけ起動する）を選びます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001699/04af70f5-44cd-097f-b425-d666a189c4bd.png)
サブスクリプションの設定は以下のようにします。Lambdaからはどんなトピックでも来るようにワイルドカード(#)を指定してIoTCoreからは`test`というトピックでパブリッシュされたときのみメッセージが届く（すなわちLambdaを起動する）ようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001699/ef79c287-03a2-3208-2dfb-7d141cae5a6f.png)

## Greengrass Groupのデプロイ
ラズパイで

```
sudo /greengrass/ggc/core/greengrassd start
```

でGreengrassをスタートして、
画面右上のアクションからデプロイを選択します。30秒ほどで完了するはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001699/93858714-810f-b9d4-4d5d-2d8200930cd0.png)

## いよいよテスト！
ラズパイでエッジ推論の旅もこれが終わればひと段落です。やっていきましょう。
MQTTテストクライアント（IoTCore内にあります）から`#`にサブスクライブして、`test`になんでも良いのでメッセージをパブリッシュしてみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001699/1e37cf47-5419-6242-7ead-8cb302f1d8ce.png)
このようにうまくいったでしょうか。自分の例だとマウスを映してみました。Lambdaのcamera.pyにあるようにラズパイ側でカメラ起動後に2秒スリープが入っているのでそれがボトルネックになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001699/c0a4c172-e5a9-ac3e-8db3-d6d29df79e1c.png)

今回はラズパイでGreengrass,Lambda,DLRを使ってエッジ推論を行いました。このパターンをマスターすればクラウドとの連携がスムーズなり、IoTプロジェクトにも幅が広がることでしょう。以上です。

# トラブルシューティング
Greengrass Groupのデプロイで失敗する方は[Greengrass Group9つのハマりどころ](https://qiita.com/hdmn54321/items/e10990014620d674d3ec#lambda%E4%BD%9C%E6%88%90%E6%99%82)を参照ください。Greengrassのデプロイlogは解読が難解なのでここでつまづいて時間を取られたくないものです。

Lambdaのlogファイルは`/greengrass/ggc/var/log/user/<デフォルトリージョン名>/<ユーザーアカウントに対応した13桁の数字>/<Lambdaの関数名>`となっているので、まず`sudo su`でroot権限にして`cat`などで確認できます。

## 権限まわり
筆者自身まだ原因が分かりきっていないですが、権限まわりのエラーがときどき出ます。よくあるエラーはユーザー（主にデフォルトのユーザー）が特定のフォルダに対してwrite権限がないか、`vchiq`インスタンスに対する権限がないことです。それぞれ`chmod +w <対象となるフォルダ>`や`chmod 777 vchiq`で対応しました。

# 資料集

### Lambdaをroot権限で実行するためのラズパイ側の設定
おそらく使うタイミングはないと思いますが、メモとして残しておきます。
/greengrass/config/config.txtを以下のように変更するとLambdaがrootで実行できます。

```
    "runtime": {
        "cgroup": {
            "useSystemd": "yes"
        }
    },
```

となっているのを以下のように`"allowFunctionsToRunAsRoot" : "yes",`を付け足す。

```
    "runtime": {"allowFunctionsToRunAsRoot" : "yes",
        "cgroup": {
            "useSystemd": "yes"
        }
    },

```


### ラズパイカメラ少し発展編

英語ですが、ラズパイカメラの動かし方は以下にまとめてあります。
https://projects.raspberrypi.org/en/projects/getting-started-with-picamera/3

nanoかviエディタで以下のようなファイルを作ってください。

```camera.sh
#!/bin/bash

DATE=$(date +"%Y-%m-%d_%H%M")

raspistill  -o /home/pi/camera/$DATE.jpg
```

試しにカメラで一枚撮ってみます。

```
/home/camera/camera.sh
```

lsで、

```
2021-05-17_1003.jpg
```

といったファイルが生成されているはずです。VNCで接続して確認できます。

### PythonのPATH問題が出てきたときは
Lambdaは独自のPython PATHを持っているようなのでそこはlogを注視するしかなさそうですが、それ以外なら

```
pip show dlr
```

でdlrの場所を確かめて、そこにPythonのパスが通っているか確かめます。

```
python
>>>import sys
>>>from pprint import pprint
>>>pprint(sys.path)

['',
 '/usr/lib/python37.zip',
 '/usr/lib/python3.7',
 '/usr/lib/python3.7/lib-dynload',
 '/home/pi/.local/lib/python3.7/site-packages',
 '/usr/local/lib/python3.7/dist-packages',
 '/usr/lib/python3/dist-packages']
```
dlrのあるディレクトリがなかったら、`mv`や`cp`でdlrディレクトリをPATHが通っているディレクトリに持っていきます。

(ちなみにPythonのインタラクティブモードからはCtl+Dで抜けれます)

### DLRとは
Deep Learning Runtimeの略でAWSのSageMaker NeoでコンパイルしたMLモデルをMacやラズパイ、Jetson上で動かすためのランタイムです。

# あとがき
このAWSドキュメントの不備は細かく上げていくとキリがないですが、大まかにいうと、

・書かれているやり方ではdlrがインストールできない
・インストールできても主にPATHの問題でLambdaでは動かせない
・Lambdaをroot権限で動かす必要がないのにroot権限を推奨している。また詳しくはこちらと飛ばされたページの最初にroot権限は原則使わないと書いてある。元の記事にはなぜroot権限なのか書いてない。
・コンテナモードのLambdaとコンテナなしモードのLambdaの記述が混在している。

などです。時間が経ってoutdateしてしまったなら情状酌量の余地はありますが、根本的に記述がおかしい箇所が多い。AWSでAI/IoTを始めようとしている人が多く読むドキュメントで、こういう絶対転ぶ階段をそのままにしているAWSの体たらくに怒りを感じます。これモノ売るっていうレベルじゃねえぞ！！

Greengrass v2を使ったほうがいいのかもしれない、と思う今日このごろ。ただAWS Forumを見ているとv2使用者の悲鳴が毎日のように聞こえてきてこのGreengrassとやらは本当にAWSの問題児だな、としみじみと感じております。

v2で物体検出：https://docs.aws.amazon.com/ja_jp/greengrass/v2/developerguide/dlr-object-detection-component.html
AWS Greengrass Forum（筆者がこの記事を完成できたのもこのフォーラムのおかげです。特にMichael Dombrowski氏、センキュー）：https://forums.aws.amazon.com/forum.jspa?forumID=254&start=0