plots.py 32 KB

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