Files
scikit-image/skimage/viewer/viewers/core.py
T
2012-09-27 20:01:45 +01:00

280 lines
9.0 KiB
Python

"""
ImageViewer class for viewing and interacting with images.
"""
try:
from PyQt4 import QtGui, QtCore
from PyQt4.QtGui import QMainWindow
except ImportError:
QMainWindow = object # hack to prevent nosetest and autodoc errors
print("Could not import PyQt4 -- skimage.viewer not available.")
from skimage.util.dtype import dtype_range
from .. import utils
from ..widgets import Slider
__all__ = ['ImageViewer', 'CollectionViewer']
class ImageCanvas(utils.MatplotlibCanvas):
"""Canvas for displaying images."""
def __init__(self, parent, image, **kwargs):
self.fig, self.ax = utils.figimage(image, **kwargs)
super(ImageCanvas, self).__init__(parent, self.fig, **kwargs)
class ImageViewer(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
--------
>>> from skimage import data
>>> image = data.coins()
>>> viewer = ImageViewer(image)
>>> # viewer.show()
"""
def __init__(self, image):
# Start main loop
utils.init_qtapp()
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()
utils.start_qtapp()
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)
clim = dtype_range[image.dtype.type]
if clim[0] < 0 and image.min() >= 0:
clim = (0, clim[1])
self._image_plot.set_clim(clim)
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 ""
class CollectionViewer(ImageViewer):
"""Viewer for displaying image collections.
Select the displayed frame of the image collection using the slider or
with the following keyboard shortcuts:
left/right arrows
Previous/next image in collection.
number keys, 0--9
0% to 90% of collection. For example, "5" goes to the image in the
middle (i.e. 50%) of the collection.
home/end keys
First/last image in collection.
Subclasses and plugins will likely extend the `update_image` method to add
custom overlays or filter the displayed image.
Parameters
----------
image_collection : list of images
List of images to be displayed.
update_on : {'on_slide' | 'on_release'}
Control whether image is updated on slide or release of the image
slider. Using 'on_release' will give smoother behavior when displaying
large images or when writing a plugin/subclass that requires heavy
computation.
"""
def __init__(self, image_collection, update_on='move', **kwargs):
self.image_collection = image_collection
self.index = 0
self.num_images = len(self.image_collection)
first_image = image_collection[0]
super(CollectionViewer, self).__init__(first_image)
slider_kws = dict(value=0, low=0, high=self.num_images - 1)
slider_kws['update_on'] = update_on
slider_kws['callback'] = self.update_index
slider_kws['value_type'] = 'int'
self.slider = Slider('frame', **slider_kws)
self.layout.addWidget(self.slider)
#TODO: Adjust height to accomodate slider; the following doesn't work
# s_size = self.slider.sizeHint()
# cs_size = self.canvas.sizeHint()
# self.resize(cs_size.width(), cs_size.height() + s_size.height())
def update_index(self, name, index):
"""Select image on display using index into image collection."""
index = int(round(index))
if index == self.index:
return
# clip index value to collection limits
index = max(index, 0)
index = min(index, self.num_images - 1)
self.index = index
self.slider.val = index
self.update_image(self.image_collection[index])
def update_image(self, image):
"""Update displayed image.
This method can be overridden or extended in subclasses and plugins to
react to image changes.
"""
self.image = image
def keyPressEvent(self, event):
if type(event) == QtGui.QKeyEvent:
key = event.key()
# Number keys (code: 0 = key 48, 9 = key 57) move to deciles
if 48 <= key < 58:
index = 0.1 * int(key - 48) * self.num_images
self.update_index('', index)
event.accept()
else:
event.ignore()