ラベル matplotlib の投稿を表示しています。 すべての投稿を表示
ラベル matplotlib の投稿を表示しています。 すべての投稿を表示

2020年6月29日月曜日

matplotlibでアニメーション(ライフゲーム)

ライフゲームを表示するスクリプトをmatplotlibのアニメーションで書いたものがこちら。 前のものは、FuncAnimation()へ引き渡す関数をlambda式で作成することで、 フレーム番号以外のパラメータを関数に持たせて受け渡す方法を用いましたが、 Pythonなら、クラスを用いるのもよいです。 クラスにすると、なにかパラメータを保持させていることが明確です。
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation

def Rule(alive, neighbours):
    n = sum(neighbours)
    if not alive and n==3:
        return 1 
    if alive and n==2 or n==3:
        return 1 
    if alive and n<=1:
        return 0 
    if alive and n>=4:
        return 0 
    else:
        return alive

def LifeGameUpdate(world, rule):
    w, h = world.shape
    updated_world = np.zeros((w, h))
    for i in range(h):
        for j in range(w):
            im, ip = (i-1) % h, (i+1) % h
            jm, jp = (j-1) % w, (j+1) % w
            neighbours = (
                    world[im, jm], world[im, j], world[im, jp], 
                    world[i, jm], world[i, jp],
                    world[ip, jm], world[ip, j], world[ip, jp])
            updated_world[i, j] = rule(world[i, j], neighbours)
    return updated_world

class LifeGame(object):
    def __init__(self, world, rule):
        self.world = world
        self.rule = rule

    def __call__(self, frame_count):
        plt.cla()
        plt.imshow(self.world)
        self.world = LifeGameUpdate(self.world, self.rule)

if __name__ == '__main__':
    fig = plt.figure()
    world = np.round(np.random.random((100, 100)))
    lifegame = LifeGame(world, Rule)
    anim = matplotlib.animation.FuncAnimation(fig, lifegame, interval=10)
    plt.show()
            

2020年6月28日日曜日

matplotlibでアニメーション

ある画像データをパラメータの変化に合わせてアニメーションをさせながら表示させたい。 そのような場合のメモをしておく。

pcolormesh()を用いて画像データをプロットし、 その下にplot()を用いたパラメータの変化を示す。 さらに、pcolormesh()で表示されている場所をパラメータの変化のグラフ上に線で示す。 pcolormeth()に対するカラーバーの位置も制御したい。 レイアウトの自由度を持たせるために、add_axes()メソッドを用いている。

アニメーションは、

matplotlib.animation.ArtistAnimation
matplotlib.animation.FuncAnimation
のどちらかを用いて行える。

ArtistAnimationでは、各フレームのArtistオブジェクトを保存して、 Animationを作成する。 フレーム数が多くなる場合も考えると、FuncAnimationを利用したい。 ArtistAnimation, FuncAnimationとも、blitキーワードをTrueに設定すると、 指定した場所のみ書き換えるようになるため速く描画できると思われるが、 グラフ(Axes)の外は書き換えが行われなかったりと制約もできてしまう。 pcolormesh()の書き換えでは、軸の枠線がプロットに上書きされて消えてしまうようなことも起こる。 この場合は、blit=Falseを指定することをお勧めする。

MakeData()で、(nx, ny, nz)のshapeをもつデータを作成し、 各zに対する幅nx、高さnyのデータとみて、プロットをしてみる。

FuncAnimation()には、各フレームごとに呼び出される関数を渡す必要がある。 その関数には、フレーム番号を受け取る一つの引数が必要となる。 実際のupdate()関数では、関数内部で用いるfigure、 data、az とフレーム番号を受け取るように書きたい。 そうすると、引数が多くなってしまうので、lambdaを用いて、引数をフレーム番号のみに減らし、 各フレームごとに呼び出される関数としている。

最後のほうにある、

anim.save('anim.gif', writer='pillow')

のコメントを外せば、アニメーションGIFファイルとして保存できる。 writer='pillow'を指定するためにpillow(PIL)をインストールしておく必要がある。

モジュールのバージョン
python : 3.8.3
matplotlib : 3.2.2
numpy : 1.19.0
PIL(pillow): 7.1.2


import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import numpy as np

def MakeData(nx, ny, nz):
    data = np.ones((nx, ny, nz))
    ax = np.linspace(-3, 3, nx)
    ay = np.linspace(-3, 3, ny)
    az = np.linspace(0, np.pi, nz)
    for k, z in enumerate(az):
        for j, y in enumerate(ay): 
            for i, x in enumerate(ax):
                data[i, j, k] = np.sin(x+z) * np.cos(y+z)
    return data, az

def update(fig, data, az, iz):
    fig.clear()
    # create axes
    ax0 = fig.add_axes([0.1, 0.4, 0.7, 0.5])
    cbar_ax0 = fig.add_axes([0.82, 0.4, 0.05, 0.5])
    ax1 = fig.add_axes([0.1, 0.1, 0.7, 0.2])
    
    # plot data
    im0 = ax0.pcolormesh(data[:, :, iz].T)
    fig.colorbar(im0, cax=cbar_ax0)
    ax1.plot(az, '-', color='#0000FF')
    ax1.plot([iz, iz], [0, np.pi], '-', color='#FF0000')

    # put text labels
    label = ax1.text(0.1, 0.8, '%.5g (rad)' % az[iz], 
            transform=ax1.transAxes)
    ax0.set_title('Frame %d' % iz)


nx, ny, nz = 30, 60, 100
data, az = MakeData(nx, ny, nz)

fig = plt.figure(figsize=(5, 6))

anim = FuncAnimation(fig, lambda iz: update(fig, data, az, iz), nz, 
        interval=100, blit=False, repeat=False)

#anim.save('anim.gif', writer='pillow')

plt.show() 

2015年5月23日土曜日

matplotlibのMultiCursor AutoScale時の挙動修正

matplotlibのMultiCursor()の使い方は、公式のページにありますが、matplotlib 1.3.0あたりからクロスラインカーソルも利用できます。

multi = MultiCursor(fig.canvas, (ax1, ax2), color='r', lw=1, horizOn=True)
のように、horizOnキーワード引数にTrueを渡すと水平線を描画します。

ここで、クロスラインカーソルを表示したいaxes、(ax1, ax2)を渡しているのですが、
最後のax2でないaxes、ここではax1の軸がオートスケールになっていると、あれっ?
と思う挙動をします。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import MultiCursor

t = np.arange(0.0, 2.0, 0.01)
s1 = np.sin(2*np.pi*t)+400
s2 = np.sin(4*np.pi*t)
fig = plt.figure()
ax1 = fig.add_subplot(211)
ax1.plot(t, s1)

ax2 = fig.add_subplot(212, sharex=ax1)
ax2.plot(t, s2)

multi = MultiCursor(fig.canvas, (ax1, ax2), color='r', lw=1, horizOn=True)
plt.show()
ax1に描画する波形を400だけオフセットしただけですが、実行すると、以下のようになります。
y軸のレンジが0からになってしまいます。 オートスケールで期待する結果とは異なります。 matplotlibパッケージのwidgets.pyにある、MultiCursorのクラスを見てみると、 どうやら、カーソル線の初期位置の計算に、渡したaxesの最後のものだけを使っているために 生じるようです。

これをwidgets.pyをいじらずに回避するには、MultiCursorの__init__()で行なわれる カーソル線の生成を、別に行えばよさそうです。 そこで、MultiCursorの__init__()には、vertOn、 horizOnともにFalseを渡します。
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import MultiCursor

class MyMultiCursor(MultiCursor):
    def __init__(self, canvas, axes, 
            useblit=True, horizOn=True, vertOn=True, **lineprops): 
        MultiCursor.__init__(self, canvas, axes, 
                useblit=useblit, horizOn=False, vertOn=False, **lineprops)

        xmids = [0.5 * (xmin + xmax) for xmin, xmax in [ax.get_xlim() for ax in axes]] 
        ymids = [0.5 * (ymin + ymax) for ymin, ymax in [ax.get_ylim() for ax in axes]] 

        if self.useblit:
            lineprops['animated'] = True
        if vertOn:
            self.vlines = [ax.axvline(xmids[i], visible=False, **lineprops)
                           for i, ax in enumerate(axes)]
        else:
            self.vlines = []

        if horizOn:
            self.hlines = [ax.axhline(ymids[i], visible=False, **lineprops)
                           for i, ax in enumerate(axes)]
        else:
            self.hlines = []

        self.vertOn, self.horizOn = vertOn, horizOn


t = np.arange(0.0, 2.0, 0.01)
s1 = np.sin(2*np.pi*t)+400
s2 = np.sin(4*np.pi*t)
fig = plt.figure()
ax1 = fig.add_subplot(211)
ax1.plot(t, s1)

ax2 = fig.add_subplot(212, sharex=ax1)
ax2.plot(t, s2)

multi = MyMultiCursor(fig.canvas, (ax1, ax2), color='r', lw=1, horizOn=True)
plt.show()

MultiCursorを継承したMyMultiCursorを利用します。 実行すると、以下のように期待されたグラフが得られました。

2014年1月31日金曜日

matplotlibのMultiCursor

matplotlibは、wxPythonと簡単に組み合わせて利用できて便利ですね。
matplotlibのグラフ上にマウスカーソルに合わせて動く十字のカーソルを表示するMultiCursorというものがありますが、 twinx()してY2軸を利用すると奇妙なことがおこりました。
カーソルは、Y軸に描画されながらも、その表示位置はY2軸上の値になるという状態です。
もちろん、カーソルを描画する対象をY2軸のほうにすると矛盾のない状態にはなります。
そもそもMultiCursorを利用する対象にY2軸を追加するような利用の仕方は、あまりなさそうです。そうはいっても、そのような要求があったため、Y2軸を追加しても、カーソル位置は、Y軸のほうで決まるようにしてみました。
マウス移動のイベントを処理するonmove()に渡されるeventからキャンバス上のx,y座標が得られるので、それを利用してグラフ軸上のデータに焼きなおすようにしました。
使い方はオリジナルのMultiCursorとほぼ同じですが、parentウィンドウを渡すようにして、そちらへ座標を通知するイベントをポストしています。parentウィンドウでそのイベントを拾えば、カーソル移動にあわせて座標位置を表示できます。

from matplotlib.widgets import MultiCursor
import wx
import wx.lib.newevent
class MyMultiCursor(MultiCursor):
    def __init__(self, parent, canvas, axes, color='r', lw=1,  
            useblit = True, horizOn=True, vertOn=True): 
        if matplotlib.__version__ >= '1.3.0':
            MultiCursor.__init__(self, canvas, axes, color=color, 
                    lw=lw, useblit=useblit, horizOn=horizOn, vertOn=vertOn)
        else:
            MultiCursor.__init__(self, canvas, axes, color=color, lw=lw, useblit=useblit)
        
        # Produce New Event for Cursor Move Notification to parent window.
        self.parent = parent
        self.CursorMoveEvent, self.EVT_CURSOR_MOVE = wx.lib.newevent.NewEvent()
        self.visible = False

    def xydata(self, x, y):
        #make new xdata, ydata
        cwidth, cheight = self.canvas.GetSize()
        target = None
        for ax in self.axes:
            #find the axes in which the mouse cursor is.
            bbox = ax.get_position()
            [[left,bottom], [right, top]] = bbox.get_points()
            xc = (x+1)/float(cwidth)
            yc = y/float(cheight) 
            if ((left < xc) and (xc < right) and 
                (bottom < yc) and (yc < top)):
                target = ax
                break
        if not target: 
            # if there is no target
            return (None, None)

        width = right-left
        height =top-bottom
        xmin, xmax = ax.get_xlim()
        ymin, ymax = ax.get_ylim()
        xdata = xmin + (xc - left) * (xmax - xmin)/width
        ydata = ymin + (yc - bottom) * (ymax - ymin)/height
        return (xdata, ydata)

    def onmove(self, event):
        event.xdata, event.ydata = self.xydata(event.x, event.y)
        MultiCursor.onmove(self, event)
        #make event data
        evt = self.CursorMoveEvent(pos = (event.xdata, event.ydata))
        #post event to parent window
        wx.PostEvent(self.parent, evt)
        self.visible = True


2014年1月27日月曜日

matplotlibでorthogonal projectionの3D plot

グラフを作るのに、gnuplotもよいけど、pythonからmatplotlibを使うのももよいですよね。
下の図は、matplotlibで3次元プロットした例ですが、遠近感(perspective effect)が観られます。



しばしば、そのような遠近感の効果をなくして3次元プロットしてみたいという要求があります。
言い換えると、透視投影(perspective projection)から平行投影(orthogonal projection)に変更したいわけです。調べてみたところ、mpl_toolkits.mplot3dの中のproj3dにあるpersp_transformation関数を書き換えればよいということです。

ということで、以下のようにしてみました。

#!/usr/bin/env python

import matplotlib
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy

#--------- Patch for Orthogonal Projection of 3Dplot ------
from mpl_toolkits.mplot3d import proj3d
def orthogonal_transformation(zfront, zback):
    a = 2/(zfront-zback)
    b = -1*(zfront+zback)/(zfront-zback)
    c = zback
    return numpy.array([[1,0,0,0],
                        [0,1,0,0],
                        [0,0,a,b],
                        [0,0,0,c]])
proj3d.persp_transformation = orthogonal_transformation
#-----------------------------------------------------

#---------- make canvas for figure -------------
fig = plt.figure(figsize=(6, 6), facecolor='w')

#---------- 3D Plot ----------------
ax = fig.add_subplot(111, projection='3d', axisbg='w')

# making sample data
x = numpy.arange(-3, 3, 0.25)
y = numpy.arange(-3, 3, 0.25)
xm, ym = numpy.meshgrid(x, y)
z = numpy.exp(-1*xm**2) * numpy.exp(-1*ym**2)

# draw x, y, z labels
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')

# plot 3d
surf = ax.plot_surface(xm, ym, z, rstride=1, cstride=1,
        linewidth=1, antialiased=True)

# set view point
ax.view_init(elev=30, azim=-45)

# display the figures
plt.show()
このスクリプトでプロットすると、下のようになりました。