Matplotlib でお絵かきアプリっぽいものを作る

Matplotlib でお絵かきアプリっぽいものを作ります。

1. matplotlib のバージョン

GUIAPIの仕様は変わる可能性が高いため、matplotlib のバージョンを記載しておきます。
バージョン 2.2.2 で動作確認済みです。以下のようにするとバージョンの確認ができます。

import matplotlib
print(matplotlib.__version__)
# Out: '2.2.2'

2. 仕様

アプリの仕様は以下とします。

No 状態 動作
1 マウス左ボタン プレス ペンを下ろす
2 マウス左ボタン リリース ペンを上げて、軌跡を消去
3 マウス左ボタンをプレス中にマウスを動かす ペンで軌跡を描画
4 マウス左ボタンをリリース中にマウスを動かす なにもしない

簡単にいってしまうと、マウス左ボタンを押している間だけマウスの軌跡を描画するアプリです。ボタンを離すと軌跡は消えます。一筆書きをしている間は軌跡をみることができます。

3. コード

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Arrow

fig, ax = plt.subplots()

being_pressed = False
pre_x = None
pre_y = None

def on_motion(event):
    global pre_x, pre_y
    if None in (event.xdata, event.ydata): # マウスが画面の外にいるときは何もしない
        #print('x=%d, y=%d, xdata=%f, ydata=%f' %(event.x, event.y, event.xdata, event.ydata))
        return
    if being_pressed:
        ax.add_patch(Circle((event.xdata, event.ydata), 0.01, color='r'))
        if None not in (pre_x, pre_y): # pre_x と pre_y が存在するとき矢印を描画
            ax.add_patch(
                Arrow(pre_x, pre_y, event.xdata-pre_x, event.ydata-pre_y,
                      linestyle='solid', color='blue', width=0.02))
        pre_x = event.xdata
        pre_y = event.ydata
        fig.canvas.draw() # 必須!

def on_button_press(event):
    print('button pressed')
    global being_pressed
    being_pressed = True

def on_button_release(event):
    print('button released')
    global being_pressed, pre_x, pre_y
    being_pressed = False
    pre_x = None
    pre_y = None
    # reset patches
    for patch in reversed(ax.patches):
        patch.remove()
    fig.canvas.draw() # 必須!


cid_1 = fig.canvas.mpl_connect('motion_notify_event', on_motion)
cid_2 = fig.canvas.mpl_connect('button_press_event', on_button_press)
cid_3 = fig.canvas.mpl_connect('button_release_event', on_button_release)

plt.show()

4. コードの説明

マウスの左ボタンを押しているかどうかを being_pressed 変数で管理することとしています。pre_x, pre_y はボタンが押されているときの、マウスを動かす前のマウスの座標を保存するために定義しています。(pre_x, pre_y はボタンが押されてないときは存在しないので None で初期化します)

ボタンが押されたときに独自関数(on_button_press関数)を呼んでほしいため

fig.canvas.mpl_connect('button_press_event', on_button_press)

としています。これにより canvas で button_press_event が発生すると on_button_press 関数が呼ばれるようになります。同様に、 ボタンが離されたとき(button_release_event イベントが発生したとき)には on_button_release、 マウスが動いたとき(motion_notify_event イベントが発生したとき)には on_motion 関数が呼ばれるように設定します。

話はそれますが、matplotlib で他にどのようなイベントが提供されているか興味のある方は、以下を参照してください。
https://matplotlib.org/users/event_handling.html より抜粋

Event name Class and description
'button_press_event' MouseEvent - mouse button is pressed
'button_release_event' MouseEvent - mouse button is released
'draw_event' DrawEvent - canvas draw (but before screen update)
'key_press_event' KeyEvent - key is pressed
'key_release_event' KeyEvent - key is released
'motion_notify_event' MouseEvent - mouse motion
'pick_event' PickEvent - an object in the canvas is selected
'resize_event' ResizeEvent - figure canvas is resized
'scroll_event' MouseEvent - mouse scroll wheel is rolled
'figure_enter_event' LocationEvent - mouse enters a new figure
'figure_leave_event' LocationEvent - mouse leaves a figure
'axes_enter_event' LocationEvent - mouse enters a new axes
'axes_leave_event' LocationEvent - mouse leaves an axes

話を戻して、以下のコードで現在のマウスのx, y座標に丸を描画します。0.01 は 丸の大きさです。何度か試して 0.01 が良さそうだったのでそうしています。

ax.add_patch(Circle((event.xdata, event.ydata), 0.01, color='r'))

以下のコードで、前のマウスのx, y座標と現在の座標に矢印を描画します。0.02 は線の太さです。これも試した結果これくらいがよさそうだったのでそうしています。

ax.add_patch(
    Arrow(pre_x, pre_y, event.xdata-pre_x, event.ydata-pre_y,
    linestyle='solid', color='blue', width=0.02))

これら 0.01 や 0.02 を計算でだせるようにするのは私の宿題としておきます。

5. 実行結果

f:id:keimina:20190705002816g:plain
matplotlibによるお絵かきアプリっぽいもの

6. まとめ・考察

matploblib でお絵かきアプリっぽいものを作りました。
マウスがクリックされたら〜するとか、マウスが動いたら〜するなどいろいろ応用できるのではないかと思います。筆跡を時系列データとして機械学習して分類するのもおもしろそうです、夏休みの自由研究にもできそうではないでしょうか。

7. 今後の予定

個人的にはこれを使って軌跡に囲まれた領域のデータの点だけを取得して、それらのデータの可視化したいです(実はもうしたけどそれは次回以降、書きます)。

8. 宿題

matplotlib の Arrow の太さとか Circle の大きさを計算できるように仕様を把握する

それでは、おやすみなさい。