Skip to content
Advertisement

2 Interactive Poly Editor with matplotlib Not Working

Following a tutorial Poly Editor I’ve created 2 polygons, added to the figure, and then instantiated 2 interactive polygons. I expected that when I adjusted the individual polygon it would work separately. However they were conflicting with each other.

Then I have tried some changes in the original code yet still not produce a successful result. If you’re familiar I would like to have your help. I really appreciate in advance.

Here is my following adjusted code:

"""
===========
Poly Editor
===========

This is an example to show how to build cross-GUI applications using
Matplotlib event handling to interact with objects on the canvas.
"""
import numpy as np
from matplotlib.lines import Line2D
from matplotlib.artist import Artist


def dist(x, y):
"""
Return the distance between two points.
"""
d = x - y
return np.sqrt(np.dot(d, d))


def dist_point_to_segment(p, s0, s1):
"""
Get the distance of a point to a segment.
  *p*, *s0*, *s1* are *xy* sequences
This algorithm from
http://geomalgorithms.com/a02-_lines.html
"""
v = s1 - s0
w = p - s0
c1 = np.dot(w, v)
if c1 <= 0:
    return dist(p, s0)
c2 = np.dot(v, v)
if c2 <= c1:
    return dist(p, s1)
b = c1 / c2
pb = s0 + b * v
return dist(p, pb)


class PolygonInteractor:
"""
A polygon editor.

Key-bindings

  't' toggle vertex markers on and off.  When vertex markers are on,
      you can move them, delete them

  'd' delete the vertex under point

  'i' insert a vertex at point.  You must be within epsilon of the
      line connecting two existing vertices

"""

showverts = True
epsilon = 5  # max pixel distance to count as a vertex hit

def __init__(self, ax, poly):
  if poly.figure is None:
      raise RuntimeError('You must first add the polygon to a figure '
                         'or canvas before defining the interactor')
  self.ax = ax
  self.poly = poly

  x, y = zip(*self.poly.xy)
  self.line = Line2D(x, y,
                     marker='o', markerfacecolor='r',
                     animated=True)
  self.ax.add_line(self.line)
  self._ind = None  # the active vert

def connect(self):
  'connect to all the events we need'
  self.cid         = self.poly.add_callback(self.poly_changed)
  self.ciddraw     = self.poly.figure.canvas.mpl_connect('draw_event', self.on_draw)
  self.cidpress    = self.poly.figure.canvas.mpl_connect('button_press_event', self.on_button_press)
  self.cidrelease  = self.poly.figure.canvas.mpl_connect('button_release_event', self.on_button_release)
  self.cidkeypress = self.poly.figure.canvas.mpl_connect('key_press_event', self.on_key_press)
  self.cidmotion   = self.poly.figure.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)

def on_draw(self, event):
  self.background = self.poly.figure.canvas.copy_from_bbox(self.poly.figure.bbox)
  self.ax.draw_artist(self.poly)
  self.ax.draw_artist(self.line)
  # do not need to blit here, this will fire before the screen is
  # updated

def poly_changed(self, poly):
  """This method is called whenever the pathpatch object is called."""
  # only copy the artist props to the line (except visibility)
  vis = self.line.get_visible()
  Artist.update_from(self.line, poly)
  self.line.set_visible(vis)  # don't use the poly visibility state

def get_ind_under_point(self, event):
  """
  Return the index of the point closest to the event position or *None*
  if no point is within ``self.epsilon`` to the event position.
  """
  # display coords
  xy = np.asarray(self.poly.xy)
  xyt = self.poly.get_transform().transform(xy)
  xt, yt = xyt[:, 0], xyt[:, 1]
  d = np.hypot(xt - event.x, yt - event.y)
  indseq, = np.nonzero(d == d.min())
  ind = indseq[0]

  if d[ind] >= self.epsilon:
      ind = None

  return ind

def on_button_press(self, event):
  """Callback for mouse button presses."""
  if not self.showverts:
      return
  if event.inaxes is None:
      return
  if event.button != 1:
      return
  
  self._ind = self.get_ind_under_point(event)

def on_button_release(self, event):
  """Callback for mouse button releases."""
  if not self.showverts:
      return
  if event.button != 1:
      return
  
  self._ind = None

def on_key_press(self, event):
  """Callback for key presses."""
  if not event.inaxes:
      return
  if event.key == 't':
      self.showverts = not self.showverts
      self.line.set_visible(self.showverts)
      if not self.showverts:
          self._ind = None
  elif event.key == 'd':
      ind = self.get_ind_under_point(event)
      if ind is not None:
          self.poly.xy = np.delete(self.poly.xy,
                                   ind, axis=0)
          self.line.set_data(zip(*self.poly.xy))
  elif event.key == 'i':
      xys = self.poly.get_transform().transform(self.poly.xy)
      p = event.x, event.y  # display coords
      for i in range(len(xys) - 1):
          s0 = xys[i]
          s1 = xys[i + 1]
          d = dist_point_to_segment(p, s0, s1)
          if d <= self.epsilon:
              self.poly.xy = np.insert(
                  self.poly.xy, i+1,
                  [event.xdata, event.ydata],
                  axis=0)
              self.line.set_data(zip(*self.poly.xy))
              break
  if self.line.stale:
      self.poly.figure.canvas.draw_idle()

def on_mouse_move(self, event):
  """Callback for mouse movements."""
  if not self.showverts:
      return
  if self._ind is None:
      return
  if event.inaxes is None:
      return
  if event.button != 1:
      return
  
  x, y = event.xdata, event.ydata

  self.poly.xy[self._ind] = x, y
  if self._ind == 0:
      self.poly.xy[-1] = x, y
  elif self._ind == len(self.poly.xy) - 1:
      self.poly.xy[0] = x, y
  self.line.set_data(zip(*self.poly.xy))

  self.poly.figure.canvas.restore_region(self.background)
  self.poly.figure.draw_artist(self.poly)
  self.poly.figure.draw_artist(self.line)
  self.poly.figure.canvas.blit(self.poly.figure.bbox)

def disconnect(self):
  'disconnect all the stored connection ids'
  self.poly.figure.canvas.mpl_disconnect(self.ciddraw)
  self.poly.figure.canvas.mpl_disconnect(self.cidpress)
  self.poly.figure.canvas.mpl_disconnect(self.cidrelease)
  self.poly.figure.canvas.mpl_disconnect(self.cidmotion)
  self.poly.figure.canvas.mpl_disconnect(self.cidkeypress)

if __name__ == '__main__':
  import matplotlib.pyplot as plt
  from matplotlib.patches import Polygon
  from matplotlib.collections import PatchCollection
  import matplotlib


# Fixing random state for reproducibility
np.random.seed(19680801)

fig, ax = plt.subplots()

N, LP = (4, 3)
poly_4_pts = Polygon(np.random.rand(N, 2), True, fill=None, animated=True)
poly_2_pts = Polygon(np.random.rand(LP, 2), True, fill=None, animated=True)

ax.add_patch(poly_4_pts)
fig.patches.extend([poly_2_pts])
ax.add_patch(poly_2_pts)

poly2 = PolygonInteractor(ax, poly_2_pts)
poly2.connect()

poly1 = PolygonInteractor(ax, poly_4_pts)
poly1.connect()

ax.set_title('Click and drag a point to move it')
ax.set_xlim((-2, 2))
ax.set_ylim((-2, 2))

plt.show()

Advertisement

Answer

I think that the problem is that you don’t restore the animated artists when restoring backgronds. See tutorial on blitting here. I could get it working adding some modifications to restore all the animated object added to the figure, since also the contour lines are animated you should declare them in the main and not in the class then restore all animated inside the class. Here’s my code and result:

"""
===========
Poly Editor
===========

This is an example to show how to build cross-GUI applications using
Matplotlib event handling to interact with objects on the canvas.
"""
import numpy as np
from matplotlib.lines import Line2D
from matplotlib.artist import Artist


def dist(x, y):
    """
    Return the distance between two points.
    """
    d = x - y
    return np.sqrt(np.dot(d, d))


def dist_point_to_segment(p, s0, s1):
    """
    Get the distance of a point to a segment.
      *p*, *s0*, *s1* are *xy* sequences
    This algorithm from
    http://geomalgorithms.com/a02-_lines.html
    """
    v = s1 - s0
    w = p - s0
    c1 = np.dot(w, v)
    if c1 <= 0:
        return dist(p, s0)
    c2 = np.dot(v, v)
    if c2 <= c1:
        return dist(p, s1)
    b = c1 / c2
    pb = s0 + b * v
    return dist(p, pb)


class PolygonInteractor:
    """
    A polygon editor.

    Key-bindings

      't' toggle vertex markers on and off.  When vertex markers are on,
          you can move them, delete them

      'd' delete the vertex under point

      'i' insert a vertex at point.  You must be within epsilon of the
          line connecting two existing vertices

    """

    showverts = True
    epsilon = 5  # max pixel distance to count as a vertex hit

    def __init__(self, ax, poly, contour, animated_artists=()):
        if poly.figure is None:
            raise RuntimeError('You must first add the polygon to a figure '
                               'or canvas before defining the interactor')
        self.ax = ax
        canvas = poly.figure.canvas
        self.poly = poly

        self.line = contour

        self._ind = None  # the active vert

        canvas.mpl_connect('draw_event', self.on_draw)
        canvas.mpl_connect('button_press_event', self.on_button_press)
        canvas.mpl_connect('key_press_event', self.on_key_press)
        canvas.mpl_connect('button_release_event', self.on_button_release)
        canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
        self.canvas = canvas

        self._artists = []
        for a in animated_artists:
            self.add_artist(a)
        #self.add_artist(self.line)

    def add_artist(self, art):
        if art.figure != self.canvas.figure:
            raise RuntimeError
        art.set_animated(True)
        self._artists.append(art)

    def on_draw(self, event):
        self.background = self.canvas.copy_from_bbox(self.ax.bbox)
        self.ax.draw_artist(self.poly)
        self.ax.draw_artist(self.line)
        self._draw_animated()
        self.canvas.blit(self.ax.bbox)
        # do not need to blit here, this will fire before the screen is
        # updated

    def _draw_animated(self):
        """Draw all of the animated artists."""
        fig = self.canvas.figure
        for a in self._artists:
            fig.draw_artist(a)

    def get_ind_under_point(self, event):
        """
        Return the index of the point closest to the event position or *None*
        if no point is within ``self.epsilon`` to the event position.
        """
        # display coords
        xy = np.asarray(self.poly.xy)
        xyt = self.poly.get_transform().transform(xy)
        xt, yt = xyt[:, 0], xyt[:, 1]
        d = np.hypot(xt - event.x, yt - event.y)
        indseq, = np.nonzero(d == d.min())
        ind = indseq[0]

        if d[ind] >= self.epsilon:
            ind = None

        return ind

    def on_button_press(self, event):
        """Callback for mouse button presses."""
        if not self.showverts:
            return
        if event.inaxes is None:
            return
        if event.button != 1:
            return
        self._ind = self.get_ind_under_point(event)

    def on_button_release(self, event):
        """Callback for mouse button releases."""
        if not self.showverts:
            return
        if event.button != 1:
            return
        self._ind = None

    def on_key_press(self, event):
        """Callback for key presses."""
        if not event.inaxes:
            return
        if event.key == 't':
            self.showverts = not self.showverts
            self.line.set_visible(self.showverts)
            if not self.showverts:
                self._ind = None
        elif event.key == 'd':
            ind = self.get_ind_under_point(event)
            if ind is not None:
                self.poly.xy = np.delete(self.poly.xy,
                                         ind, axis=0)
                self.line.set_data(zip(*self.poly.xy))
        elif event.key == 'i':
            xys = self.poly.get_transform().transform(self.poly.xy)
            p = event.x, event.y  # display coords
            for i in range(len(xys) - 1):
                s0 = xys[i]
                s1 = xys[i + 1]
                d = dist_point_to_segment(p, s0, s1)
                if d <= self.epsilon:
                    self.poly.xy = np.insert(
                        self.poly.xy, i+1,
                        [event.xdata, event.ydata],
                        axis=0)
                    self.line.set_data(zip(*self.poly.xy))
                    break
        if self.line.stale:
            self.canvas.draw_idle()

    def on_mouse_move(self, event):
        """Callback for mouse movements."""
        if not self.showverts:
            return
        if self._ind is None:
            return
        if event.inaxes is None:
            return
        if event.button != 1:
            return
        x, y = event.xdata, event.ydata

        self.poly.xy[self._ind] = x, y
        if self._ind == 0:
            self.poly.xy[-1] = x, y
        elif self._ind == len(self.poly.xy) - 1:
            self.poly.xy[0] = x, y
        self.line.set_data(zip(*self.poly.xy))

        self.canvas.restore_region(self.background)
        self._draw_animated()
        self.canvas.draw_idle()
        # self.ax.draw_artist(self.poly)
        # self.ax.draw_artist(self.line)
        self.canvas.blit(self.ax.bbox)


if __name__ == '__main__':
    import matplotlib.pyplot as plt
    from matplotlib.patches import Polygon

    theta = np.arange(0, 2*np.pi, 0.1)
    r = 1.5

    xs = r * np.cos(theta)
    ys = r * np.sin(theta)

    poly = Polygon(np.column_stack([xs, ys]), animated=True)
    xc1, yc1 = zip(*poly.xy)
    poly_contour = Line2D(xc1, yc1, marker='o', markerfacecolor='r',
                          animated=True)

    poly2 = Polygon(np.column_stack([xs+3, ys+3]), animated=True)
    xc2, yc2 = zip(*poly2.xy)
    poly_contour2 = Line2D(xc2, yc2, marker='o', markerfacecolor='r',
                          animated=True)

    fig, ax = plt.subplots()
    ax.add_patch(poly)
    ax.add_line(poly_contour)
    ax.add_patch(poly2)
    ax.add_line(poly_contour2)
    p = PolygonInteractor(ax, poly, poly_contour, [poly, poly2, poly_contour, poly_contour2])
    p2 = PolygonInteractor(ax, poly2, poly_contour2, [poly, poly2, poly_contour, poly_contour2])

    ax.set_title('Click and drag a point to move it')
    ax.set_xlim((-2, 6))
    ax.set_ylim((-2, 6))
    plt.show()

2poly-interactive

User contributions licensed under: CC BY-SA
9 People found this is helpful
Advertisement