ESP-WROOM-02で二次元温度センサ(ROBOBA041, Grid-EYE)を使ってみる 後編

grideye_viewer_1

しばらく間が空いてしまいました、申し訳ありません。育児と趣味(工作)の両立はなかなかやはり、難しいもので。基本的に子供が寝ている時間しか趣味の時間がとれないという。そして、その貴重な時間が、この一ヶ月間、全部ゼルダに持っていかれているというこの有様。

。。。いや、だって、めちゃめちゃ面白いんですもの。。。数年ぶりに寝る間を惜しんでゲームしてしまっています。日々、攻略サイトを見るか見ないかの葛藤を続けていますが、とりあえず、何も見ずに、四体の神獣を解放し、マスターソードを手に入れるところまでは進みました。

ということで、ようやく一区切り(?)ついたので、前回やり残していたことを片付けてしまいたいと思います。二次元温度センサ Grid-EYEで取得し、クラウド(ThingSpeak)に蓄積していたデータを二次元平面で可視化してみます。

可視化については、Grid-EYEのArduinoライブラリを作成されている方が、Processingを使って取得データをリアルタイムに可視化するものを既に作られています。なので、それをそのまま流用させてもらった方が話が早いのかもしれませんが、自分がProcessingにあまり馴染みがないため、Pythonでイチから作ってみることにしました。

 

はじめは、本業の方で使い慣れている、Pythonのデータ分析界隈ではおなじみの可視化ツール・matplotlibを使って何とかならないかなあと思ってやってみました。

試しにデータを読み込んで、一回分の取得データをヒートマップで可視化してみると、こんな感じになります。

import csv
import numpy as np

######## データの読み込み ########

in_file_name = 'feeds.csv'
pixel_frame = []

with open(in_file_name,'r') as in_file:
    reader = csv.reader(in_file)
    header = next(reader)
    for row in reader:
        time_utc = row[0]
        id = row[1]
        temperature  = row[2]
        temp_grid_1 = row[3].split(':')
        temp_grid_2 = row[4].split(':')
        pixels = [[0 for i in range(8)] for j in range(8)]
        for i in range(4):
            for j in range(8):
                pixels[i][j] = float(temp_grid_1[i*7+j])
                pixels[i+4][j] = float(temp_grid_2[i*7+j])
        pixel_frame.append(pixels)

# 読み込んだ配列をnumpy配列に変換
np_pixel_frame = np.array(pixel_frame)

######## matplotlibで可視化 ########
import matplotlib.pyplot as plt
%matplotlib inline

data = np_pixel_frame[0]

# 描画する
fig, ax = plt.subplots()
heatmap = ax.pcolor(data, cmap=plt.cm.OrRd)
ax.invert_yaxis()    # y軸の0を上に
ax.xaxis.tick_top() # x軸の値を上に
plt.show()

画像はこちら。

 grideye_viewer_2.png

うん、とりあえず表示されました。ちなみにSeabornを場合は、上のソースの「matplotlibで可視化」の部分を以下に差し替えてみてください。

import seaborn as sns

data = np_pixel_frame[0]

ax = sns.heatmap(data, cmap='OrRd', square=True)
sns.plt.axis("off")
sns.plt.show()

Seabornを使ったときの画像はこちら。

grideye_viewer_3

 横にバーがついて見やすくなりました。

 

さて、取得したデータを一つずつ可視化するならこれでOKなのですが、できればやっぱり、時間経過と共にどう変わっていくかを見えるようにしたいところです。それで、matplotlibのアニメーション機能で何とかならないかと試行錯誤してみたのですが、結局うまくいきませんでした。

というわけで、素直にGUIアプリケーションを作ってみることにしました。PythonでGUIアプリケーションを作るのは何気に初めてでしたので、色々調べてみたところ、何だかTkinterというものが使いやすそうでしたので、これをベースに作ってみることにしました。

とりあえず機能を最小限に絞って、こんな感じで作ってみました。

grideye_viewer_1

“Start”ボタンで、右に書いてある速さ(秒)で1分ごとの取得データを表示していきます。速さは0.1s〜1.0sまで、0.1s刻みで自由に変更できるようにしています。”Start”ボタンはアニメーション開始後に”Stop”ボタンに変わります。Stop中は、”Back”ボタンで1分前のデータに遡って表示、”Next”ボタンで1分後のデータを表示するようになります。”Reset”ボタンで、今回読み込んだ最初のデータの表示に戻ります。

実際にはこんな感じで動きます。

 

ソースコードは以下です。

import tkinter as tk
import csv
import time
import threading
from pytz import timezone
from dateutil import parser

# データの読み込み----------------------------------

in_file_name = 'feeds.csv'
time_frame = []
pixel_frame = []

with open(in_file_name,'r') as in_file:
    reader = csv.reader(in_file)
    header = next(reader)
    for row in reader:
        time_utc = row[0]
        id = row[1]
        temperature  = row[2]
        temp_grid_1 = row[3].split(':')
        temp_grid_2 = row[4].split(':')

        # 時刻情報を日本時間に変換して保持
        time_jst = parser.parse(time_utc).astimezone(timezone('Asia/Tokyo'))
        time_frame.append(time_jst)
        # 8x8の二次元配列を定義
        pixels = [[0 for i in range(8)] for j in range(8)]
        for i in range(4):
            for j in range(8):
                pixels[i][j] = float(temp_grid_1[i*7+j])
                pixels[i+4][j] = float(temp_grid_2[i*7+j])
        pixel_frame.append(pixels)

########################################################
# GUI作成
########################################################

frame_index = 0
animation_on = False
TEMPERATURE_MAX = 35
TEMPERATURE_MIN  = 15
TEMPERATURE_RANGE = TEMPERATURE_MAX - TEMPERATURE_MIN

root = tk.Tk()
root.title("GridEye Viewer")
root.geometry("225x350")

# 時刻テキスト定義
lb_time = tk.Label(text='Time')

# グリッド定義
color_grids = []
for i in range(0,8):
    color_grid_row = []
    for j in range(0,8):
        color_grid_row.append(tk.Label(text='     ',  bg='#f0f0f0'))
    color_grids.append(color_grid_row)

# アニメーション定義
class AnimationThread(threading.Thread):

    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        global frame_index
        global animation_on
        global speed_var

        while animation_on:
            frame = pixel_frame[frame_index]
            for i in range(0,8):
                for j in range(0,8):
                    temp = frame[i][j]
                    temp_diff = 0
                    if temp < TEMPERATURE_MIN:
                        temp_diff = 0
                    elif temp > TEMPERATURE_MAX:
                        temp_diff = TEMPERATURE_RANGE
                    else:
                        temp_diff = temp - TEMPERATURE_MIN
                    r = 255
                    g = 255 -  int(temp_diff/TEMPERATURE_RANGE*255)
                    b = 255 -  int(temp_diff/TEMPERATURE_RANGE*255)
                    color = '#%02x%02x%02x' % (r,g,b)
                    color_grids[i][j].configure(bg = color)
                    lb_time.configure(text=time_frame[frame_index])
            frame_index += 1
            time.sleep(float(speed_var.get()[:-2])) # 単位を削除して変換

# アニメーション開始/停止イベントハンドラ
def animation():
    global animation_on
    animation_on = not animation_on
    if animation_on:
        # アニメーションスレッド生成・開始
        th_anim = AnimationThread()
        th_anim.start()
        bt_anim.configure(text="Stop")
    else:
        bt_anim.configure(text="Start")

# ステップ実行関数
def update_grids(step):
    global frame_index
    frame_index += step
    if frame_index < 0:
        frame_index = 0
    frame = pixel_frame[frame_index]
    for i in range(0,8):
        for j in range(0,8):
            temp = frame[i][j]
            temp_diff = 0
            if temp < TEMPERATURE_MIN:
                temp_diff = 0
            elif temp > TEMPERATURE_MAX:
                temp_diff = TEMPERATURE_RANGE
            else:
                temp_diff = temp - TEMPERATURE_MIN
            r = 255
            g = 255 -  int(temp_diff/TEMPERATURE_RANGE*255)
            b = 255 -  int(temp_diff/TEMPERATURE_RANGE*255)
            color = '#%02x%02x%02x' % (r,g,b)
            color_grids[i][j].configure(bg = color)
            lb_time.configure(text=time_frame[frame_index])

# ステップ実行(戻る)ハンドラ
def step_back():
    update_grids(-1)

# ステップ実行(進む)ハンドラ
def step_next():
    update_grids(1)

# リセットハンドラ
def reset():
    global frame_index
    frame_index = -1
    update_grids(1)

# アニメーション開始/停止ボタン定義
bt_anim = tk.Button(root, width=6, height=2, text="Start", command=animation)

# ステップ実行(戻る)ボタン定義
bt_back = tk.Button(root, width=6, height=2, text="Back", command=step_back)

# ステップ実行(進む)ボタン定義
bt_next = tk.Button(root, width=6, height=2, text="Next", command=step_next)

# リセットボタン定義
bt_reset = tk.Button(root, width=12, height=2, text="Reset", command=reset)

# ドロップダウンリスト(オプションメニュー)定義
speed_vars = ["0.1 s","0.2 s","0.3 s","0.4 s","0.5 s","0.6 s","0.7 s","0.8 s","0.9 s","1.0 s"] 
speed_var   = tk.StringVar()
speed_var.set(speed_vars[0]) # デフォルト値
dl_speed = tk.OptionMenu(root, speed_var, *speed_vars)

########################################################

# ボタン配置
bt_anim.grid(row=0, rowspan=1, column=0, columnspan=4, padx=0, pady=0) 
# 再生スピード設定配置
dl_speed.grid(row=0, rowspan=1, column=4, columnspan=4, padx=0, pady=0) 
#lb_speed.grid(row=0, rowspan=1, column=7, columnspan=1, padx=0, pady=0) 

# ボタン配置
bt_back.grid(row=1, rowspan=1, column=0, columnspan=4, padx=0, pady=0) 
bt_next.grid(row=1, rowspan=1, column=4, columnspan=4, padx=0, pady=0) 

#時刻情報配置
lb_time.grid(row=2, rowspan=1, column=0, columnspan=8, padx=0, pady=0) 

# グリッド配置
for i in range(0,8):
    for j in range(0,8):
        color_grids[i][j].grid(row=(i+3), rowspan=1, column=j, columnspan=1, padx=1, pady=1)

# ボタン配置
bt_reset.grid(row=11, rowspan=1, column=1, columnspan=6, padx=0, pady=0) 

########################################################

# ウィンドウを閉じるときにアニメーションのスレッドが動いていたら、先にそちらを止めてから終了させる
def window_close():
    global animation_on
    if animation_on:
        animation_on = False
    root.destroy()

root.protocol("WM_DELETE_WINDOW", window_close)

# メインループ開始
root.mainloop()

グローバル変数を多用していて何だかカッコ悪いですが。。。昔はJavaでオブジェクト指向を意識しながらプログラムを書いていたものですが、最近は簡単な処理をPythonで書くことが多くて、すっかりオブジェクト指向の書き方を忘れてしまいました。

ともあれ、これで一応最低限のベースは作ったので、あとは必要に応じて機能拡張していけば良いと思います。

 

これでGrid-EYEを使って云々は一区切りです。次にやりたいことは色々あるのですが、さて、何から手をつけよう。