以前M5Stackで画面をタップするものを作り、
M5Stackでタブレットクリッカーを作る - Qiita
https://qiita.com/coppercele/items/a46bb2437f9c73ffa061
色認識してターゲットを追跡するものを作ったので、
RoverCとM5StickCとM5StickVでグリッパロボットを作る - Qiita
https://qiita.com/coppercele/items/2c4e6c84a7a96e348dc1
これなら音ゲーをプレイするものが作れるんじゃないか?と思ったので作ってみました
ミリシタはレーンが2個しかないモードがあるのでうってつけでした
ミリシタでレッツリズム!
アイドルマスター ミリオンライブ! シアターデイズ | バンダイナムコエンターテインメント公式サイト
https://millionlive.idolmaster.jp/theaterdays/
ちなみにミリシタの仕様として
・ホールドは途中で離していい
・ホールドの最後はタップでもリリースでもどっちでもいい
という感じになっているのでピンクの玉をタップすればフリック以外は取れます
##ハードウェアの構築
M5stack
M5Stackでタブレットクリッカーを作る - Qiita
https://qiita.com/coppercele/items/a46bb2437f9c73ffa061
こちらで使ったものをそのまま使用します
M5StickV
音ゲーの画面をカメラに映すためレゴでやぐらを組んで設置します
M5StickVを固定している透明の部品は以前3Dプリンタで作ったホルダーを流用しました
上手いことこういう感じに映るように調整します
M5StickVとM5StackをGroveケーブルで直結します
ちなみにM5StackのGroveは21,22を使うのでリレータッチボードを21に接続してると使えなくなります(Groveが優先)
上側のGPIOは22,23,19,18は他の機能と兼用されてるので避けないといけないので左側のGPIO16に接続しました
上手いこと調整できたら歌織さんの胸ノーツを叩くポイントにリレータッチボードを貼り付けてセッティングします
##ソフトウェアの構築
M5StickVとM5Stack間はUARTで通信します
M5SticV側ソース(送信のみ)
from fpioa_manager import fm
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)
data_packet = bytearray([0, 0])
uart_Port.write(data_packet)
M5Stack側(受信のみ)
void setup() {
Serial1.begin(115200, SERIAL_8N1, 21, 22);
}
void loop() {
M5.update();
if (Serial1.available()) {
uint8_t rx_buffer[1];
int rx_size = Serial1.readBytes(rx_buffer, 1);
for (int i = 0; i < rx_size; i++) {
Serial.printf("%d, ", rx_buffer[i]);
}
Serial.printf("\n");
}
delay(1);
}
##ノーツを認識する
こちらを参考にして閾値エディタでピンクを認識できるようにします
【キョロキョロV④】m5StickV サーボ2軸マシンでまずは色検出をしてみました - パスコンパスの日記
https://yoichi-41.hatenablog.com/entry/2019/09/15/204835
また、色認識するには周囲の環境の影響が大きいです
昼光色、電球色、自然光や画面に表示してるもの明るさによってカメラの露出などが変わってしまいさっきまで認識できていたのに認識できなくなってしまうということがよく起きます
なのでなるべくカメラの影響が少なくなるように設定します
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_brightness(0) #カメラの明るさ
sensor.set_saturation(0) #カメラの飽和度?
sensor.set_contrast(0) #カメラのコントラスト
sensor.run(1)
それぞれ-2~2の間で設定できるので自分の環境に合わせて調整しましょう
ちゃんと設定するとこんな感じで認識できるようになります
また、PerfectのPの赤に反応したりするので認識する部分を緑の部分だけに限定します
red_threshold = (88, 54, 21, 93, -6, 60)
img=sensor.snapshot()
blobs = img.find_blobs([red_threshold])
if blobs:
for blob in reversed(blobs):
#認識したノーツを逆順に処理する
if (blob[2] * blob[3] < 200):
#幅*高さで面積を出し小さいものは除外
continue
if 160 < blob[6] :
#緑枠の下限より下の物は除外
#blob[6]は認識領域の中心y座標
continue
if blob[6] < 180 and blob[5] < 90 or 230 < blob[5] :
#緑領域の中か判定
#blob[5]は認識領域の中心x座標
#四角と十字を描画
tmp=img.draw_rectangle(blob[0:4],color=(128,0,0))
tmp=img.draw_cross(blob[5], blob[6],color=(128,0,0))
c=img.get_pixel(blob[5], blob[6])
if blob[5] < 160:
#左側ならM5Stackに0を送信
data_packet = bytearray([0])
else:
#右側ならM5Stackに1を送信
data_packet = bytearray([1])
uart_Port.write(data_packet)
break #タイミングラインに近い一番後ろの要素を処理したら終了
音ゲーである以上ノーツを認識してからいい感じのタイミングでタップしないとミスになってしまいます
タイミングラインの近くで認識したら短く、遠くで認識されたら長くsleepを入れます
if blob[5] < 160:
#左側ならM5Stackに0を送信
data_packet = bytearray([0])
else:
#右側ならM5Stackに1を送信
data_packet = bytearray([1])
# 画面の1番上なら0.6秒、タイミングラインに近ければ0になるようにwaitを計算する
wait = (160 - blob[6]) / 160 * 0.6
print("wait=" + str(wait))
#待ってから送信
time.sleep(wait)
uart_Port.write(data_packet)
break #タイミングラインに近い一番後ろの要素を処理したら終了
M5Stack側では受信した内容に合わせて画面をタップします
void setup() {
M5.begin();
pinMode(26, OUTPUT);
pinMode(16, OUTPUT);
digitalWrite(26, LOW);
digitalWrite(16, LOW);
}
void loop() {
M5.update();
if (Serial1.available()) {
uint8_t rx_buffer[1];
int rx_size = Serial1.readBytes(rx_buffer, 1);
if (rx_buffer[0] == 0x00) {
// 0なら左タップ
Serial.printf("left\n");
// 左
digitalWrite(16, HIGH);
delay(50);
// リリース
digitalWrite(16, LOW);
}
else {
// 0以外なら右タップ
Serial.printf("right\n");
// 右
digitalWrite(26, HIGH);
delay(50);
// リリース
digitalWrite(26, LOW);
}
}
##音ゲーをプレイしてみよう
いい感じで構成してからミリシタのリハーサルで試してみます(通常プレイするとBANされるかも知れない)
認識率9割は行ったんじゃね?🤣🤣🤣
— もけ@ムギ㌠ (@coppercele) May 20, 2020
(フリックはそもそも無理なので除く)#M5StickV #M5Stack pic.twitter.com/mtqY703U7p
このツイートでは9割と言ってますがフリック以外フルコン取れるようになってます
ソース
M5SticV側
m5sticv
import sensor
import image
import lcd
import time
from fpioa_manager import fm
from machine import UART
fm.register(35, fm.fpioa.UART2_TX, force=True)
fm.register(34, fm.fpioa.UART2_RX, force=True)
lcd.init()
lcd.rotation(2)
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_brightness(0)
sensor.set_saturation(0)
sensor.set_contrast(0)
sensor.run(1)
red_threshold = (88, 54, 21, 93, -6, 60)
uart_Port = UART(UART.UART2, 115200,8,0,0, timeout=1000, read_buf_len= 4096)
while True:
img=sensor.snapshot()
img.draw_rectangle(140, 0, 40, 40, color=(255,255,255))
img.draw_rectangle(100, 40, 120, 40, color=(255,255,255))
img.draw_rectangle(90, 80, 140, 40, color=(255,255,255))
img.draw_rectangle(90, 120, 140, 40, color=(255,255,255))
img.draw_rectangle(90, 160, 140 , 40, color=(255,255,255))
img.draw_rectangle(0, 0, 90 , 180, color=(0,255,0))
img.draw_rectangle(230, 0, 90 , 180, color=(0,255,0))
blobs = img.find_blobs([red_threshold])
if blobs:
for blob in reversed(blobs):
if (blob[2] * blob[3] < 200):
continue
if 160 < blob[6] :
continue
if blob[6] < 180 and blob[5] < 90 or 230 < blob[5] :
tmp=img.draw_rectangle(blob[0:4],color=(128,0,0))
tmp=img.draw_cross(blob[5], blob[6],color=(128,0,0))
c=img.get_pixel(blob[5], blob[6])
if blob[5] < 160:
data_packet = bytearray([0])
else:
data_packet = bytearray([1])
wait = (160 - blob[6]) / 160 * 0.6
print("wait=" + str(wait))
time.sleep(wait)
uart_Port.write(data_packet)
break
lcd.display(img)
M5Stack側
M5Stack
#include <M5Stack.h>
// タッチの間隔
int milli = 500;
// スイッチ連打状態かのフラグ
bool state = false;
void drawScreen() {
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(0,0);
// 間隔を表示
M5.Lcd.printf("%4dms",milli);
M5.Lcd.setCursor(0,220);
// 画面下部の表示
M5.Lcd.printf(" - %s +", state ? "OFF" : "ON ");
}
void setup() {
Serial.begin(115200);
M5.begin();
M5.Lcd.setRotation(1);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextSize(2);
M5.Lcd.setBrightness(64);
M5.Lcd.fillScreen(BLACK);
pinMode(26, OUTPUT);
pinMode(16, OUTPUT);
digitalWrite(26, LOW);
digitalWrite(16, LOW);
drawScreen();
Serial1.begin(115200, SERIAL_8N1, 21, 22);
}
void loop() {
M5.update();
if (Serial1.available()) {
uint8_t rx_buffer[1];
int rx_size = Serial1.readBytes(rx_buffer, 1);
for (int i = 0; i < rx_size; i++) {
Serial.printf("%d, ", rx_buffer[i]);
}
Serial.printf("\n");
if (rx_buffer[0] == 0x00) {
if(state) {
Serial.printf("left\n");
// 左
digitalWrite(16, HIGH);
delay(50);
// リリース
digitalWrite(16, LOW);
}
}
else {
if(state) {
Serial.printf("right\n");
// 右
digitalWrite(26, HIGH);
delay(50);
// リリース
digitalWrite(26, LOW);
}
}
}
if ( M5.BtnA.wasPressed() ) {
digitalWrite(16, HIGH);
delay(50);
// リリース
digitalWrite(16, LOW);
}
if ( M5.BtnB.wasPressed() ) {
state = !state;
drawScreen();
}
if ( M5.BtnC.wasPressed() ) {
// 右
digitalWrite(26, HIGH);
delay(50);
// リリース
digitalWrite(26, LOW);
}
delay(1);
}