I want to create an animation with matplotlib to monitor the convergence of a clustering algorithm. It should draw a scatterplot of my data when called the first time and draw error ellipses each time the plot is updated. I am trying to use canvas_copy_from_bbox()
and restore_region()
to save the scatterplot and then draw a new set of ellipses whenever I'm updating the plot.
However, the code just plots the new ellipses on top of the old ones, without clearing the previous plot first.
I suspect, somehow this approach doesn't work well with the Ellipse()
and add_path()
commands, but I don't know how to fix this.
Here is the code:
import wx
import math
from math import pi
from matplotlib.patches import Ellipse
from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import \
FigureCanvasWxAgg as FigureCanvas
TIMER_ID = wx.NewId()
class _MonitorPlot(wx.Frame):
def __init__(self, data, scale=1):
self.scale = scale
wx.Frame.__init__(self, None, wx.ID_ANY,
title="FlowVB Progress Monitor", size=(800, 600))
self.fig = Figure((8, 6), 100)
self.canvas = FigureCanvas(self, wx.ID_ANY, self.fig)
self.ax = self.fig.add_subplot(111)
x_lims = [data[:, 0].min(), data[:, 0].max()]
y_lims = [data[:, 1].min(), data[:, 1].max()]
self.ax.set_xlim(x_lims)
self.ax.set_ylim(y_lims)
self.ax.set_autoscale_on(False)
self.l_data = self.ax.plot(data[:, 0], data[:, 1], color='blue',
linestyle='', marker='o')
self.canvas.draw()
self.bg = self.canvas.copy_from_bbox(self.ax.bbox)
self.Bind(wx.EVT_IDLE, self._onIdle)
def update_plot(self, pos, cov):
self.canvas.restore_region(self.bg)
for k in range(pos.shape[0]):
l_center, = self.ax.plot(pos[k, 0], pos[k, 1],
color='red', marker='+')
U, s, Vh = np.linalg.svd(cov[k, :, :])
orient = math.atan2(U[1, 0], U[0, 0]) * 180 / pi
ellipsePlot = Ellipse(xy=pos[k, :], width=2.0 * math.sqrt(s[0]),
height=2.0 * math.sqrt(s[1]),
angle=orient, facecolor='none',
edgecolor='red')
self.ax.add_patch(ellipsePlot)
self.canvas.draw()
self.canvas.blit(self.ax.bbox)
What's happening is that you're adding new patches to the plot each time, and then drawing all of them when you call self.canvas.draw()
.
The quickest fix is to just call self.canvas.draw_artist(ellipsePlot)
after adding each patch and remove the call to self.canvas.draw()
As a simple, stand-alone example:
# Animates 3 ellipses overlain on a scatterplot
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
import numpy as np
num = 10
x = np.random.random(num)
y = np.random.random(num)
plt.ion()
fig = plt.figure()
ax = fig.add_subplot(111)
line = ax.plot(x, y, 'bo')
fig.canvas.draw()
bg = fig.canvas.copy_from_bbox(ax.bbox)
# Pseudo-main loop
for i in range(100):
fig.canvas.restore_region(bg)
# Make a new ellipse each time... (inefficient!)
for i in range(3):
width, height, angle = np.random.random(3)
angle *= 180
ellip = Ellipse(xy=(0.5, 0.5), width=width, height=height,
facecolor='red', angle=angle, alpha=0.5)
ax.add_patch(ellip)
ax.draw_artist(ellip)
fig.canvas.blit(ax.bbox)
However, this will probably cause memory consumptions problems over time, as the axes object will keep track of all artists added to it. If your axes doesn't hang around for a long time, this may be negligible, but you should at least be aware that it will cause a memory leak. One way around this is to remove the ellipsis artists from the axes by calling ax.remove(ellipsePlot)
for each ellipse after drawing them. However, this is still slightly inefficient, as you're constantly creating and destroying ellipse artists, when you could just update them. (Creating and destroying them doesn't have much overhead at all, though, it's mostly a stylistic issue...)
If the number of ellipses is staying the same over time it's better and easier to just update the properties of each ellipse artist object instead of creating and adding new ones. This will avoid have remove "old" ellipses from the axes, as only the number that you need will ever exist.
As a simple, stand-alone example of this:
# Animates 3 ellipses overlain on a scatterplot
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
import numpy as np
num = 10
x = np.random.random(num)
y = np.random.random(num)
plt.ion()
fig = plt.figure()
ax = fig.add_subplot(111)
line = ax.plot(x, y, 'bo')
fig.canvas.draw()
bg = fig.canvas.copy_from_bbox(ax.bbox)
# Make and add the ellipses the first time (won't ever be drawn)
ellipses = []
for i in range(3):
ellip = Ellipse(xy=(0.5, 0.5), width=1, height=1,
facecolor='red', alpha=0.5)
ax.add_patch(ellip)
ellipses.append(ellip)
# Pseudo-main loop
for i in range(100):
fig.canvas.restore_region(bg)
# Update the ellipse artists...
for ellip in ellipses:
ellip.width, ellip.height, ellip.angle = np.random.random(3)
ellip.angle *= 180
ax.draw_artist(ellip)
fig.canvas.blit(ax.bbox)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With