Как дождаться окончания анимации matplotlib?

Рассмотрим следующий код, непосредственно взятый из документации Matplotlib:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import time # optional for testing only
import cv2 # optional for testing only

fig = plt.figure()   

def f(x, y):
    return np.sin(x) + np.cos(y)

x = np.linspace(0, 2 * np.pi, 120)
y = np.linspace(0, 2 * np.pi, 100).reshape(-1, 1)

im = plt.imshow(f(x, y), animated=True)    

def updatefig(*args):
    global x, y
    x += np.pi / 15.
    y += np.pi / 20.
    im.set_array(f(x, y))
    return im,

ani = animation.FuncAnimation(fig, updatefig, interval=50, blit=True)
plt.show()

Эта работа отлично работает в моей системе. Теперь попробуйте добавить следующий код к приведенному выше коду:

while True: 
  #I have tried any of these 3 commands, without success:  
    pass
    #time.sleep(1)
    #cv2.waitKey(10)

Что происходит, так это то, что программа зависает. По-видимому, класс "Animation" Matplotlib запускает анимацию в отдельном потоке. Поэтому у меня есть следующие два вопроса:

1) Если процесс запускается в отдельном потоке, почему он нарушен последующим циклом?

2) Как сказать python подождать, пока анимация не закончится?

Ответ 1

Благодаря помощи Ed Smith и MiteshNinja мне наконец удалось найти надежный метод, который работает не только с консолью Ipython, но также с консолью Python и командной строкой. Кроме того, он позволяет полностью контролировать процесс анимации. Код является самоочевидным.

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from multiprocessing import Process
import time # optional for testing only
import matplotlib.animation as animation

# A. First we define some useful tools:

def wait_fig(): 
    # Block the execution of the code until the figure is closed.
    # This works even with multiprocessing.
    if matplotlib.pyplot.isinteractive():
        matplotlib.pyplot.ioff() # this is necessary in mutliprocessing
        matplotlib.pyplot.show(block=True)
        matplotlib.pyplot.ion() # restitute the interractive state
    else:
        matplotlib.pyplot.show(block=True) 

    return    


def wait_anim(anim_flag, refresh_rate = 0.1):    
    #This will be used in synergy with the animation class in the example
    #below, whenever the user want the figure to close automatically just 
    #after the animation has ended.
    #Note: this function uses the controversial event_loop of Matplotlib, but 
    #I see no other way to obtain the desired result.

    while anim_flag[0]: #next code extracted from plt.pause(...)
        backend = plt.rcParams['backend']
        if backend in plt._interactive_bk:
            figManager = plt._pylab_helpers.Gcf.get_active()
            if figManager is not None:
                figManager.canvas.start_event_loop(refresh_rate)  


def draw_fig(fig = None):    
    #Draw the artists of a figure immediately.
    #Note: if you are using this function inside a loop, it should be less time 
    #consuming to set the interactive mode "on" using matplotlib.pyplot.ion()
    #before the loop, event if restituting the previous state after the loop.

    if matplotlib.pyplot.isinteractive():
        if fig is None:
            matplotlib.pyplot.draw()
        else: 
            fig.canvas.draw()            
    else:   
        matplotlib.pyplot.ion() 
        if fig is None:
            matplotlib.pyplot.draw()
        else: 
            fig.canvas.draw() 
        matplotlib.pyplot.ioff() # restitute the interactive state

    matplotlib.pyplot.show(block=False)
    return


def pause_anim(t): #This is taken from plt.pause(...), but without unnecessary 
                   #stuff. Note that the time module should be previously imported.
                   #Again, this use the controversial event_loop of Matplotlib. 
    backend = matplotlib.pyplot.rcParams['backend']
    if backend in matplotlib.pyplot._interactive_bk:
        figManager = matplotlib.pyplot._pylab_helpers.Gcf.get_active()
        if figManager is not None:
            figManager.canvas.start_event_loop(t)
            return
    else: time.sleep(t) 


#--------------------------

# B. Now come the particular functions that will do the job.
def f(x, y):
    return np.sin(x) + np.cos(y)


def plot_graph():
    fig = plt.figure()
    x = np.linspace(0, 2 * np.pi, 120)
    y = np.linspace(0, 2 * np.pi, 100).reshape(-1, 1)
    im = fig.gca().imshow(f(x, y))    
    draw_fig(fig)
    n_frames = 50

    #==============================================    
    #First method - direct animation: This use the start_event_loop, so is 
    #somewhat controversial according to the Matplotlib doc.
    #Uncomment and put the "Second method" below into comments to test.

    '''for i in range(n_frames): # n_frames iterations    
        x += np.pi / 15.
        y += np.pi / 20.
        im.set_array(f(x, y))
        draw_fig(fig)  
        pause_anim(0.015) # plt.pause(0.015) can also be used, but is slower

    wait_fig() # simply suppress this command if you want the figure to close 
               # automatically just after the animation has ended     
    '''    
    #================================================
    #Second method: this uses the Matplotlib prefered animation class.    
    #Put the "first method" above in comments to test it.
    def updatefig(i, fig, im, x, y, anim_flag, n_frames):
        x = x + i * np.pi / 15.
        y = y + i * np.pi / 20.
        im.set_array(f(x, y))        

        if i == n_frames-1:
            anim_flag[0] = False

    anim_flag = [True]    
    animation.FuncAnimation(fig, updatefig, repeat = False, frames = n_frames, 
         interval=50, fargs = (fig, im, x, y, anim_flag, n_frames), blit=False) 
                            #Unfortunately, blit=True seems to causes problems

    wait_fig()  
    #wait_anim(anim_flag) #replace the previous command by this one if you want the 
                     #figure to close automatically just after the animation 
                     #has ended                                                                
    #================================================           
    return

#--------------------------

# C. Using multiprocessing to obtain the desired effects. I believe this 
# method also works with the "threading" module, but I haven't test that.

def main(): # it is important that ALL the code be typed inside 
           # this function, otherwise the program will do weird 
           # things with the Ipython or even the Python console. 
           # Outside of this condition, type nothing but import
           # clauses and function/class definitions.
    if __name__ != '__main__': return                      
    p = Process(target=plot_graph)
    p.start()
    print('hello', flush = True) #just to have something printed here
    p.join() # suppress this command if you want the animation be executed in
             # parallel with the subsequent code
    for i in range(3): # This allows to see if execution takes place after the 
                       #process above, as should be the case because of p.join().
        print('world', flush = True) 
        time.sleep(1)        

main()

Ответ 2

Для меня копирование в ipython работает как ожидалось (анимация сначала воспроизводит бесконечный цикл), но при запуске script он зависает.

1) Я точно не знаю, как cpython обрабатывает многопоточность, особенно в сочетании с конкретным бэкэндом matplotlib, но он, кажется, терпит неудачу здесь. Одна из возможностей заключается в том, чтобы указать, как вы хотите использовать потоки, используя

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import multiprocessing as mp


fig = plt.figure()   

def f(x, y):
    return np.sin(x) + np.cos(y)

x = np.linspace(0, 2 * np.pi, 120)
y = np.linspace(0, 2 * np.pi, 100).reshape(-1, 1)

im = plt.imshow(f(x, y), animated=True)    

def updatefig(*args):
    global x, y
    x += np.pi / 15.
    y += np.pi / 20.
    im.set_array(f(x, y))
    return im,

#A function to set thread number 0 to animate and the rest to loop
def worker(num):
    if num == 0:
        ani = animation.FuncAnimation(fig, updatefig, interval=50, blit=True)
        plt.show()
    else:
        while True: 
            print("in loop")
            pass

    return


# Create two threads
jobs = []
for i in range(2):
    p = mp.Process(target=worker, args=(i,))
    jobs.append(p)
    p.start()

Определяет два потока и устанавливает один для работы с анимацией, один для цикла.

2) Чтобы исправить это, как предлагает @Mitesh Shah, вы можете использовать plt.show(block=True). Для меня script затем ведет себя как и ожидалось с анимацией, а затем цикл. Полный код:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

fig = plt.figure()   

def f(x, y):
    return np.sin(x) + np.cos(y)

x = np.linspace(0, 2 * np.pi, 120)
y = np.linspace(0, 2 * np.pi, 100).reshape(-1, 1)

im = plt.imshow(f(x, y), animated=True)    

def updatefig(*args):
    global x, y
    x += np.pi / 15.
    y += np.pi / 20.
    im.set_array(f(x, y))
    return im,

ani = animation.FuncAnimation(fig, updatefig, interval=50, blit=True)
plt.show(block=True)

while True: 
    print("in loop")
    pass

UPDATE: альтернатива - просто использовать интерактивный режим,

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

fig = plt.figure()   
plt.ion()
plt.show()

def f(x, y):
    return np.sin(x) + np.cos(y)

def updatefig(*args):
    global x, y


x = np.linspace(0, 2 * np.pi, 120)
y = np.linspace(0, 2 * np.pi, 100).reshape(-1, 1)
im = plt.imshow(f(x, y))    

for i in range(500):

    x += np.pi / 15.
    y += np.pi / 20.
    im.set_array(f(x, y))
    plt.draw()
    plt.pause(0.0000001)

Ответ 3

Мы можем запустить функцию анимации в отдельном потоке. Затем запустите этот поток. После создания нового потока выполнение будет продолжено.
Затем мы используем p.join(), чтобы подождать, пока наш ранее созданный поток завершит выполнение. Как только выполнение закончится (или по какой-то причине прекратится), код продолжит работу дальше.

Также matplotlib работает по-разному в оболочках Interactive Python и оболочках командной строки, нижеприведенный код должен работать для обоих этих сценариев:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from multiprocessing import Process
import time # optional for testing only
#import cv2 # optional for testing only

fig = plt.figure()   

def f(x, y):
    return np.sin(x) + np.cos(y)

x = np.linspace(0, 2 * np.pi, 120)
y = np.linspace(0, 2 * np.pi, 100).reshape(-1, 1)

im = plt.imshow(f(x, y), animated=True)    


def plot_graph(*args):
    def updatefig(*args):
        global x, y
        x += np.pi / 15.
        y += np.pi / 20.
        im.set_array(f(x, y))
        return im,

    ani = animation.FuncAnimation(fig, updatefig, interval=50, blit=True)
    plt.show()

p = Process(target=plot_graph)
p.start()
# Code here computes while the animation is running
for i in range(10):
    time.sleep(1)
    print('Something')

p.join()
print("Animation is over")
# Code here to be computed after animation is over

Надеюсь, это помогло! Вы можете найти более подробную информацию здесь: Есть ли способ отсоединить графики matplotlib, чтобы можно было продолжить вычисление?

Ура!:)