I am trying to use matplotlib's FuncAnimation to make an animated video. Each frame is just a boolean n x n array visualised as white/black squares. I can do this successfully by defining all the arrays in advance and then going through them one by one. This uses code similar to matplotlib's example.
My items are rather large and I want to run the simulation for a long time. I thus don't want to create the entire list of arrays then go through them one by one. Instead, I want to define the animate function to do each step. Let me explain with a minimal non-working example. My actual example includes far larger arrays!
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
def create_video(n):
global X
X = np.random.binomial(1, 0.3, size = (n,n))
fig = plt.figure()
im = plt.imshow(X, cmap = plt.cm.gray)
def animate(t):
global X
X = np.roll(X, +1, axis = 0)
im.set_array(X)
anim = FuncAnimation(
fig,
animate,
frames = 100,
interval = 1000 / 30,
blit = True
)
return anim
anim = create_video(10)
This initialises some random 10 x 10 set of 0/1s then just 'rolls' it at each step. I get an error.
RuntimeError: The animation function must return a sequence of Artist objects.
If I remove the return anim, replacing it with pass, and replacing anim = create_video(10) with create_video(10), then I get a warning.
UserWarning: Animation was deleted without rendering anything. This is most likely unintended. To prevent deletion, assign the Animation to a variable that exists for as long as you need the Animation.
Clearly, I don't understand well enough FuncAnimation. What I want to happen is for the function animate to update the array X, by 'rolling' it one step, as well as doing im.set_array(X).
As explained in this answer:
As the error suggests, and as can be seen e.g. in the
simple_animation example, but also from the FuncAnimation
documentation, the init_func as well as the updating func are
supposed to return an iterable of artists to animate.
The documentation does not say that this is actually only needed when
using blit=True, but since you are using blitting here, it is
definitely needed.
So you have two ways:
add
return im,
to animate function
set blit = False in FuncAnimation
Complete Code
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
def create_video(n):
global X
X = np.random.binomial(1, 0.3, size = (n, n))
fig = plt.figure()
im = plt.imshow(X, cmap = plt.cm.gray)
def animate(t):
global X
X = np.roll(X, +1, axis = 0)
im.set_array(X)
return im,
anim = FuncAnimation(
fig,
animate,
frames = 100,
interval = 1000/30,
blit = True
)
plt.show()
return anim
anim = create_video(10)
Related
I'm trying to animate a figure using matplotlib->FuncAnimate function. However, I'm having trouble understanding how Blit works. With each frame, I want to draw only the new data point on top of the old one. It says that using Blit it should automatically update only the values that changed. Thus, if I turn it on (blit=True) the previous data points should remain in my figure. But this is not the case. The previous data get deleted and the figure gets redraw from scratch.
In the documentation, it says that I have to return "iterable_of_artists" and the algorithm will know which data has changed. I want to just pass the new data and just plot on top of the old one. By the way, what is an "iterable_of_artists", is that just a list of objects that can be drawn? if someone could point me out to the definition, I would appreciate it.
Anyway, I have worked several base examples that show the odd behavior. In the first example, I'm turning Blit=True and drawing only the new data using the animate function. This in theory should draw on top of the old ones, but is not the case, only the new data is drawn.
import time
import random
import numpy
import matplotlib
import matplotlib.pyplot as pyplot
from matplotlib.animation import FuncAnimation
def livePlot():
fig, ax = pyplot.subplots(1,1)
ax = pyplot.axes(xlim=(0, 2), ylim=(0, 100))
line, = ax.plot([], [], 'ro') #ax.plot will return a tupple
def init():
line.set_data(0, 50)
return line, #Return is not necessary when blit=False
def animate(frame):
x = frame
y = random.randint(0, 100)
line.set_data(x,y)
return line, #Return is not necessary when blit=False
animation = FuncAnimation(
fig, animate,
init_func = init,
frames= [0.5, 1, 1.5, 2.0],
interval=1000,
repeat=False,
blit=True, # Turning on Blit
cache_frame_data = True)
pyplot.show()
if __name__ == "__main__":
livePlot()
I was able to achieve my goal by tricking the FuncAnimate method. I can use the ax and plot in each frame the new data. If I do that, the old data remains and only the new data is drawn. However, I can do that with Blit=True or Blit=False, it has no effect. So, I'm so confused on how Blit works and what would be the correct way to plot only the new data without having to create a list with all the data to plot. Passing a large list will create a large variable in memory if I have a long set of data points. Here is my workaround but I'm not sure if this is the correct way to do it or if there is a better ways of using Blit=True and just redraw the new data.
import time
import random
import numpy
import matplotlib
import matplotlib.pyplot as pyplot
from matplotlib.animation import FuncAnimation
def livePlot():
fig, ax = pyplot.subplots(1,1)
ax = pyplot.axes(xlim=(0, 2), ylim=(0, 100))
def init():
ax.plot(0, 50, 'ro')
return []
def animate(frame):
x = frame
y = random.randint(0, 100)
ax.plot(x, y, 'ro') # plotting directly on the axis. This keeps the old data
return [] # fooling the blit algorithm with an empty stream
animation = FuncAnimation(
fig, animate,
init_func = init,
frames= [0.5, 1, 1.5, 2.0],
interval=1000,
repeat=False,
blit=True,
cache_frame_data = True)
pyplot.show()
if __name__ == "__main__":
livePlot()
I want to animate some time propagation of a wavefunction. But I don't want to calculate all time steps every time because it takes a huge amount of time but take the previous value of the wavefunction as initial value. I don't know how to realize this with animation.FuncAnimation.
That's what I thought of:
import numpy as np
from matplotlib import animation
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
wavefunction_0 = some array
def next_wavefunction(wavefunction_init):
wavefunction = time_propagation(Psi = wavefunction_init)
return wavefunction
def animate(framenumber, wavefunction, surf):
if framenumber == 0:
wavefunction = wavefunction_0
else:
wavefunction = next_wavefunction(wavefunction)
ax.clear()
surf = ax.plot_surface(X, Y, np.reshape(np.abs(wavefunction), (space_shape)), rstride=1, cstride=1, linewidth=0, antialiased=False, cmap='jet', edgecolor='none')
return surf, wavefunction
anim = animation.FuncAnimation(fig, animate, fargs=(wavefunction, surf),
interval=200, blit=False)
At the moment it is not working since fargs = wavefunction but wavefunction is a return value of animate(...). Is it possible to take the return value of animate and pass it as fargs?
Matplotlib expects that the animate function passed to matplotlib.animation.FuncAnimation returns a list of artists, as such it is not possible (at least to my understanding) to return non-artists like
return surf, wavefunction
So even if you pass wavefunction into animate, you would not be able to return the mutated array. Unless you can refactor the code into a manner such that the array for the current frame can be calculated without information from the previous frame you cannot use this approach.
There are two ways you could get around this, one is to use a global variable to store the wavefunction array and mutate it as needed, that way changes made in the function persist beyond the end of the function. For demonstration, here is an example of this implementation which is slightly simpler than a changing wavefunction in 3 dimensions,
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation
n = 100
wf = np.zeros((n,2))
def next_wf():
global wf
offset = wf[0,0] + 0.1
wf[:,0] = np.linspace(offset, np.pi*4+offset, wf.shape[0])
wf[:,1] = np.sin(wf[:,0])
def animate(frame):
next_wf()
plt.cla()
plot, = plt.plot(wf[:,0], wf[:,1])
return plot,
next_wf()
fig, ax = plt.subplots(1)
anim = animation.FuncAnimation(fig, animate, interval=25)
This will create an animation like the following
However, it should be noted that using global variables is explicitly advised against by the Variables and Scope page of the documentation,
Note that it is usually very bad practice to access global variables from inside functions, and even worse practice to modify them. This makes it difficult to arrange our program into logically encapsulated parts which do not affect each other in unexpected ways. If a function needs to access some external value, we should pass the value into the function as a parameter. [...]
In a simple, self contained script it is unlikely to cause harm but in more complicated code it can be detrimental. The more 'proper' way to do this is to wrap the entire thing in a class, i.e.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation
class waveanim:
def __init__(self):
n = 100
self.wf = np.zeros((n,2))
self.next_wf()
fig, ax = plt.subplots(1)
anim = animation.FuncAnimation(fig, self.animate, interval=25, blit=True)
anim.save('./animation.gif', writer='imagemagick')
def next_wf(self):
offset = self.wf[0,0] + 0.1
self.wf[:,0] = np.linspace(offset, np.pi*4+offset, self.wf.shape[0])
self.wf[:,1] = np.sin(self.wf[:,0])
def animate(self, frame):
self.next_wf()
plt.cla()
plot, = plt.plot(self.wf[:,0], self.wf[:,1])
return plot,
waveanim()
Which gives the same result as above.
I have tried to animate two different artists plt.quiver() and plt.hist() in matplotlib recently and both times I ran into the same problem. Apparently those classes (I hope my OOP literacy is holding up) both don't have a set_data like method. Well, technically plt.quiver() does have set_UVC, but that doesn't work with Line3D instances, only with Line2D. Also, there is an example for animating a histogram, but it seemed like some serious jerry-rigging to me. I tried to simply define my artist with new values in the update() function and then just return the new artist instead of defining the artist outside the update() and then updating the data of the artist using a set_data() method. But this only results in an animation in which all frames are kept in the plot and overlap. Below are the animations for both the Histogram and the Quiver plot.
Histogram:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
"""
evolution of mean values produced by 1000 dice rolls with
more and more dices, which lead to a narrowing variance
with a growing number of dices.
"""
fig, ax = plt.subplots()
def update(i):
k = [np.mean(np.random.randint(0,7,i)) for j in range(1000)]
lol = ax.hist(k,bins=20)
return lol
ani = FuncAnimation(fig, update, frames=(1,2,10,100,1000))
plt.show()
Quiver:
from mpl_toolkits.mplot3d import axes3d
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.animation import FuncAnimation
def rot_z(angle):
o = 2*np.pi*(angle/360)
mat = np.array(((np.cos(o),-np.sin(o),0),
(np.sin(o), np.cos(o),0),
( 0 , 0 ,0)))
return mat
fig = plt.figure()
ax = fig.gca(projection='3d')
ax.set_xlim(-1.5,1.5)
ax.set_ylim(-1.5,1.5)
ax.set_zlim(-1.5,1.5)
def update(frame):
x,y,z = rot_z(frame).dot(np.array((1,1,1)))
quiv = ax.quiver(0,
0,
0,
x,
y,
z,
length=1)
return quiv
ani = FuncAnimation(fig, update, frames=np.linspace(0,360,100))
plt.show()
If you run them, you can see the issue. So I wanted to know: Isn't there an easier, abstractable way of animating artists, or am I at the mercy of potentially non-existent setters? I have checked both dir(plt.quiver), dir(plt.hist) to see if I was simply overlooking those methods in the docs, but the example of the animated histogram seemed to confirm my fears.
You could try to clear the image at every update with ax.clear(). Maybe the histogram animation would be more smooth if you would extend an array of throws instead of restarting from scratch at each frame?
Edit: the code below includes a test to reuse the same samples
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
fig, ax = plt.subplots()
randnums = [np.random.randint(0,7,1000) for j in range(1000)]
def update(i):
k = [np.mean(randnums[j][:i]) for j in range(1000)]
ax.clear()
lol = ax.hist(k,bins=20)
return lol
ani = FuncAnimation(fig, update, frames=[2**t for t in range(11)])
plt.show()
I'm trying to animate a 3D array in python using the first dimension as time.
I'm not sure where I am going wrong, as I recieve no error with this code. But my animation is stationary, stuck on the first page of the array.
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.animation import FuncAnimation
array = np.random.random(size=(10, 20, 30))
empty = np.zeros(array[0].shape)
fig = plt.figure()
mat = plt.imshow(empty)
def func(i):
mat.set_data(array[i])
return mat
frames = len(array)
FuncAnimation(fig, func, frames)
plt.show()
I'd like the use the below code but I haven't seen an annonymous function used anywhere with FuncAnimation. It produces the same result except mat is not created setting the initial axes.
fig = plt.figure()
func = lambda i: plt.imshow(array[i])
frames = len(array)
FuncAnimation(fig, func, frames)
plt.show()
The main difference between your code and any example you find on matplotlib animations is that you do not actually store the FuncAnimation. Depending on how you run things it would then directly be garbage-collected.
ani = FuncAnimation(...)
I would like to save an animation using Python but I get the frames superposed! I want to get the frames displayed individually.
Please here what I used:
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation
from numpy import pi, cos, sin
fig = plt.figure()
plt.axis([-1.5, 1.5,-1.5, 1.5])
ax = plt.gca()
ax.set_aspect(1)
N=100
xp = [None] * N
yp = [None] * N
def init():
# initialize an empty list of cirlces
return []
def animate(i):
xp[i]=sin(i*pi/10)
yp[i]=cos(i*pi/10)
patches = []
patches.append(ax.add_patch( plt.Circle((xp[i],yp[i]),0.02,color='b') ))
return patches
anim = animation.FuncAnimation(fig, animate, init_func=init,
frames=N-1, interval=20, blit=True)
anim.save("example.avi")
plt.show()
There are some things I'm not sure about and it really seems to be that the axis.plot() behavior and FuncAnimate() behavior are different. However, the code below works for both.
Use only one patch (in your case)
The key point from your code is that you are adding a new circle in addition to the old circles every iteration:
patches = []
patches.append(ax.add_patch( plt.Circle((xp[i],yp[i]),0.02,color='b') ))
Even though you clear the patches list, they are still stored in the axis.
Instead, just create one circle and change its position.
Clear first frame with init()
Also, init() needs to clear the patch from the base frame.
Standalone Example
from matplotlib import pyplot as plt
from matplotlib import animation
from numpy import pi, cos, sin
fig = plt.figure()
plt.axis([-1.5, 1.5, -1.5, 1.5])
ax = plt.gca()
ax.set_aspect(1)
N = 100
xp = []
yp = []
# add one patch at the beginning and then change the position
patch = plt.Circle((0, 0), 0.02, color='b')
ax.add_patch(patch)
def init():
patch.set_visible(False)
# return what you want to be cleared when axes are reset
# this actually clears even if patch not returned it so I'm not sure
# what it really does
return tuple()
def animate(i):
patch.set_visible(True) # there is probably a more efficient way to do this
# just change the position of the patch
x, y = sin(i*pi/10), cos(i*pi/10)
patch.center = x, y
# I left this. I guess you need a history of positions.
xp.append(x)
yp.append(y)
# again return what you want to be cleared after each frame
# this actually clears even if patch not returned it so I'm not sure
# what it really does
return tuple()
anim = animation.FuncAnimation(fig, animate, init_func=init,
frames=N-1, interval=20, blit=True)
# for anyone else, if you get strange errors, make sure you have ffmpeg
# on your system and its bin folder in your path or use whatever
# writer you have as: writer=animation.MencoderWriter etc...
# and then passing it to save(.., writer=writer)
anim.save('example.mp4')
plt.show()
Return values???
Regarding the return values of init() and animate(), It doesn't seem to matter what is returned. The single patch still gets moved around and drawn correctly without clearing previous ones.