PikaPikaLight

主に光モノ工作の忘備録

LEDテープでメビウスの輪を作った

1メートルのRGB LEDテープを点灯させて遊んでいたら、ふと思いついた。
「これでメビウスの輪を作ったら面白そう。」
作ってみました。

材料

LEDテープをメビウスの輪の形にする

まず最初にLEDテープについているごっついコネクタケーブルを外して、M5Atomに簡単に接続できるGroveケーブルに付けなおします。
f:id:PikaPikaLight:20210512201015p:plain まっすぐ半田付けすると切れやすいので、あえて逆方向につけて折り返すという小技を使っています。

メビウスの輪にするなら裏表にLEDが配置されるようにしないと意味ないので、テープを半分の長さで折り曲げます。最初は単純に折り曲げてしまおうと思ったのですが、このLEDテープはちょうど半分のところではんだ付けされていました。
f:id:PikaPikaLight:20210512201029p:plain
72個分のLEDがのっている基板を2枚つないで作られています。冷静に考えると、こんな長い基板どうやって作ってどうやって部品実装しているのだろう??

ハンダをいったん外して裏表にしてはんだ付けしなおします。基板の裏側にもランドがあるので、簡単でした。 両面テープで貼りつけましたが、シリコンチューブだと粘着が弱いので、ところどころセロテープでぐるぐる巻いてみました。 これをひとひねりしてメビウスの輪になるように端と端をつなぎます。セロテープだけでつなぐのは無理だったので、プラバンを補強板にしてセロテープで巻きました。
f:id:PikaPikaLight:20210512201048p:plain f:id:PikaPikaLight:20210512201105p:plain

点灯プログラム

メビウスの輪なのでLED1灯がエンドレスでぐるぐる回るような動きをさせたい。単色では味気ないので、レインボウカラーに変化しながら。
これをM5AtomLiteでプログラムするわけですが、、、
前から気付いていました、M5Stack(ESP32)でAdafruit_NeoPixelライブラリは完璧には動かない事を。 LEDの数が少ない場合はまぁまぁ動きますが、LEDの数が多いと誤動作が目立ちます。
これまでLEDの数が多い場合は別のマイコン(XIAOとか)を使うなどしてしのいでいましたが、この機に向き合うことにしました。

ネット情報からESP32ではFastLEDライブラリを使うといいらしい。 https://github.com/FastLED/FastLED
とりあえずこのライブラリをインストールし、スケッチ例を適当に開いて、ピン番号とLED数だけ変えてM5Atomで動かしてみると、ちゃんとそれらしい動きでLEDが点灯しました
スケッチ例のCylonを動かすとレインボウに変化する点灯動作をしたので、色の変化はこれをマネすれば行けそうです。 あとは1灯を順番に点灯させていくだけですが、メビウスの輪になっているので単純に0~143をつければいいわけではありません。 図に書いて見るとこんな感じになります。 f:id:PikaPikaLight:20210512201649p:plain この変則的な並びをいったん配列に入れておいて、loop()はなるべくシンプルになるようにしてみました。

#include <FastLED.h>

#define NUM_LEDS 144
#define DATA_PIN 26

#define DELAYVAL 20
#define BRIGHTNESS  200
CRGB leds[NUM_LEDS];

uint16_t array_conv[NUM_LEDS] ;

void setup() {

  LEDS.addLeds<WS2812, DATA_PIN, RGB>(leds, NUM_LEDS);
  LEDS.setBrightness(BRIGHTNESS);

  uint16_t j = 0;

  for (int i = 0; i < NUM_LEDS / 2; i++) {
    array_conv[j] = i;
    j++;
  }
  for (int i = NUM_LEDS - 1 ; i > NUM_LEDS / 2 - 1; i--) {
    array_conv[j] = i;
    j++;
  }
}

void loop() {
  static uint8_t hue = 0;
  for (int i = 0; i < NUM_LEDS; i++) {
    FastLED.clear();
    leds[array_conv[i]] = CHSV(hue++, 255, 255);
    FastLED.show();
    delay(DELAYVAL);
  }
}

虹色を作っているのは CHSV(hue++, 255, 255)
これでどうやってレインボウカラーの変化になるのか理解してません ^^;
FastLEDに関してはこれからじっくり触っていきたいと思います。 f:id:PikaPikaLight:20210512202836p:plain

TWELITE CUEとM5Atomで玄関のカギの開閉状態監視システムを作った

初めに

外出先で「玄関のカギかけたっけ?」となる心配性な私。鍵の状態をスマホでチェックすることができたら超便利だと思う。
Qrio みたいなスマートロックを付けるという方法もあるが、開け閉めまでできなくてもいい(なんか不安だし。そんな私は心配性)
単純にカギかかっているかだけを確認できる仕組みが欲しい。
今年のゴールデンウィークもどこにも行けそうになくてヒマそうなので、自作してみた。

設計図

うちの玄関ドアのカギは2か所についているので2つモニターする必要あり。 f:id:PikaPikaLight:20210506074431p:plain

開発環境、アプリバージョン

windows10
ArduinoIDE v1.8
TWE Programmer v3.7.5
App_Wings_MONOSTICK_BLUE_L1304_V1-1-4.bin

TWELITEについて

モノをつなぐ無線マイコンモジュール TWELITE-トワイライト - MONO-WIRELESS.COM
TWELITEはIEEE802.15.4(2.4GHz)規格の無線マイコンで、手軽に複数の子機の無線網を構築できるのが特徴です。
自分的には5年ほど前、まだモノワイヤレス株式会社に分社化される前の東京コスモス電機の時代にTWELITEをいくつか使用したことがありました。その時はTWELITE 2525(ニコニコ)という、コイン電池で動いて加速度が取れるモジュールを3Dプリンタのフィラメントロールにつけて、3Dプリンタが途中で止まっていないかをロールの回転による加速度変化でモニターして、止まったらアラームを出すといったことをしたりしてました。

今回、鍵のモニターも2525に磁気センサつければできるなと漠然と考えていて、久しぶりにTWELITEのホームページを見たところ、すごく進化していて、新製品がいっぱいでした。
そして2525はすでに新規採用非推奨品となっていたのですが、なんと後継機TWELITE CUEは磁気センサを搭載しているではないですか。モノワイヤレスさん素晴らしいです。
加速度、磁石センサータグ TWELITE CUE-トワイライトキュー | MONO-WIRELESS.COM

材料

製品名 価格 用途
M5Atom Lite 1287円 Wi-Fiでインターネット接続
TWELITE CUE(Blue) 2481円 x2台 無線子機(磁気センサ)
TWELITE DIP(Blue) 1980円 無線親機
TWELITE R2 1500円 TWELITEの設定、アプリ変更に必要
  • 価格は2021/5/5現在のものです。

  • TWELITEには標準出力のBLUEと高出力タイプのREDがあります。値段的にREDのほうが1.5倍くらい高いです。

  • 自分はTWELITE DIP(旧製品の基板アンテナ型)とTWELITE R(2でなく旧型)とM5Atomを持っていたので、今回購入したのはTWELITE CUE2台だけです。

その他必要なもの

  • コイン型電池(CR2032) 2個:TWELITE CUE用
  • ブレッドボード:TWELITE DIPとM5Atom の接続用
  • マジックテープ:TWELITE CUEをドアに貼り付ける用

作り方

TWELITE CUEの設定

設定変更はインタラクティブモードで行います。 TWELITE CUEにTWELITE R2を接続し、TWELITE STAGE APPを使えばできるようです。その辺はホームページに詳しく記載されているので省略します。
自分の場合は旧製品であるTWELITE Rを使ったために、すんなりはいきませんでしたが、ちゃんと旧製品も使えるように詳細情報が記載されていました。モノワイヤレスさんのこういうところは信頼できてよいです。
USBアダプター TWELITE R-トワイライター - MONO-WIRELESS.COM
TWELITEプログラマというソフトを使うことで、TWELITE RでもCUEとの接続ができました。

肝心の設定内容ですが、変更するのは少なくとも4か所です。 f:id:PikaPikaLight:20210505193613p:plain
これが初期設定値ですが、変更するのは

a アプリケーションID
このIDが同じ端末同士で通信を行います。
初期値のままでもいいですが、もしもご近所でTWELITEを使っている人がいた場合、お互いに迷惑かけることになるので違う番号に変更したほうがいいでしょう。(もし近所で使っている人がいればお友達になりたいですが)
使える番号には制約があります。詳細はこちら
TWELITE APPS - インタラクティブモード - MONO-WIRELESS.COM

i 論理デバイスID
子機につけるナンバーです。同じセンサーの子機が複数あってもこの番号で個体識別できます。
今回は2台の子機を使うので、に設定しておきます。

t 送信間隔の設定
定期送信の送信間隔を秒単位で設定します。1〜4095の値で指定可能。
初期値は5なので5秒おきに通信されます。これはあくまで定期送信の話で、開閉センサーパルモードの場合、磁気センサの値に変化があった場合にも送信してくれます。なので定期送信は別になくてもよく、最大の4095秒(68分)でもいいのですが、 送信受信で取りこぼしが出る可能性を考えると60秒くらいが適当かと思います。(心配性アゲイン)

p センサ固有パラメータの設定
これは今回、開閉センサーパルモードを使うので、04000000を設定します。

設定変更後はコマンド Sで忘れずに保存しましょう。

TWELITE CUEの設置

f:id:PikaPikaLight:20210506075646p:plain

TWELITE DIPの設定

TWELITE DIPはまず親機用のアプリを書き込む必要があります。
書き込むのは親機・中継機アプリ(App_Wings)です。ホームページではMONOSTICK 用とありますが、TWELITE DIPに書き込んでも特に問題なく動きました。
TWELITE R2とTWELITE STAGE APPを使えば多分簡単にできることでしょう。

アプリを書き込んだらインタラクティブモードで設定変更をします
f:id:PikaPikaLight:20210505180033p:plain
これが初期設定値です。
変更するのは a アプリケーションIDだけです。CUEに設定した番号と同じものに変更します。

TWELITE DIPとM5AtomLiteの配線

Groveコネクタ使って配線できればよいのですが、TWELITEの電源は3.3VなのでGroveの5V電源は使えません。
M5Atomの背面のソケットピンを使うことになりますが、ブレッドボードを使うのが簡単です。(ワイヤーで結線してももちろんかまいません)
接続はわずか3ピンでOK。電源(3.3V)とGNDとTWELITEのUART TX→M5AtomのUART RXです。
f:id:PikaPikaLight:20210505204234p:plain f:id:PikaPikaLight:20210505204417p:plain

M5AtomLiteのプログラム

電文のデコード

まず受信電文を確認するために、単純に受信した値をそのままprintさせてみます。

#include "M5Atom.h"
void setup()
{
  M5.begin(true, false, true);
  Serial.begin(115200);
  Serial2.begin(115200, SERIAL_8N1, 21, 25);// RX=21 TX=25(TXは使わないので適当)
}
void loop()
{
  char c;
  if (Serial2.available() > 0 ) {
    c = Serial2.read();
    Serial.print(c);
  }
}

CUEに磁石をつけたり離したりして通信が来るのを確認します。
こんな電文が確認できました。

:800000004E0006810BFF090180810311300802099C1130010204A900000001026D42
:80000000480007810BFF090180810311300802099C1130010204A900000001005D59
:800000006C000B810BFF090180810311300802099C1130010204F70000000101B08F
:800000006F000C810BFF090180810311300802099C1130010204BA00000001818078

バイナリをアスキーにした電文が来ます。
電文の詳細はホームページに記載されています。パルアプリ - WINGS
必要な情報は3つ

  • 番号5 送信元の論理デバイスID

  • 番号d データ(子機の電池電圧(mV))

  • 番号n データ(磁気センサの状態 )
    0x00=近くに磁石がない
    0x01=N極が近い
    0x02=S極が近い
    0x80= 定期送信

    今回は磁気センサしか使っていないので、単純に:から始まる電文の文字数で必要なデータを取り出すことができます。
    余裕があれば拡張性がある作りにしたいところでしたが、今回はとりあえず決め打ちでプログラムしました。(とりあえずで作ったプログラムが直されることはほぼないが)

    IoTサービスへの考慮

    センサーからデータ受信するたびにIoTサービスへインターネット通信するのが最も簡単ですが、フリーで使えるIoTサービスはデータの更新間隔や一日のデータ数などに制約があることがほとんどです。
    とあるフリーのIoTサービスは1日3,000件(約30秒間隔)、データの送信は最低5秒あける必要があるとの条件でした。それを考えると単純に受信したら投げるという作りにはできません。その条件を満たす実装を考えます。
    基本的には1分程度に1回、定期的にその時の状態を送るでいいと思いますが、問題はカギが開き閉めされた時です。
    1分に1回だとその間の開き閉めを見逃す可能性があります。それは避けたい。 特にドアの上側のカギはメインで使われるので、ここが閉→開の動きをした場合は確実にデータアップしたい。
    そういった考えを盛り込んで最終的なプログラムをしました。

    コード

    Wi-Fi接続およびIoTサービスへのデータ送信に関してはあえて省いています。(あまりにプライベートなものなので) ここまで出来る人であればこの先のIoT化はできるでしょう。

#include "M5Atom.h"

#define SENSOR_NUM   2
#define OPEN    1
#define CLOSE   0
uint8_t door_lock[ SENSOR_NUM ] = {0};
uint16_t batt_vol[ SENSOR_NUM ] = {0};

unsigned long send_time = 0;
uint8_t door1_before = CLOSE;

void setup() {
  M5.begin(true, false, true);
  Serial.begin(115200);
  Serial2.begin(115200, SERIAL_8N1, 21, 25);// RX=21   TX=25
  send_time = millis();
}

void loop() {
  char str[70]; //受信用配列
  int str_len;
  unsigned long now_time;

  if (Serial2.available() > 0 ) {
    str_len = Serial2RecvStr(str);
    if (str_len != 69) {
      return;
    }

    //Serial.println(str);
    char id[] = {str[23], str[24], '\0'};
    char bat[] = {str[39], str[40], str[41], str[42], '\0'};
    char door[] = {str[64], '\0'};
    int id_num = atoi(id);
    int bat_mv = strtol(bat, 0, 16);
    int d_num = atoi(door);

    switch (id_num) {
      case 1:
        if (d_num == 0 ) {
          door_lock[0] = 0; //door close
          door1_before = CLOSE;
          M5.dis.drawpix(0, 0x000000);
        } else {
          door_lock[0] = 1; //door open
          M5.dis.drawpix(0, 0x00f000);//開いている場合赤LED点灯
          if (door1_before == CLOSE) {
            while (1) {
              now_time = millis();
              if (send_time > now_time) { //飽和対策
                send_time = 5000;
              }
              if (now_time - send_time > 5000) {
                send_mes();
                break;
              }
            }
          }
          door1_before = OPEN;
        }
        batt_vol[0] = bat_mv;
        break;
      case 2:
        if (d_num == 0 ) {
          door_lock[1] = 0; //door close
        } else {
          door_lock[1] = 1; //door open
        }
        batt_vol[1] = bat_mv;
        break;
    }
  }
  now_time = millis();
  if (send_time > now_time) { //飽和対策
    send_time = 60000;
  }
  if (now_time - send_time > 60000) {
    send_mes();
  }
}

void send_mes(void) {
  Serial.print("door1 = ");
  Serial.println(door_lock[0]);
  Serial.print("bat1 = ");
  Serial.println(batt_vol[0]);
  Serial.print("door2 = ");
  Serial.println(door_lock[1]);
  Serial.print("bat2 = ");
  Serial.println(batt_vol[1]);

/*
 ここにIoTサービスへのデータ送信を記述すればよい
*/

  send_time = millis();
}
int Serial2RecvStr(char *buf)
{
  int i = 0;
  char c;
  if (Serial2.available() > 0 ) {
    c = Serial2.read();

    if (i == 0 ) {
      if (c == ':') {
        buf[i] = c;
        i++;
      } else
        return 0;
    }

    while (1) {
      if (Serial2.available() > 0 ) {
        c = Serial2.read();
        if ( (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') ) {
          buf[i] = c;
          i++;
        } else {
          return 0; // ERROR
        }
        if (i == 69) {
          buf[i] = '\0';
          return i;
        }
      }
    }
  }
}

動作の様子

TWELITE DIPとM5AtomLiteの設置

電源はM5AtomのUSB端子で給電しますのでUSB電源ポートが必要です。
自分は最初、ドアから距離5mほど高さ2mほどにあるWi-Fiルータ(通称:カニルータ)のそばに置いてみたのですが、下側のカギにつけたセンサからの無線が不安定でした。上側のカギは問題なし。無線機の特性として地面に近いところに設置したものは通信距離が悪くなるので、多分そのせいだと思います。設置場所を変えてドアの近くに設置したら両方とも問題なく受信できました。Wi-Fiカニルータのおかげもあって余裕でつながりました。
ブレッドボードに挿してあるだけなので、テープで固定してタッパ的なケースに入れて完成としました。

最後に

今回はカギの状態だけを見ていますが、M5AtomのGroveコネクタも空いていることなので、何か他のセンサを追加して本格的なIoTをしてみるのも面白いと思っています。
今回のプログラミングはいわゆるゲートウェイを作るもので普段なじみがなく、我ながら不細工なコードだと思いますが、目標の期間で動くものができたのでよしとします。
TWELITE CUEの電池寿命はホームページページによると「1日に200回の開閉を行なった場合、約4年です。」とありますが、実際にどのくらい持つのか楽しみです。
稼働日:2021/5/4

立方体万華鏡をLEDテープを使ってピカピカさせてみた

f:id:PikaPikaLight:20210502151227j:plain

立方体万華鏡 CUMOS

ヤマザキミノリ氏 が発明された立方体万華鏡 CUMOS という、鏡を6面組み合わせて作る、とても幻想的な万華鏡があります。


私がCUMOSを最初に知ったのはこのツイートです。作り方の動画もありました。

こちらでは色を付けるのにマジックを使っていますが、これをカラーLEDでやったらキレイなんじゃないかと思って実際に作ってみました。

材料

作り方

まずはCUMOSを作成します。

CUMOSの作り方を紹介しているページがありますので、そちらを参考にして作成しました。
おうちで自由研究道場【立方体万華鏡】
国立大学56工学系学部ホームページ
こちらではマジックやセロハンで色を付けていますが、その代わりにLEDテープを使用します。


鏡に線傷をつけたところにWS2812B LEDテープ を貼ります。

WS2812B LEDテープ はハサミで好きなところをカットして分割できるようになっていますので、鏡の大きさに合わせてカットし、ワイヤーをはんだ付けして配線します。 こんな感じです。(雑な工作ですいません) f:id:PikaPikaLight:20210502154804p:plain M5Atom Lite への接続はGroveケーブルの片方を直接LEDテープ にはんだ付けしました。
f:id:PikaPikaLight:20210502160008j:plain
これでハードは完成です。 f:id:PikaPikaLight:20210502170204p:plain

コード

ArduinoでプログラムしてLEDテープを点灯させるわけですが、 Adafruit_NeoPixelライブラリのサンプルプログラムをそのまま使うのが簡単です。 ただし、M5Atomに使われているESP32マイコンだとAdafruit_NeoPixelライブラリは完璧には動作しません。LEDの数が多いと誤点灯します。今回は誤点灯してもそれほど影響ないので、このライブラリを使いました。 github.com githubからライブラリのZIPをダウンロードし、ArduinoIDEにてライブラリインクルードをします。
サンプルプログラムのstrandtestがいい感じの点灯パターンです。 f:id:PikaPikaLight:20210502161712p:plain

修正するのは2か所だけ。LED制御信号のピン番号とLEDの数の設定だけ直せばOKです。 LED_PIN はM5AtomのGroveポートの26番ピンを使っているので26に、
LED_COUNT は今回72個使っているので、そのように修正します。

// Which pin on the Arduino is connected to the NeoPixels?
// On a Trinket or Gemma we suggest changing this to 1:
//#define LED_PIN    6
#define LED_PIN    26

// How many NeoPixels are attached to the Arduino?
//#define LED_COUNT 60
#define LED_COUNT 72

プログラムを書きこめば完成です。
strandtestによる点灯パターンの様子はこちら

消費電流確認

WS2812B LEDテープを使用するときは消費電流に気を付ける必要があります。
strandtestプログラムで72個使った場合の消費電流を簡易的にUSB電流計で測ってみました。
M5Atomの分も含めて最大320mA@5V でした。結構大きいですが普通のモバイルバッテリーで給電可能な値でした。 これ以上だとちょっと気にしないといけないと思うので、プログラム改造するときはこれ以上明るくしないよう注意必要と思います。

ツイッターにあげた動画

UNIT-VでopenMV使ってIMEモード判別してみた

f:id:PikaPikaLight:20210430213308p:plain

初めに

ブラインドタッチができない人間(私)が日々悩まされている、「日本語モードだと思って打っていたら英数モードだった(その逆もしかり)問題」の対策として「あ」or「A」を判別させるAIカメラの作成に取り掛かった。
ファーストステップとしてM5StickVのV-Trainingで狙いの動作をさせることができた。詳細は以下の記事にて。 pikapikalight.hatenadiary.com

M5StickVにはLCDとバッテリーがついていて便利なのだが、今回やりたいことに対してはこの機能は不要、むしろいらない機能といえる。 今回の目的に対してはUNIT-Vを使うのが最適なので、セカンドステップとしてUNIT-Vで同じものを作ることにした。

エッジAIの挫折

M5StickVでは割とすんなりできたので、UNIT-Vでも簡単にできると思って始めたのだが、これが予想外の苦戦を強いられる。
V-Trainingは学習曲線を見るかぎり問題なく学習できていそうなのだが、そのKモデルをUNIT-Vにいれて動かすと正答率がすこぶる悪い。カメラの位置、角度をシビアに調整してやっと正解が出るといった具合で、使い物にならないレベル。
写真の撮り方、写真サイズ、ブライトネス、コントラスト、色々変えてみたりしたが、うまくいかず。果てはV-Trainingを使わず、自力で学習してモデル作成するということもやってみたが、それでもやっぱり実機動作で正答率悪し。 そのあたりのいきさつはこちら。

結局はAIによる判別はあきらめたのですが、Google Colaboratoryを使っての自力学習の方法など非常に得たものは大きかったので、詳細は別の記事にしたいと思います。

openMVに挑戦

色々と試している過程で必然的にMaixPyの機能について調べることとなったのだが、思っていた以上にいろいろな機能があることが分かった。openMVとやらの機能が使えて、フィルターをかけたり、特定の色の領域を判別させたりと、AI使わなくてもひょっとしてできるかもと思い始めた。
さらに調べるとこのような記事を発見 マーカーシールを使ってメーターの針の位置を読み取るというもの。このマーカーシールを使う方法で今回やりたいことを実現することができた。

概要

  • マーカーシールを「あ」の真下に貼る
  • sensor.set_auto_gain(False)  でオートゲインを無効にする
  • MaixPyのしきい値エディタでマーカーのしきい値と「あ」「A」のしきい値を確認
  • .find_blobsでマーカーシールの領域を特定
  • マーカーシールの領域情報から「あ」の領域を特定し、その領域を切り出す
  • 切り出した領域に対して.find_blobsで「あ」「A」の領域サイズを判定
  • マーカーシールのサイズと「あ」「A」の領域サイズの比から「あ」を判別(「あ」のほうが明らかにサイズが大きいので区別ができる)

しきい値エディタ

MaixPyでフレームバッファにカメラの絵が出ている状態で メニューの「ツール」→「マシンビジョン」→「しきい値エディタ」→ソースイメージの場所 フレームバッファ でしきい値エディタを起動する。 スライドバーを適当に動かして、マーカーシールのみが表示されるしきい値を確認する。 f:id:PikaPikaLight:20210430213932p:plain 同じように「あ」「A」のしきい値も確認する

コード

import sensor, image, time
#import gc, math
from board import board_info
from Maix import GPIO
from fpioa_manager import fm
import sys

while 1:
    try:
        sensor.reset() #Reset sensor may failed, let's try some times
        break
    except:
        time.sleep(0.1)
        continue
sensor.set_hmirror(1)
sensor.set_vflip(1)                 # run automatically, call sensor.run(0) to stop
sensor.set_pixformat(sensor.RGB565) # Set pixel format to RGB565 (or GRAYSCALE)
sensor.set_framesize(sensor.QVGA)   # Set frame size to QVGA (320x240)
sensor.skip_frames(time = 1000)     # Wait for settings take effect.
#sensor.set_brightness(-2)
#sensor.set_contrast(-2)
sensor.set_auto_gain(False)    #これ重要。無効にしないと周りの明るさで大きく変化してしまう。
#clock = time.clock()                # Create a clock object to track the FPS.

#LED点灯用
fm.register(35, fm.fpioa.GPIOHS0, force=True)
pinout35 = GPIO(GPIO.GPIOHS0, GPIO.OUT)
pinout35.value(0)

mark_threshold  = (0, 100, -128, -28, -128, 127)    #マークシールのしきい値
text_threshold = (100, 34, -128, 127, 127, -128)    #「あ」のしきい値

while True:
    img=sensor.snapshot()
    blobs = img.find_blobs([mark_threshold])
    if blobs:
        for b in blobs:
            if b.area() > 1000:    #ゴミとりのため、小さい領域は無視する。
                #print(b)
                tmp = img.draw_rectangle(b[0:4],color=(0,255,0))
                tmp = img.draw_cross(b[5], b[6],color=(0,255,0))
                mark_x = b[0]
                mark_y = b[1]
                mark_w = b[2]
                mark_area = b.area()
                try:
                    tmp = img.draw_rectangle(mark_x,mark_y - int(mark_w*1.5),mark_w,mark_w,color=(0,0,255))  #シールの位置から文字の位置を特定
                    img2 = img.copy((mark_x,mark_y - int(mark_w*1.5),mark_w,mark_w))  #文字の領域を切り出す
                    blobs2 = img2.find_blobs([text_threshold]) #切り出した領域に対してしきい値判定
                    if blobs2:
                        for b in blobs2:
                            #print(b.area())
                            if b.area() > 200:  #小さい領域は無視する。
                                img.draw_rectangle(mark_x+b[0],mark_y - int(mark_w*1.5) + b[1],b[2],b[3],color=(255,255,255))
                                text_area = b.area()/mark_area #比を計算
                                print(text_area)
                                if text_area > 0.2:   #「あ」の判定基準。実機確認の結果より。
                                        pinout35.value(1)
                                else:
                                        pinout35.value(0)
                except:
                    pass

サードステップ

今回の実施条件は照明がほとんど一定の環境下のもの。照明条件が大きく変わるような条件、例えば直射日光が当たるような場所では正しく動かないはずである。
ホワイトバランスをとるような仕組みがあればより安定した動作ができると思う。今後の検討課題である。
また、「あ」と「A」の区別を領域のサイズ比で行ったが、この判定部分をAIにさせることでより精度の高い判定ができそうな気がする。 マシンビジョンとAIの組み合わせ、なんかカッコいいのでトライしてみたい。

【作品まとめ】M5Stackで霹靂一閃

Twitterにあげていた作品のまとめ f:id:PikaPikaLight:20210421212814j:plain


ハーフミラーを使って面白もの作れないかと考えていて思いつきました。
Yin and Yangの絵はM5Stackライブラリの中のサンプルコードより。 M5Stack\examples\Advanced\Display\Sprite\one_bit_Yin_Yang
これをベースにLovyanGFXを使って上側の絵と下側の絵を別のspriteで作成し、パカッという動きを実現させています。


クモの絵はパワーポイントに入っていた素材を使用。
WS2812を144個使用。M5Stackでは144個の制御ができなかったので、WS2812の制御とM5Stack2台へのタイミング信号の送り出しは別のマイコンで実現。
クモの絵のパカッという動きのプログラムはspriteを理解することで実現。 もしリクエストあればコード公開します。(現状、全然整理してないコードなのでとてもお見せできない)


光モノの工作を色々してきたが、ここにきて影の面白さに目覚めた。


舞台裏編



【作品まとめ】M5Stackでホログラム

Twitterにあげていた作品のまとめ
f:id:PikaPikaLight:20210418214459p:plain www.hitachi.co.jp こちらの記事を見て、M5Stackなら自分でプログラム作れば好きな絵を浮かばせられるじゃん、と思って始めました。 実際にはこれは「ホログラム」とは違うものとは承知していますが、他に呼びようないのでホログラムと言っちゃってます。


LovyanGFXの描画速度の速さが体感できます。一番早いのは30回転首振りしているはずですが全然見えません。


Wio Terminalのほうが画面サイズ大きいので向いているかもしれません。


もともとM5Stack 2台持っていましたが。これやるために2台買い増し。


このために電動ターンテーブルも購入




M5StickVでIME入力モードを判別してLチカさせてみた

f:id:PikaPikaLight:20210417084318p:plain

構想

M5StickVでパソコンのモニター画面のIMEのモード表示「あ」or「A」を判別させ、「あ」ならLEDを点灯させ、日本語モードであることがキーボードに目線を置いていても分かるようにする。ブラインドタッチができない人間(私)が日々悩まされている問題に先端技術を持って取り組む。

環境&Version

Windows10
SDカード:SanDisk 32G HC Class10
EasyLoader(V5.1.2)
VTraining-Client-VerA02B01

V-Training実施レポ

M5StickVはV-Trainingという画像判別のための至れり尽くせりなサービスがある。 M5Stack Docs - The reference docs for M5Stack products.
基本的にはこのページに従って進めていけばOK。

  1. EasyLoaderでファームウェアの書き込み

  2. VTraining-Client.zipをダウンロードして中身をSDカードに書き込み。

  3. M5StickVの電源を入れると学習用データ撮影モードのソフトが起動する。

学習には3クラス以上必要で、1クラス35枚以上の写真が必要と書いてあるので、「あ」、「A」、「×」の3種類の写真を35枚ずつ撮影。

f:id:PikaPikaLight:20210416205033p:plain
学習用の写真 3クラス
trainフォルダとvaildフォルダができるので二つのフォルダを選択した状態で(これ肝心らしい)→送る→圧縮(zip形式)フォルダ でzipファイルを作成。
それをデータアップロードページでアップロード
http://v-training.m5stack.com/
しばらく待つとメールで結果が返ってくる。

が、1回目の結果は

[V-Trainer] *** Online Training Request Failed
Hi! Sorry for that, there is some error happened during the training process. We attached the reason below, if you have any questions, welcome to report to us through Twitter, Facebook, or Forum. Or, check out our docs here: https://docs.m5stack.com/#/en/related_documents/v-training
USERID: *** CONTENT: Lake of Enough Train Dataset, Only 78 pictures found, but you need 90 in total.

Notice:
Image 16.jpg was removed for being too close to 82.jpg, hashdiff: 2
Image 93.jpg was removed for being too close to 82.jpg, hashdiff: 2
・・・

自分のつたない英語力でも読み取れます。「90個以上の写真がないとだめだけど君のは78個しかなかったよ。ちなみに似た画像があったから消しちゃったよ。 」
なるほど。似た画像になってしまうのは避けられないので、これはいっぱい写真を撮るしかない、ということで2回目は各100枚の写真を撮って、再チャレンジ。 今度は成功。

[V-Trainer] *** Online Training Request Finished
Model: Classification MobileNetV1 Alpha: 0.7 Depth: 1 f:id:PikaPikaLight:20210416210949p:plain

学習曲線がついていて、学習がうまくいったのか分かる仕組み。これが無料でつかえるとは有り難い。
メールのURLから結果ファイルをダウンロードして、中身をSDカードに書きこめばOK
SDカード直下のboot.pyが起動時に実行されるようなので、最初の撮影用のファイル一式は別フォルダ作って移動させておいた。  

起動して、パソコン画面にカメラを向けると、「おお!判別している」 (文字ちっちゃ ) けど正答率はいまいち。
手持ち撮影による不安定さが原因かもしれないと思い、今度はカメラを台座に固定して、しっかり位置キープした状態で再々チャレンジ。

初めてのPython

M5StickV、ボタン押しにくい。台座に固定した状態ではなおさら。なので1回押したら100回連続で撮影するようにプログラムを修正することにした。

元のコード のこの部分に

try:
    while(True):
        img = sensor.snapshot()
        if but_a.value() == 0 and isButtonPressedA == 0:
            if currentImage <= 30 or currentImage > 35:

for文を追加

try:
    while(True):
        img = sensor.snapshot()
        if but_a.value() == 0 and isButtonPressedA == 0:
            for num in range(100):
                if currentImage <= 30 or currentImage > 35:

生まれて初めてのPythonプログラミング 。コンパイルとかせずに、ただ書いたコードをSDに入れるだけで動くというのも驚きの仕組み。
ドキドキしながらシャッター(Aボタン)オン → 画面固まる → カチャ音100回 → 同じ写真が100枚取れる。

forループの中に  img = sensor.snapshot() を入れてなかった。サウンドも消してなかったので延々カチャカチャ。
それでもエラーではなかったので、気を取り直して修正。
validationデータが31-35の5枚だけなのもよくないかと思い、5枚に1枚をvalidationデータにするように修正。

try:
    while(True):
        img = sensor.snapshot()

        if but_a.value() == 0 and isButtonPressedA == 0:
            for num in range(100):  #100回撮影
                time.sleep(0.1)  #100ms delay入れておく。
                img = sensor.snapshot()
                #if currentImage <= 30 or currentImage > 35:
                if currentImage % 5 != 0: #5枚に1枚をvalidationデータにする
                #以下略。play_sound("/sd/kacha.wav")はコメントアウト

再々チャレンジの結果はまずまず。カメラ位置の調整はしないといけないが、ちゃんと3クラスの判別ができている。
こんなに簡単にAIカメラが作れるとは驚き。

試しているとフリーズする現象が発生。調子いい時もあるが、だいたい途中でフリーズする。USBシリアルでログを見てみると pmax=max(plist) のところで型エラーが起きているみたい。 Pythonの型が全く分かっていないのでエラーを読み解けない。とりあえず覚えたての必殺技 try: except: pass でエラー起きても無視させてみる。結果、フリーズすることはなくなった。こんなんでいいのかと思いながらも先に進む。

あとは「あ」の時にLEDを光らせる仕組みを入れる。
M5StickV自身にもLEDはついているがカメラ側についているのでこれは使えない。使えるピンはGroveコネクタのところだけなので、ここを使うしかない。LED光らせるだけなら直接ドライブさせてもいいと思うが、手持ちにLEDドライバの基板があったのでこれを利用。コード的には直接ドライブと一緒。
あとLCDの画面は位置合わせの時には便利だが、常時ついていると気になるので、ボタンを押したら消せるようにしてみた。

最終的なコードがこれ(初めてのPython

import image
import lcd
import sensor
import sys
import time
import KPU as kpu
from fpioa_manager import *
from board import board_info
import KPU as kpu
from Maix import GPIO

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

fm.register(35, fm.fpioa.GPIOHS1, force=True)
pinout35 = GPIO(GPIO.GPIOHS1, GPIO.OUT)
pinout35.value(0)

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/XXX_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)

lcd.clear()

isButtonPressedA = 0
off_flg = 0

while(True):
    try:
        img = sensor.snapshot()
        fmap = kpu.forward(task, img)
        plist=fmap[:]
        pmax=max(plist)
        max_index=plist.index(pmax)
        if pmax > 0.95:
            # lcd.draw_string(40, 60, "Accu:%.2f Type:%s"%(pmax, labels[max_index].strip()))
            print(labels[max_index])
            if max_index == 0:
                pinout35.value(1)
            else:
                pinout35.value(0)
        if but_a.value() == 0 and isButtonPressedA == 0:
            isButtonPressedA = 1
            if off_flg == 0:
                lcd.set_backlight(False)
                lcd.clear()
                off_flg = 1
            else:
                lcd.set_backlight(True)
                lcd.clear()
                off_flg = 0
        if but_a.value() == 1:
            isButtonPressedA = 0
        if  off_flg == 0:
            a = lcd.display(img)
    except:
        pass
a = kpu.deinit(task)

ファーストステップ完成

セカンドステップ

M5StickVにはLCDとバッテリーがついていて便利なのだが、今回やりたいことに対してはこの機能は不要、むしろいらない機能といえる。
今回の目的に対してはUNIT-Vを使うのが最適なので、UNIT-Vで同じものを作ることにする。 2021/4/16現在、取組中。学習はいい結果が出せているが、モデルをUNIT-Vに入れて判別動作をさせると正答率がかなり悪いという問題で苦闘中。
その結果はまた記事にしたいと思います。