Как легко создавать гифки для анимации ваших визуализаций
Есть много способов создавать анимацию в 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, а затем мы проверили несколько различных типов диаграмм и некоторые проблемы, которые могут возникнуть при работе с каждым из них.
Я использовал те же самые концепции для написания букв на диаграмме рассеяния. Вы можете узнать больше об этом проекте здесь.
Спасибо, что прочитали мою статью!