Python has some great data visualization librairies, but few can render GIFs or video animations. This post shows how to use MoviePy as a generic animation plugin for any other library.
MoviePy lets you define custom animations with a function make_frame(t), which returns the video frame corresponding to time t (in seconds):
123456789101112
frommoviepy.editorimportVideoClipdefmake_frame(t):""" returns an image of the frame at time t """# ... create the frame with any libraryreturnframe_for_time_t# (Height x Width x 3) Numpy arrayanimation=VideoClip(make_frame,duration=3)# 3-second clip# For the export, many options/formats/optimizations are supportedanimation.write_videofile("my_animation.mp4",fps=24)# export as videoanimation.write_gif("my_animation.gif",fps=24)# export as GIF (slow)
In previous posts I used this method to animate vector graphics (with the library Gizeh), and ray-traced 3D scenes (generated by POV-Ray). This post covers the scientific libraries Mayavi, Vispy, Matplotlib, Numpy, and Scikit-image.
Animations with Mayavi
Mayavi is a Python module for interactive 3D data visualization with a simple interface. In this first example we animate a surface whose elevation depends on the time t:
12345678910111213141516171819202122
importnumpyasnpimportmayavi.mlabasmlabimportmoviepy.editorasmpyduration=2# duration of the animation in seconds (it will loop)# MAKE A FIGURE WITH MAYAVIfig_myv=mlab.figure(size=(220,220),bgcolor=(1,1,1))X,Y=np.linspace(-2,2,200),np.linspace(-2,2,200)XX,YY=np.meshgrid(X,Y)ZZ=lambdad:np.sinc(XX**2+YY**2)+np.sin(XX+d)# ANIMATE THE FIGURE WITH MOVIEPY, WRITE AN ANIMATED GIFdefmake_frame(t):mlab.clf()# clear the figure (to reset the colors)mlab.mesh(YY,XX,ZZ(2*np.pi*t/duration),figure=fig_myv)returnmlab.screenshot(antialiased=True)animation=mpy.VideoClip(make_frame,duration=duration)animation.write_gif("sinc.gif",fps=20)
Another example with a wireframe mesh whose coordinates and view angle depend on the time :
12345678910111213141516171819202122232425262728
importnumpyasnpimportmayavi.mlabasmlabimportmoviepy.editorasmpyduration=2# duration of the animation in seconds (it will loop)# MAKE A FIGURE WITH MAYAVIfig=mlab.figure(size=(500,500),bgcolor=(1,1,1))u=np.linspace(0,2*np.pi,100)xx,yy,zz=np.cos(u),np.sin(3*u),np.sin(u)# Pointsl=mlab.plot3d(xx,yy,zz,representation="wireframe",tube_sides=5,line_width=.5,tube_radius=0.2,figure=fig)# ANIMATE THE FIGURE WITH MOVIEPY, WRITE AN ANIMATED GIFdefmake_frame(t):""" Generates and returns the frame for time t. """y=np.sin(3*u)*(0.2+0.5*np.cos(2*np.pi*t/duration))l.mlab_source.set(y=y)# change y-coordinates of the meshmlab.view(azimuth=360*t/duration,distance=9)# camera anglereturnmlab.screenshot(antialiased=True)# return a RGB imageanimation=mpy.VideoClip(make_frame,duration=duration).resize(0.5)# Video generation takes 10 seconds, GIF generation takes 25sanimation.write_videofile("wireframe.mp4",fps=20)animation.write_gif("wireframe.gif",fps=20)
As Mayavi relies on the powerful ITK visualization engine it can also process complex datasets. Here is an animation derived from a Mayavi example:
Vispy is another interactive 3D data visualization library, based on OpenGL. As for Mayavi, we first create a figure and a mesh, that we animate with MoviePy.
12345678910111213141516171819202122232425262728
frommoviepy.editorimportVideoClipimportnumpyasnpfromvispyimportapp,scenefromvispy.gloo.utilimport_screenshotcanvas=scene.SceneCanvas(keys='interactive')view=canvas.central_widget.add_view()view.set_camera('turntable',mode='perspective',up='z',distance=2,azimuth=30.,elevation=65.)xx,yy=np.arange(-1,1,.02),np.arange(-1,1,.02)X,Y=np.meshgrid(xx,yy)R=np.sqrt(X**2+Y**2)Z=lambdat:0.1*np.sin(10*R-2*np.pi*t)surface=scene.visuals.SurfacePlot(x=xx-0.1,y=yy+0.2,z=Z(0),shading='smooth',color=(0.5,0.5,1,1))view.add(surface)canvas.show()# ANIMATE WITH MOVIEPYdefmake_frame(t):surface.set_data(z=Z(t))# Update the mathematical surfacecanvas.on_draw(None)# Update the image on Vispy's canvasreturn_screenshot((0,0,canvas.size[0],canvas.size[1]))[:,:,:3]animation=VideoClip(make_frame,duration=1).resize(width=350)animation.write_gif('sinc_vispy.gif',fps=20,opt='OptimizePlus')
Here are more advanced examples (derived from the Vispy gallery) where C code snippets are embedded in the Python code to fine-tune the 3D shaders:
The 2D/3D plotting library Matplotlib already has an animation module, but I found that MoviePy produces lighter, better quality videos, while being up to two times faster (not sure why, see here for more details). Here is how you animate Matplotlib with MoviePy:
123456789101112131415161718192021222324
importmatplotlib.pyplotaspltimportnumpyasnpfrommoviepy.video.io.bindingsimportmplfig_to_npimageimportmoviepy.editorasmpy# DRAW A FIGURE WITH MATPLOTLIBduration=2fig_mpl,ax=plt.subplots(1,figsize=(5,3),facecolor='white')xx=np.linspace(-2,2,200)# the x vectorzz=lambdad:np.sinc(xx**2)+np.sin(xx+d)# the (changing) z vectorax.set_title("Elevation in y=0")ax.set_ylim(-1.5,2.5)line,=ax.plot(xx,zz(0),lw=3)# ANIMATE WITH MOVIEPY (UPDATE THE CURVE FOR EACH t). MAKE A GIF.defmake_frame_mpl(t):line.set_ydata(zz(2*np.pi*t/duration))# <= Update the curvereturnmplfig_to_npimage(fig_mpl)# RGB image of the figureanimation=mpy.VideoClip(make_frame_mpl,duration=duration)animation.write_gif("sinc_mpl.gif",fps=20)
Matplotlib has many beautiful themes and works well with numerical modules like Pandas or Scikit-Learn. Let us watch a SVM classifier getting a better understanding of the map as the number of training point increases.
importnumpyasnpimportmatplotlib.pyplotaspltfromsklearnimportsvm# sklearn = scikit-learnfromsklearn.datasetsimportmake_moonsfrommoviepy.editorimportVideoClipfrommoviepy.video.io.bindingsimportmplfig_to_npimageX,Y=make_moons(50,noise=0.1,random_state=2)# semi-random datafig,ax=plt.subplots(1,figsize=(4,4),facecolor=(1,1,1))fig.subplots_adjust(left=0,right=1,bottom=0)xx,yy=np.meshgrid(np.linspace(-2,3,500),np.linspace(-1,2,500))defmake_frame(t):ax.clear()ax.axis('off')ax.set_title("SVC classification",fontsize=16)classifier=svm.SVC(gamma=2,C=1)# the varying weights make the points appear one after the otherweights=np.minimum(1,np.maximum(0,t**2+10-np.arange(50)))classifier.fit(X,Y,sample_weight=weights)Z=classifier.decision_function(np.c_[xx.ravel(),yy.ravel()])Z=Z.reshape(xx.shape)ax.contourf(xx,yy,Z,cmap=plt.cm.bone,alpha=0.8,vmin=-2.5,vmax=2.5,levels=np.linspace(-2,2,20))ax.scatter(X[:,0],X[:,1],c=Y,s=50*weights,cmap=plt.cm.bone)returnmplfig_to_npimage(fig)animation=VideoClip(make_frame,duration=7)animation.write_gif("svm.gif",fps=15)
Put simply, the background colors tell us where the classifier thinks the black points and white points belong. At the begining it has no real clue, but as more points appear it progressively understands that they are distributed along moon-shaped regions.
Animations with Numpy
If you are working with Numpy arrays (Numpy is the central numerical library in Python), you don’t need any external plotting library, you can feed the arrays directly to MoviePy.
This is well illustrated by this simulation of a zombie outbreak in France (inspired by this blog post by Max Berggren). France is modelled as a grid (Numpy array) on which all the computations for dispersion and infection are done. At regular intervals, a few Numpy operations tranform the grid into a valid RGB image, and send it to MoviePy.
What is better than an animation ? Two animations ! You can take advantage of MoviePy’s video composition capabilities to mix animations from different libraries:
123456
importmoviepy.editorasmpy# We use the GIFs generated earlier to avoid recomputing the animations.clip_mayavi=mpy.VideoFileClip("sinc.gif")clip_mpl=mpy.VideoFileClip("sinc_mpl.gif").resize(height=clip_mayavi.h)animation=mpy.clips_array([[clip_mpl,clip_mayavi]])animation.write_gif("sinc_plot.gif",fps=20)
Or for something more artistic:
12345678
# Make the white color transparent in clip_mayaviclip_mayavi2=(clip_mayavi.fx(mpy.vfx.mask_color,[255,255,255]).set_opacity(.4)# whole clip is semi-transparent.resize(height=0.85*clip_mpl.h).set_pos('center'))animation=mpy.CompositeVideoClip([clip_mpl,clip_mayavi2])animation.write_gif("sinc_plot2.gif",fps=20)
It may be a tad too flashy, but sometimes you must give your audience something they can tweet.
You can also annotate the animations, which is useful when comparing different filters or algorithms. Let’s display four image transformations from the library Scikit-image:
1234567891011121314151617181920212223242526
importmoviepy.editorasmpyimportskimage.exposureasske# rescaling, histogram eq.importskimage.filterasskf# gaussian blurclip=mpy.VideoFileClip("sinc.gif")gray=clip.fx(mpy.vfx.blackwhite).to_mask()defapply_effect(effect,title,**kw):""" Returns a clip with the effect applied and a title"""filtr=lambdaim:effect(im,**kw)new_clip=gray.fl_image(filtr).to_RGB()txt=(mpy.TextClip(title,font="Purisa-Bold",fontsize=15).set_position(("center","top")).set_duration(clip.duration))returnmpy.CompositeVideoClip([new_clip,txt])# Apply 4 different effects to the original animationequalized=apply_effect(ske.equalize_hist,"Equalized")rescaled=apply_effect(ske.rescale_intensity,"Rescaled")adjusted=apply_effect(ske.adjust_log,"Adjusted")blurred=apply_effect(skf.gaussian_filter,"Blurred",sigma=4)# Put the clips together on a 2x2 grid, and write to a file.finalclip=mpy.clips_array([[equalized,adjusted],[blurred,rescaled]])final_clip.write_gif("test2x2.gif",fps=20)
If we replace CompositeVideoClip and clips_array by concatenate_videoclips we get a title-effect type animation:
12345678910111213141516171819202122232425
importmoviepy.editorasmpyimportskimage.exposureasskeimportskimage.filterasskfclip=mpy.VideoFileClip("sinc.gif")gray=clip.fx(mpy.vfx.blackwhite).to_mask()defapply_effect(effect,label,**kw):""" Returns a clip with the effect applied and a top label"""filtr=lambdaim:effect(im,**kw)new_clip=gray.fl_image(filtr).to_RGB()txt=(mpy.TextClip(label,font="Amiri-Bold",fontsize=25,bg_color='white',size=new_clip.size).set_position(("center")).set_duration(1))returnmpy.concatenate_videoclips([txt,new_clip])equalized=apply_effect(ske.equalize_hist,"Equalized")rescaled=apply_effect(ske.rescale_intensity,"Rescaled")adjusted=apply_effect(ske.adjust_log,"Adjusted")blurred=apply_effect(skf.gaussian_filter,"Blurred",sigma=4)clips=[equalized,adjusted,blurred,rescaled]animation=mpy.concatenate_videoclips(clips)animation.write_gif("sinc_cat.gif",fps=15)
Finally, MoviePy will be particularly practical when dealing with video data, as it is its first job. For our last example we estimate the size of a growing bacterial population by thresholding the video frames and counting the white pixels. The third panel shows that the population size grows exponentially in time.
I hope to have given you enough recipes to impress your colleagues at your next presentation. Any other library could be animated with MoviePy, as long as its output can be converted to a Numpy array.
Some libraries have their own animation modules, but these are usually a pain to fix and maintain. Thanks to the many users who have tested it in very different contexts, MoviePy seems to have become stable (or people stopped reporting bugs), and can be adapted to many situations. There is still a lot to do, but it would be nice if authors started relying on it for video and GIF rendering, like Pandas and Scikit-Learn rely on Matplotlib for plotting.
For completeness, and because it may better fit your needs, I must mention ImageIO, another Python library with video writing capabilities which focuses on providing a very simple interface to read or write any kind of image, video or volumetric data. For instance you use imwrite() to write any image, mimwrite() for any video/GIF, volwrite() for volumetric data, or simply write() for streamed data.