plots.py 32 KB

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