Редактор многоугольников Matplotlib для приложений с кросс-GUI

Beginner

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

Введение

Этот практикум представляет собой пошаговое руководство по созданию кросс-приложений с графическим интерфейсом, использующих обработку событий Matplotlib для взаимодействия с объектами на холсте. Мы создадим редактор многоугольников, который позволит вам перемещать, удалять и вставлять вершины.

Советы по работе с ВМ

После запуска ВМ перейдите в левый верхний угол и переключитесь на вкладку Notebook, чтобы приступить к практике с использованием 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

Нам нужно создать класс 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)
        ## здесь не нужно использовать blit, это будет срабатывать перед обновлением экрана

    def poly_changed(self, poly):
        """Этот метод вызывается при каждом вызове объекта pathpatch."""
        ## копируем только свойства художника в линию (за исключением видимости)
        vis = self.line.get_visible()
        Artist.update_from(self.line, poly)
        self.line.set_visible(vis)  ## не используем состояние видимости многоугольника

    def get_ind_under_point(self, event):
        """
        Возвращает индекс точки, ближайшей к позиции события, или *None*,
        если нет точки в пределах ``self.epsilon`` от позиции события.
        """
        ## координаты отображения
        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.