Source code for glue.viewers.image.qt.viewer_widget
from __future__ import absolute_import, division, print_function
import os
from glue.external.modest_image import imshow
from glue.external.qt.QtCore import Qt
from glue.external.qt import QtGui, QtCore
from glue.core.callback_property import add_callback, delay_callback
from glue import core
from glue.viewers.image.ds9norm import DS9Normalize
from glue.viewers.image.client import MplImageClient
from glue.viewers.common.qt.toolbar import GlueToolbar
from glue.viewers.common.qt.mouse_mode import (RectangleMode, CircleMode, PolyMode,
ContrastMode)
from glue.icons.qt import get_icon
from glue.utils.qt.widget_properties import CurrentComboProperty, ButtonProperty, connect_current_combo, _find_combo_data
from glue.viewers.common.qt.data_slice_widget import DataSlice
from glue.viewers.common.qt.data_viewer import DataViewer
from glue.viewers.common.qt.mpl_widget import MplWidget, defer_draw
from glue.utils import nonpartial, Pointer
from glue.utils.qt import cmap2pixmap, update_combobox, load_ui
# We do the following import to register the custom Qt Widget there
from glue.viewers.image.qt.rgb_edit import RGBEdit # pylint: disable=W0611
WARN_THRESH = 10000000 # warn when contouring large images
__all__ = ['ImageWidget', 'StandaloneImageWidget', 'ImageWidgetBase']
[docs]class ImageWidgetBase(DataViewer):
"""
Widget for ImageClient
This base class avoids any matplotlib-specific logic
"""
LABEL = "Image Viewer"
_property_set = DataViewer._property_set + \
'data attribute rgb_mode rgb_viz ratt gatt batt slice'.split()
attribute = CurrentComboProperty('ui.attributeComboBox',
'Current attribute')
data = CurrentComboProperty('ui.displayDataCombo',
'Current data')
aspect_ratio = CurrentComboProperty('ui.aspectCombo',
'Aspect ratio for image')
rgb_mode = ButtonProperty('ui.rgb',
'RGB Mode?')
rgb_viz = Pointer('ui.rgb_options.rgb_visible')
def __init__(self, session, parent=None):
super(ImageWidgetBase, self).__init__(session, parent)
self._setup_widgets()
self.client = self.make_client()
self._setup_tools()
tb = self.make_toolbar()
self.addToolBar(tb)
self._connect()
def _setup_widgets(self):
self.central_widget = self.make_central_widget()
self.label_widget = QtGui.QLabel("", self.central_widget)
self.setCentralWidget(self.central_widget)
self.option_widget = QtGui.QWidget()
self.ui = load_ui('options_widget.ui', self.option_widget,
directory=os.path.dirname(__file__))
self.ui.slice = DataSlice()
self.ui.slice_layout.addWidget(self.ui.slice)
self._tweak_geometry()
self.ui.aspectCombo.addItem("Square Pixels", userData='equal')
self.ui.aspectCombo.addItem("Automatic", userData='auto')
[docs] def make_client(self):
""" Instantiate and return an ImageClient subclass """
raise NotImplementedError()
[docs] def make_central_widget(self):
""" Create and return the central widget to display the image """
raise NotImplementedError()
[docs] def make_toolbar(self):
""" Create and return the toolbar for this widget """
raise NotImplementedError()
@staticmethod
def _get_default_tools():
return []
def _setup_tools(self):
"""
Set up additional tools for this widget
"""
from glue import config
self._tools = []
for tool in config.tool_registry.members[self.__class__]:
self._tools.append(tool(self))
def _tweak_geometry(self):
self.central_widget.resize(600, 400)
self.resize(self.central_widget.size())
self.ui.rgb_options.hide()
self.statusBar().setSizeGripEnabled(False)
self.setFocusPolicy(Qt.StrongFocus)
@defer_draw
[docs] def add_data(self, data):
"""
Add a new dataset to the viewer
"""
# overloaded from DataViewer
# need to delay callbacks, otherwise might
# try to set combo boxes to nonexisting items
with delay_callback(self.client, 'display_data', 'display_attribute'):
# If there is not already any image data set, we can't add 1-D
# datasets (tables/catalogs) to the image widget yet.
if data.data.ndim == 1 and self.client.display_data is None:
QtGui.QMessageBox.information(self.window(), "Note",
"Cannot create image viewer from a 1-D "
"dataset. You will need to first "
"create an image viewer using data "
"with 2 or more dimensions, after "
"which you will be able to overlay 1-D "
"data as a scatter plot.",
buttons=QtGui.QMessageBox.Ok)
return
r = self.client.add_layer(data)
if r is not None and self.client.display_data is not None:
self.add_data_to_combo(data)
if self.client.can_image_data(data):
self.client.display_data = data
self.set_attribute_combo(self.client.display_data)
return r is not None
@defer_draw
[docs] def add_subset(self, subset):
self.client.add_scatter_layer(subset)
assert subset in self.client.artists
[docs] def add_data_to_combo(self, data):
""" Add a data object to the combo box, if not already present
"""
if not self.client.can_image_data(data):
return
combo = self.ui.displayDataCombo
try:
pos = _find_combo_data(combo, data)
except ValueError:
combo.addItem(data.label, userData=data)
@property
def ratt(self):
"""ComponentID assigned to R channel in RGB Mode"""
return self.ui.rgb_options.attributes[0]
@ratt.setter
def ratt(self, value):
att = list(self.ui.rgb_options.attributes)
att[0] = value
self.ui.rgb_options.attributes = att
@property
def gatt(self):
"""ComponentID assigned to G channel in RGB Mode"""
return self.ui.rgb_options.attributes[1]
@gatt.setter
def gatt(self, value):
att = list(self.ui.rgb_options.attributes)
att[1] = value
self.ui.rgb_options.attributes = att
@property
def batt(self):
"""ComponentID assigned to B channel in RGB Mode"""
return self.ui.rgb_options.attributes[2]
@batt.setter
def batt(self, value):
att = list(self.ui.rgb_options.attributes)
att[2] = value
self.ui.rgb_options.attributes = att
@property
def slice(self):
return self.client.slice
@slice.setter
def slice(self, value):
self.client.slice = value
[docs] def set_attribute_combo(self, data):
""" Update attribute combo box to reflect components in data"""
labeldata = ((f.label, f) for f in data.visible_components)
update_combobox(self.ui.attributeComboBox, labeldata)
def _connect(self):
ui = self.ui
ui.monochrome.toggled.connect(self._update_rgb_console)
ui.rgb_options.colors_changed.connect(self.update_window_title)
# sync client and widget slices
ui.slice.slice_changed.connect(lambda: setattr(self, 'slice', self.ui.slice.slice))
update_ui_slice = lambda val: setattr(ui.slice, 'slice', val)
add_callback(self.client, 'slice', update_ui_slice)
add_callback(self.client, 'display_data', self.ui.slice.set_data)
# sync window title to data/attribute
add_callback(self.client, 'display_data', nonpartial(self._display_data_changed))
add_callback(self.client, 'display_attribute', nonpartial(self._display_attribute_changed))
add_callback(self.client, 'display_aspect', nonpartial(self.client._update_aspect))
# sync data/attribute combos with client properties
connect_current_combo(self.client, 'display_data', self.ui.displayDataCombo)
connect_current_combo(self.client, 'display_attribute', self.ui.attributeComboBox)
connect_current_combo(self.client, 'display_aspect', self.ui.aspectCombo)
def _display_data_changed(self):
if self.client.display_data is None:
self.ui.attributeComboBox.clear()
return
with self.client.artists.ignore_empty():
self.set_attribute_combo(self.client.display_data)
self.client.add_layer(self.client.display_data)
self.client._update_and_redraw()
self.update_window_title()
def _display_attribute_changed(self):
if self.client.display_attribute is None:
return
self.client._update_and_redraw()
self.update_window_title()
@defer_draw
def _update_rgb_console(self, is_monochrome):
if is_monochrome:
self.ui.rgb_options.hide()
self.ui.mono_att_label.show()
self.ui.attributeComboBox.show()
self.client.rgb_mode(False)
else:
self.ui.mono_att_label.hide()
self.ui.attributeComboBox.hide()
self.ui.rgb_options.show()
rgb = self.client.rgb_mode(True)
if rgb is not None:
self.ui.rgb_options.artist = rgb
[docs] def register_to_hub(self, hub):
super(ImageWidgetBase, self).register_to_hub(hub)
self.client.register_to_hub(hub)
dc_filt = lambda x: x.sender is self.client._data
display_data_filter = lambda x: x.data is self.client.display_data
hub.subscribe(self,
core.message.DataCollectionAddMessage,
handler=lambda x: self.add_data_to_combo(x.data),
filter=dc_filt)
hub.subscribe(self,
core.message.DataCollectionDeleteMessage,
handler=lambda x: self.remove_data_from_combo(x.data),
filter=dc_filt)
hub.subscribe(self,
core.message.DataUpdateMessage,
handler=lambda x: self._sync_data_labels()
)
hub.subscribe(self,
core.message.ComponentsChangedMessage,
handler=lambda x: self.set_attribute_combo(x.data),
filter=display_data_filter)
[docs] def unregister(self, hub):
super(ImageWidgetBase, self).unregister(hub)
for obj in [self, self.client]:
hub.unsubscribe_all(obj)
[docs] def remove_data_from_combo(self, data):
""" Remove a data object from the combo box, if present """
combo = self.ui.displayDataCombo
pos = combo.findText(data.label)
if pos >= 0:
combo.removeItem(pos)
def _set_norm(self, mode):
""" Use the `ContrastMouseMode` to adjust the transfer function """
# at least one of the clip/vmin pairs will be None
clip_lo, clip_hi = mode.get_clip_percentile()
vmin, vmax = mode.get_vmin_vmax()
stretch = mode.stretch
return self.client.set_norm(clip_lo=clip_lo, clip_hi=clip_hi,
stretch=stretch,
vmin=vmin, vmax=vmax,
bias=mode.bias, contrast=mode.contrast)
@property
def window_title(self):
if self.client.display_data is None or self.client.display_attribute is None:
title = ''
else:
data = self.client.display_data.label
a = self.client.rgb_mode()
if a is None: # monochrome mode
title = "%s - %s" % (self.client.display_data.label,
self.client.display_attribute.label)
else:
r = a.r.label if a.r is not None else ''
g = a.g.label if a.g is not None else ''
b = a.b.label if a.b is not None else ''
title = "%s Red = %s Green = %s Blue = %s" % (data, r, g, b)
return title
def _sync_data_combo_labels(self):
combo = self.ui.displayDataCombo
for i in range(combo.count()):
combo.setItemText(i, combo.itemData(i).label)
def _sync_data_labels(self):
self.update_window_title()
self._sync_data_combo_labels()
def __str__(self):
return "Image Widget"
def _confirm_large_image(self, data):
"""Ask user to confirm expensive operations
:rtype: bool. Whether the user wishes to continue
"""
warn_msg = ("WARNING: Image has %i pixels, and may render slowly."
" Continue?" % data.size)
title = "Contour large image?"
ok = QtGui.QMessageBox.Ok
cancel = QtGui.QMessageBox.Cancel
buttons = ok | cancel
result = QtGui.QMessageBox.question(self, title, warn_msg,
buttons=buttons,
defaultButton=cancel)
return result == ok
@defer_draw
[docs] def restore_layers(self, rec, context):
with delay_callback(self.client, 'display_data', 'display_attribute'):
self.client.restore_layers(rec, context)
for artist in self.layers:
self.add_data_to_combo(artist.layer.data)
self.set_attribute_combo(self.client.display_data)
self._sync_data_combo_labels()
[docs] def closeEvent(self, event):
# close window and all plugins
super(ImageWidgetBase, self).closeEvent(event)
if event.isAccepted():
for t in self._tools:
t.close()
[docs]class ImageWidget(ImageWidgetBase):
"""
A matplotlib-based image widget
"""
[docs] def make_client(self):
return MplImageClient(self._data,
self.central_widget.canvas.fig,
layer_artist_container=self._layer_artist_container)
[docs] def make_toolbar(self):
result = GlueToolbar(self.central_widget.canvas, self, name='Image')
for mode in self._mouse_modes():
result.add_mode(mode)
cmap = _colormap_mode(self, self.client.set_cmap)
result.addWidget(cmap)
# connect viewport update buttons to client commands to
# allow resampling
cl = self.client
result.buttons['HOME'].triggered.connect(nonpartial(cl.check_update))
result.buttons['FORWARD'].triggered.connect(nonpartial(
cl.check_update))
result.buttons['BACK'].triggered.connect(nonpartial(cl.check_update))
return result
def _mouse_modes(self):
axes = self.client.axes
def apply_mode(mode):
for roi_mode in roi_modes:
if roi_mode != mode:
roi_mode._roi_tool.reset()
self.apply_roi(mode.roi())
rect = RectangleMode(axes, roi_callback=apply_mode)
circ = CircleMode(axes, roi_callback=apply_mode)
poly = PolyMode(axes, roi_callback=apply_mode)
roi_modes = [rect, circ, poly]
contrast = ContrastMode(axes, move_callback=self._set_norm)
self._contrast = contrast
# Get modes from tools
tool_modes = []
for tool in self._tools:
tool_modes += tool._get_modes(axes)
add_callback(self.client, 'display_data', tool._display_data_hook)
return [rect, circ, poly, contrast] + tool_modes
[docs] def paintEvent(self, event):
super(ImageWidget, self).paintEvent(event)
pos = self.central_widget.canvas.mapFromGlobal(QtGui.QCursor.pos())
x, y = pos.x(), self.central_widget.canvas.height() - pos.y()
self._update_intensity_label(x, y)
def _intensity_label(self, x, y):
x, y = self.client.axes.transData.inverted().transform([(x, y)])[0]
value = self.client.point_details(x, y)['value']
lbl = '' if value is None else "data: %s" % value
return lbl
def _update_intensity_label(self, x, y):
lbl = self._intensity_label(x, y)
self.label_widget.setText(lbl)
fm = self.label_widget.fontMetrics()
w, h = fm.width(lbl), fm.height()
g = QtCore.QRect(20, self.central_widget.geometry().height() - h, w, h)
self.label_widget.setGeometry(g)
def _connect(self):
super(ImageWidget, self)._connect()
self.ui.rgb_options.current_changed.connect(lambda: self._toolbars[0].set_mode(self._contrast))
self.central_widget.canvas.resize_end.connect(self.client.check_update)
class ColormapAction(QtGui.QAction):
def __init__(self, label, cmap, parent):
super(ColormapAction, self).__init__(label, parent)
self.cmap = cmap
pm = cmap2pixmap(cmap)
self.setIcon(QtGui.QIcon(pm))
def _colormap_mode(parent, on_trigger):
from glue import config
# actions for each colormap
acts = []
for label, cmap in config.colormaps:
a = ColormapAction(label, cmap, parent)
a.triggered.connect(nonpartial(on_trigger, cmap))
acts.append(a)
# Toolbar button
tb = QtGui.QToolButton()
tb.setWhatsThis("Set color scale")
tb.setToolTip("Set color scale")
icon = get_icon('glue_rainbow')
tb.setIcon(icon)
tb.setPopupMode(QtGui.QToolButton.InstantPopup)
tb.addActions(acts)
return tb
[docs]class StandaloneImageWidget(QtGui.QMainWindow):
"""
A simplified image viewer, without any brushing or linking,
but with the ability to adjust contrast and resample.
"""
window_closed = QtCore.Signal()
def __init__(self, image=None, wcs=None, parent=None, **kwargs):
"""
:param image: Image to display (2D numpy array)
:param parent: Parent widget (optional)
:param kwargs: Extra keywords to pass to imshow
"""
super(StandaloneImageWidget, self).__init__(parent)
self.central_widget = MplWidget()
self.setCentralWidget(self.central_widget)
self._setup_axes()
self._im = None
self._norm = DS9Normalize()
self.make_toolbar()
if image is not None:
self.set_image(image=image, wcs=wcs, **kwargs)
def _setup_axes(self):
from glue.viewers.common.viz_client import init_mpl
_, self._axes = init_mpl(self.central_widget.canvas.fig, axes=None, wcs=True)
self._axes.set_aspect('equal', adjustable='datalim')
[docs] def set_image(self, image=None, wcs=None, **kwargs):
"""
Update the image shown in the widget
"""
if self._im is not None:
self._im.remove()
self._im = None
kwargs.setdefault('origin', 'upper')
if wcs is not None:
self._axes.reset_wcs(wcs)
self._im = imshow(self._axes, image, norm=self._norm, cmap='gray', **kwargs)
self._im_array = image
self._wcs = wcs
self._redraw()
@property
def axes(self):
"""
The Matplolib axes object for this figure
"""
return self._axes
def _redraw(self):
self.central_widget.canvas.draw()
def _set_cmap(self, cmap):
self._im.set_cmap(cmap)
self._redraw()
[docs] def mdi_wrap(self):
"""
Embed this widget in a GlueMdiSubWindow
"""
from glue.app.qt.mdi_area import GlueMdiSubWindow
sub = GlueMdiSubWindow()
sub.setWidget(self)
self.destroyed.connect(sub.close)
self.window_closed.connect(sub.close)
sub.resize(self.size())
self._mdi_wrapper = sub
return sub
[docs] def closeEvent(self, event):
self.window_closed.emit()
return super(StandaloneImageWidget, self).closeEvent(event)
def _set_norm(self, mode):
""" Use the `ContrastMouseMode` to adjust the transfer function """
clip_lo, clip_hi = mode.get_clip_percentile()
vmin, vmax = mode.get_vmin_vmax()
stretch = mode.stretch
self._norm.clip_lo = clip_lo
self._norm.clip_hi = clip_hi
self._norm.stretch = stretch
self._norm.bias = mode.bias
self._norm.contrast = mode.contrast
self._norm.vmin = vmin
self._norm.vmax = vmax
self._im.set_norm(self._norm)
self._redraw()
[docs] def make_toolbar(self):
"""
Setup the toolbar
"""
result = GlueToolbar(self.central_widget.canvas, self,
name='Image')
result.add_mode(ContrastMode(self._axes, move_callback=self._set_norm))
cm = _colormap_mode(self, self._set_cmap)
result.addWidget(cm)
self._cmap_actions = cm.actions()
self.addToolBar(result)
return result