エンジニア ろっきーの探究ノート

"不"を"したい"にひっくり返すモノ/コトづくり(H/W, S/W, IoTシステム, アイデア発想)技術を探究します。

ラズパイ× tensorflowでプラレール自動運転(3) 学習したモデルでエッジコンピューティング

tensorflowで駅の写真を学習・推論させ、プラレールをラズパイで自動運転する第3回!学習させたモデルを使い、エッジで推論させながら駅を見つけたら停車させます。

1.学習および動作環境

撮った写真はGoogle ColaboratoryでKerasを利用したpythonプログラムを組んで学習させます。出力層は「駅があるとき」「駅がないとき」の2層だけの分類とし、中間層を2層や3層、Dropoutさせたりさせなかったりしましたが評価データに対する精度=0.89くらい、F1スコア=0.85くらいなのはおよそ変わらず。世の中にころがっているサンプルと大きく変わらないので、この記載は省略。

ただし、Google ColaboratoryはデフォルトでTensorflow 2.x系が走ります。ラズパイの推論プログラムをTensorflow 1.x系で作っていて学習モデル読み込み時にエラーが起こったため、Google Colaboratoryで学習させる時は

%tensorflow_version 1.x

を実行して「TensorFlow 1.x selected.」と表示されたあとに学習プログラムを走らせます(これがうまく反映されないときは「メニュー→ランタイム→ランタイムを再起動」をクリック)。こうして同じディレクトリ内に生成される学習モデル「.h5ファイル」と「.jsonファイル」をラズパイにコピーします。

バージョンの不一致はつまずくポイントですよね。今回動作したバージョン一覧はこちら。

対象 バージョン
ラズパイ Raspberry Pi 3 Model B+
OS Raspbian GNU/Linux 10 (buster)
OS image Release date:2021-05-07
Keras 2.3.1
Tensorflow 1.14.0
python 3.7.3

2.自動運転、やってみよう!

写真を撮りながら推論し、90%以上の確率で駅のある画像と判定されたら減速→停車→再力行と動かすプログラムは以下。

import numpy as np
import keras
from keras.models import model_from_json
from keras.datasets import mnist
import random, glob, picamera, time, shutil
from keras.preprocessing.image import load_img, img_to_array
import RPi.GPIO as GPIO
from PIL import Image

GPIO.setmode(GPIO.BOARD)

# Raspberry Pi用 MAX14870搭載 デュアルモータードライバのピンアサイ
DIR_1 = 18
DIR_2 = 22
EN_N = 29
PWM1 = 32
PWM2 = 33

# 各ピンを出力ピンに設定
GPIO.setup(DIR_1, GPIO.OUT, initial=GPIO.LOW)  # LOW:forward
GPIO.setup(DIR_2, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(EN_N, GPIO.OUT, initial=GPIO.LOW)   # LOW active
GPIO.setup(PWM1, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(PWM2, GPIO.OUT, initial=GPIO.LOW)

# PWM オブジェクトのインスタンスを作成
p1 = GPIO.PWM(PWM1, 4000)
p2 = GPIO.PWM(PWM2, 4000)
#----------------------------------------------------------------
#モデルを読み込む(プログラムと同じディレクトリに入れておく)
model = model_from_json(open('model_station.json').read())

#重みを読み込む(プログラムと同じディレクトリに入れておく)
model.load_weights('model_station.h5')

#損失関数、オプティマイザを指定
model.compile(loss='categorical_crossentropy', optimizer='adam')
print("mode loaded")
#----------------------------------------------------------------
# PWM信号を出力
p1.start(0)
p2.start(0)
p1.ChangeDutyCycle(1)
p2.ChangeDutyCycle(1)

try:
    while 1:
#----------------------------------------------------------------
        with picamera.PiCamera() as camera:

            camera.resolution = (640, 480)
            camera.rotation = 180
            # 撮影し、ファイルに保存
            camera.capture("pic_1_org" + '.jpg')
#----------------------------------------------------------------
        # 写真の右下(駅が映り込む部分)をトリミングする
        im = Image.open('/home/pi/Desktop/pic_1_org.jpg')             
        im_crop = im.crop((320, 240, 640, 480))
        im_crop.save('/home/pi/Desktop/pic_1.jpg', quality=95)
        filename = "pic_1" + '.jpg'
#----------------------------------------------------------------
        data = load_img(filename, target_size=(32,32))    #推論用の画像32x32で読み込み
        data = img_to_array(data)                    #3次元PILから3次元ndarrayに
        data = data.astype('float32')/ 255.0         #データを0.0~1.0へ正規化
        data = np.expand_dims(data, axis=0)          #次元を合わせる
#---------------------------------------------------------------------
        #推論する
        classifi = model.predict(data)                                 

        #predsのインデックスでソートする
        index_sort = np.argsort(classifi)

        #最大のインデックスを出す
        index = index_sort[0][-1]

        #どのラベルと分類されたかを表示する
        label_list = ["nostation", "station_OK"]
        print("予測 : " +str(label_list[index]))

        #分類した結果の確率を表示する
        probability = preds[0][index] * 100
        print("確率 : " + str(probability) + " %")
#---------------------------------------------------------------------
        if index == 1 and probability >= 90:
            p1.ChangeDutyCycle(0.2)    #駅を見つけたら1秒間は減速して走る
            p2.ChangeDutyCycle(0.2)
            time.sleep(1)  
            p1.ChangeDutyCycle(0.0)    #2秒停車する
            p2.ChangeDutyCycle(0.0)
            time.sleep(2)      
            p1.ChangeDutyCycle(0.2)    #再びゆっくり走りだす
            p2.ChangeDutyCycle(0.2)  
            time.sleep(1)                             
        else:
            p1.ChangeDutyCycle(28)
            p2.ChangeDutyCycle(28)

except KeyboardInterrupt:                
            # PWM を停止
    p1.stop()
    p2.stop()

駅を見つけて停まった!

駅を見つけて減速した瞬間

駅を発見できず通り過ぎることはなかったが、まれに全然関係ないところで勝手に停まるのはなんだろう、何を駅と間違えているのだ、、、。いずれにせよ学習データ集め~クラウドで学習~エッジで推論してH/Wを制御という一連の流れを行った。今後は、

  • 推論側の処理速度向上・最適化
  • 他の駅や前方に他の車両があるときも学習
  • 分類ではなく回帰としてPWMの速度制御

ができそうかな。おしまい。

ラズパイ× tensorflowでプラレール自動運転(2) 学習用の写真を撮りまくる

tensorflowで駅の写真を学習・推論させ、プラレールをラズパイで自動運転する第2回!学習用の写真を撮るプログラムを2種類、作成しました。

1.どんな写真を学習させたらいいか

カメラを取り付けた様子がこちら。

f:id:rockhack_design:20220403193147j:plain
この瞬間をとらえて停車させたい

写真を撮ると、こんな感じ。

f:id:rockhack_design:20220403193558j:plain
今回はこの方向での進入時のみを学習させる

背景に写っている物を誤認識されたくないので、駅が映り込む右下1/4の部分のみトリミングして取り込むことにする(←精度向上のためコレが大事でした!)。

2.走りながら写真を撮るプログラム

import tkinter as tk
import picamera, time, shutil
import RPi.GPIO as GPIO
from PIL import Image

# 写真ファイル名の連番スタート値を設定
count_0 = 0

# 使用するピン番号を設定(MAX14870)
GPIO.setmode(GPIO.BOARD)
DIR_1 = 18
DIR_2 = 22
EN_N = 29
PWM1 = 32
PWM2 = 33

# 各ピンを出力ピンに設定
GPIO.setup(DIR_1, GPIO.OUT, initial=GPIO.LOW)  # LOW:forward
GPIO.setup(DIR_2, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(EN_N, GPIO.OUT, initial=GPIO.LOW)  # Low active
GPIO.setup(PWM1, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(PWM2, GPIO.OUT, initial=GPIO.LOW)

p1 = GPIO.PWM(PWM1, 3000)
p2 = GPIO.PWM(PWM2, 3000)

p1.start(0)
p2.start(0)

p1.ChangeDutyCycle(6)
p2.ChangeDutyCycle(6)

try:
    while (1):
        with picamera.PiCamera() as camera:
            count_0 += 1
            camera.resolution = (640, 480)
            camera.rotation = 180     # カメラが上下反対なので反転する
            #camera.start_preview()
            time.sleep(0.2)
            #camera.stop_preview()
            camera.capture("pic_" + str(count_0) + '.jpg')
            
            # 右下1/4部分のみトリミングする
            im = Image.open('/home/pi/Desktop/pic_' + str(count_0) + '.jpg')    
            im_crop = im.crop((320, 240, 640, 480))
            im_crop.save('/home/pi/Desktop/pic_' + str(count_0) + '.jpg', quality=95)
            
            filename = 'pic_' + str(count_0) + '.jpg'
            source = r'/home/pi/Desktop/'+filename
            destination = r'/home/pi/Desktop/case0/'+filename
            shutil.move(source, destination)
                
except KeyboardInterrupt:                
# PWM を停止
    p1.stop()
    p2.stop()
  • PWMのDuty比は6という小さな値でゆっくり走らせる。
  • 右下1/4をトリミングした後、デスクトップにある「case0」フォルダに移動させる。
  • 写真を撮るごとに変数count_0がインクリメントされ、ファイル名の連番になる。
  • 動作を止めるには「Ctrl + C」

というプログラムです。 また、駅が映り込んだ瞬間のデータを増やすべく止まって撮るバージョンがこちら。

import tkinter as tk
import picamera, time, shutil
from PIL import Image

count_0 = 0
count_1 = 0
count_2 = 0

class Application(tk.Frame):

    def __init__(self, master=None):
        super().__init__(master)
 
        # ウィンドウの設定
        self.master.title("gather picture")
 
        self.pack()
        self.create_widget()
 
    def create_widget(self):
 
        self.label1 = tk.Label(self,text="3 case")
        self.label1.pack()

        def button0_click(): # button0をクリックした時の処理
            with picamera.PiCamera() as camera:
                global count_0
                count_0 += 1
                camera.resolution = (640, 480)
                camera.rotation = 180
                camera.start_preview()
                time.sleep(1)
                camera.stop_preview()
                camera.capture("pic_0" + str(count_0) + '.jpg')
            
                im = Image.open('/home/pi/Desktop/pic_0' + str(count_0) + '.jpg')    
                im_crop = im.crop((320, 240, 640, 480))
                im_crop.save('/home/pi/Desktop/pic_0' + str(count_0) + '.jpg', quality=95)
                
                filename = 'pic_0' + str(count_0) + '.jpg'
                source = r'/home/pi/Desktop/'+filename
                destination = r'/home/pi/Desktop/case0/'+filename
                shutil.move(source, destination)
                
        def button1_click(): # button1をクリックした時の処理
            with picamera.PiCamera() as camera:
                global count_1
                count_1 += 1                
                camera.resolution = (640, 480)
                camera.rotation = 180
                camera.start_preview()
                time.sleep(1)
                camera.stop_preview()
                camera.capture("pic_1" + str(count_1) + '.jpg')
                
                im = Image.open('/home/pi/Desktop/pic_1' + str(count_1) + '.jpg')    
                im_crop = im.crop((320, 240, 640, 480))
                im_crop.save('/home/pi/Desktop/pic_1' + str(count_1) + '.jpg', quality=95)
                
                filename = 'pic_1' + str(count_1) + '.jpg'
                source = r'/home/pi/Desktop/'+filename
                destination = r'/home/pi/Desktop/case1/'+filename
                shutil.move(source, destination)                
                
        def button2_click(): # button2をクリックした時の処理
            with picamera.PiCamera() as camera:
                global count_2
                count_2 += 1
                camera.resolution = (640, 480)
                camera.rotation = 180
                camera.start_preview()
                time.sleep(1)
                camera.stop_preview()
                camera.capture("pic_2" + str(count_2) + '.jpg')
                
                im = Image.open('/home/pi/Desktop/pic_2' + str(count_2) + '.jpg')    
                im_crop = im.crop((320, 240, 640, 480))
                im_crop.save('/home/pi/Desktop/pic_2' + str(count_2) + '.jpg', quality=95)

                filename = 'pic_2' + str(count_2) + '.jpg'
                source = r'/home/pi/Desktop/'+filename
                destination = r'/home/pi/Desktop/case2/'+filename
                shutil.move(source, destination)                  
                
        self.button0 = tk.Button(self,text="case 0", command=button0_click, width=30, heigh=3)
        self.button0.pack() # button0ウィジェット配置
        self.button1 = tk.Button(self,text="case 1", command=button1_click, width=30, heigh=3) 
        self.button1.pack() # button1ウィジェット配置
        self.button2 = tk.Button(self,text="case 2", command=button2_click, width=30, heigh=3) 
        self.button2.pack() # button2ウィジェット配置
        
if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master=root)
    app.mainloop()
  • 3つのボタンを配置し、分類させたいケースに合わせて格納フォルダを分けられる。
  • 撮った瞬間に1秒だけプレビューを表示する。
  • 各フォルダに格納時、右下1/4をトリミングする。
  • 写真を撮るごとに変数count_0がインクリメントされ、ファイル名の連番になる。

実行時の様子がこちら。

f:id:rockhack_design:20220403195810p:plain
学習させたいケース別に写真を撮る場合

ケースを分けられるようにしたのは、前方に他の車両がいて減速させるケースなどの実装も見込んでのことですが、駅があるときの停車だけを考える今回は、駅があるとき・ないときをそれぞれ20~30ずつ集めました。

f:id:rockhack_design:20220403200100p:plain
駅があるときー、ないときー。

では学習させてラズパイで実行(推論)してみよー。

ラズパイ× tensorflowでプラレール自動運転(1) モータドライバを準備する

AIで自動運転!わーい、この響きを実行したかった。

tensorflowで駅の写真を学習・推論させ、プラレールをラズパイで自動運転するまでを3回に分けて書いていきます。 今回はH/W編、「Raspberry Pi用 MAX14870搭載 デュアルモータードライバ」を動かします。

1.接続の様子

以下のように6ピン余るようにGPIOピンに挿します。ラズパイは「Raspberry Pi 3 Model B+」です。

f:id:rockhack_design:20220403172736j:plain
MAX14870の付け方

MAX14870は4.5V以上の電圧が必要なので、単三電池4本をモータ駆動用の電源とします。3本だと電池が新品の時しか動きませんでした。このMotor Driver Output端子とプラレールの電池ボックス端子を配線します。

1.サンプルプログラム

ライブラリも公開されていますが、接続確認のために2秒だけモータを回す簡単なプログラムを載せておきます。

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BOARD)

# 使用するピン番号を設定
DIR_1 = 18
DIR_2 = 22
EN_N = 29
PWM1 = 32
PWM2 = 33

# 各ピンを出力ピンに設定
GPIO.setup(DIR_1, GPIO.OUT, initial=GPIO.LOW)  # LOW:forward
GPIO.setup(DIR_2, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(EN_N, GPIO.OUT, initial=GPIO.LOW) # Low active
GPIO.setup(PWM1, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(PWM2, GPIO.OUT, initial=GPIO.LOW)

# PWM オブジェクトのインスタンスを作成、周波数:3000Hz
p1 = GPIO.PWM(PWM1, 3000)
p2 = GPIO.PWM(PWM2, 3000)

# PWM信号を出力
p1.start(0)
p2.start(0)

p1.ChangeDutyCycle(50)
p2.ChangeDutyCycle(50)

# 2.0秒待つ
time.sleep(2.0)

# PWM を停止
p1.stop()
p2.stop()

GPIO.cleanup()

さあ次は学習用の写真をとりまくるぞー!

ラズパイのnode-redとESP32(Arduino)でMQTTするときにつまずいたこと3つ

ラズパイのnode-redでMQTTを用いて、ESP32(Arduino)からpublishされたセンサ値を取得したいときに気を付けることを3つ、備忘録としてメモ。

1.node-redのバージョンは最新に

beebotteを使ってMQTTのノードを設定するには

  1. サーバ:mqtt.beebotte.com:8883
  2. トピック:(例えば) test/ras_1
  3. SSL/TLSを接続を使用、にチェック
  4. セキュリティタブからユーザ名にtokenを入れる(token:token_xxxxxxxx)

とするだけで良いはずだが、これらを設定しても接続状態にならずブローカーへの接続に失敗していた。

f:id:rockhack_design:20211229223732p:plain
MQTTノードが"接続中"から進まず、ノード自体も更新できず

思えばラズパイのmicro SDカードを作成したのは1年以上前だった気がする、、、。npmの更新でエラーが出ているようで、npmを更新するためのコマンドをいろいろ試したが解決に至らず。けっきょく、

micro SDカードの作成からやり直した!

すると何事もなかったかのように接続に成功。ブローカーへの接続に失敗したら可能性の1つはバージョンを疑おう。

ちなみに、21年12月現在でラズパイ公式のイメージ書き込みツール「Raspberry Pi Imager」を使うとmicro SDカードの作成はとっても楽になったが、最初からnode-redは入っていなかったので

sudo apt install nodered

でインストールしよう。また、ダッシュボードのノードも入っていなかったのでパレットの管理から「node-red-dashboard」を検索してノードを追加しよう。

2..subscribeとpublishでtopicは変える

ESP32(Arduino)側の話。subscribeとpublishを両方実装していて同じtopicにしてしまうとArduino IDEコンパイルでエラーが出るので、topicは分けよう。

3.センサ値をpublishするときの型変換

これもESP32(Arduino)側の話。以下はint型の"v"でセンサ値を取得したときに、それだけをpublishするソースコードwifiやMQTTブローカーへは接続できているものとして、mainのloop内で型変換するにあたって必要な箇所のみ抜粋する。

const char* topic_2 = "test/ras_1";


void loop() {

  int v = get_data();   // センサ値取得の関数
  String data = String(v, DEC);
  int length = data.length();
  char msgBuffer[length];
  data.toCharArray(msgBuffer,length+1);
  payload = msgBuffer;
  client.publish(topic_2, payload);
  Serial.print("MQTT published ");

}

こうしてnode-redのダッシュボードでいうgauge(メーター)やchart(グラフ)で値を表示できるデータとして飛ばすことができる。

おしまい。

ミニ四駆アニメのワンシーンを爆速で体現する~Node-redで音声認識~

筆者のモノづくりの原点と言えば、小中学生時代のミニ四駆でした。 当時のアニメでは「行けーっ」と叫べばミニ四駆が走ったり飛んだりしたものです。これを大人になった今、やってみます。

音声認識をどうする??

ラピットプロトタイピングで(会話ではなく機器を指示するための分類器として)音声認識させたいとき

という方法もありますが、今回は”Red moblie”を使います。これにspeech to textノードが標準搭載されているので入力された音声を分類し、その結果をMQTTで機器に飛ばして制御します。

Red mobileでNode-redを準備する

Red mobileはAndroidで動作するNode-redのアプリです(有償ですが買い切りで使い続けられます)。 インストールして起動すると以下の画面になります。

f:id:rockhack_design:20211022151404p:plain
Red mobile起動画面

「Start」を押してしばらく待つと「Start」が「http:://~」というアドレスに変わるので、このアドレスをPCのブラウザからアクセスし、node-red編集画面を開きます。 ノードの中からspeech to textやダッシュボードで音声認識を起動するボタン、マイコンに指示するMQTT OUTなどを接続します。MQTTはbeebotteを使いました。

f:id:rockhack_design:20211022150953p:plain
speech to text、ダッシュボードのボタン、MQTT OUTなどを接続

speech to textノードは以下を流すと起動できます。日本語対応、入力している言葉の候補を5つ生成させる設定を含んでいます。

//音声認識起動ノード
msg.payload = {
    "language": "ja-JP",
    "matches": 5,
    "prompt": "please talk",
    "showPopup": true
}
return msg;

次にspeech to textノードが出力した文字列を連結し、

  • 「行」が含まれていたら→{"data":"go"}

  • 「止」が含まれていたら→{"data":"stop"}

に指示を分類してMQTTで送るメッセージを生成します。

//テキスト分類ノード
var str_voice = msg.payload[0] + msg.payload[1] + msg.payload[2] + msg.payload[3] + msg.payload[4];

var result_go = str_voice.search( '行' );
var result_stop = str_voice.search( '止' );

if (result_go >= 0){
    msg.payload  = {"data":"go"};
    return msg;
}else if(result_stop >= 0){
    msg.payload  = {"data":"stop"};
    return msg;
}

var result_go = str_voice.search( '行' );

はstr_voiceに「行」が含まれていたら1以上の値、含まれていなかったら-1を返します。

ではやってみよう!

機器側はマイコンをESP32として、モータ制御に「Raspberry Pi用 MAX14870搭載 デュアルモータードライバ」を使いました。ESP32でMQTTメッセージをサブスクライブし、goならモータON、stopならモータOFFとします。ミニ四駆に組み込んで単独で走らせることまでは考えず、まずはつなげてモータ回しまでやってみます。

Node-redをデプロイし、スマホのRed mobileからダッシュボードを立ち上げます。

f:id:rockhack_design:20211022152602p:plain
ダッシュボードを立ち上げて叫んでみる

回った!何年かぶりに聞いたこのモータ音!

f:id:rockhack_design:20211022152805j:plain
実機確認の様子

やってみた感想

一人でやっていたとは言え、「行けー、マグナム」と叫ぶのが思った以上に恥ずかしかった。大人になってしまったんだと思った。

おしまい。

光る紙で”ピカッ”とあのキャラが夜の足元を照らす

”ヒカルカミ(光る紙)”なるものをスイッチサイエンスで購入しました。

f:id:rockhack_design:20211112171028j:plain
フレキシブル有機EL照明 A88MA2B 白 43.4×15.9mm

フレキシブル有機EL照明で、薄くて曲げられます。 端子に配線をはんだ付けして単三電池を3本つなげば、ほら光る。

f:id:rockhack_design:20211112171131j:plain
単三電池3本をつなげて光らせる(極性あり)

最初は子供のおもちゃや絵本に仕込んで何かしようと思いましたが、思った以上に明るいので暗い廊下を照らすセンサーライトを自作することにしました。LEDセンサーライトではなく有機ELセンサーライトということになる。

構成要素

  1. マイコン  :ESP-WROOM-32
  2. 照度センサ :TSL2561デジタル光センサ
  3. 人勧センサ :PIRモーションセンサ
  4. リレーユニット :セロワン リレー回路 拡張基板
  5. 光る紙用の電源 :単三電池3本
  6. マイコン用の電源:USBから供給

センサ類は珍しい情報ではないので詳細は割愛(ESP-WROOM-32の場合はArduino IDEでインポートできるTSL2561の標準ライブラリでは動作しないようなので注意)。ESP-WROOM-32のプログラムも照度が低いときにモーションセンサが検知していたらリレーをオンするだけのもの。

リレーユニットはマイコン系の電源と光る紙の電源を分けるために使いました。 これはラズパイセロ用の拡張基板でSRD-5VDC-SL-Cが載っておりAC 250V/10A, DC30V/10Aでオーバースペックですが手元にあったので活用。

f:id:rockhack_design:20211112205007p:plain
リレー回路 拡張基板のピンアサイ

問題は筐体である。 日常で実際に使って運用したいので見た目も重要だが、3Dプリンタで作るといった手間もかけたくない。

そこで探して出会ったのがコイツ。

f:id:rockhack_design:20211112171201j:plain
ピカッ!

メルカリで時計機能が壊れて出品されていたものを発見し即購入。時計をくりぬいて中に回路を埋め込むことにした。ちょうど時計があったところからフレキシブル有機ELや各センサーが出るように固定すれば完成。

f:id:rockhack_design:20211112203214j:plain
筐体の中に回路を入れ込む

実運用へ

暗い廊下に設置して、歩いてみると

f:id:rockhack_design:20211112171222j:plain
おててがライトを持っているみたいでかわいい

光ってお出迎え! 光る紙が面として光るのが良い感じ。ふつーにLEDでもいいんだけどね。

おしまい。

python「TKinter」のウィジェットを指定して画面を操る一例

日経BPさんの「みんなのラズパイコンテスト2020」でアイデア賞をいただきました! その作品で活用したpythonTKinter」について解説します。

やりたいこと

作品ではTKinterウィジェットをラズパイに取り付けたセンサーで操作しましたが、このブログではTKinterに関わる以下の機能だけに絞って書きます。

  1. 好きな画像をウィンドウに配置する
  2. 配置した画像を隠すようにマス目上に正方形を敷き詰める
  3. 消すボタンを押すごとにその正方形がランダムで消えて画像が表れていく
  4. 戻すボタンを押すと正方形が敷き詰められて画像が隠れる
  5. 終わるボタンで終了する

こんな感じ。

f:id:rockhack_design:20211103151009p:plain
プログラム実行時の様子

実行環境

下記に載せるソースコードwindowsでpython3.xをインストールし動作確認したものです。ラズパイで動作させる場合は、(アップデートさせていない限り)python2.xに対応させるべく

import tkinter
import tkinter as tk

の部分を

import Tkinter
import Tkinter as tk

とすればOKです("t"を大文字にする)。

また、ソースコードと同じディレクトリに「picture.gif」ファイルを置けば実行可能です。

ソースコード

import sys,random
import tkinter
import tkinter as tk
from tkinter import messagebox

root = tkinter.Tk()
# 画面タイトル
root.title(u"TKinterで配置したウィジェットを操作する")
# 画面サイズの設定
root.geometry("720x720")
# 使用する背景画像の読み込み
picture = tk.PhotoImage(file="picture.gif")
#キャンバスエリア
canvas = tkinter.Canvas(root, width = 720, height = 540)
canvas.create_image(0, 0, image=picture, tags="picture", anchor=tk.NW)
#キャンバスバインド
canvas.place(x=0, y=0)

yoko = 8
tate = 6
size = 90
list = [[0 for i in range(yoko)] for j in range(tate)]

#「戻す」ボタンが押されたら
def retry(event):
    create_panel()

#「消す」ボタンが押されたら
def delete(event):
    delete_panel()

#「終わる」ボタンが押されたら
def exit(event):
    sys.exit()

#パネル(正方形)をランダムで1つ消す
def delete_panel():
    rand_yoko = random.randint(0,yoko-1)
    rand_tate = random.randint(0,tate-1)
    total = 1

    #すべてのパネルが消えているかチェック
    for i in range(yoko):
        for j in range(tate):
            total = total * list[j][i]

    #変数"total"が0ではなかったときすべてのパネルが消えている
    if total != 0:
        ret = messagebox.showinfo('ミッションコンプリート!', '「戻す」で再開するか「終わる」で終了ください')
    #変数"total"が0のとき1つ以上のパネルが残っている  
    else:
        while True:   #残っているパネルの位置を探す
            if list[rand_tate][rand_yoko] > 1:
                rand_yoko = random.randint(0,yoko-1)
                rand_tate = random.randint(0,tate-1)
            if list[rand_tate][rand_yoko] == 0:
                break
        
        canvas.delete("oval")
        canvas.delete("hide" + str(rand_yoko) + str(rand_tate))
        list[rand_tate][rand_yoko] = 2

#パネル(正方形)を敷き詰めて初期状態に戻す
def create_panel():
    for i in range(yoko):
        for j in range(tate):
            #パネルを生成する(サイズ、位置、色、tagを指定して配置)
            canvas.create_rectangle(size*i, size*j, size*i+size, size*j+size, outline="white", fill="blue", tags="hide" + str(i) + str(j))
    for i in range(yoko):
        for j in range(tate):
            list[j][i] = 0


# 「消す」ボタンを配置
button_draw = tkinter.Button(root, text=u'消す',width=15,height=2)
button_draw.bind("<Button-1>",delete)
button_draw.place(x=10,y=570)

# 「戻す」ボタンを配置
button_draw = tkinter.Button(root, text=u'戻す',width=15,height=2)
button_draw.bind("<Button-1>",retry)
button_draw.place(x=160,y=570)

# 「終わる」ボタンを配置
button_draw = tkinter.Button(root, text=u'終わる',width=15,height=2)
button_draw.bind("<Button-1>",exit)
button_draw.place(x=310,y=570)

create_panel()
root.mainloop()

解説

敷き詰めるパネルは2次元配列"list"と対応させ、例えば

  • 左から3番目、上から1番目のパネルが消えたらlist[2][0]は"0"
  • 左から7番目、上から2番目のパネルが残っていればlist[6][1]が"2"

としてパネルを管理できるようにします。pythonウィジェットはtagsという引数で紐づけられるようになっています。

f:id:rockhack_design:20211103151040p:plain
パネルの2次元配列との対応とtagでの紐づけ

以下のコードで縦と横に対応したタグを付加しながらパネルを生成しています。

canvas.create_rectangle(size*i, size*j, size*i+size, size*j+size, outline="white", fill="blue", tags="hide" + str(i) + str(j))

以下のコードで縦と横に対応したタグを指定してパネルを削除しています。

canvas.delete("hide" + str(rand_yoko) + str(rand_tate))

上記を応用して例えばラズパイに接続したセンサーでウィジェットを生成したり削除したり操作するには、マルチスレッドを起動してそこでセンサー情報を監視、条件に一致したら操作する関数を呼び出します。こうして画面UIとリアルを(ネット接続を介さず)連動させたものを作れました。

おしまい。