matplotlib animation: write to png files without third party module - python

The animation module in matplotlib usually requires third party modules like FFmpeg, mencoder or imagemagik to be able to save the animation to a file (e.g. here: https://stackoverflow.com/a/25143651/5082048).
Even the MovieWriter class in matplotlib seems to be build in a way that third party modules will be incorporated (starting and closing processes, cummunicating via pipe): http://matplotlib.org/api/animation_api.html#matplotlib.animation.MovieWriter.
I am looking for a way, how I can save a matplotlib.animation.FuncAnimation object frame to frame to png - directly, within python. Afterwards, I want to show the .png files as animation in an iPython notebook using this approach: https://github.com/PBrockmann/ipython_animation_javascript_tool/
Therefore my questions are:
How can I save an matplotlib.animation.FuncAnimation object directly to .png files without the need to use third party modules?
Is there a writer class implemented for this usecase?
How can I get figure objects frame by frame out of the FuncAnimation object (so that I could save them myself)?
Edit: The matplotlib.animation.FuncAnimation object is given, the task is to save it's frames using pure Python. Unfortunately, I cannot change the underlying animation function as ImportanceOfBeingErnest suggested.

Althoough this may seem a bit complicated, saving the frames of an animation may be easily done within the animation itself.
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.animation
import numpy as np
def animate(i):
line.set_ydata(np.sin(2*np.pi*i / 50)*np.sin(x))
#fig.canvas.draw() not needed see comment by #tacaswell
plt.savefig(str(i)+".png")
return line,
fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_xlim(0, 2*np.pi)
ax.set_ylim(-1,1)
x = np.linspace(0, 2*np.pi, 200)
line, = ax.plot(x, np.zeros_like(x))
plt.draw()
ani = matplotlib.animation.FuncAnimation(fig, animate, frames=5, repeat=False)
plt.show()
Mind the repeat = False argument, which will prevent the animation to run continuously and repeat writing the same files to disk.
Note that if you're willing to loosen the restriction of "no external package" you may use imagemagick to save the pngs
ani.save("anim.png", writer="imagemagick")
which will save files anim-1.png, anim-2.png etc.
Finally note that there are of course easier methods to show an animation in a jupyter notebook.

You want to look at the FileMovieWriter sub-classes (See http://matplotlib.org/2.0.0rc2/api/animation_api.html#writer-classes) You probably want to sub-class FileMoveWriter, something like
import matplotlib.animation as ma
class BunchOFiles(ma.FileMovieWriter):
def setup(self, fig, dpi, frame_prefix):
super().setup(fig, dpi, frame_prefix, clear_temp=False)
def _run(self):
# Uses subprocess to call the program for assembling frames into a
# movie file. *args* returns the sequence of command line arguments
# from a few configuration options.
pass
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.
'''
# Tell the figure to save its data to the sink, using the
# frame format and dpi.
with self._frame_sink() as myframesink:
self.fig.savefig(myframesink, format=self.frame_format,
dpi=self.dpi, **savefig_kwargs)
def cleanup(self):
# explictily skip a step in the mro
ma.MovieWriter.cleanup(self)
(this is not tested, might be better to just implement a class that implements saving, grab_frame, finished, and setup)

I could not get tacaswell's answer to work without modification. So, here is my take at it.
from matplotlib.animation import FileMovieWriter
class BunchOFiles(FileMovieWriter):
supported_formats = ['png', 'jpeg', 'bmp', 'svg', 'pdf']
def __init__(self, *args, extra_args=None, **kwargs):
# extra_args aren't used but we need to stop None from being passed
super().__init__(*args, extra_args=(), **kwargs)
def setup(self, fig, dpi, frame_prefix):
super().setup(fig, dpi, frame_prefix, clear_temp=False)
self.fname_format_str = '%s%%d.%s'
self.temp_prefix, self.frame_format = self.outfile.split('.')
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.
'''
# Tell the figure to save its data to the sink, using the
# frame format and dpi.
with self._frame_sink() as myframesink:
self.fig.savefig(myframesink, format=self.frame_format,
dpi=self.dpi, **savefig_kwargs)
def finish(self):
self._frame_sink().close()
We can save a set of files with:
anim.save('filename.format', writer=BunchOFiles())
and it will save the files in the form 'filename{number}.format'.

Related

Can I show a plot created in another file on JupyterLab?

I wish to have an interactive map that you can click where, once clicked, a SkewT and Hodograph will be plotted showing the information for that location. I have thus created a class where I add all the necessary informations using the metpy library and I am able to successfully create these graphs:
SkewT and Hodograph plotted
The problem comes when I'm trying to import the classes I've created to generate these plots into jupyterlab. Since the code to actually make these plots is quite cumbersome, I'd rather
keep the code in a separate file and import my SoundingGraphs class, but it's not working. The graphs never get plotted inside a cell, they instead appear in the logs as a Warning and as an Info and I have no idea why:
Graphs appearing inside logs
Tried to use plt.show() inside my file, tried returning plt to then use plt.show() inside a cell of the notebook, tried using %matplotlib widget, %matplotlib notebook and %matplotlib inline, tried changing jupyterlab versions, none of these changed anything.
I have found one solution that I disliked, but that does work, which is rather than doing a plt.show(), to instead do this inside my class:
buffer = BytesIO()
plt.savefig(buffer, format='png')
return buffer
And in the notebook I would do:
image = Image()
display(image)
def on_generate_button_clicked(b):
buffer = SoundingGraphs(infos)
buffer.seek(0)
image.value=buffer.read()
image.format='png'
generate_button.on_click(on_generate_button_clicked)
I don't quite like this approach because further down the line I would like to add interactivity to my plots, like show values of plot when hovered and things like that, thus I don't just want to show an image. So I'd like to know if it is indeed possible to plt.show() a plot created inside another file in a cell.
Using:
Python 3.6.9
jupyterlab==3.2.9
jupyterlab-pygments==0.1.2
jupyterlab-server==2.10.3
jupyterlab-widgets==1.1.0
ipykernel==5.5.6
ipyleaflet==0.14.0
ipympl==0.8.8
ipython==7.16.3
ipython-genutils==0.2.0
ipywidgets==7.7.0
matplotlib==3.3.4
Thanks!
Yes, it is possible after all!
%matplotlib widget needs to be used at the start of the notebook and since the class method will be called from another function (on a button.on_click event), it is possible to use the #out.capture() decorator above it so that the plt.show() gets displayed. It's also possible to make the figure a class attribute to be able to have more control.
So here's a bit of working code if someone would like to replicate:
Notebook
%matplotlib widget
from ipywidgets import Button, Output
from myfile import MyClass
out = Output()
example_button = Button(
description='Example',
disabled=False,
button_style='',
tooltip='Click me'
)
#out.capture()
def on_example_button_clicked(b):
example_button.disabled = True
myclass = MyClass()
myclass.create_plot()
out.clear_output(wait=True)
display(myclass.fig.canvas)
example_button.disabled = False
example_button.on_click(on_example_button_clicked)
display(example_button)
display(out)
myfile.py
import matplotlib.pyplot as plt
class MyClass():
def __init__(self):
plt.ioff() # otherwise it'll also show inside logs
plt.clf()
self.fig = plt.figure()
def create_plot(self):
plt.plot([1, 2, 3, 4])
plt.ylabel('some numbers')

matplotlib.animate in python using multiprocessing

I am trying to use a python process to animate a plot as shown below:
from multiprocessing import Process
import datetime as dt
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np
process_enabled = 1;
print("Process enabled: ", process_enabled)
x = []
y = []
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
def start_animation():
# Set up plot to call animate() function periodically
ani = animation.FuncAnimation(fig, animate, fargs=(x, y), interval=1000)
print("Called animate function")
plt.show()
# This function is called periodically from FuncAnimation
def animate(i, xs, ys):
fx=[0.045,0.02,0.0,0.04,0.015,-0.01,0.015,0.045,0.035,0.01,
0.055,0.04,0.02,0.025,0.0,-0.005,-0.005,-0.02,-0.05,-0.03] # fx values
# Add x and y to lists
xs.append(dt.datetime.now().strftime('%H:%M:%S.%f'))
if(i<len(fx)):
ys.append(fx[i])
# Draw x and y lists
ax.clear()
if(i<len(fx)):
ys_stacked = np.stack((np.array(ys),0.1+np.array(ys)),axis=1)
ax.plot(xs, ys_stacked)
print("Animating")
# Format plot
if(i<len(fx)):
plt.xticks(rotation=45, ha='right')
plt.subplots_adjust(bottom=0.30)
plt.title('Force/Torque Sensor Data')
plt.ylabel('Fx (N)')
if(process_enabled):
p_graph = Process(name='Graph', target=start_animation)
print("Created graph process")
p_graph.start()
print("Started graph process")
else:
start_animation()
When I disable the process, the start_animation() function works fine and the plot is displayed and the animation begins. However, when the process is enabled, the process starts and then the code breaks at print("Called animate function"). There is no plot window and there are no error messages in the terminal).
I'm new to both multiprocessing in python and indeed matplotlib. Any direction would be much appreciated.
Cheers,
Tony
I'm trying to solve this same problem, but haven't quite figured it out completely. However, I think I can provide a few useful comments on your question.
To start, is there any reason why you want to handle the animation in a separate process? Your approach seems to work fine within a single process. There's a number of issues you'll need to address to do this. If you truly do require a separate process, then the following might be useful.
First, you won't be able to use your global variables in the 'graph' process, as that process doesn't share the same instances of those variables (see Globals variables and Python multiprocessing).
You can share state between processes, but this is difficult for complex objects that you'd want to share (i.e. plt.figure()). See the multiprocessing reference for more information (https://docs.python.org/3/library/multiprocessing.html#sharing-state-between-processes)
One final suggestion would be to do away with the pyplot interface. This is handy for straightforward scripts and interactive data analysis, but it obfuscates a lot of important things - like knowing which figure, axis etc you're dealing with when you call plt methods.
I've provided an alternative, object-oriented approach using a custom class, that can run your animation (without a separate process):
import sys
from multiprocessing import Process, Queue
import datetime as dt
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.backends.qt_compat import QtWidgets
import matplotlib.animation as animation
class StripChart(FigureCanvasQTAgg):
def __init__(self):
self.fig = Figure(figsize=(8,5), dpi=100)
self.ax = self.fig.add_subplot(111)
# hold a copy of our torque data
self.fx = [0.045,0.02,0.0,0.04,0.015,-0.01,0.015,0.045,0.035,0.01,
0.055,0.04,0.02,0.025,0.0,-0.005,-0.005,-0.02,-0.05,-0.03]
super().__init__(self.fig)
# instantiate the data arrays
self.xs = []
self.ys = []
def start_animation(self):
print("starting animation")
# set up the animation
self.ani = animation.FuncAnimation(self.fig, self.animate, init_func=self.clear_frame,
frames=100, interval=500, blit=False)
def clear_frame(self):
self.ax.clear()
self.ax.plot([], [])
def animate(self, i):
print("animate frame")
# get the current time
t_now = dt.datetime.now()
# update trace values
self.xs.append(t_now.strftime("%H:%M:%S.%f"))
self.ys.append(self.fx[i % len(self.fx)])
# keep max len(self.fx) points
if len(self.xs) > len(self.fx):
self.xs.pop(0)
self.ys.pop(0)
self.ax.clear()
self.ax.plot(self.xs, self.ys)
# need to reapply format after clearing axes
self.fig.autofmt_xdate(rotation=45)
self.fig.subplots_adjust(bottom=0.30)
self.ax.set_title('Force/Torque Sensor Data')
self.ax.set_ylabel('Fx (N)')
if __name__=='__main__':
# start a new qapplication
qapp = QtWidgets.QApplication(sys.argv)
# create our figure in the main process
strip_chart = StripChart()
strip_chart.show()
strip_chart.start_animation()
# start qt main loop
qapp.exec()
Things of note in this example:
you'll need to have a backend installed in your environment (i.e. pip install pyqt5)
I've added an init_func to the animation, you don't really need this as you can call self.ax.clear() in the animate method.
If you need better performance for your animation, you can use blit=True but you'll need to modify the clear_frame and animate methods to return the artists that you want to update (see https://jakevdp.github.io/blog/2012/08/18/matplotlib-animation-tutorial/ for more info). One drawback is that you won't be able to update the axis labels with that approach.
I've set it up to run infinitely until you close the window
I'm assuming that the reason you want to run the animation in a separate process is that there is some time consuming/CPU intensive task that is involved in either updating the graph data, or drawing all the points. Perhaps you have this embedded in some other UI?
I've tried to execute the animation in a separate process, but you need to pass the instance of the figure that's displayed. As I mentioned this isn't straightforward, although there do appear to be ways to do it (https://stackoverflow.com/a/57793267/13752965). I'll update if I find a working solution.

python: How to monitor a variable ? Execute command when variable change

I have a class like this
class WaveData(object):
def __init__(self, data):
self.data = data
and create a data object, plot a figure
wave = WaveData([[1, 2, 3],
[7, 5, 6]])
import matplotlib.pyplot as plt
fig=plt.figure()
plot1, = fig.canvas.figure.subplots().plot(wave.data[0])
plot2, = fig.canvas.figure.subplots().plot(wave.data[1])
I hope when I change the wave value , plot will change synchronously
wave.data[1]=[5,6,7] # hope figure change together
I try to add method changedata for WaveData class, but:
it need use global variable fig , maybe not reasonale (I can put fig as self attribute , but in fact , the fig also link other class object which not written in here)
I cannot change fig by changing data directly: wave.data[1] =[5,6,7]
class WaveData(object):
def __init__(self, data):
self.data = data
def changedata(self,value,index):
self.data[index]=value
#-- change the plot index th plot data--#
global plot1,plot2,fig
plot1.set_ydata(self.data[1])
plot2.set_ydata(self.data[2])
fig.canvas.draw_idle()
#-- change the plot index th plot data--#
I want to create a watcher to monitor wave.data value . When detecte the value change , execute some action
How to do?
Right: plotting is not a dynamic or interactive process. You started the right way, with an access method to change the wave form. Now you have to re-plot and re-show the result ... which will possibly require closing the first plot manually, depending on the interface of your chosen drawing package (e.g. matplotlib).
To get a fully interactive experience, you may want to use an animation package, such as PyGame, where changes in the visual display are part of the package assumptions.

Can I save to disk a plot generated by pandas df.plot? [duplicate]

In ipython Notebook, first create a pandas Series object, then by calling the instance method .hist(), the browser displays the figure.
I am wondering how to save this figure to a file (I mean not by right click and save as, but the commands needed in the script).
Use the Figure.savefig() method, like so:
ax = s.hist() # s is an instance of Series
fig = ax.get_figure()
fig.savefig('/path/to/figure.pdf')
It doesn't have to end in pdf, there are many options. Check out the documentation.
Alternatively, you can use the pyplot interface and just call the savefig as a function to save the most recently created figure:
import matplotlib.pyplot as plt
s.hist()
plt.savefig('path/to/figure.pdf') # saves the current figure
Plots from multiple columns
Added from a comment toto_tico made on 2018-05-11
If you are getting this error AttributeError: 'numpy.ndarray' object has no attribute 'get_figure', then it is likely that you are plotting multiple columns.
In this case, ax will be an array of all the axes.
ax = s.hist(columns=['colA', 'colB'])
# try one of the following
fig = ax[0].get_figure()
fig = ax[0][0].get_figure()
fig.savefig('figure.pdf')
You can use ax.figure.savefig():
import pandas as pd
s = pd.Series([0, 1])
ax = s.plot.hist()
ax.figure.savefig('demo-file.pdf')
This has no practical benefit over ax.get_figure().savefig() as suggested in Philip Cloud's answer, so you can pick the option you find the most aesthetically pleasing. In fact, get_figure() simply returns self.figure:
# Source from snippet linked above
def get_figure(self):
"""Return the `.Figure` instance the artist belongs to."""
return self.figure
You can simply save your (e.g. histogram) plot like this:
df.plot.hist().get_figure().savefig('name')
Just wanted to add that the default resolution is 100dpi, which is fine for screen but won't work if you want to enlarge or print it. You can pass a 'dpi' parameter to get a high-resolution file:
ax = s.hist() # s is an instance of Series
ax.figure.savefig('/path/to/figure.png', dpi=300)

save a pandas.Series histogram plot to file

In ipython Notebook, first create a pandas Series object, then by calling the instance method .hist(), the browser displays the figure.
I am wondering how to save this figure to a file (I mean not by right click and save as, but the commands needed in the script).
Use the Figure.savefig() method, like so:
ax = s.hist() # s is an instance of Series
fig = ax.get_figure()
fig.savefig('/path/to/figure.pdf')
It doesn't have to end in pdf, there are many options. Check out the documentation.
Alternatively, you can use the pyplot interface and just call the savefig as a function to save the most recently created figure:
import matplotlib.pyplot as plt
s.hist()
plt.savefig('path/to/figure.pdf') # saves the current figure
Plots from multiple columns
Added from a comment toto_tico made on 2018-05-11
If you are getting this error AttributeError: 'numpy.ndarray' object has no attribute 'get_figure', then it is likely that you are plotting multiple columns.
In this case, ax will be an array of all the axes.
ax = s.hist(columns=['colA', 'colB'])
# try one of the following
fig = ax[0].get_figure()
fig = ax[0][0].get_figure()
fig.savefig('figure.pdf')
You can use ax.figure.savefig():
import pandas as pd
s = pd.Series([0, 1])
ax = s.plot.hist()
ax.figure.savefig('demo-file.pdf')
This has no practical benefit over ax.get_figure().savefig() as suggested in Philip Cloud's answer, so you can pick the option you find the most aesthetically pleasing. In fact, get_figure() simply returns self.figure:
# Source from snippet linked above
def get_figure(self):
"""Return the `.Figure` instance the artist belongs to."""
return self.figure
You can simply save your (e.g. histogram) plot like this:
df.plot.hist().get_figure().savefig('name')
Just wanted to add that the default resolution is 100dpi, which is fine for screen but won't work if you want to enlarge or print it. You can pass a 'dpi' parameter to get a high-resolution file:
ax = s.hist() # s is an instance of Series
ax.figure.savefig('/path/to/figure.png', dpi=300)

Categories

Resources