Python 在matplotlib中交互变形多边形

Python 在matplotlib中交互变形多边形,python,matplotlib,Python,Matplotlib,我想在Python中实现类似MATLAB的drawfreehand 我有一个类似于下面的东西,用户画了一个多边形,然后用Shapely简化,然后用一个交互式样条拟合它。问题是样条曲线拟合通常会超出原始多边形的限制。我的代码如下: from matplotlib.patches import Polygon from matplotlib.widgets import AxesWidget from matplotlib.lines import Line2D import matplotlib

我想在Python中实现类似MATLAB的
drawfreehand

我有一个类似于下面的东西,用户画了一个多边形,然后用Shapely简化,然后用一个交互式样条拟合它。问题是样条曲线拟合通常会超出原始多边形的限制。我的代码如下:

from matplotlib.patches import Polygon
from matplotlib.widgets import AxesWidget
from matplotlib.lines import Line2D
import matplotlib as mpl
from matplotlib import path
import matplotlib.pyplot as plt
from scipy import interpolate
from shapely.geometry import LinearRing, Polygon as shapelyPolygon
import numpy as np
import copy

class AxManager:
    def __init__(self, ax):
        self.artists = []
        self.ax = ax
        self.canvas = self.ax.figure.canvas
        self.useblit = self.canvas.supports_blit
        self.canvas.mpl_connect('draw_event', self.update_background)
        self.background = None


    def update(self):
        """draw using newfangled blit or oldfangled draw depending on
        useblit
        """
        if not self.ax.get_visible():
            return False
        if self.useblit:
            if self.background is not None:
                self.canvas.restore_region(self.background)
            for artist in self.artists:
                if artist.get_visible():
                    self.ax.draw_artist(artist)
            self.canvas.blit(self.ax.bbox)
        else:
            self.canvas.draw_idle()
        return False
    def draw(self):
        self.canvas.draw_idle()

    def update_background(self, event):
        """force an update of the background"""
        # If you add a call to `ignore` here, you'll want to check edge case:
        # `release` can call a draw event even when `ignore` is True.
        if self.useblit:
            self.background = self.canvas.copy_from_bbox(self.ax.bbox)


class mySelectorWidget(AxesWidget):
    def __init__(self, axMan, button=None, state_modifier_keys=None):
        AxesWidget.__init__(self, axMan.ax)

        self.visible = True
        self.axMan = axMan
        self.artists=[]
        self.connect_event('motion_notify_event', self.onmove)
        self.connect_event('button_press_event', self.press)
        self.connect_event('button_release_event', self.release)
        self.connect_event('key_press_event', self.on_key_press)
        self.connect_event('key_release_event', self.on_key_release)
        self.connect_event('scroll_event', self.on_scroll)

        self.state_modifier_keys = dict(move=' ', clear='escape',
                                        square='shift', center='control')
        self.state_modifier_keys.update(state_modifier_keys or {})

        if isinstance(button, int):
            self.validButtons = [button]
        else:
            self.validButtons = button

        # will save the data (position at mouseclick)
        self.eventpress = None
        # will save the data (pos. at mouserelease)
        self.eventrelease = None
        self._prev_event = None
        self.state = set()

    def set_active(self, active):
        AxesWidget.set_active(self, active)
        if active:
            self.axMan.update_background(None)

    def ignore(self, event):
        """return *True* if *event* should be ignored"""
        if not self.active or not self.ax.get_visible():
            return True

        # If canvas was locked
        if not self.canvas.widgetlock.available(self):
            return True

        if not hasattr(event, 'button'):
            event.button = None

        # Only do rectangle selection if event was triggered
        # with a desired button
        if self.validButtons is not None:
            if event.button not in self.validButtons:
                return True

        # If no button was pressed yet ignore the event if it was out
        # of the axes
        if self.eventpress is None:
            return event.inaxes != self.ax

        # If a button was pressed, check if the release-button is the
        # same.
        if event.button == self.eventpress.button:
            return False

        # If a button was pressed, check if the release-button is the
        # same.
        return (event.inaxes != self.ax or
                event.button != self.eventpress.button)

    def _get_data(self, event):
        """Get the xdata and ydata for event, with limits"""
        if event.xdata is None:
            return None, None
        x0, x1 = self.ax.get_xbound()
        y0, y1 = self.ax.get_ybound()
        xdata = max(x0, event.xdata)
        xdata = min(x1, xdata)
        ydata = max(y0, event.ydata)
        ydata = min(y1, ydata)
        return xdata, ydata

    def _clean_event(self, event):
        """Clean up an event

        Use prev event if there is no xdata
        Limit the xdata and ydata to the axes limits
        Set the prev event
        """
        if event.xdata is None:
            event = self._prev_event
        else:
            event = copy.copy(event)
        event.xdata, event.ydata = self._get_data(event)

        self._prev_event = event
        return event

    def press(self, event):
        """Button press handler and validator"""
        if not self.ignore(event):
            event = self._clean_event(event)
            self.eventpress = event
            self._prev_event = event
            key = event.key or ''
            key = key.replace('ctrl', 'control')
            # move state is locked in on a button press
            if key == self.state_modifier_keys['move']:
                self.state.add('move')
            self._press(event)
            return True
        return False

    def _press(self, event):
        """Button press handler"""
        pass

    def release(self, event):
        """Button release event handler and validator"""
        if not self.ignore(event) and self.eventpress:
            event = self._clean_event(event)
            self.eventrelease = event
            self._release(event)
            self.eventpress = None
            self.eventrelease = None
            self.state.discard('move')
            return True
        return False

    def _release(self, event):
        """Button release event handler"""
        pass

    def onmove(self, event):
        """Cursor move event handler and validator"""
        if not self.ignore(event):
            event = self._clean_event(event)
            if self.eventpress:
                self._ondrag(event)
            else:
                self._onhover(event)
            return True
        return False

    def _ondrag(self, event):
        """Cursor move event handler"""
        pass
    def _onhover(self, event):
        pass

    def on_scroll(self, event):
        """Mouse scroll event handler and validator"""
        if not self.ignore(event):
            self._on_scroll(event)

    def _on_scroll(self, event):
        """Mouse scroll event handler"""
        pass

    def on_key_press(self, event):
        """Key press event handler and validator for all selection widgets"""
        if self.active:
            key = event.key or ''
            key = key.replace('ctrl', 'control')
            if key == self.state_modifier_keys['clear']:
                for artist in self.artists:
                    artist.set_visible(False)
                self.update()
                return
            for (state, modifier) in self.state_modifier_keys.items():
                if modifier in key:
                    self.state.add(state)
            self._on_key_press(event)

    def _on_key_press(self, event):
        """Key press event handler - use for widget-specific key press actions.
        """
        pass

    def on_key_release(self, event):
        """Key release event handler and validator"""
        if self.active:
            key = event.key or ''
            for (state, modifier) in self.state_modifier_keys.items():
                if modifier in key:
                    self.state.discard(state)
            self._on_key_release(event)

    def _on_key_release(self, event):
        """Key release event handler"""
        pass

    def set_visible(self, visible):
        """ Set the visibility of our artists """
        self.visible = visible
        for artist in self.artists:
            artist.set_visible(visible)

    def addArtist(self, artist):
        self.axMan.artists.append(artist)
        self.artists.append(artist)

class myLasso(mySelectorWidget):
    def __init__(self, axMan, onselect=None, button=None):
        super().__init__(axMan, button=button)
        self.onselect = onselect
        self.verts = None
        self.polygon = Polygon([[0,0]], facecolor=(0,0,1,.1), animated=True, edgecolor=(0,0,1,.8))
        self.polygon.set_visible(False)
        self.ax.add_patch(self.polygon)
        self.addArtist(self.polygon)
#        self.set_active(True) #needed for blitting to work

    def _press(self, event):
        self.verts = [self._get_data(event)]
        self.set_visible(True)

    def _release(self, event):
        self.disconnect_events()
        if (self.verts is not None) and (self.onselect is not None):
            self.onselect(self.verts)
        self.set_visible(False)
        self.axMan.draw()

    def _ondrag(self, event):
        if self.verts is None:
            return
        self.verts.append(self._get_data(event))
        self.polygon.set_xy(self.verts)
        self.axMan.update()


class PolygonInteractor(mySelectorWidget):
    """
    A polygon editor.
    https://matplotlib.org/gallery/event_handling/poly_editor.html
    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, axMan):
        super().__init__(axMan, None)    

        self.line = Line2D([0],[0], ls="",
                           marker='o', markerfacecolor='r',
                           animated=True)
        self.ax.add_line(self.line)
        self._ind = None  # the active vert
        self._hoverInd = None

        self.line2 = Polygon([[0,0]], animated=True, facecolor=(0,0,1,.1), edgecolor=(0,0,1,.9))
        self.ax.add_patch(self.line2)
        self.addArtist(self.line2)
        self.addArtist(self.line)
        self.set_visible(False)

    def initialize(self, verts):

        x, y = zip(*verts)
        self.line.set_data(x,y)
#        xy=[[0,0],[100,100],[0,100]]
        xy = self.interpolate()
        self.line2.set_xy(xy)
        self.set_visible(True)
#        self.axMan.update()

    def interpolate(self):
        x,y = self.line.get_data()
        tck, u = interpolate.splprep([x, y], s=0, per=True)   
        # evaluate the spline fits for 1000 evenly spaced distance values
        xi, yi = interpolate.splev(np.linspace(0, 1, 1000), tck)
        return list(zip(xi,yi))

    def get_ind_under_point(self, event):
        'get the index of the vertex under point if within epsilon tolerance'
        # display coords
        xy = np.asarray(list(zip(*self.line.get_data())))
        xyt = self.line.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 _press(self, event):
        'whenever a mouse button is pressed'
        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 _release(self, event):
        'whenever a mouse button is released'
        if not self.showverts:
            return
        if event.button != 1:
            return
        self._ind = None

    def _on_key_press(self, event):
        'whenever a key is pressed'
#        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 = np.array([event.x, event.y])  # display coords
            for i in range(len(xys) - 1):
                s0 = xys[i]
                s1 = xys[i + 1]
                d = np.linalg.norm(np.cross(s0-s1, s1-p))/np.linalg.norm(s0-s1) #distance from line to click point
                if d <= self.epsilon:
                    self.line.set_data(np.insert(self.line.get_data, i+1, [event.ydata, event.xdata], axis=0))
                    break
        elif event.key == 'enter':
            self.done = True
            self.close()
            return
        if self.line.stale:
            self.canvas.draw_idle()

    def _onhover(self, event):
        lastHoverInd = self._hoverInd
        self._hoverInd = self.get_ind_under_point(event)
        if lastHoverInd != self._hoverInd:
            if self._hoverInd is not None:
                self.line.set_markerfacecolor((0,.9,1,1))
            else:
                self.line.set_markerfacecolor('r')
        self.axMan.update()

    def _ondrag(self, event):
        'on mouse movement'

        if self._ind is None:
            return
        x, y = event.xdata, event.ydata
        d = list(zip(*self.line.get_data()))
        d[self._ind] = (x,y)
        if self._ind == 0:
            d[-1] = (x, y)
        elif self._ind == len(d) - 1:
            d[0] = (x, y)
        self.line.set_data(list(zip(*d)))

        xy = self.interpolate()
        self.line2.set_xy(xy)
        self.axMan.update()


if __name__ == '__main__':

    fig, ax = plt.subplots()
    def onselect(verts):
        l = shapelyPolygon(LinearRing(verts))
        l = l.buffer(0)
        l=l.simplify(l.length/2e2, preserve_topology=False)  
        p.active=True
        p.initialize(l.exterior.coords)
    axm = AxManager(ax)
    l = myLasso(axm, onselect=onselect)
    p = PolygonInteractor(axm)
    p.active = False
从matplotlib.patches导入多边形
从matplotlib.widgets导入AxesWidget
从matplotlib.lines导入Line2D
将matplotlib导入为mpl
从matplotlib导入路径
将matplotlib.pyplot作为plt导入
从scipy导入插值
从shapely.geometry导入线性化,多边形为shapelyPolygon
将numpy作为np导入
导入副本
AxManager类:
定义初始化(自身,ax):
self.artists=[]
self.ax=ax
self.canvas=self.ax.figure.canvas
self.useblit=self.canvas.supports\u blit
self.canvas.mpl\u connect('draw\u event',self.update\u background)
self.background=None
def更新(自我):
“”“使用新的blit或旧的draw绘制,具体取决于
有用的
"""
如果不是self.ax.get_visible():
返回错误
如果self.useblit:
如果self.background不是None:
self.canvas.restore\u区域(self.background)
对于self.artists中的艺术家:
如果艺术家。使_可见():
自作画艺术家(艺术家)
self.canvas.blit(self.ax.bbox)
其他:
self.canvas.draw_idle()
返回错误
def牵引(自):
self.canvas.draw_idle()
def更新_背景(自身、事件):
“”“强制更新背景”“”
#如果在此处添加对“忽略”的调用,则需要检查边缘大小写:
#即使'ignore'为True,'release'也可以调用绘图事件。
如果self.useblit:
self.background=self.canvas.copy_from_bbox(self.ax.bbox)
类mySelectorWidget(AxesWidget):
定义初始化(self,axMan,button=None,state\u modifier\u keys=None):
AxesWidget.\uuuu init\uuuuu(self,axMan.ax)
self.visible=True
self.axMan=axMan
self.artists=[]
self.connect\u事件('motion\u notify\u event',self.onmove)
self.connect事件(“按钮按下事件”,self.press)
自连接事件(“按钮释放事件”,自释放)
self.connect\u事件('key\u press\u event',self.on\u key\u press)
self.connect\u事件(“按键释放事件”,self.on\u按键释放)
self.connect\u事件(“滚动事件”,self.on\u滚动)
self.state\u modifier\u keys=dict(move='',clear='escape',
平方=‘移位’、中心=‘控制’)
self.state\u modifier\u keys.update(state\u modifier\u keys或{})
如果isinstance(按钮,int):
self.validButtons=[按钮]
其他:
self.validButtons=按钮
#将保存数据(鼠标单击位置)
self.eventpress=None
#将保存数据(位于鼠标位置)
self.eventrelease=None
self.\u prev\u事件=无
self.state=set()
def设置_激活(自,激活):
AxesWidget.set_active(自激活,活动)
如果激活:
self.axMan.update\u后台(无)
def忽略(自身、事件):
“”“如果应忽略*事件*,则返回*True”“”
如果不是self.active或非self.ax.get_visible():
返回真值
#如果画布被锁定
如果不是self.canvas.widgetlock.available(self):
返回真值
如果不是hasattr(事件“按钮”):
event.button=None
#仅当触发事件时才进行矩形选择
#用一个想要的按钮
如果self.validButtons不是无:
如果event.button不在self.validButtons中:
返回真值
#如果未按下任何按钮,则忽略已关闭的事件
#轴心
如果self.eventpress为无:
return event.inaxes!=self.ax
#如果按下了按钮,检查释放按钮是否正确
#一样。
如果event.button==self.eventpress.button:
返回错误
#如果按下了按钮,检查释放按钮是否正确
#一样。
返回(event.inaxes!=self.ax或
event.button!=self.eventpress.button)
定义获取数据(自身、事件):
“”“获取事件的扩展数据和ydata,但有限制”“”
如果event.xdata为无:
返回None,None
x0,x1=self.ax.get_xbound()
y0,y1=self.ax.get_ybound()
扩展数据=最大值(x0,event.xdata)
扩展数据=最小值(x1,扩展数据)
ydata=最大值(y0,event.ydata)
ydata=最小值(y1,ydata)
返回扩展数据
def_清洁_事件(自身、事件):
“”“清理事件”
如果没有扩展数据,请使用prev事件
将扩展数据和ydata限制为轴限制
设置prev事件
"""
如果event.xdata为无:
事件=self.\u prev\u事件
其他:
事件=复制。复制(事件)
event.xdata,event.ydata=self.\u获取\u数据(事件)
self.\u prev\u事件=事件
返回事件
def压力(自身、事件):
“”“按钮按下处理程序和验证程序”“”
如果不是自己,则忽略(事件):
事件=自清洁事件(事件)
self.eventpress=event
self.\u prev\u事件=事件
key=event.key或“”
key=key.replace('ctrl','control')
#移动状态在按下按钮时锁定
如果key==self.state\u modifier\u keys['move']:
self.state.add('move')
新闻界(事件)
返回真值
返回错误
def_压力(自身、事件):
“”“按钮按下处理程序”“”
通过
def释放(自身、事件):
“”“按钮释放事件处理程序和验证程序”“”
如果不是self.ignore(event)和self.event,请按:
事件=自身