Как легко создавать гифки для анимации ваших визуализаций

Есть много способов создавать анимацию в Matplotlib. У них даже есть Анимационный класс с функциями и методами для поддержки этой задачи.

Но я часто нахожу эти методы слишком сложными, и много раз мне хочется сделать что-то без особой сложности.

В этой статье я рассмотрю основы создания диаграмм, их сохранения в виде изображений и использования Imageio для создания GIF.

Без лишних слов, давайте импортируем наши библиотеки и начнем создавать наш GIF.

import os
import numpy as np
import matplotlib.pyplot as plt
import imageio

Графики

Мы можем использовать линейную диаграмму, чтобы начать с чего-то простого, поскольку для этого требуется только список со значениями y.

y = np.random.randint(30, 40, size=(40))
plt.plot(y)
plt.ylim(20,50)

Круто, мы использовали NumPy для создания списка случайных целых чисел в диапазоне от 30 до 40, содержащего 40 чисел.

Идея состоит в том, чтобы отображать значения на нашей линейной диаграмме одно за другим. Мы будем использовать цикл, разрезая массив и увеличивая количество выбранных значений в каждом цикле.

Давайте сначала попробуем без цикла, просто построим, сохраним и покажем изображение.

## ONE ##
plt.plot(y[:-3])
plt.ylim(20,50)
plt.savefig('1.png')
plt.show()
## TWO ##
plt.plot(y[:-2])
plt.ylim(20,50)
plt.savefig('2.png')
plt.show()
## THREE ##
plt.plot(y[:-1])
plt.ylim(20,50)
plt.savefig('3.png')
plt.show()
## FOUR ##
plt.plot(y)
plt.ylim(20,50)
plt.savefig('4.png')
plt.show()

Отлично, теперь у нас есть четыре кадра, сохраненных в нашем рабочем каталоге. Imageio может помочь нам создать наш первый GIF.

# Build GIF
with imageio.get_writer('mygif.gif', mode='I') as writer:
    for filename in ['1.png', '2.png', '3.png', '4.png']:
        image = imageio.imread(filename)
        writer.append_data(image)

Выглядит странно, но вот наша гифка.

А теперь попробуем с петлей. Мы также можем систематически создавать имена файлов и добавлять паузу в конце, чтобы наша анимация некоторое время отображала полную диаграмму перед повторением.

filenames = []
for i in y:
    # plot the line chart
    plt.plot(y[:i])
    plt.ylim(20,50)
    
    # create file name and append it to a list
    filename = f'{i}.png'
    filenames.append(filename)
    
    # save frame
    plt.savefig(filename)
    plt.close()
# build gif
with imageio.get_writer('mygif.gif', mode='I') as writer:
    for filename in filenames:
        image = imageio.imread(filename)
        writer.append_data(image)
        
# Remove files
for filename in set(filenames):
    os.remove(filename)

Большой! Теперь, когда мы знаем самые основы, давайте попробуем это с помощью гистограммы.

Гистограммы

Мы не можем просто добавлять по одному значению для гистограмм, иначе наш GIF-файл будет длиться вечно.

Здесь нам нужно будет заменить все значения для y, чтобы все полосы двигались одновременно.

Давайте применим тот же код, который мы использовали раньше, с этими небольшими изменениями. Мы создадим значения для оси X, список списков для оси Y и будем использовать гистограмму вместо линейной диаграммы.

x = [1, 2, 3, 4, 5]
coordinates_lists = [[0, 0, 0, 0, 0],
                     [10, 30, 60, 30, 10],
                     [70, 40, 20, 40, 70],
                     [10, 20, 30, 40, 50],
                     [50, 40, 30, 20, 10],
                     [75, 0, 75, 0, 75],
                     [0, 0, 0, 0, 0]]
filenames = []
for index, y in enumerate(coordinates_lists):
    # plot charts
    plt.bar(x, y)
    plt.ylim(0,80)
    
    # create file name and append it to a list
    filename = f'{index}.png'
    filenames.append(filename)
    
    # repeat last frame
    if (index == len(coordinates_lists)-1):
        for i in range(15):
            filenames.append(filename)
            
    # save frame
    plt.savefig(filename)
    plt.close()
# build gif
with imageio.get_writer('mygif.gif', mode='I') as writer:
    for filename in filenames:
        image = imageio.imread(filename)
        writer.append_data(image)
        
# Remove files
for filename in set(filenames):
    os.remove(filename)

Ладно, не ладилось.

Пауза в конце GIF не очень помогает. Нам нужно делать паузу для каждого сюжета, а затем переходить к следующему.

Кроме того, столбцы просто перескакивают на свое следующее положение. Мы могли бы замедлить это движение, чтобы сделать переход более плавным.

Есть множество способов сделать движение более плавным. Самый простой - разделить расстояние между текущим и следующим местоположением на количество кадров, которые будет у нашего перехода.

# frames between transitions
n_frames = 10
x = [1, 2, 3, 4, 5]
coordinates_lists = [[0, 0, 0, 0, 0],
                     [10, 30, 60, 30, 10],
                     [70, 40, 20, 40, 70],
                     [10, 20, 30, 40, 50],
                     [50, 40, 30, 20, 10],
                     [75, 0, 75, 0, 75],
                     [0, 0, 0, 0, 0]]
print('Creating charts\n')
filenames = []
for index in np.arange(0, len(coordinates_lists)-1):
    # get current and next y coordinates
    y = coordinates_lists[index]
    y1 = coordinates_lists[index+1]
    
    # calculate the distance to the next position
    y_path = np.array(y1) - np.array(y)
    for i in np.arange(0, n_frames + 1):
        # divide the distance by the number of frames 
        # and multiply it by the current frame number
        y_temp = (y + (y_path / n_frames) * i)
        # plot
        plt.bar(x, y_temp)
        plt.ylim(0,80)
        # build file name and append to list of file names
        filename = f'images/frame_{index}_{i}.png'
        filenames.append(filename)
        # last frame of each viz stays longer
        if (i == n_frames):
            for i in range(5):
                filenames.append(filename)
        # save img
        plt.savefig(filename)
        plt.close()
print('Charts saved\n')
# Build GIF
print('Creating gif\n')
with imageio.get_writer('mybars.gif', mode='I') as writer:
    for filename in filenames:
        image = imageio.imread(filename)
        writer.append_data(image)
print('Gif saved\n')
print('Removing Images\n')
# Remove files
for filename in set(filenames):
    os.remove(filename)
print('DONE')

Отлично! Мы определенно можем улучшить эстетику нашей диаграммы. Попробуем добавить детали.

n_frames = 10
bg_color = '#95A4AD'
bar_color = '#283F4E'
gif_name = 'bars'
x = [1, 2, 3, 4, 5]
coordinates_lists = [[0, 0, 0, 0, 0],
                     [10, 30, 60, 30, 10],
                     [70, 40, 20, 40, 70],
                     [10, 20, 30, 40, 50],
                     [50, 40, 30, 20, 10],
                     [75, 0, 75, 0, 75],
                     [0, 0, 0, 0, 0]]
print('Creating charts\n')
filenames = []
for index in np.arange(0, len(coordinates_lists)-1):
    y = coordinates_lists[index]
    y1 = coordinates_lists[index+1]
    y_path = np.array(y1) - np.array(y)
    for i in np.arange(0, n_frames + 1):
        y_temp = (y + (y_path / n_frames) * i)
        # plot
        fig, ax = plt.subplots(figsize=(8, 4))
        ax.set_facecolor(bg_color)
        plt.bar(x, y_temp, width=0.4, color=bar_color)
        plt.ylim(0,80)
        # remove spines
        ax.spines['right'].set_visible(False)
        ax.spines['top'].set_visible(False)
        # grid
        ax.set_axisbelow(True)
        ax.yaxis.grid(color='gray', linestyle='dashed', alpha=0.7)
        # build file name and append to list of file names
        filename = f'images/frame_{index}_{i}.png'
        filenames.append(filename)
        
        # last frame of each viz stays longer
        if (i == n_frames):
            for i in range(5):
                filenames.append(filename)
        # save img
        plt.savefig(filename, dpi=96, facecolor=bg_color)
        plt.close()
print('Charts saved\n')
# Build GIF
print('Creating gif\n')
with imageio.get_writer(f'{gif_name}.gif', mode='I') as writer:
    for filename in filenames:
        image = imageio.imread(filename)
        writer.append_data(image)
print('Gif saved\n')
print('Removing Images\n')
# Remove files
for filename in set(filenames):
    os.remove(filename)
print('DONE')

Потрясающие! Определенно есть что улучшить. Вы можете добавить заголовок, изменить способ перехода, используя некоторую интерполяцию, и даже заставить полоски перемещаться по оси x.

Диаграммы разброса

Чтобы работать с диаграммами рассеяния, нам нужно учитывать оси x и y. У нас не обязательно будет одинаковое количество точек для отображения в каждом кадре, поэтому нам нужно исправить это, чтобы сделать переходы.

coordinates_lists = [[[0],[0]],
                     [[100,200,300],[100,200,300]],
                     [[400,500,600],[400,500,600]],
                     [[400,500,600,400,500,600],[400,500,600,600, 500,400]],
                     [[500],[500]],
                     [[0],[0]]]
gif_name = 'movie' 
n_frames=10
bg_color='#95A4AD'
marker_color='#283F4E' 
marker_size = 25
print('building plots\n')
filenames = []
for index in np.arange(0, len(coordinates_lists)-1):
    # get current and next coordinates
    x = coordinates_lists[index][0]
    y = coordinates_lists[index][1]
    x1 = coordinates_lists[index+1][0]
    y1 = coordinates_lists[index+1][1]
    # Check if sizes match
    while len(x) < len(x1):
        diff = len(x1) - len(x)
        x = x + x[:diff]
        y = y + y[:diff]
    while len(x1) < len(x):
        diff = len(x) - len(x1)
        x1 = x1 + x1[:diff]
        y1 = y1 + y1[:diff]
    # calculate paths
    x_path = np.array(x1) - np.array(x)
    y_path = np.array(y1) - np.array(y)
    for i in np.arange(0, n_frames + 1):                
        # calculate current position
        x_temp = (x + (x_path / n_frames) * i)
        y_temp = (y + (y_path / n_frames) * i)
        # plot
        fig, ax = plt.subplots(figsize=(6, 6), subplot_kw = dict(aspect="equal"))
        ax.set_facecolor(bg_color)
            
        plt.scatter(x_temp, y_temp, c=marker_color, s = marker_size)
        plt.xlim(0,1000)
        plt.ylim(0,1000)
        # remove spines
        ax.spines['right'].set_visible(False)
        ax.spines['top'].set_visible(False)
        # grid
        ax.set_axisbelow(True)
        ax.yaxis.grid(color='gray', linestyle='dashed', alpha=0.7)
        ax.xaxis.grid(color='gray', linestyle='dashed', alpha=0.7)
        # build file name and append to list of file names
        filename = f'images/frame_{index}_{i}.png'
        filenames.append(filename)
        if (i == n_frames):
            for i in range(5):
                filenames.append(filename)
        # save img
        plt.savefig(filename, dpi=96, facecolor=bg_color)
        plt.close()
# Build GIF
print('creating gif\n')
with imageio.get_writer(f'{gif_name}.gif', mode='I') as writer:
    for filename in filenames:
        image = imageio.imread(filename)
        writer.append_data(image)
print('gif complete\n')
print('Removing Images\n')
# Remove files
for filename in set(filenames):
    os.remove(filename)
print('done')

Вот и все!

Мы изучили, как создавать и сохранять диаграммы с помощью Matplotlib, как использовать Imageio для создания файлов GIF, а затем мы проверили несколько различных типов диаграмм и некоторые проблемы, которые могут возникнуть при работе с каждым из них.

Я использовал те же самые концепции для написания букв на диаграмме рассеяния. Вы можете узнать больше об этом проекте здесь.

Спасибо, что прочитали мою статью!