plots.py 31 KB

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