Search code examples
pythonplotanimated-gifimshowmatplotlib-animation

How to create FuncAnimation (matplotlib) animation from plt.imshow and animate text in double for loop?


LONG POST: I know many posts are available on StackOverflow on a similar topic, but none worked for me so far. The animation is either not fast enough, or the text is animated (not how I want), but the plot isn't. Any help is appreciated.

Hello,

I am implementing self-organizing maps and would like to create a gif file of the changing weights and plot text i.e. title.

X, Y  = np.meshgrid(np.arange(w.shape[0]), np.arange(w.shape[1]))
pos = np.array((X,Y))
som = SelfOrganizingMap(grid_pos=pos)

data = np.random.uniform(0, 1, (100, 3))
num_dims = data.shape[-1]
n_neuron = 5*np.sqrt(data.shape[0])
dim = int(np.ceil(np.sqrt(n_neuron)))
w = np.random.uniform(np.min(data), np.max(data), (dim, dim, num_dims))
for step in range(max_steps):
    for t in range(len(data)):
        euclidean_distance = som.e_distance(weights = w, x = data[t])
        distance_node, bmu = som.winning_neuron(euclidean_distance)
        influence_radius = som.decay(distance_node)
        w += learning_rate * influence_radius[:, :, None] * (data[t] - w) #create gif of this

The parameters are as follows:

  • w.shape=(8,8,3)
  • max_steps = 200
  • som = SelfOrganizingMaps() i.e. an instance of the class
  • data.shape =(100,3) and len(data) = 100
  • learning_rate = 0.01

So far I have tried the following:


Try #1

@gif.frame
def animate(weight):
    plt.imshow(weight)
    plt.axis('off')

and then in the main code:

frames = []
gif.options.matplotlib["dpi"] = 300
for step in range(max_steps):
     for t in range(len(data)):
          euclidean_distance = som.e_distance(weights = w, x = data[t])
          distance_node, bmu = som.winning_neuron(euclidean_distance)
          influence_radius = som.decay(distance_node)
          w += learning_rate * influence_radius[:, :, None] * (data[t] - w)
          frames.append(animate(weight = w))

gif.save(frames, "color_som.gif", duration=15)

The execution is killed because of the memory issue.


Try #2:

def animate(weight):
   text = axes.text(0.5,1.05, 'Iteration: {}'.format(step), horizontalalignment='center', verticalalignment='bottom',transform=axes.transAxes)
   im = axes.imshow(weight)
   axes.axis('off')
   frames.append([im, text])
   return frames

and then in the main code:

plt.rcParams["font.family"] = "Times New Roman"
plt.rcParams.update({'font.size': 14})
fig, axes = plt.subplots()
for step in range(max_steps):
     for t in range(len(data)):
          euclidean_distance = som.e_distance(weights = w, x = data[t])
          distance_node, bmu = som.winning_neuron(euclidean_distance)
          influence_radius = som.decay(distance_node)
          w += learning_rate * influence_radius[:, :, None] * (data[t] - w)
          images = animate(weight = w)

ani = animation.ArtistAnimation(fig, images, interval=1, blit=False, repeat_delay=50)
ani.save("./color_som.gif", writer='pillow', fps=30)

This works fine if max_steps is set to lower values but is extremely slow if it is set to higher values. The execution time for max_steps = 50 is 250.62131214141846


Try #3:

# initialize elements to be animated

plt.rcParams["font.family"] = "Times New Roman"
plt.rcParams.update({'font.size': 14})
fig, axes = plt.subplots()

w_plot = axes.imshow(w)
axes.axis('off')

def update_data(t, lr, x, w, w_plot):

    euclidean_distance = som.e_distance(weights=w, x=x[t])
    distance_node, bmu = som.winning_neuron(euclidean_distance)
    influence_radius = som.decay(distance_node)
    w += lr * influence_radius[:, :, None] * (x[t] - w)
    w_plot.set_data(w)
    step_txt.set_text("step: {}".format(t))

step_txt = fig.text(0.5, 0.95, "step: 0", ha="center", weight="bold")

anim = FuncAnimation(fig, func = partial(update_data, lr = 0.1, x=data, w=w, w_plot= w_plot), frames= len(data), interval=300, repeat_delay=500)
anim.save("test.gif")

This works and is pretty fast (execution time=3.0469424724578857), but this is not what I want because:

  1. I want to separate the calculation from the animation. I want to have a for loop outside the update_data.
  2. The above code runs only for len(data) times, i.e. 100, instead of max_steps times, i.e. 200. I cannot set frames = max_steps in FuncAnimation; otherwise, it will give an error because of x[t]. So, it just executes the inner for loop i.e.

for t in range(len(data)):

I want animation with double for loop executed


Try #4

I also tried this "Creating matplotlib animation from plt.imshow in triple for loop",

# initialize elements to be animated

plt.rcParams["font.family"] = "Times New Roman"
plt.rcParams.update({'font.size': 14})
fig, axes = plt.subplots()
w_plot = axes.imshow(w)
step_txt = fig.text(0.5, 0.95, "step: 0", ha="center", weight="bold")

def update_data(t):
    w_plot.set_data(wei[t])
    step_txt.set_text("step: {}".format(t))

weight = []
for step in range(max_steps):
    for t in range(len(data)):
        euclidean_distance = som.e_distance(weights = w, x = data[t])
        distance_node, bmu = som.winning_neuron(euclidean_distance)
        influence_radius = som.decay(distance_node)
        w += learning_rate * influence_radius[:, :, None] * (data[t] - w)
        weight.append(w)

anim = FuncAnimation(fig, func = update_data, frames= len(data), interval=50, repeat_delay=500) #, max_steps= max_steps #len(data)
anim.save("test.gif")#, writer="pillow")

This separates the calculation from the animation and executes double "for" loop.

Also, when I save the animation as gif, the animation is on the text but not the plot itself. It takes only the final plot. Also, I want step text to change from 0,1,....499 (for max_steps = 500) instead of step = 0,1...99 (i.e. len(data))

I also tried celluloid package in python, it did not work for me.

How to get a gif image of the weights with both the for loop executed and text change to step values (i.e. outer loop value) instead of t value (i.e. inner for loop)?


Solution

  • So, the solution is:

    # Initialize elements to be animated
    
    fig, axes = plt.subplots()
    w_plot = axes.imshow(w)
    step_txt = fig.text(0.5, 0.95, "step: 0", ha="center", weight="bold")
    
    def update_data(t):
        w_plot.set_data(wei[t])
        if t%len(data)==0:
            step_txt.set_text("step: {}".format(np.divmod(t, len(data))[0]))
    
    weight = []
    for step in range(max_steps):
        for t in range(len(data)):
            euclidean_distance = som.e_distance(weights = w, x = data[t])
            distance_node, bmu = som.winning_neuron(euclidean_distance)
            influence_radius = som.decay(distance_node)
            w += learning_rate * influence_radius[:, :, None] * (data[t] - w)
            weight.append(w.tolist()
    
    weight = np.array(weight)
    anim = FuncAnimation(fig, func = update_data, frames= max_steps*len(data), interval=50, repeat_delay=500) #, max_steps= max_steps #len(data)
    anim.save("test.gif")
    

    The gif file introduces some static in the animation. For that, you can either specify fps or dpi or both, but it will increase the execution time, and if max_steps is high, then it will take a lot of time. Alternatively, one can save the animation as .mp4 file. It doesn't introduce any static in the animation.

    anim.save("test.mp4")