real - matplotlib python
Gestión del trazado dinámico en matplotlib Módulo de animación (2)
Me gustaría tener un gráfico trazado de forma iterativa que permita saltar al siguiente fotograma, detenerlo y volver a un fotograma anterior.
He visto el módulo de animación matplotlib, que sería perfecto si hubiera una manera de implementar la funcionalidad de cuadro anterior (como ejecutar la animación hacia atrás para algunos fotogramas cuando se presiona una tecla)
Sería bueno algo como esto:
def update_frame(i, data):
fig.set_data(data[i])
pero de una manera que podría administrar explícitamente si el iterador i aumenta o disminuye.
¿Hay alguna manera de hacer eso en matplotlib? ¿Debería buscar un módulo de python diferente?
Para una respuesta de trabajo adecuada con el módulo Animation, vea la respuesta de ImportanceOfBeingErnest
Tengo varios problemas con la funcionalidad prevista. ¿Cómo funcionaría el progreso de la animación junto con la inversión? ¿Habría un video, pero presionando un botón comenzará a reproducirse? ¿O debería haber pasos individuales de marcos? No estoy seguro de entender cómo se puede combinar una animación con esta función de inversión; Imagino que las animaciones matplotlib son esencialmente películas.
Mi otro problema es técnico: no estoy seguro de que esto se pueda hacer con animaciones matplotlib. Los documentos explican que una FuncAnimation
se realiza superficialmente
for d in frames:
artists = func(d, *fargs)
fig.canvas.draw_idle()
plt.pause(interval)
donde frames
es esencialmente iterable . No me parece sencillo ajustar dinámicamente los frames
durante la animación, por lo que este es un obstáculo técnico.
En realidad, la funcionalidad que describiste funciona mucho mejor en mi cabeza en un enfoque basado en widgets. Los botones podrían propagar la "animación", o podría tener un botón de verificación que modifique si el próximo paso avanza o retrocede. Aquí hay una prueba de concepto simple de lo que quiero decir:
import matplotlib.pyplot as plt
from matplotlib.widgets import Button
import numpy as np # just for dummy data generation
# generate dummy data
ndat = 20
x = np.linspace(0,1,ndat)
phi = np.linspace(0,2*np.pi,100,endpoint=False)
dat = np.transpose([x[:,None]*np.cos(phi),x[:,None]*np.sin(phi)],(1,2,0))
# create figure and axes
fig = plt.figure()
ax_pl = plt.subplot2grid((5,5),(0,0),colspan=5,rowspan=3) # axes_plot
ax_bl = plt.subplot2grid((5,5),(4,0),colspan=2,rowspan=1) # axes_button_left
ax_br = plt.subplot2grid((5,5),(4,3),colspan=2,rowspan=1) # axes_button_right
# create forward/backward buttons
butt_l = Button(ax_bl, ''/N{leftwards arrow}'') # or u'''' on python 2
butt_r = Button(ax_br, ''/N{rightwards arrow}'') # or u'''' on python 2
# create initial plot
# store index of data and handle to plot as axes property because why not
ax_pl.idat = 0
hplot = ax_pl.scatter(*dat[ax_pl.idat].T)
ax_pl.hpl = hplot
ax_pl.axis(''scaled'')
ax_pl.axis([dat[...,0].min(),dat[...,0].max(),
dat[...,1].min(),dat[...,1].max()])
ax_pl.set_autoscale_on(False)
ax_pl.set_title(''{}/{}''.format(ax_pl.idat,dat.shape[0]-1))
# define and hook callback for buttons
def replot_data(ax_pl,dat):
''''''replot data after button push, assumes constant data shape''''''
ax_pl.hpl.set_offsets(dat[ax_pl.idat])
ax_pl.set_title(''{}/{}''.format(ax_pl.idat,dat.shape[0]-1))
ax_pl.get_figure().canvas.draw()
def left_onclicked(event,ax=ax_pl,dat=dat):
''''''try to decrement data index, replot if success''''''
if ax.idat > 0:
ax.idat -= 1
replot_data(ax,dat)
def right_onclicked(event,ax=ax_pl,dat=dat):
''''''try to increment data index, replot if success''''''
if ax.idat < dat.shape[0]-1:
ax.idat += 1
replot_data(ax,dat)
butt_l.on_clicked(left_onclicked)
butt_r.on_clicked(right_onclicked)
plt.show()
Tenga en cuenta que no tengo mucha experiencia con los widgets de matplotlib o las GUI en general, por lo tanto, no espere que lo anterior se ajuste a las mejores prácticas en el tema. También agregué algunos parámetros adicionales para pasar aquí y allá, porque tengo una aversión a usar nombres globales, pero esto podría ser algo supersticioso en este contexto; Honestamente, no puedo decirlo. Además, si está definiendo estos objetos dentro de una clase o función, asegúrese de mantener una referencia a los widgets, de lo contrario, podrían dejar de responder cuando se recolecte basura accidentalmente.
La figura resultante tiene un eje para trazar los gráficos de dispersión, y hay dos botones para incrementar el índice de corte. Los datos tienen forma (ndat,100,2)
, donde los índices finales definen 100 puntos en el espacio 2d. Un estado específico:
(No tiene por qué ser tan feo, simplemente no quería jugar con el diseño).
Incluso podría imaginar una configuración donde un temporizador actualiza automáticamente la trama, y la dirección de la actualización se puede establecer con un widget. No estoy seguro de cómo se podría hacer esto de manera adecuada, pero trataría de seguir este camino para el tipo de visualización que usted parece estar buscando.
También tenga en cuenta que el enfoque anterior no FuncAnimation
y otras optimizaciones que FuncAnimation
haría, pero es de esperar que esto no interfiera con su visualización.
La clase FuncAnimation
permite suministrar una función de generador al argumento de frames
. Se esperaría que esta función arroje un valor que se suministra a la función de actualización para cada paso de la animación.
Los estados de documentación de FuncAnimation
:
frames
: iterable, int, generator function, o None, opcional [..]
Si una función de generador, entonces debe tener la firma
def gen_function() -> obj:
En todos estos casos, los valores en marcos se pasan simplemente a la función proporcionada por el usuario y, por lo tanto, pueden ser de cualquier tipo.
Ahora podemos crear una función de generador que arroje números enteros, ya sea hacia adelante o hacia atrás, de modo que la animación se ejecute hacia adelante o hacia atrás . Para dirigir la animación, podemos usar matplotlib.widgets.Button
y también crear un avance de un paso o hacia atrás funcionalidad. Esto es similar a mi respuesta a la pregunta sobre el bucle a través de un conjunto de imágenes.
La siguiente es una clase llamada Player
que subclasifica FuncAnimation
e incorpora todo esto, lo que permite iniciar y detener la animación. Se puede crear una instancia similar a FuncAnimation
,
ani = Player(fig, update, mini=0, maxi=10)
donde la update
sería una función de actualización, esperando un entero como entrada, y mini
y maxi
denotarían el número mínimo y máximo que la función podría usar. Esta clase almacena el valor del índice actual ( self.i
), de modo que si la animación se detiene o se revierte, se reiniciará en el cuadro actual.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import mpl_toolkits.axes_grid1
import matplotlib.widgets
class Player(FuncAnimation):
def __init__(self, fig, func, frames=None, init_func=None, fargs=None,
save_count=None, mini=0, maxi=100, pos=(0.125, 0.92), **kwargs):
self.i = 0
self.min=mini
self.max=maxi
self.runs = True
self.forwards = True
self.fig = fig
self.func = func
self.setup(pos)
FuncAnimation.__init__(self,self.fig, self.func, frames=self.play(),
init_func=init_func, fargs=fargs,
save_count=save_count, **kwargs )
def play(self):
while self.runs:
self.i = self.i+self.forwards-(not self.forwards)
if self.i > self.min and self.i < self.max:
yield self.i
else:
self.stop()
yield self.i
def start(self):
self.runs=True
self.event_source.start()
def stop(self, event=None):
self.runs = False
self.event_source.stop()
def forward(self, event=None):
self.forwards = True
self.start()
def backward(self, event=None):
self.forwards = False
self.start()
def oneforward(self, event=None):
self.forwards = True
self.onestep()
def onebackward(self, event=None):
self.forwards = False
self.onestep()
def onestep(self):
if self.i > self.min and self.i < self.max:
self.i = self.i+self.forwards-(not self.forwards)
elif self.i == self.min and self.forwards:
self.i+=1
elif self.i == self.max and not self.forwards:
self.i-=1
self.func(self.i)
self.fig.canvas.draw_idle()
def setup(self, pos):
playerax = self.fig.add_axes([pos[0],pos[1], 0.22, 0.04])
divider = mpl_toolkits.axes_grid1.make_axes_locatable(playerax)
bax = divider.append_axes("right", size="80%", pad=0.05)
sax = divider.append_axes("right", size="80%", pad=0.05)
fax = divider.append_axes("right", size="80%", pad=0.05)
ofax = divider.append_axes("right", size="100%", pad=0.05)
self.button_oneback = matplotlib.widgets.Button(playerax, label=ur''$/u29CF$'')
self.button_back = matplotlib.widgets.Button(bax, label=ur''$/u25C0$'')
self.button_stop = matplotlib.widgets.Button(sax, label=ur''$/u25A0$'')
self.button_forward = matplotlib.widgets.Button(fax, label=ur''$/u25B6$'')
self.button_oneforward = matplotlib.widgets.Button(ofax, label=ur''$/u29D0$'')
self.button_oneback.on_clicked(self.onebackward)
self.button_back.on_clicked(self.backward)
self.button_stop.on_clicked(self.stop)
self.button_forward.on_clicked(self.forward)
self.button_oneforward.on_clicked(self.oneforward)
### using this class is as easy as using FuncAnimation:
fig, ax = plt.subplots()
x = np.linspace(0,6*np.pi, num=100)
y = np.sin(x)
ax.plot(x,y)
point, = ax.plot([],[], marker="o", color="crimson", ms=15)
def update(i):
point.set_data(x[i],y[i])
ani = Player(fig, update, maxi=len(y)-1)
plt.show()