用Python和MoviePy将数据动态可视化,pythonmoviepy,未经许可,禁止转载!英文


本文由 编橙之家 - yottatiana 翻译,黄利民 校稿。未经许可,禁止转载!
英文出处:zulko.github.io。欢迎加入翻译组。

Python有些出色的数据可视化库,但很少能渲染GIF或视频动画。本文介绍如何运用MoviePy作为其他库的通用动画插件。

有了 MoviePy ,你可以用一个函数 make_frame(t) 自定义动画,并返回相应的时间t的视频帧(秒):

Python
from moviepy.editor import VideoClip

def make_frame(t):
    """ returns an image of the frame at time t """
    # ... create the frame with any library
    return frame_for_time_t # (Height x Width x 3) Numpy array

animation = VideoClip(make_frame, duration=3) # 3-second clip

# For the export, many options/formats/optimizations are supported
animation.write_videofile("my_animation.mp4", fps=24) # export as video
animation.write_gif("my_animation.gif", fps=24) # export as GIF (slow)

在之前的文章中,我用这种方法来做制作矢量图形动画(用Gizeh库),和光线追踪三维场景(由POV-Ray做出)。这篇文章包括 MayaVi、vispy、matplotlib、NumPy 和 Scikit-image 这些科学库。

用Mayavi的动画

Mayavi是一个针对有简单接口的交互3D数据可视化的Python模块。在第一个例子中,我们做一个高度随时间t变化的表面的动画:

Python
import numpy as np
import mayavi.mlab as mlab
import  moviepy.editor as mpy

duration= 2 # duration of the animation in seconds (it will loop)

# MAKE A FIGURE WITH MAYAVI

fig_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 = lambda d: np.sinc(XX**2+YY**2)+np.sin(XX+d)

# ANIMATE THE FIGURE WITH MOVIEPY, WRITE AN ANIMATED GIF

def make_frame(t):
    mlab.clf() # clear the figure (to reset the colors)
    mlab.mesh(YY,XX,ZZ(2*np.pi*t/duration), figure=fig_myv)
    return mlab.screenshot(antialiased=True)

animation = mpy.VideoClip(make_frame, duration=duration)
animation.write_gif("sinc.gif", fps=20)

另一个例子是一个坐标和观看角度都随时间变化的线框网:

Python
import numpy as np
import mayavi.mlab as mlab
import  moviepy.editor as mpy

duration = 2 # duration of the animation in seconds (it will loop)

# MAKE A FIGURE WITH MAYAVI

fig = 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) # Points
l = 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 GIF

def make_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 mesh
    mlab.view(azimuth= 360*t/duration, distance=9) # camera angle
    return mlab.screenshot(antialiased=True) # return a RGB image

animation = mpy.VideoClip(make_frame, duration=duration).resize(0.5)
# Video generation takes 10 seconds, GIF generation takes 25s
animation.write_videofile("wireframe.mp4", fps=20)
animation.write_gif("wireframe.gif", fps=20)

因为Mayavi有着强大的ITK可视化引擎,它还可以处理复杂数据集。这里有一个源自于Mayavi例子的动画

代码

Animations with Vispy 用Vispy的动画

Vispy是另一个以OpenGL为基础的交互3D可视化库。至于Mayavi,我们用MoviePy先做出一个图和一个网。

Python
from moviepy.editor import VideoClip
import numpy as np
from vispy import app, scene
from vispy.gloo.util import _screenshot

canvas = 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 = lambda t : 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 MOVIEPY

def make_frame(t):
    surface.set_data(z = Z(t)) # Update the mathematical surface
    canvas.on_draw(None) # Update the image on Vispy's canvas
    return _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')

还有一些更高级的例子(源自于Vispy库),它是用C代码片段中嵌入Python代码,微调了3D渲染的:

代码

代码

用Matplotlib的动画

2D/3D绘图库Matplotlib已经有了动画模块,但我发现moviepy可以做出更轻量级,质量更好的视频,却达到了两倍的速度(不知道为什么?在这里看到更多细节)。这里有个如何使用matplotlib和moviepy的例子:

Python
import matplotlib.pyplot as plt
import numpy as np
from moviepy.video.io.bindings import mplfig_to_npimage
import moviepy.editor as mpy

# DRAW A FIGURE WITH MATPLOTLIB

duration = 2

fig_mpl, ax = plt.subplots(1,figsize=(5,3), facecolor='white')
xx = np.linspace(-2,2,200) # the x vector
zz = lambda d: np.sinc(xx**2)+np.sin(xx+d) # the (changing) z vector
ax.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.

def make_frame_mpl(t):
    line.set_ydata( zz(2*np.pi*t/duration))  # <= Update the curve
    return mplfig_to_npimage(fig_mpl) # RGB image of the figure

animation =mpy.VideoClip(make_frame_mpl, duration=duration)
animation.write_gif("sinc_mpl.gif", fps=20)

Matplotlib有很多漂亮的主题,而且和像Pandas或Scikit-Learn这些数字模块能很好配合。让我们看一个SVM分类器,以更好地明白训练点的数量增加时的地图。

Python
import numpy as np
import matplotlib.pyplot as plt
from sklearn import svm # sklearn = scikit-learn
from sklearn.datasets import make_moons
from moviepy.editor import VideoClip
from moviepy.video.io.bindings import mplfig_to_npimage

X, Y = make_moons(50, noise=0.1, random_state=2) # semi-random data

fig, 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))

def make_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 other
    weights = 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)

    return mplfig_to_npimage(fig)

animation = VideoClip(make_frame, duration = 7)
animation.write_gif("svm.gif", fps=15)

简单明了,背景色代表分类器辨识黑点和白点属于哪里。在一开始没有真正的线索,但随着更多的点出钱,月牙形分布区域渐渐显现了。

用Numpy的动画

如果你正用着Numpy数组(Numpy是一个Python的中央数字库),你不需要任何外部绘图库,你可以直接将这些数组馈入MoviePy。

这通过模拟在法国的僵尸爆发(灵感来自Max Berggren的博客,编橙之家已有全文翻译:《用Python在地图上模拟疫情扩散》)。法国被仿作一个所有分散和感染的计算都完成的网格(NumPy数组)。每隔一段时间,一些NumPy操作变换网格为有效的RGB图像,并将其发送到Moviepy。

代码

把动画放到一起

什么比一个动画更好呢?两个动画!你可以利用MoviePy视频合成功能,来混合来自不同库的动画:

Python
import moviepy.editor as mpy
# 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:

Python
# Make the white color transparent in clip_mayavi
clip_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)

也许有点太快了,但有时你必须给你的观众一些可以让他们议论的东西。

你也可以注解动画,当比较不同的过滤器或算法时,注解动画很实用。让我们看看四张由库scikit-image变换的图像:

Python
import moviepy.editor as mpy
import skimage.exposure as ske # rescaling, histogram eq.
import skimage.filter as skf # gaussian blur

clip = mpy.VideoFileClip("sinc.gif")
gray = clip.fx(mpy.vfx.blackwhite).to_mask()

def apply_effect(effect, title, **kw):
    """ Returns a clip with the effect applied and a title"""
    filtr = lambda im: 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))
    return mpy.CompositeVideoClip([new_clip,txt])

# Apply 4 different effects to the original animation
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)

# 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)


如果我们把CompositeVideoClip和clips_array替代成concatenate_videoclips,我们得到标题效果式动画:

Python
import moviepy.editor as mpy
import skimage.exposure as ske
import skimage.filter as skf

clip = mpy.VideoFileClip("sinc.gif")
gray = clip.fx(mpy.vfx.blackwhite).to_mask()

def apply_effect(effect, label, **kw):
    """ Returns a clip with the effect applied and a top label"""
    filtr = lambda im: 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))
    return mpy.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)

最终,处理视频数据时,MoviePy会非常好用,因为这是它的首要任务。最后一个例子,我们通过视频帧阈值和白色像素计数,估计一个细菌种群大小。第三面板显示,人口规模呈指数增长的时间。

代码

一个库就可以做所有动画了?

我希望给了你足够的例子,来提升你的同事对你下次展示中的印象。总之要他的输出能被转换成Numpy数组,其他的库也能用MoviePy来做动画。

一些库有自己的动画模块,但是修复和维护很痛苦!感谢那些在不同背景下测试MoviePy的人们,它变得更稳定了(除非没人再报bug),而且可以适用于各种环境。MoviePy  仍有很多要去做的,但如果作者开始依靠它来做视频和GIF渲染,像Pandas和scikit-Learn依赖matplotlib做绘制,那会很好。

为了本文的完整性,同时也为了更好地满足您的需要,我必须提到的ImageIO,这是另一个可以编写视频的Python库,它专注于提供一个非常简单的接口来读取或写入任何图像,视频或容积数据。比如你用imwrite()写图像,用mimwrite()写视频/ GIF,用volwrite()写体积数据,或只是write()写流数据。

谢谢大家,GIF万岁!

相关内容

    暂无相关文章

评论关闭