""" Base class for Plugins that interact with ImageViewer. """ from warnings import warn import numpy as np from ..qt import QtWidgets, QtCore, Signal from ..utils import RequiredAttr, init_qtapp class Plugin(QtWidgets.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 and canvastools. Any artists created by the plugin should be added to this list so that it gets cleaned up on close. Examples -------- >>> from skimage.viewer import ImageViewer >>> from skimage.viewer.widgets import Slider >>> from skimage import data >>> >>> plugin = Plugin(image_filter=lambda img, ... threshold: img > threshold) # doctest: +SKIP >>> plugin += Slider('threshold', 0, 255) # doctest: +SKIP >>> >>> image = data.coins() >>> viewer = ImageViewer(image) # doctest: +SKIP >>> viewer += plugin # doctest: +SKIP >>> thresholded = viewer.show()[0][0] # doctest: +SKIP 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) # Signals used when viewers are linked to the Plugin output. image_changed = Signal(np.ndarray) _started = Signal(int) def __init__(self, image_filter=None, height=0, width=400, useblit=True, dock='bottom'): init_qtapp() super(Plugin, self).__init__() self.dock = dock self.image_viewer = None # If subclass defines `image_filter` method ignore input. if not hasattr(self, 'image_filter'): self.image_filter = image_filter elif image_filter is not None: warn("If the Plugin class defines an `image_filter` method, " "then the `image_filter` argument is ignored.") self.setWindowTitle(self.name) self.layout = QtWidgets.QGridLayout(self) self.resize(width, height) self.row = 0 self.arguments = [] self.keyword_arguments = {} 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(QtCore.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 = [self.image_viewer.original_image] # 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 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 = {name: self._get_value(a) for name, a in self.keyword_arguments.items()} filtered = self.image_filter(*arguments, **kwargs) self.display_filtered_image(filtered) self.image_changed.emit(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 _update_original_image(self, image): """Update the original image argument passed to the filter function. This method is called by the viewer when the original image is updated. """ self.arguments[0] = image self._on_new_image(image) self.filter_image() def _on_new_image(self, image): """Override this method to update your plugin for new images.""" pass @property def filtered_image(self): """Return filtered image.""" return self.image_viewer.image 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 show(self, main_window=True): """Show plugin.""" super(Plugin, self).show() self.activateWindow() self.raise_() # Emit signal with x-hint so new windows can be displayed w/o overlap. size = self.frameGeometry() x_hint = size.x() + size.width() self._started.emit(x_hint) def closeEvent(self, event): """On close disconnect all artists and events from ImageViewer. Note that artists must be appended to `self.artists`. """ self.clean_up() self.close() def clean_up(self): self.remove_image_artists() if self in self.image_viewer.plugins: self.image_viewer.plugins.remove(self) self.image_viewer.reset_image() self.image_viewer.redraw() def remove_image_artists(self): """Remove artists that are connected to the image viewer.""" for a in self.artists: a.remove() def output(self): """Return the plugin's representation and data. Returns ------- image : array, same shape as ``self.image_viewer.image``, or None The filtered image. data : None Any data associated with the plugin. Notes ----- Derived classes should override this method to return a tuple containing an *overlay* of the same shape of the image, and a *data* object. Either of these is optional: return ``None`` if you don't want to return a value. """ return (self.image_viewer.image, None)