plots.py 31 KB

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