Ra mắt hai series mới cực hot Trí tuệ nhân tạo A đến ZPython công cụ không thể thiếu khi nghiên cứu Data science, Machine learning.

Vẽ đồ thị ảnh động trong Python với thư viện Animation trong Matplotlib

A picture is worth a thousand words. Một bức tranh hơn nghìn lời nói.

Ngạn ngữ Anh.

Thật vậy, mỗi khi chúng ta thực hiện một thuật toán, nếu các bước trong thuật toán có thể tạo thành một hình ảnh, đặc biệt là hình ảnh động nó giúp chúng ta hiểu thuật toán đó nhanh chóng. Bài viết này hướng dẫn các bạn thực hiện vẽ các đồ thị, biểu đồ dạng hình ảnh động có thể lưu thành file ảnh động gif hoặc dưới dạng video mp4 khi sử dụng thư viện Matplotlib trong Python. Ví dụ ảnh động dưới đây là đồ thị động của một sóng hình sin có phương trình dạng $y=\sin{2\pi (x-c)}$

Đồ thị động sóng hình sin

1. Các bước vẽ đồ thị động trong Python

Bài viết có nhiều kiến thức cơ bản liên quan đến thư viện Matplotlib, nếu bạn chưa biết nhiều về thư viện này, tham khảo bài viết Vẽ đồ thị, biểu đồ với thư viện Matplotlib.

Ảnh động, video hay hoạt hình bản chất là việc hiển thị các ảnh tĩnh, mỗi ảnh có nội dung khác nhau được hiển thị trong một khoảng thời gian nhất định. Nguyên lý này cũng được áp dụng khi vẽ đồ thị động trong Matplotlib, nó vẽ ra màn hình đồ thị tại các thời điểm, lưu tạm thành các frame và cuối cùng là lưu thành chuỗi các frame dưới dạng ảnh động GIF hoặc video MP4. Tiếp theo chúng ta sẽ tìm hiểu các bước thực hiện:

Bước 1: Import những thư viện cần thiết:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

%matplotlib inline

Trong phần này chúng ta import hai thư viện rất phổ biến của Python là Numpy và Matplotlib, một thư viện chuyên xử lý về mảng và một thư viện chuyên vẽ đồ thị biểu đồ từ dữ liệu.

Câu lệnh %matplotlib inline để hiển thị các đồ thị ngay trong cell của Jupyter Notebook. Tham khảo Hướng dẫn sử dụng Jupyter Notebook công cụ không thể thiếu khi học Python.

Bước 2: Thiết lập trục tọa độ của đồ thị

fig = plt.figure()
ax = plt.axes(xlim=(0, 4), ylim=(-2, 2))
line, = ax.plot([], [], lw=3)

Như đã tìm hiểu trong bài Vẽ đồ thị cơ bản với Matplotlib, các đối tượng trong Matplotlib được phân cấp theo hình cây. Với câu lệnh đầu tiên, chúng ta có biến fig chứa đối tượng Figure là một khung chứa, ảnh (container) chứa đựng toàn bộ các nội dung đồ thị, biểu đồ trong đó.

Tiếp đó, biến ax chứa nhiều các đối tượng Axes, đối tượng chứa trục tọa độ và đồ thị thị, ở đây chúng ta sẽ tạo ra một trục tọa độ có trục x trong dải [0,4] trục y trong dải [-2,2] và line là một đối tượng đồ thị là một đường nối các chuỗi điểm liên tiếp.

Bước 3: Tạo function vẽ đồ thị cho từng tập dữ liệu được gọi đến khi tạo frame.

def animate(i):
    x = np.linspace(0, 4, 1000)
    y = np.sin(2 * np.pi * (x - 0.01 * i))
    line.set_data(x, y)
    return line,

Function này sẽ được gọi đến khi tạo ra các frame trong ảnh động hoặc video. Cấu trúc function sẽ được mô tả trong bước 4. Chúng ta xem nội dung function này, chúng ta sẽ vẽ lên đồ thị hàm số $y=\sin{2\pi (x-c)}$ với dữ liệu $x$ được tạo ra bởi thư viện Numpy là một mảng gồm 1000 phần tử cách đều nhau trong dải [0,4].

Bước 4: Gọi FuncAnimation, save ra file hoặc hiển thị

anim = FuncAnimation(fig, animate, frames=200, interval=20, blit=True)

anim.save('sin_wave.gif', writer='imagemagick')
plt.show()

Hàm FuncAnimation tạo ra các hình ảnh động bằng cách gọi lặp đi lặp lại các hàm vẽ hoạt cảnh ở bước 3. Hàm này có một số các tham số như sau:

  • fig là đối tượng matplotlib.figure.Figure nơi toàn bộ đồ thị được chứa trong khung chứa này.
  • animate là tên function sẽ được gọi đến và thực hiện tại mỗi frame, cú pháp function này là func(frame, *fargs) trong đó tham số đầu chứa biến là chỉ số của các frame và các tham số tiếp theo là tham số tùy chọn. Tham số thứ nhất là bắt buộc nếu blit=True.
  • frames: số frame.
  • interval: Độ trễ giữa các frame tính bằng ms, mặc định là 200ms.
  • blit: Tham số chỉ dẫn việc vẽ lại đồ thị chỉ xảy ra khi có thay đổi, với blit=True nó sẽ giúp việc tạo ra các đồ thị động nhanh hơn.

anim.save('sine_wave.gif', writer='imagemagick'), câu lệnh này lưu toàn bộ các frame thành một ảnh động dạng GIF. Chú ý, bạn cần cài đặt ImageMagick nếu không sẽ có một cảnh báo về việc không tìm thấy imagemagick và chuyển sang một thư viện khác.

2. Hiển thị ảnh động hoặc video trong Jupyter Notebook

Mặc định Jupyter Notebook không hiển thị được video hoặc ảnh động, do vậy muốn hiển thị được luôn trên Jupyter Notebook chúng ta cần sử dụng một số cách sau:

Cách 1: Chuyển đổi ảnh động thành video và gọi chức năng hiển thị video trong HTML5 để hiển thị.

import numpy as np
from matplotlib import pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML

fig = plt.figure()
ax = plt.axes(xlim=(0, 4), ylim=(-2, 2))
line, = ax.plot([], [], lw=3)

def animate(i):
    x = np.linspace(0, 4, 1000)
    y = np.sin(2 * np.pi * (x - 0.01 * i))
    line.set_data(x, y)
    return line,

anim = FuncAnimation(fig, animate, frames=200, interval=20, blit=True)
HTML(anim.to_html5_video())

Cách 2: Dùng cú pháp Markdown để hiển thị ảnh động GIF.

import numpy as np
from matplotlib import pyplot as plt
from matplotlib.animation import FuncAnimation

fig = plt.figure()
ax = plt.axes(xlim=(0, 4), ylim=(-2, 2))
line, = ax.plot([], [], lw=3)

def animate(i):
    x = np.linspace(0, 4, 1000)
    y = np.sin(2 * np.pi * (x - 0.01 * i))
    line.set_data(x, y)
    return line,

anim = FuncAnimation(fig, animate, frames=200, interval=20, blit=True)
anim.save('sin_wave.gif', writer='imagemagick')

Chúng ta đã có một ảnh sin_wave.gif được tạo ra cùng thư mục với thư mục chứa file Jupyter notebook hiện tại, do đó chỉ cần tạo một cell mới trong Jupyter Notebook, chuyển về dạng Markdown và thực hiện code hiển thị một ảnh như sau:

![](sin_wave.gif)

3. Một số ví dụ về tạo ảnh động cho đồ thị trong Matplotlib

Bạn có thể tham khảo rất nhiều ví dụ thực hiện với Animation trong Matplotlib, các ví dụ này sẽ giúp bạn thành thạo hơn về Matplotlib và đặc biệt là thực hiện các ảnh động giúp minh họa cho các thuật toán một cách hiệu quả hơn.

3.1 Ví dụ 1:

Vẽ một tập hợp điểm ngẫu nhiên trong một dải giữa hai đường song song và vẽ một đường thẳng động chạy giữa hai đường này.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

fig = plt.figure()
ax = plt.axes(xlim=(0, 20), ylim=(-5, 25))

x = np.arange(0, 20, 0.1)
ax.scatter(x, x + np.random.normal(0, 3.0, len(x)))
line, = ax.plot(x, x - 5, 'r-', linewidth=2)

def animate(i):
    label = 'timestep {0}'.format(i)
    line.set_ydata(x - 5 + i)
    ax.set_xlabel(label)
    return line, ax
anim = FuncAnimation(fig, animate, frames=10, interval=50)
anim.save('line.gif', writer='imagemagick')

Kết quả như ảnh động sau:

Ví dụ 1

3.2 Ví dụ 2: Vẽ đường xoắn ốc

Thực hiện vẽ một đường xoắn ốc, trong toán học đường xoắn ốc có phương trình hệ tọa độ cực là $r=ae^{\theta \cot{b}}$. Để thực hiện vẽ đường xoắn này, chúng ta thực hiện chuyển đổi về dạng tham số như sau $$x(t)=ae^{bt}\cos{t}$$ $$y(t)=ae^{bt}\sin{t}$$

Code Python như sau:

import matplotlib.pyplot as plt 
import matplotlib.animation as animation 
import numpy as np
import math

fig = plt.figure() 
ax = plt.axes(xlim=(-200, 200), ylim=(-200, 200)) 
line, = ax.plot([], [], lw=2) 

xdata, ydata = [], [] 

def animate(i): 
    t = 0.06*i 
    a = 1.27
    b = 0.35
    x = a*math.exp(b*t)*np.sin(t) 
    y = a*math.exp(b*t)*np.cos(t) 
    xdata.append(x) 
    ydata.append(y) 
    line.set_data(xdata, ydata) 
    return line,

anim = animation.FuncAnimation(fig, animate, frames=300, interval=20, blit=True) 

anim.save('vi-du-2.gif', writer='imagemagick')

Kết quả như sau:

Ví dụ 2 về đường xoắn ốc

3.3 Ví dụ mô phỏng con lắc đôi

Đây là một ví dụ tương đối phức tạp về tính toán quỹ đạo con lắc đôi, là một ví dụ hay về mô phỏng nếu bạn có thể tìm hiểu kỹ, nó sẽ giúp bạn có thêm nhiều kiến thức lập trình Python.

from numpy import sin, cos
import numpy as np
import matplotlib.pyplot as plt
import scipy.integrate as integrate
import matplotlib.animation as animation

class DoublePendulum:
    """Double Pendulum Class

    init_state is [theta1, omega1, theta2, omega2] in degrees,
    where theta1, omega1 is the angular position and velocity of the first
    pendulum arm, and theta2, omega2 is that of the second pendulum arm
    """
    def __init__(self,
                 init_state = [120, 0, -20, 0],
                 L1=1.0,  # length of pendulum 1 in m
                 L2=1.0,  # length of pendulum 2 in m
                 M1=1.0,  # mass of pendulum 1 in kg
                 M2=1.0,  # mass of pendulum 2 in kg
                 G=9.8,  # acceleration due to gravity, in m/s^2
                 origin=(0, 0)): 
        self.init_state = np.asarray(init_state, dtype='float')
        self.params = (L1, L2, M1, M2, G)
        self.origin = origin
        self.time_elapsed = 0

        self.state = self.init_state * np.pi / 180.

    def position(self):
        """compute the current x,y positions of the pendulum arms"""
        (L1, L2, M1, M2, G) = self.params

        x = np.cumsum([self.origin[0],
                       L1 * sin(self.state[0]),
                       L2 * sin(self.state[2])])
        y = np.cumsum([self.origin[1],
                       -L1 * cos(self.state[0]),
                       -L2 * cos(self.state[2])])
        return (x, y)

    def energy(self):
        """compute the energy of the current state"""
        (L1, L2, M1, M2, G) = self.params

        x = np.cumsum([L1 * sin(self.state[0]),
                       L2 * sin(self.state[2])])
        y = np.cumsum([-L1 * cos(self.state[0]),
                       -L2 * cos(self.state[2])])
        vx = np.cumsum([L1 * self.state[1] * cos(self.state[0]),
                        L2 * self.state[3] * cos(self.state[2])])
        vy = np.cumsum([L1 * self.state[1] * sin(self.state[0]),
                        L2 * self.state[3] * sin(self.state[2])])

        U = G * (M1 * y[0] + M2 * y[1])
        K = 0.5 * (M1 * np.dot(vx, vx) + M2 * np.dot(vy, vy))

        return U + K

    def dstate_dt(self, state, t):
        """compute the derivative of the given state"""
        (M1, M2, L1, L2, G) = self.params

        dydx = np.zeros_like(state)
        dydx[0] = state[1]
        dydx[2] = state[3]

        cos_delta = cos(state[2] - state[0])
        sin_delta = sin(state[2] - state[0])

        den1 = (M1 + M2) * L1 - M2 * L1 * cos_delta * cos_delta
        dydx[1] = (M2 * L1 * state[1] * state[1] * sin_delta * cos_delta
                   + M2 * G * sin(state[2]) * cos_delta
                   + M2 * L2 * state[3] * state[3] * sin_delta
                   - (M1 + M2) * G * sin(state[0])) / den1

        den2 = (L2 / L1) * den1
        dydx[3] = (-M2 * L2 * state[3] * state[3] * sin_delta * cos_delta
                   + (M1 + M2) * G * sin(state[0]) * cos_delta
                   - (M1 + M2) * L1 * state[1] * state[1] * sin_delta
                   - (M1 + M2) * G * sin(state[2])) / den2

        return dydx

    def step(self, dt):
        """execute one time step of length dt and update state"""
        self.state = integrate.odeint(self.dstate_dt, self.state, [0, dt])[1]
        self.time_elapsed += dt

#------------------------------------------------------------
# set up initial state and global variables
pendulum = DoublePendulum([180., 0.0, -20., 0.0])
dt = 1./30 # 30 fps

#------------------------------------------------------------
# set up figure and animation
fig = plt.figure()
ax = fig.add_subplot(111, aspect='equal', autoscale_on=False,
                     xlim=(-2, 2), ylim=(-2, 2))
ax.grid()

line, = ax.plot([], [], 'o-', lw=2)
time_text = ax.text(0.02, 0.95, '', transform=ax.transAxes)
energy_text = ax.text(0.02, 0.90, '', transform=ax.transAxes)

def init():
    """initialize animation"""
    line.set_data([], [])
    time_text.set_text('')
    energy_text.set_text('')
    return line, time_text, energy_text

def animate(i):
    """perform animation step"""
    global pendulum, dt
    pendulum.step(dt)

    line.set_data(*pendulum.position())
    time_text.set_text('time = %.1f' % pendulum.time_elapsed)
    energy_text.set_text('energy = %.3f J' % pendulum.energy())
    return line, time_text, energy_text

# choose the interval based on dt and the time to animate one step
from time import time
t0 = time()
animate(0)
t1 = time()
interval = 1000 * dt - (t1 - t0)

ani = animation.FuncAnimation(fig, animate, frames=300,
                              interval=interval, blit=True, init_func=init)

ani.save('vi-du-3.gif', writer='imagemagick')

Kết quả:

Ví dụ về con lắc đôi


CÁC BÀI VIẾT KHÁC

FirebirD

Đam mê Toán học, Lập trình. Sở thích chia sẻ kiến thức, Phim hài, Bóng đá, Cà phê sáng với bạn bè.

Vẽ đồ thị trong Python với thư viện Matplotlib

Thuật toán Gradient Descent

1 Bình luận trong "Vẽ đồ thị ảnh động trong Python với thư viện Animation trong Matplotlib"

  1. Matlib Pro

    2 months ago

    Phản hồi

    Amazing post...

Thêm bình luận