AIカメラ“M5StickV”で会議室の人数を数えてデータを可視化

2020年12月18日 金曜日


【この記事を書いた人】
浮田 悠太

IIJ IoTビジネス事業部 新規事業推進課に所属。webサービス開発・運用を担当しており、現在は Machinist の開発に従事している。原動力は甘いもの。

「AIカメラ“M5StickV”で会議室の人数を数えてデータを可視化」のイメージ

IIJ 2020 TECHアドベントカレンダー 12/18(金)の記事です】

はじめに

5Gのサービスが始まったことで端末側で高度な処理を行うエッジコンピューティングに注目が集まっています。
エッジ側に学習モデルを用意して画像解析をするAIカメラも見かけますね。
今回はスイッチサイエンスで販売されているAIカメラの「M5StickV」を使って、カメラに映る人数をリアルタイムに数えるということに挑戦します。数えた人数のデータは 以前のアドベントカレンダー でも紹介した「 Machinist (マシニスト) 」に送信して可視化してみます。

分かったこと

実施にやってみたところ M5StickV の機能だけで人を認識し、簡単に人数を数えることができました。
ただし、カメラの撮影範囲がネックでした。画角が縦60°横80°程度なので、撮影範囲に限界があります。少人数の狭い会議室で使う分には問題はなさそうですが、広い会議室で使おうとすると複数台を組み合わせる等の工夫が必要だと感じました。

手順

道具の準備

用意したものです。

  • maixpy/arduino IDE が動作するPC (今回はMacBookPro Catalina)
  • M5StickV
  • M5StickC (Wi-Fi 接続で Machinist にデータ送信)
  • Groveケーブル (M5StickV と M5StickC の接続)

M5StickV のセットアップ

https://docs.m5stack.com/#/en/quick_start/m5stickv/m5stickv_quick_start
こちらの get started を参考に Maixpy をセットアップします。

M5StickC のセットアップ

https://youtu.be/ppXkl0046dc
こちらのチュートリアル動画を参考に Arduino IDE をセットアップします。

M5StickV のコード

M5Stick では、カメラの画像を解析して人を検知した場合にその人数を M5StickC に送信します。
実際に動作させてみると、M5stickV では 1秒間におよそ10フレームほど画像解析ができると分かったので、1秒間毎にデータを丸めるようにしています。M5stickV のコードは MicroPython で以下のようになります。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import time, sensor, image,lcd
from fpioa_manager import fm, board_info
import KPU as kpu
from machine import UART
# init camera
clock = time.clock()
lcd.init()
lcd.direction(lcd.YX_LRUD)
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_hmirror(0)
sensor.run(1)
# 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)
classes = ['aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike', 'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor']
task = kpu.load("/sd/model/20class.kmodel")
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)
while True:
clock.tick()
img = sensor.snapshot()
code_obj = kpu.run_yolo2(task, img)
count=0
if code_obj: # object detected
max_id = 0
max_rect = 0
for i in code_obj:
if classes[i.classid()] == 'person':
count+=1
img.draw_rectangle(i.rect())
text = ' ' + classes[i.classid()] + ' (' + str(int(i.value()*100)) + '%) '
for x in range(-1,2):
for y in range(-1,2):
img.draw_string(x+i.x(), y+i.y()+(i.h()>>1), text, color=(250,205,137), scale=2,mono_space=False)
img.draw_string(i.x(), i.y()+(i.h()>>1), text, color=(119,48,48), scale=2,mono_space=False)
id = i.classid()
rect_size = i.w() * i.h()
if rect_size > max_rect:
max_rect = rect_size
max_id = id
lcd.draw_string(10,20,str(count))
# Identification packets
data_packet = bytearray([0xFF,0xD8,0xEA])
uart_Port.write(data_packet)
data = bytearray([count])
uart_Port.write(data)
lcd.display(img)
# Send UART End
uart_Port.deinit()
del uart_Portu
import time, sensor, image,lcd from fpioa_manager import fm, board_info import KPU as kpu from machine import UART # init camera clock = time.clock() lcd.init() lcd.direction(lcd.YX_LRUD) sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.set_hmirror(0) sensor.run(1) # 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) classes = ['aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike', 'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor'] task = kpu.load("/sd/model/20class.kmodel") 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) while True: clock.tick() img = sensor.snapshot() code_obj = kpu.run_yolo2(task, img) count=0 if code_obj: # object detected max_id = 0 max_rect = 0 for i in code_obj: if classes[i.classid()] == 'person': count+=1 img.draw_rectangle(i.rect()) text = ' ' + classes[i.classid()] + ' (' + str(int(i.value()*100)) + '%) ' for x in range(-1,2): for y in range(-1,2): img.draw_string(x+i.x(), y+i.y()+(i.h()>>1), text, color=(250,205,137), scale=2,mono_space=False) img.draw_string(i.x(), i.y()+(i.h()>>1), text, color=(119,48,48), scale=2,mono_space=False) id = i.classid() rect_size = i.w() * i.h() if rect_size > max_rect: max_rect = rect_size max_id = id lcd.draw_string(10,20,str(count)) # Identification packets data_packet = bytearray([0xFF,0xD8,0xEA]) uart_Port.write(data_packet) data = bytearray([count]) uart_Port.write(data) lcd.display(img) # Send UART End uart_Port.deinit() del uart_Portu
import time, sensor, image,lcd
from fpioa_manager import fm, board_info
import KPU as kpu
from machine import UART

# init camera
clock = time.clock()
lcd.init()
lcd.direction(lcd.YX_LRUD)

sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_hmirror(0)
sensor.run(1)

# 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)

classes = ['aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike', 'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor']
task = kpu.load("/sd/model/20class.kmodel")

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)

while True:
    clock.tick()
    img = sensor.snapshot()
    code_obj = kpu.run_yolo2(task, img)
    count=0
    if code_obj: # object detected
        max_id = 0
        max_rect = 0
        for i in code_obj:
            if classes[i.classid()] == 'person':
                count+=1
            img.draw_rectangle(i.rect())
            text = ' ' + classes[i.classid()] + ' (' + str(int(i.value()*100)) + '%) '
            for x in range(-1,2):
                for y in range(-1,2):
                    img.draw_string(x+i.x(), y+i.y()+(i.h()>>1), text, color=(250,205,137), scale=2,mono_space=False)
            img.draw_string(i.x(), i.y()+(i.h()>>1), text, color=(119,48,48), scale=2,mono_space=False)
            id = i.classid()
            rect_size = i.w() * i.h()
            if rect_size > max_rect:
                max_rect = rect_size
                max_id = id
        lcd.draw_string(10,20,str(count))
    # Identification packets
    data_packet = bytearray([0xFF,0xD8,0xEA])
    uart_Port.write(data_packet)
    data = bytearray([count])
    uart_Port.write(data)

    lcd.display(img)
#   Send UART End
uart_Port.deinit()
del uart_Portu

M5StickC のコード

M5StickC では、M5StickV から Grove ポートを通じて人数のデータを受信します。
受信したデータから1分間の平均値を計算して、それをMachinistに送るようにしています。
M5StickC は Wi-Fi 経由でインターネットに接続できます。コードは C++ で以下のようになります。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#include <M5StickC.h>
#include <WiFi.h>
#include <ArduinoJson.h>
#include <HTTPClient.h>
#include <ssl_client.h>
#include <WiFiClientSecure.h>
const char* ssid = "YOUR_SSID";
const char* passwd = "YOUR_PASSWORD";
HardwareSerial serial_ext(2);
static const int RX_BUF_SIZE = 20000;
static const uint8_t packet_begin[3] = { 0xFF, 0xD8, 0xEA };
unsigned long nextUpdate = 0;
unsigned long sum=0;
unsigned long count=0;
float avg = 0.0;
uint8_t person[1];
void setup() {
M5.begin();
M5.Lcd.setRotation(3);
M5.Lcd.setCursor(0, 30, 4);
M5.Lcd.println("m5stick_uart_wifi_converter");
setup_wifi();
serial_ext.begin(115200, SERIAL_8N1, 32, 33);
}
void loop() {
M5.update();
if (serial_ext.available()) {
uint8_t rx_buffer[3];
int rx_size = serial_ext.readBytes(rx_buffer, 3);
if (rx_size == 3) { //packet receive of packet_begin
if ((rx_buffer[0] == packet_begin[0]) && (rx_buffer[1] == packet_begin[1]) && (rx_buffer[2] == packet_begin[2])) {
serial_ext.readBytes(person,1);
sum += person[0];
count += 1;
}
}
}
// send once a minute
if ( nextUpdate < millis() ) {
avg = float(sum) / count;
if(count==0) {
avg = -1;
}
Serial.println(sum);
Serial.println(count);
Serial.println(avg);
if(avg < 30) postJson();
sum = 0;
count = 0;
nextUpdate = millis() + 60000;
}
}
void setup_wifi() {
// We start by connecting to a WiFi network
Serial.println();
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());
}
void postJson()
{
//POST json
const size_t capacity = JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(3);
DynamicJsonDocument doc(capacity);
char Buffer[512];
doc["agent"] = "Meeting Person";
JsonArray metrics = doc.createNestedArray("metrics");
JsonObject metrics_0 = metrics.createNestedObject();
metrics_0["name"] = "test";
metrics_0["namespace"] = "16F";
JsonObject metrics_0_data_point = metrics_0.createNestedObject("data_point");
metrics_0_data_point["value"] = avg;
serializeJson(doc, Buffer, sizeof(Buffer));
HTTPClient http;
http.begin("https://gw.machinist.iij.jp/endpoint");
http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", "Bearer YOUR_API_KEY");
int httpCode = http.POST(Buffer);
Serial.print(httpCode);
M5.Lcd.println(httpCode);
}
#include <M5StickC.h> #include <WiFi.h> #include <ArduinoJson.h> #include <HTTPClient.h> #include <ssl_client.h> #include <WiFiClientSecure.h> const char* ssid = "YOUR_SSID"; const char* passwd = "YOUR_PASSWORD"; HardwareSerial serial_ext(2); static const int RX_BUF_SIZE = 20000; static const uint8_t packet_begin[3] = { 0xFF, 0xD8, 0xEA }; unsigned long nextUpdate = 0; unsigned long sum=0; unsigned long count=0; float avg = 0.0; uint8_t person[1]; void setup() { M5.begin(); M5.Lcd.setRotation(3); M5.Lcd.setCursor(0, 30, 4); M5.Lcd.println("m5stick_uart_wifi_converter"); setup_wifi(); serial_ext.begin(115200, SERIAL_8N1, 32, 33); } void loop() { M5.update(); if (serial_ext.available()) { uint8_t rx_buffer[3]; int rx_size = serial_ext.readBytes(rx_buffer, 3); if (rx_size == 3) { //packet receive of packet_begin if ((rx_buffer[0] == packet_begin[0]) && (rx_buffer[1] == packet_begin[1]) && (rx_buffer[2] == packet_begin[2])) { serial_ext.readBytes(person,1); sum += person[0]; count += 1; } } } // send once a minute if ( nextUpdate < millis() ) { avg = float(sum) / count; if(count==0) { avg = -1; } Serial.println(sum); Serial.println(count); Serial.println(avg); if(avg < 30) postJson(); sum = 0; count = 0; nextUpdate = millis() + 60000; } } void setup_wifi() { // We start by connecting to a WiFi network Serial.println(); 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()); } void postJson() { //POST json const size_t capacity = JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(3); DynamicJsonDocument doc(capacity); char Buffer[512]; doc["agent"] = "Meeting Person"; JsonArray metrics = doc.createNestedArray("metrics"); JsonObject metrics_0 = metrics.createNestedObject(); metrics_0["name"] = "test"; metrics_0["namespace"] = "16F"; JsonObject metrics_0_data_point = metrics_0.createNestedObject("data_point"); metrics_0_data_point["value"] = avg; serializeJson(doc, Buffer, sizeof(Buffer)); HTTPClient http; http.begin("https://gw.machinist.iij.jp/endpoint"); http.addHeader("Content-Type", "application/json"); http.addHeader("Authorization", "Bearer YOUR_API_KEY"); int httpCode = http.POST(Buffer); Serial.print(httpCode); M5.Lcd.println(httpCode); }
#include <M5StickC.h>
#include <WiFi.h>
#include <ArduinoJson.h>
#include <HTTPClient.h>
#include <ssl_client.h>
#include <WiFiClientSecure.h>
const char* ssid = "YOUR_SSID";
const char* passwd = "YOUR_PASSWORD";

HardwareSerial serial_ext(2);

static const int RX_BUF_SIZE = 20000;
static const uint8_t packet_begin[3] = { 0xFF, 0xD8, 0xEA };
unsigned long nextUpdate = 0;
unsigned long sum=0;
unsigned long count=0;
float avg = 0.0;
uint8_t person[1];

void setup() {
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.setCursor(0, 30, 4);
  M5.Lcd.println("m5stick_uart_wifi_converter");

  setup_wifi();

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

void loop() {
  M5.update();

  if (serial_ext.available()) {
    uint8_t rx_buffer[3];
    int rx_size = serial_ext.readBytes(rx_buffer, 3);
    if (rx_size == 3) {   //packet receive of packet_begin
      if ((rx_buffer[0] == packet_begin[0]) && (rx_buffer[1] == packet_begin[1]) && (rx_buffer[2] == packet_begin[2])) {

        serial_ext.readBytes(person,1);
        sum += person[0];
        count += 1;
      }
    }
  }
  // send once a minute
  if ( nextUpdate < millis() ) {
    avg = float(sum) / count;
    if(count==0) {
      avg = -1;
    }
    Serial.println(sum);
    Serial.println(count);
    Serial.println(avg);
    if(avg < 30) postJson();
    sum = 0;
    count = 0;
    nextUpdate = millis() + 60000;
  }
}

void setup_wifi() {
  // We start by connecting to a WiFi network
  Serial.println();
  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());
}

void postJson()
{
  //POST json
  const size_t capacity = JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(3);
  DynamicJsonDocument doc(capacity);
  char Buffer[512];

  doc["agent"] = "Meeting Person";

  JsonArray metrics = doc.createNestedArray("metrics");

  JsonObject metrics_0 = metrics.createNestedObject();     
  metrics_0["name"] = "test";
  metrics_0["namespace"] = "16F";
  JsonObject metrics_0_data_point = metrics_0.createNestedObject("data_point");
  metrics_0_data_point["value"] = avg;

  serializeJson(doc, Buffer, sizeof(Buffer));

  HTTPClient http;
  http.begin("https://gw.machinist.iij.jp/endpoint");     
  http.addHeader("Content-Type", "application/json");
  http.addHeader("Authorization", "Bearer YOUR_API_KEY");
  int httpCode = http.POST(Buffer);

  Serial.print(httpCode);
  M5.Lcd.println(httpCode);
}

 

機材の設置

M5StickV と M5StickC を Grove ケーブルで接続します。
M5StickV のカメラの画角を広げるため、百均で購入した広角レンズを装着しています。
今回は会議スペースにあるモニタ上の隅に設置しました。

データを確認する

Machinist で受信したデータを確認してみます。
計算で出した平均値をグラフにプロットしているため、整数ではない値となっていますが、その時間の人数の目安を知るという意味では十分かなと思います。

 

苦労したこと

稼働させたまま数日間に渡り放置していると M5StickC がフリーズすることがありました(原因の切り分けはできていません)。M5StickC がフリーズした場合には、Machinist の死活監視機能を使って状態を察知し、再起動することで対処できるようになりました。

また、M5StickVのカメラを狙った方向に向けて設置、固定するのが難しかったです。
角度調整のしやすい三脚のようなものを用意できればよかったかもしれません。

おわりに

手軽に始めることができるので、皆さんもよかったら試してみてください。
(安いコストで機材を揃えられます。Machinist も無料で使うことができます。)

IIJ Engineers blog読者プレゼントキャンペーン
  • Twitterフォロー&条件付きツイートで「IoT米」を抽選で20名にプレゼント!
    応募期間は2020/12/01~2020/12/31まで。詳細はこちらをご覧ください。
    今すぐツイートするならこちら→ フォローもお忘れなく!

浮田 悠太

2020年12月18日 金曜日

IIJ IoTビジネス事業部 新規事業推進課に所属。webサービス開発・運用を担当しており、現在は Machinist の開発に従事している。原動力は甘いもの。

Related
関連記事