a_guest
a_guest

Reputation: 36329

Add delay between specific frames of a matplotlib animation

I want to create an animation in matplotlib using FuncAnimation. The animation contains various "stages" which I would like to separate (emphasize) by adding an extra delay to the interval between the two corresponding frames. Consider the following example code that draws five circles and the drawing of each two consecutive circles should be separated by 1 second:

import time
from matplotlib.animation import FuncAnimation
import matplotlib.pyplot as plt
import numpy as np

f, ax = plt.subplots()
ax.set_xlim([-5, 5])
ax.set_ylim([-5, 5])

radius = 1
dp = 2*np.pi / 50
circles = [[(radius, 0)]]
plots = ax.plot([radius], [0])

def update(frame):
    global radius

    if frame % 50 == 0:
        radius += 1
        circles.append([(radius, 0)])
        plots.extend(ax.plot([radius], [0]))
        # I want to add a delay here, i.e. before the drawing of a new circle starts.
        # This works for `plt.show()` however it doesn't when saving the animation.
        time.sleep(1)
    angle = (frame % 50) * dp
    circles[-1].append((radius * np.cos(angle), radius * np.sin(angle)))
    plots[-1].set_data(*zip(*circles[-1]))
    return plots[-1]

animation = FuncAnimation(f, update, frames=range(1, 251), interval=50, repeat=False)

### Uncomment one of the following options.
# animation.save('test.mp4', fps=20)
# with open('test.html', 'w') as fh:
#     fh.write(animation.to_html5_video())
# plt.show()

This works when playing the animation via plt.show() however it doesn't work when saving as .mp4 or HTML5 video. This makes sense since, according to the documentation, the FPS determines the frame delay for mp4 video and the interval parameter is used for HTML5 video. Then frames are just played one after another (ignoring any compute time as well).

So how can I add a delay that will be retained upon saving the animation?

Upvotes: 1

Views: 3239

Answers (2)

ImportanceOfBeingErnest
ImportanceOfBeingErnest

Reputation: 339705

You may use the frame argument to steer your animation. Essentially a pause after frame n is the same as showing the frame number n repeatedly until the pause ends. E.g. if you run an animation at a rate of 1 frame per second, and want 3 seconds pause after the second frame, you can supply

0, 1, 1, 1, 1, 2, 3, ....

as frames, such that the frame with number 1 is shown four times.

Applying that concept can be done as follows in your code.

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

f, ax = plt.subplots()
ax.set_xlim([-5, 5])
ax.set_ylim([-5, 5])

radius = 0
bu = 50
dp = 2*np.pi / bu
circles = [[(radius, 0)]]
plots = ax.plot([radius], [0])

def update(frame):
    global radius

    if frame % bu == 0:
        radius += 1
        circles.append([(radius, 0)])
        plots.extend(ax.plot([radius], [0]))

    angle = (frame % bu) * dp
    circles[-1].append((radius * np.cos(angle), radius * np.sin(angle)))
    plots[-1].set_data(*zip(*circles[-1]))
    return plots[-1]


interval = 50 # milliseconds
pause = int(1 * 1000 / interval)
cycles = 4
frames = []
for c in range(cycles):
    frames.extend([np.arange(c*bu, (c+1)*bu), np.ones(pause)*((c+1)*bu)])
frames = np.concatenate(frames)

animation = FuncAnimation(f, update, frames=frames, interval=50, repeat=False)

### Uncomment one of the following options.
# animation.save('test.mp4', fps=20)
# with open('test.html', 'w') as fh:
#     fh.write(animation.to_html5_video())
plt.show()

Upvotes: 1

jedwards
jedwards

Reputation: 30250

You should be able to use a generating function for your frames argument. For example:

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

INTERVAL = 50  # ms
HOLD_MS  = 1000
HOLD_COUNT = HOLD_MS // INTERVAL

def frame_generator():
    for frame in range(1, 251):
        # Yield the frame first
        yield frame
        # If we should "sleep" here, yield None HOLD_COUNT times
        if frame % 50 == 0:
            for _ in range(HOLD_COUNT):
                yield None


f, ax = plt.subplots()
ax.set_xlim([-5, 5])
ax.set_ylim([-5, 5])

radius = 1
dp = 2*np.pi / 50
circles = [[(radius, 0)]]
plots = ax.plot([radius], [0])

def update(frame):
    global radius

    if frame is None: return   #--------------------------------- Added

    if frame % 50 == 0:
        radius += 1
        circles.append([(radius, 0)])
        plots.extend(ax.plot([radius], [0]))
        #-------------------------------------------------------- sleep removed

    angle = (frame % 50) * dp
    circles[-1].append((radius * np.cos(angle), radius * np.sin(angle)))
    plots[-1].set_data(*zip(*circles[-1]))
    return plots[-1]

# Slightly changed
animation = FuncAnimation(f, update, frames=frame_generator(), interval=INTERVAL, repeat=False)
plt.show()

Should work.

print(list(frame_generator()))

May help clarify what's going on.

Upvotes: 3

Related Questions