Python 中有很多不錯的數據可視化庫,但是極少能渲染 GIF 圖或視頻動畫效果。本文就分享一下如何用 MoviePy 作為其他可視化庫的通用插件,製作動畫可視化效果,畢竟這年頭,沒圖不行,有動圖更好。 MoviePy 能讓我們用函數 make_frame(t) 自定義動畫,函數會返回和時間 t ...
Python 中有很多不錯的數據可視化庫,但是極少能渲染 GIF 圖或視頻動畫效果。本文就分享一下如何用 MoviePy 作為其他可視化庫的通用插件,製作動畫可視化效果,畢竟這年頭,沒圖不行,有動圖更好。
MoviePy 能讓我們用函數 make_frame(t) 自定義動畫,函數會返回和時間 t 的視頻幀(以秒為單位):
from moviepy.editor import VideoClip
def make_frame(t):
""" returns an image of the frame at time t """
# ... 用任意庫創建幀
return frame_for_time_t # (Height x Width x 3) Numpy array
animation = VideoClip(make_frame, duration=3) # 3-second clip
# 支持導出為多種格式
animation.write_videofile("my_animation.mp4", fps=24) # 導出為視頻
animation.write_gif("my_animation.gif", fps=24) # 導出為GIF
本文會涵蓋 MayaVi、vispy、matplotlib、NumPy 和 Scikit-image 這些庫。
基於 Mayavi 製作動畫
Mayavi 是一個 Python 模塊,可以製作互動式 3D 數據可視化。在第一個例子中,我們會將一個高度隨著時間 t 不斷變化的錶面製作成動畫:
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)
# 用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)
# 用MoviePy將圖形轉換為動畫,編寫動畫GIF
def make_frame(t):
mlab.clf() # 清掉圖形(重設顏色)
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)
另外一個例子是,製作一個坐標和觀看角度都隨著時間不斷變化的線框網動畫:
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)
# 用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) # 點
l = mlab.plot3d(xx,yy,zz, representation="wireframe", tube_sides=5,
line_width=.5, tube_radius=0.2, figure=fig)
# 用MoviePy將圖形轉換為動畫,編寫動畫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) # 相機視角
return mlab.screenshot(antialiased=True) # 返回RGB圖形
animation = mpy.VideoClip(make_frame, duration=duration).resize(0.5)
# 視頻生成花費10秒, GIF 生成花費25秒
animation.write_videofile("wireframe.mp4", fps=20)
animation.write_gif("wireframe.gif", fps=20)
基於 Vispy 製作動畫
Vispy 是另一款基於 OpenGL 的互動式 3D 數據可視化庫。我們可以先用 Vispy 做出圖形和網格,然後用 MoviePy 將其製作成動畫:
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()
# 用MoviePy轉換為動畫
def make_frame(t):
surface.set_data(z = Z(t)) # 更新曲面
canvas.on_draw(None) # 更新Vispy的畫布上的 圖形
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 著色器後製作而成:
製作該動畫的代碼地址: https:// gist.github.com/Zulko/5 4e5468759396c5cbbd2
製作該動畫的代碼地址: https:// gist.github.com/Zulko/4 dcaf3e38fdc118f22a3
基於 matplotlib 製作動畫
雖然 2D/3D 繪圖庫 matplotlib 內置了動畫模塊,但是用 MoviePy 製作更輕更高質量的視頻動畫,而且運行速度更快。下麵是用 MoviePy 基於 matplotlib 製作動畫的方法:
import matplotlib.pyplot as plt
import numpy as np
from moviepy.video.io.bindings import mplfig_to_npimage
import moviepy.editor as mpy
# 用matplotlib繪製一個圖形
duration = 2
fig_mpl, ax = plt.subplots(1,figsize=(5,3), facecolor='white')
xx = np.linspace(-2,2,200) # x向量
zz = lambda d: np.sinc(xx**2)+np.sin(xx+d) # (變化的)Z向量
ax.set_title("Elevation in y=0")
ax.set_ylim(-1.5,2.5)
line, = ax.plot(xx, zz(0), lw=3)
# 用MoviePy製作動(為每個t更新曲面)。製作一個GIF
def make_frame_mpl(t):
line.set_ydata( zz(2*np.pi*t/duration)) # 更新曲面
return mplfig_to_npimage(fig_mpl) # 圖形的RGB圖像
animation =mpy.VideoClip(make_frame_mpl, duration=duration)
animation.write_gif("sinc_mpl.gif", fps=20)
Matplotlib 有很多漂亮的主題,和 Pandas、Scikit-Learn 等數字模塊的相容性也很好。我們來看一個 SVM 分類器,更好的理解隨著訓練點的數量增加時地圖的變化動態:
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) # 半隨機數據
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)
# 不斷變化的權重讓數據點一個接一個的出現
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)
零基礎入門視頻。項目實戰視頻!大牛答疑群:125240963
簡單來說,通過背景顏色我們就可以得知分類器辨識黑色點和白色點屬於哪裡。剛開始並不明顯,但隨著越來越多的數據點出現,這些點的分佈逐漸呈月牙形區域。
基於 Numpy 的動畫
如果是用 Numpy 數組(Numpy 是 Python 中的一個數字型檔),你不需要任何外部繪圖庫,你可以直接將數組輸入 MoviePy 里。
將 Numpy 和 MoviePy 結合,可以做出很炫酷的動畫效果。比如我們可以模擬僵屍病毒在法國蔓延的動態圖(模擬!模擬!),以網格形式(Numpy 數組)模擬出法國地圖,在上面執行所有模擬病毒感染和擴散效果的計算。每隔一段時間,一些 Numpy 操作會將網格轉換為有效的 RGB 圖像,並將其發送至 MoviePy:
import urllib
import numpy as np
from scipy.ndimage.filters import convolve
import moviepy.editor as mpy
#### 從網路上檢索地圖
filename = ("http://upload.wikimedia.org/wikipedia/commons/a/aa/"
"France_-_2011_population_density_-_200_m_%C3%"
"97_200_m_square_grid_-_Dark.png")
urllib.urlretrieve(filename, "france_density.png")
#### 參數和約束條件
infection_rate = 0.3
incubation_rate = 0.1
dispersion_rates = [0, 0.07, 0.03] # for S, I, R
# 該內核會模擬人類/僵屍如何用一個位置擴散至鄰近位置
dispersion_kernel = np.array([[0.5, 1 , 0.5],
[1 , -6, 1],
[0.5, 1, 0.5]])
france = mpy.ImageClip("france_density.png").resize(width=400)
SIR = np.zeros( (3,france.h, france.w), dtype=float)
SIR[0] = france.get_frame(0).mean(axis=2)/255
start = int(0.6*france.h), int(0.737*france.w)
SIR[1,start[0], start[1]] = 0.8 # infection in Grenoble at t=0
dt = 1.0 # 一次更新=實時1個小時
hours_per_second= 7*24 # one second in the video = one week in the model
world = {'SIR':SIR, 't':0}
##### 建模
def infection(SIR, infection_rate, incubation_rate):
""" Computes the evolution of #Sane, #Infected, #Rampaging"""
S,I,R = SIR
newly_infected = infection_rate*R*S
newly_rampaging = incubation_rate*I
dS = - newly_infected
dI = newly_infected - newly_rampaging
dR = newly_rampaging
return np.array([dS, dI, dR])
def dispersion(SIR, dispersion_kernel, dispersion_rates):
""" Computes the dispersion (spread) of people """
return np.array( [convolve(e, dispersion_kernel, cval=0)*r
for (e,r) in zip(SIR, dispersion_rates)])
def update(world):
""" spread the epidemic for one time step """
infect = infection(world['SIR'], infection_rate, incubation_rate)
disperse = dispersion(world['SIR'], dispersion_kernel, dispersion_rates)
world['SIR'] += dt*( infect + disperse)
world['t'] += dt
# 用MoviePy製作動畫
def world_to_npimage(world):
""" Converts the world's map into a RGB image for the final video."""
coefs = np.array([2,25,25]).reshape((3,1,1))
accentuated_world = 255*coefs*world['SIR']
image = accentuated_world[::-1].swapaxes(0,2).swapaxes(0,1)
return np.minimum(255, image)
def make_frame(t):
""" Return the frame for time t """
while world['t'] < hours_per_second*t:
update(world)
return world_to_npimage(world)
animation = mpy.VideoClip(make_frame, duration=25)
# 可以將結果寫為視頻或GIF(速度較慢)
#animation.write_gif(make_frame, fps=15)
animation.write_videofile('test.mp4', fps=20)
最終效果如下:
將動畫組合到一起
如果一個動畫不夠好看,那就來兩個!我們可以藉助 MoviePy 的視頻組合功能將來自不同庫的動畫組合在一起:
import moviepy.editor as mpy
# 我們使用之前生成的GIF圖以避免重新計算動畫
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)
或者更有藝術氣息一點:
# 在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 庫中的四張變換圖像:
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, 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])
# 為原始動畫應用4種不同的效果
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)
# 將片段一起放在2 X 2的網格上,寫入一個文件
finalclip = mpy.clips_array([[ equalized, adjusted ],
[ blurred, rescaled ]])
final_clip.write_gif("test2x2.gif", fps=20)
如果我們用 concatenate_videoclips 代替 CompositeVideoClip 和 clips_array,會得到標題效果式的動畫:
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 相對穩定的多,也可以適用於很多情況。
另外,另一個 Python 庫 ImageIO 也能編寫視頻,可以提供一個很簡單的介面來讀取或寫入任何種類的圖像、視頻和容積數據。比如你可以用 imwrite() 寫圖像,用 mimwrite() 寫視頻/ GIF,用 volwrite() 寫體積數據,或只是用 write() 寫流式數據。
快去動手操作吧,GIF 萬歲!