Matplotlib animation slows down when adding patches - python

I wanted to visualize the path of a 2D-algorithm. So I wrote a short Python code, that produces such a animation. The problem is, that for each pixel (a plt.Rectangle) I add, the code gets slower and slower. The first 20 pixels get displayed in about 1 second, the last 20 pixels take already 3 seconds. And obviously it gets even worse for bigger grids and more pixels.
from matplotlib import pyplot as plt
from matplotlib import animation
fig = plt.figure()
fig.set_dpi(100)
fig.set_size_inches(7, 7)
ax = plt.axes(xlim=(0, 20), ylim=(0, 20))
pixels = list()
def init():
return list()
def animate(i):
index = len(pixels)
index_x, index_y = index // 20, index % 20
pixel = plt.Rectangle((index_x, index_y), 1, 1, fc='r')
ax.add_patch(pixel)
pixels.append(pixel)
return pixels
anim = animation.FuncAnimation(fig, animate,
init_func=init,
frames=100,
interval=5,
blit=True)
plt.show()
It's quite clear to me, why it takes longer. Since the number of patches gets bigger and bigger each frame, matplotlibs rendering gets slower and slower. But not this slow! How can I get more speed? Is there a way that I can keep the old plot and only overwrite the current pixel?

I tried timing the animate function and for some reason there is no apparent slowdown in the function itself. Besides, blit should ensure only the latest rectangle is drawn. A possible way to prevent the redraw you suggest is to rasterize the plot with,
m=ax.add_patch(pixel)
m.set_rasterized(True)
Although this doesn't seem to improve speed for my timing. I think the increase must result somewhere in matplotlib when updating the plot. This is clear using interactive plotting, e.g.
from matplotlib import pyplot as plt
import time
fig = plt.figure()
fig.set_dpi(100)
fig.set_size_inches(7, 7)
ax = plt.axes(xlim=(0, 20), ylim=(0, 20))
npx = 20; npy = 20
Npix = npx*npy
plt.ion()
plt.show()
for i in range(Npix):
#ax.cla()
t0 = time.time()
index_x, index_y = i // 20, i % 20
pixel = plt.Rectangle((index_x, index_y), 1, 1, fc='r')
ax.add_patch(pixel)
fig.set_rasterized(True)
plt.pause(0.0001)
t1 = time.time()
print("Time=",i,t1-t0)
Which gives for the rasterised, non-rasterised and cleared axis (ax.cla) case,
I'm not sure why this happens, maybe someone with better insight into matplotlib would be able to help. One way to speed up the plot is to setup and put all rectangles in a patchcollection. Then the animation just changes the facecolor so only the rectangles which need be shown are displayed,
from matplotlib import pyplot as plt
from matplotlib import animation
import numpy as np
from matplotlib.collections import PatchCollection
fig = plt.figure()
fig.set_dpi(100)
fig.set_size_inches(7, 7)
ax = plt.axes(xlim=(0, 20), ylim=(0, 20))
npx = 20; npy = 20
Npix = npx*npy
displayed = np.zeros((npx, npy, 4))
pixels = []
def init():
for index in range(Npix):
index_x, index_y = index // npx, index % npy
pixel = plt.Rectangle((index_x, index_y), 1, 1, fc='r', ec='none')
pixels.append(pixel)
return pixels
pixels = init()
collection = PatchCollection(pixels, match_original=True, animated=True)
ax.add_collection(collection)
def animate(index):
index_x, index_y = index // npx, index % npy
displayed[index_x, index_y] = [1, 0, 0, 1]
collection.set_facecolors(displayed.reshape(-1, 4))
return (collection,)
anim = animation.FuncAnimation(fig, animate,
frames=400,
interval=1,
blit=True,
repeat=False)
plt.show()
This is much faster, although I couldn't work out how to turn edges on or off so just disable for all rectangles.

Related

How to move a patch along a path?

I am trying to animate a patch.Rectangle object using matplotlib. I want the said object to move along a path.Arc.
A roundabout way to do this would be (approximately) :
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation
import matplotlib.patches as mpat
fig, ax = plt.subplots()
ax.set(xlim=(0, 10), ylim=(0, 10))
# generate the patch
patch = mpat.Rectangle((5, 5), 1, 4)
patch.rotation_point = 'center'
# generate the path to follow
path_to_follow = mpat.Arc((5, 5), 2, 2)
ax.add_patch(path_to_follow)
def init():
patch.set(x=5, y=5)
ax.add_patch(patch)
return patch,
def animate(i, ax):
new_x = 5 + np.sin(np.radians(i)) - 0.5 # parametric form for the circle
new_y = 5 + np.cos(np.radians(i)) - 2
patch.set(x=new_x, y=new_y, angle=90-i)
return patch,
anim = animation.FuncAnimation(fig, animate,
init_func=init,
fargs=[ax],
frames=360,
interval=10,
blit=True)
plt.show()
The rectangle follows a circle, but a parametric one. Would it be possible to make it follow any path?
In other words, I would like to know if there are other simpler methods to do this (make my patch follow my path, here a circle), and if that could be generalized to other path.
Thanks in advance !
I searched into the matplotlib doc for a methods which gives the parametric form for a given path (but apparently there is not), or for a methods which directly move a patch along a path (obviously, there was not).
Here is one way to use matplotlib.path.Path to generate a path, whose vertices can be obtained using the method cleaned, to move a patch along it.
I have tried to showcase how blue and red colored Rectangles can be moved along a (blue) linear path and a (red) circular path, respectively:
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation, path
import matplotlib.patches as mpat
fig, ax = plt.subplots()
ax.set(xlim=(0, 10), ylim=(0, 10))
# generate a linear path
path1 = np.column_stack((np.arange(500)/50, np.arange(500)/50))
# generate a circular path
circle = path.Path.circle(center=(5, 5), radius=1)
path2 = circle.cleaned().vertices[:-3]
# create patches
patch1 = mpat.Rectangle((0, 0), 1, 3)
patch2 = mpat.Rectangle((0, 0), 1, 3, color='red', fill=None)
# plot path vertices
plt.scatter(x=path1[:, 0], y=path1[:, 1], s=2)
plt.scatter(x=path2[:, 0], y=path2[:, 1], color='red', s=2)
def init():
patch1.set(x=0, y=0)
patch2.set(x=5, y=6)
ax.add_patch(patch1)
ax.add_patch(patch2)
return [patch1, patch2]
def animate(i, ax):
j = i % 500 # path1 has shape (500, 2)
k = (i % 16) # path2 has shape (16, 2)
patch1.set(x=path1[j][0], y=path1[j][1], angle=-j)
patch2.set(x=path2[k][0], y=path2[k][1], angle=-k)
return [patch1, patch2]
anim = animation.FuncAnimation(fig, animate,
init_func=init,
fargs=[ax],
frames=360,
interval=100,
blit=True)
plt.show()
If your path is some collection of coordinates, you can not only translate the rectangle, but also compute the vector from one point to the next and update the rectangle angle accordingly. In the next example (mix of your code with mine), we generate from the beginning the path, but it could be instead live read from some external source.
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation
import matplotlib.patches as mpat
# create some path with pairs of X and Y coordinates
t = np.linspace(0, 360, 361)
X = 5. * np.sin(np.radians(t))
Y = t * (t-360*2) / 8000 + 7.5
# create x and y lists to store the position pairs as they are plotted
x = [X[0],]
y = [Y[0],]
# plotting
fig, ax = plt.subplots()
ax.set(xlim=(-10, 10), ylim=(-10, 10))
patch = mpat.Rectangle((x[0], y[0]), 1, 3)
def init():
patch.set(x=x[0], y=y[0])
ax.plot(X, Y)
ax.add_patch(patch)
return patch,
def animate(i, ax):
new_x = X[i] # we are getting from pre-generated data,
new_y = Y[i] # but it could be some function, or even live external source
vx = new_x - x[-1] # calculate the vectors, which are used for angle
vy = new_y - y[-1]
x.append(new_x) # store for next iteration, so that we can calculate the vectors
y.append(new_y)
new_alfa = np.degrees(np.arctan2(vy, vx))
patch.set(x=new_x, y=new_y, angle = new_alfa)
return patch,
anim = animation.FuncAnimation(fig, animate,
init_func=init,
fargs=[ax],
frames=360,
interval=20,
blit=True)
plt.show()
Thanks a lot for your answers, here is the code I made (mixing the two answers) and which does exactly what I wanted, if it helps anyone :
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation
import matplotlib.patches as mpat
fig, ax = plt.subplots()
ax.set(xlim=(-6, 6), ylim=(-6, 6))
# generate a circular path
circle = mpat.Arc((0, 0), 10, 10, theta1=20, theta2=220, color='green')
path = circle.get_transform().transform_path(circle.get_path()).cleaned().vertices[:-3] # get_path is not enough because of the transformation, so we apply to the path the same transformation as the circle has got from the identity circle
ax.add_patch(circle)
# create patch
patch = mpat.Rectangle((0, 0), 1, 4, color='red', fill=None)
patch.rotation_point = 'center'
# plot path vertices
plt.scatter(x=path[:, 0], y=path[:, 1], color='red', s=2)
shape = len(path)
def init():
patch.set(x=5-0.5, y=6-2) # we substract to get to the center
ax.add_patch(patch)
return [patch]
def animate(i, ax):
k = i % shape
new_x = path[k][0]
new_y = path[k][1]
vx = new_x - path[k-1][0]
vy = new_y - path[k-1][1]
patch.set(x=new_x-0.5, y=new_y-2, angle=np.degrees(np.arctan2(vy, vx) + 90))
return [patch]
anim = animation.FuncAnimation(fig, animate,
init_func=init,
fargs=[ax],
frames=360,
interval=200,
blit=True,
repeat=False)
plt.show()
To improve this, is anyone know how to increase the number of points given? In other words, increase the len of path to be more precise in moving the rectangle.
Thanks in advance !

Creating Multi-Scene Animation using consecutive calls to Matplotlib.FuncAnimation creates flickered images

Context: I am trying to create a teaching demo tool to show how the iteration guesses through a set of points to ultimately arrive at the root of an equation
Problem: I want to animate using matplotlib to show the iterations viusally. Specifically, given a curve (see along side) and an initial guess (say 1.5 in this particular case), I want to compose an animation of 3 scenes:
draw a vertical line at x = guess (=1.5), to meet the curve at y= 9 (aka value).
Draw a line through (guess, value) with a slope 'm' (generated by my code). Delete the vertical line at this stage and keep the second line
Delete the second line after a pause
For illustration, here is the image
Additionally here is my code:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
fig, ax = plt.subplots()
xdata, ydata = [], []
xdata2, ydata2 = [], []
ln, = plt.plot([], [])
def init():
ax.set_xlim(-3, 3)
ax.set_ylim(-10, 10)
return [ln]
def update(frame):
xdata.append(frame)
ydata.append(frame ** 3 + 4 * frame ** 2 + frame - 6)
ln.set_data(xdata, ydata)
return [ln]
def update2(frame):
xdata2.append(1.5)
ydata2.append(frame)
ln.set_data(xdata2,ydata2)
return[ln]
ani = FuncAnimation(fig, update, frames=np.linspace(-3, 3, 100),
init_func=init, blit=True)
ani2 = FuncAnimation(fig, update2, frames=np.linspace(0, 3, 100),blit=True)
plt.show()
This is a simplified version of the problem that I am trying to solve and is not part of the code that involves the calculation of the iterations etc. For now I am just trying to draw the curve in Update and post that, draw a vertical line at x=1.5.
Results: At my end, the entire animation is a set of flickering where it is apparent that matplotlib switches "thread context" very rapidly between the two FuncAnimation calls
The desired animation in your next question can be achieved in the form of drawing a curve as a base graph, adding line graphs frame by frame, and deleting that graph object when necessary.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import time
fig, ax = plt.subplots()
x = np.linspace(-3, 3, 100)
y = x ** 3 + 4 * x **2 + x -6
xx = x[74]
yy = y[74]
#print(xx,yy)
xx2 = x[65]
yy2 = y[65]
#print(xx2,yy2)
ln, = ax.plot(x, y)
ln2, = ax.plot([], [])
ln3, = ax.plot([],[])
ax.set_xlim(-3, 3)
ax.set_ylim(-10, 10)
# Move axes center and spines off
ax.spines[['top', 'right']].set_visible(False)
ax.spines[['left', 'bottom']].set_position('center')
# Show ticks axes only
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
def update(i):
ln2.set_data((xx, xx), (0, yy))
ln2.set_color('k')
if i == 2:
ln3.set_data((xx2, xx), (yy2, yy))
ln3.set_color('red')
ln3.set_width=3
if i >=3:
ln2.set_data([],[])
ln3.set_data([],[])
return ln2,ln3
ani = FuncAnimation(fig, update, frames=[0,1,2,3], interval=500, blit=True, repeat=True)
plt.show()

I want to animate the trajectory of a ball (really just a circle) using Matplotlib animation

I want to animate the trajectory of a circle (ball) defined by y = -t^2 + 11t - 18. Basically it would just be bouncing up and down (i.e. no change in x). Its intercepts are (2,0) and (9,0) so the animation should start at time t = 2 as it leaves the ground and end at time t = 9 as it returns to the ground. I am also hoping that a running display of the time could also be included in the animation. So basically between times t=0 and t=2, the ball would just be on the ground. This is the code I have so far but it doesn't seem to make sense. I'm not sure whether the animation is just going too fast.
%matplotlib notebook
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation
fig = plt.figure()
fig.set_dpi(100)
fig.set_size_inches(3, 3)
ax = plt.axes(xlim=(0, 10), ylim=(0, 15))
patch = plt.Circle((5, 0), 0.2, fc='r')
def init():
patch.center = (5, 0)
ax.add_patch(patch)
return patch,
def animate(i):
x, y = patch.center
x = 0 * i+5
y = - i**2 + 11 * i - 18
patch.center = (x, y)
return patch,
anim = animation.FuncAnimation(fig, animate,
init_func=init,
frames=3600,
interval=1,
blit=True)
plt.show()

Set matplotlib video animation size (1920x1080)

I'm trying to create a matplotlib animation that is 1920x1080. The following animation generated is (1152x648), which is the right ratio but too small. I know that I can do this after the fact with ffmpeg, but I'd like to avoid re-encoding if possible.
y = my_data
fig = plt.figure(figsize=(16,9))
def ani(i):
# Animation Code
animator = FuncAnimation(fig, ani, frames=y.shape[0])
# Can't figure out what extra_args to set here
ffmpeg_writer = FFMpegWriter(fps=y.shape[0]/sound.duration_seconds, extra_args=['-scale', '1920x1080'])
animator.save('mymovie.mp4', writer=ffmpeg_writer)
print(ffmpeg_writer.frame_size) # prints (1152, 648)
ipd.Video('mymovie.mp4')
As you can see above, I have tried a few extra_args, however I can't get anything to work.
You can set the DPI arguments as described in the following post.
The nominal DPI of your system is 72, so the resolution is 72*16x72*9 = 1152x648.
We can set the DPI to 1920/16 for getting resolution 1920x1080:
fig = plt.figure(figsize=(16, 9), dpi=1920/16)
Here is an executable code sample (used for testing):
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
# 72 is the nominal DPI that is the reason figsize=(16, 9) resolution is (1152, 648).
# Set the DPI to 120 for getting resolution is (1920, 1080)
fig = plt.figure(figsize=(16, 9), dpi=(1920/16))
ax = plt.gca() # Get current axes
l1, = ax.plot(np.arange(1, 10), np.arange(1, 10), **{'marker':'o'})
def init():
l1.set_data([],[])
def ani(i, l1):
l1.set_data(i/5, np.sin(0.5*i)*4+5)
l1.set_markersize(10)
animator = animation.FuncAnimation(fig, ani, fargs=(l1,), init_func=init, frames=50, interval=1, blit=False) # 50 frames, interval=1
ffmpeg_writer = animation.FFMpegWriter(fps=10)
animator.save('mymovie.mp4', writer=ffmpeg_writer)
plt.show() # Show for testing

Animating Particle's Path with "Disappearing Tail" in Python

I'm trying to animate a 2d path, and I would like it to have a sort of "Disappearing Tail", where at any given time, it shows only the last 5 (for example) particles.
What I currently have is quite far from this:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation, rc
from IPython.display import HTML
sample_path = np.random.uniform(size=(100,2))
fig, ax = plt.subplots()
x = np.arange(-1, 1, 0.01) # x-array
line, = ax.plot(sample_path[0,0], sample_path[0,1])
def connect(i):
(x0,y0) = sample_path[i-1,:]
(x1,y1) = sample_path[i,:]
plt.plot([x0,x1],[y0,y1],'ro-')
return line,
def init():
line.set_ydata(np.ma.array(x, mask=True))
return line,
ani = animation.FuncAnimation(fig, connect, np.arange(1, 100), init_func=init,
interval=200, blit=True)
HTML(ani.to_html5_video())
This retains a 'full tail', i.e. after k steps, it shows all of the first k locations.
Is there a way to adapt what I've got so that the animation only shows the most recent history of the particle?
You would probably want to update the line instead of adding a lot of new points to the plot. Selecting the 5 most recent points can be done via indexing, e.g.
sample_path[i-5:i, 0]
Complete example, where we take care not to have a negative index and also don't use blit (which does not make sense if saving the animation).
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation, rc
r = np.sin(np.linspace(0,3.14,100))
t = np.linspace(0, 10, 100)
sample_path = np.c_[r*(np.sin(t)+np.cos(t)), r*(np.cos(t)-np.sin(t))]/1.5
fig, ax = plt.subplots()
line, = ax.plot(sample_path[0,0], sample_path[0,1], "ro-")
def connect(i):
start=max((i-5,0))
line.set_data(sample_path[start:i,0],sample_path[start:i,1])
return line,
ax.set_xlim(-1,1)
ax.set_ylim(-1,1)
ani = animation.FuncAnimation(fig, connect, np.arange(1, 100), interval=200)
plt.show()
Not as good as ImportanceOfBeingErnest's answer technically, but it still does the job and looks pretty cool, just plot the latest points and clear the old ones. I added a few more and sped it up because I thought it looked better with a longer trail.
def connect(i):
#clear current points
plt.clf()
#prevent axis auto-resizing
plt.plot(0,0)
plt.plot(1,1)
#generate points to plot
(x0,y0) = sample_path[i-8,:]
(x1,y1) = sample_path[i-7,:]
(x2,y2) = sample_path[i-6,:]
(x3,y3) = sample_path[i-5,:]
(x4,y4) = sample_path[i-4,:]
(x5,y5) = sample_path[i-3,:]
(x6,y6) = sample_path[i-2,:]
(x7,y7) = sample_path[i-1,:]
(x8,y8) = sample_path[i,:]
#plot old points
plt.plot([x0,x1,x2,x3,x4,x5,x6,x7],[y0,y1,y2,y3,y4,y5,y6,y7],'ro-')
#plot new point in blue
plt.plot([x7,x8],[y7,y8],'bo-')
return line,
def init():
line.set_ydata(np.ma.array(x, mask=True))
return line,
ani = animation.FuncAnimation(fig, connect, frames=np.arange(1, 100),
init_func=init,
interval=50, blit=True)
HTML(ani.to_html5_video())

Categories