123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074 |
- """
- @package iscatt.plots
- @brief Plotting widgets
- Classes:
- - plots::ScatterPlotWidget
- - plots::PolygonDrawer
- - plots::ModestImage
- (C) 2013-2016 by the GRASS Development Team
- This program is free software under the GNU General Public License
- (>=v2). Read the file COPYING that comes with GRASS for details.
- @author Stepan Turek <stepan.turek seznam.cz> (mentor: Martin Landa)
- """
- import wx
- import numpy as np
- from math import ceil
- from multiprocessing import Process, Queue
- from copy import deepcopy
- from iscatt.core_c import MergeArrays, ApplyColormap
- from iscatt.dialogs import ManageBusyCursorMixin
- from core.settings import UserSettings
- try:
- import matplotlib
- matplotlib.use('WXAgg')
- from matplotlib.figure import Figure
- from matplotlib.backends.backend_wxagg import \
- FigureCanvasWxAgg as FigCanvas
- from matplotlib.lines import Line2D
- from matplotlib.artist import Artist
- from matplotlib.mlab import dist_point_to_segment
- from matplotlib.patches import Polygon, Ellipse, Rectangle
- import matplotlib.image as mi
- import matplotlib.colors as mcolors
- import matplotlib.cbook as cbook
- except ImportError as e:
- raise ImportError(_('The Scatterplot Tool needs the "matplotlib" '
- '(python-matplotlib) package to be installed. {0}').format(e))
- import grass.script as grass
- from grass.pydispatch.signal import Signal
- class ScatterPlotWidget(wx.Panel, ManageBusyCursorMixin):
- def __init__(self, parent, scatt_id, scatt_mgr, transpose,
- id=wx.ID_ANY):
- # TODO should not be transpose and scatt_id but x, y
- wx.Panel.__init__(self, parent, id)
- # bacause of aui (if floatable it can not take cursor from parent)
- ManageBusyCursorMixin.__init__(self, window=self)
- self.parent = parent
- self.full_extend = None
- self.mode = None
- self._createWidgets()
- self._doLayout()
- self.scatt_id = scatt_id
- self.scatt_mgr = scatt_mgr
- self.cidpress = None
- self.cidrelease = None
- self.rend_dt = {}
- self.transpose = transpose
- self.inverse = False
- self.SetSize((200, 100))
- self.Layout()
- self.base_scale = 1.2
- self.Bind(wx.EVT_CLOSE, lambda event: self.CleanUp())
- self.plotClosed = Signal("ScatterPlotWidget.plotClosed")
- self.cursorMove = Signal("ScatterPlotWidget.cursorMove")
- self.contex_menu = ScatterPlotContextMenu(plot=self)
- self.ciddscroll = None
- self.canvas.mpl_connect('motion_notify_event', self.Motion)
- self.canvas.mpl_connect('button_press_event', self.OnPress)
- self.canvas.mpl_connect('button_release_event', self.OnRelease)
- self.canvas.mpl_connect('draw_event', self.DrawCallback)
- self.canvas.mpl_connect('figure_leave_event', self.OnCanvasLeave)
- def DrawCallback(self, event):
- self.polygon_drawer.DrawCallback(event)
- self.axes.draw_artist(self.zoom_rect)
- def _createWidgets(self):
- # Create the mpl Figure and FigCanvas objects.
- # 5x4 inches, 100 dots-per-inch
- #
- self.dpi = 100
- self.fig = Figure((1.0, 1.0), dpi=self.dpi)
- self.fig.autolayout = True
- self.canvas = FigCanvas(self, -1, self.fig)
- self.axes = self.fig.add_axes([0.0, 0.0, 1, 1])
- pol = Polygon(list(zip([0], [0])), animated=True)
- self.axes.add_patch(pol)
- self.polygon_drawer = PolygonDrawer(self.axes, pol=pol, empty_pol=True)
- self.zoom_wheel_coords = None
- self.zoom_rect_coords = None
- self.zoom_rect = Polygon(list(zip([0], [0])), facecolor='none')
- self.zoom_rect.set_visible(False)
- self.axes.add_patch(self.zoom_rect)
- def ZoomToExtend(self):
- if self.full_extend:
- self.axes.axis(self.full_extend)
- self.canvas.draw()
- def SetMode(self, mode):
- self._deactivateMode()
- if mode == 'zoom':
- self.ciddscroll = self.canvas.mpl_connect(
- 'scroll_event', self.ZoomWheel)
- self.mode = 'zoom'
- elif mode == 'zoom_extend':
- self.mode = 'zoom_extend'
- elif mode == 'pan':
- self.mode = 'pan'
- elif mode:
- self.polygon_drawer.SetMode(mode)
- def SetSelectionPolygonMode(self, activate):
- self.polygon_drawer.SetSelectionPolygonMode(activate)
- def _deactivateMode(self):
- self.mode = None
- self.polygon_drawer.SetMode(None)
- if self.ciddscroll:
- self.canvas.mpl_disconnect(self.ciddscroll)
- self.zoom_rect.set_visible(False)
- self._stopCategoryEdit()
- def GetCoords(self):
- coords = self.polygon_drawer.GetCoords()
- if coords is None:
- return
- if self.transpose:
- for c in coords:
- tmp = c[0]
- c[0] = c[1]
- c[1] = tmp
- return coords
- def SetEmpty(self):
- return self.polygon_drawer.SetEmpty()
- def OnRelease(self, event):
- if not self.mode == "zoom":
- return
- self.zoom_rect.set_visible(False)
- self.ZoomRectangle(event)
- self.canvas.draw()
- def OnPress(self, event):
- 'on button press we will see if the mouse is over us and store some data'
- if not event.inaxes:
- return
- if self.mode == "zoom_extend":
- self.ZoomToExtend()
- if event.xdata and event.ydata:
- self.zoom_wheel_coords = {'x': event.xdata, 'y': event.ydata}
- self.zoom_rect_coords = {'x': event.xdata, 'y': event.ydata}
- else:
- self.zoom_wheel_coords = None
- self.zoom_rect_coords = None
- def _stopCategoryEdit(self):
- 'disconnect all the stored connection ids'
- if self.cidpress:
- self.canvas.mpl_disconnect(self.cidpress)
- if self.cidrelease:
- self.canvas.mpl_disconnect(self.cidrelease)
- # self.canvas.mpl_disconnect(self.cidmotion)
- def _doLayout(self):
- self.main_sizer = wx.BoxSizer(wx.VERTICAL)
- self.main_sizer.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
- self.SetSizer(self.main_sizer)
- self.main_sizer.Fit(self)
- def Plot(self, cats_order, scatts, ellipses, styles):
- """Redraws the figure
- """
- callafter_list = []
- if self.full_extend:
- cx = self.axes.get_xlim()
- cy = self.axes.get_ylim()
- c = cx + cy
- else:
- c = None
- q = Queue()
- _rendDtMemmapsToFiles(self.rend_dt)
- p = Process(target=MergeImg, args=(cats_order, scatts, styles,
- self.rend_dt, q))
- p.start()
- merged_img, self.full_extend, self.rend_dt = q.get()
- p.join()
- _rendDtFilesToMemmaps(self.rend_dt)
- merged_img = np.memmap(
- filename=merged_img['dt'],
- shape=merged_img['sh'])
- #merged_img, self.full_extend = MergeImg(cats_order, scatts, styles, None)
- self.axes.clear()
- self.axes.axis('equal')
- if self.transpose:
- merged_img = np.transpose(merged_img, (1, 0, 2))
- img = imshow(self.axes, merged_img,
- extent=[int(ceil(x)) for x in self.full_extend],
- origin='lower',
- interpolation='nearest',
- aspect="equal")
- callafter_list.append([self.axes.draw_artist, [img]])
- callafter_list.append([grass.try_remove, [merged_img.filename]])
- for cat_id in cats_order:
- if cat_id == 0:
- continue
- if cat_id not in ellipses:
- continue
- e = ellipses[cat_id]
- if not e:
- continue
- colors = styles[cat_id]['color'].split(":")
- if self.transpose:
- e['theta'] = 360 - e['theta'] + 90
- if e['theta'] >= 360:
- e['theta'] = abs(360 - e['theta'])
- e['pos'] = [e['pos'][1], e['pos'][0]]
- ellip = Ellipse(xy=e['pos'],
- width=e['width'],
- height=e['height'],
- angle=e['theta'],
- edgecolor="w",
- linewidth=1.5,
- facecolor='None')
- self.axes.add_artist(ellip)
- callafter_list.append([self.axes.draw_artist, [ellip]])
- color = map(
- lambda v: int(v) / 255.0,
- styles[cat_id]['color'].split(":"))
- ellip = Ellipse(xy=e['pos'],
- width=e['width'],
- height=e['height'],
- angle=e['theta'],
- edgecolor=color,
- linewidth=1,
- facecolor='None')
- self.axes.add_artist(ellip)
- callafter_list.append([self.axes.draw_artist, [ellip]])
- center = Line2D([e['pos'][0]], [e['pos'][1]],
- marker='x',
- markeredgecolor='w',
- # markerfacecolor=color,
- markersize=2)
- self.axes.add_artist(center)
- callafter_list.append([self.axes.draw_artist, [center]])
- callafter_list.append([self.fig.canvas.blit, []])
- if c:
- self.axes.axis(c)
- wx.CallAfter(lambda: self.CallAfter(callafter_list))
- def CallAfter(self, funcs_list):
- while funcs_list:
- fcn, args = funcs_list.pop(0)
- fcn(*args)
- self.canvas.draw()
- def CleanUp(self):
- self.plotClosed.emit(scatt_id=self.scatt_id)
- self.Destroy()
- def ZoomWheel(self, event):
- # get the current x and y limits
- if not event.inaxes:
- return
- # tcaswell
- # http://stackoverflow.com/questions/11551049/matplotlib-plot-zooming-with-scroll-wheel
- cur_xlim = self.axes.get_xlim()
- cur_ylim = self.axes.get_ylim()
- xdata = event.xdata
- ydata = event.ydata
- if event.button == 'up':
- scale_factor = 1 / self.base_scale
- elif event.button == 'down':
- scale_factor = self.base_scale
- else:
- scale_factor = 1
- extend = (xdata - (xdata - cur_xlim[0]) * scale_factor,
- xdata + (cur_xlim[1] - xdata) * scale_factor,
- ydata - (ydata - cur_ylim[0]) * scale_factor,
- ydata + (cur_ylim[1] - ydata) * scale_factor)
- self.axes.axis(extend)
- self.canvas.draw()
- def ZoomRectangle(self, event):
- # get the current x and y limits
- if not self.mode == "zoom":
- return
- if event.inaxes is None:
- return
- if event.button != 1:
- return
- cur_xlim = self.axes.get_xlim()
- cur_ylim = self.axes.get_ylim()
- x1, y1 = event.xdata, event.ydata
- x2 = deepcopy(self.zoom_rect_coords['x'])
- y2 = deepcopy(self.zoom_rect_coords['y'])
- if x1 == x2 or y1 == y2:
- return
- if x1 > x2:
- tmp = x1
- x1 = x2
- x2 = tmp
- if y1 > y2:
- tmp = y1
- y1 = y2
- y2 = tmp
- self.axes.axis((x1, x2, y1, y2))
- # self.axes.set_xlim(x1, x2)#, auto = True)
- # self.axes.set_ylim(y1, y2)#, auto = True)
- self.canvas.draw()
- def Motion(self, event):
- self.PanMotion(event)
- self.ZoomRectMotion(event)
- if event.inaxes is None:
- return
- self.cursorMove.emit(
- x=event.xdata,
- y=event.ydata,
- scatt_id=self.scatt_id)
- def OnCanvasLeave(self, event):
- self.cursorMove.emit(x=None, y=None, scatt_id=self.scatt_id)
- def PanMotion(self, event):
- 'on mouse movement'
- if not self.mode == "pan":
- return
- if event.inaxes is None:
- return
- if event.button != 1:
- return
- cur_xlim = self.axes.get_xlim()
- cur_ylim = self.axes.get_ylim()
- x, y = event.xdata, event.ydata
- mx = (x - self.zoom_wheel_coords['x']) * 0.6
- my = (y - self.zoom_wheel_coords['y']) * 0.6
- extend = (
- cur_xlim[0] - mx,
- cur_xlim[1] - mx,
- cur_ylim[0] - my,
- cur_ylim[1] - my)
- self.zoom_wheel_coords['x'] = x
- self.zoom_wheel_coords['y'] = y
- self.axes.axis(extend)
- # self.canvas.copy_from_bbox(self.axes.bbox)
- # self.canvas.restore_region(self.background)
- self.canvas.draw()
- def ZoomRectMotion(self, event):
- if not self.mode == "zoom":
- return
- if event.inaxes is None:
- return
- if event.button != 1:
- return
- x1, y1 = event.xdata, event.ydata
- self.zoom_rect.set_visible(True)
- x2 = self.zoom_rect_coords['x']
- y2 = self.zoom_rect_coords['y']
- self.zoom_rect.xy = ((x1, y1), (x1, y2), (x2, y2), (x2, y1), (x1, y1))
- # self.axes.draw_artist(self.zoom_rect)
- self.canvas.draw()
- def MergeImg(cats_order, scatts, styles, rend_dt, output_queue):
- _rendDtFilesToMemmaps(rend_dt)
- init = True
- merged_img = None
- merge_tmp = grass.tempfile()
- for cat_id in cats_order:
- if cat_id not in scatts:
- continue
- scatt = scatts[cat_id]
- # print "color map %d" % cat_id
- # TODO make more general
- if cat_id != 0 and (styles[cat_id]['opacity'] == 0.0 or
- not styles[cat_id]['show']):
- if cat_id in rend_dt and not rend_dt[cat_id]:
- del rend_dt[cat_id]
- continue
- if init:
- b2_i = scatt['bands_info']['b1']
- b1_i = scatt['bands_info']['b2']
- full_extend = (
- b1_i['min'] - 0.5,
- b1_i['max'] + 0.5,
- b2_i['min'] - 0.5,
- b2_i['max'] + 0.5)
- # if it does not need to be updated and was already rendered
- if not _renderCat(cat_id, rend_dt, scatt, styles):
- # is empty - has only zeros
- if rend_dt[cat_id] is None:
- continue
- else:
- masked_cat = np.ma.masked_less_equal(scatt['np_vals'], 0)
- vmax = np.amax(masked_cat)
- # totally empty -> no need to render
- if vmax == 0:
- render_cat_ids[cat_id] = None
- continue
- cmap = _getColorMap(cat_id, styles)
- masked_cat = np.uint8(masked_cat * (255.0 / float(vmax)))
- cmap = np.uint8(cmap._lut * 255)
- sh = masked_cat.shape
- rend_dt[cat_id] = {}
- if cat_id != 0:
- rend_dt[cat_id]['color'] = styles[cat_id]['color']
- rend_dt[cat_id]['dt'] = np.memmap(
- grass.tempfile(),
- dtype='uint8',
- mode='w+',
- shape=(
- sh[0],
- sh[1],
- 4))
- #colored_cat = np.zeros(dtype='uint8', )
- ApplyColormap(
- masked_cat,
- masked_cat.mask,
- cmap,
- rend_dt[cat_id]['dt'])
- #colored_cat = np.uint8(cmap(masked_cat) * 255)
- del masked_cat
- del cmap
- #colored_cat[...,3] = np.choose(masked_cat.mask, (255, 0))
- if init:
- merged_img = np.memmap(merge_tmp, dtype='uint8', mode='w+',
- shape=rend_dt[cat_id]['dt'].shape)
- merged_img[:] = rend_dt[cat_id]['dt']
- init = False
- else:
- MergeArrays(
- merged_img,
- rend_dt[cat_id]['dt'],
- styles[cat_id]['opacity'])
- """
- #c_img_a = np.memmap(grass.tempfile(), dtype="uint16", mode='w+', shape = shape)
- c_img_a = colored_cat.astype('uint16')[:,:,3] * styles[cat_id]['opacity']
- #TODO apply strides and there will be no need for loop
- #b = as_strided(a, strides=(0, a.strides[3], a.strides[3], a.strides[3]), shape=(3, a.shape[0], a.shape[1]))
- for i in range(3):
- merged_img[:,:,i] = (merged_img[:,:,i] * (255 - c_img_a) + colored_cat[:,:,i] * c_img_a) / 255;
- merged_img[:,:,3] = (merged_img[:,:,3] * (255 - c_img_a) + 255 * c_img_a) / 255;
- del c_img_a
- """
- _rendDtMemmapsToFiles(rend_dt)
- merged_img = {'dt': merged_img.filename, 'sh': merged_img.shape}
- output_queue.put((merged_img, full_extend, rend_dt))
- #_rendDtMemmapsToFiles and _rendDtFilesToMemmaps are workarounds for older numpy versions,
- # where memmap objects are not pickable
- def _rendDtMemmapsToFiles(rend_dt):
- for k, v in rend_dt.iteritems():
- if 'dt' in v:
- rend_dt[k]['sh'] = v['dt'].shape
- rend_dt[k]['dt'] = v['dt'].filename
- def _rendDtFilesToMemmaps(rend_dt):
- for k, v in rend_dt.iteritems():
- if 'dt' in v:
- rend_dt[k]['dt'] = np.memmap(filename=v['dt'], shape=v['sh'])
- del rend_dt[k]['sh']
- def _renderCat(cat_id, rend_dt, scatt, styles):
- return True
- if cat_id not in rend_dt:
- return True
- if not rend_dt[cat_id]:
- return False
- if scatt['render']:
- return True
- if cat_id != 0 and \
- rend_dt[cat_id]['color'] != styles[cat_id]['color']:
- return True
- return False
- def _getColorMap(cat_id, styles):
- cmap = matplotlib.cm.jet
- if cat_id == 0:
- cmap.set_bad('w', 1.)
- cmap._init()
- cmap._lut[len(cmap._lut) - 1, -1] = 0
- else:
- colors = styles[cat_id]['color'].split(":")
- cmap.set_bad('w', 1.)
- cmap._init()
- cmap._lut[len(cmap._lut) - 1, -1] = 0
- cmap._lut[:, 0] = int(colors[0]) / 255.0
- cmap._lut[:, 1] = int(colors[1]) / 255.0
- cmap._lut[:, 2] = int(colors[2]) / 255.0
- return cmap
- class ScatterPlotContextMenu:
- def __init__(self, plot):
- self.plot = plot
- self.canvas = plot.canvas
- self.cidpress = self.canvas.mpl_connect(
- 'button_press_event', self.ContexMenu)
- def ContexMenu(self, event):
- if not event.inaxes:
- return
- if event.button == 3:
- menu = wx.Menu()
- menu_items = [["zoom_to_extend", _("Zoom to scatter plot extend"),
- lambda event: self.plot.ZoomToExtend()]]
- for item in menu_items:
- item_id = wx.ID_ANY
- menu.Append(item_id, text=item[1])
- menu.Bind(wx.EVT_MENU, item[2], id=item_id)
- wx.CallAfter(self.ShowMenu, menu)
- def ShowMenu(self, menu):
- self.plot.PopupMenu(menu)
- menu.Destroy()
- self.plot.ReleaseMouse()
- class PolygonDrawer:
- """
- An polygon editor.
- """
- def __init__(self, ax, pol, empty_pol):
- if pol.figure is None:
- raise RuntimeError(
- 'You must first add the polygon to a figure or canvas before defining the interactor')
- self.ax = ax
- self.canvas = pol.figure.canvas
- self.showverts = True
- self.pol = pol
- self.empty_pol = empty_pol
- x, y = zip(*self.pol.xy)
- style = self._getPolygonStyle()
- self.line = Line2D(
- x,
- y,
- marker='o',
- markerfacecolor='r',
- animated=True)
- self.ax.add_line(self.line)
- # self._update_line(pol)
- cid = self.pol.add_callback(self.poly_changed)
- self.moving_ver_idx = None # the active vert
- self.mode = None
- if self.empty_pol:
- self._show(False)
- #self.canvas.mpl_connect('draw_event', self.DrawCallback)
- self.canvas.mpl_connect('button_press_event', self.OnButtonPressed)
- self.canvas.mpl_connect(
- 'button_release_event',
- self.ButtonReleaseCallback)
- self.canvas.mpl_connect(
- 'motion_notify_event',
- self.motion_notify_callback)
- self.it = 0
- def _getPolygonStyle(self):
- style = {}
- style['sel_pol'] = UserSettings.Get(group='scatt',
- key='selection',
- subkey='sel_pol')
- style['sel_pol_vertex'] = UserSettings.Get(group='scatt',
- key='selection',
- subkey='sel_pol_vertex')
- style['sel_pol'] = [i / 255.0 for i in style['sel_pol']]
- style['sel_pol_vertex'] = [i / 255.0 for i in style['sel_pol_vertex']]
- return style
- def _getSnapTresh(self):
- return UserSettings.Get(group='scatt',
- key='selection',
- subkey='snap_tresh')
- def SetMode(self, mode):
- self.mode = mode
- def SetSelectionPolygonMode(self, activate):
- self.Show(activate)
- if not activate and self.mode:
- self.SetMode(None)
- def Show(self, show):
- if show:
- if not self.empty_pol:
- self._show(True)
- else:
- self._show(False)
- def GetCoords(self):
- if self.empty_pol:
- return None
- coords = deepcopy(self.pol.xy)
- return coords
- def SetEmpty(self):
- self._setEmptyPol(True)
- def _setEmptyPol(self, empty_pol):
- self.empty_pol = empty_pol
- if self.empty_pol:
- # TODO
- self.pol.xy = np.array([[0, 0]])
- self._show(not empty_pol)
- def _show(self, show):
- self.show = show
- self.line.set_visible(self.show)
- self.pol.set_visible(self.show)
- self.Redraw()
- def Redraw(self):
- if self.show:
- self.ax.draw_artist(self.pol)
- self.ax.draw_artist(self.line)
- self.canvas.blit(self.ax.bbox)
- self.canvas.draw()
- def DrawCallback(self, event):
- style = self._getPolygonStyle()
- self.pol.set_facecolor(style['sel_pol'])
- self.line.set_markerfacecolor(style['sel_pol_vertex'])
- self.background = self.canvas.copy_from_bbox(self.ax.bbox)
- self.ax.draw_artist(self.pol)
- self.ax.draw_artist(self.line)
- def poly_changed(self, pol):
- 'this method is called whenever the polygon object is called'
- # only copy the artist props to the line (except visibility)
- vis = self.line.get_visible()
- Artist.update_from(self.line, pol)
- self.line.set_visible(vis) # don't use the pol visibility state
- def get_ind_under_point(self, event):
- 'get the index of the vertex under point if within threshold'
- # display coords
- xy = np.asarray(self.pol.xy)
- xyt = self.pol.get_transform().transform(xy)
- xt, yt = xyt[:, 0], xyt[:, 1]
- d = np.sqrt((xt - event.x)**2 + (yt - event.y)**2)
- indseq = np.nonzero(np.equal(d, np.amin(d)))[0]
- ind = indseq[0]
- if d[ind] >= self._getSnapTresh():
- ind = None
- return ind
- def OnButtonPressed(self, event):
- if not event.inaxes:
- return
- if event.button in [2, 3]:
- return
- if self.mode == "delete_vertex":
- self._deleteVertex(event)
- elif self.mode == "add_boundary_vertex":
- self._addVertexOnBoundary(event)
- elif self.mode == "add_vertex":
- self._addVertex(event)
- elif self.mode == "remove_polygon":
- self.SetEmpty()
- self.moving_ver_idx = self.get_ind_under_point(event)
- def ButtonReleaseCallback(self, event):
- 'whenever a mouse button is released'
- if not self.showverts:
- return
- if event.button != 1:
- return
- self.moving_ver_idx = None
- def ShowVertices(self, show):
- self.showverts = show
- self.line.set_visible(self.showverts)
- if not self.showverts:
- self.moving_ver_idx = None
- def _deleteVertex(self, event):
- ind = self.get_ind_under_point(event)
- if ind is None or self.empty_pol:
- return
- if len(self.pol.xy) <= 2:
- self.empty_pol = True
- self._show(False)
- return
- coords = []
- for i, tup in enumerate(self.pol.xy):
- if i == ind:
- continue
- elif i == 0 and ind == len(self.pol.xy) - 1:
- continue
- elif i == len(self.pol.xy) - 1 and ind == 0:
- continue
- coords.append(tup)
- self.pol.xy = coords
- self.line.set_data(zip(*self.pol.xy))
- self.Redraw()
- def _addVertexOnBoundary(self, event):
- if self.empty_pol:
- return
- xys = self.pol.get_transform().transform(self.pol.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._getSnapTresh():
- self.pol.xy = np.array(
- list(self.pol.xy[:i + 1]) +
- [(event.xdata, event.ydata)] +
- list(self.pol.xy[i + 1:]))
- self.line.set_data(zip(*self.pol.xy))
- break
- self.Redraw()
- def _addVertex(self, event):
- if self.empty_pol:
- pt = (event.xdata, event.ydata)
- self.pol.xy = np.array([pt, pt])
- self._show(True)
- self.empty_pol = False
- else:
- self.pol.xy = np.array(
- [(event.xdata, event.ydata)] +
- list(self.pol.xy[1:]) +
- [(event.xdata, event.ydata)])
- self.line.set_data(zip(*self.pol.xy))
- self.Redraw()
- def motion_notify_callback(self, event):
- 'on mouse movement'
- if not self.mode == "move_vertex":
- return
- if not self.showverts:
- return
- if self.empty_pol:
- return
- if self.moving_ver_idx is None:
- return
- if event.inaxes is None:
- return
- if event.button != 1:
- return
- self.it += 1
- x, y = event.xdata, event.ydata
- self.pol.xy[self.moving_ver_idx] = x, y
- if self.moving_ver_idx == 0:
- self.pol.xy[len(self.pol.xy) - 1] = x, y
- elif self.moving_ver_idx == len(self.pol.xy) - 1:
- self.pol.xy[0] = x, y
- self.line.set_data(zip(*self.pol.xy))
- self.canvas.restore_region(self.background)
- self.Redraw()
- class ModestImage(mi.AxesImage):
- """
- Computationally modest image class.
- ModestImage is an extension of the Matplotlib AxesImage class
- better suited for the interactive display of larger images. Before
- drawing, ModestImage resamples the data array based on the screen
- resolution and view window. This has very little affect on the
- appearance of the image, but can substantially cut down on
- computation since calculations of unresolved or clipped pixels
- are skipped.
- The interface of ModestImage is the same as AxesImage. However, it
- does not currently support setting the 'extent' property. There
- may also be weird coordinate warping operations for images that
- I'm not aware of. Don't expect those to work either.
- Author: Chris Beaumont <beaumont@hawaii.edu>
- """
- def __init__(self, minx=0.0, miny=0.0, *args, **kwargs):
- if 'extent' in kwargs and kwargs['extent'] is not None:
- raise NotImplementedError("ModestImage does not support extents")
- self._full_res = None
- self._sx, self._sy = None, None
- self._bounds = (None, None, None, None)
- self.minx = minx
- self.miny = miny
- super(ModestImage, self).__init__(*args, **kwargs)
- def set_data(self, A):
- """
- Set the image array
- ACCEPTS: numpy/PIL Image A
- """
- self._full_res = A
- self._A = A
- if self._A.dtype != np.uint8 and not np.can_cast(self._A.dtype,
- np.float):
- raise TypeError("Image data can not convert to float")
- if (self._A.ndim not in (2, 3) or
- (self._A.ndim == 3 and self._A.shape[-1] not in (3, 4))):
- raise TypeError("Invalid dimensions for image data")
- self._imcache = None
- self._rgbacache = None
- self._oldxslice = None
- self._oldyslice = None
- self._sx, self._sy = None, None
- def get_array(self):
- """Override to return the full-resolution array"""
- return self._full_res
- def _scale_to_res(self):
- """Change self._A and _extent to render an image whose
- resolution is matched to the eventual rendering."""
- ax = self.axes
- ext = ax.transAxes.transform([1, 1]) - ax.transAxes.transform([0, 0])
- xlim, ylim = ax.get_xlim(), ax.get_ylim()
- dx, dy = xlim[1] - xlim[0], ylim[1] - ylim[0]
- y0 = max(self.miny, ylim[0] - 5)
- y1 = min(self._full_res.shape[0] + self.miny, ylim[1] + 5)
- x0 = max(self.minx, xlim[0] - 5)
- x1 = min(self._full_res.shape[1] + self.minx, xlim[1] + 5)
- y0, y1, x0, x1 = map(int, [y0, y1, x0, x1])
- sy = int(max(1, min((y1 - y0) / 5., np.ceil(dy / ext[1]))))
- sx = int(max(1, min((x1 - x0) / 5., np.ceil(dx / ext[0]))))
- # have we already calculated what we need?
- if sx == self._sx and sy == self._sy and \
- x0 == self._bounds[0] and x1 == self._bounds[1] and \
- y0 == self._bounds[2] and y1 == self._bounds[3]:
- return
- self._A = self._full_res[y0 - self.miny:y1 - self.miny:sy,
- x0 - self.minx:x1 - self.minx:sx]
- x1 = x0 + self._A.shape[1] * sx
- y1 = y0 + self._A.shape[0] * sy
- self.set_extent([x0 - .5, x1 - .5, y0 - .5, y1 - .5])
- self._sx = sx
- self._sy = sy
- self._bounds = (x0, x1, y0, y1)
- self.changed()
- def draw(self, renderer, *args, **kwargs):
- self._scale_to_res()
- super(ModestImage, self).draw(renderer, *args, **kwargs)
- def imshow(axes, X, cmap=None, norm=None, aspect=None,
- interpolation=None, alpha=None, vmin=None, vmax=None,
- origin=None, extent=None, shape=None, filternorm=1,
- filterrad=4.0, imlim=None, resample=None, url=None, **kwargs):
- """Similar to matplotlib's imshow command, but produces a ModestImage
- Unlike matplotlib version, must explicitly specify axes
- @author: Chris Beaumont <beaumont@hawaii.edu>
- """
- if not axes._hold:
- axes.cla()
- if norm is not None:
- assert(isinstance(norm, mcolors.Normalize))
- if aspect is None:
- aspect = rcParams['image.aspect']
- axes.set_aspect(aspect)
- if extent:
- minx = extent[0]
- miny = extent[2]
- else:
- minx = 0.0
- miny = 0.0
- im = ModestImage(
- minx,
- miny,
- axes,
- cmap,
- norm,
- interpolation,
- origin,
- extent,
- filternorm=filternorm,
- filterrad=filterrad,
- resample=resample,
- **kwargs)
- im.set_data(X)
- im.set_alpha(alpha)
- axes._set_artist_props(im)
- if im.get_clip_path() is None:
- # image does not already have clipping set, clip to axes patch
- im.set_clip_path(axes.patch)
- # if norm is None and shape is None:
- # im.set_clim(vmin, vmax)
- if vmin is not None or vmax is not None:
- im.set_clim(vmin, vmax)
- else:
- im.autoscale_None()
- im.set_url(url)
- # update ax.dataLim, and, if autoscaling, set viewLim
- # to tightly fit the image, regardless of dataLim.
- im.set_extent(im.get_extent())
- axes.images.append(im)
- im._remove_method = lambda h: axes.images.remove(h)
- return im
|