Source code for jdaviz.configs.imviz.plugins.viewers

import numpy as np

from astropy.visualization import ImageNormalize, LinearStretch, PercentileInterval
from glue.core.link_helpers import LinkSame
from glue_jupyter.bqplot.image import BqplotImageView

from jdaviz.configs.imviz import wcs_utils
from jdaviz.configs.imviz.helper import data_has_valid_wcs, layer_is_image_data, get_top_layer_index
from jdaviz.core.astrowidgets_api import AstrowidgetsImageViewerMixin
from jdaviz.core.events import SnackbarMessage
from jdaviz.core.registries import viewer_registry
from jdaviz.configs.default.plugins.viewers import JdavizViewerMixin

__all__ = ['ImvizImageView']


[docs]@viewer_registry("imviz-image-viewer", label="Image 2D (Imviz)") class ImvizImageView(BqplotImageView, AstrowidgetsImageViewerMixin, JdavizViewerMixin): # Whether to inherit tools from glue-jupyter automatically. Set this to # False to have full control here over which tools are shown in case new # ones are added in glue-jupyter in future that we don't want here. inherit_tools = False tools = ['bqplot:home', 'jdaviz:boxzoom', 'jdaviz:boxzoommatch', 'bqplot:panzoom', 'jdaviz:panzoommatch', 'jdaviz:contrastbias', 'jdaviz:blinkonce', 'bqplot:rectangle', 'bqplot:circle', 'bqplot:ellipse'] default_class = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.init_astrowidgets_api() self.label_mouseover = None self.compass = None self.add_event_callback(self.on_mouse_or_key_event, events=['mousemove', 'mouseenter', 'mouseleave', 'keydown']) self.state.add_callback('x_min', self.on_limits_change) self.state.add_callback('x_max', self.on_limits_change) self.state.add_callback('y_min', self.on_limits_change) self.state.add_callback('y_max', self.on_limits_change) self.state.show_axes = False self.figure.fig_margin = {'left': 0, 'bottom': 0, 'top': 0, 'right': 0} # By default, glue computes a fixed resolution buffer that matches the # axes - but this means that when panning, one sees white outside of # the original buffer until the buffer updates again, thus there is a # lag in the image display. By increasing the external padding to 0.5 # the image is made larger by 50% along all four sides, helping create # the illusion of smooth panning. We can increase this further to # improve the panning experience, but this can cause a larger delay # when the image does need to update as it will be more computationally # intensive. self.state.image_external_padding = 0.5
[docs] def on_mouse_or_key_event(self, data): # Find visible layers visible_layers = [layer for layer in self.state.layers if layer.visible] if len(visible_layers) == 0: return if self.label_mouseover is None: if 'g-coords-info' in self.session.application._tools: self.label_mouseover = self.session.application._tools['g-coords-info'] else: return if data['event'] == 'mousemove': # Display the current cursor coordinates (both pixel and world) as # well as data values. For now we use the first dataset in the # viewer for the data values. # Extract first dataset from visible layers and use this for coordinates - the choice # of dataset shouldn't matter if the datasets are linked correctly image = visible_layers[0].layer # Extract data coordinates - these are pixels in the image x = data['domain']['x'] y = data['domain']['y'] if x is None or y is None: # Out of bounds self.label_mouseover.pixel = "" self.label_mouseover.reset_coords_display() self.label_mouseover.value = "" return maxsize = int(np.ceil(np.log10(np.max(image.shape)))) + 3 fmt = 'x={0:0' + str(maxsize) + '.1f} y={1:0' + str(maxsize) + '.1f}' if data_has_valid_wcs(image): # Convert these to a SkyCoord via WCS - note that for other datasets # we aren't actually guaranteed to get a SkyCoord out, just for images # with valid celestial WCS try: # Convert X,Y from reference data to the one we are actually seeing. # world_to_pixel return scalar ndarray that we need to convert to float. if self.get_link_type(image.label) == 'wcs': x, y = list(map(float, image.coords.world_to_pixel( self.state.reference_data.coords.pixel_to_world(x, y)))) self.label_mouseover.pixel = (fmt.format(x, y)) coo = image.coords.pixel_to_world(x, y).icrs self.label_mouseover.set_coords(coo) except Exception: self.label_mouseover.pixel = (fmt.format(x, y)) self.label_mouseover.reset_coords_display() else: self.label_mouseover.pixel = (fmt.format(x, y)) self.label_mouseover.reset_coords_display() # Extract data values at this position. # TODO: for now we just use the first visible layer but we should think # of how to display values when multiple datasets are present. if (x > -0.5 and y > -0.5 and x < image.shape[1] - 0.5 and y < image.shape[0] - 0.5 and hasattr(visible_layers[0], 'attribute')): attribute = visible_layers[0].attribute value = image.get_data(attribute)[int(round(y)), int(round(x))] unit = image.get_component(attribute).units self.label_mouseover.value = f'{value:+10.5e} {unit}' else: self.label_mouseover.value = '' elif data['event'] == 'mouseleave' or data['event'] == 'mouseenter': self.label_mouseover.pixel = "" self.label_mouseover.reset_coords_display() self.label_mouseover.value = "" elif data['event'] == 'keydown' and data['key'] == 'b': self.blink_once() # Also update the coordinates display. data['event'] = 'mousemove' self.on_mouse_or_key_event(data)
[docs] def on_limits_change(self, *args): try: i = get_top_layer_index(self) except IndexError: if self.compass is not None: self.compass.clear_compass() return self.set_compass(self.state.layers[i].layer)
[docs] def set_compass(self, image): """Update the Compass plugin with info from the given image Data object.""" if self.compass is None: # Maybe another viewer has it return zoom_limits = (self.state.x_min, self.state.y_min, self.state.x_max, self.state.y_max) if data_has_valid_wcs(image): wcs = image.coords # Convert X,Y from reference data to the one we are actually seeing. if self.get_link_type(image.label) == 'wcs': x = wcs.world_to_pixel(self.state.reference_data.coords.pixel_to_world( (self.state.x_min, self.state.x_max), (self.state.y_min, self.state.y_max))) zoom_limits = (x[0][0], x[1][0], x[0][1], x[1][1]) else: wcs = None arr = image[image.main_components[0]] vmin, vmax = PercentileInterval(95).get_limits(arr) norm = ImageNormalize(vmin=vmin, vmax=vmax, stretch=LinearStretch()) self.compass.draw_compass(image.label, wcs_utils.draw_compass_mpl( arr, wcs, show=False, zoom_limits=zoom_limits, norm=norm))
[docs] def set_plot_axes(self): self.figure.axes[1].tick_format = None self.figure.axes[0].tick_format = None self.figure.axes[1].label = "y: pixels" self.figure.axes[0].label = "x: pixels" # Make it so y axis label is not covering tick numbers. self.figure.axes[1].label_offset = "-50"
[docs] def data(self, cls=None): return [layer_state.layer # .get_object(cls=cls or self.default_class) for layer_state in self.state.layers if hasattr(layer_state, 'layer') and layer_is_image_data(layer_state.layer)]