1.はじめに
健康のため食品カロリーを自動で推定記録してくれるシステムをラズパイで作りました。そもそもの動機は、コロナのリモートワークで健康数値が劇的に劣化したからです。ある日、かかりつけ医に行ったら血糖値330、HbA1cが10.4と指摘されました。(どのくらいやばいかはお察しください。エコー検査や眼底検査を即実施。運動もせずに終日何かしら食べられる在宅ワーク環境は大変健康にヤバいです。)
食事カロリーを記録してダイエットしたら、4か月で72kg⇒62kgに体重10kg減、HbA1cは6.0、ついでにウエスト88cm⇒81cmになり服薬は半分に。食事改善で1日当たり1700-2000kcal程度にするとあらゆる健康数値改善に絶大な効果がある事は判りました。しかし徐々に息切れしてリバウンドし現体重は65kg。間食管理も面倒で続き難いです。困りますよね。
1.1 やりたいこと。
- 食品カロリー計測を省力化して丁寧に毎日ちゃんと記録する。でも楽をする。(ついでに自動体重計測もしてくれるとなお楽。)
- 食品を秤に載せ、カメラで秤上の食品を映してシャッターを切ると画像認識し、重量からカロリー算出して記録する。
2.システム構成と前提
- 下記の構成を構想しました。ラズパイ、Soracom、AWS EC2を用意します。
- AWSアカウントが必要です。ラズパイは3Bを使用。Soracomは無くても自宅からネット接続出来れば動作は可能ですが、今後宅外からのGateway操作を手軽にしたいのでセキュリティ確保用。
- ロードセルキット(アマゾンで約1,400円)とUSBカメラ(適当なWebCam)が必須。
- 計画段階で食品画像認識まわりが最難関です。何が使えるやら。
導入手順です。
3.環境準備
3.1. RaspberryPiのIoT環境準備
3.2. AWS EC2のクラウド環境準備
4.実装
4.1. 食品の重さを計測しクラウドへ送る
4.2. 食品を撮影し画像をクラウドへ送る
4.3. AWS Rekognition Custom Labelsで食品品目を推論する(詳細別記事)
4.4. スマホ向けダッシュボードを実装
5.導入
5.1. 台所で導入試験
3.環境準備
3.1. RaspberryPiのIoT環境準備
3.1.1. Raspberry Pi OS
ラズパイ3BにRaspberry Pi OS(Raspbian)をインストールします。(詳細はこちら。Soracom接続含めこちら。 Node-red含め[こちら] (https://qiita.com/utaani/items/7155c62d6c5e96822afb)。)64bit版OSはまだNoderedやPythonのデバイス制御関係が不安定なので安定な32bit版OSを使います。[公式ページ](https://www.raspberrypi.org/software/)からRaspberry Pi ImagerをPCにダウンロードします。
PCでRaspberry Pi Imagerを起動しOS(Raspberry Pi OS 32bit)を選択、SDカードを選択、MicroSDカードにイメージを書き込みます。SDカードは32GByte推奨。(64GByte以上はフォーマット認識でトラブルになる場合有り。)書き込んだSDカードをラズパイに挿し、HDMIモニタとUSBマウス、キーボードを接続し電源ON。画面にpiユーザのパスワード設定と、日本語などconfig画面等が出るので設定してから再起動します。wifi接続とsshの有効化をしておくと良いでしょう。
(開発PCからLAN経由でVisualStdioCodeリモート開発プラグインで接続するとshellもエディタもリモート操作出来て効率が良いですが必須では有りません。)
3.1.2. Soracom Air利用開始
SoracomAirのスターターキットがUSBドングル込み6,000円程で購入できます。
SORACOMオペレータ登録
登録ページから、メアドとパスワードでオペレータアカウント作成後、クレジットカード登録しSIM情報を登録。
ラズパイのデバイス準備
下記操作でpppとwvdial関係パッケージがインストールされ、USB認識の準備とwwan ifが設定されます。
$ curl -O https://soracom-files.s3.amazonaws.com/setup_air.sh
$ sudo bash setup_air.sh
USBドングル(AK-020)にSIMを挿入し接続
付属のクイックスタートガイドを参照して挿入。付属SIMはフルサイズです。
USBドングルは少し幅が広いので他と干渉しない様にラズパイに挿します。
LEDが点滅してドングル認識⇒PPP実行⇒オンラインに遷移します。
Soracomのユーザコンソールから接続状態を確認してオンラインになっていれば成功です。
wwanのifが認識されていてインターネットにpingも通り通信可能な筈です。
接続と切断は$ sudo ifup wwan0
と$sudo ifdown wwan0
です。
3.1.3. 開発言語の導入
今回ラズパイのGatewayアプリケーション開発にはPythonとnode-redを使います。
Pythonは最初から入っています。
pi@raspberrypi:~ $ python --version
Python 2.7.16
pi@raspberrypi:~ $ python3 --version
Python 3.7.3
node-redはグラフィカルな開発言語です。javascriptで内部コード記述ができて、出来る事は概ねPythonと似ています。Raspberry PiのデスクトップのGUIメニューから「設定」⇒「Recomended Software」⇒「Node-Red」を選択すればインストール出来ます。(が公式サイトによれば、この手順ではnpm(nodeのパッケージマネージャ)が入らないため、公式スクリプト利用を強く推奨するとのこと。取り敢えず動かすだけならば大差は無いと思います。)インストール後、直ちにadminのパスワードを設定しましょう。(コマンドラインで以下を実施して表示されたhash値を、~/.node-red/settings.jsのadminAuth設定のadminアカウントの場所を探して追記します。)
pi@raspberrypi:~ $ node-red admin hash-pw
Password: **********
**********************************
$ node-red-start
でサービスを起動します。起動時のログ表示からport 1880で待ち受けていることが判ります。Raspberry pi上のブラウザから http://localhost:1880 にアクセスするか、LAN上の開発PCからhttp://host:1880 にアクセスするとエディタ画面が表示されグラフィカルに開発可能です。
Hello Worldとpingで動作確認します。問題無ければモニタ無し電源ONでサービスが立ち上がるように$ sudo systemctl enabele nodered
します。
Node-redコード
[{"id":"667ff0a5.45beb","type":"tab","label":"/ping","disabled":false,"info":""},{"id":"f2e6f0b9.2cfad","type":"http in","z":"667ff0a5.45beb","name":"","url":"/ping/:data","method":"get","upload":false,"swaggerDoc":"","x":180,"y":240,"wires":[["b8e7f68f.77a398","599f5566.7a683c"]]},{"id":"b8e7f68f.77a398","type":"template","z":"667ff0a5.45beb","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"pong: {{req.params.data}}","output":"str","x":400,"y":240,"wires":[["115b4503.abcf3b","ecaf0d54.2d798"]]},{"id":"115b4503.abcf3b","type":"http response","z":"667ff0a5.45beb","name":"","statusCode":"","headers":{},"x":570,"y":240,"wires":[]},{"id":"599f5566.7a683c","type":"debug","z":"667ff0a5.45beb","name":"/ping 受信","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":400,"y":320,"wires":[]},{"id":"ecaf0d54.2d798","type":"debug","z":"667ff0a5.45beb","name":"pong 応答","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":580,"y":320,"wires":[]},{"id":"5710e13.0b8a72","type":"debug","z":"667ff0a5.45beb","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":810,"y":140,"wires":[]},{"id":"b84127ec.13cb18","type":"http request","z":"667ff0a5.45beb","name":"GET responce from /ping/{{payload}}","method":"GET","ret":"txt","paytoqs":"ignore","url":"http://127.0.0.1:1880/ping/{{payload}}","tls":"","persist":false,"proxy":"","authType":"","x":490,"y":140,"wires":[["5710e13.0b8a72"]]},{"id":"68f5bbac.22a404","type":"inject","z":"667ff0a5.45beb","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":180,"y":140,"wires":[["b84127ec.13cb18"]]}]
node-redでは機能を持つノードを繋いでフローを作ってプログラムします。フローの開始点はイベントを発生するノードです。フローは(グローバル変数等を共有していない限り)互いに関係を気にせず配置すれば良く、密結合になり難い点はサーバ開発に向いてます。
3.2. AWS EC2のクラウド環境準備
今回はサーバレスに拘らずにEC2インスタンスを立てます。(構成例:CentOS 7 (x86_64) with Updates HVM/t2.micro/EBS SSD 8GiB)。タグに"Name"でインスタンス名を設定すると便利です。昨今、インターネットを経由するならセキュリティは気を使うべきところでしょう。1880 portをデフォルトまま使うのは望ましく有りません。変更や認証で制限しておくべきでしょう(その辺はごめんなさい、省略)。適切な設定を進めsshで公開鍵ログイン可能な所まで進みましょう。
3.2.1. 開発言語の導入
クラウドサービス開発もPythonとnode-redを使います。CentOS7にはデフォルトではpython2系しか入っていないのでpython3を導入します。
$ sudo yum install python3 -y
これでpython3が使えます。次に後述する画像認識サービスを使う為に公式サイトに従ってAWS CLIとAWS SDK for Pythonを導入します。
$ sudo yum install zip unzip
$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
$ unzip awscliv2.zip
$ sudo ./aws/install
$ aws --version
$ aws configure
AWS Access Key ID [None]:********
AWS Secret Access Key [None]: ************
Default region name [None]: ***********
Default output format [None]: json
aws configureの対話形式で求められる認証情報に自分の認証情報を入力。output format指定はjsonがお勧め。正しく認証情報を設定出来ていれば$ aws ec2 describe-instances
でAWSに接続してec2インスタンス情報を表示出来ます。(jqコマンドを導入しておくと結果のjsonのフィルタや整形が容易になります。VisualStdioCodeリモート開発も必須では無いですが有れば便利です。)
$ sudo yum -y install epel-release
$ sudo yum -y install jq
$ sudo pip3 install boto3
下記でnode-redを導入します。ラズパイ同様adminのパスワードを設定し$ node-red-start
で起動できます。
$ curl -L https://raw.githubusercontent.com/node-red/linux-installers/master/rpm/update-nodejs-and-nodered -O
$ bash update-nodejs-and-nodered
以上で環境設定できました。
4.実装
4.1. 食品の重さを計測しクラウド送信
アマゾンから購入したロードセルキットを用いて食品重量をラズパイで読み取る秤を作ります。詳しくは作例ページ(raspberry piとhx711を使った重量測定器、飲んだ量を教えてくれるコースターを作ってみた) を参照。今回のロードセルキットはHX711を用いていますので有難く参考にします。
4.1.1. 開封と組立
半キットで半田付不要です。開封してビス止めします。
公式サイトのGPIOピン配置を参考に接続します。
hx711 | raspberypi |
---|---|
GND | GND(6pin) |
DT | GPIO5(29pin) |
SCK | GPIO6(31pin) |
VCC | 5V(4pin) |
4.1.2. 読み出しと記録
接続が完了したらgithubからHX711読み出しコードをお借りして使用させて頂きます。
$ git clone https://github.com/tatobari/hx711py
$ python hx711/example.py
Tare done! Add weight now...
6
12
27
26
22044
173044
出力はこのままですとADC読み出しのままで重さになっていません。キッチンスケールで重さを量ったものをロードセルでも量って較正します。
set_reference_unitで較正係数を設定。さらにコンソール出力に加え、ファイルへの出力を追加します。
#hx.set_reference_unit(referenceUnit)
hx.set_reference_unit(363)
# ...略...
f = open('weight.txt', 'w')
while True:
# ...略...
val = hx.get_weight(5)
print(val)
f.write(str(val)+'g\n')
f.flush()
4.1.3. 重さを連続してAWSに送る
node-redのtailノードを使って先程のファイルへの書き込みを監視し、読みとった重量数値をクラウドサーバへ送信します。APIへ送信するhttp reqノードを追加しhttpのメソッドはPOSTにします。URLにはEC2インスタンスのPublicIPアドレスを指定しportとパス(/weightなど)も指定します。単純ですね。動作優先。
tailノードはとても便利で動的に更新されるファイルをnode-redから監視出来ます。連携が面倒な場合はとにかくログファイルに出力させて監視して取り込めば何とかなります。早い結合は正義。4.2. 食品を撮影し画像をクラウド送信
4.2.1. USBカメラを接続する
適当なUSBカメラを接続してUSB(/dev/video*)認識状況を確認します。
$ v4l2-ctl --list-devices
fswebcamコマンドをインストールし/dev/videoから静止画を取得しファイルに保存してみます。
$ sudo apt install fswebcam -y
$ fswebcam -q --device /dev/video0 test0.jpg
ラズパイのローカル画面で表示確認するには$ gpicview test0.jpg
、ブラウザ経由で外部から画像を確認したい場合には、$ python3 -m http.server 8000
が手軽。(詳しくはこちら)
4.2.2. node-redでカメラ画像を撮る
node-redのパレット管理から検索してnode-red-contrib-usbcameraを追加。内部的に使うfswebcamのインストールは先ほど済んでいます。(aptやnpm等のCLIで必要な準備を済ませてからnode-redのパレット管理からサードパーティ・ノードを追加する順がスムーズ。)さらにパレット管理から検索してnode-red-image-outputをデバッグ用に追加。追加されたusbcameraノードとimage previewノードを配置し以下の様にフローを作成。インジェクションノード"撮影"を押して撮影データ取得を確認。
4.2.3. 撮影画像をAWSに送る
前節のフローにhttp reqノードを追加。メソッドをPOSTにします。URLにEC2インスタンスのPublicIPアドレスを指定しportとパス(/uploadなど)も添えます。これで撮影したデータをAWSに送信出来ます。
クラウドで静止画を受けられたのでまずは良し。ここでは手動でinjectionノードを操作していますが”スケールの重さが変化して安定した”などをトリガ要因にしてやれば静止画アップロードアプリとして利用出来ます。
4.3. AWS Rekognition Custom Labels
AWS Rekognition Custom Labelsを用いて食品画像認識し推定することにしました。
内容が長くなりましたので詳細は別記事に分割しました。別記事はこちらから。
過去画像データは潤沢にあったので活用すべく、AWS Rekognition Custom Labelsで個別学習を行って、AWS EC2から食品認識させる機能をなんとか確立。クラウドのPythonで推定結果を得ることが出来ます。学習データと推定結果画像を載せます。(ちょっとなんでかよく判らないくらい凄い。)
4.4. スマホ向けダッシュボード実装
AWS EC2上にnode-redのダッシュボード機能で作ります。パレット管理からnode-red-dashboardを検索して追加。ノードを下記の様につなぎダッシュボード出力サービスを作ります。
NodeRed コード
[{"id":"656830e3.cc32c","type":"watch","z":"1683914.ed9cb6f","name":"","files":"upload/uploaded.jpg","recursive":"","x":230,"y":820,"wires":[["7a6c2500.3c738c"]]},{"id":"7a6c2500.3c738c","type":"file in","z":"1683914.ed9cb6f","name":"","filename":"upload/uploaded.jpg","format":"","chunk":false,"sendError":false,"encoding":"none","x":480,"y":820,"wires":[["1b7a7fcd.5373f"]]},{"id":"92bfb871.aa5b08","type":"ui_template","z":"1683914.ed9cb6f","group":"bf5b85ec.68ea28","name":"撮影画像","order":1,"width":"6","height":"5","format":"<div>\n <img src=\"data:image/png;base64,{{msg.payload}}\" width=100%>\n</div>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","x":880,"y":820,"wires":[[]]},{"id":"1b7a7fcd.5373f","type":"base64","z":"1683914.ed9cb6f","name":"","action":"","property":"payload","x":700,"y":820,"wires":[["92bfb871.aa5b08"]]},{"id":"20f3ea30.c39406","type":"http in","z":"1683914.ed9cb6f","name":"","url":"/weight","method":"post","upload":false,"swaggerDoc":"","x":650,"y":640,"wires":[["337bfadd.744df6"]]},{"id":"337bfadd.744df6","type":"ui_text","z":"1683914.ed9cb6f","group":"bf5b85ec.68ea28","order":2,"width":"0","height":"0","name":"重量","label":"重量","format":"{{msg.payload}}","layout":"row-spread","x":890,"y":640,"wires":[]},{"id":"6a26453.da078bc","type":"watch","z":"1683914.ed9cb6f","name":"","files":"rekognition/result.jpg","recursive":"","x":230,"y":880,"wires":[["9a630d71.715d3"]]},{"id":"9a630d71.715d3","type":"file in","z":"1683914.ed9cb6f","name":"","filename":"rekognition/result.jpg","format":"","chunk":false,"sendError":false,"encoding":"none","x":480,"y":880,"wires":[["2bc06b6e.d6d7a4"]]},{"id":"2bc06b6e.d6d7a4","type":"base64","z":"1683914.ed9cb6f","name":"","action":"","property":"payload","x":700,"y":880,"wires":[["fdef2e98.9a318"]]},{"id":"fdef2e98.9a318","type":"ui_template","z":"1683914.ed9cb6f","group":"bf5b85ec.68ea28","name":"推測結果","order":3,"width":"6","height":"5","format":"<div>\n <img src=\"data:image/jpeg;base64,{{msg.payload}}\" width=\"100%\">\n</div>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","x":880,"y":880,"wires":[[]]},{"id":"ce1f5c96.27a0e","type":"ui_text","z":"1683914.ed9cb6f","group":"bf5b85ec.68ea28","order":2,"width":"0","height":"0","name":"品目","label":"品目(1位)","format":"{{msg.payload}}","layout":"row-spread","x":890,"y":700,"wires":[]},{"id":"8a69b089.a4a11","type":"http in","z":"1683914.ed9cb6f","name":"","url":"/food","method":"post","upload":false,"swaggerDoc":"","x":640,"y":700,"wires":[["ce1f5c96.27a0e"]]},{"id":"e652e864.9251e8","type":"ui_text","z":"1683914.ed9cb6f","group":"bf5b85ec.68ea28","order":2,"width":"0","height":"0","name":"カロリー","label":"カロリー","format":"{{msg.payload}}","layout":"row-spread","x":880,"y":760,"wires":[]},{"id":"13180ae1.2b1d95","type":"http in","z":"1683914.ed9cb6f","name":"","url":"/calorie","method":"post","upload":false,"swaggerDoc":"","x":650,"y":760,"wires":[["e652e864.9251e8"]]},{"id":"8af132b2.e39dd","type":"inject","z":"1683914.ed9cb6f","name":"起動0.1秒後","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"num","x":230,"y":660,"wires":[["7a6c2500.3c738c","9a630d71.715d3","bbee3ce1.6595"]]},{"id":"bbee3ce1.6595","type":"template","z":"1683914.ed9cb6f","name":"No Data","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"No Data","output":"str","x":440,"y":660,"wires":[["e652e864.9251e8","ce1f5c96.27a0e","337bfadd.744df6"]]},{"id":"bf5b85ec.68ea28","type":"ui_group","name":"測定結果","tab":"17534e5b.b782a2","order":3,"disp":true,"width":"6","collapse":false},{"id":"17534e5b.b782a2","type":"ui_tab","name":"ホーム","icon":"dashboard","disabled":false,"hidden":false}]
#5.導入
5.1. 台所で導入試験
デスクから台所に場所を移して実証試験です。100均の整理台が撮影にちょうど良い高さです。写真も撮れてスマホへの画像反映もOK。重量の更新も十分リアルタイム。(重量数値が"37716"とかなのはロードセル読み出しを変換するキャリブレーションを未だプログラムしていなかった為。)。。ここで重大な機能欠如に気が付きました。改善検討に記述します。
#6.まとめ
AWS Rekognition Custom Labelsが大変良いものだということが判りました。心強いです。
カロリーを自動推定するシステムの動作確認が出来ましたのでぼちぼち試用中です。
直したいところが色々有ります。
- 改善検討中
-
風袋引き機能が無い
- お皿だけを載せて一旦ゼロにするやつ。その後で食品を量ります。宇宙の全キッチンスケールについていますね。本文でも書きましたが。。。最大の誤認。食品認識に気を取られ過ぎました。ゼロリセット操作の為にはボタンが要りますがラズパイにはボタンが無い。直接付けても良いですが、重量表示も無いと使い勝手が悪い。かといってLCDを付けたら理想のポータブルIoTから離れていきます。(ボタンも表示も有るインタフェース機器として今後M5stackにご登板頂く予定です。BLE通信までは出来た。)
- 追加学習がフローに入っていない
- PCで時々食品のアノテーション作業してAIを育てる。手が掛かります。__そんな暇はない。__一旦スマホで食品選択をしたらからには追加学習して欲しい。(こんな贅沢言えるのは、Rekognition Custom Labelsならやれそうな目が見えたからですかね。)
- カロリーDBが無い
- 省きました。品目がカスタム定義("salad chiken"とか)の時点で汎用食品DBが使えない。品目別カロリーをハードコード。
- 体重計が繋がっていない
- Q:繋がっている必要が有りますか?A:有りません。(でも面白そうなんですよね、巷に溢れるMaid in China体重計のBluetooth通信。)
-
風袋引き機能が無い
残り事項
下記は接続できましたら追記します。
4.5. M5StickCで手元Bluetooth操作する(ToDo)
4.6. RaspberryPiからBluetooth体重計の体重取集する(ToDo)
7.補足
このブログはSoracomラズパイコンテスト応募記事です。
・受賞者様方の記事がDIYレシピに掲載されました。色々と参考になります。
・参加賞頂きました!
8. 参考資料