plots.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956
  1. """!
  2. @package iscatt.plots
  3. @brief Ploting widgets.
  4. Classes:
  5. - plots::ScatterPlotWidget
  6. - plots::PolygonDrawer
  7. - plots::ModestImage
  8. (C) 2013 by the GRASS Development Team
  9. This program is free software under the GNU General Public License
  10. (>=v2). Read the file COPYING that comes with GRASS for details.
  11. @author Stepan Turek <stepan.turek seznam.cz> (mentor: Martin Landa)
  12. """
  13. import wx
  14. import numpy as np
  15. from math import ceil
  16. from multiprocessing import Process, Queue
  17. from copy import deepcopy
  18. from iscatt.core_c import MergeArrays, ApplyColormap
  19. from iscatt.dialogs import ManageBusyCursorMixin
  20. from core.settings import UserSettings
  21. try:
  22. import matplotlib
  23. matplotlib.use('WXAgg')
  24. from matplotlib.figure import Figure
  25. from matplotlib.backends.backend_wxagg import \
  26. FigureCanvasWxAgg as FigCanvas
  27. from matplotlib.lines import Line2D
  28. from matplotlib.artist import Artist
  29. from matplotlib.mlab import dist_point_to_segment
  30. from matplotlib.patches import Polygon, Ellipse, Rectangle
  31. import matplotlib.image as mi
  32. import matplotlib.colors as mcolors
  33. import matplotlib.cbook as cbook
  34. except ImportError as e:
  35. raise ImportError(_("Unable to import matplotlib (try to install it).\n%s") % e)
  36. import grass.script as grass
  37. from grass.pydispatch.signal import Signal
  38. class ScatterPlotWidget(wx.Panel, ManageBusyCursorMixin):
  39. def __init__(self, parent, scatt_id, scatt_mgr, transpose,
  40. id = wx.ID_ANY):
  41. #TODO should not be transpose and scatt_id but x, y
  42. wx.Panel.__init__(self, parent, id)
  43. # bacause of aui (if floatable it can not take cursor from parent)
  44. ManageBusyCursorMixin.__init__(self, window=self)
  45. self.parent = parent
  46. self.full_extend = None
  47. self.mode = None
  48. self._createWidgets()
  49. self._doLayout()
  50. self.scatt_id = scatt_id
  51. self.scatt_mgr = scatt_mgr
  52. self.cidpress = None
  53. self.cidrelease = None
  54. self.rend_dt = {}
  55. self.transpose = transpose
  56. self.inverse = False
  57. self.SetSize((200, 100))
  58. self.Layout()
  59. self.base_scale = 1.2
  60. self.Bind(wx.EVT_CLOSE,lambda event : self.CleanUp())
  61. self.plotClosed = Signal("ScatterPlotWidget.plotClosed")
  62. self.cursorMove = Signal("ScatterPlotWidget.cursorMove")
  63. self.contex_menu = ScatterPlotContextMenu(plot = self)
  64. self.ciddscroll = None
  65. self.canvas.mpl_connect('motion_notify_event', self.Motion)
  66. self.canvas.mpl_connect('button_press_event', self.OnPress)
  67. self.canvas.mpl_connect('button_release_event', self.OnRelease)
  68. self.canvas.mpl_connect('draw_event', self.DrawCallback)
  69. self.canvas.mpl_connect('figure_leave_event', self.OnCanvasLeave)
  70. def DrawCallback(self, event):
  71. self.polygon_drawer.DrawCallback(event)
  72. self.axes.draw_artist(self.zoom_rect)
  73. def _createWidgets(self):
  74. # Create the mpl Figure and FigCanvas objects.
  75. # 5x4 inches, 100 dots-per-inch
  76. #
  77. self.dpi = 100
  78. self.fig = Figure((1.0, 1.0), dpi=self.dpi)
  79. self.fig.autolayout = True
  80. self.canvas = FigCanvas(self, -1, self.fig)
  81. self.axes = self.fig.add_axes([0.0,0.0,1,1])
  82. pol = Polygon(list(zip([0], [0])), animated=True)
  83. self.axes.add_patch(pol)
  84. self.polygon_drawer = PolygonDrawer(self.axes, pol = pol, empty_pol = True)
  85. self.zoom_wheel_coords = None
  86. self.zoom_rect_coords = None
  87. self.zoom_rect = Polygon(list(zip([0], [0])), facecolor = 'none')
  88. self.zoom_rect.set_visible(False)
  89. self.axes.add_patch(self.zoom_rect)
  90. def ZoomToExtend(self):
  91. if self.full_extend:
  92. self.axes.axis(self.full_extend)
  93. self.canvas.draw()
  94. def SetMode(self, mode):
  95. self._deactivateMode()
  96. if mode == 'zoom':
  97. self.ciddscroll = self.canvas.mpl_connect('scroll_event', self.ZoomWheel)
  98. self.mode = 'zoom'
  99. elif mode == 'zoom_extend':
  100. self.mode = 'zoom_extend'
  101. elif mode == 'pan':
  102. self.mode = 'pan'
  103. elif mode:
  104. self.polygon_drawer.SetMode(mode)
  105. def SetSelectionPolygonMode(self, activate):
  106. self.polygon_drawer.SetSelectionPolygonMode(activate)
  107. def _deactivateMode(self):
  108. self.mode = None
  109. self.polygon_drawer.SetMode(None)
  110. if self.ciddscroll:
  111. self.canvas.mpl_disconnect(self.ciddscroll)
  112. self.zoom_rect.set_visible(False)
  113. self._stopCategoryEdit()
  114. def GetCoords(self):
  115. coords = self.polygon_drawer.GetCoords()
  116. if coords is None:
  117. return
  118. if self.transpose:
  119. for c in coords:
  120. tmp = c[0]
  121. c[0] = c[1]
  122. c[1] = tmp
  123. return coords
  124. def SetEmpty(self):
  125. return self.polygon_drawer.SetEmpty()
  126. def OnRelease(self, event):
  127. if not self.mode == "zoom": return
  128. self.zoom_rect.set_visible(False)
  129. self.ZoomRectangle(event)
  130. self.canvas.draw()
  131. def OnPress(self, event):
  132. 'on button press we will see if the mouse is over us and store some data'
  133. if not event.inaxes:
  134. return
  135. if self.mode == "zoom_extend":
  136. self.ZoomToExtend()
  137. if event.xdata and event.ydata:
  138. self.zoom_wheel_coords = { 'x' : event.xdata, 'y' : event.ydata}
  139. self.zoom_rect_coords = { 'x' : event.xdata, 'y' : event.ydata}
  140. else:
  141. self.zoom_wheel_coords = None
  142. self.zoom_rect_coords = None
  143. def _stopCategoryEdit(self):
  144. 'disconnect all the stored connection ids'
  145. if self.cidpress:
  146. self.canvas.mpl_disconnect(self.cidpress)
  147. if self.cidrelease:
  148. self.canvas.mpl_disconnect(self.cidrelease)
  149. #self.canvas.mpl_disconnect(self.cidmotion)
  150. def _doLayout(self):
  151. self.main_sizer = wx.BoxSizer(wx.VERTICAL)
  152. self.main_sizer.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
  153. self.SetSizer(self.main_sizer)
  154. self.main_sizer.Fit(self)
  155. def Plot(self, cats_order, scatts, ellipses, styles):
  156. """ Redraws the figure
  157. """
  158. callafter_list = []
  159. if self.full_extend:
  160. cx = self.axes.get_xlim()
  161. cy = self.axes.get_ylim()
  162. c = cx + cy
  163. else:
  164. c = None
  165. q = Queue()
  166. p = Process(target=MergeImg, args=(cats_order, scatts, styles,
  167. self.rend_dt, q))
  168. p.start()
  169. merged_img, self.full_extend, self.rend_dt = q.get()
  170. p.join()
  171. #merged_img, self.full_extend = MergeImg(cats_order, scatts, styles, None)
  172. self.axes.clear()
  173. self.axes.axis('equal')
  174. if self.transpose:
  175. merged_img = np.transpose(merged_img, (1, 0, 2))
  176. img = imshow(self.axes, merged_img,
  177. extent= [int(ceil(x)) for x in self.full_extend],
  178. origin='lower',
  179. interpolation='nearest',
  180. aspect="equal")
  181. callafter_list.append([self.axes.draw_artist, [img]])
  182. callafter_list.append([grass.try_remove, [merged_img.filename]])
  183. for cat_id in cats_order:
  184. if cat_id == 0:
  185. continue
  186. if not ellipses.has_key(cat_id):
  187. continue
  188. e = ellipses[cat_id]
  189. if not e:
  190. continue
  191. colors = styles[cat_id]['color'].split(":")
  192. if self.transpose:
  193. e['theta'] = 360 - e['theta'] + 90
  194. if e['theta'] >= 360:
  195. e['theta'] = abs(360 - e['theta'])
  196. e['pos'] = [e['pos'][1], e['pos'][0]]
  197. ellip = Ellipse(xy=e['pos'],
  198. width=e['width'],
  199. height=e['height'],
  200. angle=e['theta'],
  201. edgecolor="w",
  202. linewidth=1.5,
  203. facecolor='None')
  204. self.axes.add_artist(ellip)
  205. callafter_list.append([self.axes.draw_artist, [ellip]])
  206. color = map(lambda v : int(v)/255.0, styles[cat_id]['color'].split(":"))
  207. ellip = Ellipse(xy=e['pos'],
  208. width=e['width'],
  209. height=e['height'],
  210. angle=e['theta'],
  211. edgecolor=color,
  212. linewidth=1,
  213. facecolor='None')
  214. self.axes.add_artist(ellip)
  215. callafter_list.append([self.axes.draw_artist, [ellip]])
  216. center = Line2D([e['pos'][0]], [e['pos'][1]],
  217. marker='x',
  218. markeredgecolor='w',
  219. #markerfacecolor=color,
  220. markersize=2)
  221. self.axes.add_artist(center)
  222. callafter_list.append([self.axes.draw_artist, [center]])
  223. callafter_list.append([self.fig.canvas.blit, []])
  224. if c:
  225. self.axes.axis(c)
  226. wx.CallAfter(lambda : self.CallAfter(callafter_list))
  227. def CallAfter(self, funcs_list):
  228. while funcs_list:
  229. fcn, args = funcs_list.pop(0)
  230. fcn(*args)
  231. self.canvas.draw()
  232. def CleanUp(self):
  233. self.plotClosed.emit(scatt_id = self.scatt_id)
  234. self.Destroy()
  235. def ZoomWheel(self, event):
  236. # get the current x and y limits
  237. if not event.inaxes:
  238. return
  239. # tcaswell
  240. # http://stackoverflow.com/questions/11551049/matplotlib-plot-zooming-with-scroll-wheel
  241. cur_xlim = self.axes.get_xlim()
  242. cur_ylim = self.axes.get_ylim()
  243. xdata = event.xdata
  244. ydata = event.ydata
  245. if event.button == 'up':
  246. scale_factor = 1/self.base_scale
  247. elif event.button == 'down':
  248. scale_factor = self.base_scale
  249. else:
  250. scale_factor = 1
  251. extend = (xdata - (xdata - cur_xlim[0]) * scale_factor,
  252. xdata + (cur_xlim[1] - xdata) * scale_factor,
  253. ydata - (ydata - cur_ylim[0]) * scale_factor,
  254. ydata + (cur_ylim[1] - ydata) * scale_factor)
  255. self.axes.axis(extend)
  256. self.canvas.draw()
  257. def ZoomRectangle(self, event):
  258. # get the current x and y limits
  259. if not self.mode == "zoom": return
  260. if event.inaxes is None: return
  261. if event.button != 1: return
  262. cur_xlim = self.axes.get_xlim()
  263. cur_ylim = self.axes.get_ylim()
  264. x1, y1 = event.xdata, event.ydata
  265. x2 = deepcopy(self.zoom_rect_coords['x'])
  266. y2 = deepcopy(self.zoom_rect_coords['y'])
  267. if x1 == x2 or y1 == y2:
  268. return
  269. self.axes.axis((x1, x2, y1, y2))
  270. #self.axes.set_xlim(x1, x2)#, auto = True)
  271. #self.axes.set_ylim(y1, y2)#, auto = True)
  272. self.canvas.draw()
  273. def Motion(self, event):
  274. self.PanMotion(event)
  275. self.ZoomRectMotion(event)
  276. if event.inaxes is None:
  277. return
  278. self.cursorMove.emit(x=event.xdata, y=event.ydata, scatt_id=self.scatt_id)
  279. def OnCanvasLeave(self, event):
  280. self.cursorMove.emit(x=None, y=None, scatt_id=self.scatt_id)
  281. def PanMotion(self, event):
  282. 'on mouse movement'
  283. if not self.mode == "pan":
  284. return
  285. if event.inaxes is None:
  286. return
  287. if event.button != 1:
  288. return
  289. cur_xlim = self.axes.get_xlim()
  290. cur_ylim = self.axes.get_ylim()
  291. x,y = event.xdata, event.ydata
  292. mx = (x - self.zoom_wheel_coords['x']) * 0.6
  293. my = (y - self.zoom_wheel_coords['y']) * 0.6
  294. extend = (cur_xlim[0] - mx, cur_xlim[1] - mx, cur_ylim[0] - my, cur_ylim[1] - my)
  295. self.zoom_wheel_coords['x'] = x
  296. self.zoom_wheel_coords['y'] = y
  297. self.axes.axis(extend)
  298. #self.canvas.copy_from_bbox(self.axes.bbox)
  299. #self.canvas.restore_region(self.background)
  300. self.canvas.draw()
  301. def ZoomRectMotion(self, event):
  302. if not self.mode == "zoom": return
  303. if event.inaxes is None: return
  304. if event.button != 1: return
  305. x1, y1 = event.xdata, event.ydata
  306. self.zoom_rect.set_visible(True)
  307. x2 = self.zoom_rect_coords['x']
  308. y2 = self.zoom_rect_coords['y']
  309. self.zoom_rect.xy = ((x1, y1), (x1, y2), (x2, y2), (x2, y1), (x1, y1))
  310. #self.axes.draw_artist(self.zoom_rect)
  311. self.canvas.draw()
  312. def MergeImg(cats_order, scatts, styles, rend_dt, output_queue):
  313. init = True
  314. merged_img = None
  315. merge_tmp = grass.tempfile()
  316. for cat_id in cats_order:
  317. if not scatts.has_key(cat_id):
  318. continue
  319. scatt = scatts[cat_id]
  320. #print "color map %d" % cat_id
  321. #TODO make more general
  322. if cat_id != 0 and (styles[cat_id]['opacity'] == 0.0 or \
  323. not styles[cat_id]['show']):
  324. if rend_dt.has_key(cat_id) and not rend_dt[cat_id]:
  325. del rend_dt[cat_id]
  326. continue
  327. if init:
  328. b2_i = scatt['bands_info']['b1']
  329. b1_i = scatt['bands_info']['b2']
  330. full_extend = (b1_i['min'] - 0.5, b1_i['max'] + 0.5, b2_i['min'] - 0.5, b2_i['max'] + 0.5)
  331. # if it does not need to be updated and was already rendered
  332. if not _renderCat(cat_id, rend_dt, scatt, styles):
  333. # is empty - has only zeros
  334. if rend_dt[cat_id] is None:
  335. continue
  336. else:
  337. masked_cat = np.ma.masked_less_equal(scatt['np_vals'], 0)
  338. vmax = np.amax(masked_cat)
  339. # totally empty -> no need to render
  340. if vmax == 0:
  341. render_cat_ids[cat_id] = None
  342. continue
  343. cmap = _getColorMap(cat_id, styles)
  344. masked_cat = np.uint8(masked_cat * (255.0 / float(vmax)))
  345. cmap = np.uint8(cmap._lut * 255)
  346. sh =masked_cat.shape
  347. rend_dt[cat_id] = {}
  348. if cat_id != 0:
  349. rend_dt[cat_id]['color'] = styles[cat_id]['color']
  350. rend_dt[cat_id]['dt'] = np.memmap(grass.tempfile(), dtype='uint8', mode='w+',
  351. shape=(sh[0], sh[1], 4))
  352. #colored_cat = np.zeros(dtype='uint8', )
  353. ApplyColormap(masked_cat, masked_cat.mask, cmap, rend_dt[cat_id]['dt'])
  354. #colored_cat = np.uint8(cmap(masked_cat) * 255)
  355. del masked_cat
  356. del cmap
  357. #colored_cat[...,3] = np.choose(masked_cat.mask, (255, 0))
  358. if init:
  359. merged_img = np.memmap(merge_tmp, dtype='uint8', mode='w+',
  360. shape=rend_dt[cat_id]['dt'].shape)
  361. merged_img[:] = rend_dt[cat_id]['dt']
  362. init = False
  363. else:
  364. MergeArrays(merged_img, rend_dt[cat_id]['dt'], styles[cat_id]['opacity'])
  365. """
  366. #c_img_a = np.memmap(grass.tempfile(), dtype="uint16", mode='w+', shape = shape)
  367. c_img_a = colored_cat.astype('uint16')[:,:,3] * styles[cat_id]['opacity']
  368. #TODO apply strides and there will be no need for loop
  369. #b = as_strided(a, strides=(0, a.strides[3], a.strides[3], a.strides[3]), shape=(3, a.shape[0], a.shape[1]))
  370. for i in range(3):
  371. merged_img[:,:,i] = (merged_img[:,:,i] * (255 - c_img_a) + colored_cat[:,:,i] * c_img_a) / 255;
  372. merged_img[:,:,3] = (merged_img[:,:,3] * (255 - c_img_a) + 255 * c_img_a) / 255;
  373. del c_img_a
  374. """
  375. output_queue.put((merged_img, full_extend, rend_dt))
  376. def _renderCat(cat_id, rend_dt, scatt, styles):
  377. return True
  378. if not rend_dt.has_key(cat_id):
  379. return True
  380. if not rend_dt[cat_id]:
  381. return False
  382. if scatt['render']:
  383. return True
  384. if cat_id != 0 and \
  385. rend_dt[cat_id]['color'] != styles[cat_id]['color']:
  386. return True
  387. return False
  388. def _getColorMap(cat_id, styles):
  389. cmap = matplotlib.cm.jet
  390. if cat_id == 0:
  391. cmap.set_bad('w',1.)
  392. cmap._init()
  393. cmap._lut[len(cmap._lut) - 1, -1] = 0
  394. else:
  395. colors = styles[cat_id]['color'].split(":")
  396. cmap.set_bad('w',1.)
  397. cmap._init()
  398. cmap._lut[len(cmap._lut) - 1, -1] = 0
  399. cmap._lut[:, 0] = int(colors[0])/255.0
  400. cmap._lut[:, 1] = int(colors[1])/255.0
  401. cmap._lut[:, 2] = int(colors[2])/255.0
  402. return cmap
  403. class ScatterPlotContextMenu:
  404. def __init__(self, plot):
  405. self.plot = plot
  406. self.canvas = plot.canvas
  407. self.cidpress = self.canvas.mpl_connect(
  408. 'button_press_event', self.ContexMenu)
  409. def ContexMenu(self, event):
  410. if not event.inaxes:
  411. return
  412. if event.button == 3:
  413. menu = wx.Menu()
  414. menu_items = [["zoom_to_extend", _("Zoom to scatter plot extend"),
  415. lambda event : self.plot.ZoomToExtend()]]
  416. for item in menu_items:
  417. item_id = wx.ID_ANY
  418. menu.Append(item_id, text = item[1])
  419. menu.Bind(wx.EVT_MENU, item[2], id = item_id)
  420. wx.CallAfter(self.ShowMenu, menu)
  421. def ShowMenu(self, menu):
  422. self.plot.PopupMenu(menu)
  423. menu.Destroy()
  424. self.plot.ReleaseMouse()
  425. class PolygonDrawer:
  426. """
  427. An polygon editor.
  428. """
  429. def __init__(self, ax, pol, empty_pol):
  430. if pol.figure is None:
  431. raise RuntimeError('You must first add the polygon to a figure or canvas before defining the interactor')
  432. self.ax = ax
  433. self.canvas = pol.figure.canvas
  434. self.showverts = True
  435. self.pol = pol
  436. self.empty_pol = empty_pol
  437. x, y = zip(*self.pol.xy)
  438. style = self._getPolygonStyle()
  439. self.line = Line2D(x, y, marker='o', markerfacecolor='r', animated=True)
  440. self.ax.add_line(self.line)
  441. #self._update_line(pol)
  442. cid = self.pol.add_callback(self.poly_changed)
  443. self.moving_ver_idx = None # the active vert
  444. self.mode = None
  445. if self.empty_pol:
  446. self._show(False)
  447. #self.canvas.mpl_connect('draw_event', self.DrawCallback)
  448. self.canvas.mpl_connect('button_press_event', self.OnButtonPressed)
  449. self.canvas.mpl_connect('button_release_event', self.ButtonReleaseCallback)
  450. self.canvas.mpl_connect('motion_notify_event', self.motion_notify_callback)
  451. self.it = 0
  452. def _getPolygonStyle(self):
  453. style = {}
  454. style['sel_pol'] = UserSettings.Get(group='scatt',
  455. key='selection',
  456. subkey='sel_pol')
  457. style['sel_pol_vertex'] = UserSettings.Get(group='scatt',
  458. key='selection',
  459. subkey='sel_pol_vertex')
  460. style['sel_pol'] = [i / 255.0 for i in style['sel_pol']]
  461. style['sel_pol_vertex'] = [i / 255.0 for i in style['sel_pol_vertex']]
  462. return style
  463. def _getSnapTresh(self):
  464. return UserSettings.Get(group='scatt',
  465. key='selection',
  466. subkey='snap_tresh')
  467. def SetMode(self, mode):
  468. self.mode = mode
  469. def SetSelectionPolygonMode(self, activate):
  470. self.Show(activate)
  471. if not activate and self.mode:
  472. self.SetMode(None)
  473. def Show(self, show):
  474. if show:
  475. if not self.empty_pol:
  476. self._show(True)
  477. else:
  478. self._show(False)
  479. def GetCoords(self):
  480. if self.empty_pol:
  481. return None
  482. coords = deepcopy(self.pol.xy)
  483. return coords
  484. def SetEmpty(self):
  485. self._setEmptyPol(True)
  486. def _setEmptyPol(self, empty_pol):
  487. self.empty_pol = empty_pol
  488. if self.empty_pol:
  489. #TODO
  490. self.pol.xy = np.array([[0, 0]])
  491. self._show(not empty_pol)
  492. def _show(self, show):
  493. self.show = show
  494. self.line.set_visible(self.show)
  495. self.pol.set_visible(self.show)
  496. self.Redraw()
  497. def Redraw(self):
  498. if self.show:
  499. self.ax.draw_artist(self.pol)
  500. self.ax.draw_artist(self.line)
  501. self.canvas.blit(self.ax.bbox)
  502. self.canvas.draw()
  503. def DrawCallback(self, event):
  504. style=self._getPolygonStyle()
  505. self.pol.set_facecolor(style['sel_pol'])
  506. self.line.set_markerfacecolor(style['sel_pol_vertex'])
  507. self.background = self.canvas.copy_from_bbox(self.ax.bbox)
  508. self.ax.draw_artist(self.pol)
  509. self.ax.draw_artist(self.line)
  510. def poly_changed(self, pol):
  511. 'this method is called whenever the polygon object is called'
  512. # only copy the artist props to the line (except visibility)
  513. vis = self.line.get_visible()
  514. Artist.update_from(self.line, pol)
  515. self.line.set_visible(vis) # don't use the pol visibility state
  516. def get_ind_under_point(self, event):
  517. 'get the index of the vertex under point if within treshold'
  518. # display coords
  519. xy = np.asarray(self.pol.xy)
  520. xyt = self.pol.get_transform().transform(xy)
  521. xt, yt = xyt[:, 0], xyt[:, 1]
  522. d = np.sqrt((xt-event.x)**2 + (yt-event.y)**2)
  523. indseq = np.nonzero(np.equal(d, np.amin(d)))[0]
  524. ind = indseq[0]
  525. if d[ind]>=self._getSnapTresh():
  526. ind = None
  527. return ind
  528. def OnButtonPressed(self, event):
  529. if not event.inaxes:
  530. return
  531. if event.button in [2, 3]:
  532. return
  533. if self.mode == "delete_vertex":
  534. self._deleteVertex(event)
  535. elif self.mode == "add_boundary_vertex":
  536. self._addVertexOnBoundary(event)
  537. elif self.mode == "add_vertex":
  538. self._addVertex(event)
  539. elif self.mode == "remove_polygon":
  540. self.SetEmpty()
  541. self.moving_ver_idx = self.get_ind_under_point(event)
  542. def ButtonReleaseCallback(self, event):
  543. 'whenever a mouse button is released'
  544. if not self.showverts: return
  545. if event.button != 1: return
  546. self.moving_ver_idx = None
  547. def ShowVertices(self, show):
  548. self.showverts = show
  549. self.line.set_visible(self.showverts)
  550. if not self.showverts: self.moving_ver_idx = None
  551. def _deleteVertex(self, event):
  552. ind = self.get_ind_under_point(event)
  553. if ind is None or self.empty_pol:
  554. return
  555. if len(self.pol.xy) <= 2:
  556. self.empty_pol = True
  557. self._show(False)
  558. return
  559. coords = []
  560. for i,tup in enumerate(self.pol.xy):
  561. if i == ind:
  562. continue
  563. elif i == 0 and ind == len(self.pol.xy) - 1:
  564. continue
  565. elif i == len(self.pol.xy) - 1 and ind == 0:
  566. continue
  567. coords.append(tup)
  568. self.pol.xy = coords
  569. self.line.set_data(zip(*self.pol.xy))
  570. self.Redraw()
  571. def _addVertexOnBoundary(self, event):
  572. if self.empty_pol:
  573. return
  574. xys = self.pol.get_transform().transform(self.pol.xy)
  575. p = event.x, event.y # display coords
  576. for i in range(len(xys)-1):
  577. s0 = xys[i]
  578. s1 = xys[i+1]
  579. d = dist_point_to_segment(p, s0, s1)
  580. if d<=self._getSnapTresh():
  581. self.pol.xy = np.array(
  582. list(self.pol.xy[:i + 1]) +
  583. [(event.xdata, event.ydata)] +
  584. list(self.pol.xy[i + 1:]))
  585. self.line.set_data(zip(*self.pol.xy))
  586. break
  587. self.Redraw()
  588. def _addVertex(self, event):
  589. if self.empty_pol:
  590. pt = (event.xdata, event.ydata)
  591. self.pol.xy = np.array([pt, pt])
  592. self._show(True)
  593. self.empty_pol = False
  594. else:
  595. self.pol.xy = np.array(
  596. [(event.xdata, event.ydata)] +
  597. list(self.pol.xy[1:]) +
  598. [(event.xdata, event.ydata)])
  599. self.line.set_data(zip(*self.pol.xy))
  600. self.Redraw()
  601. def motion_notify_callback(self, event):
  602. 'on mouse movement'
  603. if not self.mode == "move_vertex": return
  604. if not self.showverts: return
  605. if self.empty_pol: return
  606. if self.moving_ver_idx is None: return
  607. if event.inaxes is None: return
  608. if event.button != 1: return
  609. self.it += 1
  610. x,y = event.xdata, event.ydata
  611. self.pol.xy[self.moving_ver_idx] = x,y
  612. if self.moving_ver_idx == 0:
  613. self.pol.xy[len(self.pol.xy) - 1] = x,y
  614. elif self.moving_ver_idx == len(self.pol.xy) - 1:
  615. self.pol.xy[0] = x,y
  616. self.line.set_data(zip(*self.pol.xy))
  617. self.canvas.restore_region(self.background)
  618. self.Redraw()
  619. class ModestImage(mi.AxesImage):
  620. """
  621. Computationally modest image class.
  622. ModestImage is an extension of the Matplotlib AxesImage class
  623. better suited for the interactive display of larger images. Before
  624. drawing, ModestImage resamples the data array based on the screen
  625. resolution and view window. This has very little affect on the
  626. appearance of the image, but can substantially cut down on
  627. computation since calculations of unresolved or clipped pixels
  628. are skipped.
  629. The interface of ModestImage is the same as AxesImage. However, it
  630. does not currently support setting the 'extent' property. There
  631. may also be weird coordinate warping operations for images that
  632. I'm not aware of. Don't expect those to work either.
  633. Author: Chris Beaumont <beaumont@hawaii.edu>
  634. """
  635. def __init__(self, minx=0.0, miny=0.0, *args, **kwargs):
  636. if 'extent' in kwargs and kwargs['extent'] is not None:
  637. raise NotImplementedError("ModestImage does not support extents")
  638. self._full_res = None
  639. self._sx, self._sy = None, None
  640. self._bounds = (None, None, None, None)
  641. self.minx = minx
  642. self.miny = miny
  643. super(ModestImage, self).__init__(*args, **kwargs)
  644. def set_data(self, A):
  645. """
  646. Set the image array
  647. ACCEPTS: numpy/PIL Image A
  648. """
  649. self._full_res = A
  650. self._A = A
  651. if self._A.dtype != np.uint8 and not np.can_cast(self._A.dtype,
  652. np.float):
  653. raise TypeError("Image data can not convert to float")
  654. if (self._A.ndim not in (2, 3) or
  655. (self._A.ndim == 3 and self._A.shape[-1] not in (3, 4))):
  656. raise TypeError("Invalid dimensions for image data")
  657. self._imcache =None
  658. self._rgbacache = None
  659. self._oldxslice = None
  660. self._oldyslice = None
  661. self._sx, self._sy = None, None
  662. def get_array(self):
  663. """Override to return the full-resolution array"""
  664. return self._full_res
  665. def _scale_to_res(self):
  666. """ Change self._A and _extent to render an image whose
  667. resolution is matched to the eventual rendering."""
  668. ax = self.axes
  669. ext = ax.transAxes.transform([1, 1]) - ax.transAxes.transform([0, 0])
  670. xlim, ylim = ax.get_xlim(), ax.get_ylim()
  671. dx, dy = xlim[1] - xlim[0], ylim[1] - ylim[0]
  672. y0 = max(self.miny, ylim[0] - 5)
  673. y1 = min(self._full_res.shape[0] + self.miny, ylim[1] + 5)
  674. x0 = max(self.minx, xlim[0] - 5)
  675. x1 = min(self._full_res.shape[1] + self.minx, xlim[1] + 5)
  676. y0, y1, x0, x1 = map(int, [y0, y1, x0, x1])
  677. sy = int(max(1, min((y1 - y0) / 5., np.ceil(dy / ext[1]))))
  678. sx = int(max(1, min((x1 - x0) / 5., np.ceil(dx / ext[0]))))
  679. # have we already calculated what we need?
  680. if sx == self._sx and sy == self._sy and \
  681. x0 == self._bounds[0] and x1 == self._bounds[1] and \
  682. y0 == self._bounds[2] and y1 == self._bounds[3]:
  683. return
  684. self._A = self._full_res[y0 - self.miny:y1 - self.miny:sy,
  685. x0 - self.minx:x1 - self.minx:sx]
  686. x1 = x0 + self._A.shape[1] * sx
  687. y1 = y0 + self._A.shape[0] * sy
  688. self.set_extent([x0 - .5, x1 - .5, y0 - .5, y1 - .5])
  689. self._sx = sx
  690. self._sy = sy
  691. self._bounds = (x0, x1, y0, y1)
  692. self.changed()
  693. def draw(self, renderer, *args, **kwargs):
  694. self._scale_to_res()
  695. super(ModestImage, self).draw(renderer, *args, **kwargs)
  696. def imshow(axes, X, cmap=None, norm=None, aspect=None,
  697. interpolation=None, alpha=None, vmin=None, vmax=None,
  698. origin=None, extent=None, shape=None, filternorm=1,
  699. filterrad=4.0, imlim=None, resample=None, url=None, **kwargs):
  700. """Similar to matplotlib's imshow command, but produces a ModestImage
  701. Unlike matplotlib version, must explicitly specify axes
  702. Author: Chris Beaumont <beaumont@hawaii.edu>
  703. """
  704. if not axes._hold:
  705. axes.cla()
  706. if norm is not None:
  707. assert(isinstance(norm, mcolors.Normalize))
  708. if aspect is None:
  709. aspect = rcParams['image.aspect']
  710. axes.set_aspect(aspect)
  711. if extent:
  712. minx=extent[0]
  713. miny=extent[2]
  714. else:
  715. minx=0.0
  716. miny=0.0
  717. im = ModestImage(minx, miny, axes, cmap, norm, interpolation, origin, extent,
  718. filternorm=filternorm,
  719. filterrad=filterrad, resample=resample, **kwargs)
  720. im.set_data(X)
  721. im.set_alpha(alpha)
  722. axes._set_artist_props(im)
  723. if im.get_clip_path() is None:
  724. # image does not already have clipping set, clip to axes patch
  725. im.set_clip_path(axes.patch)
  726. #if norm is None and shape is None:
  727. # im.set_clim(vmin, vmax)
  728. if vmin is not None or vmax is not None:
  729. im.set_clim(vmin, vmax)
  730. else:
  731. im.autoscale_None()
  732. im.set_url(url)
  733. # update ax.dataLim, and, if autoscaling, set viewLim
  734. # to tightly fit the image, regardless of dataLim.
  735. im.set_extent(im.get_extent())
  736. axes.images.append(im)
  737. im._remove_method = lambda h: axes.images.remove(h)
  738. return im