Fast rendering to buffer in Matplotlib - python

I have a Kivy application that uses matplotlib to render figures in the application GUI. It means that the application creates a matplotlib Figure and get the Figure's buffer to display it in an Image widget.
For now, each time I want to update the figure, I recreate a Figure and draw everthing, calling refresh_gui_image.
import matplotlib.pyplot as plt
def draw_matplotlib_buffer(image, *elements):
fig = plt.figure(figsize=(5,5), dpi=200)
ax = plt.Axes([0, 0, 1, 1])
ax.set_axis_off()
fig.add_axis(ax)
ax.imshow(image)
for elem in elements:
# Suppose such a function exists and return a matplotlib.collection.PatchCollection
patchCollection = elem.get_collection()
ax.add_collection(patchCollection)
buffer = fig.canvas.print_to_buffer()
plt.close(fig)
return buffer
# imageWidget is a kivy Widget instance
def refresh_gui_image(imageWidget, image, *elements):
size = image.shape()
imageBuffer = draw_matplotlib_buffer(image, *elements)
imageWidget.texture.blit_buffer(imageBuffer, size=size, colorfmt='rgba', bufferfmt='ubyte')
imageWidget.canvas.ask_update()
In the code above, *elements represent multiple sets of objects. Typically, I have 2 to 4 sets which contains between 10 to 2000 objects. Each objects is represented with a patch, and each set is a PatchCollection on the Figure.
It works very well. With the current code, every patch is redrawn each time refresh_gui_image is called. When the sets becomes bigger (like 2000) objects, the update is too slow (few seconds). I want to make a faster rendering with matplotlib, knowing that some of the sets do not have to be redrawn, and that the image stays in the background, and do not have to be redrawn either.
I know blitting and animated artists can be used, this is what I tried, following this tutorial of the matplotlib documentation:
import matplotlib.pyplot as plt
# fig and ax are now global variable
# bg holds the background that stays identical
fig = None
ax = None
bg = None
def init_matplotlib_data(image, *elements):
global fig, ax, bg
fig = plt.figure(figsize=(5,5), dpi=200)
ax = plt.Axes([0, 0, 1, 1])
ax.set_axis_off()
fig.add_axis(ax)
ax.imshow(image)
fig.canvas.draw() # I don't want a window to open, just want to have a cached renderer
bg = fig.canvas.copy_from_bbox(fig.bbox)
for elem in elements:
# Suppose such a function exists and return a matplotlib.collection.PatchCollection
patchCollection = elem.get_collection(animated=True)
patchCollection.set_animated(True)
ax.add_collection(patchCollection)
def draw_matplotlib_buffer(image, *artists_to_redraw):
global fig, ax, bg
fig.canvas.restore_region(bg)
for artist in artists_to_redraw:
ax.draw_artist(artist)
fig.canvas.blit(fig.bbox)
buffer = fig.canvas.print_to_buffer()
return buffer
I call init_matplotlib_data once, and the refresh_gui_image as many time as I need, with artists I need to update. The point is that I correctly get my image background, but I cannot succeed to get the patches collections on the buffer returned by fig.canvas.print_to_buffer(). I unset the animated flag of the collection and this time they appear correctly. It seems to me, after some tests that ax.draw_artist() and fig.canvas.blit() have no effect. Another behavior I do not understand is that event if I pass animated=True to ax.imshow(image), the image is still drawn.
Why does the ax.draw_artist and fig.canvas.blit functions does not update the buffer returned by fig.canvas.print_to_buffer as expected ?

Apparently, blitting is a particular feature meant for GUI. Even thought the Agg backend support blitting, it does not mean that blitting can be used solely with it.
I came up with a solution where I store every artist I want to draw, and change their data whenever I need. I then use fig.canvas.print_to_buffer(), I am not sure what it does exactly, but I think the figure is fully redrawn. It is probably not as fast as what blitting can do, but it has the advantage to not reallocate and recreate every artists for each update. One can also remove artists from the canvas by calling the remove() method of an artist, and put it again with ax.add_artist(..).
I think this solution answer my question, since it is the fastest solution to have dynamic plotting with matplotlib while dumping the canvas into a buffer.

Related

How to add border or frame around individual subplots

I want to create an image like this, but I'm unable to put the individual plots inside a frame.
Figures and axes have a patch attribute, which is the rectangle that makes up the background. Setting a figure frame is hence pretty straightforward:
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 1)
# add a bit more breathing room around the axes for the frames
fig.subplots_adjust(top=0.85, bottom=0.15, left=0.2, hspace=0.8)
fig.patch.set_linewidth(10)
fig.patch.set_edgecolor('cornflowerblue')
# When saving the figure, the figure patch parameters are overwritten (WTF?).
# Hence we need to specify them again in the save command.
fig.savefig('test.png', edgecolor=fig.get_edgecolor())
Now the axes are a much tougher nut to crack. We could use the same approach as for the figure (which #jody-klymak I think is suggesting), however, the patch only corresponds to the area that is inside the axis limits, i.e. it does not include the tick labels, axis labels, nor the title.
However, axes have a get_tightbbox method, which is what we are after. However, using that also has some gotchas, as explained in the code comments.
# We want to use axis.get_tightbbox to determine the axis dimensions including all
# decorators, i.e. tick labels, axis labels, etc.
# However, get_tightbox requires the figure renderer, which is not initialized
# until the figure is drawn.
plt.ion()
fig.canvas.draw()
for ii, ax in enumerate(axes):
ax.set_title(f'Title {ii+1}')
ax.set_ylabel(f'Y-Label {ii+1}')
ax.set_xlabel(f'X-Label {ii+1}')
bbox = ax.get_tightbbox(fig.canvas.get_renderer())
x0, y0, width, height = bbox.transformed(fig.transFigure.inverted()).bounds
# slightly increase the very tight bounds:
xpad = 0.05 * width
ypad = 0.05 * height
fig.add_artist(plt.Rectangle((x0-xpad, y0-ypad), width+2*xpad, height+2*ypad, edgecolor='red', linewidth=3, fill=False))
fig.savefig('test2.png', edgecolor=fig.get_edgecolor())
plt.show()
I found something very similar and somehow configured it out what its doing .
autoAxis1 = ax8i[1].axis() #ax8i[1] is the axis where we want the border
import matplotlib.patches as ptch
rec = ptch.Rectangle((autoAxis1[0]-12,autoAxis1[2]-30),(autoAxis1[1]-
autoAxis1[0])+18,(autoAxis1[3]-
autoAxis1[2])+35,fill=False,lw=2,edgecolor='cyan')
rec = ax8i[1].add_patch(rec)
rec.set_clip_on(False)
The code is a bit complex but once we get to know what part of the bracket inside the Rectangle() is doing what its quite easy to get the code .

Matplotlib: How to animate 2d arrays (with annotation)

I have N=lots of 256x256 images (grayscales saved as numpy.ndarray with shape=(N, 256, 256)) and want to look at all of them by use of animation. I also want to add some label showing details related to each of the images, such as its index, its maximum value, etc. I'm using matplotlib, which I'm not familiar with.
There are a number of StackOverflow topics concerned with this exact problem (e.g. 1, 2, 4), as well as numerous tutorials (e.g. 3). I pieced together below attempts at solving the problem from these sources.
The two possibilities I have tried are using the matplotlib.animation classes FuncAnimation and ArtistAnimation. I'm not happy with my solutions because:
I have not been able to display and animate text information together with the images. I can display animated text on top of the images using axes.text but don't know how to put text next to the image.
I strongly dislike the FuncAnimation solution for aesthetic reasons (use of global variables, etc.)
I also want an animated colorbar. I think this is possible (somehow) with FuncAnimation but I don't see how it is possible with ArtistAnimation
ArtistAnimation gets slow since a large number of Artists (each picture) are required
# python 3.6
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation, ArtistAnimation
images = np.random.rand(1000,256,256)
fig, ax = plt.subplots()
# ####################### Solution using ArtistAnimation ##################################################
# for much larger numbers of pictures this gets very slow
# How do I display information about the current picture as text next to the plot?
ims = []
for i in range(images.shape[0]):
ims.append([plt.imshow(images[i], animated=True)])
ani = ArtistAnimation(fig, ims, interval=250, blit=True, repeat_delay=5000)
plt.show()
# ####################### Solution using FuncAnimation ##################################################
# I don't like to use global variables in principle (but still want to know how to make this work).
# I can't figure out a way to display text while animating.
# Here I try to animate title and return it from update_figure (since it's an Artist and should update?!) but it has no effect.
nof_frames = images.shape[0]
i = 0
im = plt.imshow(images[0], animated=True)
# I do know that variables that aren't changed need not be declared global.
# However, I want to mark them and don't like accessing global variables in the first place.
def update_figure(frame, *frargs):
global i, nof_frames, ax, images, im
if i < nof_frames - 1:
i += 1
else:
i = 0
im.set_array(images[i])
ax.set_title(str(i)) # this has no effect
return im, ax
ani = FuncAnimation(fig, update_figure, interval=300, blit=True)
plt.show()

(python) matplotlib animation doesn't show

I wrote the following code based on the matplotlib site example.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
fig = plt.figure()
nFreqs = 1024
nFFTWindows = 512
viewport = np.ones((nFreqs, nFFTWindows))
im = plt.imshow(viewport, animated=True)
def updatefig(*args):
global viewport
print viewport
viewport = np.roll(viewport, -1, axis=1)
viewport[:, -1] = 0
im.set_array(viewport)
return im,
ani = animation.FuncAnimation(fig, updatefig, interval=50, blit=True)
plt.show()
Before changing the animation works, but now it doesn't. I expected it to start with a purple plot, which slowly turns yellow from the right edge to the left. The viewport variable does update correctly (checked it with print in my function).
I get the static image (all ones, like it was initially):
Where did I go wrong here?
The problem is you are defining a plot initially with a single colour (1.0) so the colour range is set to this. When you update the figure, the range of colours is 1.0 +- some small value so you don't see the change. You need to set the colour range to between one and zero with vmin/vmax arguments as follows:
im = plt.imshow(viewport, animated=True, vmin=0., vmax=1.)
The rest of the code stays the same and this should work as expected. Another alternative is to add the call,
im.autoscale()
after im.set_array(viewpoint) to force the colour range to be updated each time.
The imshow plot is initialized with one single value (1 in this case), so any value normalized to the range between 1 and 1 becomes the same color.
In order to change this, you may
initiate the imshowplot with limits for the color (vmin=0, vmax=1).
initiate the imshow plot with a normalization instance
norm = matplotlib.colors.Normalize(vmin=0, vmax=1)
im = plt.imshow(arr, norm=norm)
Set the limits afterwards using im.set_clim(0,1).
Preferences > IPython Console > Graphics > Backend and change it from "Inline" to "Automatic"
Do not forget to restart you IDE (Spyder, PyCharm, etc.) after applying above change.
Cheers
:)

Efficiently Animating pcolormesh

Im trying to save a GIF with the evolucion of some waves in 2d using pcolormesh (using surface or wireframe would also be ok).
This has been my aproach so far:
set the quadmesh to plot in polar coordinates:
from matplotlib import pyplot
from matplotlib.animation import FuncAnimation as FuncAnimation
pi=np.pi
rmax=6.
r=2*np.linspace(0,np.sqrt(rmax*.5),100)**2
phi=np.linspace(0,2*pi,80)
R, P = np.meshgrid(r, phi)
X, Y = R*np.cos(P), R*np.sin(P)
set the figure and functions for the animation:
count is the amount of frames i have.
Z is a count*2D-array with the values i want to plot.
(it has the sum of some fourier like series)
fig, ax = pyplot.subplots()
def anim_I(count,r,phi):
anim=np.zeros((count,len(phi), len(r)))
for i in range(count):
anim[i,:,:]=coef_transf(final_coefs[i,:,:,:,0],r,phi)**2
return anim
Z=anim_I(count,r,phi)
def animate(i):
pyplot.title('Time: %s'%time[i])
#This is where new data is inserted into the plot.
plot=ax.pcolormesh(X, Y,Z[i,:,:],cmap=pyplot.get_cmap('viridis'),vmin=0., vmax=15.)
return plot,
ax.pcolormesh(X, Y,Z[0,:,:],cmap=pyplot.get_cmap('viridis'),vmin=0., vmax=15.)
pyplot.colorbar()
anim = FuncAnimation(fig, animate, frames = range(0,count,7), blit = False)
i don't really need to see it live, so i just save a gif.
anim.save('%d_%d_%d-%d.%d.%d-2dgif.gif' %(localtime()[0:6]), writer='imagemagick')
pyplot.close()
While this works, it can take to an hour to make the gif of a even a hundred frames.
I wan't to know what would be the correct way to do this so it could be usable.
I have seen the other post in this regard, but i couldn't get the code working, or it would be just as inneficient.
You could try to write
def animate(i):
pyplot.title('Time: %s'%time[i])
#This is where new data is inserted into the plot.
plot=plot.set_array(Z[i,:,:].ravel())
return plot,
instead of
def animate(i):
pyplot.title('Time: %s'%time[i])
#This is where new data is inserted into the plot.
plot=ax.pcolormesh(X, Y,Z[i,:,:],cmap=pyplot.get_cmap('viridis'),vmin=0., vmax=15.)
return plot,
This does not create a new object every time you call the animate funtion. Instead it changes the image of object that was already created.
However, the set_array method seems to need a flattened array, hence the .ravel().
This only produces the right image if you set the shading option of the pcolormap function to shading='gouraud'.
I don't know why, unfortunatelly, it seems to have to do with the sorting of the array.
I hoped, that helped a little.
I suggest inserting a
pyplot.clf()
at the beginning of your animate(i) function. This will start each frame with a blank figure. Otherwise, I suspect the plot will not be cleared, and the long time is due to actually plotting all previous frame below the new one.

Clear overlay scatter on matplotlib image

So I am back again with another silly question.
Consider this piece of code
x = linspace(-10,10,100);
[X,Y]=meshgrid(x,x)
g = np.exp(-(square(X)+square(Y))/2)
plt.imshow(g)
scat = plt.scatter(50,50,c='r',marker='+')
Is there a way to clear only the scatter point on the graph without clearing all the image?
In fact, I am writing a code where the appearance of the scatter point is bound with a Tkinter Checkbutton and I want it to appear/disappear when I click/unclick the button.
Thanks for your help!
The return handle of plt.scatter has several methods, including remove(). So all you need to do is call that. With your example:
x = np.linspace(-10,10,100);
[X,Y] = np.meshgrid(x,x)
g = np.exp(-(np.square(X) + np.square(Y))/2)
im_handle = plt.imshow(g)
scat = plt.scatter(50,50,c='r', marker='+')
# image, with scatter point overlayed
scat.remove()
plt.draw()
# underlying image, no more scatter point(s) now shown
# For completeness, can also remove the other way around:
plt.clf()
im_handle = plt.imshow(g)
scat = plt.scatter(50,50,c='r', marker='+')
# image with both components
im_handle.remove()
plt.draw()
# now just the scatter points remain.
(almost?) all matplotlib rendering functions return a handle, which have some method to remove the rendered item.
Note that you need the call to redraw to see the effects of remove() -- from the remove help (my emphasis):
Remove the artist from the figure if possible. The effect will not be
visible until the figure is redrawn, e.g., with
:meth:matplotlib.axes.Axes.draw_idle.

Categories