M5Stack

M5StackでつくるAI機械学習機能搭載インターフォンカメラ【第2回】

M5StickVとM5StickCをセットアップして写真を撮る

 

第1回: M5Stackとカメラを使ってできること、必要なもの

 

こんにちは、ヨシケンです!

 

今回の連載では、M5Stickとカメラをつなげて、AI機械学習からそこに映っている人を自動判別できる機能を搭載したカメラを作っていきたいと思います。第2回となる今回は、M5StickVカメラで画像の撮影に挑戦。このデータをWi-Fiが付いたM5StickCに送れるようにします。そしてM5StickVで撮影した画像を、LINEに投稿できるようにしてみましょう。

 

なお、今回の連載では「M5StickC」を使用していますが、2022年10月現在は売り止めとなっているようで、後継機の「M5StickC Plus」がありますので概要欄にはそちらを紹介させて頂きます。

M5StickC Plusは、M5StickCの後継機で、従来のものより液晶画面が18.7%拡大し、画面解像度も高解像度のものになっています。また、内蔵バッテリーが95mAhから120mAhに増量されているなど細かい仕様の変更がありますが、基本的なプログラムの記述や書き込み方法などは従来品と変更ありません。

 

今回の記事の流れ

  1. M5StickVカメラでの開発
  2. M5StickCでデータ受け取り、LINEに転送
  3. M5StickVで撮った写真をM5StickCに送る
  4. まとめ

 

このデバイスをくつるのに必要なもの:

名前、説明 デバイス
M5StickC Plus
ESP32を搭載したArduino互換機。ディスプレイ、BLE、Wi-Fiおよびモーションセンサなどが始めから入っている。
M5StickV
カメラとAI機械学習が可能なチップをセットにしたデバイス(ESP32は内蔵しない)
GROVEケーブル、USB Type-Cケーブルなど 適宜使用して下さい。

 

 

1. M5StickVカメラでの開発

まず、カメラ機能であるM5StickVの開発をおこなっていきます。M5StickVで様々な画像を撮影し、それを外部に転送できるようにしましょう。

M5StickVの開発には、前回セットアップしたように、MaixPyという開発環境を使って、プログラムを作っていきます。

 

最初に、M5StickVにプリインストールされているプログラムを使用して、それに機能を追加することにしましょう。これはboot.pyという起動プログラムに入っていて、以下プログラムのようになっています。

このプログラムは、M5StickVのカメラに顔が写った場合、LCD画面上に四角で囲って表示します。またAボタン(LCD画面横のボタン)を押すと、カメラ側のLEDが白色に光るようになっています(以下プログラム中の黄色部分がその主なプログラム)。

 

MaixPyhttps://github.com/sipeed/MaixPy_scripts

 

import lcd,image,time,uos

lcd.init()
lcd.rotation(2) #Rotate the lcd 180deg

try:
   img = image.Image("/flash/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)

from Maix import I2S, GPIO
import audio
from Maix import GPIO
from fpioa_manager import *

fm.register(board_info.SPK_SD, fm.fpioa.GPIO0)
spk_sd=GPIO(GPIO.GPIO0, GPIO.OUT)
spk_sd.value(1) #Enable the SPK output

fm.register(board_info.SPK_DIN,fm.fpioa.I2S0_OUT_D1)
fm.register(board_info.SPK_BCLK,fm.fpioa.I2S0_SCLK)
fm.register(board_info.SPK_LRCLK,fm.fpioa.I2S0_WS)

wav_dev = I2S(I2S.DEVICE_0)

try:
   player = audio.Audio(path = "/flash/ding.wav")
   player.volume(100)
   wav_info = player.play_process(wav_dev)
   wav_dev.channel_config(wav_dev.CHANNEL_1, I2S.TRANSMITTER,resolution = I2S.RESOLUTION_16_BIT, align_mode = I2S.STANDARD_MODE)
   wav_dev.set_sample_rate(wav_info[1])
   while True:
       ret = player.play()
       if ret == None:
           break
       elif ret==0:
           break
   player.finish()
except:
   pass

fm.register(board_info.BUTTON_A, fm.fpioa.GPIO1)
but_a=GPIO(GPIO.GPIO1, GPIO.IN, GPIO.PULL_UP) #PULL_UP is required here!

if but_a.value() == 0: #If dont want to run the demo
   sys.exit()

fm.register(board_info.BUTTON_B, fm.fpioa.GPIO2)
but_b = GPIO(GPIO.GPIO2, GPIO.IN, GPIO.PULL_UP) #PULL_UP is required here!

fm.register(board_info.LED_W, fm.fpioa.GPIO3)
led_w = GPIO(GPIO.GPIO3, GPIO.OUT)
led_w.value(1) #RGBW LEDs are Active Low

fm.register(board_info.LED_R, fm.fpioa.GPIO4)
led_r = GPIO(GPIO.GPIO4, GPIO.OUT)
led_r.value(1) #RGBW LEDs are Active Low

fm.register(board_info.LED_G, fm.fpioa.GPIO5)
led_g = GPIO(GPIO.GPIO5, GPIO.OUT)
led_g.value(1) #RGBW LEDs are Active Low

fm.register(board_info.LED_B, fm.fpioa.GPIO6)
led_b = GPIO(GPIO.GPIO6, GPIO.OUT)
led_b.value(1) #RGBW LEDs are Active Low

time.sleep(0.5) # Delay for few seconds to see the start-up screen :p

import sensor
import KPU as kpu

err_counter = 0

while 1:
   try:
       sensor.reset() #Reset sensor may failed, let's try some times
       break
   except:
       err_counter = err_counter + 1
       if err_counter == 20:
           lcd.draw_string(lcd.width()//2-100,lcd.height()//2-4, "Error: Sensor Init Failed", lcd.WHITE, lcd.RED)
       time.sleep(0.1)
       continue

sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA) #QVGA=320x240
sensor.run(1)

task = kpu.load(0x300000) # Load Model File from Flash
anchor = (1.889, 2.5245, 2.9465, 3.94056, 3.99987, 5.3658, 5.155437, 6.92275, 6.718375, 9.01025)
# Anchor data is for bbox, extracted from the training sets.
kpu.init_yolo2(task, 0.5, 0.3, 5, anchor)

but_stu = 1

try:
   while(True):
       img = sensor.snapshot() # Take an image from sensor
       bbox = kpu.run_yolo2(task, img) # Run the detection routine
       if bbox:
           for i in bbox:
               print(i)
               img.draw_rectangle(i.rect())
       lcd.display(img)

       if but_a.value() == 0 and but_stu == 1:
           if led_w.value() == 1:
               led_w.value(0)
           else:
               led_w.value(1)
           but_stu = 0
       if but_a.value() == 1 and but_stu == 0:
           but_stu = 1

except KeyboardInterrupt:
   a = kpu.deinit(task)
   sys.exit()

 

ここでは、このプログラムに機能を追加して作っていきます。

 

まず、M5StickVで取得したデータを外部に送信できるようにしましょう。M5StickVと外部との接続は、UARTというシリアル接続でおこないます。シリアル接続のためのUARTライブラリを追加してください。そして、Aボタンが押されると、そのデータをシリアル信号として転送するプログラムになっています(以下の水色部分の記述を追加)。

 

M5_face_detect_UART.py

 

import lcd,image,time,uos

lcd.init()
lcd.rotation(2) #Rotate the lcd 180deg

#M5StickV UART
from fpioa_manager import fm, board_info
from machine import 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)

…

try:
   while(True):
       img = sensor.snapshot() # Take an image from sensor
       bbox = kpu.run_yolo2(task, img) # Run the detection routine
       if bbox:
           for i in bbox:
               print(i)
               img.draw_rectangle(i.rect())
               lcd.draw_string(100, 100,"Face!", lcd.RED, lcd.BLACK)
       lcd.display(img)

       if but_a.value() == 0 and but_stu == 1:
           if led_w.value() == 1:
               led_w.value(0)
           else:
               led_w.value(1)

           lcd.draw_string(100, 100, "Shot and Sent!", lcd.RED, lcd.BLACK)
           img_buf = img.compress(quality=70)
           img_size1 = (img.size()& 0xFF0000)>>16
           img_size2 = (img.size()& 0x00FF00)>>8
           img_size3 = (img.size()& 0x0000FF)>>0
           data_packet = bytearray([0xFF,0xD8,0xEA,0x01,img_size1,img_size2,img_size3,0x00,0x00,0x00])
           uart_Port.write(data_packet)
           uart_Port.write(img_buf)
           led_w.value(1)

           but_stu = 0
       if but_a.value() == 1 and but_stu == 0:
           but_stu = 1

except KeyboardInterrupt:
   a = kpu.deinit(task)
   sys.exit()

 

これで、M5StickVからデータを転送する準備ができました。

 

2. M5StickCでデータを受け取り、LINEに転送

次にM5StickVとM5StickCを接続して、M5StickVからの信号を受け取れるようにします。受け取ったデータを、インターネットにつながったM5StickCから、LINEに送る機能を作ります。

 

まずはLINEの設定です。LINE Notifyにアクセスして、ご自分のアカウントでログオンします。

https://notify-bot.line.me/ja/

 

そこの最下段にある[トークンを発行する]ボタンを押してトークンをコピーしておきましょう。

 

このLINE Notify用のトークンから、手動でLINEメッセージを送ってみます。パソコンのコンソールから以下コマンドで、LINEにメッセージが届くか試しましょう。XXXXには先ほどのトークンをセットします。

 

$ curl -X POST -H 'Authorization: Bearer  ZZZZ' -F "message=test" https://notify-api.line.me/api/notify

 

LINEトークンを使ったNotifyができるようになったら、M5StickCでプログラムを作って、そこから送信できるようにします。M5StickCとパソコンをUSB Type-Cでつないで、ArduinoIDEを立ち上げてください。

 

まず、M5StickCのAボタン(液晶下のM5と書かれたボタン)を押すと、LINEにデータを送信するようにします。そのためのM5StickC_Wifi_Lineプログラムはこちら。M5StickCのAボタンを押すと、LINEにメッセージが飛ぶようになっています。XXXX、YYYYはWifiの接続キーを、ZZZZには先ほどのトークンをセットしてください。

 

[ M5StickC_Wifi_Line.ino ]

#include <M5StickC.h>
#include <WiFi.h>
#include <ssl_client.h>
#include <WiFiClientSecure.h>

const char* ssid = "XXXX";
const char* passwd = "YYYY";
const char* host = "notify-api.line.me";
const char* token= "ZZZZ";

WiFiClientSecure client;
HardwareSerial serial_ext(2);

void setup() {
  M5.begin();
  M5.Lcd.setRotation(1);
  M5.Lcd.setTextSize(3);
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.println("M5StickV-C Image");

  client.setInsecure();
  setup_wifi();

  serial_ext.begin(115200, SERIAL_8N1, 32, 33);
}

void loop() {
  M5.update();
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(YELLOW);
  if (serial_ext.available()) {
     M5.Lcd.println(“Msg received!”);
  }

  if (M5.BtnA.wasReleased()) {    
    const char* message1 = "Button A";
    M5.Lcd.println(message1);
    if (!client.connect(host, 443)) { // 443ポート(HTTPS)に接続
      M5.Lcd.println("Connection failed");
      return;
    }
    //リクエストを送信
    String query = String("message=") + message1;
    String request = String("") +
               "POST /api/notify HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" +
               "Authorization: Bearer " + token + "\r\n" +
               "Content-Length: " + String(query.length()) +  "\r\n" + 
               "Content-Type: application/x-www-form-urlencoded\r\n\r\n" +
                query + "\r\n";
    client.print(request);
    while (client.connected()) {
      M5.Lcd.println("Sent to LINE!");
      String line = client.readStringUntil('\n');
      if (line == "\r")   break;
    }
  }
}

void setup_wifi() {
  M5.Lcd.setTextSize(2);
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  M5.Lcd.print(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());
  M5.Lcd.print("!");
}

 

これをM 5StickCに流し込みます。そしてAボタンを押すと、見事LINEにメッセージが送れたでしょうか?

 

このようなButton Aというメッセージが送られていたら、プログラムが正常に動いています。

 

3. M5StickVで撮った写真をM5StickCに送る

それでは、最後に写真もM5StickCに送ってみましょう。M5StickVとM 5StickCをGroveケーブルでつなぎます。

 

先ほどのM5_Wifi_Lineプログラムに追加して、M5StickVで撮影した写真を受け取り、その画像もLINEに送れるようにしています。

以下M5_Wifi_Line_Imageスケッチ中の黄色の部分が、写真の受け取りとそれをLINEに送るプログラムになっています。

 

[ M5_Wifi_Line_Image.ino ]

#include <M5StickC.h>
#include <WiFi.h>
#include <ssl_client.h>
#include <WiFiClientSecure.h>

const char* ssid = "XXXX";
const char* passwd = "YYYY";
const char* host = "notify-api.line.me";
const char* token= "ZZZZ";

WiFiClientSecure client;
HardwareSerial serial_ext(2);

typedef struct {
  uint32_t length;
  uint8_t *buf;
} jpeg_data_t;
jpeg_data_t jpeg_data;
static const int RX_BUF_SIZE = 20000;
static const uint8_t packet_begin[3] = { 0xFF, 0xD8, 0xEA };

void sendLineNotify(uint8_t* image_data, size_t image_sz) {
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(YELLOW);
    
  if (!client.connect(host, 443))   return;
  int httpCode = 404;
  size_t image_size = image_sz;
  String boundary = "-- M5StickV Img --";
  String body = "--" + boundary + "\r\n";

  String message = "今日の写真を撮ったよ!";
  M5.Lcd.println("Picture!");

  body += "Content-Disposition: form-data; name=\"message\"\r\n\r\n" + message + " \r\n";
  if (image_data != NULL && image_sz > 0 ) {
    image_size = image_sz;
    body += "--" + boundary + "\r\n";
    body += "Content-Disposition: form-data; name=\"imageFile\"; filename=\"image.jpg\"\r\n";
    body += "Content-Type: image/jpeg\r\n\r\n";
  }
  String body_end = "--" + boundary + "--\r\n";
  size_t body_length = body.length() + image_size + body_end.length();
  String header = "POST /api/notify HTTP/1.1\r\n";
  header += "Host: notify-api.line.me\r\n";
  header += "Authorization: Bearer " + String(token) + "\r\n";
  header += "User-Agent: " + String("M5StickC") + "\r\n";
  header += "Connection: close\r\n";
  header += "Cache-Control: no-cache\r\n";
  header += "Content-Length: " + String(body_length) + "\r\n";
  header += "Content-Type: multipart/form-data; boundary=" + boundary + "\r\n\r\n";

  client.print(header + body);
  Serial.print(header + body);

  bool Success_h = false;
  uint8_t line_try = 3;
  while (!Success_h && line_try-- > 0) {
    if (image_size > 0) {
      size_t BUF_SIZE = 1024;
      if ( image_data != NULL) {
        uint8_t *p = image_data;
        size_t sz = image_size;
        while ( p != NULL && sz) {
          if ( sz >= BUF_SIZE) {
            client.write( p, BUF_SIZE);
            p += BUF_SIZE; sz -= BUF_SIZE;
          } else {
            client.write( p, sz);
            p += sz; sz = 0;
          }
        }
      }
      client.print("\r\n" + body_end);
      Serial.print("\r\n" + body_end);

      while ( client.connected() && !client.available()) delay(10);
      if ( client.connected() && client.available() ) {
        M5.Lcd.print(" to LINE!");
        String resp = client.readStringUntil('\n');
        httpCode    = resp.substring(resp.indexOf(" ") + 1, resp.indexOf(" ", resp.indexOf(" ") + 1)).toInt();
        Success_h   = (httpCode == 200);
        Serial.println(resp);
      }
      delay(10);
    }
  }
  client.stop();
}

void setup() {
  M5.begin();
  M5.Lcd.setRotation(1);
  M5.Lcd.setTextSize(3);
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.println("M5StickV-C Image");

  client.setInsecure();
  setup_wifi();

  jpeg_data.buf = (uint8_t *) malloc(sizeof(uint8_t) * RX_BUF_SIZE);
  jpeg_data.length = 0;
  if (jpeg_data.buf == NULL) {
    Serial.println("malloc jpeg buffer 1 error");
  }

  serial_ext.begin(115200, SERIAL_8N1, 32, 33);
}

void loop() {
  M5.update();
  if (serial_ext.available()) {
    M5.Lcd.println("Msg received!");
    uint8_t rx_buffer[10];
    int rx_size = serial_ext.readBytes(rx_buffer, 10);
    if (rx_size == 10) {   //packet receive of packet_begin
      if ((rx_buffer[0] == packet_begin[0]) && (rx_buffer[1] == packet_begin[1]) && (rx_buffer[2] == packet_begin[2])) {
        //image size receive of packet_begin
        jpeg_data.length = (uint32_t)(rx_buffer[4] << 16) | (rx_buffer[5] << 8) | rx_buffer[6];
        int rx_size = serial_ext.readBytes(jpeg_data.buf, jpeg_data.length);
        //image processing, for example, line notify send image
        sendLineNotify(jpeg_data.buf, jpeg_data.length);
        //image processing end
      }
    }
  }

 …

このプログラムを実行して、M5StickVのAボタンを押して、写真を撮ります。そうするとM5StickC側に転送され、見事LINEにメッセージとともに表示されました。

 

4. まとめ

今回は、最初にカメラの付いたM5StickVの開発をMaixPyというIDEでおこないました。そこで撮った写真をGroveケーブル経由で、M5StickCに転送することができるようになりました。さらに、インターネットにつながったM5StickCから、LINEに写真とメッセージを送信。これで、カメラとデータ転送、ネット接続の仕組みを作ることができるようになりました。

 

次回は、M5StickVのカメラ画像から機械学習させて、写っているものの判別をできるようにしたいと思います。こんな小さいカメラでも、ちゃんとAI学習ができるようになりますよ!

お楽しみに!

 

 

今回の連載の流れ
第1回: M5Stackとカメラを使ってできること、必要なもの
第2回: M5StickVカメラとM5StickCをセットアップして写真を撮る(今回)
第3回: M5StickVカメラでAI機械学習
第4回: カメラとM5StickCを連動させて、インターネットにも接続して完成!

普通の会社に勤めるサラリーマンですが、モノ作りが好きな週末メイカーで、電子書籍MESHBOOKを出したり、ブログを書いたりしています!

http://blog.ktrips.net

ラズパイと液晶画面で、顔を判別してぴったりの動画を流すデジタルサイネージをつくろう!