Interaktiivsed ja animeeritud graafikud

Valter Kiisk
TÜ Füüsika Instituut
Viimati muudetud: 31.03.2018
$\renewcommand{\vec}{\boldsymbol}$ $\newcommand{\erf}{\mathop{\rm erf}\nolimits}$ $\newcommand{\mod}{\mathop{\rm mod}\nolimits}$
In [1]:
# üldised vahendid ja seadistused
import numpy as np
from numpy import zeros, linspace, arange, exp, sin, cos, log, sqrt, pi
from scipy.misc import derivative
from matplotlib import rcParams
rcParams['figure.dpi'] = 100
rcParams['lines.markeredgewidth'] = 0
rcParams['lines.markersize'] = 5
rcParams['font.size'] = 12
from matplotlib.pyplot import *

Interaktiivne graafik

Kui kasutada %matplotlib inline (vaikimisi) asemel %matplotlib notebook, saame interaktiivse graafiku otse Jupyteri keskkonnas. Selline graafik kuvab hiirekursori koordinaate ja sisaldab vahendeid graafiku suumimiseks, liigutamiseks ja salvestamiseks.

Mooduli matplotlib.pyplot käsud figure, plot, text, jne — nagu ka klasside Figure, Axes, jne vastavad meetodid — tagastavad viida loodud objektile, ja selle võib säilitada muutujas. Interaktiivse graafiku puhul saab selle muutuja vahendusel hiljem graafiku sisu muuta:

In [3]:
%matplotlib notebook

f = lambda x: sin(2*pi*x)
x = linspace(0, 1, 200)
x0 = 0.5
tõus = derivative(f, x0, 1e-4)

figure(figsize=(5,3))
plot(x, f(x), 'r-')
punkt, = plot(x0, f(x0), 'bo')
puutuja, = plot(x, f(x0) + (x - x0) * tõus, 'b-')
xlim(0, 1)
ylim(-1.1, 1.1)
grid()
lipik = text(0.75, 0.85, 'tõus=%.2f' % tõus)
tight_layout(pad=0.2)
show()

Nüüd seni kuni see graafik on veel "elav" (st pole vajutatud nupule Stop Interaction), saab järgmise koodi abil muuta graafiku sisu:

In [4]:
x0 = 0.6
tõus = derivative(f, x0, 1e-4)
punkt.set_data(x0, f(x0))
puutuja.set_ydata( f(x0) + (x - x0) * tõus )
lipik.set_text( 'tõus=%.2f' % tõus )

Et parameetreid graafiliselt muuta, tuleb kasutada graafilise kasutajaliidese elemente (widgets) moodulist ipywidgets. Mugavaim element on liugur (slider), mille tekitab (vastavalt andmetüübile) IntSlider või FloatSlider. Aga võimalik on tekitada ka tekstiväli (Text), rippmenüü (Dropdown), loend (Select), lüliti (ToggleButton või Checkbox), raadionupud (RadioButtons), jne (vt täielikku nimekirja). Lihtsamal juhul saab interaktiivse seose arvutusülesande ja sisestuselemendi vahel luua käsuga ipywidgets.interact.

In [5]:
from ipywidgets import interact, FloatSlider

figure(figsize=(5,3))
plot(x, f(x), 'r-')
punkt, puutuja = plot((), (), 'bo', (), (), 'b-')
xlim(0, 1)
ylim(-1.1, 1.1)
grid()
lipik = text(0.75, 0.85, '')
tight_layout(pad=0.2)
show()

def uuenda(x0):
    tõus = derivative(f, x0, 1e-4)
    punkt.set_data(x0, f(x0))
    puutuja.set_data(x, f(x0) + (x - x0) * tõus)
    lipik.set_text( 'tõus=%.2f' % tõus )

slider = FloatSlider(min=0, max=1, step=0.01, value=0.5, description='x0')
interact(uuenda, x0=slider);

Võrreldava tulemuse saab ka staatilise graafikuga (%matplotlib inline), aga sel juhul tuleb iga kord terve graafik tekitada uuesti.

In [63]:
%matplotlib inline

def graafik(x0):
    tõus = derivative(f, x0, 1e-4)
    figure(figsize=(5,3))    
    plot(x, f(x), 'r-')
    plot(x0, f(x0), 'bo')
    plot(x, f(x0) + (x - x0) * tõus, 'b-')
    xlim(0, 1)
    ylim(-1.1, 1.1)
    grid()
    text(0.75, 0.85, 'tõus=%.2f' % tõus)
    tight_layout(pad=0.2)
    show()

slider = FloatSlider(min=0, max=1, step=0.01, value=0.5, description='x0')
interact(graafik, x0=slider);

Elementide paigutamine üksteise kõrvale või nende kujunduse muutmine nõuab juba rohkem programmeerimist:

In [6]:
%matplotlib notebook
from ipywidgets import interactive_output, HBox
from IPython.display import display

figure(figsize=(6,3))
komp1, = plot((), (), 'g-', linewidth=1)
komp2, = plot((), (), 'r-', linewidth=1)
summa, = plot((), (), 'b-', linewidth=2)
xlim(0, 8)
ylim(-2, 2)
grid()
tight_layout(pad=0.2)
show()

t = linspace(0, 8, 800)
def uuenda(sagedus, faas):
    x1 = sin(2*pi*t)
    x2 = sin(2*pi*sagedus*t + faas)
    komp1.set_data(t, x1)
    komp2.set_data(t, x2)
    summa.set_data(t, x1+x2)

slider1 = FloatSlider(min=0, max=2, step=0.02, value=1, description='sagedus')
slider2 = FloatSlider(min=0, max=pi, step=0.01*pi, value=0, description='faasinihe')
display(
    HBox( (slider1, slider2) ),
    interactive_output(uuenda, {'sagedus': slider1, 'faas': slider2})
)
In [7]:
import scipy.signal
from ipywidgets import IntSlider, Layout

t = linspace(0, 2.5, 800)
wt = 2*pi*t
f = scipy.signal.square(wt, 0.5)
fourier = lambda k: 4 * sin((2 * k - 1) * wt) / (pi * (2 * k - 1))

figure(figsize=(6,3))
plot(t, f, 'r-', linewidth=1)
summa, = plot((), (), 'b-', linewidth=2)
xlim(0, 2.5)
ylim(-1.3, 1.3)
grid()
tight_layout(pad=0.2)
show()

def uuenda(n):
    x = zeros( len(t) )
    for k in range(1, n):
        x += fourier(k)
    summa.set_data(t, x)

slider = IntSlider(min=2, max=30, value=2,
             description='harmooniliste komponentide arv',
             style = {'description_width': 'initial'},
             layout=Layout(width='90%'))
interact(uuenda, n=slider);

Animeerimine

Lihtne liides graafiku animeerimiseks on matplotlib.animation.FuncAnimation.

In [8]:
from matplotlib.animation import FuncAnimation

graafik = figure(figsize=(6,3))
plot(t, f, 'r-', linewidth=1)
summa, = plot((), (), 'b-', linewidth=2)
xlim(0, 2.5)
ylim(-1.3, 1.3)
grid()
arv = text(0.75, 0.95, '', ha='center', va='top')
tight_layout(pad=0.2)
show()

def uuenda(kaader):
    n = kaader + 2
    x = zeros( len(t) )
    for k in range(1, n):
        x += fourier(k)
    summa.set_data(t, x)
    arv.set_text('n=' + str(n))
    return summa, arv

ani = FuncAnimation(graafik, uuenda, frames=20, interval=500, blit=True)

Animatsiooni võib salvestada omaette videofailina. Seda saab teha näiteks järgmisel viisil. Esmalt tuleb käsuga savefig salvestada iga kaader eraldi (rastergraafika) faili, kus failinimes sisalduks kaadri järjekorranumber (näiteks kaader000.png, kaader001.png, jne):

In [131]:
%matplotlib inline

figure(figsize=(8,4), dpi=150)
for kaader in range(20):
    n = kaader + 2
    x = zeros( len(t) )
    for k in range(1, n):
        x += fourier(k)    
    plot(t, f, 'r-', linewidth=0.8)
    plot(t, x, 'b-')
    xlim(0, 2.5)
    ylim(-1.3, 1.3)
    grid()
    text(0.75, 0.95, 'n=' + str(n), ha='center', va='top')
    xlabel('aeg')
    ylabel('signaal')
    tight_layout(pad=0.2)
    savefig('kaadrid/kaader%03d.png' % kaader)
    clf()  # pärast salvestamist kustutame graafiku
<matplotlib.figure.Figure at 0x33ed2c1e48>

Seejärel tuleb kasutada mõnda programmi, mis võimaldaks kompileerida kõik kaadrid üheks videoks või animeeritud GIF-failiks. Näiteks programm ffmpeg teeb selle töö käsuga:
ffmpeg -r 10 -i kaader%03d.png -b:v 2M animation.mp4
Siin kaadrisagedust (-r) ja bitikiirust (-b:v) võib muuta vastavalt vajadusele. Veidi põhjalikum näide, kus täpsustatakse videokoodek ja antakse erinevad kaadrisagedused sisend- ja väljundvoo jaoks:
ffmpeg -r 2 -i kaader%03d.png -c:v libx264 -pix_fmt yuv420p -r 10 animation.mp4
Lihtsaim käsk sama programmiga GIF animatsiooni loomiseks:
ffmpeg -r 2 -i kaader%03d.png animation.gif
GIF animatsioonide tegemiseks leidub ka veebipõhiseid vahendeid.

Äsjase ülesande animatsioon: MP4 video ja GIF.