Вероятно най-лесният начин да анимирате вашите сюжети на Python

Генерирайте рамки от сюжети и ги комбинирайте като GIF

Визуализацията винаги е важна употреба на Python за Data Science и Data Analytics. Понякога искаме да оставим сюжетите си да се движат за по-разширено представяне и прозрения. Повечето библиотеки за анимирани визуализации обаче изискват допълнителни усилия за научаване, като например matplotlib.animation. Когато просто искаме да генерираме някаква анимирана графика по бърз и лесен начин, това може да не е най-добрият подход.

В тази статия ще представя сравнително по-малко мащабируем, но много по-лесен начин за анимиране на нашите графики на Python, който използва библиотеката ImageIO. Тази библиотека обикновено се използва за манипулиране на изображения в Python, както и за комбиниране на множество изображения за генериране на GIF файлове. Използва се изненадващо лесно.

Преди да можем да използваме библиотеката ImageIO, трябва да я инсталираме с помощта на pip както следва.

pip install imageio

1. Пример за линейна диаграма

Нека започнем с основна линейна диаграма за тази демонстрация. За да го опростя, искам да генерирам масив NumPy с 50 цели числа. След това тези цели числа могат да бъдат начертани като линейна диаграма.

import numpy as np
import matplotlib.pyplot as plt
import imageio
np.random.seed(0)
SIZE = 50
y = np.random.randint(-5, 5, SIZE)
plt.plot(y)
plt.ylim(-10, 10)
plt.show()

В кода по-горе зададох случайното начало, така че да можете да възпроизведете точно същия резултат като моя, ако желаете. SIZE се дефинира като константа, така че можете да го промените според вашите предпочитания.

Целите числа се генерират в диапазона [-5,5]. Освен това, за да направя графиката по-лесна за четене, бих искал да добавя ylim(-10, 10), така че всички точки да са в средната част на диаграмата.

Сега имаме тази статична основна линейна диаграма. Да кажем, че искаме да анимираме линейната диаграма, като начертаем точките една по една. Също така би било по-добре резултатът да е в GIF, така че да можем да го вградим навсякъде, където пожелаем.

Разбира се, подходящият метод е да използвате модула matplotlib.animation. Въпреки това, бързият и лесен начин, който бих представил, е да генерирам рамките като отделни PNG изображения. След това можем да използваме ImageIO, за да ги комбинираме заедно като GIF.

1.1 Генериране на рамки

Всъщност генерирането на рамки не е много трудно. Идеята е да начертаете линейната диаграма с 2 точки, 3 точки, … и 50 точки. Всеки график трябва да бъде записан в отделно изображение.

Кодът е както следва

for i in range(2, SIZE+1):
    plt.plot(y[0:i])
    plt.ylim(-10, 10)
    plt.savefig(f'line-{i}.png')
    plt.close()

For-цикълът започва с 2 точки, тъй като една точка не може да бъде генерирана като валидна линейна диаграма. Той ще спре на SIZE+1, така че range() ще бъде прекратен на точния брой точки. С други думи, последното i е 50 в нашия случай.

Важно е да дефинирате ylim във всеки кадър. В противен случай оста y ще бъде автоматично определена, така че да е различна в рамките.

След като чертежът е конфигуриран, можем да запазим фигурата като PNG изображение с предварително дефиниран модел на име на файл. Този модел на име на файл ще се използва по-късно за попълване на кадрите. И накрая, трябва да затворим сюжета, като извикаме plt.close(). Така че следващият кадър в следващия цикъл може да бъде генериран правилно.

След стартиране на този код ще бъдат генерирани 49 PNG файла в текущата работна директория.

1.2 Генериране на GIF

Следващата стъпка ще бъде комбинирането им заедно в GIF. Библиотеката ImageIO предоставя „писец“, който лесно можем да добавим изображенията като рамки.

with imageio.get_writer('line.gif', mode='i') as writer:
    for i in range(2, SIZE+1):
        image = imageio.imread(f'line-{i}.png')
        writer.append_data(image)

Можем да използваме оператора with, за да не се притесняваме за затварянето на потока. Името на GIF файла ще се казва line.gif. Флагът mode='i' е намек, който казва на ImageIO, че входът ще бъде изображения.

След това можем да имаме друг for-цикъл, за да получим индекса. Така че можем да използваме индекса, за да получим имената на файловете на кадрите. След като имаме името на файла, просто го добавете към текущия писател.

След като изпълним горния код, GIF файлът line.gif трябва вече да е генериран.

Между другото, ако рамките вече не са полезни, можем да използваме следния код, за да ги изтрием.

import os
for i in range(2, SIZE+1):
    os.remove(f'line-{i}.png')

1.3 Малко подобрение

Показаният по-горе GIF може да не е идеален. По-конкретно, оста x също се движи с рамките. Може да е добре в зависимост от това какво искаме да постигнем, но да предположим, че искаме да коригираме оста x в GIF, можем да го направим определено.

Този пример също така показва, че методът, който се въвежда тук, е доста гъвкав. По принцип, когато искаме да имаме някои функции в нашите анимирани диаграми, ние не разчитаме на API на библиотеката, а на това как са генерирани кадрите.

В този случай просто трябва да добавим конфигурация xlim() в оригиналния код, за да генерираме кадрите с фиксирана ос x.

for i in range(2, SIZE+1):
    plt.plot(y[0:i])
    plt.ylim(-10, 10)
    plt.xlim(0, 50)
    plt.savefig(f'line-{i}.png')
    plt.close()

След това дори не е необходимо да се променя кодът на ImageIO за генериране на GIF, тъй като сме актуализирали съдържанието на кадрите, но броят на изображенията остава същият.

И накрая, новият GIF изглежда по следния начин.

2. По-красив пример за стълбовидна диаграма

Методът е добре представен в горния пример на линейната диаграма. Аз обаче просто искам да покажа, че методът има по-голям потенциал. Този път нека анимираме стълбовидна диаграма.

Преди демонстрацията да може да започне, трябва да дефинираме ос x и списък на оста y. Причината, поради която се нуждаем от списък със стойности на оста y, е, че искаме да позволим на GIF да показва множество кадъра и всеки кадър е лентова диаграма. С други думи, те имат една и съща ос x, но различни стойности на y.

x_axis = [1, 2, 3]
y_axis_list = [
    [0, 0, 0],
    [1, 2, 3],
    [3, 2, 1],
    [5, 5, 5],
    [7, 7, 7],
    [9, 2, 9],
    [2, 9, 2],
    [1, 1, 1],
    [9, 9, 9],
    [0, 0, 0]
]

Горният код е само пример. Няма модел, просто са измислени :)

2.1 Генериране на GIF на лентова диаграма

След това нека генерираме лентовата диаграма на рамките.

png_files = []
for i, y_axis in enumerate(y_axis_list):
    # Create Plot
    plt.bar(x_axis, y_axis)
    plt.ylim(0,10)
    # Create PNG file
    filename = f'bar-{i}.png'
    png_files.append(filename)
    # Save Figure
    plt.savefig(filename)
    plt.close()
with imageio.get_writer('bar.gif', mode='i') as writer:
    for filename in png_files:
        image = imageio.imread(filename)
        writer.append_data(image)

Този път не искам да се занимавам с метода range() във for-цикъла. Можем да дефинираме списък png_files, който да съдържа всички имена на файлове на кадрите. Така че по-късно, когато искаме да генерираме GIF от PNG изображенията, просто трябва да вземем имената на файловете от този списък.

Кодът за генериране на GIF не се различава по нищо от предишния пример. Това също показва, че методът е доста общ.

Генерираният GIF е както следва.

Мммм, работи, но не идеално. Всъщност изобщо не може да се чете.

2.2 Генерирайте GIF с плавен преход

Мислене от гледна точка на анимацията. Добре изглеждащата анимация не е нищо друго освен повече FPS и плавен преход. Основната идея е да добавите повече преходни кадри между две състояния.

Следователно можем да направим следното.

smooth_coef = 10
png_files = []
for i in range(0, len(y_axis_list)-1):
    # Get Current & Next Frame
    y_axis_curr = y_axis_list[i]
    y_axis_next = y_axis_list[i+1]
# Generate Middle Frames
    y_diff = np.array(y_axis_next) - np.array(y_axis_curr)
    for j in range(0, smooth_coef+1):
        y_axis = (y_axis_curr + (y_diff / smooth_coef) * j)
        # Create Plot
        plt.bar(x_axis, y_axis)
        plt.ylim(0,10)  
        # Create PNG file
        filename = f'bar-{i}-{j}.png'
        png_files.append(filename)
        # Stretch the last frame
        if j == smooth_coef:
            for _ in range(5):
                png_files.append(filename)
        # Save Figure
        plt.savefig(filename)
        plt.close()

Нека обясня това. smooth_coef означава „коефициент на гладкост“. Това означава, че искаме да добавим 10 преходни кадъра между всеки два основни кадъра. Основните рамки са точно оста y в демонстрационния набор от данни.

Във for-цикъла ще получим текущата у-ос и следващата. След това можем да използваме следващите y-стойности, за да извадим текущата. Така че резултатът ще бъде „разликата“ между тези два основни кадъра. Тогава, ако оставим „разликата“ да се раздели на гладкия коефициент, ще получим стъпаловидна разлика. В обобщение, можем да изчислим разликите между всеки преходен кадър. Всички добавени преходни кадри ще превърнат текущия кадър в следващия.

y_axis = (y_axis_curr + (y_diff / smooth_coef) * j)

Друг трик е да оставите GIF-а да „спре“ на критичния кадър за известно време, за да можем да го видим за малко по-дълго време. Това може да стане чрез повтаряне на имената на файловете в определени моменти, така че рамката да се повтаря. В генерирания GIF ще видим рамката няколко пъти, което също означава по-дълго време.

Нека да видим резултата.

Това е много по-добре!

3. Методът е общ

За разлика от всякакви специфични библиотеки за чертане като matplotlib.animation, методът ImageIO-GIF е много по-общ. Тоест ние дори не сме ограничени да използваме какви библиотеки. Докато фигурата може да бъде запазена във файлове с изображения, можем да използваме този метод, за да я оставим да се движи.

Бърз пример е следният. Можем да използваме Seaborn, за да генерираме стълбовидна диаграма.

import seaborn as sns
sns.set_style('darkgrid')

Модулът Seaborn е импортиран и стилът е зададен. Тогава единствената част от кода, която трябва да променим, е както следва.

sns.barplot(x_axis, y_axis)

Полученият GIF изглежда по следния начин.

Резюме

В тази статия въведох метод за анимиране на визуализация на Python в GIF, без да използвам никакви специално създадени библиотеки за сюжетна анимация. Идеята е да генерирате сюжета кадър по кадър във файлове с изображения. След това тези файлове с изображения могат да се комбинират заедно като GIF с помощта на модула ImageIO.

Методът също е доста общ. Не е необходимо да изучаваме API или методи за определени стилове на анимация. Всичко, което трябва да направим, е да генерираме различни рамки, за да постигнем изискванията. Също така, той дори не се ограничава до никакви библиотеки за визуализация, защото разчита на изображенията, генерирани като рамки.

Моля, имайте предвид, че този метод може да не е обичаен за чертане. Един от проблемите може да бъде скалируемостта, защото трябва да генерираме много отделни файлове като рамки, преди да можем да получим GIF.



Ако смятате, че статиите ми са полезни, моля, помислете дали да не се присъедините към Medium Membership, за да подкрепите мен и хиляди други автори! (Щракнете върху връзката по-горе)