M5UnitV2のBuilt-in FunctionをM5Core2から切り替えて、推論結果をLcdに描画する
M5UnitV2とは
M5Stack社が販売している小型のAIカメラです。
顔認識やコード検出など最初から12個のBuit-inのAI Fuctionが動くようになっています。
Linuxが動くのでSSHでログインしたりすることも可能です。
Sigmstar SSD202D
Dual Cortex-A7 1.2Ghz Processor
128MB DDR3
512MB NAND Flash
GC2145 1080P Colored Sensor
Microphone
WiFi 2.4GHz
M5UnitV2はデフォルト設定の状態で、USBをPCとつないだ状態でブラウザから10.254.239.1と入力すると、Built-in FunctionがWeb UIで操作できます。
Built-in Functionはこの12種類です。
- Audio FFT
- Code Detector
- Face Detector
- Lane Line Tracker
- Motion Tracker
- Shape Matching
- Camera Stream
- Online Classifier
- Color Tracker
- Face Recognition
- Target Tracker
- Shape Detector
- Object Recognition
試してみると顔認識は認識率高かったりして、Built-in Functionでも結構遊べそうです。
また、M5UnitV2はAI機能の推論結果をシリアル通信で送信してくれます。
そこで機能の理解を兼ねて、M5Core2でこの推論結果の受信して、Lcd上に描画することをやってみました。描画にはLovyanGFXを使っています。
例えば顔認識した結果をM5Core2に描画した時の動画です。
左のWebアプリと同じようにM5Core2のLcd上に顔枠(緑)と顔パーツ位置(オレンジ)を描画しています。
残念ながらシリアルで送信されるのは推論結果のみで、カメラ画はないので、これだけ描画しても直接何かの役に立つわけじゃないですが、これができれば何か実際にM5UnitV2使ったアプリケーションを作るときにやりやすいかと思いやってみました。
ソースコード
Arduinoのソースはこちらに置いてあります。
さすがに全機能は実施しておらず、今実装しているのはこの機能だけです。
- Code Detector
- Face Detector
- Target Tracker
- Object Recognition
M5UnitV2からのシリアルデータ(JSON)の受信
M5UnitV2のBuilt-in Functionのシリアル通信についてはこちらのサイトに詳しく記載されています。Functionの出力結果はJSONで出力されています。
例えば顔検出の結果は
{
"num":2,
"face":[
{
"x":85.92229462,
"y":140.5598145,
"w":143.0657349,
"h":169.97229,
"prob":0.995571315,
"mark":[
{
"x":134,
"y":192
},
{
"x":198,
"y":214
},
{
"x":160,
"y":238
},
{
"x":118,
"y":252
},
{
"x":172,
"y":270
}
]
},
{
"x":416.109375,
"y":95.21626282,
"w":146.4984131,
"h":180.2038422,
"prob":0.986289799,
"mark":[
{
"x":446,
"y":160
},
{
"x":510,
"y":156
},
{
"x":472,
"y":194
},
{
"x":456,
"y":230
},
{
"x":504,
"y":226
}
]
}
],
"running":"Face Detector"
}
顔枠の位置情報、顔のパーツの位置情報が入っています。
これをパースして処理します。パースについてはあとで説明します。
M5UnitV2とM5Core2はGroveコネクタで接続します。
受信したデータは1回の推論結果単位で最後改行ありで送られてくるので、改行まで来たら1つのデータとしてqueueに保存します。シリアルはHardwareSerial の1を指定します。
HardwareSerial SerialPortA(1);
bool recvUart(std::string &rs) {
int32_t recv_size = SerialPortA.available();
if (recv_size > 0) {
char c;
for (int i = 0; i < recv_size; i++) {
c = (char)SerialPortA.read();
rs += c;
}
return true;
} else {
return false;
}
}
void loop(void) {
//省略
// serial communication
if (recvUart(recv_uart_str)) {
if (recv_uart_str.find("\n") != std::string::npos) {
Serial.printf("%s", recv_uart_str.c_str());
uv2drawer.pushEvent(recv_uart_str);
recv_uart_str.clear();
}
if (recv_uart_str.size() >= 3000) {
Serial.println("recv size over");
recv_uart_str.clear();
}
}
if (recv_uart_str.find("\n") != std::string::npos) {
で改行を見つけたら、描画用のClass uv2drawerに文字列をpushしています。
受信したJSONのパース
JSONのパース(Deserialize)にはArudinoJSON ライブラリを使用します。
ArduinoJSONには、JSONを入力するとパース用のコードを出力するアシスタント機能があります。
これに先ほどの顔検出のJSONを入力すると必要なサイズやコードを出力してくれます。
// Stream& input;
StaticJsonDocument<1024> doc;
DeserializationError error = deserializeJson(doc, input);
if (error) {
Serial.print(F("deserializeJson() failed: "));
Serial.println(error.f_str());
return;
}
int num = doc["num"]; // 2
for (JsonObject face_item : doc["face"].as<JsonArray>()) {
double face_item_x = face_item["x"]; // 85.92229462, 416.109375
double face_item_y = face_item["y"]; // 140.5598145, 95.21626282
double face_item_w = face_item["w"]; // 143.0657349, 146.4984131
double face_item_h = face_item["h"]; // 169.97229, 180.2038422
double face_item_prob = face_item["prob"]; // 0.995571315, 0.986289799
for (JsonObject face_item_mark_item : face_item["mark"].as<JsonArray>()) {
int face_item_mark_item_x = face_item_mark_item["x"]; // 134, 198, 160, 118, 172
int face_item_mark_item_y = face_item_mark_item["y"]; // 192, 214, 238, 252, 270
}
}
const char* running = doc["running"]; // "Face Detector"
これをもとにパースする関数を用意します。
void parseJsonFaceDetector() {
int num = doc["num"]; // 2
const char *running = doc["running"]; // "Face Detector"
uv2drawer.clearFullScreen();
for (JsonObject face_item : doc["face"].as<JsonArray>()) {
FaceDetector face;
double face_item_x = face_item["x"]; // 87.22392273, 298.6412659
double face_item_y = face_item["y"]; // 99.36726379, 47.6712265
double face_item_w = face_item["w"]; // 96.59121704, 95.04016113
double face_item_h = face_item["h"]; // 125.2233276, 121.9224625
double face_item_prob = face_item["prob"]; // 0.997230828, 0.992135584
face.x = face_item_x;
face.y = face_item_y;
face.w = face_item_w;
face.h = face_item_h;
face.prob = face_item_prob;
for (JsonObject face_item_mark_item :
face_item["mark"].as<JsonArray>()) {
int face_item_mark_item_x =
face_item_mark_item["x"]; // 114, 154, 140, 124, 156
int face_item_mark_item_y =
face_item_mark_item["y"]; // 148, 140, 170, 194, 188
FaceMark mark = {face_item_mark_item_x, face_item_mark_item_y};
face.mark.push_back(mark);
}
uv2drawer.drawFaceDetector(face);
}
uv2drawer.updateScreen();
}
これでJSONを扱えるデータに変換できました。
描画処理
パースしたJSONを使ってLcd上に顔枠を描画します。
上のparseJsonFaceDetector()にあるFaceDetectorは描画用の構造体です。
struct FaceMark {
int32_t x;
int32_t y;
};
struct FaceDetector {
float x;
float y;
float w;
float h;
float prob;
std::vector<FaceMark> mark;
};
これをuv2drawer.drawFaceDetector(face)
に渡してLovyanGFXの描画関数に渡します。
void UV2Drawer::drawFaceDetector(FaceDetector &face) {
canvas->drawRect(convLcdRate(face.x), convLcdRate(face.y),
convLcdRate(face.w), convLcdRate(face.h), PALETTE_GREEN);
const int32_t MARK_HALF_LEN = convLcdRate(face.w) / MARK_DIV_RATE;
for (auto m : face.mark) {
canvas->drawLine(convLcdRate(m.x) - MARK_HALF_LEN, convLcdRate(m.y),
convLcdRate(m.x) + MARK_HALF_LEN, convLcdRate(m.y),
PALETTE_ORANGE);
canvas->drawLine(convLcdRate(m.x), convLcdRate(m.y) - MARK_HALF_LEN,
convLcdRate(m.x), convLcdRate(m.y) + MARK_HALF_LEN,
PALETTE_ORANGE);
face.mark.pop_back();
}
canvas->setTextSize(0.5);
canvas->setCursor(convLcdRate(face.x),
convLcdRate(face.y) - canvas->fontHeight());
// canvas->setTextColor(PALETTE_WHITE, PALETTE_GREEN);
canvas->setTextColor(PALETTE_GREEN);
canvas->printf("%.2f", face.prob);
}
顔1つずつdrawFaceDetector()
を呼び、最後にまとめて、uv2drawer.updateScreen()
で描画面を更新しています。
void UV2Drawer::updateScreen() { canvas->pushSprite(0, 0); }
LovyanGFXの使い方はこちらのサイトが参考になります。
これでM5UnitV2から送られる顔枠とパーツ位置情報をもとに描画することが出来ました。
Functionの切り替え
M5UnitV2から送信するデータは推論結果ですが、Built-in Functionを切り替えるコマンドも用意されています。
そちらもこのドキュメントに詳細載っています。
例えば顔検出に切り替えるときはこのjsonを送ります。argsは空でも必要です。
です。(ないとエラーでM5UnitV2側ではじかれます。)
{
"function":"Face Detector",
"args":[
""
]
}
Object Recognitionの場合はargsにyolo_20classなどと設定します。
{
"function":"Object Recognition",
"args":[
"yolo_20class"
]
}
切り替え関数はこのようにしています。
void UV2FuncSwitcher::sendSwitchCommand(std::string func_name,
HardwareSerial &serial) {
if (func_name.empty()) return;
doc["function"] = func_name.c_str();
// check functions name is in list
if (func_name == "Object Recognition") {
doc["args"][0] = "yolo_20class";
} else {
doc["args"][0] = "";
}
std::string json;
serializeJson(doc, json);
serial.println(json.c_str());
}
これもArudinoJSONを使ってSerializeしてデータをシリアルで送信します。
細かいですが、serializeJsonで送信出来るんですが、最後改行が必要だったので、serial.println(json.c_str())
で送るようにしました。
具体的な操作としては左右のボタンでFunctionを選択して、中央ボタンでコマンド送信しています。
その後推論結果がM5UnitV2から送信されると描画処理を実行するようにしています。
さいごに
細かいことをいうと、パースする関数をFunction名を見て切り替えたりとか、Target Trackerは認識対象の枠設定が必要なので決め打ちで中央に枠を設定しているんですが、そこは説明割愛しています。興味があったらソースコードをみてください。
M5UnitV2はUnitとついているのはM5Stackの他のUnitモジュール(温湿度系など)のようにAIカメラとしてM5Stackとつないで使えるカメラという印象でした。
最初はBuilt-in Functionでも十分遊べると思うので、ちょっとお高いですが興味あったら使ってみてください。
余談:さらにLinuxで動いて中のFrameworkのソースコードも公開されているのでそれをいじって使うこともできます。
Discussion