fc2ブログ

M5Stack UnitV2 AIカメラで遊ぶ(1)

 最近、安価なカメラモジュールやマイコンを使って、画像処理や手軽なAI判定ができるデバイスを物色しています。直近のブログで「M5Stack Core2とカメラ」や「SONY Neural Network Console」の話をしたのもその流れです。その中で、M5Stack UnitV2 AIカメラというものを見つけました。2021年5月末に発売されていたようですね。すでに乗り遅れた感がありますが、早速購入して味見をしてみました。スイッチサイエンスで注文するつもりでしたが、当日(9/20)間が悪くホームページのリニューアルを夜遅くまで行っており、発注ができなかったので、待ちきれずに、秋月で購入しました・・。三連休(9/23-9/25)にゆっくり触りたかったので、急ぎの発注でした。

 届いたUnitV2はRaspberry Pi Zero、Raspberry Pi Picoよりも小型の筐体でした。M5StackUnitV2_1_220925.pngOSはすでにLinuxがインストールされています。M5StackUnitV2_2_220925.png中身は本体とUSBケーブルと本体を三脚に固定する治具の三点セットです。M5StackUnitV2_3_220925.pngこの他に何も情報がなかったので、ネットでセットアップ方法の書かれたページを見つけて、作業を進めました。Mac上の作業環境を構築しました。M5StackUnitV2_setup_220925.png ダウンロードしたpkgファイルが開かずにインストールできなかったですが、同添付のpdfファイル(SR9900_mac_os_11_install_guide.pdf)に解決方法があり、SIP(System Integrity Protection:システム整合性保護)を無効にする必要がありました。MaxOS El Capitanから追加された機能ですね。怪しいドライバやソフトのインストールからPCを保護する機能ですね。M5Stackは中華製なのでセキュリティは大丈夫かなと一瞬躊躇いましたが、自己責任?で先に進みました。

 Macへのドライバのインストールを終え、UnitV2本体とMacをUSBケーブルでつなぎました。本体からはFANの回転音が聞こえてきました。Macのブラウザ上から「10.254.239.1」のIPアドレスを入力すると、UnitV2の操作画面ページが出てきました。さまざまな機能を手っ取り早く確認するメニューです。このメニューから「Camera Stream」を押すとカメラ画像が表示されました。M5StackUnitV2_run1_220925.pngカメラアイコンを押すと、「GALLERY」に画像が保存されました。M5StackUnitV2_run2_220925.png「Code Detector」を押すとQRコードを認識しました。M5StackUnitV2_run3_220925.pngまた、「Object Recognition」を押すと、AIで物体認識をしてくれます。犬は認識OKです。M5StackUnitV2_run4_220925.png次の画像を確認しましたが、象を馬と誤認識しました。デフォルトで入っているAIエンジンが貧弱なのでしょう。yoloを組み込めるようですが、これは次回以降に確認したいと思います。M5StackUnitV2_run5_220925.png 多機能そうなので、何かとしばらく遊べそうです。
スポンサーサイト



台風14号近づく

 今日(9/19)は台風14号が近づくので外出もせずに自宅でゆっくりしていました。風も時折強く吹くので外に出ない方が無難でした。ただ、関西地方には夜中頃に接近しますので、さっさと風呂に入って寝た方がいいかも知れませんね。博多の実家の方は台風は通過後で、風は強かったが、家と今にも壊れそうな車庫も無事で、まずは一安心です。

 すでにJRは今日の夕方以降全線不通です。明日(9/20)の早朝は運行取りやめがアナウンスされていますが、通勤時間帯にJRが動かないなら在宅勤務に変更しなくてはなりません。昼から出勤しても効率が悪いので。ただ、現在、勢力も弱まりつつあり、進行速度も速くなっているようなので、明日になってみないと結局分からないですね。
JR_220919.png
 自宅で温湿度・気圧センサのBME280を使ってデータを取り「IoTデータの可視化サービス」のAmbientにデータを上げていることは以前のブログでお話ししました。Raspberry Pi2(今でも現役)を使ったシステムです。DataLoggerToAmbient_220919.png久々にデータを確認すると、無事に動いていました。気圧が台風が近づくにつれて徐々に下がっていることが分かりました。終日頭痛がするのはそのせいかも??AtmosphericPressure_220919.png
 今、外は風もなく穏やかです。昨日、庭のダイダイの木にアマガエルが休憩していました。ダイダイも現在8個の青い実がなっています。frog_220918.png台風通過後にアマガエルもダイダイも無事であることを祈ります・・。

SONY Neural Network Consoleで遊ぶ(1)

 台風14号が近づきつつある3連休の午後ですが、外出もできないので、自宅で溜まっていたブログネタを掃き出そうと思います。

 アンチS○NY(○で隠されていない・・・笑)の私ですが、超高価なSONY製のIoTデバイスのSPRESENSEを触っている中で、SONYのAIのNeural Network Console(以下、NNC)というものがあることを知り、早速興味本位で触ってみました。いつもの衝動的な発作です。
 画像AIは本格的にやったことがなく、NNモデルを作って最適化するのが難儀なイメージがあります。最近は楽にNNを組めるソフトウェアがあるような話をよく聞きますが、NNCもその類なのか?分からない状態で、まずは触ってみました。

 NNCはWindowsのアプリで、クラウド版とデスクトップ版があります。クラウド版は一定処理数を超えると有償になるので、後者のデスクトップ版を使いました。また、私のメインPCはMac miniなので、Parallels Desktopから起動させたWindows11上で実行させました。インストールとアプリのアクティベートの際に、GoogleアカウントかSONYアカウントが必要になりますが、前者で対応しました。インストール手順はネット上に記載がありますので割愛します。

 今回、画像AIを行う題材は以前のブログでお話しした「手書きのひらがな」の識別にしました。「あ」「い」「う」の3文字について、「あ-い」「あ-う」「い-う」のそれぞれを分類させました。各画像は48×48[pixels]のサイズで、8ビットのモノクロ画像です。

 新しいプロジェクトを作成し、編集画面左側のコンポーネント(部品)をダブルクリックすることで、NNモデルのフローを作成します。NNC_220919_1.png細かいパラメータは左下の「レイヤープロパティ」で編集します。このフローとプロパティは人が行わなくてはならない部分で、全自動というわけには行きません。コードを書かなくて良いとは言え、AIの基礎的な知識はそれなりに必要と思います。今回は上記のフロー(モデル)にしましたが、AIに関する知識があまりないので、最適か否かは分かりません・・。

 次に「データセット」を押して、Inputに入れるデータセットの設定を行います。NNC_220919_2.pngここでは、TrainingとValidation(Test)のデータ画像を設定しますが、それぞれの画像はTrainとTestフォルダにまとめておきます。今回はTrain画像300枚、Test画像100枚としました。その上で、以下の通り、Train/Testではどの画像ファイルを読み込み、その画像の分類(判定)結果も書いておきます。NNC_220919_3.png データの準備ができれば、編集画面で実行ボタンを押します。学習画面に切り替えると、実行中の学習曲線などをウォッチできます。NNC_220919_4.png学習が終われば、学習画面の右上には評価の実行ボタンがありますので、それを押します。NNC_220919_5.png評価結果の出力を確認します。NNC_220919_6.pngまた、混合行列を確認すると、Test画像の100枚、完全に分類できていることが分かりました。NNC_220919_7.pngちなみに、「あ-う分類」「い-う分類」の結果は以下の通りでした。NNC_220919_8.pngNNC_220919_9.png 最後に作成したNNモデルはPythonコードにエクスポート可能でした。こんな感じです。
import nnabla as nn
import nnabla.functions as F
import nnabla.parametric_functions as PF

def network(x, y, test=False):
# Input:x -> 1,48,48
# Affine -> 100
h = PF.affine(x, (100,), name='Affine')
# Tanh
h = F.tanh(h)
# Affine_2 -> 1
h = PF.affine(h, (1,), name='Affine_2')
# Sigmoid
h = F.sigmoid(h)
# BinaryCrossEntropy
h = F.binary_cross_entropy(h, y)
return h
 今回初めて、NNCを触りましたが、GUIとしては使いやすいのかも知れませんが、AIの知識をある程度知っている人で、コードも書くような人向けの確認ツールであるような気がしました。また、暇を見つけて触ろうかな・・?。

M5Stack Core2とカメラで遊ぶ(1)

 以前のブログでSONYのSPRESENSE HDRカメラボードのカメラ画像をSDカードに保存する話をしました。このカメラ画像をLCDに映したいのですが、推奨の「LCD+2SW kit for SPRESENSE」を追加で購入するのも癪ですね。カメラボードを購入以来、その後メインボード、拡張ボードを買い足して、さらにLCD基板を購入しなくてはならないなんて、SONY商法?(個人の意見です・・)に振り回されたくないので。
 そこで、手持ちのM5Stack Core2のLCDを使えないかなと思い、前段階で実験したのが今回のお話です。

 今回は、まず第一段階として、単純にM5Stack Core2のSDカードに保存されている画像をLCD(320×240)に表示させました。プログラムは以下のような単純なものです。
#include <M5Core2.h>

void setup() {
M5.begin();
M5.Lcd.clear();
M5.Lcd.drawJpgFile(SD, "/LePenseur.jpg");
}

void loop() {
// put your main code here, to run repeatedly:
}
結果はこんな感じです。問題なく実行できました。ImageOutput_220904.png 次に、第二段階として、カメラの画像をM5Stack Core2のLCDに表示させました。カメラは一足飛びにSONYカメラではなく、3年ほど前に購入して使用していなかった?「Grove-シリアルカメラキット」を使いました。
 サンプルプログラムブログ記事を参考に、以下のコードを実行させました。
#include <M5Core2.h>

#define PIC_MAX_SIZE 10000
#define PIC_PKT_LEN 128 //data length of each read, dont set this too big because ram is limited
#define PIC_FMT_VGA 7
#define PIC_FMT_CIF 5
#define PIC_FMT_OCIF 3
#define CAM_ADDR 0
#define PIC_FMT PIC_FMT_CIF

File myFile;

HardwareSerial CAM_SERIAL(1);

const byte cameraAddr = (CAM_ADDR << 5); // addr
unsigned long picTotalLen = 0; // picture length
int picNameNum = 0;

void setup() {
M5.begin();

CAM_SERIAL.begin(9600, SERIAL_8N1, 33, 32); // GROVE端子の場合

initialize();
preCapture();
}

void loop() {
Capture();
GetData();
}

void clearRxBuf()
{
while (CAM_SERIAL.available())
{
CAM_SERIAL.read();
}
}

void sendCmd(char cmd[], int cmd_len)
{
for (char i = 0; i < cmd_len; i++) CAM_SERIAL.write(cmd[i]);
}

int readBytes(char *dest, int len, unsigned int timeout)
{
int read_len = 0;
unsigned long t = millis();
while (read_len < len)
{
while (CAM_SERIAL.available()<1)
{
if ((millis() - t) > timeout)
{
return read_len;
}
}
*(dest+read_len) = CAM_SERIAL.read();
// Serial.write(*(dest+read_len));
read_len++;
}
return read_len;
}

void initialize()
{
char cmd[] = {0xaa,0x0d|cameraAddr,0x00,0x00,0x00,0x00} ;
unsigned char resp[6];

Serial.print("initializing camera...");

while (1)
{
sendCmd(cmd,6);
if (readBytes((char *)resp, 6,1000) != 6)
{
Serial.print(".");
continue;
}
if (resp[0] == 0xaa && resp[1] == (0x0e | cameraAddr) && resp[2] == 0x0d && resp[4] == 0 && resp[5] == 0)
{
if (readBytes((char *)resp, 6, 500) != 6) continue;
if (resp[0] == 0xaa && resp[1] == (0x0d | cameraAddr) && resp[2] == 0 && resp[3] == 0 && resp[4] == 0 && resp[5] == 0) break;
}
}
cmd[1] = 0x0e | cameraAddr;
cmd[2] = 0x0d;
sendCmd(cmd, 6);
//Serial.println("\nCamera initialization done.");
}

void preCapture()
{
char cmd[] = { 0xaa, 0x01 | cameraAddr, 0x00, 0x07, 0x00, PIC_FMT };
unsigned char resp[6];

while (1)
{
clearRxBuf();
sendCmd(cmd, 6);
if (readBytes((char *)resp, 6, 100) != 6) continue;
if (resp[0] == 0xaa && resp[1] == (0x0e | cameraAddr) && resp[2] == 0x01 && resp[4] == 0 && resp[5] == 0) break;
}
}

void Capture()
{
char cmd[] = { 0xaa, 0x06 | cameraAddr, 0x08, PIC_PKT_LEN & 0xff, (PIC_PKT_LEN>>8) & 0xff ,0};
unsigned char resp[6];

while (1)
{
clearRxBuf();
sendCmd(cmd, 6);
if (readBytes((char *)resp, 6, 100) != 6) continue;
if (resp[0] == 0xaa && resp[1] == (0x0e | cameraAddr) && resp[2] == 0x06 && resp[4] == 0 && resp[5] == 0) break;
}
cmd[1] = 0x05 | cameraAddr;
cmd[2] = 0;
cmd[3] = 0;
cmd[4] = 0;
cmd[5] = 0;
while (1)
{
clearRxBuf();
sendCmd(cmd, 6);
if (readBytes((char *)resp, 6, 100) != 6) continue;
if (resp[0] == 0xaa && resp[1] == (0x0e | cameraAddr) && resp[2] == 0x05 && resp[4] == 0 && resp[5] == 0) break;
}
cmd[1] = 0x04 | cameraAddr;
cmd[2] = 0x1;
while (1)
{
clearRxBuf();
sendCmd(cmd, 6);
if (readBytes((char *)resp, 6, 100) != 6) continue;
if (resp[0] == 0xaa && resp[1] == (0x0e | cameraAddr) && resp[2] == 0x04 && resp[4] == 0 && resp[5] == 0)
{
if (readBytes((char *)resp, 6, 1000) != 6)
{
continue;
}
if (resp[0] == 0xaa && resp[1] == (0x0a | cameraAddr) && resp[2] == 0x01)
{
picTotalLen = (resp[3]) | (resp[4] << 8) | (resp[5] << 16);
Serial.print("picTotalLen:");
Serial.println(picTotalLen);
break;
}
}
}
}

void GetData()
{
unsigned int pktCnt = (picTotalLen) / (PIC_PKT_LEN - 6);
if ((picTotalLen % (PIC_PKT_LEN-6)) != 0) pktCnt += 1;

char cmd[] = { 0xaa, 0x0e | cameraAddr, 0x00, 0x00, 0x00, 0x00 };
unsigned char pkt[PIC_PKT_LEN];

char picName[] = "/pic00.jpg";
picName[4] = picNameNum/10 + '0';
picName[5] = picNameNum%10 + '0';

if(SD.exists(picName)){
SD.remove(picName);
}

myFile = SD.open(picName, FILE_WRITE);
if(!myFile){
Serial.println("myFile open fail...");
}
else{
Serial.setTimeout(1000);

for (unsigned int i = 0; i < pktCnt; i++)
{
cmd[4] = i & 0xff;
cmd[5] = (i >> 8) & 0xff;

int retry_cnt = 0;
retry:
delay(10);
clearRxBuf();
sendCmd(cmd, 6);
uint16_t cnt = readBytes((char *)pkt, PIC_PKT_LEN, 200);

unsigned char sum = 0;
for (int y = 0; y < cnt - 2; y++)
{
sum += pkt[y];
}
if (sum != pkt[cnt-2])
{
if (++retry_cnt < 100) goto retry;
else break;
}

myFile.write((const uint8_t *)&pkt[4], cnt-6);
}

cmd[4] = 0xf0;
cmd[5] = 0xf0;
sendCmd(cmd, 6);
}

myFile.close();
Serial.println(picName);

M5.Lcd.clear();
M5.Lcd.drawJpgFile(SD, picName);

picNameNum++;
}
実験風景はこんな感じです。CameraCapture_220904.png結果画像は以下の通り。レンズのネジ部を回しつつ、ピントの合う所まで調整しました。CameraOutput_220904.png次のステップは本題のSONYカメラの映像をM5Stack Core2のLCDに取り込みたいと思います。頑張ろっと。


ご訪問者数

(Since 24 July, 2016)

タグクラウド


プロフィール

Dr.BobT

Author: Dr.BobT
興味のおもむくままに生涯考え続けるエンジニアでありたい。

月別アーカイブ

メールフォーム

名前:
メール:
件名:
本文: