LoginSignup
22
17

More than 3 years have passed since last update.

【Matplotlib入門】Axes3Dアニメーション:3Dリサジュー図形で遊んでみた♬

Last updated at Posted at 2020-09-09

3日前にTwitterに流れて来た以下のSin-Cos-tの3Dアニメ、ハマったのでまとめておきます。
普通のリサジュー図形だろという軽いノリでもっと面白そうなリサジュー図形を描きたくて、。。。いやハマりました。
https://twitter.com/i/status/1302164499139502080
同じ絵を描こうと思いましたが、切りのいい以下のところで断念。
※あと、⇒とSinとCosの先端から渦巻の先端を結ぶだけで、やり方見えたけどここで止めます。
sin_wave_el20.0_az35.0.gif

やったこと

・Axes3Dのアニメーション
・Sin-Cos-tのアニメーション
・update()関数を直接呼んで描画する
・Z軸をずらす
・図形の回転
・3Dリサジュー図形

・Axes3Dのアニメーション

当初、3Dとはいえ、参考のようなもので行けるだろうともやっと考えていました。
【参考】
【matplotlib基本】動的グラフを書いてみる♬~動画出力;Gifアニメーション
実際、sin-cos-tでは、ググると似たようなものは以下のようなコードが見つかります。
【参考】
[Matplotlib] 3次元データの可視化

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# Figureを追加
fig = plt.figure(figsize = (8, 8))
# 3DAxesを追加
ax = fig.add_subplot(111, projection='3d')
# Axesのタイトルを設定
ax.set_title("Helix", size = 20)
# 軸ラベルを設定
ax.set_xlabel("x", size = 14)
ax.set_ylabel("y", size = 14)
ax.set_zlabel("z", size = 14)
# 軸目盛を設定
ax.set_xticks([-1.0, -0.5, 0.0, 0.5, 1.0])
ax.set_yticks([-1.0, -0.5, 0.0, 0.5, 1.0])
# 円周率の定義
pi = np.pi
# パラメータ分割数
n = 256
# パラメータtを作成
t = np.linspace(-6*pi, 6*pi, n)
# らせんの方程式
x = np.cos(t)
y = np.sin(t)
z = t
# 曲線を描画
ax.plot(x, y, z, color = "red")
plt.show()

出力は以下のとおり、動かないけど似たような図が得られました。
あとは、動かせばいいということで、アニメーションを探します。
Figure_helix.png
簡単に見つかるのは、matplotlib.orgのAn animated plot in 3D.が見つかりますが、以下でより近いものが見つかりました。

・Sin-Cos-tのアニメーション

【参考】
3D animation using matplotlib
これは、ほぼ答えになりそうです。

from matplotlib import pyplot as plt
import numpy as np
import mpl_toolkits.mplot3d.axes3d as p3
from matplotlib import animation

fig = plt.figure()
ax = p3.Axes3D(fig)

def gen(n):
    phi = 0
    while phi < 2*np.pi:
        yield np.array([np.cos(phi), np.sin(phi), phi])
        phi += 2*np.pi/n

def update(num, data, line):
    line.set_data(data[:2, :num])
    line.set_3d_properties(data[2, :num])

N = 100
data = np.array(list(gen(N))).T
line, = ax.plot(data[0, 0:1], data[1, 0:1], data[2, 0:1])
# Setting the axes properties
ax.set_xlim3d([-1.0, 1.0])
ax.set_xlabel('X')
ax.set_ylim3d([-1.0, 1.0])
ax.set_ylabel('Y')
ax.set_zlim3d([0.0, 10.0])
ax.set_zlabel('Z')
ani = animation.FuncAnimation(fig, update, N, fargs=(data, line), interval=10000/N, blit=False)
#ani.save('matplot003.gif', writer='imagemagick')
plt.show()

matplot003.gif
ところが、3つのアニメーションを描こうとすると、以下のようになります。

aniy = animation.FuncAnimation(fig, update, N, fargs=(datay, liney), interval=10000/N, blit=False)
anix = animation.FuncAnimation(fig, update, N, fargs=(datax, linex), interval=10000/N, blit=False)
aniz = animation.FuncAnimation(fig, update, N, fargs=(dataxy, linexy), interval=10000/N, blit=False)

この方式だと、絵的には似たようなアニメーションが得られますが、この3つのアニメーションをまとめて保存する方法が思いつきません。

・update()関数を直接呼んで描画する

以下のようにそれぞれをplt.pause(0.001)でまとめてプロットし、その図を保存することとしました。最後にそれらの図から、gif_animationを生成することとしました。

frames =100
s=180
for n in range(frames): 
    s+=1
    num = s%800+200
    update(num,datax,linex)
    update(num,datay,liney)
    update(num,dataz,linez)
    plt.pause(0.001)
    plt.savefig('./sin_wave/'+str(n)+'.png'.format(elev, azim))

from PIL import Image,ImageFilter
images = []
for n in range(frames):
    exec('a'+str(n)+'=Image.open("./sin_wave/'+str(n)+'.png")')
    images.append(eval('a'+str(n)))
images[0].save('./sin_wave/sin_wave_el{}_az{}.gif'.format(elev, azim),
               save_all=True,
               append_images=images[1:],
               duration=100,
               loop=0)

そして、以下のコードのようにline.set_data(data[:2,num-200 :num])とnum = s%800+200により描画範囲を限定することにより、結果は所望のものに近づきました。
sin_wave_el0_az0.gif

from matplotlib import pyplot as plt
import numpy as np
import mpl_toolkits.mplot3d.axes3d as p3
from matplotlib import animation

fig = plt.figure()
ax = p3.Axes3D(fig)

def genxy(n,fx,fy,fxc=0,fyc=0):
    phi = 0
    sk = 0
    while phi < 360:
        yield np.array([fx*np.cos(phi)+fxc, fy*np.sin(phi)+fyc,phi])
        phi += 36*5/n

def update(num, data, line):
    line.set_data(data[:2,num-200 :num]) #描画区間を制限する
    line.set_3d_properties(data[2,num-200 :num])

N = 960
datay = np.array(list(genxy(N,fx=0,fy=1,fxc=-2,fyc=0))).T
liney, = ax.plot(datay[0, 0:1], datay[1, 0:1], datay[2, 0:1])
datax = np.array(list(genxy(N,fx=1,fy=0,fxc=0,fyc=2))).T
linex, = ax.plot(datax[0, 0:1], datax[1, 0:1], datax[2, 0:1])
dataz = np.array(list(genxy(N,fx=1,fy=1,fxc=0,fyc=0))).T
linez, = ax.plot(dataz[0, 0:1], dataz[1, 0:1], dataz[2, 0:1])

# Setting the axes properties
ax.set_xlim3d([-2.0,2.0])
ax.set_xlabel('X')

ax.set_ylim3d([-2.0, 2.0])
ax.set_ylabel('Y')

ax.set_zlim3d([36.0, 80.])
ax.set_zlabel('Z')

frames =36
s=180
for n in range(frames):
    s+=1
    num = s%800+200
    update(num,datax,linex)
    update(num,datay,liney)
    update(num,dataz,linez)
    plt.pause(0.001)
    plt.savefig('./sin_wave/'+str(n)+'.png'.format(0, 0))

from PIL import Image,ImageFilter
images = []
for n in range(frames):
    exec('a'+str(n)+'=Image.open("./sin_wave/'+str(n)+'.png")')
    images.append(eval('a'+str(n)))
images[0].save('./sin_wave/sin_wave_el{}_az{}.gif'.format(0, 0),
               save_all=True,
               append_images=images[1:],
               duration=100,
               loop=0)    

・Z軸をずらす

update()関数を以下のように書き換えて、Z軸をずらすことにします。
これによって、画像のZ軸方向は同じ位置に描画されるようになりました。
ここで軸の表示も消去します。
※因みに、Z軸の目盛も消したいところですが、最後まで消せませんでした

def update(num, data, line):
    line.set_data(data[:2,num-200 :num])
    line.set_3d_properties(data[2,num-200 :num])
    ax.set_zlim3d([0+data[2,num-200], 34+data[2,num-200]])
    ax.set_zticklabels([])
    ax.grid(False)

sin_wave_el0_az0.gif

・図形の回転

さて、あとは座標軸をあの配置に持っていければとりあえずの完成です。
が、これが難しかったのです。
まず、軸の回転は以下で行えます。
以下では初期値の絵を軸を変えて出力しています。

for elev in range(0,360,10): #仰角の変更
    for azim in range(-180,180,5): #方位角の変更
        ax.view_init(elev, azim) #ここで座標軸の向きを調整
        frames =1  #34
        s=180
        for n in range(1): #frames
            s+=1
            num = s%800+200
            update(num,datax,linex)
            update(num,datay,liney)
            update(num,dataz,linez)
            plt.pause(0.001)
            plt.savefig('./sin_wave/'+str(n)+'{}{}.png'.format(elev, azim))

結果、あの配置は得られませんでした。
イマジネーションが足りないのですね。
上記の角度範囲以外が思いつきません。
しかし、X-Y-Zの軸見出しを変更することを思いつきました。
yield np.array([phi,fx*np.cos(phi)+fxc, fy*np.sin(phi)+fyc])
としています。
つまり、軸見出しを変更すれば螺旋が横に寝てくれます。
ということで、以下のコードにたどり着きました。

from matplotlib import pyplot as plt
import numpy as np
import mpl_toolkits.mplot3d.axes3d as p3
from matplotlib import animation

fig = plt.figure()
ax = p3.Axes3D(fig)

def genxy(n,fx,fy,fxc=0,fyc=0):
    phi = 0
    while phi < 360:
        yield np.array([phi,fx*np.cos(phi)+fxc, fy*np.sin(phi)+fyc])
        phi += 36*5/n

def update(num, data, line):
    line.set_data(data[:2,num-200 :num])
    line.set_3d_properties(data[2,num-200 :num])
    ax.set_xlim3d([0+data[0,num-200], 36+data[0,num-200]])
    ax.set_xticklabels([])
    ax.grid(False)

N = 960
datay = np.array(list(genxy(N,fx=0,fy=1,fxc=-2,fyc=0))).T
liney, = ax.plot(datay[0, 0:1], datay[1, 0:1], datay[2, 0:1])
datax = np.array(list(genxy(N,fx=1,fy=0,fxc=0,fyc=-2))).T
linex, = ax.plot(datax[0, 0:1], datax[1, 0:1], datax[2, 0:1])
dataz = np.array(list(genxy(N,fx=1,fy=1,fxc=0,fyc=0))).T
linez, = ax.plot(dataz[0, 0:1], dataz[1, 0:1], dataz[2, 0:1])

# Setting the axes properties
ax.set_xlim3d([20, 70.0])
ax.set_xlabel('Z')
ax.set_ylim3d([-2.0, 2.0])
ax.set_ylabel('X')
ax.set_zlim3d([-2.0, 2.0])
ax.set_zlabel('Y')

elev=20. 
azim=35.
ax.view_init(elev, azim)

frames =100
s=180
for n in range(frames): #frames
    s+=1
    num = s%800+200
    update(num,datax,linex)
    update(num,datay,liney)
    update(num,dataz,linez)
    plt.pause(0.001)
    plt.savefig('./sin_wave/'+str(n)+'.png'.format(elev, azim))

from PIL import Image,ImageFilter
images = []
for n in range(frames):
    exec('a'+str(n)+'=Image.open("./sin_wave/'+str(n)+'.png")')
    images.append(eval('a'+str(n)))
images[0].save('./sin_wave/sin_wave_el{}_az{}.gif'.format(elev, azim),
               save_all=True,
               append_images=images[1:],
               duration=100,
               loop=0)

こうして、最初に掲載した3Dアニメーションが出力できました。

・3Dリサジュー図形

もっとも、本来やりたかったのは3Dリサジュー図形なので、いくつかやってみます。
以下を変更して滑らかに繋がるようにframes=200を変更すれば出来ます。
yield np.array([phi,fx*np.cos(3*phi/2)+fxc, fy*np.sin(2*phi/3)+fyc])

X : Y = 1 : 2
sin_wave1-2_el20.0_az35.0.gif
X : Y = 1/2 : 2/3
sin_wave1w2-2w3_el20.0_az35.0.gif
X : Y = 3/2 : 2/3
sin_wave3w2-2w3-2_el20.0_az35.0.gif

まとめ

・Sin-Cos-tの3Dアニメーションを描いて見た
・3Dアニメーションが描けるようになった
・3Dリサジュー図形を描いてみた

※二次元リサジュー図形(上記のXY面への写像)はググってみてください

おまけ

完成版 
 原点にリサジュー図形を描画するように改良しました
3d_animation_Risajyu/3D_animation_risajyu.py

point追加
sin_wave_point_el20.0_az35.0.gif

22
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
17