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'] # categories: zoom resets, zoom, pan, subset, select tools, shortcuts tools_nested = [ ['bqplot:home'], ['jdaviz:boxzoom', 'jdaviz:boxzoommatch'], ['bqplot:panzoom', 'jdaviz:panzoommatch'], ['bqplot:circle', 'bqplot:rectangle', 'bqplot:ellipse'], ['jdaviz:blinkonce', 'jdaviz:contrastbias'], ['jdaviz:sidebar_plot', 'jdaviz:sidebar_export', 'jdaviz:sidebar_compass'] ] default_class = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.init_astrowidgets_api() self._initialize_toolbar_nested() self.label_mouseover = None self.compass = None self.line_profile_xy = 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 self.line_profile_xy is None: try: self.line_profile_xy = self.session.jdaviz_app.get_tray_item_from_name( 'imviz-line-profile-xy') except KeyError: # pragma: no cover 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': key_pressed = data['key'] if key_pressed == 'b': self.blink_once() # Also update the coordinates display. data['event'] = 'mousemove' self.on_mouse_or_key_event(data) elif key_pressed == 'l': # Same data as mousemove above. self.line_profile_xy.selected_x = data['domain']['x'] self.line_profile_xy.selected_y = data['domain']['y'] self.line_profile_xy.selected_viewer = self.reference_id self.line_profile_xy.vue_draw_plot()
[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)
def _get_zoom_limits(self, image): """Return ``(x_min, y_min, x_max, y_max)`` for given image. This is needed because viewer values are only based on reference image, which can be inaccurate if given image is dithered and they are linked by WCS. """ if data_has_valid_wcs(image) and self.get_link_type(image.label) == 'wcs': # Convert X,Y from reference data to the one we are actually seeing. x = image.coords.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: zoom_limits = (self.state.x_min, self.state.y_min, self.state.x_max, self.state.y_max) return zoom_limits
[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._get_zoom_limits(image) # Downsample input data to about 400px (as per compass.vue) for performance. xstep = max(1, round(image.shape[1] / 400)) ystep = max(1, round(image.shape[0] / 400)) arr = image[image.main_components[0]][::ystep, ::xstep] 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, orig_shape=image.shape, wcs=image.coords, 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)]