31

I have a scatter plot set up and plotted the way I want it, and I want to create an .mp4 video of the figure rotating in space, as if I had used plt.show() and dragged the viewpoint around.

This answer is almost exactly what I want, except to save a movie I would have to manually call into FFMpeg with a folder of images. Instead of saving individual frames I'd prefer to use Matplotlib's built in animation support. Code reproduced below:

from mpl_toolkits.mplot3d import Axes3D
ax = Axes3D(fig)
ax.scatter(xx,yy,zz, marker='o', s=20, c="goldenrod", alpha=0.6)
for ii in xrange(0,360,1):
    ax.view_init(elev=10., azim=ii)
    savefig("movie"%ii+".png")
Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
Nate
  • 2,462
  • 1
  • 20
  • 28
  • **Very much related:** [How to animate a rotating box using Python.](https://mas.to/@artilectzed/103986205750272190) – rugk Apr 12 '20 at 16:59

4 Answers4

40

If you want to learn more about matplotlib animations you should really follow this tutorial. It explains in great length how to create animated plots.

Note: Creating animated plots require ffmpeg or mencoder to be installed.

Here is a version of his first example changed to work with your scatterplot.

# First import everthing you need
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation
from mpl_toolkits.mplot3d import Axes3D

# Create some random data, I took this piece from here:
# http://matplotlib.org/mpl_examples/mplot3d/scatter3d_demo.py
def randrange(n, vmin, vmax):
    return (vmax - vmin) * np.random.rand(n) + vmin
n = 100
xx = randrange(n, 23, 32)
yy = randrange(n, 0, 100)
zz = randrange(n, -50, -25)

# Create a figure and a 3D Axes
fig = plt.figure()
ax = Axes3D(fig)

# Create an init function and the animate functions.
# Both are explained in the tutorial. Since we are changing
# the the elevation and azimuth and no objects are really
# changed on the plot we don't have to return anything from
# the init and animate function. (return value is explained
# in the tutorial.
def init():
    ax.scatter(xx, yy, zz, marker='o', s=20, c="goldenrod", alpha=0.6)
    return fig,

def animate(i):
    ax.view_init(elev=10., azim=i)
    return fig,

# Animate
anim = animation.FuncAnimation(fig, animate, init_func=init,
                               frames=360, interval=20, blit=True)
# Save
anim.save('basic_animation.mp4', fps=30, extra_args=['-vcodec', 'libx264'])
Viktor Kerkez
  • 45,070
  • 12
  • 104
  • 85
  • 1
    Sorry, I hope you aren't actually offended. Not sure I agree with you on the lack of return values. I think `blit` needs them to work properly, but that section of the library is still a bit of a black box to me. (I did up-vote the answer). – tacaswell Aug 21 '13 at 00:20
  • 1
    Of course you haven't! :D I actually respect your comments very much, since they come from an experienced `matplotlib` developer! And I try to learn from them and behave according to your suggestions (yesterday about the question flagging). So please **DO** correct me every time you think I did something wrong :). It's both in the interest of the SO community, and my own learning process. It just happened that we had a slight methodology disagreement on the previous question so I added this as a joke since I knew you would read all the `matplotlib` questions :) – Viktor Kerkez Aug 21 '13 at 00:38
  • About the return value. In the blog post Jake said: "It is important that this function return the line object, because this tells the animator which objects on the plot to update after each frame". But since none of the objects on the plot change, I assumed that nothing should be returned. I'll edit the post if that's an incorrect assumption. – Viktor Kerkez Aug 21 '13 at 00:41
  • My understanding of how `blit` works is that it saves a screen-buffer of the 'base' image, and then draws the updated artists on top of it, only re-rendering those that change. Before the next frame, the visible screen in replaced by the saved buffer and the next frame is drawn on top. I suspect that when changing the view angle, some magic happens underneath that forces a full re-draw anyway, making the blitting irrelevant. As I said, this is section of the library is very-black boxy for me. – tacaswell Aug 21 '13 at 00:51
  • OK but what should I return in this case, since the `ax.view_init` returns `None`. I suppose I should keep a reference to the return value of the `ax.scatter` and always return that? – Viktor Kerkez Aug 21 '13 at 09:33
  • 4
    This needs to `return ()` in both `init` and `animate` in order to not crash – Eric May 11 '16 at 22:23
  • The code crashes on me in Python 3 IPython notebook. The error i get is "RuntimeError: The init_func must return a sequence of Artist objects.". So I guess something needs to be returned after all. If this did work in 2013 maybe some update removed the option of not returning anything in `init` or `animate`. – Harsh Jun 19 '17 at 19:18
  • 2
    when I copy and paste this code into my IDLE i get the following error, anyone has any idea? File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/matplotlib/animation.py", line 1260, in axes = {a.axes for a in artists} TypeError: unhashable type: 'list' – user3015729 Jan 24 '19 at 23:22
  • @user3015729 try removing blit=true. It worked for me without that. – Phoenix May 16 '22 at 19:55
10

Was looking into animating my plots with matplotlib when I stumbled upon this: http://zulko.wordpress.com/2012/09/29/animate-your-3d-plots-with-pythons-matplotlib/

It provides simple function to animate around a plot and output in a number of formats.

Jameshobbs
  • 524
  • 7
  • 17
  • this answer is actually amazing. Btw this code in the link needs `pip install pillow` first. source: https://github.com/ipython/ipython/issues/8052 – F.S. Aug 17 '18 at 23:50
3

The solution provided by Viktor Kerkez does not work for me (matplotlib version 3.4.2). I get the same error as report by user3015729. Below is an adapted version that produces the same results but works with matplotlib 3.4.2.

# First import everthing you need
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation
from mpl_toolkits.mplot3d import Axes3D

# Create some random data, I took this piece from here:
# http://matplotlib.org/mpl_examples/mplot3d/scatter3d_demo.py


def randrange(n, vmin, vmax):
    return (vmax - vmin) * np.random.rand(n) + vmin


n = 100
xx = randrange(n, 23, 32)
yy = randrange(n, 0, 100)
zz = randrange(n, -50, -25)

# Create a figure and a 3D Axes
fig = plt.figure()
ax = Axes3D(fig)
scat = ax.scatter(xx, yy, zz, marker='o', s=20, c="goldenrod", alpha=0.6)

# Create an init function and the animate functions.
# Both are explained in the tutorial. Since we are changing
# the the elevation and azimuth and no objects are really
# changed on the plot we don't have to return anything from
# the init and animate function. (return value is explained
# in the tutorial.


def init():
    ax.view_init(elev=10., azim=0)
    return [scat]


def animate(i):
    ax.view_init(elev=10., azim=i)
    return [scat]


# Animate
anim = animation.FuncAnimation(fig, animate, init_func=init,
                               frames=360, interval=20, blit=True)
# Save
anim.save('basic_animation.mp4', fps=30, extra_args=['-vcodec', 'libx264'])
2

It's a bit of a hack, but if you are using Jupyter notebook you can use cell magics to run the command line version of ffmpeg directly from the notebook itself. In one cell run your script to generate raw frames

from mpl_toolkits.mplot3d import Axes3D
ax = Axes3D(fig)
ax.scatter(xx,yy,zz, marker='o', s=20, c="goldenrod", alpha=0.6)
for ii in xrange(0,360,1):
    ax.view_init(elev=10., azim=ii)
    savefig("movie%d.png" % ii)

Now, in a new notebook cell, enter the following and then run the cell

%%bash 
ffmpeg -r 30 -i movie%d.png -c:v libx264 -vf fps=25 -pix_fmt yuv420p out.mp4
wil3
  • 2,877
  • 2
  • 18
  • 22