6

Consider the following code directly taken from the Matplotlib documentation:

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

This work fine on my system. Now, try to append the following piece of code to the above code:

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

What happens is that the program freezes. Apparently, the "Animation" class of Matplotlib runs the animation in a separate thread. So I have the 2 following questions:

1) If the process runs in a separate thread, why is it disturbed by the subsequent loop ?

2) How to say to python to wait until the animation has ended ?

MikeTeX
  • 529
  • 4
  • 18
  • Possible Duplicate: http://stackoverflow.com/questions/458209/is-there-a-way-to-detach-matplotlib-plots-so-that-the-computation-can-continue#458295 – oxalorg Dec 23 '15 at 11:09
  • I see no duplicate here, at most some far hints. Could you explain where is the answer to my questions in the thread you've cited ? – MikeTeX Dec 23 '15 at 12:05

3 Answers3

3

For me, copying into ipython works as expected (animation plays first then the infinite loop) but when running the script it freezes.

1) I'm not sure exactly how cpython handles multi-threading, especially when combined with a particular matplotlib backend but it seems to be failing here. One possibility is to be explicit about how you want to use threads, by using

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

Which defines two threads and sets one to work on animation, one to loop.

2) To fix this, as suggested by @Mitesh Shah, you can use plt.show(block=True). For me, the script then behaves as expected with animation and then loop. Full code:

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: Alternative is to simply use interactive mode,

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)
Ed Smith
  • 12,716
  • 2
  • 43
  • 55
  • Thank you for answering me. I have tried your second solution on my leptop and it did not work: the loop is immediately executed, and the figure freezes. Regarding your first solution, I don't understand it (where and how is the function "worker" used ?) – MikeTeX Dec 23 '15 at 13:46
  • Strange 2) did not work, may be because of a difference in backend or version of `matplotlib` (I'm using `1.4.3` with `Qt4Agg` backend) With `Agg` backend for example, 2) also behaves as you describe for me. Maybe try changing backend (`plt.switch_backend("Qt4Agg")`. The first solution uses multi-threading, defining two threads each calling worker which assigns tasks based on their number. See example here (https://pymotw.com/2/threading/) – Ed Smith Dec 23 '15 at 13:56
  • With the Qt4Agg backend, the animation actually occurs, but is executed after the loop (use a for loop and time.sleep(1) inside the loop to visualize this effect). Note: Even if the animation were executed in parallel with the loop (which would be still useful for me), this would not really be the answer to the question, because I asked how to block the execution of the program following the animation until it has ended. – MikeTeX Dec 23 '15 at 14:11
  • Blocking should be automatic with `plt.show()` until the end of the animation. This works for me so difficult to help further with your particular implementation/backend. In my experience, animations in matplotlib do not play well with anything else in a script beyond simple examples. Another option is to avoid animation and use interactive mode with `plt.ion()`, I'll add an example. Note you need to pause in order to get the redraw to update the figure, which may be why sleep works for you. – Ed Smith Dec 23 '15 at 15:13
  • Yes, of course, this last solution works. It simply don't use the Animation class and builds its own animation from crash. I have a last question: if no loop is typed in my example above, the animation runs endlessly on my system. So, how is it possible that you've got the animation end and pass to the loop after show(block=True) in your second solution above ? – MikeTeX Dec 23 '15 at 15:23
  • You're right, goes endlessly (as `plt.show` blocks), then when you close the figure it moves to the loop. You can close the animation in the script after a number of loops with something like `if args == 100: plt.close()` or set max number of frames (which also doesn't seem to work as expected for me)... – Ed Smith Dec 23 '15 at 16:11
3

Thanks to the help of Ed Smith and MiteshNinja, I have finally succeeded in finding a robust method that works not only with the Ipython console, but also with the Python console and the command line. Furthermore, it allows total control on the animation process. Code is self explanatory.

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()
MikeTeX
  • 529
  • 4
  • 18
2

We can run the animation function in a separate thread. Then start that thread. Once a new thread is created, the execution will continue.
We then use p.join() to wait for our previously created thread to finish execution. As soon as the execution finished (or terminates for some reason) the code will continue further.

Also matplotlib works differently in Interactive Python shells vs. system command line shells, the below code should work for both these scenarios:

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

I hope this helped! You can find more information here: Is there a way to detach matplotlib plots so that the computation can continue?

Cheers! :)

Community
  • 1
  • 1
oxalorg
  • 2,768
  • 1
  • 16
  • 27
  • I have tried your code in my leptop (Windows 7). The loop is executed but the movie freezes until the end of the loop, so no animation can be seen. But what is more surprising for me is that the command p.join() stops the execution of the animation, while according to the doc, it should stop the execution of the subsequent code until the thread end. Can you explain that ? – MikeTeX Dec 23 '15 at 13:59
  • @MikeTeX May I ask 'how' are you running the script? iPython/cmd/idle etc? Take a look at this, it might help: http://matplotlib.org/users/shell.html – oxalorg Dec 23 '15 at 14:24
  • I am running the script from the IPython console of spyder 2.3.8 (a recent version), coming with anaconda. I will read the doc you've provided. – MikeTeX Dec 23 '15 at 15:05
  • I tried to run this code, but my animation never finishes and than the program hangs on p.join(). I mean, the animation is over but I guess the multiprocessing module still believes the thread to be running and the join never happens. What to do? – Nima Mousavi Jan 30 '18 at 13:24