Source code for jdaviz.configs.imviz.helper

import os
import re
from copy import deepcopy

from astropy.coordinates import SkyCoord
from astropy.utils.introspection import minversion
from astropy.wcs import NoConvergence
from astropy.wcs.wcsapi import BaseHighLevelWCS
from echo import delay_callback
from glue.core import BaseData

from jdaviz.core.events import SnackbarMessage
from jdaviz.core.helpers import ConfigHelper

__all__ = ['Imviz']

ASTROPY_LT_4_3 = not minversion('astropy', '4.3')


[docs]class Imviz(ConfigHelper): """Imviz Helper class""" _default_configuration = 'imviz'
[docs] def load_data(self, data, parser_reference=None, **kwargs): """Load data into Imviz. Parameters ---------- data : obj or str File name or object to be loaded. Supported formats include: * ``'filename.fits'`` (or any extension that ``astropy.io.fits`` supports; first image extension found is loaded unless ``ext`` keyword is also given) * ``'filename.fits[SCI]'`` (loads only first SCI extension) * ``'filename.fits[SCI,2]'`` (loads the second SCI extension) * ``'filename.jpg'`` (requires ``scikit-image``; grayscale only) * ``'filename.png'`` (requires ``scikit-image``; grayscale only) * JWST ASDF-in-FITS file (requires ``jwst``; ``data`` or given ``ext`` + GWCS) * ``astropy.io.fits.HDUList`` object (first image extension found is loaded unless ``ext`` keyword is also given) * ``astropy.io.fits.ImageHDU`` object * ``astropy.nddata.NDData`` object (2D only but may have unit, mask, or uncertainty attached) * Numpy array (2D only) parser_reference This is used internally by the app. kwargs : dict Extra keywords to be passed into app-level parser. The only one you might call directly here is ``ext`` (any FITS extension format supported by ``astropy.io.fits``) and ``show_in_viewer`` (bool). Notes ----- When loading image formats that support RGB color like JPG or PNG, the files are converted to greyscale. This is done following the algorithm of ``skimage.color.rgb2grey``, which involves weighting the channels as ``0.2125 R + 0.7154 G + 0.0721 B``. If you prefer a different weighting, you can use ``skimage.io.imread`` to produce your own greyscale image as Numpy array and load the latter instead. """ if isinstance(data, str): filelist = data.split(',') if len(filelist) > 1 and 'data_label' in kwargs: raise ValueError('Do not manually overwrite data_label for ' 'a list of images') for data in filelist: kw = deepcopy(kwargs) filepath, ext, data_label = split_filename_with_fits_ext(data) # This, if valid, will overwrite input. if ext is not None: kw['ext'] = ext # This will only overwrite if not provided. if 'data_label' not in kw: kw['data_label'] = data_label self.app.load_data( filepath, parser_reference=parser_reference, **kw) else: self.app.load_data( data, parser_reference=parser_reference, **kwargs)
[docs] def center_on(self, point): """Centers the view on a particular point. Parameters ---------- point : tuple or `~astropy.coordinates.SkyCoord` If tuple of ``(X, Y)`` is given, it is assumed to be in data coordinates and 0-indexed. Raises ------ AttributeError Sky coordinates are given but image does not have a valid WCS. """ viewer = self.app.get_viewer("viewer-1") if isinstance(point, SkyCoord): i_top = get_top_layer_index(viewer) image = viewer.layers[i_top].layer if hasattr(image, 'coords') and isinstance(image.coords, BaseHighLevelWCS): try: pix = image.coords.world_to_pixel(point) # 0-indexed X, Y except NoConvergence as e: # pragma: no cover self.app.hub.broadcast(SnackbarMessage( f'{point} is likely out of bounds: {repr(e)}', color="warning", sender=self.app)) return else: raise AttributeError(f'{image.label} does not have a valid WCS') else: pix = point # Disallow centering outside of display. if (pix[0] < viewer.state.x_min or pix[0] > viewer.state.x_max or pix[1] < viewer.state.y_min or pix[1] > viewer.state.y_max): self.app.hub.broadcast(SnackbarMessage( f'{pix} is out of bounds', color="warning", sender=self.app)) return with delay_callback(viewer.state, 'x_min', 'x_max', 'y_min', 'y_max'): width = viewer.state.x_max - viewer.state.x_min height = viewer.state.y_max - viewer.state.y_min viewer.state.x_min = pix[0] - (width * 0.5) viewer.state.y_min = pix[1] - (height * 0.5) viewer.state.x_max = viewer.state.x_min + width viewer.state.y_max = viewer.state.y_min + height
[docs] def offset_to(self, dx, dy, skycoord_offset=False): """Move the center to a point that is given offset away from the current center. Parameters ---------- dx, dy : float or `~astropy.units.Quantity` Offset value. The presence of unit depends on ``skycoord_offset``. skycoord_offset : bool If `True`, offset (lon, lat) must be given as ``Quantity``. Otherwise, they are in pixel values (float). Raises ------ AttributeError Sky offset is given but image does not have a valid WCS. """ viewer = self.app.get_viewer("viewer-1") width = viewer.state.x_max - viewer.state.x_min height = viewer.state.y_max - viewer.state.y_min if skycoord_offset: i_top = get_top_layer_index(viewer) image = viewer.layers[i_top].layer if hasattr(image, 'coords') and isinstance(image.coords, BaseHighLevelWCS): # To avoid distortion headache, assume offset is relative to # displayed center. x_cen = viewer.state.x_min + (width * 0.5) y_cen = viewer.state.y_min + (height * 0.5) sky_cen = image.coords.pixel_to_world(x_cen, y_cen) if ASTROPY_LT_4_3: from astropy.coordinates import SkyOffsetFrame new_sky_cen = sky_cen.__class__( SkyOffsetFrame(dx, dy, origin=sky_cen.frame).transform_to(sky_cen)) else: new_sky_cen = sky_cen.spherical_offsets_by(dx, dy) self.center_on(new_sky_cen) else: raise AttributeError(f'{image.label} does not have a valid WCS') else: with delay_callback(viewer.state, 'x_min', 'x_max', 'y_min', 'y_max'): viewer.state.x_min += dx viewer.state.y_min += dy viewer.state.x_max = viewer.state.x_min + width viewer.state.y_max = viewer.state.y_min + height
def split_filename_with_fits_ext(filename): """Split a ``filename[ext]`` input into filename and FITS extension. Parameters ---------- filename : str Can be a plain filename or ``filename[ext]``. The latter is a form of input that is commonly used by DS9. Example values: * ``'myimage.fits'`` * ``'myimage.fits[SCI]'`` (assumes ``EXTVER=1``) * ``'myimage.fits[SCI,1]'`` Returns ------- filepath : str Path to the file, without extension. ext : str, tuple, or `None` FITS extension, if given. Examples: ``'SCI'`` or ``('SCI', 1)`` data_label : str Human-readable data label for Glue. Extension info will be added later in the parser. """ s = os.path.splitext(filename) ext_match = re.match(r'(.+)\[(.+)\]', s[1]) if ext_match is None: sfx = s[1] ext = None else: sfx = ext_match.group(1) ext = ext_match.group(2) if ',' in ext: ext = ext.split(',') ext[1] = int(ext[1]) ext = tuple(ext) elif not re.match(r'\D+', ext): ext = int(ext) filepath = f'{s[0]}{sfx}' data_label = os.path.basename(s[0]) return filepath, ext, data_label def get_top_layer_index(viewer): """Get index of the top visible layer in Imviz. This is because when blinked, first layer might not be top visible layer. """ return [i for i, lyr in enumerate(viewer.layers) if lyr.visible and isinstance(lyr.layer, BaseData)][-1]