LoginSignup
6
8

More than 1 year has passed since last update.

M5Stick-VとM5Stick-Cを繋ぎ、Alexaスマートホームスキルとして使用できる顔認識デバイスを作ってみた

Last updated at Posted at 2020-04-16

はじめに

M5Stick-Vを使おうと思ったキッカケは、Alexaスマートホームスキルの起動トリガーとして、顔認識でAlexaスキルが起動できたら面白いと思ったから。センサー用のスマートホームスキルを作成することで、外部のデバイスやアプリをセンサーに見立ててAlexaを操作する事ができるとわかったのだけど、そもそもセンサーも作ったことなければ、顔認識もやったことなかったので、何から手をつければ良いかわからなかった。なので、こういったデバイスというか機械に詳しい人に相談したところ、M5Stick-Vだったら簡単にできると教えてもらったので、これを使うことにした。

M5StickとGroveケーブルは、マルツ秋葉原本店で購入。やっぱり実物を見て購入できる方が好きかな。この店は、20:00まで営業しているので、会社帰りに立ち寄れるので嬉しい。※営業時間は確認してください。

前提

使用機器

  • M5Stick-V
  • M5Stick-C
  • Groveケーブル

全体像

全体のシステムイメージはこんな感じ。

全体図

流れ

  1. M5Stick-Vで顔認識するためのモデル作成
  2. M5Stick-Vプログラム作成
  3. M5Stick-Cプログラム作成
  4. M5Stick-VとM5Stick-Cを接続
  5. テスト

M5Stick-Vで顔認識するためのモデル作成

M5Stic-Vの物体認識モデルを作成する。手順は下図の通りで、簡単に書くと次のようになる。

  1. Material Trainingする(Shoot...の部分)
  2. Zipで固めV-Trainingサイトにアップする(ZIP & Uploadの部分)
  3. モデルができたらダウンロードする(Download Modelの部分)

モデル作成

※Zipの注意点

このようにtrainvaildを選択してZIPする。はじめはこれがわからずtrainvaildの上位フォルダをZIPしてアップロードしたのだが、それではモデルを作成してくれずにエラーとなってしまった。
 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:ぐらい。マイコン初心者なのでこの程度しかできない。

M5Stick-Cへ送信
data_packet = bytearray([0xFF,0xD8,0xEA,0x01,0x00])
uart_Port.write(data_packet)

0xFF,0xD8,0xEAはパケットの識別をするためのもの。0x01は判別した人の番号(僕=1、子供=2)。0x00は予備。といっても使うことはないと思われる。と書いたものの、以下のサイトの情報をただパクっただけ・・・

boot.py
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プログラム作成

処理の流れ

  1. M5Stick-Vからパケットが来たら
  2. LWAから新しいTOKENを取得して
  3. 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 GatawayChangeReport送信する。

4バイト目の数値で処理を判別
        if ((int)rx_buffer[3] == 1) {
          
        } else if ((int)rx_buffer[3] == 2) {

4バイト目が1か2で処理を分ける。下のコードは、1でも2でも処理内容は同じにしている。それ以外の場合は処理をしない。

SendToAlexaEventGataway.ino
#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の方はコネクタが入りにくい?仕様みたいなのでコネクタのツメの部分をハサミで切り落として指すだけにした。

M5Stick-VとM5Stick-C

テスト

Alexaのセンサー用のスマートホームスキルと連動できるかテストした。

デモ

おわりに

素人でも簡単に顔認識をすることができた。認識の精度うんぬんはあるかもしれないけど、手軽に短時間でこれだけのモノができるのだから素晴らしいと思った。
結果、時刻起動でもなく、モーションセンサー起動でもなく、人の顔をトリガーにしてスキルを起動する事ができた。最終的には以下のような感じにしようかな・・・と思っている。

ダンボー

参考

関連

6
8
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
8