plots.py 32 KB

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