【问题标题】:Managing dynamic plotting in matplotlib Animation module在 matplotlib 动画模块中管理动态绘图
【发布时间】:2017-12-12 15:33:54
【问题描述】:

我想要一个迭代绘制的图表,允许跳到下一帧,停止它并返回到前一帧。

我查看了 matplotlib 动画模块,如果有一种方法可以实现前一帧功能(例如按下键时向后运行动画几帧),那将是完美的选择

最好是这样的:

def update_frame(i, data):
    fig.set_data(data[i])

但我可以明确地管理 i 迭代器是增加还是减少。

有没有办法在 matplotlib 中做到这一点? 我应该寻找不同的 python 模块吗?

【问题讨论】:

    标签: python animation matplotlib


    【解决方案1】:

    FuncAnimation 类允许 supply a generator functionframes 参数。该函数预计会产生一个值,该值将提供给动画的每个步骤的更新函数。

    FuncAnimation doc 声明:

    frames :可迭代,int,生成器函数,或无,可选 [..]
    如果是生成器函数,则必须有签名
    def gen_function() -> obj:
    在所有这些情况下,frames 中的值只是简单地传递给用户提供的 func,因此可以是任何类型。

    我们现在可以创建一个生成器函数,它产生正向或反向整数,以便动画向前运行 或向后运行。为了控制动画,我们可以使用matplotlib.widgets.Buttons 并创建一个向前一步的 或向后的 功能。这类似于my answer 中关于循环遍历一组图像的问题。

    下面是一个名为Player 的类,它是FuncAnimation 的子类并包含所有这些,允许启动和停止动画。可以像FuncAnimation一样实例化,

    ani = Player(fig, update, mini=0, maxi=10)
    

    其中update 将是一个更新函数,需要一个整数作为输入,minimaxi 表示该函数可以使用的最小和最大数字。此类存储当前索引的值 (self.i),这样如果动画停止或恢复,它将在当前帧重新开始。

    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=u'$\u25C0$')
            self.button_stop = matplotlib.widgets.Button(sax, label=u'$\u25A0$')
            self.button_forward = matplotlib.widgets.Button(fax, label=u'$\u25B6$')
            self.button_oneforward = matplotlib.widgets.Button(ofax, label=u'$\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()
    


    注意:这不是以允许 blitting 的方式编写的。

    【讨论】:

    • 感谢您的解释。不知何故,我没有想到这可以通过使用生成器来解决。
    • 我尝试将生成器提供给FuncAnimationframes,并且生成器正在将数据流式传输到我的动画师中,直到最后一次迭代我在@ 中得到StopIteration Traceback 987654353@
    • @importanceofbeingernest 我也有tried setting repeat=Falseinterval=1
    • 谢谢!这应该是一个 matplotlib 示例
    • 我很想知道如何将它嵌入到 tkinter 人物画布上。我知道如何为单个数字执行此操作,但实现我的目标并不像删除上面的plt.show 并在ani 下面插入以下行那么简单:root=Tk() canvas = FigureCanvasTkAgg(fig, master=root) canvas.draw() canvas.get_tk_widget().pack(side=TOP,fill=BOTH,expand=1) root.mainloop()
    【解决方案2】:

    有关动画模块的正确工作答案,请参阅the answer of ImportanceOfBeingErnest

    您的预期功能存在多个问题。动画的进度如何与反转一起工作?会有视频,但按下按钮开始播放?还是应该有单独的框架步骤?我不确定我是否理解动画如何与这种反转功能相结合;我把 matplotlib 动画想象成电影。

    我的另一个问题是技术问题:我不确定这可以通过 matplotlib 动画来完成。 The docs explain FuncAnimation 表面上的表现

    for d in frames:
       artists = func(d, *fargs)
       fig.canvas.draw_idle()
       plt.pause(interval)
    

    在哪里frames is essentially an iterable。在动画期间动态调整frames 对我来说似乎并不简单,所以这是一个技术障碍。

    实际上,您描述的功能在我的脑海中在基于小部件的方法中效果更好。 Buttons 可以传播“动画”,或者您可以使用check button 来修改下一步是前进还是后退。这是我的意思的简单概念证明:

    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()
    

    请注意,我一般对 matplotlib 小部件或 GUI 并没有真正的经验,因此不要期望上述内容符合该主题的最佳实践。我还添加了一些额外的参数要在这里和那里传递,因为我不喜欢使用全局名称,但这在这种情况下可能有点迷信;老实说,我说不出来。此外,如果您在类或函数中定义这些对象,请确保保留对小部件的引用,否则它们可能会在意外垃圾收集时变得无响应。

    生成的图形有一个用于绘制散点图的轴,并且有两个按钮可以增加切片索引。数据形状为(ndat,100,2),其中尾随索引定义二维空间中的 100 个点。特定状态:

    (没必要这么丑,我只是不想摆弄设计。)

    我什至可以想象一个计时器自动更新绘图的设置,并且可以使用小部件设置更新的方向。我不确定如何正确地做到这一点,但我会尝试走这条路,以获得您似乎追求的那种可视化。

    另外请注意,上述方法完全缺少 FuncAnimation 会做的 blitting 和其他优化,但希望这不会干扰您的可视化。

    【讨论】:

    • 您说“在动画期间动态调整帧对我来说似乎并不简单”。您可以查看my answer 这个问题,了解如何做到这一点。重点是frames可以被赋予一个生成器函数。
    • @ImportanceOfBeingErnest 谢谢,我也确实说过我是一个 GUI 菜鸟;)我确实看到了生成器函数选项,但这让我觉得在回调之间没有简单的方法来操作函数。干得好!
    • 谢谢,这个答案真的很有帮助
    • 抱歉!我会将我的建议移到讨论中更合适的位置。谢谢。
    • @fishbacp 不用担心。但这是 2017 年的答案,可能是我唯一一次看 mpl 动画 :) 如果我可以帮助你,请告诉我。但欧内斯特的回答可能更适合您的目的。
    猜你喜欢
    • 1970-01-01
    • 2012-06-12
    • 1970-01-01
    • 2020-11-06
    • 2015-11-02
    • 1970-01-01
    • 2020-10-12
    • 1970-01-01
    相关资源
    最近更新 更多