plots.py 31 KB

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