はじめに
M5Stick-Vを使おうと思ったキッカケは、Alexaスマートホームスキルの起動トリガーとして、顔認識でAlexaスキルが起動できたら面白いと思ったから。センサー用のスマートホームスキルを作成することで、外部のデバイスやアプリをセンサーに見立ててAlexaを操作する事ができるとわかったのだけど、そもそもセンサーも作ったことなければ、顔認識もやったことなかったので、何から手をつければ良いかわからなかった。なので、こういったデバイスというか機械に詳しい人に相談したところ、M5Stick-Vだったら簡単にできると教えてもらったので、これを使うことにした。
M5StickとGroveケーブルは、マルツ秋葉原本店で購入。やっぱり実物を見て購入できる方が好きかな。この店は、20:00まで営業しているので、会社帰りに立ち寄れるので嬉しい。※営業時間は確認してください。
前提
- Alexaスマートホームスキルが作れる
- Arduino Software (IDE)
- Python
使用機器
- M5Stick-V
- M5Stick-C
- Groveケーブル
全体像
全体のシステムイメージはこんな感じ。
流れ
M5Stick-Vで顔認識するためのモデル作成
M5Stic-Vの物体認識モデルを作成する。手順は下図の通りで、簡単に書くと次のようになる。
-
Material Training
する(Shoot...
の部分) - Zipで固めV-Trainingサイトにアップする(
ZIP & Upload
の部分) - モデルができたらダウンロードする(
Download Model
の部分)
※Zipの注意点
このようにtrain
とvaild
を選択してZIPする。はじめはこれがわからずtrain
とvaild
の上位フォルダをZIPしてアップロードしたのだが、それではモデルを作成してくれずにエラーとなってしまった。
M5Stick-Vプログラム作成
M5Stick-VはWi-Fi機能を持っていないため、外部送信できない。ではどうするか?ネットを漁っていたところ、M5Stick-VとM5Stick-Cを繋いでM5Stick-Cが通信機能を担うといった記事があったため、さっそく参考にした。
今回は、僕と子供の2人分の顔情報を登録して判別できるようにした。念のためもう1人分、まったく関係ない人物を登録し、elseとして判別するようにしたのでプログラムはlabels=["1","2","3"]
となっている。
プログラムの大部分はDownload Model
でダウンロードした時のboot.py
のままで、変更したところはM5Stick-Cとの通信部分の追加と、閾値の変更pmax > 0.98:
ぐらい。マイコン初心者なのでこの程度しかできない。
data_packet = bytearray([0xFF,0xD8,0xEA,0x01,0x00])
uart_Port.write(data_packet)
0xFF,0xD8,0xEA
はパケットの識別をするためのもの。0x01
は判別した人の番号(僕=1、子供=2)。0x00
は予備。といっても使うことはないと思われる。と書いたものの、以下のサイトの情報をただパクっただけ・・・
import network
import socket
import image
import lcd
import sensor
import sys
import time
import KPU as kpu
from Maix import GPIO
from fpioa_manager import fm, board_info
from machine import UART
lcd.init()
lcd.rotation(2)
try:
from pmu import axp192
pmu = axp192()
pmu.enablePMICSleepMode(True)
except:
pass
try:
img = image.Image("/sd/startup.jpg")
lcd.display(img)
except:
lcd.draw_string(lcd.width()//2-100,lcd.height()//2-4, "Error: Cannot find start.jpg", lcd.WHITE, lcd.RED)
task = kpu.load("/sd/3ca317adcac44597_mbnet10_quant.kmodel")
labels=["1","2","3"] #You can check the numbers here to real names.
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_windowing((224, 224))
sensor.run(1)
#M5StickV GPIO_UART
fm.register(35, fm.fpioa.UART2_TX, force=True)
fm.register(34, fm.fpioa.UART2_RX, force=True)
uart_Port = UART(UART.UART2, 115200,8,0,0, timeout=1000, read_buf_len= 4096)
lcd.clear()
while(True):
img = sensor.snapshot()
fmap = kpu.forward(task, img)
plist=fmap[:]
pmax=max(plist)
max_index=plist.index(pmax)
a = lcd.display(img)
if pmax > 0.98:
if labels[max_index].strip() == '1' :
lcd.draw_string(40, 60, "Accu:%.2f Type:%s"%(pmax, labels[max_index].strip()))
lcd.draw_string(40, 70, "Person A")
data_packet = bytearray([0xFF,0xD8,0xEA,0x01,0x00])
uart_Port.write(data_packet)
time.sleep(5)
elif labels[max_index].strip() == '2' :
lcd.draw_string(40, 60, "Accu:%.2f Type:%s"%(pmax, labels[max_index].strip()))
lcd.draw_string(40, 70, "Person B")
data_packet = bytearray([0xFF,0xD8,0xEA,0x02,0x00])
uart_Port.write(data_packet)
time.sleep(5)
a = kpu.deinit(task)
# Send UART End
uart_Port.deinit()
del uart_Port
print("finish")
M5Stick-Cプログラム作成
処理の流れ
- M5Stick-Vからパケットが来たら
- LWAから新しいTOKENを取得して
- AlexaイベントゲートウェイへChangeReportを送信する
uint8_t rx_buffer[5];
int rx_size = serial_ext.readBytes(rx_buffer, 4);
if (rx_size == 4) {
if ((rx_buffer[0] == packet_begin[0]) && (rx_buffer[1] == packet_begin[1]) && (rx_buffer[2] == packet_begin[2]))
対象のパケット(先頭3バイトが0xFF,0xD8,0xEA
)を受信したら、Login with Amazon
から新しいTOKEN
を取得して、Alexa Event Gataway
にChangeReport
を送信する。
if ((int)rx_buffer[3] == 1) {
:
} else if ((int)rx_buffer[3] == 2) {
4バイト目が1か2で処理を分ける。下のコードは、1でも2でも処理内容は同じにしている。それ以外の場合は処理をしない。
#include <M5StickC.h>
#include <WiFi.h>
#include <ssl_client.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include <time.h>
// Wi-Fi
const char* ssid = "<YOUR SSID>";
const char* passwd = "<YOUR SSID PASSWORD>";
struct tm timeInfo;//時刻オブジェクト
char ISO8601[22]; //ISO8601形式の日付文字列
// Amazon API
const size_t capacity = 1024;
const char* host_amazon_api = "api.amazon.com";
String access_token = "";
String refresh_token = "<LWA REFRESH TOKEN>";
String alexa_client_id = "<ALEXA SMARTHOME SKILL CLIENT ID>";
String alexa_client_secret = "<ALEXA SMARTHOME SKILL CLIENT SECRET>";
// Amazon SmartHome EventGateway
const char* host_eventgateway = "api.fe.amazonalexa.com";
// Packet marker
static const uint8_t packet_begin[3] = { 0xFF, 0xD8, 0xEA };
HardwareSerial serial_ext(2);
// the setup routine runs once when M5StickC starts up
void setup() {
M5.begin(); // initialize the M5StickC object
M5.Lcd.setRotation(1);
M5.Lcd.setCursor(10, 0, 2);
M5.Lcd.println("Send 2 Alexa EventGW");
setup_wifi(); // Wi-Fi
configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp"); // NTP
serial_ext.begin(115200, SERIAL_8N1, 32, 33);
}
// the loop routine runs over and over again forever
void loop(){
M5.update();
if (serial_ext.available()>0) {
uint8_t rx_buffer[5];
int rx_size = serial_ext.readBytes(rx_buffer, 4);
if (rx_size == 4) {
if ((rx_buffer[0] == packet_begin[0]) && (rx_buffer[1] == packet_begin[1]) && (rx_buffer[2] == packet_begin[2])) {
if ((int)rx_buffer[3] == 1) {
access_token = getAccessToken();
if (access_token.length() > 0) {
sendToEventgateway(access_token);
Serial.println(" ChangeReport sent to Alexa Event Gateway Server");
}
} else if ((int)rx_buffer[3] == 2) {
access_token = getAccessToken();
if (access_token.length() > 0) {
sendToEventgateway(access_token);
Serial.println(" ChangeReport sent to Alexa Event Gateway Server");
}
} else {
Serial.println(" TARGET OTHER");
}
}
}
}
vTaskDelay(2000 / portTICK_RATE_MS);
}
void setup_wifi() {
Serial.println(); // We start by connecting to a WiFi network
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, passwd);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}
String getAccessToken() {
WiFiClientSecure client;
Serial.println("Try");
if (!client.connect(host_amazon_api, 443)) {
Serial.println("Connection failed");
return "";
}
Serial.println("Connected");
String query_amazon_api = String("");
query_amazon_api += "grant_type=refresh_token";
query_amazon_api += "&refresh_token=" + refresh_token;
query_amazon_api += "&client_id=" + alexa_client_id;
query_amazon_api += "&client_secret=" + alexa_client_secret;
String request_amazon_api = String("");
request_amazon_api += "POST /auth/o2/token HTTP/1.1\r\n";
request_amazon_api += "Host: " + String(host_amazon_api) + "\r\n";
request_amazon_api += "Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\n";
request_amazon_api += "Content-Length: " + String(query_amazon_api.length()) + "\r\n";
request_amazon_api += "\r\n";
request_amazon_api += query_amazon_api + "\r\n";
client.print(request_amazon_api);
Serial.println(request_amazon_api);
// Skip header
while (client.connected()) {
String line = client.readStringUntil('\n');
Serial.println(line);
line.trim();
if (line.length() == 0) {
break;
}
}
String buffer="";
while (client.connected()) {
String line = client.readStringUntil('\n');
Serial.println(line);
buffer.concat(line);
line.trim();
if (line.length() == 0) {
break;
}
}
String line = client.readStringUntil('\n');
Serial.println(line);
client.stop(); // Connection terminated
DynamicJsonDocument doc(capacity);
DeserializationError err = deserializeJson(doc, buffer.c_str());
Serial.printf("DeserializationError:%s\n",err.c_str());
const char* new_token = doc["access_token"];
return String(new_token);
}
void sendToEventgateway(String token) {
WiFiClientSecure client2;
Serial.println("Try");
if (!client2.connect(host_eventgateway, 443)) {
Serial.println("Connection failed");
return;
}
Serial.println("Connected");
getLocalTime(&timeInfo);
sprintf(ISO8601, "%04d-%02d-%02dT%02d:%02d:%02d.00Z",
timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec); // ISO8601形式にする "2020-04-11T23:20:50.52Z"
String query_eventgateway = String("");
query_eventgateway += "{";
query_eventgateway += " \"context\": {},";
query_eventgateway += " \"event\": {";
query_eventgateway += " \"header\": {";
query_eventgateway += " \"messageId\": \"TS-000-000-328\",";
query_eventgateway += " \"namespace\": \"Alexa\",";
query_eventgateway += " \"name\": \"ChangeReport\",";
query_eventgateway += " \"payloadVersion\": \"3\"";
query_eventgateway += " },";
query_eventgateway += " \"endpoint\": {";
query_eventgateway += " \"scope\": {";
query_eventgateway += " \"type\":\"BearerToken\",";
query_eventgateway += " \"token\":\"" + token + "\"";
query_eventgateway += " },";
query_eventgateway += " \"endpointId\" : \"sensor-001\"";
query_eventgateway += " },";
query_eventgateway += " \"payload\": {";
query_eventgateway += " \"change\": {";
query_eventgateway += " \"cause\": {";
query_eventgateway += " \"type\": \"PHYSICAL_INTERACTION\"";
query_eventgateway += " },";
query_eventgateway += " \"properties\": [";
query_eventgateway += " {";
query_eventgateway += " \"namespace\": \"Alexa.MotionSensor\",";
query_eventgateway += " \"name\": \"detectionState\",";
query_eventgateway += " \"value\": \"DETECTED\",";
query_eventgateway += " \"timeOfSample\": \"" + String(ISO8601) + "\",";
query_eventgateway += " \"uncertaintyInMilliseconds\": 0";
query_eventgateway += " }";
query_eventgateway += " ]";
query_eventgateway += " }";
query_eventgateway += " }";
query_eventgateway += " }";
query_eventgateway += "}";
String request_eventgateway = String("") +
request_eventgateway += "POST /v3/events HTTP/1.1\r\n";
request_eventgateway += "Host: " + String(host_eventgateway) + "\r\n";
request_eventgateway += "Authorization: Bearer " + token + "\r\n";
request_eventgateway += "Content-Length: " + String(query_eventgateway.length()) + "\r\n";
request_eventgateway += "Content-Type: application/json; charset=UTF-8\r\n";
request_eventgateway += "\r\n";
request_eventgateway += query_eventgateway + "\r\n";
client2.print(request_eventgateway);
Serial.println(request_eventgateway);
while (client2.connected()) {
String line = client2.readStringUntil('\n');
Serial.println(line);
if (line == "\r") {
break;
}
}
String line = client2.readStringUntil('\n');
Serial.println(line);
client2.stop(); // Connection terminated
}
M5Stick-VとM5Stick-Cを接続
M5Stick-VとM5Stick-CをGrove用ケーブルで繋ぐだけで、特に設定等の作業は必要ないみたい。コンパクトにしたかったので、今回は5cmのケーブルを使用した。M5Stick-Vの方はコネクタが入りにくい?仕様みたいなのでコネクタのツメの部分をハサミで切り落として指すだけにした。
テスト
Alexaのセンサー用のスマートホームスキルと連動できるかテストした。
おわりに
素人でも簡単に顔認識をすることができた。認識の精度うんぬんはあるかもしれないけど、手軽に短時間でこれだけのモノができるのだから素晴らしいと思った。
結果、時刻起動でもなく、モーションセンサー起動でもなく、人の顔をトリガーにしてスキルを起動する事ができた。最終的には以下のような感じにしようかな・・・と思っている。
参考
- M5StickVとM5StickCでお知らせ機能付きエッジAIカメラを作った
- Wi-FiがないM5StickVを、M5StickCと繋ぎLINEに投稿してみるまでの手順
- M5Stack Docs - V-Training
- マルツエレック株式会社