plots.py 31 KB

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