mirror of
https://github.com/wassname/scikit-image.git
synced 2026-07-10 21:27:17 +08:00
@@ -0,0 +1 @@
|
||||
from viewers import ImageViewer
|
||||
@@ -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)
|
||||
@@ -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'))
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
from core import *
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
from .core import *
|
||||
@@ -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 ""
|
||||
@@ -0,0 +1,2 @@
|
||||
from core import *
|
||||
from history import *
|
||||
@@ -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()
|
||||
@@ -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_()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -0,0 +1,7 @@
|
||||
from skimage import data
|
||||
from skimage.viewer import ImageViewer
|
||||
|
||||
|
||||
image = data.camera()
|
||||
viewer = ImageViewer(image)
|
||||
viewer.show()
|
||||
Reference in New Issue
Block a user