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()