From 6b591e27a0b2542340c4d2f0f41b1de5d305f3a2 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 20 Jul 2012 17:49:28 -0500 Subject: [PATCH] Add PlotPlugin and cleanup code. --- skimage/viewer/plugins/base.py | 136 +++++++++++++++++++++++++++++--- skimage/viewer/plugins/canny.py | 2 +- skimage/viewer/viewers/core.py | 47 ++++++++++- 3 files changed, 171 insertions(+), 14 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 9a8c2cda..d1ef3eec 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -1,7 +1,31 @@ from PyQt4 import QtGui + +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt +from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg from skimage.io._plugins.q_color_mixer import IntelligentSlider +class PlotCanvas(FigureCanvasQTAgg): + """Canvas for displaying images. + + This canvas derives from Matplotlib, and has attributes `fig` and `ax`, + which point to Matplotlib figure and axes. + """ + def __init__(self, parent, height, width, **kwargs): + print height, width + self.fig, self.ax = plt.subplots(figsize=(height, width), **kwargs) + + FigureCanvasQTAgg.__init__(self, self.fig) + FigureCanvasQTAgg.setSizePolicy(self, + QtGui.QSizePolicy.Expanding, + QtGui.QSizePolicy.Expanding) + FigureCanvasQTAgg.updateGeometry(self) + # Note: `setParent` must be called after `FigureCanvasQTAgg.__init__`. + self.setParent(parent) + + class Plugin(QtGui.QDialog): """Base class for widgets that interact with the axes. @@ -12,24 +36,22 @@ class Plugin(QtGui.QDialog): useblit : bool If True, use blitting to speed up animation. Only available on some backends. If None, set to True when using Agg backend, otherwise False. - figure : :class:`~matplotlib.figure.Figure` - If None, create a figure with a single axes. - no_toolbar : bool - If True, figure created by plugin has no toolbar. This has no effect - on figures passed into `Plugin`. Attributes ---------- - viewer : ImageViewer + image_viewer : ImageViewer Window containing image used in measurement. image : array Image used in measurement/manipulation. overlay : array Image used in measurement/manipulation. """ - def __init__(self, image_viewer, callback=None, height=100, width=400): - self._viewer = image_viewer + def __init__(self, image_viewer, callback=None, height=100, width=400, + useblit=None): + self.image_viewer = image_viewer QtGui.QDialog.__init__(self, image_viewer) + self.image_viewer.plugins.append(self) + self.setWindowTitle('Image Plugin') self.layout = QtGui.QGridLayout(self) self.resize(width, height) @@ -40,8 +62,17 @@ class Plugin(QtGui.QDialog): self.arguments = [image_viewer.original_image] self.keyword_arguments= {} - self.overlay = self._viewer.overlay - self.image = self._viewer.image + self.overlay = self.image_viewer.overlay + self.image = self.image_viewer.image + + + if useblit is None: + useblit = True if mpl.backends.backend.endswith('Agg') else False + self.useblit = useblit + self.cids = [] + self.artists = [] + + self.connect_event('draw_event', self.on_draw) def caller(self, *args): arguments = [self._get_value(a) for a in self.arguments] @@ -69,3 +100,88 @@ class Plugin(QtGui.QDialog): self.layout.addWidget(slider, self.row, 0) self.row += 1 return name.replace(' ', '_'), slider + + def redraw(self): + self.canvas.draw_idle() + + def closeEvent(self, event): + """Disconnect all artists and events from ImageViewer. + + Note that events must be connected using `self.connect_event` and + artists must be appended to `self.artists`. + """ + self.disconnect_image_events() + self.remove_artists() + self.image_viewer.plugins.remove(self) + self.image_viewer.redraw() + self.close() + + def connect_event(self, event, callback): + """Connect callback with an event. + + This should be used in lieu of `figure.canvas.mpl_connect` since this + function stores call back ids for later clean up. + """ + cid = self.image_viewer.connect_event(event, callback) + self.cids.append(cid) + + def disconnect_image_events(self): + """Disconnect all events created by this widget.""" + for c in self.cids: + self.image_viewer.disconnect_event(c) + + def remove_artists(self): + """Disconnect artists that are connected to the *image plot*.""" + for a in self.artists: + self.image_viewer.remove_artist(a) + + +class PlotPlugin(Plugin): + """Plugin for ImageViewer that contains a plot Canvas. + + Parameters + ---------- + image_viewer : ImageViewer instance. + Window containing image used in measurement/manipulation. + figure : :class:`~matplotlib.figure.Figure` + If None, create a figure with a single axes. + useblit : bool + If True, use blitting to speed up animation. Only available on some + backends. If None, set to True when using Agg backend, otherwise False. + + Attributes + ---------- + image_viewer : ImageViewer + Window containing image used in measurement. + image : array + Image used in measurement/manipulation. + overlay : array + Image used in measurement/manipulation. + """ + def __init__(self, image_viewer, useblit=None, **kwargs): + Plugin.__init__(self, image_viewer, **kwargs) + # Add plot for displaying intensity profile. + self.add_plot() + self.connect_event('draw_event', self.on_draw) + + def on_draw(self, event): + """Save image background when blitting. + + The saved image is used to "clear" the figure before redrawing artists. + """ + if self.useblit: + bbox = self.image_viewer.ax.bbox + self.img_background = self.image_viewer.canvas.copy_from_bbox(bbox) + + def add_plot(self, height=4, width=4): + self.canvas = PlotCanvas(self, height, width) + self.fig = self.canvas.fig + #TODO: Converted color is slightly different than Qt background. + qpalette = QtGui.QPalette() + qcolor = qpalette.color(QtGui.QPalette.Window) + bgcolor = qcolor.toRgb().value() + if np.isscalar(bgcolor): + bgcolor = str(bgcolor / 255.) + self.fig.patch.set_facecolor(bgcolor) + self.ax = self.canvas.ax + self.layout.addWidget(self.canvas, self.row, 0) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index db702fc9..176dcf0d 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -17,4 +17,4 @@ class CannyPlugin(Plugin): def callback(self, *args, **kwargs): image = canny(*args, **kwargs) - self._viewer.overlay = image + self.image_viewer.overlay = image diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index e1c4d003..76eb6070 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -53,6 +53,17 @@ class ImageViewer(QtGui.QMainWindow): self.canvas = ImageCanvas(self.main_widget, image) self.fig = self.canvas.fig self.ax = self.canvas.ax + self.ax.autoscale(enable=False) + self.image_plot = self.ax.images[0] + self.plugins = [] + + # List of axes artists to check for removal. + self._axes_artists = [self.ax.artists, + self.ax.collections, + self.ax.images, + self.ax.lines, + self.ax.patches, + self.ax.texts] self.layout = QtGui.QVBoxLayout(self.main_widget) self.layout.addWidget(self.canvas) @@ -61,6 +72,8 @@ class ImageViewer(QtGui.QMainWindow): # self.statusBar().showMessage("coordinates") self.original_image = image self.image = image + + self.overlay_plot = None self._overlay = None @property @@ -80,10 +93,10 @@ class ImageViewer(QtGui.QMainWindow): @overlay.setter def overlay(self, image): self._overlay = image - if len(self.ax.images) == 1: - self.ax.imshow(image, cmap=self.overlay_cmap) + if self.overlay_plot is None: + self.overlay_plot = self.ax.imshow(image, cmap=self.overlay_cmap) else: - self.ax.images[1].set_array(image) + self.overlay_plot.set_array(image) self.canvas.draw_idle() def closeEvent(self, ce): @@ -93,4 +106,32 @@ class ImageViewer(QtGui.QMainWindow): super(ImageViewer, self).show() sys.exit(qApp.exec_()) + @property + def climits(self): + return self.image_plot.get_clim() + + @climits.setter + def climits(self, limits): + cmin, cmax = limits + self.image_plot.set_clim(vmin=cmin, vmax=cmax) + + def connect_event(self, event, callback): + cid = self.canvas.mpl_connect(event, callback) + return cid + + def disconnect_event(self, callback_id): + self.canvas.mpl_disconnect(callback_id) + + def add_artist(self, artist): + self.ax.add_artist(artist) + + def remove_artist(self, artist): + """Disconnect all artists created by this widget.""" + # There's probably a smarter way to do this. + for artist_list in self._axes_artists: + if artist in artist_list: + artist_list.remove(artist) + + def redraw(self): + self.canvas.draw_idle()