grbl1.1+Arduino CNCシールドV3.5+bCNCを使用中。
BluetoothモジュールおよびbCNCのPendant機能でスマホからもワイヤレス操作可能。
その他、電子工作・プログラミング、機械学習などもやっています。
MacとUbuntuを使用。

CNCマシン全般について:
国内レーザー加工機と中国製レーザー加工機の比較
中国製レーザーダイオードについて
CNCミリングマシンとCNCルーターマシンいろいろ
その他:
利用例や付加機能など:
CNCルーター関係:



*CNCマシンの制作記録は2016/04/10〜の投稿に書いてあります。


2018年4月23日月曜日

Jupyter Notebook: matplotlib /fill() + fill_between() Animation

前回のmatplotlibアニメーションの続き。
今回は領域を塗るfill()とfill_between()関数を使ってみました。静止画なら簡単なのですが、アニメーションになると思った通りにいかないことが多々あったのでメモ。

まず、アニメーションでは:

ArtistAnimation()
FuncAnimation()

がありますが、今回は比較的簡単なArtistAnimationを使用。
追記:
FuncAnimation()の手抜きのやり方も追加しておきました(投稿中程)。

そしてグラフ領域の塗り分けについては、

fill(x, y)
fill_between(x, y1, y2)

があるのですが、
fill()に渡すxとyは、塗る領域を指定するリストにしなければならない。これが少し面倒。
fill_between()の場合は、xはそのままxの数式、y1に下限の数式、y2上限の数式を入れればいいだけなので簡単です。

今回つくりたいグラフは以下のような感じです(今回つくったGIF動画です)。fill_between()を使えば簡単にできそうですが結構ハマりました。
このような領域を塗るサンプルを見てみると、アニメーションのときはfill()を使っていることが多かったのですが、塗る領域を座標指定するのが少々面倒。数式代入だけで済むfill_between()を使うと、

TypeError: unsupported operand type(s) for +: 'PolyCollection' and 'list'

こんな感じのエラーが出て、ポリコレクションとリストを一緒に入れるなみたいに怒られます。便利な分、複雑なデータ形式でそのままでは使えないのかと、ポリコレクションを調べたり、その内部の値にアクセスする方法はないのか探したりしていました。どうやらfill_between()はアニメーションでは使いにくいのか、参考になるサンプルもあまりありませんでした。しかし、非常に単純な理由でfill_between()も使えるということがわかりました。なんだそんなことかという感じです。


結論:
fill_between()が簡単。
ArtistAnimationを使うなら、単にプロットした内容を空のリストにappend()していけばいいのですが、そのままfill_between()のプロットを入れると、上記のようなエラーが出て受け付けません。そこで、[ ]で括って無理やりリスト形式にして入れると動きました。たったそれだけの違いでした。

これに気づくまでかなり時間かかりました。結局は少し面倒だけど、fill()を使うしかないのかなと思っていたところ、ようやく上手くいったという感じです。


Fill_between() + ArtistAnimation()の場合:
一応、Jupyter Notebookを使う前提です。
今回は、HTML()で出力するのでバックエンドは「inline」。前回の投稿に追記しましたが、ipymplがバージョンアップしたので、それを使うこともできます(その場合は「nbagg」になる)。ただし、「inline」のほうが安定しているので、今回はそのまま。


%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation
from IPython.display import HTML

x = np.arange(0,10,0.2)     # x値範囲:(xmin,xmax,pitch)
y = x*np.sin(x)             # メイン関数
sigma = np.cos(x*2) + 2     # シグマ計算式
y1 = y - sigma              # シグマ下限
y2 = y + sigma              # シグマ上限

fig = plt.figure()
plt.axis([0,10,-10,10])     # グラフ範囲:[xmin,xmax,ymin,ymax]

ims = []                    # アニメ用プロット格納リスト

for i in range(len(x)):
    P0 = plt.plot(x,y,'b--',alpha=0.3)                                    # 固定描画(青破線)
    P1 = plt.fill_between(x[:i+1],y1[:i+1],y2[:i+1],color='r',alpha=0.2)  # シグマ値描画 (塗り幅:赤帯)
    P2 = plt.plot(x[:i+1],y[:i+1], 'r-')                                  # 軌跡描画(赤実線)
    P3 = plt.plot(x[i],y[i], 'ro')                                        # 移動描画(赤丸)
    ims.append(P0 + [P1] + P2 + P3)     # 各プロット格納:fill_beteenのプロットP1だけ[]で括って入れる
    
ani = animation.ArtistAnimation(fig=fig, artists=ims, interval=100)       # アニメ関数
plt.close()

HTML(ani.to_html5_video())                                                # HTML5 Video 出力
#HTML(ani.to_jshtml())                                                    # JavascriptHTML 出力

先ほど書いたように、ims=[]に複数のプロットを格納して行く際、fill_between()でつくったプロットP1だけ[]で括って入れます。

ims.append(P0 + [P1] + P2 + P3)

こうすれば、ポリコレクションであっても受け付けてくれるようです。
それから今回気づきましたが、変化しない固定グラフはアニメーションリストに加えなくてもいいと思っていましたが、そうするとアルファ値などが反映されなくなってしまう(どんどん上塗りされて濃くなっていく)ので、リストに加えることにしました。


追加:Fill_between() + FuncAnimation()の場合:
追加しました。
この組み合わせだけ上手くいかず、いろいろ実験していましたが、またもや非常に簡単な方法で表示可能となりました。
今回はフレーム数を少なめにして、そのかわり各座標にポリゴン描画する順番で塗面の座標値に番号を記入してみました。
fill_between()なので、fill()のようにポリゴン座標値は必要ないのですが、位置などずれていないか確かめるために挿入してみました。以下に続く、fill()を使う場合にはこの座標値の順番が参考になるかと。

FuncAnimationの場合、plot関数に予め空の[]を入れておき、update(i)関数内でその値を更新していくというパターンが多いのですが、今回は表示させたいものを全てupdate(i)関数内に入れてしまい無理やりアニメーション化しました。しかしそうすると画面がリフレッシュされないために、最初にax.cla()を入れておきました。特に渡す変数などなくて非常に簡単です。


import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from IPython.display import HTML
plt.style.use('ggplot')

fig = plt.figure(figsize=(5,4))
ax = plt.subplot()

x = np.arange(0,10,0.4)
y = x*np.sin(x)
s = np.cos(x) + 2
y1 = y - s
y2 = y + s

def update(i):
    ax.cla()                                                          # 事前に画面クリアで内容更新
    ax.axis([0,10,-8,8])                                              # 座標範囲設定
    ax.plot(x,y,'r--',alpha=0.2)                                      # メイン関数固定プロット(赤破線)
    ax.plot(x[:i+1],y[:i+1],'r-',lw=3)                                # 軌跡描画(赤実線)
    ax.fill_between(x[:i+1],y1[:i+1],y2[:i+1],color='r',alpha=0.1)    # 塗面描画(透過赤:10%)
    
    xs = np.concatenate([x[:i+1],x[i::-1]])                           # 塗面ポリゴン座標番号リスト
    ys = np.concatenate([y1[:i+1],y2[i::-1]])
#     ax.plot(x[:i+1],y1[:i+1],'b+')                                  # 塗面下端描画(青+:現在非表示)
#     ax.plot(x[:i+1],y2[:i+1],'g+')                                  # 塗面上端描画(緑+:現在非表示)
    for j in range(len(x[:i+1])*2):    
        ax.text(xs[j],ys[j],str(j),fontsize=10,color='m')             # 塗面ポリゴン座標番号表示
        ax.plot(xs[j],ys[j],'r+')                                     # 各座標位置を+で表示
    
    return fig,                                                       # とりあえずfigをリターン

ani = animation.FuncAnimation(fig, update,frames=len(x),interval=200) # Funcアニメーション使用
# ani.save("fb_func.gif", writer = "imagemagick")                     # GIFアニメ保存
plt.close()
HTML(ani.to_html5_video())                                            # HTML5 Video 出力
#HTML(ani.to_jshtml())        


特に難しいところはないのですが、update(i)関数からのリターンがないとエラーがでるので、特に影響のでないfigをリターンさせています。ax.cla()で毎フレームクリアしているためか、表示範囲が崩れることがあります。そのため、すぐにax.axis()で表示範囲を設定する必要があります。
基本的に、plot()やfill_between()、そしてtext()など、表示させたいものをupdate(i)関数内に入れてあるだけです。本来のFuncAnimation()のやり方のようにきちんとset関数などで値を更新してあげればax.cla()でリフレッシュする必要もないのでしょうが、とりあえず非常に簡単に表示できるのでこの方法も便利かと。
fill_between()におけるx、y1、y2のset関数が見当たらず、Pathを使って自前で塗面描画するはめになったりと、かえって手間がかかってしまったので、今回のようなやり方が発見できてよかったです。


x[i]とx[:i+1]について:
ここもちょっとハマりました。
ArtistAnimationの場合は毎回ループでi番目をプロットして見せるだけなので、そのまま見せたいプロット関数を並べておけばいいだけです。
しかし今回のように、過去の軌跡も表示するグラフ、変化する現在位置だけを表示するグラフが共存すると表示されるindex番号が一コマずれてしまうことが発生。アニメーションのピッチがこまかければ気づきにくい点ですが、よくみるとx[i]とx[:i]の現在地がずれていました。
そのため、現在地だけを表示するx[i]を基準にして、軌跡も表すx[:i]についてはx[i+1]にして表示する部分を揃えることにしました。
逆に、x[:i]を基準にして、x[i]をx[i-1]にしてしまうと最後のコマが表示されなくなるので、x[i]を基準にしたほうがいいということになったわけです。

このindex番号については、次に書くfill()において結構悩みました。


Fill() + ArtistAnimation()の場合:
fill_between()と比較するために、fill()のコードも書いてみました。やはり少し面倒でした。数式をそのまま代入すればいいfill_between()とは違って、塗面の範囲を座標を使って囲んでいかなければいけません。


%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation
from IPython.display import HTML

x = np.arange(0, 10, 0.2)                                    # x値範囲:(xmin,xmax,pitch)
y = x*np.sin(x)                                              # 描画用基本関数(任意)
sigma = np.cos(x*2) + 2                                      # シグマ値(線幅変化用)

y_high = y + sigma                                           # シグマ上限値
y_low = y - sigma                                            # シグマ下限値

fig = plt.figure()
plt.axis([0,10,-10,10])                                      #グラフ範囲指定:[xmin,xmax,ymin,ymax]

ims = []                                                     # アニメプロット格納リスト

for i in range(len(x)): 
    P0 = plt.plot(x,y,'g--',alpha=0.3)                       # 固定描画(緑破線)
    
    xf = np.concatenate([x[:i+1], (x[:i+1])[::-1]])          # fill用x: [x順列,x逆列]
    yf = np.concatenate([y_low[:i+1], (y_high[:i+1])[::-1]]) # fill用y: [y下限順列,y上限逆列]
    P1 = plt.fill(xf,yf,facecolor='b', alpha=0.2)            # シグマ値描画 (塗り幅:青帯)
    
    P2 = plt.plot(x[:i+1],y[:i+1],'b-')                      # 移動点軌跡描画(青実線)    
    P3 = plt.plot(x[i],y[i],'bo')                            # 移動点描画(青丸)
    
    ims.append(P0 + P1 + P2 + P3)                            # 各プロットをアニメ用リストに格納

ani = animation.ArtistAnimation(fig,ims,interval=100)        # アニメ関数
# ani.save("anim.gif", writer = "imagemagick")               # GIFアニメ保存(Ubuntu用にImagemagick使用)
plt.close()
HTML(ani.to_html5_video())                                   # HTML5 Video 出力(mp4ダウンロード可)
# HTML(ani.to_jshtml())                                      # JavascriptHTML 出力

内容はほぼ同じですが、fill()を使う場合はnp.concatenate()を使って、塗る領域の座標値リストをつくらなければいけないようです。そのまま式をいれただけだときちんと表示されませんでした。
また、ims.append()にはfill()を格納した変数をそのまま入れることができます。fill_between()のときのように[]でくくって入れる必要はないので、その分使いやすかったのですが、今となってはfill_between()の方が簡単。
今回の場合、塗る領域の下端の線と上端の線を連結したリストをつくり、それをfill()に渡してあげればいいようです。
以下がそれの図解です。
赤破線がメイン関数で、それに沿って帯状に塗るわけですが、緑の下端線0〜6と上端線7〜13までをつないだリストをつくるということです。0と13が閉じていませんが、内部的な処理では最後の座標で図形を閉じるので自動的に0と13はつながるようです。
面倒なのは、下端線が左から右、上端線が右から左という順番で並んでいるので、その順番にリスト内の要素も並べないといけないという部分。
ポリゴン図形を描くように、領域をぐるりと回るような座標取りになっています。
x座標は単なる往復でいいのですが、yは下端(順方向)と上端(逆方向)を組み合わせなければならないというわけです。

しかも今回は表示される区間が変化するため、もう少し面倒になります。
ということから、i番目まで表示するなら、

xf = x[:i+1] + x[i::-1]

となります。

x[:i+1] = i番目までの値全てのリスト
x[i::-1] = i番目以下すべてを反転したリスト

ただし、np.concatenate()を使って連結します。

xf = np.concatenate([ x[:i+1], x[i::-1] ])

二重の[]で括られている部分に要注意。
この手続きを踏んでfill()に渡してあげればいいというわけです。

しかし、ここでまたindexをiにするかi+1にするかで一コマずれの表示が発生したり、連結した座標値リストのサイズがxとyとで異なりエラーがでたりと、少々面倒です。最終的に調整した結果、ソースコードのようになっています。


リストのindex番号について:
x[:i]とかx[i::-1]など分かりにくいので以下にまとめてみました。
尚、リバース関数x.reverse()を使うと、元のxが反転してしまうようなので、今回は一時的に反転したりしなかったりするので使わないことに。そのかわり、リスト内全ての値の並びを反転するにはx[::-1]を使っています。
例えば、以下のようなリストがあったとします、


x = np.arange(5)                             # [0 1 2 3 4]
i= 4                                         # 0 1 2 3 4 
print(x[i])                                  # 4            i番目の値
print(x[:i])                                 # [0 1 2 3]    i未満全て(i含まず)
print(x[i:])                                 # [4]          i以上全て

#以下、反転パターン
print(x[::-1])                               # [4 3 2 1 0] 全ての反転
print(x[i::-1])                              # [4 3 2 1 0] i以下全ての反転
print((x[:i])[::-1])                         # [3 2 1 0]    i未満の全ての反転

print(np.concatenate([x[:i],x[i::-1]]))      # [0 1 2 3 4 3 2 1 0]
print(np.concatenate([x[:i],(x[:i])[::-1]])) # [0 1 2 3 3 2 1 0]


fill()を使う際どの組み合わせがいいのかいろいろ試してみました。要素数が一つ足りなくてエラーがでたときもありましたが、表示上一コマずれていたりと微妙に違います。
また、先ほど書いたように、x[i]を基準とすると(i番目の座標を表示しようとすると)、過去の軌跡表示に使用するx[:i]の場合は、i番目未満までの表示となってしまうため、x[:i]をx[:i+1]に変更して、現在位置とそれ以前の軌跡を表示しなければいけなくなり、結構面倒です。ちなみに、現在位置と軌跡を含めた表現に使うx[:i+1]を基準に比較してみると、、


x = np.arange(5)                                 # [0 1 2 3 4]
i= 4                                             # 0 1 2 3 4 
print(x[i])                                      # 4          
print(x[:i+1])                                   # [0 1 2 3 4]

print(x[i+1::-1])                                # [4 3 2 1 0]
print((x[:i+1])[i::-1])                          # [4 3 2 1 0]

print(np.concatenate([x[:i+1],x[i+1::-1]]))      # [0 1 2 3 4 4 3 2 1 0]
print(np.concatenate([x[:i+1],(x[:i+1])[::-1]])) # [0 1 2 3 4 4 3 2 1 0]

このようになり、こちらのほうが今回の場合は都合がよさそうです。特に最後の行はfill()に代入するために連結させた結果、ひとつ前の例だと、

[0,1,2,3,4,3,2,1,0]

のように4が真ん中にひとつしかなかったり、

[0,1,2,3,3,2,1,0]

のように4が抜け落ちていたりすることがあります。
今回必要だったのは、

[0,1,2,3,4,4,3,2,1,0]

だったので、ソースコードに書いてある方法を採用しました。
特に、

x[:i] = [0,1,2,3]
x[:i+1] = [0,1,2,3,4]

の違いだったり、

x[i::-1] = [4,3,2,1,0]
x[:i+1::-1] = [4,3,2,1,0]
(x[:i+1])[::-1] = [4,3,2,1,0]

が同じだったりすることで、連結した結果が微妙に異なりました。最終的には、やや見づらいけれども、

(x[:i+1])[i::-1] = [4,3,2,1,0]

を使うことにしました。
この場合、x[:i+1]を一旦括弧()でくくって、それを[::-1]を使って反転させています。というのは、iに最大値4を入れたときはいいのですが、最小値0を入れるとまた微妙に結果が変わってくるからです。


x = np.arange(5)                                 # [0 1 2 3 4]
i= 0                                             # 0 1 2 3 4 
print(x[i])                                      # 0
print(x[:i+1])                                   # [0]

print(x[i+1::-1])                                # [1 0]
print((x[:i+1])[i::-1])                          # [0]

print(np.concatenate([x[:i+1],x[i+1::-1]]))      # [0 1 0]
print(np.concatenate([x[:i+1],(x[:i+1])[::-1]])) # [0 0]

先ほどは下2行の結果は同じでしたが、今回は違います。求めている結果は[0,1,0]ではなく[0,0]なので最後のやり方を使いました。
この辺の調整は理屈というよりも、やりながら挙動を確かめて修正という感じです。


まとめ:
ということから、fill()を使うといろいろと勉強にはなったけど面倒です。やはり、fill_between()のほうがはるかに簡単でした。

関連:

0 件のコメント:

コメントを投稿

人気の投稿