From 81764f693b6df82d99a3a7b325cb228aea0b0f6a Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 20 Jul 2012 14:02:36 -0500 Subject: [PATCH 01/62] ENH: Add orientation kwarg to IntelligentSlider --- skimage/io/_plugins/q_color_mixer.py | 35 +++++++++++++++++++++------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/skimage/io/_plugins/q_color_mixer.py b/skimage/io/_plugins/q_color_mixer.py index 3fe9e29c..085bd751 100644 --- a/skimage/io/_plugins/q_color_mixer.py +++ b/skimage/io/_plugins/q_color_mixer.py @@ -1,13 +1,15 @@ # the module for the qt color_mixer plugin from PyQt4 import QtGui, QtCore from PyQt4.QtGui import (QWidget, QStackedWidget, QSlider, QGridLayout, QLabel) +from PyQt4.QtCore import Qt from util import ColorMixer + class IntelligentSlider(QWidget): ''' A slider that adds a 'name' attribute and calls a callback - with 'name' as an argument to the registerd callback. + with 'name' as an argument to the registered callback. This allows you to create large groups of sliders in a loop, but still keep track of the individual events @@ -17,7 +19,7 @@ class IntelligentSlider(QWidget): The range of the slider is hardcoded from zero - 1000, but it supports a conversion factor so you can scale the results''' - def __init__(self, name, a, b, callback): + def __init__(self, name, a, b, callback, orientation='vertical'): QWidget.__init__(self) self.name = name self.callback = callback @@ -25,24 +27,41 @@ class IntelligentSlider(QWidget): self.b = b self.manually_triggered = False - self.slider = QSlider() + if orientation == 'vertical': + orientation_slider = Qt.Vertical + alignment = QtCore.Qt.AlignHCenter + align_text = QtCore.Qt.AlignCenter + align_value = QtCore.Qt.AlignCenter + elif orientation == 'horizontal': + orientation_slider = Qt.Horizontal + alignment = QtCore.Qt.AlignVCenter + align_text = QtCore.Qt.AlignLeft + align_value = QtCore.Qt.AlignRight + + self.slider = QSlider(orientation_slider) self.slider.setRange(0, 1000) self.slider.setValue(500) self.slider.valueChanged.connect(self.slider_changed) self.name_label = QLabel() self.name_label.setText(self.name) - self.name_label.setAlignment(QtCore.Qt.AlignCenter) + self.name_label.setAlignment(align_text) self.value_label = QLabel() self.value_label.setText('%2.2f' % (self.slider.value() * self.a + self.b)) - self.value_label.setAlignment(QtCore.Qt.AlignCenter) + self.value_label.setAlignment(align_value) self.layout = QGridLayout(self) - self.layout.addWidget(self.name_label, 0, 0) - self.layout.addWidget(self.slider, 1, 0, QtCore.Qt.AlignHCenter) - self.layout.addWidget(self.value_label, 2, 0) + + if orientation == 'vertical': + self.layout.addWidget(self.name_label, 0, 0) + self.layout.addWidget(self.slider, 1, 0, alignment) + self.layout.addWidget(self.value_label, 2, 0) + elif orientation == 'horizontal': + self.layout.addWidget(self.name_label, 0, 0) + self.layout.addWidget(self.slider, 0, 1, alignment) + self.layout.addWidget(self.value_label, 0, 2) # bind this to the valueChanged signal of the slider def slider_changed(self, val): From c27119b0cd919a23266c3a4581779c18c0dbf9bf Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 20 Jul 2012 14:03:47 -0500 Subject: [PATCH 02/62] ENH: Add image viewer based on Qt and Matplotlib --- skimage/viewer/__init__.py | 1 + skimage/viewer/plugins/__init__.py | 0 skimage/viewer/plugins/base.py | 70 ++++++++++++++++++ skimage/viewer/plugins/canny.py | 20 ++++++ skimage/viewer/utils/__init__.py | 1 + skimage/viewer/utils/core.py | 75 +++++++++++++++++++ skimage/viewer/viewers/__init__.py | 1 + skimage/viewer/viewers/core.py | 96 +++++++++++++++++++++++++ viewer_examples/plugins/canny.py | 10 +++ viewer_examples/viewers/image_viewer.py | 7 ++ 10 files changed, 281 insertions(+) create mode 100644 skimage/viewer/__init__.py create mode 100644 skimage/viewer/plugins/__init__.py create mode 100644 skimage/viewer/plugins/base.py create mode 100644 skimage/viewer/plugins/canny.py create mode 100644 skimage/viewer/utils/__init__.py create mode 100644 skimage/viewer/utils/core.py create mode 100644 skimage/viewer/viewers/__init__.py create mode 100644 skimage/viewer/viewers/core.py create mode 100644 viewer_examples/plugins/canny.py create mode 100644 viewer_examples/viewers/image_viewer.py diff --git a/skimage/viewer/__init__.py b/skimage/viewer/__init__.py new file mode 100644 index 00000000..e20546b0 --- /dev/null +++ b/skimage/viewer/__init__.py @@ -0,0 +1 @@ +from viewers import ImageViewer diff --git a/skimage/viewer/plugins/__init__.py b/skimage/viewer/plugins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py new file mode 100644 index 00000000..a92140cb --- /dev/null +++ b/skimage/viewer/plugins/base.py @@ -0,0 +1,70 @@ +from PyQt4 import QtGui +from skimage.io._plugins.q_color_mixer import IntelligentSlider + + +class Plugin(QtGui.QDialog): + """Base class for widgets that interact with the axes. + + Parameters + ---------- + image_viewer : ImageViewer instance. + Window containing image used in measurement/manipulation. + 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 + Window containing image used in measurement. + image : array + Image used in measurement/manipulation. + overlay : array + Image used in measurement/manipulation. + """ + def __init__(self, callback, parent=None, height=100, width=400): + self._viewer = parent + QtGui.QDialog.__init__(self, parent) + self.setWindowTitle('Image Plugin') + self.layout = QtGui.QGridLayout(self) + self.resize(width, height) + self.row = 0 + self.callback = callback + + self.arguments = [parent.original_image] + self.keyword_arguments= {} + + self.overlay = self._viewer.overlay + self.image = self._viewer.image + + def caller(self, *args): + arguments = [self._get_value(a) for a in self.arguments] + kwargs = dict([(name, self._get_value(a)) + for name, a in self.keyword_arguments.iteritems()]) + self.callback(*arguments, **kwargs) + + def _get_value(self, param): + if hasattr(param, 'val'): + return param.val() + else: + return param + + def add_argument(self, name, low, high, callback): + name, slider = self.add_slider(name, low, high, callback) + self.arguments[name] = slider + + def add_keyword_argument(self, name, low, high, callback): + name, slider = self.add_slider(name, low, high, callback) + self.keyword_arguments[name] = slider + + def add_slider(self, name, low, high, callback): + slider = IntelligentSlider(name, low, high, callback, + orientation='horizontal') + self.layout.addWidget(slider, self.row, 0) + self.row += 1 + return name.replace(' ', '_'), slider diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py new file mode 100644 index 00000000..cf417a10 --- /dev/null +++ b/skimage/viewer/plugins/canny.py @@ -0,0 +1,20 @@ +from .base import Plugin +from skimage.filter import canny + + +class CannyPlugin(Plugin): + + def __init__(self, parent, *args, **kwargs): + height = kwargs.get('height', 100) + width = kwargs.get('width', 400) + super(CannyPlugin, self).__init__(self.callback, parent=parent, + width=width, height=height) + self.add_keyword_argument('sigma', 0.005, 0, self.caller) + self.add_keyword_argument('low_threshold', 0.255, 0, self.caller) + self.add_keyword_argument('high_threshold', 0.255, 0, self.caller) + # Call callback so that image is updated to slider values. + self.caller() + + def callback(self, *args, **kwargs): + image = canny(*args, **kwargs) + self._viewer.overlay = image diff --git a/skimage/viewer/utils/__init__.py b/skimage/viewer/utils/__init__.py new file mode 100644 index 00000000..5af24064 --- /dev/null +++ b/skimage/viewer/utils/__init__.py @@ -0,0 +1 @@ +from core import * diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py new file mode 100644 index 00000000..efad540d --- /dev/null +++ b/skimage/viewer/utils/core.py @@ -0,0 +1,75 @@ +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.colors import LinearSegmentedColormap + + +__all__ = ['figimage', 'LinearColormap', 'ClearColormap', 'clear_red'] + + +def figimage(image, scale=1, dpi=None, **kwargs): + """Return figure and axes with figure tightly surrounding image. + + Unlike pyplot.figimage, this actually plots onto an axes object, which + fills the figure. Plotting the image onto an axes allows for subsequent + overlays of axes artists. + + Parameters + ---------- + image : array + image to plot + scale : float + If scale is 1, the figure and axes have the same dimension as the + image. Smaller values of `scale` will shrink the figure. + dpi : int + Dots per inch for figure. If None, use the default rcParam. + """ + dpi = dpi if dpi is not None else plt.rcParams['figure.dpi'] + kwargs.setdefault('interpolation', 'nearest') + kwargs.setdefault('cmap', 'gray') + + h, w, d = np.atleast_3d(image).shape + figsize = np.array((w, h), dtype=float) / dpi * scale + + fig, ax = plt.subplots(figsize=figsize, dpi=dpi) + fig.subplots_adjust(left=0, bottom=0, right=1, top=1) + + ax.set_axis_off() + ax.imshow(image, **kwargs) + return fig, ax + + +class LinearColormap(LinearSegmentedColormap): + """LinearSegmentedColormap in which color varies smoothly. + + This class is a simplification of LinearSegmentedColormap, which doesn't + support jumps in color intensities. + + Parameters + ---------- + name : str + Name of colormap. + + segmented_data : dict + Dictionary of 'red', 'green', 'blue', and (optionally) 'alpha' values. + Each color key contains a list of `x`, `y` tuples. `x` must increase + monotonically from 0 to 1 and corresponds to input values for a mappable + object (e.g. an image). `y` corresponds to the color intensity. + + """ + def __init__(self, name, segmented_data, **kwargs): + segmented_data = dict((key, [(x, y, y) for x, y in value]) + for key, value in segmented_data.iteritems()) + LinearSegmentedColormap.__init__(self, name, segmented_data, **kwargs) + + +class ClearColormap(LinearColormap): + def __init__(self, name, rgb): + r, g, b = rgb + cg_speq = {'blue': [(0.0, b), (1.0, b)], + 'green': [(0.0, g), (1.0, g)], + 'red': [(0.0, r), (1.0, r)], + 'alpha': [(0.0, 0.0), (1.0, 1.0)]} + LinearColormap.__init__(self, name, cg_speq) + +clear_red = ClearColormap('clear_red', (0.7, 0, 0)) + diff --git a/skimage/viewer/viewers/__init__.py b/skimage/viewer/viewers/__init__.py new file mode 100644 index 00000000..bb67a43f --- /dev/null +++ b/skimage/viewer/viewers/__init__.py @@ -0,0 +1 @@ +from .core import * diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py new file mode 100644 index 00000000..e1c4d003 --- /dev/null +++ b/skimage/viewer/viewers/core.py @@ -0,0 +1,96 @@ +import sys + +from PyQt4 import QtGui, QtCore +from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg + +from skimage.viewer.utils import figimage, clear_red + + +qApp = None + + +class ImageCanvas(FigureCanvasQTAgg): + """Canvas for displaying images. + + This canvas derives from Matplotlib, so your normal + """ + def __init__(self, parent, image, **kwargs): + self.fig, self.ax = figimage(image, **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 ImageViewer(QtGui.QMainWindow): + + def __init__(self, image): + # Start main loop + global qApp + if qApp is None: + qApp = QtGui.QApplication(sys.argv) + super(ImageViewer, self).__init__() + + #TODO: Add ImageViewer to skimage.io window manager + + self.overlay_cmap = clear_red + + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.setWindowTitle("Image Viewer") + + self.file_menu = QtGui.QMenu('&File', self) + self.file_menu.addAction('&Quit', self.close, + QtCore.Qt.CTRL + QtCore.Qt.Key_Q) + self.menuBar().addMenu(self.file_menu) + + self.main_widget = QtGui.QWidget() + self.setCentralWidget(self.main_widget) + + self.canvas = ImageCanvas(self.main_widget, image) + self.fig = self.canvas.fig + self.ax = self.canvas.ax + + self.layout = QtGui.QVBoxLayout(self.main_widget) + self.layout.addWidget(self.canvas) + + #TODO: Add coordinate display + # self.statusBar().showMessage("coordinates") + self.original_image = image + self.image = image + self._overlay = None + + @property + def image(self): + return self._img + + @image.setter + def image(self, image): + self._img = image + self.ax.images[0].set_array(image) + self.canvas.draw_idle() + + @property + def overlay(self): + return self._overlay + + @overlay.setter + def overlay(self, image): + self._overlay = image + if len(self.ax.images) == 1: + self.ax.imshow(image, cmap=self.overlay_cmap) + else: + self.ax.images[1].set_array(image) + self.canvas.draw_idle() + + def closeEvent(self, ce): + self.close() + + def show(self): + super(ImageViewer, self).show() + sys.exit(qApp.exec_()) + + diff --git a/viewer_examples/plugins/canny.py b/viewer_examples/plugins/canny.py new file mode 100644 index 00000000..d5e858ec --- /dev/null +++ b/viewer_examples/plugins/canny.py @@ -0,0 +1,10 @@ +from skimage import data +from skimage.viewer import ImageViewer +from skimage.viewer.plugins.canny import CannyPlugin + + +image = data.camera() +viewer = ImageViewer(image) +p = CannyPlugin(viewer) +p.show() +viewer.show() diff --git a/viewer_examples/viewers/image_viewer.py b/viewer_examples/viewers/image_viewer.py new file mode 100644 index 00000000..e0932787 --- /dev/null +++ b/viewer_examples/viewers/image_viewer.py @@ -0,0 +1,7 @@ +from skimage import data +from skimage.viewer import ImageViewer + + +image = data.camera() +viewer = ImageViewer(image) +viewer.show() From 1903ed892d60eb35e4d5dde3f0b6c66814b0b74c Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 20 Jul 2012 14:45:32 -0500 Subject: [PATCH 03/62] API change: switch order of image viewer and callback arguments. --- skimage/viewer/plugins/base.py | 11 ++++++----- skimage/viewer/plugins/canny.py | 4 ++-- skimage/viewer/utils/core.py | 2 ++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index a92140cb..9a8c2cda 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -27,16 +27,17 @@ class Plugin(QtGui.QDialog): overlay : array Image used in measurement/manipulation. """ - def __init__(self, callback, parent=None, height=100, width=400): - self._viewer = parent - QtGui.QDialog.__init__(self, parent) + def __init__(self, image_viewer, callback=None, height=100, width=400): + self._viewer = image_viewer + QtGui.QDialog.__init__(self, image_viewer) self.setWindowTitle('Image Plugin') self.layout = QtGui.QGridLayout(self) self.resize(width, height) self.row = 0 - self.callback = callback + if callback is not None: + self.callback = callback - self.arguments = [parent.original_image] + self.arguments = [image_viewer.original_image] self.keyword_arguments= {} self.overlay = self._viewer.overlay diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index cf417a10..db702fc9 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -4,10 +4,10 @@ from skimage.filter import canny class CannyPlugin(Plugin): - def __init__(self, parent, *args, **kwargs): + def __init__(self, image_viewer, *args, **kwargs): height = kwargs.get('height', 100) width = kwargs.get('width', 400) - super(CannyPlugin, self).__init__(self.callback, parent=parent, + super(CannyPlugin, self).__init__(image_viewer, width=width, height=height) self.add_keyword_argument('sigma', 0.005, 0, self.caller) self.add_keyword_argument('low_threshold', 0.255, 0, self.caller) diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index efad540d..f7dc6433 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -63,6 +63,8 @@ class LinearColormap(LinearSegmentedColormap): class ClearColormap(LinearColormap): + """Color map that varies linearly from alpha = 0 to 1 + """ def __init__(self, name, rgb): r, g, b = rgb cg_speq = {'blue': [(0.0, b), (1.0, b)], From 6b591e27a0b2542340c4d2f0f41b1de5d305f3a2 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 20 Jul 2012 17:49:28 -0500 Subject: [PATCH 04/62] 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() From afd33afc5e6112ec12ed34ec3bca40de4311b753 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 20 Jul 2012 17:52:01 -0500 Subject: [PATCH 05/62] Add LineProfile plugin. --- skimage/viewer/plugins/lineprofile.py | 239 +++++++++++++++++++++++++ viewer_examples/plugins/lineprofile.py | 10 ++ 2 files changed, 249 insertions(+) create mode 100644 skimage/viewer/plugins/lineprofile.py create mode 100644 viewer_examples/plugins/lineprofile.py diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py new file mode 100644 index 00000000..3839a6e0 --- /dev/null +++ b/skimage/viewer/plugins/lineprofile.py @@ -0,0 +1,239 @@ +import numpy as np +import scipy.ndimage as ndi +from skimage.util.dtype import dtype_range + +from .base import PlotPlugin + + +__all__ = ['LineProfile'] + + +class LineProfile(PlotPlugin): + """Plugin to compute interpolated intensity under a scan line on an image. + + Parameters + ---------- + image_viewer : ImageViewer instance. + Window containing image used in measurement. + 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. + linewidth : float + Line width for interpolation. Wider lines average over more pixels. + epsilon : float + Maximum pixel distance allowed when selecting end point of scan line. + limits : tuple or {None, 'image', 'dtype'} + (minimum, maximum) intensity limits for plotted profile. The following + special values are defined: + + None : rescale based on min/max intensity along selected scan line. + 'image' : fixed scale based on min/max intensity in image. + 'dtype' : fixed scale based on min/max intensity of image dtype. + """ + + def __init__(self, image_viewer, useblit=None, + linewidth=1, epsilon=5, limits='image'): + super(LineProfile, self).__init__(image_viewer, height=200, width=600, + useblit=useblit) + + self.linewidth = linewidth + self.epsilon = epsilon + + if limits == 'image': + self.limits = (np.min(self.image), np.max(self.image)) + elif limits == 'dtype': + self.limits = dtype_range[self.image.dtype.type] + elif limits is None or len(limits) == 2: + self.limits = limits + else: + raise ValueError("Unrecognized `limits`: %s" % limits) + + if not limits is None: + self.ax.set_ylim(self.limits) + + h, w = self.image.shape + + self._init_end_pts = np.array([[w/3, h/2], [2*w/3, h/2]]) + self.end_pts = self._init_end_pts.copy() + + x, y = np.transpose(self.end_pts) + self.scan_line = self.image_viewer.ax.plot(x, y, 'y-s', markersize=5, + lw=linewidth, alpha=0.5, + solid_capstyle='butt')[0] + self.artists.append(self.scan_line) + + scan_data = profile_line(self.image, self.end_pts) + self.profile = self.ax.plot(scan_data, 'k-')[0] + self._autoscale_view() + + self._active_pt = None + + self.connect_event('key_press_event', self.on_key_press) + self.connect_event('button_press_event', self.on_mouse_press) + self.connect_event('button_release_event', self.on_mouse_release) + self.connect_event('motion_notify_event', self.on_move) + self.connect_event('scroll_event', self.on_scroll) + + self.image_viewer.redraw() + print self.help() + + def help(self): + helpstr = ("Line profile tool", + "+ and - keys or mouse scroll changes width of scan line.", + "Select and drag ends of the scan line to adjust it.") + return '\n'.join(helpstr) + + def get_profile(self): + """Return intensity profile of the selected line. + + Returns + ------- + end_pts: (2, 2) array + The positions ((x1, y1), (x2, y2)) of the line ends. + profile: 1d array + Profile of intensity values. + """ + end_pts = self.scan_line.get_xydata() + profile = self.profile.get_ydata() + return end_pts, profile + + def on_scroll(self, event): + if not event.inaxes: return + if event.button == 'up': + self._thicken_scan_line() + elif event.button == 'down': + self._shrink_scan_line() + + def on_key_press(self, event): + if not event.inaxes: return + elif event.key == '+': + self._thicken_scan_line() + elif event.key == '-': + self._shrink_scan_line() + elif event.key == 'r': + self.reset() + + def _thicken_scan_line(self): + self.linewidth += 1 + self.line_changed(None, None) + + def _shrink_scan_line(self): + if self.linewidth > 1: + self.linewidth -= 1 + self.line_changed(None, None) + + def _autoscale_view(self): + if self.limits is None: + self.ax.autoscale_view(tight=True) + else: + self.ax.autoscale_view(scaley=False, tight=True) + + def get_pt_under_cursor(self, event): + """Return index of the end point under cursor, if sufficiently close""" + xy = np.asarray(self.scan_line.get_xydata()) + xyt = self.scan_line.get_transform().transform(xy) + xt, yt = xyt[:, 0], xyt[:, 1] + d = np.sqrt((xt - event.x)**2 + (yt - event.y)**2) + indseq = np.nonzero(np.equal(d, np.amin(d)))[0] + ind = indseq[0] + if d[ind] >= self.epsilon: + ind = None + return ind + + def on_mouse_press(self, event): + if event.button != 1: return + if event.inaxes==None: return + self._active_pt = self.get_pt_under_cursor(event) + + def on_mouse_release(self, event): + if event.button != 1: return + self._active_pt = None + + def on_move(self, event): + if event.button != 1: return + if self._active_pt is None: return + if not self.image_viewer.ax.in_axes(event): return + x,y = event.xdata, event.ydata + self.line_changed(x, y) + + def reset(self): + self.end_pts = self._init_end_pts.copy() + self.scan_line.set_data(np.transpose(self.end_pts)) + self.line_changed(None, None) + + def line_changed(self, x, y): + if x is not None: + self.end_pts[self._active_pt, :] = x, y + self.scan_line.set_data(np.transpose(self.end_pts)) + self.scan_line.set_linewidth(self.linewidth) + + scan = profile_line(self.image, self.end_pts, linewidth=self.linewidth) + self.profile.set_xdata(np.arange(scan.shape[0])) + self.profile.set_ydata(scan) + + self.ax.relim() + + if self.useblit: + self.image_viewer.canvas.restore_region(self.img_background) + self.ax.draw_artist(self.scan_line) + self.ax.draw_artist(self.profile) + self.image_viewer.canvas.blit(self.image_viewer.ax.bbox) + + self._autoscale_view() + + self.image_viewer.redraw() + self.redraw() + + +def profile_line(img, end_pts, linewidth=1): + """Return the intensity profile of an image measured along a scan line. + + Parameters + ---------- + img : 2d array + The image. + end_pts: (2, 2) list + End points ((x1, y1), (x2, y2)) of scan line. + linewidth: int + Width of the scan, perpendicular to the line + + Returns + ------- + return_value : array + The intensity profile along the scan line. The length of the profile + is the ceil of the computed length of the scan line. + """ + point1, point2 = end_pts + x1, y1 = point1 = np.asarray(point1, dtype = float) + x2, y2 = point2 = np.asarray(point2, dtype = float) + dx, dy = point2 - point1 + + # Quick calculation if perfectly horizontal or vertical (remove?) + if x1 == x2: + pixels = img[min(y1, y2) : max(y1, y2)+1, + x1 - linewidth / 2 : x1 + linewidth / 2 + 1] + intensities = pixels.mean(axis = 1) + return intensities + elif y1 == y2: + pixels = img[y1 - linewidth / 2 : y1 + linewidth / 2 + 1, + min(x1, x2) : max(x1, x2)+1] + intensities = pixels.mean(axis = 0) + return intensities + + theta = np.arctan2(dy,dx) + a = dy/dx + b = y1 - a * x1 + length = np.hypot(dx, dy) + + line_x = np.linspace(min(x1, x2), max(x1, x2), np.ceil(length)) + line_y = line_x * a + b + y_width = abs(linewidth * np.cos(theta)/2) + perp_ys = np.array([np.linspace(yi - y_width, + yi + y_width, linewidth) for yi in line_y]) + perp_xs = - a * perp_ys + (line_x + a * line_y)[:, np.newaxis] + + perp_lines = np.array([perp_ys, perp_xs]) + pixels = ndi.map_coordinates(img, perp_lines) + intensities = pixels.mean(axis=1) + + return intensities diff --git a/viewer_examples/plugins/lineprofile.py b/viewer_examples/plugins/lineprofile.py new file mode 100644 index 00000000..cecb2539 --- /dev/null +++ b/viewer_examples/plugins/lineprofile.py @@ -0,0 +1,10 @@ +from skimage import data +from skimage.viewer import ImageViewer +from skimage.viewer.plugins.lineprofile import LineProfile + + +image = data.camera() +viewer = ImageViewer(image) +p = LineProfile(viewer) +p.show() +viewer.show() From 47d5f028e50488ccef552ecc4c3906df12d9d70b Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 20 Jul 2012 18:19:37 -0500 Subject: [PATCH 06/62] Fix: Move on_draw method to base Plugin --- skimage/viewer/plugins/base.py | 24 +++++++++++++----------- skimage/viewer/plugins/lineprofile.py | 1 + 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index d1ef3eec..98217ca8 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -46,6 +46,8 @@ class Plugin(QtGui.QDialog): overlay : array Image used in measurement/manipulation. """ + draws_on_image = False + def __init__(self, image_viewer, callback=None, height=100, width=400, useblit=None): self.image_viewer = image_viewer @@ -72,7 +74,17 @@ class Plugin(QtGui.QDialog): self.cids = [] self.artists = [] - self.connect_event('draw_event', self.on_draw) + if self.draws_on_image: + 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 caller(self, *args): arguments = [self._get_value(a) for a in self.arguments] @@ -162,16 +174,6 @@ class PlotPlugin(Plugin): 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) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 3839a6e0..90fa07b6 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -30,6 +30,7 @@ class LineProfile(PlotPlugin): 'image' : fixed scale based on min/max intensity in image. 'dtype' : fixed scale based on min/max intensity of image dtype. """ + draws_on_image = True def __init__(self, image_viewer, useblit=None, linewidth=1, epsilon=5, limits='image'): From fb3f201a2a657ca282110dbdbd1196a611cc9246 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 17:08:19 -0500 Subject: [PATCH 07/62] Clean up old code and add docstrings. --- skimage/viewer/plugins/base.py | 7 +++-- skimage/viewer/viewers/core.py | 55 ++++++++++++++++++++++------------ 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 98217ca8..4d2f39c1 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -113,9 +113,6 @@ class Plugin(QtGui.QDialog): 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. @@ -175,6 +172,10 @@ class PlotPlugin(Plugin): # Add plot for displaying intensity profile. self.add_plot() + def redraw(self): + """Redraw plot.""" + self.canvas.draw_idle() + def add_plot(self, height=4, width=4): self.canvas = PlotCanvas(self, height, width) self.fig = self.canvas.fig diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 76eb6070..a2bc2385 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -27,6 +27,29 @@ class ImageCanvas(FigureCanvasQTAgg): class ImageViewer(QtGui.QMainWindow): + """Viewer for displaying images. + + This viewer is a simple container object that holds a Matplotlib axes + for showing images. `ImageViewer` doesn't subclass the Matplotlib axes (or + figure) because of the high probability of name collisions. + + Parameters + ---------- + image : array + Image being viewed. + + Attributes + ---------- + canvas, fig, ax : Matplotlib canvas, figure, and axes + Matplotlib canvas, figure, and axes used to display image. + image : array + Image being viewed. Setting this value will update the displayed frame. + original_image : array + Plugins typically operate on (but don't change) the original image. + plugins : list + List of attached plugins. + """ + def __init__(self, image): # Start main loop @@ -54,7 +77,11 @@ class ImageViewer(QtGui.QMainWindow): self.fig = self.canvas.fig self.ax = self.canvas.ax self.ax.autoscale(enable=False) - self.image_plot = self.ax.images[0] + + self._image_plot = self.ax.images[0] + self._overlay_plot = None + self._overlay = None + self.plugins = [] # List of axes artists to check for removal. @@ -73,9 +100,6 @@ class ImageViewer(QtGui.QMainWindow): self.original_image = image self.image = image - self.overlay_plot = None - self._overlay = None - @property def image(self): return self._img @@ -83,7 +107,7 @@ class ImageViewer(QtGui.QMainWindow): @image.setter def image(self, image): self._img = image - self.ax.images[0].set_array(image) + self._image_plot.set_array(image) self.canvas.draw_idle() @property @@ -93,10 +117,10 @@ class ImageViewer(QtGui.QMainWindow): @overlay.setter def overlay(self, image): self._overlay = image - if self.overlay_plot is None: - self.overlay_plot = 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.overlay_plot.set_array(image) + self._overlay_plot.set_array(image) self.canvas.draw_idle() def closeEvent(self, ce): @@ -106,27 +130,21 @@ 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): + """Connect callback function to matplotlib event and return id.""" cid = self.canvas.mpl_connect(event, callback) return cid def disconnect_event(self, callback_id): + """Disconnect callback by its id (returned by `connect_event`).""" self.canvas.mpl_disconnect(callback_id) def add_artist(self, artist): + """Add matplotlib artist to image viewer.""" self.ax.add_artist(artist) def remove_artist(self, artist): - """Disconnect all artists created by this widget.""" + """Disconnect matplotlib artist from image viewer.""" # There's probably a smarter way to do this. for artist_list in self._axes_artists: if artist in artist_list: @@ -134,4 +152,3 @@ class ImageViewer(QtGui.QMainWindow): def redraw(self): self.canvas.draw_idle() - From bd3ee7830675bf68722e0f7ed117a62efea90cb1 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 17:09:29 -0500 Subject: [PATCH 08/62] Delete overlay when deleting plugin. --- skimage/viewer/plugins/canny.py | 4 ++++ skimage/viewer/viewers/core.py | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 176dcf0d..81cc07fe 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -18,3 +18,7 @@ class CannyPlugin(Plugin): def callback(self, *args, **kwargs): image = canny(*args, **kwargs) self.image_viewer.overlay = image + + def closeEvent(self, event): + self.image_viewer.overlay = None + super(CannyPlugin, self).closeEvent(event) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index a2bc2385..65939e9a 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -117,7 +117,10 @@ class ImageViewer(QtGui.QMainWindow): @overlay.setter def overlay(self, image): self._overlay = image - if self._overlay_plot is None: + if image is None: + self.ax.images.remove(self._overlay_plot) + self._overlay_plot = None + elif self._overlay_plot is None: self._overlay_plot = self.ax.imshow(image, cmap=self.overlay_cmap) else: self._overlay_plot.set_array(image) From 9f0449e663d468899372ee1ddac04707e978a7f5 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 18:11:32 -0500 Subject: [PATCH 09/62] Add docstring for `overlay` and reorder methods. --- skimage/viewer/viewers/core.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 65939e9a..c3e8235f 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -46,6 +46,9 @@ class ImageViewer(QtGui.QMainWindow): Image being viewed. Setting this value will update the displayed frame. original_image : array Plugins typically operate on (but don't change) the original image. + overlay : array + Overlay displayed on top of image. This overlay defaults to a color map + with alpha values varying linearly from 0 to 1. plugins : list List of attached plugins. """ @@ -100,6 +103,16 @@ class ImageViewer(QtGui.QMainWindow): self.original_image = image self.image = image + def closeEvent(self, ce): + self.close() + + def show(self): + super(ImageViewer, self).show() + sys.exit(qApp.exec_()) + + def redraw(self): + self.canvas.draw_idle() + @property def image(self): return self._img @@ -108,7 +121,7 @@ class ImageViewer(QtGui.QMainWindow): def image(self, image): self._img = image self._image_plot.set_array(image) - self.canvas.draw_idle() + self.redraw() @property def overlay(self): @@ -126,13 +139,6 @@ class ImageViewer(QtGui.QMainWindow): self._overlay_plot.set_array(image) self.canvas.draw_idle() - def closeEvent(self, ce): - self.close() - - def show(self): - super(ImageViewer, self).show() - sys.exit(qApp.exec_()) - def connect_event(self, event, callback): """Connect callback function to matplotlib event and return id.""" cid = self.canvas.mpl_connect(event, callback) @@ -152,6 +158,3 @@ class ImageViewer(QtGui.QMainWindow): for artist_list in self._axes_artists: if artist in artist_list: artist_list.remove(artist) - - def redraw(self): - self.canvas.draw_idle() From c21fe1c2f9219dee5a425f1d3db4e3221e7b09be Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 18:55:38 -0500 Subject: [PATCH 10/62] Show coordinate and intensity info in status bar. --- skimage/viewer/viewers/core.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index c3e8235f..1f77915d 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -52,8 +52,6 @@ class ImageViewer(QtGui.QMainWindow): plugins : list List of attached plugins. """ - - def __init__(self, image): # Start main loop global qApp @@ -85,6 +83,8 @@ class ImageViewer(QtGui.QMainWindow): self._overlay_plot = None self._overlay = None + self.original_image = image + self.image = image self.plugins = [] # List of axes artists to check for removal. @@ -98,10 +98,14 @@ class ImageViewer(QtGui.QMainWindow): self.layout = QtGui.QVBoxLayout(self.main_widget) self.layout.addWidget(self.canvas) - #TODO: Add coordinate display - # self.statusBar().showMessage("coordinates") - self.original_image = image - self.image = image + #TODO: Set status bar to fixed-width font so status doesn't wiggle + status_bar = self.statusBar() + self.status_message = status_bar.showMessage + sb_size = status_bar.sizeHint() + cs_size = self.canvas.sizeHint() + self.resize(cs_size.width(), cs_size.height() + sb_size.height()) + + self.connect_event('motion_notify_event', self.update_status_bar) def closeEvent(self, ce): self.close() @@ -158,3 +162,18 @@ class ImageViewer(QtGui.QMainWindow): for artist_list in self._axes_artists: if artist in artist_list: artist_list.remove(artist) + + def update_status_bar(self, event): + if event.inaxes and event.inaxes.get_navigate(): + self.status_message(self._format_coord(event.xdata, event.ydata)) + else: + self.status_message('') + + def _format_coord(self, x, y): + # callback function to format coordinate display in status bar + x = int(x + 0.5) + y = int(y + 0.5) + try: + return "%4s @ [%4s, %4s]" % (self.image[y, x], x, y) + except IndexError: + return "" From 221cf733d1bf1d6f40f942d4b6496e6cfa7d7131 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 19:48:26 -0500 Subject: [PATCH 11/62] Add plugin names --- skimage/viewer/plugins/base.py | 4 ++-- skimage/viewer/plugins/canny.py | 2 ++ skimage/viewer/plugins/lineprofile.py | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 4d2f39c1..aa7229ad 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -46,6 +46,7 @@ class Plugin(QtGui.QDialog): overlay : array Image used in measurement/manipulation. """ + name = 'Plugin' draws_on_image = False def __init__(self, image_viewer, callback=None, height=100, width=400, @@ -54,7 +55,7 @@ class Plugin(QtGui.QDialog): QtGui.QDialog.__init__(self, image_viewer) self.image_viewer.plugins.append(self) - self.setWindowTitle('Image Plugin') + self.setWindowTitle(self.name) self.layout = QtGui.QGridLayout(self) self.resize(width, height) self.row = 0 @@ -67,7 +68,6 @@ class Plugin(QtGui.QDialog): 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 diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 81cc07fe..88312036 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -4,6 +4,8 @@ from skimage.filter import canny class CannyPlugin(Plugin): + name = 'Canny Filter' + def __init__(self, image_viewer, *args, **kwargs): height = kwargs.get('height', 100) width = kwargs.get('width', 400) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 90fa07b6..485839d0 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -30,6 +30,7 @@ class LineProfile(PlotPlugin): 'image' : fixed scale based on min/max intensity in image. 'dtype' : fixed scale based on min/max intensity of image dtype. """ + name = 'Line Profile' draws_on_image = True def __init__(self, image_viewer, useblit=None, From 4739165b8cda941f8e775eabe31fb6223aed5729 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 20:08:47 -0500 Subject: [PATCH 12/62] Add `update_on` parameter to slider and allow update on release. --- skimage/io/_plugins/q_color_mixer.py | 16 ++++++++++++---- skimage/viewer/plugins/base.py | 12 ++++++------ skimage/viewer/plugins/canny.py | 9 ++++++--- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/skimage/io/_plugins/q_color_mixer.py b/skimage/io/_plugins/q_color_mixer.py index 085bd751..7a5d1778 100644 --- a/skimage/io/_plugins/q_color_mixer.py +++ b/skimage/io/_plugins/q_color_mixer.py @@ -6,7 +6,6 @@ from PyQt4.QtCore import Qt from util import ColorMixer - class IntelligentSlider(QWidget): ''' A slider that adds a 'name' attribute and calls a callback with 'name' as an argument to the registered callback. @@ -19,7 +18,8 @@ class IntelligentSlider(QWidget): The range of the slider is hardcoded from zero - 1000, but it supports a conversion factor so you can scale the results''' - def __init__(self, name, a, b, callback, orientation='vertical'): + def __init__(self, name, a, b, callback, orientation='vertical', + update_on='move'): QWidget.__init__(self) self.name = name self.callback = callback @@ -41,7 +41,12 @@ class IntelligentSlider(QWidget): self.slider = QSlider(orientation_slider) self.slider.setRange(0, 1000) self.slider.setValue(500) - self.slider.valueChanged.connect(self.slider_changed) + if update_on == 'move': + self.slider.sliderMoved.connect(self.slider_changed) + elif update_on == 'release': + self.slider.sliderReleased.connect(self.slider_changed) + else: + raise ValueError("Unexpected value %s for 'update_on'" % update_on) self.name_label = QLabel() self.name_label.setText(self.name) @@ -62,9 +67,12 @@ class IntelligentSlider(QWidget): self.layout.addWidget(self.name_label, 0, 0) self.layout.addWidget(self.slider, 0, 1, alignment) self.layout.addWidget(self.value_label, 0, 2) + else: + msg = "Unexpected value %s for 'orientation'" + raise ValueError(msg % orientation) # bind this to the valueChanged signal of the slider - def slider_changed(self, val): + def slider_changed(self): val = self.val() self.value_label.setText(str(val)[:4]) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index aa7229ad..1757f257 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -98,17 +98,17 @@ class Plugin(QtGui.QDialog): else: return param - def add_argument(self, name, low, high, callback): - name, slider = self.add_slider(name, low, high, callback) + def add_argument(self, name, low, high, callback, **kwargs): + name, slider = self.add_slider(name, low, high, callback, **kwargs) self.arguments[name] = slider - def add_keyword_argument(self, name, low, high, callback): - name, slider = self.add_slider(name, low, high, callback) + def add_keyword_argument(self, name, low, high, callback, **kwargs): + name, slider = self.add_slider(name, low, high, callback, **kwargs) self.keyword_arguments[name] = slider - def add_slider(self, name, low, high, callback): + def add_slider(self, name, low, high, callback, **kwargs): slider = IntelligentSlider(name, low, high, callback, - orientation='horizontal') + orientation='horizontal', **kwargs) self.layout.addWidget(slider, self.row, 0) self.row += 1 return name.replace(' ', '_'), slider diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 88312036..cf3b5253 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -11,9 +11,12 @@ class CannyPlugin(Plugin): width = kwargs.get('width', 400) super(CannyPlugin, self).__init__(image_viewer, width=width, height=height) - self.add_keyword_argument('sigma', 0.005, 0, self.caller) - self.add_keyword_argument('low_threshold', 0.255, 0, self.caller) - self.add_keyword_argument('high_threshold', 0.255, 0, self.caller) + self.add_keyword_argument('sigma', 0.005, 0, self.caller, + update_on='release') + self.add_keyword_argument('low_threshold', 0.255, 0, self.caller, + update_on='release') + self.add_keyword_argument('high_threshold', 0.255, 0, self.caller, + update_on='release') # Call callback so that image is updated to slider values. self.caller() From 48ac757ab8303709cdeebb7903440f564664bd60 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 20:29:43 -0500 Subject: [PATCH 13/62] Refactor image overlays to special plugin base class. --- skimage/viewer/plugins/base.py | 53 ++++++++++++++++++++++++++------- skimage/viewer/plugins/canny.py | 8 ++--- skimage/viewer/viewers/core.py | 25 +--------------- 3 files changed, 47 insertions(+), 39 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 1757f257..eb138295 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -6,6 +6,8 @@ import matplotlib.pyplot as plt from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg from skimage.io._plugins.q_color_mixer import IntelligentSlider +from skimage.viewer.utils import clear_red + class PlotCanvas(FigureCanvasQTAgg): """Canvas for displaying images. @@ -33,6 +35,11 @@ class Plugin(QtGui.QDialog): ---------- image_viewer : ImageViewer instance. Window containing image used in measurement/manipulation. + callback : function + Function that gets called to update ImageViewer. Alternatively, this + can also be defined as a method in a Plugin subclass. + height, width : int + Size of plugin window in pixels. 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. @@ -43,8 +50,6 @@ class Plugin(QtGui.QDialog): Window containing image used in measurement. image : array Image used in measurement/manipulation. - overlay : array - Image used in measurement/manipulation. """ name = 'Plugin' draws_on_image = False @@ -65,7 +70,6 @@ class Plugin(QtGui.QDialog): self.arguments = [image_viewer.original_image] self.keyword_arguments= {} - self.overlay = self.image_viewer.overlay self.image = self.image_viewer.image if useblit is None: @@ -152,11 +156,6 @@ class PlotPlugin(Plugin): ---------- 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 ---------- @@ -164,10 +163,8 @@ class PlotPlugin(Plugin): 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): + def __init__(self, image_viewer, **kwargs): Plugin.__init__(self, image_viewer, **kwargs) # Add plot for displaying intensity profile. self.add_plot() @@ -188,3 +185,37 @@ class PlotPlugin(Plugin): self.fig.patch.set_facecolor(bgcolor) self.ax = self.canvas.ax self.layout.addWidget(self.canvas, self.row, 0) + + +class OverlayPlugin(Plugin): + """Plugin for ImageViewer that displays an overlay on top of main image. + + Attributes + ---------- + overlay : array + Overlay displayed on top of image. This overlay defaults to a color map + with alpha values varying linearly from 0 to 1. + """ + + def __init__(self, image_viewer, **kwargs): + Plugin.__init__(self, image_viewer, **kwargs) + self.overlay_cmap = clear_red + self._overlay_plot = None + self._overlay = None + + @property + def overlay(self): + return self._overlay + + @overlay.setter + def overlay(self, image): + self._overlay = image + ax = self.image_viewer.ax + if image is None: + ax.images.remove(self._overlay_plot) + self._overlay_plot = None + elif self._overlay_plot is None: + self._overlay_plot = ax.imshow(image, cmap=self.overlay_cmap) + else: + self._overlay_plot.set_array(image) + self.image_viewer.redraw() diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index cf3b5253..feeaee0d 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -1,8 +1,8 @@ -from .base import Plugin +from .base import OverlayPlugin from skimage.filter import canny -class CannyPlugin(Plugin): +class CannyPlugin(OverlayPlugin): name = 'Canny Filter' @@ -22,8 +22,8 @@ class CannyPlugin(Plugin): def callback(self, *args, **kwargs): image = canny(*args, **kwargs) - self.image_viewer.overlay = image + self.overlay = image def closeEvent(self, event): - self.image_viewer.overlay = None + self.overlay = None super(CannyPlugin, self).closeEvent(event) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 1f77915d..b6c1db69 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -3,7 +3,7 @@ import sys from PyQt4 import QtGui, QtCore from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg -from skimage.viewer.utils import figimage, clear_red +from skimage.viewer.utils import figimage qApp = None @@ -46,9 +46,6 @@ class ImageViewer(QtGui.QMainWindow): Image being viewed. Setting this value will update the displayed frame. original_image : array Plugins typically operate on (but don't change) the original image. - overlay : array - Overlay displayed on top of image. This overlay defaults to a color map - with alpha values varying linearly from 0 to 1. plugins : list List of attached plugins. """ @@ -61,8 +58,6 @@ class ImageViewer(QtGui.QMainWindow): #TODO: Add ImageViewer to skimage.io window manager - self.overlay_cmap = clear_red - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.setWindowTitle("Image Viewer") @@ -80,8 +75,6 @@ class ImageViewer(QtGui.QMainWindow): self.ax.autoscale(enable=False) self._image_plot = self.ax.images[0] - self._overlay_plot = None - self._overlay = None self.original_image = image self.image = image @@ -127,22 +120,6 @@ class ImageViewer(QtGui.QMainWindow): self._image_plot.set_array(image) self.redraw() - @property - def overlay(self): - return self._overlay - - @overlay.setter - def overlay(self, image): - self._overlay = image - if image is None: - self.ax.images.remove(self._overlay_plot) - self._overlay_plot = None - elif self._overlay_plot is None: - self._overlay_plot = self.ax.imshow(image, cmap=self.overlay_cmap) - else: - self._overlay_plot.set_array(image) - self.canvas.draw_idle() - def connect_event(self, event, callback): """Connect callback function to matplotlib event and return id.""" cid = self.canvas.mpl_connect(event, callback) From 51711213f733a1adc73bbb29bb6ce3916aff8a22 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 20:45:54 -0500 Subject: [PATCH 14/62] Move OverlayPlugin and PlotPlugin to their own modules. --- skimage/viewer/plugins/base.py | 96 ------------------------- skimage/viewer/plugins/canny.py | 2 +- skimage/viewer/plugins/lineprofile.py | 2 +- skimage/viewer/plugins/overlayplugin.py | 37 ++++++++++ skimage/viewer/plugins/plotplugin.py | 63 ++++++++++++++++ 5 files changed, 102 insertions(+), 98 deletions(-) create mode 100644 skimage/viewer/plugins/overlayplugin.py create mode 100644 skimage/viewer/plugins/plotplugin.py diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index eb138295..07b74f22 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -1,32 +1,8 @@ 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 -from skimage.viewer.utils import clear_red - - -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. @@ -147,75 +123,3 @@ class Plugin(QtGui.QDialog): """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. - - Attributes - ---------- - image_viewer : ImageViewer - Window containing image used in measurement. - image : array - Image used in measurement/manipulation. - """ - def __init__(self, image_viewer, **kwargs): - Plugin.__init__(self, image_viewer, **kwargs) - # Add plot for displaying intensity profile. - self.add_plot() - - def redraw(self): - """Redraw plot.""" - self.canvas.draw_idle() - - 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) - - -class OverlayPlugin(Plugin): - """Plugin for ImageViewer that displays an overlay on top of main image. - - Attributes - ---------- - overlay : array - Overlay displayed on top of image. This overlay defaults to a color map - with alpha values varying linearly from 0 to 1. - """ - - def __init__(self, image_viewer, **kwargs): - Plugin.__init__(self, image_viewer, **kwargs) - self.overlay_cmap = clear_red - self._overlay_plot = None - self._overlay = None - - @property - def overlay(self): - return self._overlay - - @overlay.setter - def overlay(self, image): - self._overlay = image - ax = self.image_viewer.ax - if image is None: - ax.images.remove(self._overlay_plot) - self._overlay_plot = None - elif self._overlay_plot is None: - self._overlay_plot = ax.imshow(image, cmap=self.overlay_cmap) - else: - self._overlay_plot.set_array(image) - self.image_viewer.redraw() diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index feeaee0d..25500e01 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -1,5 +1,5 @@ -from .base import OverlayPlugin from skimage.filter import canny +from .overlayplugin import OverlayPlugin class CannyPlugin(OverlayPlugin): diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 485839d0..32ea967d 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -2,7 +2,7 @@ import numpy as np import scipy.ndimage as ndi from skimage.util.dtype import dtype_range -from .base import PlotPlugin +from .plotplugin import PlotPlugin __all__ = ['LineProfile'] diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py new file mode 100644 index 00000000..a2b12630 --- /dev/null +++ b/skimage/viewer/plugins/overlayplugin.py @@ -0,0 +1,37 @@ +from ..utils import clear_red + +from .base import Plugin + + +class OverlayPlugin(Plugin): + """Plugin for ImageViewer that displays an overlay on top of main image. + + Attributes + ---------- + overlay : array + Overlay displayed on top of image. This overlay defaults to a color map + with alpha values varying linearly from 0 to 1. + """ + + def __init__(self, image_viewer, **kwargs): + Plugin.__init__(self, image_viewer, **kwargs) + self.overlay_cmap = clear_red + self._overlay_plot = None + self._overlay = None + + @property + def overlay(self): + return self._overlay + + @overlay.setter + def overlay(self, image): + self._overlay = image + ax = self.image_viewer.ax + if image is None: + ax.images.remove(self._overlay_plot) + self._overlay_plot = None + elif self._overlay_plot is None: + self._overlay_plot = ax.imshow(image, cmap=self.overlay_cmap) + else: + self._overlay_plot.set_array(image) + self.image_viewer.redraw() diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py new file mode 100644 index 00000000..38762dc4 --- /dev/null +++ b/skimage/viewer/plugins/plotplugin.py @@ -0,0 +1,63 @@ +import numpy as np +from PyQt4 import QtGui + +import matplotlib.pyplot as plt +from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg + +from .base import Plugin + + +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): + 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 PlotPlugin(Plugin): + """Plugin for ImageViewer that contains a plot Canvas. + + Parameters + ---------- + image_viewer : ImageViewer instance. + Window containing image used in measurement/manipulation. + + Attributes + ---------- + image_viewer : ImageViewer + Window containing image used in measurement. + image : array + Image used in measurement/manipulation. + """ + def __init__(self, image_viewer, **kwargs): + Plugin.__init__(self, image_viewer, **kwargs) + # Add plot for displaying intensity profile. + self.add_plot() + + def redraw(self): + """Redraw plot.""" + self.canvas.draw_idle() + + 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) From 4d747720de210525aa8e3d7c36da38e3a71f3067 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 22:03:46 -0500 Subject: [PATCH 15/62] Change ImageViewer to automatically call plugins. --- skimage/viewer/viewers/core.py | 2 ++ viewer_examples/plugins/canny.py | 3 +-- viewer_examples/plugins/lineprofile.py | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index b6c1db69..6ef83d27 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -104,6 +104,8 @@ class ImageViewer(QtGui.QMainWindow): self.close() def show(self): + for p in self.plugins: + p.show() super(ImageViewer, self).show() sys.exit(qApp.exec_()) diff --git a/viewer_examples/plugins/canny.py b/viewer_examples/plugins/canny.py index d5e858ec..e84a8270 100644 --- a/viewer_examples/plugins/canny.py +++ b/viewer_examples/plugins/canny.py @@ -5,6 +5,5 @@ from skimage.viewer.plugins.canny import CannyPlugin image = data.camera() viewer = ImageViewer(image) -p = CannyPlugin(viewer) -p.show() +CannyPlugin(viewer) viewer.show() diff --git a/viewer_examples/plugins/lineprofile.py b/viewer_examples/plugins/lineprofile.py index cecb2539..310c23e4 100644 --- a/viewer_examples/plugins/lineprofile.py +++ b/viewer_examples/plugins/lineprofile.py @@ -5,6 +5,5 @@ from skimage.viewer.plugins.lineprofile import LineProfile image = data.camera() viewer = ImageViewer(image) -p = LineProfile(viewer) -p.show() +LineProfile(viewer) viewer.show() From 31c2810dee399c1f9c4dcbb1dd5ac75129e59e6a Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 21 Jul 2012 23:27:05 -0400 Subject: [PATCH 16/62] ENH: Align image and plugin windows --- skimage/viewer/viewers/core.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 6ef83d27..f4d3b587 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -103,7 +103,19 @@ class ImageViewer(QtGui.QMainWindow): def closeEvent(self, ce): self.close() + def auto_layout(self): + """Move viewer to top-left and align plugin on right edge of viewer.""" + size = self.geometry() + self.move(0, 0) + w = size.width() + y = 0 + #TODO: Layout isn't correct for multiple plots (overlaps). + for p in self.plugins: + p.move(w, y) + y += p.geometry().height() + def show(self): + self.auto_layout() for p in self.plugins: p.show() super(ImageViewer, self).show() From bc6c81606ffebd82ecd6f2b2688fd9b24e895883 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 22 Jul 2012 01:25:58 -0400 Subject: [PATCH 17/62] Fix `add_argument`. `arguments` is a list, but I was treating it like a dict. --- skimage/viewer/plugins/base.py | 2 +- skimage/viewer/plugins/canny.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 07b74f22..e4bcd17b 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -80,7 +80,7 @@ class Plugin(QtGui.QDialog): def add_argument(self, name, low, high, callback, **kwargs): name, slider = self.add_slider(name, low, high, callback, **kwargs) - self.arguments[name] = slider + self.arguments.append(slider) def add_keyword_argument(self, name, low, high, callback, **kwargs): name, slider = self.add_slider(name, low, high, callback, **kwargs) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 25500e01..376638e5 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -1,4 +1,5 @@ from skimage.filter import canny + from .overlayplugin import OverlayPlugin From 887a9119b294bf5ba702603dc15014ea21fe3435 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 22 Jul 2012 02:02:29 -0400 Subject: [PATCH 18/62] ENH: Simplify creation of Slider widget. --- skimage/viewer/plugins/base.py | 23 +++++++++++++++-------- skimage/viewer/plugins/canny.py | 9 +++------ skimage/viewer/widgets/__init__.py | 1 + skimage/viewer/widgets/core.py | 21 +++++++++++++++++++++ 4 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 skimage/viewer/widgets/__init__.py create mode 100644 skimage/viewer/widgets/core.py diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index e4bcd17b..1005384a 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -1,7 +1,7 @@ from PyQt4 import QtGui import matplotlib as mpl -from skimage.io._plugins.q_color_mixer import IntelligentSlider +from ..widgets import Slider class Plugin(QtGui.QDialog): @@ -78,17 +78,17 @@ class Plugin(QtGui.QDialog): else: return param - def add_argument(self, name, low, high, callback, **kwargs): - name, slider = self.add_slider(name, low, high, callback, **kwargs) + def add_argument(self, name, low, high, **kwargs): + name, slider = self.add_slider(name, low, high, **kwargs) self.arguments.append(slider) - def add_keyword_argument(self, name, low, high, callback, **kwargs): - name, slider = self.add_slider(name, low, high, callback, **kwargs) + def add_keyword_argument(self, name, low, high, **kwargs): + name, slider = self.add_slider(name, low, high, **kwargs) self.keyword_arguments[name] = slider - def add_slider(self, name, low, high, callback, **kwargs): - slider = IntelligentSlider(name, low, high, callback, - orientation='horizontal', **kwargs) + def add_slider(self, name, low, high, **kwargs): + slider = Slider(name, low, high, **kwargs) + slider.callback = self.caller self.layout.addWidget(slider, self.row, 0) self.row += 1 return name.replace(' ', '_'), slider @@ -110,6 +110,13 @@ class Plugin(QtGui.QDialog): This should be used in lieu of `figure.canvas.mpl_connect` since this function stores call back ids for later clean up. + + Parameters + ---------- + event : str + Matplotlib event. + callback : function + Callback function with a matplotlib Event object as its argument. """ cid = self.image_viewer.connect_event(event, callback) self.cids.append(cid) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 376638e5..c93d5b0d 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -12,12 +12,9 @@ class CannyPlugin(OverlayPlugin): width = kwargs.get('width', 400) super(CannyPlugin, self).__init__(image_viewer, width=width, height=height) - self.add_keyword_argument('sigma', 0.005, 0, self.caller, - update_on='release') - self.add_keyword_argument('low_threshold', 0.255, 0, self.caller, - update_on='release') - self.add_keyword_argument('high_threshold', 0.255, 0, self.caller, - update_on='release') + self.add_keyword_argument('sigma', 0, 5, update_on='release') + self.add_keyword_argument('low_threshold', 0, 255, update_on='release') + self.add_keyword_argument('high_threshold', 0, 255, update_on='release') # Call callback so that image is updated to slider values. self.caller() diff --git a/skimage/viewer/widgets/__init__.py b/skimage/viewer/widgets/__init__.py new file mode 100644 index 00000000..5af24064 --- /dev/null +++ b/skimage/viewer/widgets/__init__.py @@ -0,0 +1 @@ +from core import * diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py new file mode 100644 index 00000000..e68bda7f --- /dev/null +++ b/skimage/viewer/widgets/core.py @@ -0,0 +1,21 @@ +from skimage.io._plugins.q_color_mixer import IntelligentSlider + +class Slider(IntelligentSlider): + """Slider widget. + + Parameters + ---------- + name : str + Name of slider parameter. If this parameter is passed as a keyword + argument, it must match the name of that keyword argument. In addition, + this name is displayed as the name of the slider. + low, high : float + Range of slider values. + ptype : {'arg' | 'kwarg' | ...} + Parameter + """ + def __init__(self, name, low, high, ptype='kwarg', callback=None, **kwargs): + self.ptype = ptype + kwargs.setdefault('orientation', 'horizontal') + scale = (high - low) / 1000.0 + super(Slider, self).__init__(name, scale, low, callback, **kwargs) From 9b4c6222b52c5aa2c5e37356d6652c00c05efe56 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 22 Jul 2012 02:08:16 -0400 Subject: [PATCH 19/62] ENH: Rename callback functions for clarity. --- skimage/viewer/plugins/base.py | 12 ++++++------ skimage/viewer/plugins/canny.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 1005384a..14fda815 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -30,7 +30,7 @@ class Plugin(QtGui.QDialog): name = 'Plugin' draws_on_image = False - def __init__(self, image_viewer, callback=None, height=100, width=400, + def __init__(self, image_viewer, image_filter=None, height=100, width=400, useblit=None): self.image_viewer = image_viewer QtGui.QDialog.__init__(self, image_viewer) @@ -40,8 +40,8 @@ class Plugin(QtGui.QDialog): self.layout = QtGui.QGridLayout(self) self.resize(width, height) self.row = 0 - if callback is not None: - self.callback = callback + if image_filter is not None: + self.image_filter = image_filter self.arguments = [image_viewer.original_image] self.keyword_arguments= {} @@ -66,11 +66,11 @@ class Plugin(QtGui.QDialog): bbox = self.image_viewer.ax.bbox self.img_background = self.image_viewer.canvas.copy_from_bbox(bbox) - def caller(self, *args): + def filter_image(self, *args): arguments = [self._get_value(a) for a in self.arguments] kwargs = dict([(name, self._get_value(a)) for name, a in self.keyword_arguments.iteritems()]) - self.callback(*arguments, **kwargs) + self.image_filter(*arguments, **kwargs) def _get_value(self, param): if hasattr(param, 'val'): @@ -88,7 +88,7 @@ class Plugin(QtGui.QDialog): def add_slider(self, name, low, high, **kwargs): slider = Slider(name, low, high, **kwargs) - slider.callback = self.caller + slider.callback = self.filter_image self.layout.addWidget(slider, self.row, 0) self.row += 1 return name.replace(' ', '_'), slider diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index c93d5b0d..08099795 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -15,10 +15,10 @@ class CannyPlugin(OverlayPlugin): self.add_keyword_argument('sigma', 0, 5, update_on='release') self.add_keyword_argument('low_threshold', 0, 255, update_on='release') self.add_keyword_argument('high_threshold', 0, 255, update_on='release') - # Call callback so that image is updated to slider values. - self.caller() + # Update image overlay to default slider values. + self.filter_image() - def callback(self, *args, **kwargs): + def image_filter(self, *args, **kwargs): image = canny(*args, **kwargs) self.overlay = image From 385382f64a0bf49f0583cfa1babf77132c5c06dd Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 22 Jul 2012 02:11:56 -0400 Subject: [PATCH 20/62] ENH: Simplify widget addition. --- skimage/viewer/plugins/base.py | 17 +++++++---------- skimage/viewer/plugins/canny.py | 6 +++--- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 14fda815..22ec3533 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -78,17 +78,14 @@ class Plugin(QtGui.QDialog): else: return param - def add_argument(self, name, low, high, **kwargs): - name, slider = self.add_slider(name, low, high, **kwargs) - self.arguments.append(slider) - - def add_keyword_argument(self, name, low, high, **kwargs): - name, slider = self.add_slider(name, low, high, **kwargs) - self.keyword_arguments[name] = slider - - def add_slider(self, name, low, high, **kwargs): + def add_widget(self, name, low, high, **kwargs): slider = Slider(name, low, high, **kwargs) - slider.callback = self.filter_image + if slider.ptype == 'kwarg': + self.keyword_arguments[name] = slider + slider.callback = self.filter_image + elif slider.ptype == 'arg': + self.keyword_arguments[name] = slider + self.arguments.append(slider) self.layout.addWidget(slider, self.row, 0) self.row += 1 return name.replace(' ', '_'), slider diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 08099795..278a7931 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -12,9 +12,9 @@ class CannyPlugin(OverlayPlugin): width = kwargs.get('width', 400) super(CannyPlugin, self).__init__(image_viewer, width=width, height=height) - self.add_keyword_argument('sigma', 0, 5, update_on='release') - self.add_keyword_argument('low_threshold', 0, 255, update_on='release') - self.add_keyword_argument('high_threshold', 0, 255, update_on='release') + self.add_widget('sigma', 0, 5, update_on='release') + self.add_widget('low_threshold', 0, 255, update_on='release') + self.add_widget('high_threshold', 0, 255, update_on='release') # Update image overlay to default slider values. self.filter_image() From 3271e210beddd1fd1fd953af1111810a589f9479 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 22 Jul 2012 02:15:16 -0400 Subject: [PATCH 21/62] ENH: Move closeEvent definition to base class. --- skimage/viewer/plugins/canny.py | 4 ---- skimage/viewer/plugins/overlayplugin.py | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 278a7931..020157a4 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -21,7 +21,3 @@ class CannyPlugin(OverlayPlugin): def image_filter(self, *args, **kwargs): image = canny(*args, **kwargs) self.overlay = image - - def closeEvent(self, event): - self.overlay = None - super(CannyPlugin, self).closeEvent(event) diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index a2b12630..b55d6587 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -35,3 +35,7 @@ class OverlayPlugin(Plugin): else: self._overlay_plot.set_array(image) self.image_viewer.redraw() + + def closeEvent(self, event): + self.overlay = None + super(OverlayPlugin, self).closeEvent(event) From 06449581bd4f1a7adbd8897ed2a244c660204980 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 22 Jul 2012 02:26:03 -0400 Subject: [PATCH 22/62] ENH: Generalize add_widget function. --- skimage/viewer/plugins/base.py | 19 +++++++++---------- skimage/viewer/plugins/canny.py | 7 ++++--- skimage/viewer/widgets/core.py | 5 +++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 22ec3533..43ad4c0a 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -78,17 +78,16 @@ class Plugin(QtGui.QDialog): else: return param - def add_widget(self, name, low, high, **kwargs): - slider = Slider(name, low, high, **kwargs) - if slider.ptype == 'kwarg': - self.keyword_arguments[name] = slider - slider.callback = self.filter_image - elif slider.ptype == 'arg': - self.keyword_arguments[name] = slider - self.arguments.append(slider) - self.layout.addWidget(slider, self.row, 0) + def add_widget(self, widget): + if widget.ptype == 'kwarg': + name = widget.name.replace(' ', '_') + self.keyword_arguments[name] = widget + widget.callback = self.filter_image + elif widget.ptype == 'arg': + self.arguments.append(widget) + widget.callback = self.filter_image + self.layout.addWidget(widget, self.row, 0) self.row += 1 - return name.replace(' ', '_'), slider def closeEvent(self, event): """Disconnect all artists and events from ImageViewer. diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 020157a4..175b749d 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -1,6 +1,7 @@ from skimage.filter import canny from .overlayplugin import OverlayPlugin +from ..widgets import Slider class CannyPlugin(OverlayPlugin): @@ -12,9 +13,9 @@ class CannyPlugin(OverlayPlugin): width = kwargs.get('width', 400) super(CannyPlugin, self).__init__(image_viewer, width=width, height=height) - self.add_widget('sigma', 0, 5, update_on='release') - self.add_widget('low_threshold', 0, 255, update_on='release') - self.add_widget('high_threshold', 0, 255, update_on='release') + self.add_widget(Slider('sigma', 0, 5, update_on='release')) + self.add_widget(Slider('low threshold', 0, 255, update_on='release')) + self.add_widget(Slider('high threshold', 0, 255, update_on='release')) # Update image overlay to default slider values. self.filter_image() diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index e68bda7f..9184ccf6 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -7,8 +7,9 @@ class Slider(IntelligentSlider): ---------- name : str Name of slider parameter. If this parameter is passed as a keyword - argument, it must match the name of that keyword argument. In addition, - this name is displayed as the name of the slider. + argument, it must match the name of that keyword argument (spaces are + replaced with underscores). In addition, this name is displayed as the + name of the slider. low, high : float Range of slider values. ptype : {'arg' | 'kwarg' | ...} From 86b428952dbfecb3623fb3daab8889f1340f1ad9 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 22 Jul 2012 13:24:41 -0400 Subject: [PATCH 23/62] ENH: Allow `Plugin.add_widget` to hook into Plugin attributes. The `ptype` parameter of widget can now be set to 'plugin'. When this is the case, the plugin will set a plugin attribute whenever the widget is updated. As an example, this commit adds a ComboBox widget which is hooked into the overlay color of the OverlayPlugin. --- skimage/viewer/plugins/base.py | 7 ++- skimage/viewer/plugins/canny.py | 3 +- skimage/viewer/plugins/overlayplugin.py | 30 ++++++++++-- skimage/viewer/utils/core.py | 7 +-- skimage/viewer/widgets/core.py | 65 ++++++++++++++++++++++++- 5 files changed, 99 insertions(+), 13 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 43ad4c0a..17387b8b 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -1,7 +1,6 @@ from PyQt4 import QtGui import matplotlib as mpl -from ..widgets import Slider class Plugin(QtGui.QDialog): @@ -43,6 +42,7 @@ class Plugin(QtGui.QDialog): if image_filter is not None: self.image_filter = image_filter + #TODO: Always passing image as first argument may be bad assumption. self.arguments = [image_viewer.original_image] self.keyword_arguments= {} @@ -86,9 +86,14 @@ class Plugin(QtGui.QDialog): elif widget.ptype == 'arg': self.arguments.append(widget) widget.callback = self.filter_image + elif widget.ptype == 'plugin': + widget.callback = self.update_plugin self.layout.addWidget(widget, self.row, 0) self.row += 1 + def update_plugin(self, name, value): + setattr(self, name, value) + def closeEvent(self, event): """Disconnect all artists and events from ImageViewer. diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 175b749d..97465e6a 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -1,7 +1,7 @@ from skimage.filter import canny from .overlayplugin import OverlayPlugin -from ..widgets import Slider +from ..widgets import Slider, ComboBox class CannyPlugin(OverlayPlugin): @@ -16,6 +16,7 @@ class CannyPlugin(OverlayPlugin): self.add_widget(Slider('sigma', 0, 5, update_on='release')) self.add_widget(Slider('low threshold', 0, 255, update_on='release')) self.add_widget(Slider('high threshold', 0, 255, update_on='release')) + self.add_widget(ComboBox('color', self.color_names, ptype='plugin')) # Update image overlay to default slider values. self.filter_image() diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index b55d6587..e9a6a42a 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -1,6 +1,5 @@ -from ..utils import clear_red - from .base import Plugin +from ..utils import ClearColormap class OverlayPlugin(Plugin): @@ -12,12 +11,19 @@ class OverlayPlugin(Plugin): Overlay displayed on top of image. This overlay defaults to a color map with alpha values varying linearly from 0 to 1. """ + colors = {'red': (1, 0, 0), + 'yellow': (1, 1, 0), + 'green': (0, 1, 0), + 'cyan': (0, 1, 1)} def __init__(self, image_viewer, **kwargs): Plugin.__init__(self, image_viewer, **kwargs) - self.overlay_cmap = clear_red self._overlay_plot = None self._overlay = None + self.cmap = None + self.color_names = self.colors.keys() + #TODO: `color` doesn't update GUI widget when set manually. + self.color = 0 @property def overlay(self): @@ -31,7 +37,7 @@ class OverlayPlugin(Plugin): ax.images.remove(self._overlay_plot) self._overlay_plot = None elif self._overlay_plot is None: - self._overlay_plot = ax.imshow(image, cmap=self.overlay_cmap) + self._overlay_plot = ax.imshow(image, cmap=self.cmap) else: self._overlay_plot.set_array(image) self.image_viewer.redraw() @@ -39,3 +45,19 @@ class OverlayPlugin(Plugin): def closeEvent(self, event): self.overlay = None super(OverlayPlugin, self).closeEvent(event) + + @property + def color(self): + return self._color + + @color.setter + def color(self, index): + # Update colormap whenever color is changed. + name = self.color_names[index] + self._color = name + rgb = self.colors[name] + self.cmap = ClearColormap(rgb) + + if self._overlay_plot is not None: + self._overlay_plot.set_cmap(self.cmap) + self.image_viewer.redraw() diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index f7dc6433..6a29b07c 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -3,7 +3,7 @@ import matplotlib.pyplot as plt from matplotlib.colors import LinearSegmentedColormap -__all__ = ['figimage', 'LinearColormap', 'ClearColormap', 'clear_red'] +__all__ = ['figimage', 'LinearColormap', 'ClearColormap'] def figimage(image, scale=1, dpi=None, **kwargs): @@ -65,13 +65,10 @@ class LinearColormap(LinearSegmentedColormap): class ClearColormap(LinearColormap): """Color map that varies linearly from alpha = 0 to 1 """ - def __init__(self, name, rgb): + def __init__(self, rgb, name='clear_color'): r, g, b = rgb cg_speq = {'blue': [(0.0, b), (1.0, b)], 'green': [(0.0, g), (1.0, g)], 'red': [(0.0, r), (1.0, r)], 'alpha': [(0.0, 0.0), (1.0, 1.0)]} LinearColormap.__init__(self, name, cg_speq) - -clear_red = ClearColormap('clear_red', (0.7, 0, 0)) - diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 9184ccf6..02d29545 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -1,5 +1,21 @@ +""" +Widgets for interacting with ImageViewer. + +These widgets should be added to a Plugin subclass using its `add_widget` +method. The Plugin will delegate action based on the widget's parameter type +specified by its `ptype` attribute, which can be: + + 'arg' : positional argument passed to Plugin's `filter_image` method. + 'kwarg' : keyword argument passed to Plugin's `filter_image` method. + 'plugin' : attribute of Plugin. You'll probably need to make the attribute + a class property that updates the display. + +""" +from PyQt4 import QtGui +from PyQt4 import QtCore from skimage.io._plugins.q_color_mixer import IntelligentSlider + class Slider(IntelligentSlider): """Slider widget. @@ -12,11 +28,56 @@ class Slider(IntelligentSlider): name of the slider. low, high : float Range of slider values. - ptype : {'arg' | 'kwarg' | ...} - Parameter + ptype : {'arg' | 'kwarg' | 'plugin'} + Parameter type. """ def __init__(self, name, low, high, ptype='kwarg', callback=None, **kwargs): self.ptype = ptype kwargs.setdefault('orientation', 'horizontal') scale = (high - low) / 1000.0 super(Slider, self).__init__(name, scale, low, callback, **kwargs) + + +class ComboBox(QtGui.QWidget): + """ComboBox widget for selecting among a list of choices. + + Parameters + ---------- + name : str + Name of slider parameter. If this parameter is passed as a keyword + argument, it must match the name of that keyword argument (spaces are + replaced with underscores). In addition, this name is displayed as the + name of the slider. + items: list + Allowed parameter values. + ptype : {'arg' | 'kwarg' | 'plugin'} + Parameter type. + """ + + def __init__(self, name, items, ptype='kwarg', callback=None): + super(ComboBox, self).__init__() + self.ptype = ptype + self.callback = callback + + self.name = name + self.name_label = QtGui.QLabel() + self.name_label.setText(self.name) + self.name_label.setAlignment(QtCore.Qt.AlignLeft) + + self._combo_box = QtGui.QComboBox() + self._combo_box.addItems(items) + + self.layout = QtGui.QHBoxLayout(self) + self.layout.addWidget(self.name_label) + self.layout.addWidget(self._combo_box, alignment=QtCore.Qt.AlignLeft) + + self._combo_box.currentIndexChanged.connect(self._value_changed) + # self.connect(self._combo_box, + # SIGNAL("currentIndexChanged(int)"), self.updateUi) + + @property + def val(self): + return self._combo_box.value() + + def _value_changed(self, value): + self.callback(self.name, value) From 9d1df0c3ce118b92fcdd6f2190a843adbbb88f54 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 22 Jul 2012 22:11:33 -0400 Subject: [PATCH 24/62] Make alpha value to ClearColormap adjustable. --- skimage/viewer/utils/core.py | 4 ++-- skimage/viewer/viewers/core.py | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index 6a29b07c..cdcc10bf 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -65,10 +65,10 @@ class LinearColormap(LinearSegmentedColormap): class ClearColormap(LinearColormap): """Color map that varies linearly from alpha = 0 to 1 """ - def __init__(self, rgb, name='clear_color'): + def __init__(self, rgb, max_alpha=1, name='clear_color'): r, g, b = rgb cg_speq = {'blue': [(0.0, b), (1.0, b)], 'green': [(0.0, g), (1.0, g)], 'red': [(0.0, r), (1.0, r)], - 'alpha': [(0.0, 0.0), (1.0, 1.0)]} + 'alpha': [(0.0, 0.0), (1.0, max_alpha)]} LinearColormap.__init__(self, name, cg_speq) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index f4d3b587..ac004987 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -10,10 +10,7 @@ qApp = None class ImageCanvas(FigureCanvasQTAgg): - """Canvas for displaying images. - - This canvas derives from Matplotlib, so your normal - """ + """Canvas for displaying images.""" def __init__(self, parent, image, **kwargs): self.fig, self.ax = figimage(image, **kwargs) @@ -91,7 +88,6 @@ class ImageViewer(QtGui.QMainWindow): self.layout = QtGui.QVBoxLayout(self.main_widget) self.layout.addWidget(self.canvas) - #TODO: Set status bar to fixed-width font so status doesn't wiggle status_bar = self.statusBar() self.status_message = status_bar.showMessage sb_size = status_bar.sizeHint() @@ -109,7 +105,7 @@ class ImageViewer(QtGui.QMainWindow): self.move(0, 0) w = size.width() y = 0 - #TODO: Layout isn't correct for multiple plots (overlaps). + #TODO: Layout isn't correct for multiple plugins (overlaps). for p in self.plugins: p.move(w, y) y += p.geometry().height() From f47312a3d1f730ca8766dcbe4a2bbefc0b14fcfd Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 23 Jul 2012 00:12:21 -0400 Subject: [PATCH 25/62] API Change: Attach ImageViewer to Plugin after init. Plugin is now added to the viewer using an inplace add on the viewer instead of on initialization of the plugin. This change means that operations requiring the viewer must be delayed until attach operation. --- skimage/viewer/plugins/base.py | 25 +++++++------ skimage/viewer/plugins/canny.py | 13 ++++--- skimage/viewer/plugins/lineprofile.py | 47 +++++++++++++------------ skimage/viewer/plugins/overlayplugin.py | 7 ++-- skimage/viewer/plugins/plotplugin.py | 5 +-- skimage/viewer/viewers/core.py | 4 +++ viewer_examples/plugins/canny.py | 2 +- viewer_examples/plugins/lineprofile.py | 2 +- 8 files changed, 61 insertions(+), 44 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 17387b8b..ba68ecb6 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -1,4 +1,5 @@ from PyQt4 import QtGui +from PyQt4.QtCore import Qt import matplotlib as mpl @@ -23,17 +24,13 @@ class Plugin(QtGui.QDialog): ---------- image_viewer : ImageViewer Window containing image used in measurement. - image : array - Image used in measurement/manipulation. """ name = 'Plugin' draws_on_image = False - def __init__(self, image_viewer, image_filter=None, height=100, width=400, - useblit=None): - self.image_viewer = image_viewer - QtGui.QDialog.__init__(self, image_viewer) - self.image_viewer.plugins.append(self) + def __init__(self, image_filter=None, height=100, width=400, useblit=None): + QtGui.QDialog.__init__(self) + self.image_viewer = None self.setWindowTitle(self.name) self.layout = QtGui.QGridLayout(self) @@ -42,18 +39,24 @@ class Plugin(QtGui.QDialog): if image_filter is not None: self.image_filter = image_filter - #TODO: Always passing image as first argument may be bad assumption. - self.arguments = [image_viewer.original_image] + self.arguments = [] self.keyword_arguments= {} - 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 = [] + def attach(self, image_viewer): + self.setParent(image_viewer) + self.setWindowFlags(Qt.Dialog) + + self.image_viewer = image_viewer + self.image_viewer.plugins.append(self) + #TODO: Always passing image as first argument may be bad assumption. + self.arguments.append(self.image_viewer.original_image) + if self.draws_on_image: self.connect_event('draw_event', self.on_draw) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 97465e6a..5ec6e415 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -8,15 +8,18 @@ class CannyPlugin(OverlayPlugin): name = 'Canny Filter' - def __init__(self, image_viewer, *args, **kwargs): - height = kwargs.get('height', 100) - width = kwargs.get('width', 400) - super(CannyPlugin, self).__init__(image_viewer, - width=width, height=height) + def __init__(self, *args, **kwargs): + kwargs.setdefault('height', 100) + kwargs.setdefault('width', 400) + super(CannyPlugin, self).__init__(**kwargs) + self.add_widget(Slider('sigma', 0, 5, update_on='release')) self.add_widget(Slider('low threshold', 0, 255, update_on='release')) self.add_widget(Slider('high threshold', 0, 255, update_on='release')) self.add_widget(ComboBox('color', self.color_names, ptype='plugin')) + + def attach(self, image_viewer): + super(CannyPlugin, self).attach(image_viewer) # Update image overlay to default slider values. self.filter_image() diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 32ea967d..f41407b3 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -33,43 +33,46 @@ class LineProfile(PlotPlugin): name = 'Line Profile' draws_on_image = True - def __init__(self, image_viewer, useblit=None, - linewidth=1, epsilon=5, limits='image'): - super(LineProfile, self).__init__(image_viewer, height=200, width=600, + def __init__(self, useblit=None, linewidth=1, epsilon=5, limits='image'): + super(LineProfile, self).__init__(height=200, width=600, useblit=useblit) - self.linewidth = linewidth self.epsilon = epsilon + self._active_pt = None + self._limit_type = limits + self.line_kwargs = dict(color='y', lw=linewidth, alpha=0.5, marker='s', + markersize=5, solid_capstyle='butt') + print self.help() - if limits == 'image': - self.limits = (np.min(self.image), np.max(self.image)) - elif limits == 'dtype': - self.limits = dtype_range[self.image.dtype.type] - elif limits is None or len(limits) == 2: - self.limits = limits + def attach(self, image_viewer): + super(LineProfile, self).attach(image_viewer) + + image = image_viewer.original_image + + if self._limit_type == 'image': + self.limits = (np.min(image), np.max(image)) + elif self._limit_type == 'dtype': + self.self._limit_type = dtype_range[image.dtype.type] + elif self._limit_type is None or len(self._limit_type) == 2: + self.limits = self._limit_type else: - raise ValueError("Unrecognized `limits`: %s" % limits) + raise ValueError("Unrecognized `limits`: %s" % self._limit_type) - if not limits is None: + if not self._limit_type is None: self.ax.set_ylim(self.limits) - h, w = self.image.shape - + h, w = image.shape self._init_end_pts = np.array([[w/3, h/2], [2*w/3, h/2]]) self.end_pts = self._init_end_pts.copy() x, y = np.transpose(self.end_pts) - self.scan_line = self.image_viewer.ax.plot(x, y, 'y-s', markersize=5, - lw=linewidth, alpha=0.5, - solid_capstyle='butt')[0] + self.scan_line = image_viewer.ax.plot(x, y, **self.line_kwargs)[0] self.artists.append(self.scan_line) - scan_data = profile_line(self.image, self.end_pts) + scan_data = profile_line(image, self.end_pts) self.profile = self.ax.plot(scan_data, 'k-')[0] self._autoscale_view() - self._active_pt = None - self.connect_event('key_press_event', self.on_key_press) self.connect_event('button_press_event', self.on_mouse_press) self.connect_event('button_release_event', self.on_mouse_release) @@ -77,7 +80,6 @@ class LineProfile(PlotPlugin): self.connect_event('scroll_event', self.on_scroll) self.image_viewer.redraw() - print self.help() def help(self): helpstr = ("Line profile tool", @@ -169,7 +171,8 @@ class LineProfile(PlotPlugin): self.scan_line.set_data(np.transpose(self.end_pts)) self.scan_line.set_linewidth(self.linewidth) - scan = profile_line(self.image, self.end_pts, linewidth=self.linewidth) + scan = profile_line(self.image_viewer.original_image, self.end_pts, + linewidth=self.linewidth) self.profile.set_xdata(np.arange(scan.shape[0])) self.profile.set_ydata(scan) diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index e9a6a42a..3b051e1a 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -16,12 +16,15 @@ class OverlayPlugin(Plugin): 'green': (0, 1, 0), 'cyan': (0, 1, 1)} - def __init__(self, image_viewer, **kwargs): - Plugin.__init__(self, image_viewer, **kwargs) + def __init__(self, **kwargs): + super(OverlayPlugin, self).__init__(**kwargs) self._overlay_plot = None self._overlay = None self.cmap = None self.color_names = self.colors.keys() + + def attach(self, image_viewer): + super(OverlayPlugin, self).attach(image_viewer) #TODO: `color` doesn't update GUI widget when set manually. self.color = 0 diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py index 38762dc4..66afa676 100644 --- a/skimage/viewer/plugins/plotplugin.py +++ b/skimage/viewer/plugins/plotplugin.py @@ -40,8 +40,9 @@ class PlotPlugin(Plugin): image : array Image used in measurement/manipulation. """ - def __init__(self, image_viewer, **kwargs): - Plugin.__init__(self, image_viewer, **kwargs) + + def attach(self, image_viewer): + super(PlotPlugin, self).attach(image_viewer) # Add plot for displaying intensity profile. self.add_plot() diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index ac004987..fe22d750 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -96,6 +96,10 @@ class ImageViewer(QtGui.QMainWindow): self.connect_event('motion_notify_event', self.update_status_bar) + def __iadd__(self, plugin): + plugin.attach(self) + return self + def closeEvent(self, ce): self.close() diff --git a/viewer_examples/plugins/canny.py b/viewer_examples/plugins/canny.py index e84a8270..eaa33330 100644 --- a/viewer_examples/plugins/canny.py +++ b/viewer_examples/plugins/canny.py @@ -5,5 +5,5 @@ from skimage.viewer.plugins.canny import CannyPlugin image = data.camera() viewer = ImageViewer(image) -CannyPlugin(viewer) +viewer += CannyPlugin() viewer.show() diff --git a/viewer_examples/plugins/lineprofile.py b/viewer_examples/plugins/lineprofile.py index 310c23e4..2f1b2cdc 100644 --- a/viewer_examples/plugins/lineprofile.py +++ b/viewer_examples/plugins/lineprofile.py @@ -5,5 +5,5 @@ from skimage.viewer.plugins.lineprofile import LineProfile image = data.camera() viewer = ImageViewer(image) -LineProfile(viewer) +viewer += LineProfile() viewer.show() From df18d402906266f9b762872eae0e674506aa3169 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 23 Jul 2012 00:18:32 -0400 Subject: [PATCH 26/62] Minor cleanup. --- skimage/viewer/plugins/base.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index ba68ecb6..7532472b 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -76,10 +76,7 @@ class Plugin(QtGui.QDialog): self.image_filter(*arguments, **kwargs) def _get_value(self, param): - if hasattr(param, 'val'): - return param.val() - else: - return param + return param if not hasattr(param, 'val') else param.val() def add_widget(self, widget): if widget.ptype == 'kwarg': From 977d17134df25d691fc3ee84b7366a48a60de677 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 23 Jul 2012 00:45:11 -0400 Subject: [PATCH 27/62] Change image_viewer to Plugin property. Raise an error when using Plugin.image_viewer before it is set. This error prevents other, more obscure, errors from getting raised. --- skimage/viewer/plugins/base.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 7532472b..560eec8c 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -48,6 +48,16 @@ class Plugin(QtGui.QDialog): self.cids = [] self.artists = [] + @property + def image_viewer(self): + if self._image_viewer is None: + raise RuntimeError("Plugin is not attached to ImageViewer") + return self._image_viewer + + @image_viewer.setter + def image_viewer(self, image_viewer): + self._image_viewer = image_viewer + def attach(self, image_viewer): self.setParent(image_viewer) self.setWindowFlags(Qt.Dialog) From 92ca8374711b775364bd5b544b4c7c7a0d0e67a1 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 23 Jul 2012 01:13:03 -0400 Subject: [PATCH 28/62] ENH: Let Qt handle most of the window sizing. --- skimage/viewer/plugins/base.py | 9 +++++---- skimage/viewer/plugins/canny.py | 2 -- skimage/viewer/plugins/lineprofile.py | 3 +-- skimage/viewer/plugins/plotplugin.py | 1 + 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 560eec8c..5a626480 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -28,16 +28,17 @@ class Plugin(QtGui.QDialog): name = 'Plugin' draws_on_image = False - def __init__(self, image_filter=None, height=100, width=400, useblit=None): - QtGui.QDialog.__init__(self) + def __init__(self, image_filter=None, height=0, width=400, useblit=None): + super(Plugin, self).__init__() + self.image_viewer = None + if image_filter is not None: + self.image_filter = image_filter self.setWindowTitle(self.name) self.layout = QtGui.QGridLayout(self) self.resize(width, height) self.row = 0 - if image_filter is not None: - self.image_filter = image_filter self.arguments = [] self.keyword_arguments= {} diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 5ec6e415..b161fd87 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -9,8 +9,6 @@ class CannyPlugin(OverlayPlugin): name = 'Canny Filter' def __init__(self, *args, **kwargs): - kwargs.setdefault('height', 100) - kwargs.setdefault('width', 400) super(CannyPlugin, self).__init__(**kwargs) self.add_widget(Slider('sigma', 0, 5, update_on='release')) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index f41407b3..7b2e1c10 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -34,8 +34,7 @@ class LineProfile(PlotPlugin): draws_on_image = True def __init__(self, useblit=None, linewidth=1, epsilon=5, limits='image'): - super(LineProfile, self).__init__(height=200, width=600, - useblit=useblit) + super(LineProfile, self).__init__(useblit=useblit) self.linewidth = linewidth self.epsilon = epsilon self._active_pt = None diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py index 66afa676..5c1ce7bb 100644 --- a/skimage/viewer/plugins/plotplugin.py +++ b/skimage/viewer/plugins/plotplugin.py @@ -23,6 +23,7 @@ class PlotCanvas(FigureCanvasQTAgg): FigureCanvasQTAgg.updateGeometry(self) # Note: `setParent` must be called after `FigureCanvasQTAgg.__init__`. self.setParent(parent) + self.setMinimumHeight(150) class PlotPlugin(Plugin): From 36b0fbd84e86c6bca62ce435d4c87906ca93b721 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 23 Jul 2012 01:22:05 -0400 Subject: [PATCH 29/62] Rename (dis)connect_event to (dis)connect_image_events. This clarifies action since these events are on the image viewer, not the plugin. --- skimage/viewer/plugins/base.py | 14 +++++++------- skimage/viewer/plugins/lineprofile.py | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 5a626480..fd7cb005 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -69,7 +69,7 @@ class Plugin(QtGui.QDialog): self.arguments.append(self.image_viewer.original_image) if self.draws_on_image: - self.connect_event('draw_event', self.on_draw) + self.connect_image_event('draw_event', self.on_draw) def on_draw(self, event): """Save image background when blitting. @@ -108,17 +108,17 @@ class Plugin(QtGui.QDialog): def closeEvent(self, event): """Disconnect all artists and events from ImageViewer. - Note that events must be connected using `self.connect_event` and + Note that events must be connected using `self.connect_image_event` and artists must be appended to `self.artists`. """ self.disconnect_image_events() - self.remove_artists() + self.remove_image_artists() self.image_viewer.plugins.remove(self) self.image_viewer.redraw() self.close() - def connect_event(self, event, callback): - """Connect callback with an event. + def connect_image_event(self, event, callback): + """Connect callback with an event in the image viewer. This should be used in lieu of `figure.canvas.mpl_connect` since this function stores call back ids for later clean up. @@ -138,7 +138,7 @@ class Plugin(QtGui.QDialog): for c in self.cids: self.image_viewer.disconnect_event(c) - def remove_artists(self): - """Disconnect artists that are connected to the *image plot*.""" + def remove_image_artists(self): + """Disconnect artists that are connected to the image viewer.""" for a in self.artists: self.image_viewer.remove_artist(a) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 7b2e1c10..5afe78f4 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -72,11 +72,11 @@ class LineProfile(PlotPlugin): self.profile = self.ax.plot(scan_data, 'k-')[0] self._autoscale_view() - self.connect_event('key_press_event', self.on_key_press) - self.connect_event('button_press_event', self.on_mouse_press) - self.connect_event('button_release_event', self.on_mouse_release) - self.connect_event('motion_notify_event', self.on_move) - self.connect_event('scroll_event', self.on_scroll) + self.connect_image_event('key_press_event', self.on_key_press) + self.connect_image_event('button_press_event', self.on_mouse_press) + self.connect_image_event('button_release_event', self.on_mouse_release) + self.connect_image_event('motion_notify_event', self.on_move) + self.connect_image_event('scroll_event', self.on_scroll) self.image_viewer.redraw() From daae405945adce516b080492fe9f0afce0287955 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 23 Jul 2012 21:55:30 -0400 Subject: [PATCH 30/62] DOC: Add todo note --- skimage/viewer/widgets/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 02d29545..e2086b4f 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -16,6 +16,8 @@ from PyQt4 import QtCore from skimage.io._plugins.q_color_mixer import IntelligentSlider +#TODO: Add WidgetBase class (requires reimplementation of IntelligentSlider). + class Slider(IntelligentSlider): """Slider widget. From 0e8f444fbb4ef53338289fa6372be5d4ed389993 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 24 Jul 2012 00:28:37 -0400 Subject: [PATCH 31/62] ENH: Display overlay by default --- skimage/viewer/plugins/base.py | 10 +++++++++- skimage/viewer/plugins/canny.py | 6 +----- skimage/viewer/plugins/overlayplugin.py | 4 ++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index fd7cb005..d5db0f5c 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -84,7 +84,12 @@ class Plugin(QtGui.QDialog): arguments = [self._get_value(a) for a in self.arguments] kwargs = dict([(name, self._get_value(a)) for name, a in self.keyword_arguments.iteritems()]) - self.image_filter(*arguments, **kwargs) + filtered = self.image_filter(*arguments, **kwargs) + self.display_filtered_image(filtered) + + def display_filtered_image(self, image): + """Override this method to display image on image viewer.""" + pass def _get_value(self, param): return param if not hasattr(param, 'val') else param.val() @@ -102,6 +107,9 @@ class Plugin(QtGui.QDialog): self.layout.addWidget(widget, self.row, 0) self.row += 1 + def __iadd__(self, widget): + self.add_widget(widget) + def update_plugin(self, name, value): setattr(self, name, value) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index b161fd87..154fb6da 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -9,7 +9,7 @@ class CannyPlugin(OverlayPlugin): name = 'Canny Filter' def __init__(self, *args, **kwargs): - super(CannyPlugin, self).__init__(**kwargs) + super(CannyPlugin, self).__init__(image_filter=canny, **kwargs) self.add_widget(Slider('sigma', 0, 5, update_on='release')) self.add_widget(Slider('low threshold', 0, 255, update_on='release')) @@ -20,7 +20,3 @@ class CannyPlugin(OverlayPlugin): super(CannyPlugin, self).attach(image_viewer) # Update image overlay to default slider values. self.filter_image() - - def image_filter(self, *args, **kwargs): - image = canny(*args, **kwargs) - self.overlay = image diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index 3b051e1a..3aa5a445 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -45,6 +45,10 @@ class OverlayPlugin(Plugin): self._overlay_plot.set_array(image) self.image_viewer.redraw() + def display_filtered_image(self, image): + """Display image over image in viewer.""" + self.overlay = image + def closeEvent(self, event): self.overlay = None super(OverlayPlugin, self).closeEvent(event) From 1ae662f712d20e1a2b50f21726214be96fe297ea Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 24 Jul 2012 00:35:26 -0400 Subject: [PATCH 32/62] ENH: filter image when Plugin is attached to ImageViewer --- skimage/viewer/plugins/base.py | 7 +++++-- skimage/viewer/plugins/canny.py | 5 ----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index d5db0f5c..18768530 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -32,8 +32,7 @@ class Plugin(QtGui.QDialog): super(Plugin, self).__init__() self.image_viewer = None - if image_filter is not None: - self.image_filter = image_filter + self.image_filter = image_filter self.setWindowTitle(self.name) self.layout = QtGui.QGridLayout(self) @@ -70,6 +69,8 @@ class Plugin(QtGui.QDialog): if self.draws_on_image: self.connect_image_event('draw_event', self.on_draw) + # Call filter so that filtered image matches widget values + self.filter_image() def on_draw(self, event): """Save image background when blitting. @@ -81,6 +82,8 @@ class Plugin(QtGui.QDialog): self.img_background = self.image_viewer.canvas.copy_from_bbox(bbox) def filter_image(self, *args): + if self.image_filter is None: + return arguments = [self._get_value(a) for a in self.arguments] kwargs = dict([(name, self._get_value(a)) for name, a in self.keyword_arguments.iteritems()]) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 154fb6da..3431c6af 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -15,8 +15,3 @@ class CannyPlugin(OverlayPlugin): self.add_widget(Slider('low threshold', 0, 255, update_on='release')) self.add_widget(Slider('high threshold', 0, 255, update_on='release')) self.add_widget(ComboBox('color', self.color_names, ptype='plugin')) - - def attach(self, image_viewer): - super(CannyPlugin, self).attach(image_viewer) - # Update image overlay to default slider values. - self.filter_image() From 4b3f6d6c3016e5dee2ce893f96e1b494842f1e43 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 24 Jul 2012 00:51:31 -0400 Subject: [PATCH 33/62] BUG: in-place add should return object Actually, I didn't mean to add `__iadd__` a couple of commits ago, so this was supposed to be an enhancement that allows you to access `add_widget` using in place adding. --- skimage/viewer/plugins/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 18768530..749d5741 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -92,7 +92,7 @@ class Plugin(QtGui.QDialog): def display_filtered_image(self, image): """Override this method to display image on image viewer.""" - pass + self.image_viewer.image = image def _get_value(self, param): return param if not hasattr(param, 'val') else param.val() @@ -112,6 +112,7 @@ class Plugin(QtGui.QDialog): def __iadd__(self, widget): self.add_widget(widget) + return self def update_plugin(self, name, value): setattr(self, name, value) From 6182cc9f1b58f96479a46dea03ffe98a6244e7e0 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 24 Jul 2012 00:55:48 -0400 Subject: [PATCH 34/62] ENH: Add example of adding widgets to plugin --- viewer_examples/plugins/canny_simple.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 viewer_examples/plugins/canny_simple.py diff --git a/viewer_examples/plugins/canny_simple.py b/viewer_examples/plugins/canny_simple.py new file mode 100644 index 00000000..49bc15eb --- /dev/null +++ b/viewer_examples/plugins/canny_simple.py @@ -0,0 +1,20 @@ +from skimage import data +from skimage.filter import canny + +from skimage.viewer import ImageViewer +from skimage.viewer.widgets import Slider +from skimage.viewer.plugins.overlayplugin import OverlayPlugin + + +image = data.camera() +# Note: ImageViewer must be called before Plugin b/c it starts the event loop. +viewer = ImageViewer(image) +# You can create a UI for a filter just by passing a filter function... +plugin = OverlayPlugin(image_filter=canny) +# ... and adding widgets to adjust parameter values. +plugin += Slider('sigma', 0, 5, update_on='release') +plugin += Slider('low threshold', 0, 255, update_on='release') +plugin += Slider('high threshold', 0, 255, update_on='release') +# Finally, attach the plugin to the image viewer. +viewer += plugin +viewer.show() From c1a859acae6c2c9d2525e159f1f471a673088de9 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 24 Jul 2012 00:58:35 -0400 Subject: [PATCH 35/62] ENH: Change inplace-add to normal add to support alternate syntax Widgets can be added to Plugins inline, and Plugins can be added inline to Viewers. --- skimage/viewer/plugins/base.py | 2 +- skimage/viewer/viewers/core.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 749d5741..d9649b22 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -110,7 +110,7 @@ class Plugin(QtGui.QDialog): self.layout.addWidget(widget, self.row, 0) self.row += 1 - def __iadd__(self, widget): + def __add__(self, widget): self.add_widget(widget) return self diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index fe22d750..3fd9724e 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -96,7 +96,7 @@ class ImageViewer(QtGui.QMainWindow): self.connect_event('motion_notify_event', self.update_status_bar) - def __iadd__(self, plugin): + def __add__(self, plugin): plugin.attach(self) return self From c2d2919bae8a029910ab23fdc5a6c1e177d6ce9f Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 24 Jul 2012 01:31:47 -0400 Subject: [PATCH 36/62] BUG: Don't override `image_filter` method if defined by subclass --- skimage/viewer/plugins/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index d9649b22..cb76ffc3 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -32,7 +32,9 @@ class Plugin(QtGui.QDialog): super(Plugin, self).__init__() self.image_viewer = None - self.image_filter = image_filter + # If subclass defines `image_filter` method ignore input. + if not hasattr(self, 'image_filter'): + self.image_filter = image_filter self.setWindowTitle(self.name) self.layout = QtGui.QGridLayout(self) From ff98b059e71c35530a01dbc46b3cdc922c0b3292 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 24 Jul 2012 23:06:53 -0400 Subject: [PATCH 37/62] STY: Remove unused `add_artist` method. --- skimage/viewer/viewers/core.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 3fd9724e..7b2b7f9a 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -1,3 +1,6 @@ +""" +ImageViewer class for viewing and interacting with images. +""" import sys from PyQt4 import QtGui, QtCore @@ -42,9 +45,16 @@ class ImageViewer(QtGui.QMainWindow): image : array Image being viewed. Setting this value will update the displayed frame. original_image : array - Plugins typically operate on (but don't change) the original image. + Plugins typically operate on (but don't change) the *original* image. plugins : list List of attached plugins. + + Examples + -------- + >>> viewer = ImageViewer(image) + >>> viewer += SomePlugin() + >>> viewer.show() + """ def __init__(self, image): # Start main loop @@ -97,10 +107,11 @@ class ImageViewer(QtGui.QMainWindow): self.connect_event('motion_notify_event', self.update_status_bar) def __add__(self, plugin): + """Add plugin to ImageViewer""" plugin.attach(self) return self - def closeEvent(self, ce): + def closeEvent(self, event): self.close() def auto_layout(self): @@ -109,12 +120,13 @@ class ImageViewer(QtGui.QMainWindow): self.move(0, 0) w = size.width() y = 0 - #TODO: Layout isn't correct for multiple plugins (overlaps). + #TODO: Layout isn't quite correct for multiple plugins (overlaps). for p in self.plugins: p.move(w, y) y += p.geometry().height() def show(self): + """Show ImageViewer and attached plugins.""" self.auto_layout() for p in self.plugins: p.show() @@ -143,12 +155,10 @@ class ImageViewer(QtGui.QMainWindow): """Disconnect callback by its id (returned by `connect_event`).""" self.canvas.mpl_disconnect(callback_id) - def add_artist(self, artist): - """Add matplotlib artist to image viewer.""" - self.ax.add_artist(artist) - def remove_artist(self, artist): - """Disconnect matplotlib artist from image viewer.""" + """Disconnect matplotlib artist from image viewer. + + """ # There's probably a smarter way to do this. for artist_list in self._axes_artists: if artist in artist_list: From 86526064f263c687a2134440d4db3a81cdb51bef Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 24 Jul 2012 23:22:55 -0400 Subject: [PATCH 38/62] DOC: Improve docstrings for ImageViewer. Oops: also changed added leading underscore to `update_status_bar`. --- skimage/viewer/viewers/core.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 7b2b7f9a..1046ca7c 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -104,7 +104,7 @@ class ImageViewer(QtGui.QMainWindow): cs_size = self.canvas.sizeHint() self.resize(cs_size.width(), cs_size.height() + sb_size.height()) - self.connect_event('motion_notify_event', self.update_status_bar) + self.connect_event('motion_notify_event', self._update_status_bar) def __add__(self, plugin): """Add plugin to ImageViewer""" @@ -126,7 +126,10 @@ class ImageViewer(QtGui.QMainWindow): y += p.geometry().height() def show(self): - """Show ImageViewer and attached plugins.""" + """Show ImageViewer and attached plugins. + + This behaves much like `matplotlib.pyplot.show` and `QWidget.show`. + """ self.auto_layout() for p in self.plugins: p.show() @@ -158,13 +161,23 @@ class ImageViewer(QtGui.QMainWindow): def remove_artist(self, artist): """Disconnect matplotlib artist from image viewer. + The `closeEvent` method of a Plugin should remove artists (Matplotlib + lines, markers, etc.) from the viewer so that they aren't stranded. + + Parameters + ---------- + artist : Matplotlib Artist + Artists created by Matplotlib functions (e.g., `plot` returns list + of `Line2D` artists) should be saved by the plugin for removal. """ - # There's probably a smarter way to do this. + # Note: an `add_artist` method is unnecessary since Matplotlib + + # There's probably a smarter way to find where the artist is stored. for artist_list in self._axes_artists: if artist in artist_list: artist_list.remove(artist) - def update_status_bar(self, event): + def _update_status_bar(self, event): if event.inaxes and event.inaxes.get_navigate(): self.status_message(self._format_coord(event.xdata, event.ydata)) else: From 51e61b3e46faa5ffd154a4ec8bc7bd7c4cabceeb Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 25 Jul 2012 00:21:56 -0400 Subject: [PATCH 39/62] DOC: Improve docstrings for Plugin class. --- skimage/viewer/plugins/base.py | 145 +++++++++++++++++++++++++-------- 1 file changed, 112 insertions(+), 33 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index cb76ffc3..2e7c8980 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -1,3 +1,6 @@ +""" +Base class for Plugins that interact with ImageViewer. +""" from PyQt4 import QtGui from PyQt4.QtCore import Qt @@ -5,25 +8,64 @@ import matplotlib as mpl class Plugin(QtGui.QDialog): - """Base class for widgets that interact with the axes. + """Base class for plugins that interact with an ImageViewer. + + A plugin connects an image filter (or another function) to an image viewer. + Note that a Plugin is initialized *without* an image viewer and attached in + a later step. See example below for details. Parameters ---------- - image_viewer : ImageViewer instance. + image_viewer : ImageViewer Window containing image used in measurement/manipulation. - callback : function - Function that gets called to update ImageViewer. Alternatively, this - can also be defined as a method in a Plugin subclass. + image_filter : function + Function that gets called to update image in image viewer. This value + can be `None` if, for example, you have a plugin that extracts + information from an image and doesn't manipulate it. Alternatively, + this function can be defined as a method in a Plugin subclass. height, width : int - Size of plugin window in pixels. + Size of plugin window in pixels. Note that Qt will automatically resize + a window to fit components. So if you're adding rows of components, you + can leave `height = 0` and just let Qt determine the final height. 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. + Matplotlib backends. If None, set to True when using Agg backend. + This only has an effect if you draw on top of an image viewer. Attributes ---------- image_viewer : ImageViewer Window containing image used in measurement. + name : str + Name of plugin. This is displayed as the window title. + artist : list + List of Matplotlib artists. Any artists created by the plugin should + be added to this list so that it gets cleaned up on close. + + Examples + -------- + >>> def my_func(image, arg1, arg2, optional_arg=0): + >>> ... + >>> + >>> viewer = ImageViewer(image) + >>> + >>> plugin = Plugin(image_filter=my_func) + >>> plugin += Widget('arg1', ..., ptype='arg') + >>> plugin += Widget('arg2', ..., ptype='arg') + >>> plugin += Widget('optional_arg', ..., ptype='kwarg') + >>> + >>> viewer.show() + + The plugin will automatically delegate parameters to `image_filter` based + on its parameter type, i.e., `ptype` (widgets for required arguments must + be added in the order they appear in the function). The image attached + to the viewer is **automatically passed as the first argument** to the + filter function. + + #TODO: Add flag so image is not passed to filter function by default. + + `ptype = 'kwarg'` is the default for most widgets so it's unnecessary here. + """ name = 'Plugin' draws_on_image = False @@ -61,6 +103,16 @@ class Plugin(QtGui.QDialog): self._image_viewer = image_viewer def attach(self, image_viewer): + """Attach the plugin to an ImageViewer. + + Note that the ImageViewer will automatically call this method when the + plugin is added to the ImageViewer. For example: + + >>> viewer += Plugin(...) + + Also note that `attach` automatically calls the filter function so that + the image matches the filtered value specified by attached widgets. + """ self.setParent(image_viewer) self.setWindowFlags(Qt.Dialog) @@ -74,32 +126,16 @@ class Plugin(QtGui.QDialog): # Call filter so that filtered image matches widget values self.filter_image() - 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 filter_image(self, *args): - if self.image_filter is None: - return - arguments = [self._get_value(a) for a in self.arguments] - kwargs = dict([(name, self._get_value(a)) - for name, a in self.keyword_arguments.iteritems()]) - filtered = self.image_filter(*arguments, **kwargs) - self.display_filtered_image(filtered) - - def display_filtered_image(self, image): - """Override this method to display image on image viewer.""" - self.image_viewer.image = image - - def _get_value(self, param): - return param if not hasattr(param, 'val') else param.val() - def add_widget(self, widget): + """Add widget to plugin. + + Alternatively, Plugin's `__add__` method is overloaded to add widgets: + + >>> plugin += Widget(...) + + Widgets can adjust required or optional arguments of filter function or + parameters for the plugin. This is specified by the Widget's `ptype'. + """ if widget.ptype == 'kwarg': name = widget.name.replace(' ', '_') self.keyword_arguments[name] = widget @@ -116,11 +152,54 @@ class Plugin(QtGui.QDialog): self.add_widget(widget) return self + 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 filter_image(self, *widget_arg): + """Call `image_filter` with widget args and kwargs + + Note: `display_filtered_image` is automatically called. + """ + # `widget_arg` is passed by the active widget but is unused since all + # filter arguments are pulled directly from attached the widgets. + + if self.image_filter is None: + return + arguments = [self._get_value(a) for a in self.arguments] + kwargs = dict([(name, self._get_value(a)) + for name, a in self.keyword_arguments.iteritems()]) + filtered = self.image_filter(*arguments, **kwargs) + self.display_filtered_image(filtered) + + def _get_value(self, param): + # If param is a widget, return its `val` attribute. + return param if not hasattr(param, 'val') else param.val() + + def display_filtered_image(self, image): + """Display the filtered image on image viewer. + + If you don't want to simply replace the displayed image with the + filtered image (e.g., you want to display a transparent overlay), + you can override this method. + """ + self.image_viewer.image = image + def update_plugin(self, name, value): + """Update keyword parameters of the plugin itself. + + These parameters will typically be implemented as class properties so + that they update the image or some other component. + """ setattr(self, name, value) def closeEvent(self, event): - """Disconnect all artists and events from ImageViewer. + """On close disconnect all artists and events from ImageViewer. Note that events must be connected using `self.connect_image_event` and artists must be appended to `self.artists`. From 260a336eb91edbfe0fe0e7ddb1c9809a1a0ac8b9 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 25 Jul 2012 00:31:08 -0400 Subject: [PATCH 40/62] ENH: allow color to be set by name --- skimage/viewer/plugins/overlayplugin.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index 3aa5a445..1473ca51 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -5,11 +5,18 @@ from ..utils import ClearColormap class OverlayPlugin(Plugin): """Plugin for ImageViewer that displays an overlay on top of main image. + The base Plugin class displays the filtered image directly on the viewer. + OverlayPlugin will instead overlay an image with a transparent colormap. + + See base Plugin class for additional details. + Attributes ---------- overlay : array Overlay displayed on top of image. This overlay defaults to a color map with alpha values varying linearly from 0 to 1. + color : int + Color of overlay. """ colors = {'red': (1, 0, 0), 'yellow': (1, 1, 0), @@ -46,10 +53,11 @@ class OverlayPlugin(Plugin): self.image_viewer.redraw() def display_filtered_image(self, image): - """Display image over image in viewer.""" + """Display filtered image as an overlay on top of image in viewer.""" self.overlay = image def closeEvent(self, event): + # clear overlay from ImageViewer on close self.overlay = None super(OverlayPlugin, self).closeEvent(event) @@ -60,7 +68,10 @@ class OverlayPlugin(Plugin): @color.setter def color(self, index): # Update colormap whenever color is changed. - name = self.color_names[index] + if isinstance(index, basestring) and index not in self.color_names: + raise ValueError("%s not defined in OverlayPlugin.colors" % index) + else: + name = self.color_names[index] self._color = name rgb = self.colors[name] self.cmap = ClearColormap(rgb) From d72baa484f566507a0310a8ea3145498396d3d16 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 25 Jul 2012 00:32:26 -0400 Subject: [PATCH 41/62] STY: reorder methods for clarity. --- skimage/viewer/plugins/overlayplugin.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index 1473ca51..dbf15e7d 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -52,15 +52,6 @@ class OverlayPlugin(Plugin): self._overlay_plot.set_array(image) self.image_viewer.redraw() - def display_filtered_image(self, image): - """Display filtered image as an overlay on top of image in viewer.""" - self.overlay = image - - def closeEvent(self, event): - # clear overlay from ImageViewer on close - self.overlay = None - super(OverlayPlugin, self).closeEvent(event) - @property def color(self): return self._color @@ -79,3 +70,12 @@ class OverlayPlugin(Plugin): if self._overlay_plot is not None: self._overlay_plot.set_cmap(self.cmap) self.image_viewer.redraw() + + def display_filtered_image(self, image): + """Display filtered image as an overlay on top of image in viewer.""" + self.overlay = image + + def closeEvent(self, event): + # clear overlay from ImageViewer on close + self.overlay = None + super(OverlayPlugin, self).closeEvent(event) From 49bdc3ae6f772de63c5992c8d45787ccca6779cd Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 25 Jul 2012 00:36:51 -0400 Subject: [PATCH 42/62] DOC: clean up docstring for PlotPlugin --- skimage/viewer/plugins/plotplugin.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py index 5c1ce7bb..0c298381 100644 --- a/skimage/viewer/plugins/plotplugin.py +++ b/skimage/viewer/plugins/plotplugin.py @@ -27,19 +27,12 @@ class PlotCanvas(FigureCanvasQTAgg): class PlotPlugin(Plugin): - """Plugin for ImageViewer that contains a plot Canvas. + """Plugin for ImageViewer that contains a plot canvas. - Parameters - ---------- - image_viewer : ImageViewer instance. - Window containing image used in measurement/manipulation. + Base class for plugins that contain a Matplotlib plot canvas, which can, + for example, display an image histogram. - Attributes - ---------- - image_viewer : ImageViewer - Window containing image used in measurement. - image : array - Image used in measurement/manipulation. + See base Plugin class for additional details. """ def attach(self, image_viewer): From f261e76ef0c6131a7527ad6081422ac7d7f5d039 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 25 Jul 2012 00:40:36 -0400 Subject: [PATCH 43/62] DOC: cleanup docstring and reuse parameter defined by parent class --- skimage/viewer/plugins/lineprofile.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 5afe78f4..9dee09e2 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -11,13 +11,10 @@ __all__ = ['LineProfile'] class LineProfile(PlotPlugin): """Plugin to compute interpolated intensity under a scan line on an image. + See PlotPlugin and Plugin classes for additional details. + Parameters ---------- - image_viewer : ImageViewer instance. - Window containing image used in measurement. - 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. linewidth : float Line width for interpolation. Wider lines average over more pixels. epsilon : float @@ -33,8 +30,8 @@ class LineProfile(PlotPlugin): name = 'Line Profile' draws_on_image = True - def __init__(self, useblit=None, linewidth=1, epsilon=5, limits='image'): - super(LineProfile, self).__init__(useblit=useblit) + def __init__(self, linewidth=1, epsilon=5, limits='image', **kwargs): + super(LineProfile, self).__init__(**kwargs) self.linewidth = linewidth self.epsilon = epsilon self._active_pt = None From 9285898d39b5e6de09041f480e0b8c82e612311a Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 25 Jul 2012 00:41:54 -0400 Subject: [PATCH 44/62] DOC: Add class docstring --- skimage/viewer/plugins/canny.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index 3431c6af..7c7bfb3b 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -5,6 +5,7 @@ from ..widgets import Slider, ComboBox class CannyPlugin(OverlayPlugin): + """Canny filter plugin to show edges of an image.""" name = 'Canny Filter' From 539b12dc2ca6240db78acda29d257de477369213 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 27 Jul 2012 21:57:46 -0400 Subject: [PATCH 45/62] STY: Refactor MatplotlibCanvas from ImageCanvas and PlotCanvas. --- skimage/viewer/plugins/plotplugin.py | 14 +++----------- skimage/viewer/utils/core.py | 17 ++++++++++++++++- skimage/viewer/viewers/core.py | 14 +++----------- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py index 0c298381..fa06088d 100644 --- a/skimage/viewer/plugins/plotplugin.py +++ b/skimage/viewer/plugins/plotplugin.py @@ -2,12 +2,12 @@ import numpy as np from PyQt4 import QtGui import matplotlib.pyplot as plt -from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg +from ..utils import MatplotlibCanvas from .base import Plugin -class PlotCanvas(FigureCanvasQTAgg): +class PlotCanvas(MatplotlibCanvas): """Canvas for displaying images. This canvas derives from Matplotlib, and has attributes `fig` and `ax`, @@ -15,17 +15,9 @@ class PlotCanvas(FigureCanvasQTAgg): """ def __init__(self, parent, height, width, **kwargs): 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) + super(PlotCanvas, self).__init__(parent, self.fig, **kwargs) self.setMinimumHeight(150) - class PlotPlugin(Plugin): """Plugin for ImageViewer that contains a plot canvas. diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index cdcc10bf..81d66183 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -1,9 +1,11 @@ import numpy as np import matplotlib.pyplot as plt from matplotlib.colors import LinearSegmentedColormap +from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg +from PyQt4 import QtGui -__all__ = ['figimage', 'LinearColormap', 'ClearColormap'] +__all__ = ['figimage', 'LinearColormap', 'ClearColormap', 'MatplotlibCanvas'] def figimage(image, scale=1, dpi=None, **kwargs): @@ -72,3 +74,16 @@ class ClearColormap(LinearColormap): 'red': [(0.0, r), (1.0, r)], 'alpha': [(0.0, 0.0), (1.0, max_alpha)]} LinearColormap.__init__(self, name, cg_speq) + + +class MatplotlibCanvas(FigureCanvasQTAgg): + """Canvas for displaying images.""" + def __init__(self, parent, figure, **kwargs): + self.fig = figure + 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) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 1046ca7c..dc38b422 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -4,26 +4,18 @@ ImageViewer class for viewing and interacting with images. import sys from PyQt4 import QtGui, QtCore -from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg -from skimage.viewer.utils import figimage +from ..utils import figimage, MatplotlibCanvas qApp = None -class ImageCanvas(FigureCanvasQTAgg): +class ImageCanvas(MatplotlibCanvas): """Canvas for displaying images.""" def __init__(self, parent, image, **kwargs): self.fig, self.ax = figimage(image, **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) + super(ImageCanvas, self).__init__(parent, self.fig, **kwargs) class ImageViewer(QtGui.QMainWindow): From 4620ee734edb0cc13fef239224a70990ba32fc5c Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 27 Jul 2012 22:21:26 -0400 Subject: [PATCH 46/62] ENH: Create new Slider with editbox. Also, make the behavior more consistent between updating plugin and widget parameters. --- skimage/viewer/plugins/base.py | 2 +- skimage/viewer/widgets/core.py | 118 +++++++++++++++++++++++++++++++-- 2 files changed, 113 insertions(+), 7 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 2e7c8980..ee2cbb5d 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -179,7 +179,7 @@ class Plugin(QtGui.QDialog): def _get_value(self, param): # If param is a widget, return its `val` attribute. - return param if not hasattr(param, 'val') else param.val() + return param if not hasattr(param, 'val') else param.val def display_filtered_image(self, image): """Display the filtered image on image viewer. diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index e2086b4f..6e48197e 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -11,16 +11,28 @@ specified by its `ptype` attribute, which can be: a class property that updates the display. """ +from PyQt4.QtCore import Qt from PyQt4 import QtGui from PyQt4 import QtCore -from skimage.io._plugins.q_color_mixer import IntelligentSlider + + +__all__ = ['Slider', 'ComboBox'] #TODO: Add WidgetBase class (requires reimplementation of IntelligentSlider). -class Slider(IntelligentSlider): +class Slider(QtGui.QWidget): """Slider widget. + 'name' attribute and calls a callback + with 'name' as an argument to the registered callback. + + This allows you to create large groups of sliders in a loop, + but still keep track of the individual events + + It also prints a label below the slider. + + Parameters ---------- name : str @@ -30,14 +42,108 @@ class Slider(IntelligentSlider): name of the slider. low, high : float Range of slider values. + value : float + Default slider value. If None, use midpoint between `low` and `high`. ptype : {'arg' | 'kwarg' | 'plugin'} Parameter type. + callback : function + Callback function called in response to slider changes. + orientation : {'horizontal' | 'vertical'} + Slider orientation. + update_on : {'move' | 'release'} + Control when callback function is called: on slider move or release. """ - def __init__(self, name, low, high, ptype='kwarg', callback=None, **kwargs): + def __init__(self, name, low=0.0, high=1.0, value=None, ptype='kwarg', + callback=None, max_edit_width=60, orientation='horizontal', + update_on='move'): + super(Slider, self).__init__() + self.name = name self.ptype = ptype - kwargs.setdefault('orientation', 'horizontal') - scale = (high - low) / 1000.0 - super(Slider, self).__init__(name, scale, low, callback, **kwargs) + self.callback = callback + + # divide slider into 1000 discrete values + slider_min = 0 + slider_max = 1000 + if value is None: + value = 500 + scale = float(high - low) / slider_max + self._scale = scale + self._low = low + self._high = high + + if orientation == 'vertical': + orientation_slider = Qt.Vertical + alignment = QtCore.Qt.AlignHCenter + align_text = QtCore.Qt.AlignHCenter + align_value = QtCore.Qt.AlignHCenter + self.layout = QtGui.QVBoxLayout(self) + elif orientation == 'horizontal': + orientation_slider = Qt.Horizontal + alignment = QtCore.Qt.AlignVCenter + align_text = QtCore.Qt.AlignLeft + align_value = QtCore.Qt.AlignRight + self.layout = QtGui.QHBoxLayout(self) + else: + msg = "Unexpected value %s for 'orientation'" + raise ValueError(msg % orientation) + + self.slider = QtGui.QSlider(orientation_slider) + self.slider.setRange(slider_min, slider_max) + self.slider.setValue(value) + if update_on == 'move': + self.slider.valueChanged.connect(self._on_slider_changed) + elif update_on == 'release': + self.slider.sliderReleased.connect(self._on_slider_changed) + else: + raise ValueError("Unexpected value %s for 'update_on'" % update_on) + + self.name_label = QtGui.QLabel() + self.name_label.setText(self.name) + self.name_label.setAlignment(align_text) + + self.editbox = QtGui.QLineEdit() + self.editbox.setMaximumWidth(max_edit_width) + self.editbox.setText('%2.2f' % self.val) + self.editbox.setAlignment(align_value) + self.editbox.editingFinished.connect(self._on_editbox_changed) + + self.layout.addWidget(self.name_label, alignment=align_text) + self.layout.addWidget(self.slider, alignment=alignment) + self.layout.addWidget(self.editbox, alignment=align_value) + + def _on_slider_changed(self): + """Call callback function with slider's name and value as parameters""" + value = self.val + self.editbox.setText(str(value)[:4]) + self.callback(self.name, value) + + def _on_editbox_changed(self): + """Validate input and set slider value""" + try: + value = float(self.editbox.text()) + except ValueError: + self._bad_editbox_input() + return + if not self._low <= value <= self._high: + self._bad_editbox_input() + return + + slider_value = (value - self._low) / self._scale + self.slider.setValue(slider_value) + self._good_editbox_input() + + def _good_editbox_input(self): + self.editbox.setStyleSheet("background-color: rgb(255, 255, 255)") + + def _bad_editbox_input(self): + self.editbox.setStyleSheet("background-color: rgb(255, 200, 200)") + + @property + def val(self): + return self.slider.value() * self._scale + self._low + + def _value_changed(self, value): + self.callback(self.name, value) class ComboBox(QtGui.QWidget): From 117d13a800bea7291a4cba53a01405a6fb6129c1 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 27 Jul 2012 22:27:24 -0400 Subject: [PATCH 47/62] Revert modifications of IntelligentSlider. Slider added in last commit removes the need for these modifications. --- skimage/io/_plugins/q_color_mixer.py | 46 ++++++---------------------- 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/skimage/io/_plugins/q_color_mixer.py b/skimage/io/_plugins/q_color_mixer.py index 7a5d1778..09e8709f 100644 --- a/skimage/io/_plugins/q_color_mixer.py +++ b/skimage/io/_plugins/q_color_mixer.py @@ -8,7 +8,7 @@ from util import ColorMixer class IntelligentSlider(QWidget): ''' A slider that adds a 'name' attribute and calls a callback - with 'name' as an argument to the registered callback. + with 'name' as an argument to the registerd callback. This allows you to create large groups of sliders in a loop, but still keep track of the individual events @@ -18,8 +18,7 @@ class IntelligentSlider(QWidget): The range of the slider is hardcoded from zero - 1000, but it supports a conversion factor so you can scale the results''' - def __init__(self, name, a, b, callback, orientation='vertical', - update_on='move'): + def __init__(self, name, a, b, callback): QWidget.__init__(self) self.name = name self.callback = callback @@ -27,52 +26,27 @@ class IntelligentSlider(QWidget): self.b = b self.manually_triggered = False - if orientation == 'vertical': - orientation_slider = Qt.Vertical - alignment = QtCore.Qt.AlignHCenter - align_text = QtCore.Qt.AlignCenter - align_value = QtCore.Qt.AlignCenter - elif orientation == 'horizontal': - orientation_slider = Qt.Horizontal - alignment = QtCore.Qt.AlignVCenter - align_text = QtCore.Qt.AlignLeft - align_value = QtCore.Qt.AlignRight - - self.slider = QSlider(orientation_slider) + self.slider = QSlider() self.slider.setRange(0, 1000) self.slider.setValue(500) - if update_on == 'move': - self.slider.sliderMoved.connect(self.slider_changed) - elif update_on == 'release': - self.slider.sliderReleased.connect(self.slider_changed) - else: - raise ValueError("Unexpected value %s for 'update_on'" % update_on) + self.slider.valueChanged.connect(self.slider_changed) self.name_label = QLabel() self.name_label.setText(self.name) - self.name_label.setAlignment(align_text) + self.name_label.setAlignment(QtCore.Qt.AlignCenter) self.value_label = QLabel() self.value_label.setText('%2.2f' % (self.slider.value() * self.a + self.b)) - self.value_label.setAlignment(align_value) + self.value_label.setAlignment(QtCore.Qt.AlignCenter) self.layout = QGridLayout(self) - - if orientation == 'vertical': - self.layout.addWidget(self.name_label, 0, 0) - self.layout.addWidget(self.slider, 1, 0, alignment) - self.layout.addWidget(self.value_label, 2, 0) - elif orientation == 'horizontal': - self.layout.addWidget(self.name_label, 0, 0) - self.layout.addWidget(self.slider, 0, 1, alignment) - self.layout.addWidget(self.value_label, 0, 2) - else: - msg = "Unexpected value %s for 'orientation'" - raise ValueError(msg % orientation) + self.layout.addWidget(self.name_label, 0, 0) + self.layout.addWidget(self.slider, 1, 0, QtCore.Qt.AlignHCenter) + self.layout.addWidget(self.value_label, 2, 0) # bind this to the valueChanged signal of the slider - def slider_changed(self): + def slider_changed(self, val): val = self.val() self.value_label.setText(str(val)[:4]) From 7615a856c5308ea9743c98457ca1d78b293d6b46 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 27 Jul 2012 22:32:42 -0400 Subject: [PATCH 48/62] Remove lineprofile temporarily (saved in a branch) --- skimage/viewer/plugins/lineprofile.py | 240 ------------------------- skimage/viewer/plugins/plotplugin.py | 50 ------ viewer_examples/plugins/lineprofile.py | 9 - 3 files changed, 299 deletions(-) delete mode 100644 skimage/viewer/plugins/lineprofile.py delete mode 100644 skimage/viewer/plugins/plotplugin.py delete mode 100644 viewer_examples/plugins/lineprofile.py diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py deleted file mode 100644 index 9dee09e2..00000000 --- a/skimage/viewer/plugins/lineprofile.py +++ /dev/null @@ -1,240 +0,0 @@ -import numpy as np -import scipy.ndimage as ndi -from skimage.util.dtype import dtype_range - -from .plotplugin import PlotPlugin - - -__all__ = ['LineProfile'] - - -class LineProfile(PlotPlugin): - """Plugin to compute interpolated intensity under a scan line on an image. - - See PlotPlugin and Plugin classes for additional details. - - Parameters - ---------- - linewidth : float - Line width for interpolation. Wider lines average over more pixels. - epsilon : float - Maximum pixel distance allowed when selecting end point of scan line. - limits : tuple or {None, 'image', 'dtype'} - (minimum, maximum) intensity limits for plotted profile. The following - special values are defined: - - None : rescale based on min/max intensity along selected scan line. - 'image' : fixed scale based on min/max intensity in image. - 'dtype' : fixed scale based on min/max intensity of image dtype. - """ - name = 'Line Profile' - draws_on_image = True - - def __init__(self, linewidth=1, epsilon=5, limits='image', **kwargs): - super(LineProfile, self).__init__(**kwargs) - self.linewidth = linewidth - self.epsilon = epsilon - self._active_pt = None - self._limit_type = limits - self.line_kwargs = dict(color='y', lw=linewidth, alpha=0.5, marker='s', - markersize=5, solid_capstyle='butt') - print self.help() - - def attach(self, image_viewer): - super(LineProfile, self).attach(image_viewer) - - image = image_viewer.original_image - - if self._limit_type == 'image': - self.limits = (np.min(image), np.max(image)) - elif self._limit_type == 'dtype': - self.self._limit_type = dtype_range[image.dtype.type] - elif self._limit_type is None or len(self._limit_type) == 2: - self.limits = self._limit_type - else: - raise ValueError("Unrecognized `limits`: %s" % self._limit_type) - - if not self._limit_type is None: - self.ax.set_ylim(self.limits) - - h, w = image.shape - self._init_end_pts = np.array([[w/3, h/2], [2*w/3, h/2]]) - self.end_pts = self._init_end_pts.copy() - - x, y = np.transpose(self.end_pts) - self.scan_line = image_viewer.ax.plot(x, y, **self.line_kwargs)[0] - self.artists.append(self.scan_line) - - scan_data = profile_line(image, self.end_pts) - self.profile = self.ax.plot(scan_data, 'k-')[0] - self._autoscale_view() - - self.connect_image_event('key_press_event', self.on_key_press) - self.connect_image_event('button_press_event', self.on_mouse_press) - self.connect_image_event('button_release_event', self.on_mouse_release) - self.connect_image_event('motion_notify_event', self.on_move) - self.connect_image_event('scroll_event', self.on_scroll) - - self.image_viewer.redraw() - - def help(self): - helpstr = ("Line profile tool", - "+ and - keys or mouse scroll changes width of scan line.", - "Select and drag ends of the scan line to adjust it.") - return '\n'.join(helpstr) - - def get_profile(self): - """Return intensity profile of the selected line. - - Returns - ------- - end_pts: (2, 2) array - The positions ((x1, y1), (x2, y2)) of the line ends. - profile: 1d array - Profile of intensity values. - """ - end_pts = self.scan_line.get_xydata() - profile = self.profile.get_ydata() - return end_pts, profile - - def on_scroll(self, event): - if not event.inaxes: return - if event.button == 'up': - self._thicken_scan_line() - elif event.button == 'down': - self._shrink_scan_line() - - def on_key_press(self, event): - if not event.inaxes: return - elif event.key == '+': - self._thicken_scan_line() - elif event.key == '-': - self._shrink_scan_line() - elif event.key == 'r': - self.reset() - - def _thicken_scan_line(self): - self.linewidth += 1 - self.line_changed(None, None) - - def _shrink_scan_line(self): - if self.linewidth > 1: - self.linewidth -= 1 - self.line_changed(None, None) - - def _autoscale_view(self): - if self.limits is None: - self.ax.autoscale_view(tight=True) - else: - self.ax.autoscale_view(scaley=False, tight=True) - - def get_pt_under_cursor(self, event): - """Return index of the end point under cursor, if sufficiently close""" - xy = np.asarray(self.scan_line.get_xydata()) - xyt = self.scan_line.get_transform().transform(xy) - xt, yt = xyt[:, 0], xyt[:, 1] - d = np.sqrt((xt - event.x)**2 + (yt - event.y)**2) - indseq = np.nonzero(np.equal(d, np.amin(d)))[0] - ind = indseq[0] - if d[ind] >= self.epsilon: - ind = None - return ind - - def on_mouse_press(self, event): - if event.button != 1: return - if event.inaxes==None: return - self._active_pt = self.get_pt_under_cursor(event) - - def on_mouse_release(self, event): - if event.button != 1: return - self._active_pt = None - - def on_move(self, event): - if event.button != 1: return - if self._active_pt is None: return - if not self.image_viewer.ax.in_axes(event): return - x,y = event.xdata, event.ydata - self.line_changed(x, y) - - def reset(self): - self.end_pts = self._init_end_pts.copy() - self.scan_line.set_data(np.transpose(self.end_pts)) - self.line_changed(None, None) - - def line_changed(self, x, y): - if x is not None: - self.end_pts[self._active_pt, :] = x, y - self.scan_line.set_data(np.transpose(self.end_pts)) - self.scan_line.set_linewidth(self.linewidth) - - scan = profile_line(self.image_viewer.original_image, self.end_pts, - linewidth=self.linewidth) - self.profile.set_xdata(np.arange(scan.shape[0])) - self.profile.set_ydata(scan) - - self.ax.relim() - - if self.useblit: - self.image_viewer.canvas.restore_region(self.img_background) - self.ax.draw_artist(self.scan_line) - self.ax.draw_artist(self.profile) - self.image_viewer.canvas.blit(self.image_viewer.ax.bbox) - - self._autoscale_view() - - self.image_viewer.redraw() - self.redraw() - - -def profile_line(img, end_pts, linewidth=1): - """Return the intensity profile of an image measured along a scan line. - - Parameters - ---------- - img : 2d array - The image. - end_pts: (2, 2) list - End points ((x1, y1), (x2, y2)) of scan line. - linewidth: int - Width of the scan, perpendicular to the line - - Returns - ------- - return_value : array - The intensity profile along the scan line. The length of the profile - is the ceil of the computed length of the scan line. - """ - point1, point2 = end_pts - x1, y1 = point1 = np.asarray(point1, dtype = float) - x2, y2 = point2 = np.asarray(point2, dtype = float) - dx, dy = point2 - point1 - - # Quick calculation if perfectly horizontal or vertical (remove?) - if x1 == x2: - pixels = img[min(y1, y2) : max(y1, y2)+1, - x1 - linewidth / 2 : x1 + linewidth / 2 + 1] - intensities = pixels.mean(axis = 1) - return intensities - elif y1 == y2: - pixels = img[y1 - linewidth / 2 : y1 + linewidth / 2 + 1, - min(x1, x2) : max(x1, x2)+1] - intensities = pixels.mean(axis = 0) - return intensities - - theta = np.arctan2(dy,dx) - a = dy/dx - b = y1 - a * x1 - length = np.hypot(dx, dy) - - line_x = np.linspace(min(x1, x2), max(x1, x2), np.ceil(length)) - line_y = line_x * a + b - y_width = abs(linewidth * np.cos(theta)/2) - perp_ys = np.array([np.linspace(yi - y_width, - yi + y_width, linewidth) for yi in line_y]) - perp_xs = - a * perp_ys + (line_x + a * line_y)[:, np.newaxis] - - perp_lines = np.array([perp_ys, perp_xs]) - pixels = ndi.map_coordinates(img, perp_lines) - intensities = pixels.mean(axis=1) - - return intensities diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py deleted file mode 100644 index fa06088d..00000000 --- a/skimage/viewer/plugins/plotplugin.py +++ /dev/null @@ -1,50 +0,0 @@ -import numpy as np -from PyQt4 import QtGui - -import matplotlib.pyplot as plt - -from ..utils import MatplotlibCanvas -from .base import Plugin - - -class PlotCanvas(MatplotlibCanvas): - """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): - self.fig, self.ax = plt.subplots(figsize=(height, width), **kwargs) - super(PlotCanvas, self).__init__(parent, self.fig, **kwargs) - self.setMinimumHeight(150) - -class PlotPlugin(Plugin): - """Plugin for ImageViewer that contains a plot canvas. - - Base class for plugins that contain a Matplotlib plot canvas, which can, - for example, display an image histogram. - - See base Plugin class for additional details. - """ - - def attach(self, image_viewer): - super(PlotPlugin, self).attach(image_viewer) - # Add plot for displaying intensity profile. - self.add_plot() - - def redraw(self): - """Redraw plot.""" - self.canvas.draw_idle() - - 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/viewer_examples/plugins/lineprofile.py b/viewer_examples/plugins/lineprofile.py deleted file mode 100644 index 2f1b2cdc..00000000 --- a/viewer_examples/plugins/lineprofile.py +++ /dev/null @@ -1,9 +0,0 @@ -from skimage import data -from skimage.viewer import ImageViewer -from skimage.viewer.plugins.lineprofile import LineProfile - - -image = data.camera() -viewer = ImageViewer(image) -viewer += LineProfile() -viewer.show() From 129070596db85160bc287126ba897e3ba05ad3fe Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 27 Jul 2012 22:36:33 -0400 Subject: [PATCH 49/62] Remove unused import --- skimage/io/_plugins/q_color_mixer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skimage/io/_plugins/q_color_mixer.py b/skimage/io/_plugins/q_color_mixer.py index 09e8709f..3fe9e29c 100644 --- a/skimage/io/_plugins/q_color_mixer.py +++ b/skimage/io/_plugins/q_color_mixer.py @@ -1,7 +1,6 @@ # the module for the qt color_mixer plugin from PyQt4 import QtGui, QtCore from PyQt4.QtGui import (QWidget, QStackedWidget, QSlider, QGridLayout, QLabel) -from PyQt4.QtCore import Qt from util import ColorMixer From 449f3e4cbf19e4a89901eda5e6967318b75e5861 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 27 Jul 2012 22:58:20 -0400 Subject: [PATCH 50/62] STY: Refactor BaseWidget from Slider and ComboBox --- skimage/viewer/widgets/core.py | 39 ++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 6e48197e..55d0cfc3 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -16,12 +16,27 @@ from PyQt4 import QtGui from PyQt4 import QtCore -__all__ = ['Slider', 'ComboBox'] +__all__ = ['BaseWidget', 'Slider', 'ComboBox'] -#TODO: Add WidgetBase class (requires reimplementation of IntelligentSlider). +class BaseWidget(QtGui.QWidget): -class Slider(QtGui.QWidget): + def __init__(self, name, ptype, callback): + super(BaseWidget, self).__init__() + self.name = name + self.ptype = ptype + self.callback = callback + + @property + def val(self): + msg = "Subclass of BaseWidget requires `val` property" + raise NotImplementedError(msg) + + def _value_changed(self, value): + self.callback(self.name, value) + + +class Slider(BaseWidget): """Slider widget. 'name' attribute and calls a callback @@ -56,10 +71,7 @@ class Slider(QtGui.QWidget): def __init__(self, name, low=0.0, high=1.0, value=None, ptype='kwarg', callback=None, max_edit_width=60, orientation='horizontal', update_on='move'): - super(Slider, self).__init__() - self.name = name - self.ptype = ptype - self.callback = callback + super(Slider, self).__init__(name, ptype, callback) # divide slider into 1000 discrete values slider_min = 0 @@ -142,11 +154,8 @@ class Slider(QtGui.QWidget): def val(self): return self.slider.value() * self._scale + self._low - def _value_changed(self, value): - self.callback(self.name, value) - -class ComboBox(QtGui.QWidget): +class ComboBox(BaseWidget): """ComboBox widget for selecting among a list of choices. Parameters @@ -163,11 +172,8 @@ class ComboBox(QtGui.QWidget): """ def __init__(self, name, items, ptype='kwarg', callback=None): - super(ComboBox, self).__init__() - self.ptype = ptype - self.callback = callback + super(ComboBox, self).__init__(name, ptype, callback) - self.name = name self.name_label = QtGui.QLabel() self.name_label.setText(self.name) self.name_label.setAlignment(QtCore.Qt.AlignLeft) @@ -186,6 +192,3 @@ class ComboBox(QtGui.QWidget): @property def val(self): return self._combo_box.value() - - def _value_changed(self, value): - self.callback(self.name, value) From b6045a8d5f1adc00860ab55e40f4201dd0e27f63 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 28 Jul 2012 00:13:33 -0400 Subject: [PATCH 51/62] BUG: Fix behavior when initial overlay limits are bad. Intensity limits are calculated by the initial input image. If this image has, for example, all black pixels, then subsequent overlays will remain all black because of the initialized limits. Set limits based on data type to fix this issue. --- skimage/viewer/plugins/overlayplugin.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index dbf15e7d..11ca5ab5 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -1,7 +1,15 @@ +import numpy as np + +from skimage.util import dtype from .base import Plugin from ..utils import ClearColormap +#TODO: Maybe this bool definition should be moved to skimage.util.dtype. +dtype_range = dtype.dtype_range.copy() +dtype_range[np.bool_] = (False, True) + + class OverlayPlugin(Plugin): """Plugin for ImageViewer that displays an overlay on top of main image. @@ -47,7 +55,9 @@ class OverlayPlugin(Plugin): ax.images.remove(self._overlay_plot) self._overlay_plot = None elif self._overlay_plot is None: - self._overlay_plot = ax.imshow(image, cmap=self.cmap) + vmin, vmax = dtype_range[image.dtype.type] + self._overlay_plot = ax.imshow(image, cmap=self.cmap, + vmin=vmin, vmax=vmax) else: self._overlay_plot.set_array(image) self.image_viewer.redraw() From 1000c73ceb1dd38ac1e67320b471ab5fd4c9571f Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 28 Jul 2012 11:40:04 -0400 Subject: [PATCH 52/62] DOC: Explain use of callback parameter. --- skimage/viewer/widgets/core.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 55d0cfc3..23bc46b7 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -62,7 +62,8 @@ class Slider(BaseWidget): ptype : {'arg' | 'kwarg' | 'plugin'} Parameter type. callback : function - Callback function called in response to slider changes. + Callback function called in response to slider changes. This function + is typically set when the widget is added to a plugin. orientation : {'horizontal' | 'vertical'} Slider orientation. update_on : {'move' | 'release'} @@ -169,6 +170,9 @@ class ComboBox(BaseWidget): Allowed parameter values. ptype : {'arg' | 'kwarg' | 'plugin'} Parameter type. + callback : function + Callback function called in response to slider changes. This function + is typically set when the widget is added to a plugin. """ def __init__(self, name, items, ptype='kwarg', callback=None): From 7d533e19a4505650476c4fbbec680818c2ad589e Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 28 Jul 2012 11:43:20 -0400 Subject: [PATCH 53/62] DOC: Add explanation of add operator. --- skimage/viewer/widgets/core.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 23bc46b7..826dc4f5 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -2,8 +2,12 @@ Widgets for interacting with ImageViewer. These widgets should be added to a Plugin subclass using its `add_widget` -method. The Plugin will delegate action based on the widget's parameter type -specified by its `ptype` attribute, which can be: +method or calling:: + + plugin += Widget(...) + +on a Plugin instance. The Plugin will delegate action based on the widget's +parameter type specified by its `ptype` attribute, which can be: 'arg' : positional argument passed to Plugin's `filter_image` method. 'kwarg' : keyword argument passed to Plugin's `filter_image` method. From 3340d0612dbc7dc8dfd8273346e01be467fbecff Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 28 Jul 2012 14:38:46 -0400 Subject: [PATCH 54/62] BUG: Fix scaling when setting default slider value. --- skimage/viewer/widgets/core.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 826dc4f5..31d177ef 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -81,9 +81,11 @@ class Slider(BaseWidget): # divide slider into 1000 discrete values slider_min = 0 slider_max = 1000 - if value is None: - value = 500 scale = float(high - low) / slider_max + if value is None: + slider_value = 500 + else: + slider_value = (value - low) / scale self._scale = scale self._low = low self._high = high @@ -106,7 +108,7 @@ class Slider(BaseWidget): self.slider = QtGui.QSlider(orientation_slider) self.slider.setRange(slider_min, slider_max) - self.slider.setValue(value) + self.slider.setValue(slider_value) if update_on == 'move': self.slider.valueChanged.connect(self._on_slider_changed) elif update_on == 'release': From ce017cc035e22ea05fa635f48b378c1afe79d111 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 1 Aug 2012 00:11:48 -0400 Subject: [PATCH 55/62] ENH: Add Slider `value_type` to allow int values. --- skimage/viewer/widgets/core.py | 67 +++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 31d177ef..7e1b6e9e 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -63,6 +63,8 @@ class Slider(BaseWidget): Range of slider values. value : float Default slider value. If None, use midpoint between `low` and `high`. + value : {'float' | 'int'} + Numeric type of slider value. ptype : {'arg' | 'kwarg' | 'plugin'} Parameter type. callback : function @@ -73,31 +75,24 @@ class Slider(BaseWidget): update_on : {'move' | 'release'} Control when callback function is called: on slider move or release. """ - def __init__(self, name, low=0.0, high=1.0, value=None, ptype='kwarg', - callback=None, max_edit_width=60, orientation='horizontal', - update_on='move'): + def __init__(self, name, low=0.0, high=1.0, value=None, value_type='float', + ptype='kwarg', callback=None, max_edit_width=60, + orientation='horizontal', update_on='move'): super(Slider, self).__init__(name, ptype, callback) - # divide slider into 1000 discrete values - slider_min = 0 - slider_max = 1000 - scale = float(high - low) / slider_max if value is None: - slider_value = 500 - else: - slider_value = (value - low) / scale - self._scale = scale - self._low = low - self._high = high + value = (high - low) / 2. + # Set widget orientation + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if orientation == 'vertical': - orientation_slider = Qt.Vertical + self.slider = QtGui.QSlider(Qt.Vertical) alignment = QtCore.Qt.AlignHCenter align_text = QtCore.Qt.AlignHCenter align_value = QtCore.Qt.AlignHCenter self.layout = QtGui.QVBoxLayout(self) elif orientation == 'horizontal': - orientation_slider = Qt.Horizontal + self.slider = QtGui.QSlider(Qt.Horizontal) alignment = QtCore.Qt.AlignVCenter align_text = QtCore.Qt.AlignLeft align_value = QtCore.Qt.AlignRight @@ -105,10 +100,30 @@ class Slider(BaseWidget): else: msg = "Unexpected value %s for 'orientation'" raise ValueError(msg % orientation) + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + # Set slider behavior for float and int values. + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + if value_type == 'float': + # divide slider into 1000 discrete values + slider_max = 1000 + self._scale = float(high - low) / slider_max + self.slider.setRange(0, slider_max) + self.value_fmt = '%2.2f' + elif value_type == 'int': + self.slider.setRange(low, high) + self.value_fmt = '%d' + else: + msg = "Expected `value_type` to be 'float' or 'int'; received: %s" + raise ValueError(msg % value_type) + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + self.value_type = value_type + self._low = low + self._high = high + # Update slider position to default value + self.val = value - self.slider = QtGui.QSlider(orientation_slider) - self.slider.setRange(slider_min, slider_max) - self.slider.setValue(slider_value) if update_on == 'move': self.slider.valueChanged.connect(self._on_slider_changed) elif update_on == 'release': @@ -122,7 +137,7 @@ class Slider(BaseWidget): self.editbox = QtGui.QLineEdit() self.editbox.setMaximumWidth(max_edit_width) - self.editbox.setText('%2.2f' % self.val) + self.editbox.setText(self.value_fmt % self.val) self.editbox.setAlignment(align_value) self.editbox.editingFinished.connect(self._on_editbox_changed) @@ -147,8 +162,7 @@ class Slider(BaseWidget): self._bad_editbox_input() return - slider_value = (value - self._low) / self._scale - self.slider.setValue(slider_value) + self.val = value self._good_editbox_input() def _good_editbox_input(self): @@ -159,7 +173,16 @@ class Slider(BaseWidget): @property def val(self): - return self.slider.value() * self._scale + self._low + value = self.slider.value() + if self.value_type == 'float': + value = value * self._scale + self._low + return value + + @val.setter + def val(self, value): + if self.value_type == 'float': + value = (value - self._low) / self._scale + self.slider.setValue(value) class ComboBox(BaseWidget): From 54af4176dd3de0adc2cdd1e819910e4897831598 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 3 Aug 2012 20:45:01 -0400 Subject: [PATCH 56/62] ENH: Add RequiredAttr to raise warnings when attr not set. --- skimage/viewer/plugins/base.py | 16 ++++------------ skimage/viewer/utils/core.py | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index ee2cbb5d..562237b2 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -3,9 +3,10 @@ Base class for Plugins that interact with ImageViewer. """ from PyQt4 import QtGui from PyQt4.QtCore import Qt - import matplotlib as mpl +from ..utils import RequiredAttr + class Plugin(QtGui.QDialog): """Base class for plugins that interact with an ImageViewer. @@ -68,6 +69,7 @@ class Plugin(QtGui.QDialog): """ name = 'Plugin' + image_viewer = RequiredAttr("%s is not attached to ImageViewer" % name) draws_on_image = False def __init__(self, image_filter=None, height=0, width=400, useblit=None): @@ -92,18 +94,8 @@ class Plugin(QtGui.QDialog): self.cids = [] self.artists = [] - @property - def image_viewer(self): - if self._image_viewer is None: - raise RuntimeError("Plugin is not attached to ImageViewer") - return self._image_viewer - - @image_viewer.setter - def image_viewer(self, image_viewer): - self._image_viewer = image_viewer - def attach(self, image_viewer): - """Attach the plugin to an ImageViewer. + """Attach the plugin to an ImageViewer. Note that the ImageViewer will automatically call this method when the plugin is added to the ImageViewer. For example: diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index 81d66183..36476df8 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -5,7 +5,24 @@ from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg from PyQt4 import QtGui -__all__ = ['figimage', 'LinearColormap', 'ClearColormap', 'MatplotlibCanvas'] +__all__ = ['figimage', 'LinearColormap', 'ClearColormap', 'MatplotlibCanvas', + 'RequiredAttr'] + + +class RequiredAttr(object): + """A class attribute that must be set before use.""" + + def __init__(self, msg): + self.msg = msg + self.val = None + + def __get__(self, obj, objtype): + if self.val is None: + raise RuntimeError(self.msg) + return self.val + + def __set__(self, obj, val): + self.val = val def figimage(image, scale=1, dpi=None, **kwargs): From 9ac42728c6d542570d7cca28d848b660fe0c020c Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 3 Aug 2012 20:47:42 -0400 Subject: [PATCH 57/62] DOC: Clean up docstring for Slider --- skimage/viewer/widgets/core.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 7e1b6e9e..27cb187f 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -11,8 +11,8 @@ parameter type specified by its `ptype` attribute, which can be: 'arg' : positional argument passed to Plugin's `filter_image` method. 'kwarg' : keyword argument passed to Plugin's `filter_image` method. - 'plugin' : attribute of Plugin. You'll probably need to make the attribute - a class property that updates the display. + 'plugin' : attribute of Plugin. You'll probably need to add a class + property of the same name that updates the display. """ from PyQt4.QtCore import Qt @@ -41,16 +41,7 @@ class BaseWidget(QtGui.QWidget): class Slider(BaseWidget): - """Slider widget. - - 'name' attribute and calls a callback - with 'name' as an argument to the registered callback. - - This allows you to create large groups of sliders in a loop, - but still keep track of the individual events - - It also prints a label below the slider. - + """Slider widget for adjusting numeric parameters. Parameters ---------- From 4ab583ba31800fa986a63e00c1ecc475889b2dea Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 3 Aug 2012 21:50:28 -0400 Subject: [PATCH 58/62] ENH: Add SaveButtons widget. --- skimage/viewer/plugins/base.py | 1 + skimage/viewer/widgets/__init__.py | 1 + skimage/viewer/widgets/core.py | 5 ++ skimage/viewer/widgets/history.py | 66 ++++++++++++++++++++++++ viewer_examples/plugins/median_filter.py | 18 +++++++ 5 files changed, 91 insertions(+) create mode 100644 skimage/viewer/widgets/history.py create mode 100644 viewer_examples/plugins/median_filter.py diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 562237b2..7c00ded8 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -137,6 +137,7 @@ class Plugin(QtGui.QDialog): widget.callback = self.filter_image elif widget.ptype == 'plugin': widget.callback = self.update_plugin + widget.plugin = self self.layout.addWidget(widget, self.row, 0) self.row += 1 diff --git a/skimage/viewer/widgets/__init__.py b/skimage/viewer/widgets/__init__.py index 5af24064..6552a313 100644 --- a/skimage/viewer/widgets/__init__.py +++ b/skimage/viewer/widgets/__init__.py @@ -1 +1,2 @@ from core import * +from history import * diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 27cb187f..8610f910 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -19,17 +19,22 @@ from PyQt4.QtCore import Qt from PyQt4 import QtGui from PyQt4 import QtCore +from ..utils import RequiredAttr + __all__ = ['BaseWidget', 'Slider', 'ComboBox'] class BaseWidget(QtGui.QWidget): + plugin = RequiredAttr("Widget is not attached to a Plugin.") + def __init__(self, name, ptype, callback): super(BaseWidget, self).__init__() self.name = name self.ptype = ptype self.callback = callback + self.plugin = None @property def val(self): diff --git a/skimage/viewer/widgets/history.py b/skimage/viewer/widgets/history.py new file mode 100644 index 00000000..daaf38cd --- /dev/null +++ b/skimage/viewer/widgets/history.py @@ -0,0 +1,66 @@ +import os +from textwrap import dedent + +from PyQt4 import QtGui + +from skimage import io +from .core import BaseWidget + + +__all__ = ['SaveButtons'] + + +class SaveButtons(BaseWidget): + + def __init__(self, default_format='png'): + name = 'Save to:' + ptype = None + callback = None + super(SaveButtons, self).__init__(name, ptype, callback) + + self.default_format = default_format + + self.name_label = QtGui.QLabel() + self.name_label.setText(name) + + self.save_file = QtGui.QPushButton('File') + self.save_file.clicked.connect(self.save_to_file) + self.save_stack = QtGui.QPushButton('Stack') + self.save_stack.clicked.connect(self.save_to_stack) + + self.layout = QtGui.QHBoxLayout(self) + self.layout.addWidget(self.name_label) + self.layout.addWidget(self.save_stack) + self.layout.addWidget(self.save_file) + + def save_to_stack(self): + image = self.plugin.image_viewer.image.copy() + io.push(image) + + msg = dedent('''\ + The image has been pushed to the io stack. + Use io.pop() to retrieve the most recently pushed image. + NOTE: The io stack only works in interactive sessions.''') + notify(msg) + + def save_to_file(self): + filename = str(QtGui.QFileDialog.getSaveFileName()) + if len(filename) == 0: + return + #TODO: io plugins should assign default image formats + basename, ext = os.path.splitext(filename) + if not ext: + filename = '%s.%s' % (filename, self.default_format) + io.imsave(filename, self.plugin.image_viewer.image) + + +def notify(msg): + msglabel = QtGui.QLabel(msg) + dialog = QtGui.QDialog() + ok = QtGui.QPushButton('OK', dialog) + ok.clicked.connect(dialog.accept) + ok.setDefault(True) + dialog.layout = QtGui.QGridLayout(dialog) + dialog.layout.addWidget(msglabel, 0, 0, 1, 3) + dialog.layout.addWidget(ok, 1, 1) + dialog.exec_() diff --git a/viewer_examples/plugins/median_filter.py b/viewer_examples/plugins/median_filter.py new file mode 100644 index 00000000..57d04397 --- /dev/null +++ b/viewer_examples/plugins/median_filter.py @@ -0,0 +1,18 @@ +from skimage import data +from skimage.filter import median_filter + +from skimage.viewer import ImageViewer +from skimage.viewer.widgets import Slider +from skimage.viewer.widgets.history import SaveButtons +from skimage.viewer.plugins.overlayplugin import Plugin + + +image = data.coins() +viewer = ImageViewer(image) + +plugin = Plugin(image_filter=median_filter) +plugin += Slider('radius', 2, 10, value_type='int', update_on='release') +plugin += SaveButtons() + +viewer += plugin +viewer.show() From 398b320477186c4b89d00d099e6fe587085abbc0 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 3 Aug 2012 22:21:59 -0400 Subject: [PATCH 59/62] BUG: reset image when plugin is closed. --- skimage/viewer/plugins/base.py | 1 + skimage/viewer/viewers/core.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 7c00ded8..fc2951b2 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -200,6 +200,7 @@ class Plugin(QtGui.QDialog): self.disconnect_image_events() self.remove_image_artists() self.image_viewer.plugins.remove(self) + self.image_viewer.reset_image() self.image_viewer.redraw() self.close() diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index dc38b422..3775e511 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -76,7 +76,7 @@ class ImageViewer(QtGui.QMainWindow): self._image_plot = self.ax.images[0] self.original_image = image - self.image = image + self.image = image.copy() self.plugins = [] # List of axes artists to check for removal. @@ -141,6 +141,9 @@ class ImageViewer(QtGui.QMainWindow): self._image_plot.set_array(image) self.redraw() + def reset_image(self): + self.image = self.original_image.copy() + def connect_event(self, event, callback): """Connect callback function to matplotlib event and return id.""" cid = self.canvas.mpl_connect(event, callback) From e96aca563792d1bab41ccdd239b452be362e2fff Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 3 Aug 2012 22:27:05 -0400 Subject: [PATCH 60/62] ENH: Add OK/Cancel buttons --- skimage/viewer/widgets/core.py | 2 +- skimage/viewer/widgets/history.py | 39 ++++++++++++++++++++---- viewer_examples/plugins/median_filter.py | 5 +-- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 8610f910..625e707b 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -29,7 +29,7 @@ class BaseWidget(QtGui.QWidget): plugin = RequiredAttr("Widget is not attached to a Plugin.") - def __init__(self, name, ptype, callback): + def __init__(self, name, ptype=None, callback=None): super(BaseWidget, self).__init__() self.name = name self.ptype = ptype diff --git a/skimage/viewer/widgets/history.py b/skimage/viewer/widgets/history.py index daaf38cd..cc586579 100644 --- a/skimage/viewer/widgets/history.py +++ b/skimage/viewer/widgets/history.py @@ -7,16 +7,43 @@ from skimage import io from .core import BaseWidget -__all__ = ['SaveButtons'] +__all__ = ['OKCancelButtons', 'SaveButtons'] + + +class OKCancelButtons(BaseWidget): + """Buttons that close the parent plugin. + + OK will replace the original image with the current (filtered) image. + Cancel will just close the plugin. + """ + def __init__(self): + name = 'OK/Cancel' + super(OKCancelButtons, self).__init__(name) + + self.ok = QtGui.QPushButton('OK') + self.ok.clicked.connect(self.update_original_image) + self.cancel = QtGui.QPushButton('Cancel') + self.cancel.clicked.connect(self.close_plugin) + + self.layout = QtGui.QHBoxLayout(self) + self.layout.addWidget(self.cancel) + self.layout.addWidget(self.ok) + + def update_original_image(self): + image = self.plugin.image_viewer.image + self.plugin.image_viewer.original_image = image + self.plugin.close() + + def close_plugin(self): + # Image viewer will restore original image on close. + self.plugin.close() class SaveButtons(BaseWidget): + """Buttons to save image to io.stack or to a file.""" - def __init__(self, default_format='png'): - name = 'Save to:' - ptype = None - callback = None - super(SaveButtons, self).__init__(name, ptype, callback) + def __init__(self, name='Save to:', default_format='png'): + super(SaveButtons, self).__init__(name) self.default_format = default_format diff --git a/viewer_examples/plugins/median_filter.py b/viewer_examples/plugins/median_filter.py index 57d04397..3a050382 100644 --- a/viewer_examples/plugins/median_filter.py +++ b/viewer_examples/plugins/median_filter.py @@ -3,8 +3,8 @@ from skimage.filter import median_filter from skimage.viewer import ImageViewer from skimage.viewer.widgets import Slider -from skimage.viewer.widgets.history import SaveButtons -from skimage.viewer.plugins.overlayplugin import Plugin +from skimage.viewer.widgets.history import OKCancelButtons, SaveButtons +from skimage.viewer.plugins.base import Plugin image = data.coins() @@ -13,6 +13,7 @@ viewer = ImageViewer(image) plugin = Plugin(image_filter=median_filter) plugin += Slider('radius', 2, 10, value_type='int', update_on='release') plugin += SaveButtons() +plugin += OKCancelButtons() viewer += plugin viewer.show() From cfd0b84a9b0a99a7ba39913baa6f1ddbdc0d7a3f Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 3 Aug 2012 23:04:51 -0400 Subject: [PATCH 61/62] STY: Tweak button sizes. --- skimage/viewer/widgets/history.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/widgets/history.py b/skimage/viewer/widgets/history.py index cc586579..bb65b7dd 100644 --- a/skimage/viewer/widgets/history.py +++ b/skimage/viewer/widgets/history.py @@ -16,16 +16,19 @@ class OKCancelButtons(BaseWidget): OK will replace the original image with the current (filtered) image. Cancel will just close the plugin. """ - def __init__(self): + def __init__(self, button_width=80): name = 'OK/Cancel' super(OKCancelButtons, self).__init__(name) self.ok = QtGui.QPushButton('OK') self.ok.clicked.connect(self.update_original_image) + self.ok.setMaximumWidth(button_width) self.cancel = QtGui.QPushButton('Cancel') self.cancel.clicked.connect(self.close_plugin) + self.cancel.setMaximumWidth(button_width) self.layout = QtGui.QHBoxLayout(self) + self.layout.addStretch() self.layout.addWidget(self.cancel) self.layout.addWidget(self.ok) From ffe2ebacae4292f93f560aca526422e07bbee580 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 22 Aug 2012 22:47:54 -0400 Subject: [PATCH 62/62] BUG: Update filter when edit-box is changed. --- skimage/viewer/widgets/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 625e707b..169a4401 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -160,6 +160,7 @@ class Slider(BaseWidget): self.val = value self._good_editbox_input() + self.callback(self.name, value) def _good_editbox_input(self): self.editbox.setStyleSheet("background-color: rgb(255, 255, 255)")