Merge pull request #229 from tonysyu/qtmpl-viewer

ENH: Qt-MPL viewer.
This commit is contained in:
Stefan van der Walt
2012-08-24 05:53:13 -07:00
16 changed files with 1014 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
from viewers import ImageViewer
View File
+231
View File
@@ -0,0 +1,231 @@
"""
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.
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
Window containing image used in measurement/manipulation.
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. 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
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'
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):
super(Plugin, self).__init__()
self.image_viewer = None
# 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)
self.resize(width, height)
self.row = 0
self.arguments = []
self.keyword_arguments= {}
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):
"""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)
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_image_event('draw_event', self.on_draw)
# Call filter so that filtered image matches widget values
self.filter_image()
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
widget.callback = self.filter_image
elif widget.ptype == 'arg':
self.arguments.append(widget)
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
def __add__(self, widget):
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):
"""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`.
"""
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()
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.
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)
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_image_artists(self):
"""Disconnect artists that are connected to the image viewer."""
for a in self.artists:
self.image_viewer.remove_artist(a)
+18
View File
@@ -0,0 +1,18 @@
from skimage.filter import canny
from .overlayplugin import OverlayPlugin
from ..widgets import Slider, ComboBox
class CannyPlugin(OverlayPlugin):
"""Canny filter plugin to show edges of an image."""
name = 'Canny Filter'
def __init__(self, *args, **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'))
self.add_widget(Slider('high threshold', 0, 255, update_on='release'))
self.add_widget(ComboBox('color', self.color_names, ptype='plugin'))
+91
View File
@@ -0,0 +1,91 @@
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.
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),
'green': (0, 1, 0),
'cyan': (0, 1, 1)}
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
@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:
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()
@property
def color(self):
return self._color
@color.setter
def color(self, index):
# Update colormap whenever color is changed.
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)
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)
+1
View File
@@ -0,0 +1 @@
from core import *
+106
View File
@@ -0,0 +1,106 @@
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', '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):
"""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):
"""Color map that varies linearly from alpha = 0 to 1
"""
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, 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)
+1
View File
@@ -0,0 +1 @@
from .core import *
+188
View File
@@ -0,0 +1,188 @@
"""
ImageViewer class for viewing and interacting with images.
"""
import sys
from PyQt4 import QtGui, QtCore
from ..utils import figimage, MatplotlibCanvas
qApp = None
class ImageCanvas(MatplotlibCanvas):
"""Canvas for displaying images."""
def __init__(self, parent, image, **kwargs):
self.fig, self.ax = figimage(image, **kwargs)
super(ImageCanvas, self).__init__(parent, self.fig, **kwargs)
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.
Examples
--------
>>> viewer = ImageViewer(image)
>>> viewer += SomePlugin()
>>> viewer.show()
"""
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.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.ax.autoscale(enable=False)
self._image_plot = self.ax.images[0]
self.original_image = image
self.image = image.copy()
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)
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 __add__(self, plugin):
"""Add plugin to ImageViewer"""
plugin.attach(self)
return self
def closeEvent(self, event):
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 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.
This behaves much like `matplotlib.pyplot.show` and `QWidget.show`.
"""
self.auto_layout()
for p in self.plugins:
p.show()
super(ImageViewer, self).show()
sys.exit(qApp.exec_())
def redraw(self):
self.canvas.draw_idle()
@property
def image(self):
return self._img
@image.setter
def image(self, image):
self._img = image
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)
return cid
def disconnect_event(self, callback_id):
"""Disconnect callback by its id (returned by `connect_event`)."""
self.canvas.mpl_disconnect(callback_id)
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.
"""
# 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):
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 ""
+2
View File
@@ -0,0 +1,2 @@
from core import *
from history import *
+224
View File
@@ -0,0 +1,224 @@
"""
Widgets for interacting with ImageViewer.
These widgets should be added to a Plugin subclass using its `add_widget`
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.
'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
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=None, callback=None):
super(BaseWidget, self).__init__()
self.name = name
self.ptype = ptype
self.callback = callback
self.plugin = None
@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 for adjusting numeric parameters.
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.
low, high : float
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
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'}
Control when callback function is called: on slider move or release.
"""
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)
if value is None:
value = (high - low) / 2.
# Set widget orientation
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
if orientation == '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':
self.slider = QtGui.QSlider(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)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 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
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(self.value_fmt % 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
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)")
def _bad_editbox_input(self):
self.editbox.setStyleSheet("background-color: rgb(255, 200, 200)")
@property
def val(self):
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):
"""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.
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):
super(ComboBox, self).__init__(name, ptype, callback)
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()
+96
View File
@@ -0,0 +1,96 @@
import os
from textwrap import dedent
from PyQt4 import QtGui
from skimage import io
from .core import BaseWidget
__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, 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)
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, name='Save to:', default_format='png'):
super(SaveButtons, self).__init__(name)
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_()
+9
View File
@@ -0,0 +1,9 @@
from skimage import data
from skimage.viewer import ImageViewer
from skimage.viewer.plugins.canny import CannyPlugin
image = data.camera()
viewer = ImageViewer(image)
viewer += CannyPlugin()
viewer.show()
+20
View File
@@ -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()
+19
View File
@@ -0,0 +1,19 @@
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 OKCancelButtons, SaveButtons
from skimage.viewer.plugins.base 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()
plugin += OKCancelButtons()
viewer += plugin
viewer.show()
+7
View File
@@ -0,0 +1,7 @@
from skimage import data
from skimage.viewer import ImageViewer
image = data.camera()
viewer = ImageViewer(image)
viewer.show()