Matplotlib.animation: how to remove white margin - python

I try to generate a movie using the matplotlib movie writer. If I do that, I always get a white margin around the video. Has anyone an idea how to remove that margin?
Adjusted example from http://matplotlib.org/examples/animation/moviewriter.html
# This example uses a MovieWriter directly to grab individual frames and
# write them to a file. This avoids any event loop integration, but has
# the advantage of working with even the Agg backend. This is not recommended
# for use in an interactive setting.
# -*- noplot -*-
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.animation as manimation
FFMpegWriter = manimation.writers['ffmpeg']
metadata = dict(title='Movie Test', artist='Matplotlib',
comment='Movie support!')
writer = FFMpegWriter(fps=15, metadata=metadata, extra_args=['-vcodec', 'libx264'])
fig = plt.figure()
ax = plt.subplot(111)
plt.axis('off')
fig.subplots_adjust(left=None, bottom=None, right=None, wspace=None, hspace=None)
ax.set_frame_on(False)
ax.set_xticks([])
ax.set_yticks([])
plt.axis('off')
with writer.saving(fig, "writer_test.mp4", 100):
for i in range(100):
mat = np.random.random((100,100))
ax.imshow(mat,interpolation='nearest')
writer.grab_frame()

Passing None as an arguement to subplots_adjust does not do what you think it does (doc). It means 'use the deault value'. To do what you want use the following instead:
fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=None, hspace=None)
You can also make your code much more efficent if you re-use your ImageAxes object
mat = np.random.random((100,100))
im = ax.imshow(mat,interpolation='nearest')
with writer.saving(fig, "writer_test.mp4", 100):
for i in range(100):
mat = np.random.random((100,100))
im.set_data(mat)
writer.grab_frame()
By default imshow fixes the aspect ratio to be equal, that is so your pixels are square. You either need to re-size your figure to be the same aspect ratio as your images:
fig.set_size_inches(w, h, forward=True)
or tell imshow to use an arbitrary aspect ratio
im = ax.imshow(..., aspect='auto')

I searched all day for this and ended up using this solution from #matehat when creating each image.
import matplotlib.pyplot as plt
import matplotlib.animation as animation
To make a figure without the frame :
fig = plt.figure(frameon=False)
fig.set_size_inches(w,h)
To make the content fill the whole figure
ax = plt.Axes(fig, [0., 0., 1., 1.])
ax.set_axis_off()
fig.add_axes(ax)
Draw the first frame, assuming your movie is stored in 'imageStack':
movieImage = ax.imshow(imageStack[0], aspect='auto')
I then wrote an animation function:
def animate(i):
movieImage.set_array(imageStack[i])
return movieImage
anim = animation.FuncAnimation(fig,animate,frames=len(imageStack),interval=100)
anim.save('myMovie.mp4',fps=20,extra_args=['-vcodec','libx264']
It worked beautifully!
Here is the link to the whitespace removal solution:
1: remove whitespace from image

In a recent build of matplotlib, it looks like you can pass arguments to the writer:
def grab_frame(self, **savefig_kwargs):
'''
Grab the image information from the figure and save as a movie frame.
All keyword arguments in savefig_kwargs are passed on to the 'savefig'
command that saves the figure.
'''
verbose.report('MovieWriter.grab_frame: Grabbing frame.',
level='debug')
try:
# Tell the figure to save its data to the sink, using the
# frame format and dpi.
self.fig.savefig(self._frame_sink(), format=self.frame_format,
dpi=self.dpi, **savefig_kwargs)
except RuntimeError:
out, err = self._proc.communicate()
verbose.report('MovieWriter -- Error running proc:\n%s\n%s' % (out,
err), level='helpful')
raise
If this was the case, you could pass bbox_inches="tight" and pad_inches=0 to grab_frame -> savefig and this should remove most of the border. The most up to date version on Ubuntu however, still has this code:
def grab_frame(self):
'''
Grab the image information from the figure and save as a movie frame.
'''
verbose.report('MovieWriter.grab_frame: Grabbing frame.',
level='debug')
try:
# Tell the figure to save its data to the sink, using the
# frame format and dpi.
self.fig.savefig(self._frame_sink(), format=self.frame_format,
dpi=self.dpi)
except RuntimeError:
out, err = self._proc.communicate()
verbose.report('MovieWriter -- Error running proc:\n%s\n%s' % (out,
err), level='helpful')
raise
So it looks like the functionality is being put in. Grab this version and give it a shot!

If you "just" want to save a matshow/imshow rendering of a matrix without axis annotation then newest developer version of scikit-video (skvideo) may also be relevant, - if you have avconv installed. An example in the distribution shows a dynamic image constructed from numpy function: https://github.com/aizvorski/scikit-video/blob/master/skvideo/examples/test_writer.py
Here is my modification of the example:
# Based on https://github.com/aizvorski/scikit-video/blob/master/skvideo/examples/test_writer.py
from __future__ import print_function
from skvideo.io import VideoWriter
import numpy as np
w, h = 640, 480
checkerboard = np.tile(np.kron(np.array([[0, 1], [1, 0]]), np.ones((30, 30))), (30, 30))
checkerboard = checkerboard[:h, :w]
filename = 'checkerboard.mp4'
wr = VideoWriter(filename, frameSize=(w, h), fps=8)
wr.open()
for frame_num in range(300):
checkerboard = 1 - checkerboard
image = np.tile(checkerboard[:, :, np.newaxis] * 255, (1, 1, 3))
wr.write(image)
print("frame %d" % (frame_num))
wr.release()
print("done")

Related

UserWarning: Animation was deleted without rendering anything

Note: only the last code chunk brings an error. The earlier chunks are to give context to the animation that I want. This is all in Jupyter for Windows.
I have a matplotlib pyplot with two line segments, one is "wrong" and the other is "right." I want to animate the graph to start with both lines where the blue "wrong" line is,and have the red one pivot and move to be in the right place. The "wrong" line goes from (x,y) = (-1.25,9.1) to (0.75,8.0). The "right" line goes from (-1.25,9.7) to (0.75,7.5)
Here is the code for the static comparison:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.animation import FuncAnimation
boring_fig = plt.figure()
blue = plt.plot([-1.25,.75], [9.1,8], color = 'b', label = 'wrong')
red = plt.plot([-1.25,.75], [9.7,7.5], color = 'r', label = 'right')
plt.show()
Now I want to start them both where the blue line is, and then have the red line incrementally move to the correct position. I made these two arrays for y coordinates to incrementally change between lines. Then I graph everything but the red line, in hopes of adding it as an animation after this.
y_left = np.array([9.1, 9.16, 9.22, 9.28, 9.34, 9.4, 9.46, 9.52, 9.58, 9.64, 9.7])
y_right = np.array([8.0, 7.95, 7.9, 7.85, 7.8, 7.75, 7.7, 7.65, 7.6, 7.55, 7.5])
fig = plt.figure()
blue = plt.plot([-1.25,.75], [9.1,8], color = 'b', label = 'wrong')
plt.show()
And then I try to animate the red line segment shifting along those incremented y values. I get the error somewhere in here:
def animate_1(i):
return plt.plot([-1.25,.75], [y_left[i],y_right[i]], color = 'r'),
anim = FuncAnimation(fig = fig, func = animate_1, interval = 100, frames = 10)
plt.show(anim)
And I get this message: "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."
I spent hours trying to figure this out, but I am too much of a noob. Please help.
It turns out I had to install ffmpeg for windows:
https://www.wikihow.com/Install-FFmpeg-on-Windows
Now the graph shows up but I can't see it animating. Oh well. That is an entirely different question.
here is the end graph I made (with a couple of extra details)
Add this line to the beginning of your code in order to see the animation work:
%matplotlib notebook
In Jupyter Notebook (at least on Windows) specifying the GUI toolkit will allow you to see the animation updating, not just a static graph. You can read more about toolkits and user interfaces here.
If there is a message that says:
Warning: Cannot change to a different GUI toolkit: ...
then restart the Jupyter kernel and run the cell again to update the GUI. For troubleshooting the GUI, see this answer.
In regards to the original question, if you set blit = False in FuncAnimation then the animation should display. Otherwise, you can add a return blue, statement to the animate function. See my code below and feel free to ask questions!
Complete Code
%matplotlib notebook
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.animation import FuncAnimation
def move_line(num_frames):
x_vals = [-1.25,.75]
y_left_array = np.linspace(9.1, 9.7, num=num_frames, endpoint=True)
y_right_array = np.linspace(8.0, 7.5, num=num_frames, endpoint=True)
fig = plt.figure()
red = plt.plot(x_vals, [9.7,7.5], color = 'r', label = 'right')
blue, = plt.plot([], [], color = 'b', label = 'wrong')
def animate(I):
y_vals = [y_left_array[i],y_right_array[I]]
blue.set_data(x_vals,y_vals)
return blue,
anim = FuncAnimation(
fig,
animate,
frames = num_frames,
interval = 1000/30,
repeat = False,
blit = True
)
plt.show()
return anim
animate_lines = move_line(100)

buffer_rgba() mysteriously adds whitespace to matplotlib figure

I have some simple code in a notebook to visualize an image with matplotlib
f = plt.figure()
plt.imshow(rgb_img)
# f.tight_layout(pad=0) doesn't fix the issue
f.canvas.draw()
# save figure as a np array for easy visualization w/ imshow later
fig_as_np_array = np.array(f.canvas.renderer.buffer_rgba())
At this point everything looks fine:
I then try to view the saved np array (plt.imshow(fig_as_np_array)) which I expect to display the same thing but instead I get odd whitespace plus a new sets of axis:
I can't for the life of me figure out what is adding the extra whitespace/axis, the shapes are slightly different as well:
print(f'rgb shape: {rgb_img.shape}') # prints: rgb shape: (480, 640, 3)
print(f'saved fig shape: {fig_as_np_array.shape}') # prints: saved fig shape: (288, 432, 4)
Any idea what is going on (fwiw I am visualizing this in a notebook). Thanks for your time
If I understood your question correctly, you'll have to ensure to create the figure with the correct dimensions and then remove the axes (via ax.set_axis_off()) and the frame of the figure around the image (via frameon=False) before writing to buffer, see the comments below:
import matplotlib as mpl
mpl.use("tkagg") # <— you may not need this,
# but I had to specify an agg backend manually
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import numpy as np
## image taken from
# "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5e/Empty_road_at_night.jpg/1024px-Empty_road_at_night.jpg"
filename = "1024px-Empty_road_at_night.jpg"
im = mpimg.imread(filename)
## create the figure with the correct dpi & resolution
# and make sure that you specify to show "no frame" around the image
figure_dpi = 72
fig = plt.figure(figsize=(1024/figure_dpi,768/figure_dpi),dpi=figure_dpi,frameon=False,facecolor="w")
ax = fig.add_subplot()
## turn of axes, make imshow use the whole frame
ax.set_axis_off()
plt.subplots_adjust(top = 1, bottom = 0, right = 1, left = 0, hspace = 0, wspace = 0)
plt.margins(0,0)
## show image
ax.imshow(im,zorder=0,alpha=1.0,origin="upper")
## add some text label
ax.text(300,600,"this is the middle lane",fontsize=30,color="w")
def fig2rgb_array(fig):
"""adapted from: https://stackoverflow.com/questions/21939658/"""
fig.canvas.draw()
buf = fig.canvas.tostring_rgb()
ncols, nrows = fig.canvas.get_width_height()
print("to verify, our resolution is: ",ncols,nrows)
return np.frombuffer(buf, dtype=np.uint8).reshape(nrows, ncols, 3)
## make a new figure and read from buffer
fig2,ax2 = plt.subplots()
ax2.imshow(fig2rgb_array(fig))
plt.show()
yields (note there is now only one set of axes around the image, not two):

Keep original image data when saving to pdf

I have plots that I annotate using images:
def add_image(axe, filename, position, zoom):
img = plt.imread(filename)
off_img = matplotlib.offsetbox.OffsetImage(img, zoom = zoom, resample = False)
art = matplotlib.offsetbox.AnnotationBbox(off_img, position, xybox = (0, 0),
xycoords = axe.transAxes, boxcoords = "offset points", frameon = False)
axe.add_artist(art)
Then I save the figure to some pdf file, say fig.pdf. I expect the exact original image to be embedded in the resulting pdf, without resampling. However, the image is resampled according to the dpi parameter of savefig().
How can I force matplotlib to NOT resample the image (there is no point in doing that for a vector output anyway) ?
For more details, here is a simple example, using this image as image.png:
import numpy as np
import matplotlib
matplotlib.use("agg")
import matplotlib.pyplot as plt
def add_image(axe, filename, position, zoom):
img = plt.imread(filename)
off_img = matplotlib.offsetbox.OffsetImage(img, zoom = zoom, resample = False)
art = matplotlib.offsetbox.AnnotationBbox(off_img, position, xybox = (0, 0),
xycoords = axe.transAxes, boxcoords = "offset points", frameon = False)
axe.add_artist(art)
# ==========
fig = plt.figure()
axe = plt.axes()
fig.set_size_inches(3, 1.5)
axe.plot(np.arange(10), np.arange(10))
add_image(axe, "image.png", position = (0.2, 0.7), zoom = 0.07)
fig.savefig("temp.pdf", bbox_inches = "tight", pad_inches = 0)
Expected result:
Actual result:
EDIT: There is a bug/feature issue for this question
Just a quick summary of the discussion in https://github.com/matplotlib/matplotlib/issues/16268:
Passing the image through without resampling is indeed a desireable feature, mostly because for vector output, it should really be up to the renderer (e.g. pdf viewer, printer etc.) to determine the resolution.
The fact that matplotlib currently does not allow for this is mostly an oversight.
A workaround solution (a quick hack) is to add the following code before producing the figure:
from matplotlib.backends.backend_mixed import MixedModeRenderer
def _check_unsampled_image(self, renderer):
if isinstance(renderer, MixedModeRenderer):
return True
else:
return False
matplotlib.image.BboxImage._check_unsampled_image = _check_unsampled_image
This is not meant to be used in production code though, and a more robust solution needs to be implemented in a future matplotlib version. Contributions are welcome.

Getting a sample color in matplotlib

Is it possible to get an idea of what the color will appear in a plot by supplying the original RGB value (or other colormaps like RdYlBu) in python? Currently I have to really first convert a float number using
cm.RdYlBu(x)
and make a plot to see how the colors show up. But is there a quick way to check the color? Not necessarily in python, but any website that takes the value and returns the color according to the colormap would be fine.
Quick check in python console
You may use python itself to quickly show a figure with the respective color from the matplotlib colormap.
E.g. write a function
def c(x):
col = plt.cm.RdYlBu(x)
fig, ax = plt.subplots(figsize=(1,1))
fig.set_facecolor(col)
ax.axis("off")
plt.show()
and then call it with the number of interest, e.g. c(0.4).
Example how that looks in a jupyter console:
A colormap viewer
Of course one could also write some color map viewer with matplotlib. For example:
import sys
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.widgets
class CMViewer():
def __init__(self, cmap, valinit=0.5):
self.cmap = plt.get_cmap(cmap)
col = self.cmap(valinit)
self.fig, axes = plt.subplots(nrows=4, figsize=(5,3), num=cmap,
gridspec_kw={"height_ratios" : [0.3,1,1,1]})
self.fig.subplots_adjust(right=0.85)
self.sliderax, self.cax, self.fax, self.lax = axes
self.slider = matplotlib.widgets.Slider(self.sliderax, cmap, 0., 1.,
valinit=valinit)
self.slider.on_changed(self.update)
self.slider.vline.set_color("k")
x = np.linspace(0.,1.,256)
self.cax.imshow(x[np.newaxis, :], cmap=cmap, aspect="auto")
ta = dict(axis="both", bottom=0, left=0, labelbottom=0, labelleft=0)
self.cax.tick_params(**ta)
self.fax.tick_params(**ta)
self.lax.tick_params(**ta)
x = np.array([0,0,1,2,2])
self.lax.imshow(x[np.newaxis, :], cmap="gray", aspect="auto",
extent=[0,1,-0.2,1.2])
self.lines = []
for i in range(5):
line = self.lax.plot([0,1],[i/4.,i/4.], alpha=0.2+i/5., color=col)[0]
self.lines.append(line)
self.update(0)
plt.show()
def fmtcol(self, col):
return "{:.3f} | {:.3f} | {:.3f}".format(*col)
def update(self, val):
x = self.slider.val
col = self.cmap(x)
self.fax.set_facecolor(col)
for line in self.lines:
line.set_color(col)
self.slider.poly.set_color(col)
self.slider.vline.set_xdata(x)
self.lax.set_xlabel(self.fmtcol(col))
self.fig.canvas.draw_idle()
if __name__ == "__main__":
if len(sys.argv) > 1:
CMViewer(sys.argv[1])
else:
CMViewer("RdYlBu")
If this is saved as CMViewer.py it could be run as script like
> python CMViewer.py
or with a colormap specified,
> python CMViewer.py viridis
Look at the color converter here. It takes RGB values as input.
The matplotlib docs have a colormap reference guide here:
https://matplotlib.org/stable/gallery/color/colormap_reference.html
Here's a screenshot for the diverging colormaps, which contains RdYlBu.

matplotlib hatched contourf visibility depends on pdf reader

I'm struggling with the pdf backend of matplotlib and the contourf function. I try to plot forbidden areas on a 2D colored map. The forbidden areas are represented by hatched contourf with transparent (alpha=0.4) black color. the used code is given below, with two classes written to generate a user defined legend:
import matplotlib
print matplotlib.__version__
import matplotlib.patches as mpatches
class ConstrainedArea(object):
def __init__(self,axe,_xdata,_ydata,_zdata,boundaries,fc='none',ec='none',lw=None,alpha=None,hatch='//',ls='-',fill=False,label=None):
self.bnd = boundaries
self.fc = fc
self.ec = ec
self.lw = lw
self.ls = ls
self.al = alpha
self.hh = hatch
self.fl = fill
self.lb = label
self.ctr = axe.contour(_xdata,_ydata,_zdata,boundaries,linewidths=lw,colors=ec,linestyles=ls)
#self.ctf = axe.contourf(_xdata,_ydata,_zdata,boundaries,hatches=hatch,colors=fc,facecolors=fc,alpha=alpha)
self.ctf = axe.contourf(_xdata,_ydata,_zdata,boundaries,hatches=hatch,colors=fc,alpha=alpha,antialiased=False)
pass
class ConstrainedAreaHandler(object):
def legend_artist(self,legend,orig_handle,fontsize,handlebox):
x0,y0 = handlebox.xdescent,handlebox.ydescent
wi,he = handlebox.width,handlebox.height
patch = mpatches.Rectangle([x0,y0],wi,he,facecolor=orig_handle.fc,edgecolor=orig_handle.ec,hatch=orig_handle.hh,lw=orig_handle.lw,ls=orig_handle.ls,fill=orig_handle.fl,transform=handlebox.get_transform(),label=orig_handle.lb)
handlebox.add_artist(patch)
if __name__ == "__main__":
matplotlib.rcParams['backend'] = 'PDF'
import numpy,matplotlib.pyplot as plt
xs, ys = numpy.mgrid[0:30, 0:40]
zs = (xs - 15) ** 2 + (ys - 20) ** 2 + (numpy.sin(ys) + 10) ** 2
fig = plt.figure('test',figsize=(16.0,11.8875))
axe = fig.add_subplot(111)
pcm = axe.pcolormesh(xs,ys,zs,shading='gouraud')
cas = []
for bnd,hch,ls,lb in zip([[zs.min(),200],[400,zs.max()]],['/','\\'],['-','--'],[r'$f<200$',r'$f>400$']):
cas.append(ConstrainedArea(axe,xs,ys,zs,bnd,hatch=hch,fc='k',ec='k',lw=3,ls=ls,alpha=0.2,fill=False,label=lb))
cbr = fig.colorbar(pcm)
legframe = axe.legend(cas,[c.lb for c in cas],loc=3,handler_map={ConstrainedArea:ConstrainedAreaHandler()},ncol=3,fontsize=matplotlib.rcParams['font.size']*1.2**4,numpoints=1,framealpha=0.8)
#fig.savefig('test.pdf',bbox_inches='tight',facecolor='none',edgecolor='none',transparent=True)
fig.savefig('test.pdf',bbox_inches='tight',transparent=True)
After reading the tracks on matplotlib issues
GitHub matplotlib issue 3023, and
GitHub matplotlib issue 7421, I installed matplotlib 2.0.0 thinking it would solve my problem, but it didn't.
PROBLEM DEFINITION
Using the pdf backend I save the result as pdf, but reading the same file with evince, okular, or Acrobat Reader gives different screenshots, as illustrated on the figures below:
INFORMATION
The expected output is the one given by evince (visible hatches). As already mentioned in other tracks, the rasterization of the contourf object does give the expected result but I need vectorial images. Furthermore, if rasterized hatches are used with high dpi (>300), the hatch width tends to 0 yielding wrong output. Finally I found this track matplotlib generated PDF cannot be viewed with acrobat reader issue which yielded this workaround solution :
open the matplotlib output pdf file with evince
print it to pdf
vizualise the evince-printed output with okular
which gives the screenshot below:
Thanks a lot in advance for any explanation or solution for this problem. Don't hesitate if orther details/information are needed,
Tariq

Categories