适用于跨 GUI 应用的 Matplotlib 多边形编辑器

Beginner

This tutorial is from open-source community. Access the source code

简介

本实验是一个循序渐进的教程,介绍如何使用 Matplotlib 事件处理来构建跨 GUI 应用程序,以便与画布上的对象进行交互。我们将创建一个多边形编辑器,允许你移动、删除和插入顶点。

虚拟机提示

虚拟机启动完成后,点击左上角切换到“笔记本”标签,以访问 Jupyter Notebook 进行练习。

有时,你可能需要等待几秒钟,以便 Jupyter Notebook 完成加载。由于 Jupyter Notebook 的限制,操作验证无法自动化。

如果你在学习过程中遇到问题,请随时向 Labby 提问。课程结束后提供反馈,我们将立即为你解决问题。

导入所需库

我们需要为多边形编辑器导入必要的库。

import numpy as np
from matplotlib.artist import Artist
from matplotlib.lines import Line2D

创建一个计算距离的函数

我们需要创建一个函数,用于计算点与线段之间的距离。该函数稍后将用于确定是否应向多边形添加新顶点。

def dist_point_to_segment(p, s0, s1):
    """
    获取点 *p* 到线段 (*s0*, *s1*) 的距离,其中
    *p*, *s0*, *s1* 是 ``[x, y]`` 数组。
    """
    s01 = s1 - s0
    s0p = p - s0
    if (s01 == 0).all():
        return np.hypot(*s0p)
    ## 投影到线段上,不超过线段端点。
    p1 = s0 + np.clip((s0p @ s01) / (s01 @ s01), 0, 1) * s01
    return np.hypot(*(p - p1))

创建多边形交互器类

我们需要创建 PolygonInteractor 类,它是多边形编辑器的主类。这个类将处理与多边形的所有交互,比如移动、删除和插入顶点。

class PolygonInteractor:
    """
    一个多边形编辑器。

    按键绑定

      't' 切换顶点标记的显示与隐藏。当顶点标记显示时,
          你可以移动它们、删除它们

      'd' 删除点下方的顶点

      'i' 在点处插入一个顶点。你必须位于连接两个现有顶点的直线的
          容差范围内
    """

    showverts = True
    epsilon = 5  ## 计为顶点命中的最大像素距离

    def __init__(self, ax, poly):
        if poly.figure is None:
            raise RuntimeError('在定义交互器之前,你必须先将多边形添加到图形 '
                               '或画布上')
        self.ax = ax
        canvas = poly.figure.canvas
        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.cid = self.poly.add_callback(self.poly_changed)
        self._ind = None  ## 活动顶点

        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

    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)
        ## 这里不需要进行快速重绘,因为这将在屏幕更新之前触发

    def poly_changed(self, poly):
        """每当路径补丁对象被调用时,就会调用这个方法。"""
        ## 仅将艺术家属性复制到线条上(除了可见性)
        vis = self.line.get_visible()
        Artist.update_from(self.line, poly)
        self.line.set_visible(vis)  ## 不使用多边形的可见性状态

    def get_ind_under_point(self, event):
        """
        返回最接近事件位置的点的索引,如果没有点在距离事件位置
        ``self.epsilon`` 范围内,则返回 *None*。
        """
        ## 显示坐标
        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):
        """鼠标按钮按下的回调函数。"""
        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):
        """鼠标按钮释放的回调函数。"""
        if not self.showverts:
            return
        if event.button!= 1:
            return
        self._ind = None

    def on_key_press(self, event):
        """按键按下的回调函数。"""
        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  ## 显示坐标
            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):
        """鼠标移动的回调函数。"""
        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.ax.draw_artist(self.poly)
        self.ax.draw_artist(self.line)
        self.canvas.blit(self.ax.bbox)

创建多边形

我们需要使用 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)

创建绘图

我们需要创建绘图并将多边形添加到其中。

fig, ax = plt.subplots()
ax.add_patch(poly)
p = PolygonInteractor(ax, poly)

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

总结

在这个实验中,我们学习了如何使用 Matplotlib 事件处理来创建一个多边形编辑器。我们创建了一个类来处理与多边形的所有交互,并使用 Polygon 类来创建多边形本身。然后我们创建了绘图并将多边形添加到其中。有了这些知识,你可以使用 Matplotlib 创建自己的交互式应用程序。