From 35cd433716966cbda7a9a6b9e020a28c227245d4 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 20 Dec 2022 13:19:44 -0500 Subject: [PATCH] set bounding box to None to avoid layer cropping (#1908) * set bounding box to None to avoid layer cropping * mouseover display to warn when outside original bounding box Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com> --- CHANGES.rst | 3 + docs/imviz/displayimages.rst | 32 ++++ .../plugins/subset_plugin/subset_plugin.py | 2 +- .../imviz/plugins/coords_info/coords_info.py | 10 +- .../imviz/plugins/coords_info/coords_info.vue | 11 +- jdaviz/configs/imviz/plugins/parsers.py | 10 ++ jdaviz/configs/imviz/plugins/tools.py | 2 +- jdaviz/configs/imviz/plugins/viewers.py | 41 ++++- jdaviz/configs/imviz/tests/test_linking.py | 88 +++++++++- jdaviz/configs/imviz/tests/utils.py | 53 +++++- jdaviz/configs/imviz/wcs_utils.py | 23 +++ jdaviz/core/region_translators.py | 2 +- notebooks/concepts/imviz_dithered_gwcs.ipynb | 160 ++++++++++++++++++ 13 files changed, 417 insertions(+), 20 deletions(-) create mode 100644 notebooks/concepts/imviz_dithered_gwcs.ipynb diff --git a/CHANGES.rst b/CHANGES.rst index 0d6e53105a..5429ce4f03 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -140,6 +140,9 @@ Imviz - Viewer options in some plugins no longer displaying the wrong names. [#1920] +- Fixes cropped image layer with WCS linking without fast-approximation, mouseover display + for GWCS now shows when information is outside original bounding box, if applicable. [#1908] + Mosviz ^^^^^^ diff --git a/docs/imviz/displayimages.rst b/docs/imviz/displayimages.rst index 551e17b293..6a7abb8570 100644 --- a/docs/imviz/displayimages.rst +++ b/docs/imviz/displayimages.rst @@ -35,6 +35,38 @@ cursor's location in pixel space (X and Y), the RA and Dec at that point, and th of the data there. This information is displayed in the top bar of the UI, on the middle-right side. +Notes on GWCS +------------- + +If your *reference data* has GWCS with a bounding box, any coordinates transformation +outside that bounding box is less reliable. This still applies even when you are +looking at some other data that is not the reference data if they are linked by WCS +because all transformations in glue go through the reference data. Such a situation +is indicated by "(est.)" and the affected coordinates becoming gray. + +If your data of interest also has a GWCS with a bounding box, only +the mouseover data where it overlaps with the reference data's +bounding box is completely reliable. Unreliable coordinates transformation here +will also gray out in a similar fashion as above. + +To avoid inaccurate transforms, consider one of the following workflows: + +* Make sure your reference data's GWCS has a bounding box that encompasses all + the other data you are trying to visualize together. +* If the above is not possible, avoid overlaying different data with GWCS that + do not overlap. + +.. warning:: + + If you rely on the GWCS bounding box, it will be set to None when + you data is loaded into Imviz, but the original bounding box, + if available, is now in a hidden ``_orig_bounding_box`` + attribute of the GWCS object. You can restore the bounding box by + assigning the value of ``_orig_bounding_box`` back to its + ``bounding_box`` attribute. + +Note that FITS WCS has no similar concept of bounding box. + Home ==== diff --git a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py index a0984ab71d..76f8e91dff 100644 --- a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py +++ b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py @@ -249,7 +249,7 @@ def vue_recenter_subset(self, *args): # the reference data. However, Subset is always defined w.r.t. # the reference data, so we need to convert back. viewer = self.app._jdaviz_helper.default_viewer - x, y, _ = viewer._get_real_xy( + x, y, _, _ = viewer._get_real_xy( data, phot_aperstats.xcentroid, phot_aperstats.ycentroid, reverse=True) if not np.all(np.isfinite((x, y))): raise ValueError(f'Invalid centroid ({x}, {y})') diff --git a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py index e34046c11a..46a0c0cfdb 100644 --- a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py +++ b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py @@ -1,4 +1,4 @@ -from traitlets import Unicode +from traitlets import Bool, Unicode from jdaviz.core.registries import tool_registry from jdaviz.core.template_mixin import TemplateMixin @@ -19,6 +19,8 @@ class CoordsInfo(TemplateMixin): world_dec = Unicode("").tag(sync=True) world_ra_deg = Unicode("").tag(sync=True) world_dec_deg = Unicode("").tag(sync=True) + unreliable_world = Bool(False).tag(sync=True) + unreliable_pixel = Bool(False).tag(sync=True) def reset_coords_display(self): self.world_label_prefix = '\u00A0' @@ -28,8 +30,10 @@ def reset_coords_display(self): self.world_dec = '' self.world_ra_deg = '' self.world_dec_deg = '' + self.unreliable_world = False + self.unreliable_pixel = False - def set_coords(self, sky): + def set_coords(self, sky, unreliable_world=False, unreliable_pixel=False): celestial_coordinates = sky.to_string('hmsdms', precision=4, pad=True).split() celestial_coordinates_deg = sky.to_string('decimal', precision=10, pad=True).split() world_ra = celestial_coordinates[0] @@ -47,3 +51,5 @@ def set_coords(self, sky): self.world_dec = world_dec self.world_ra_deg = world_ra_deg self.world_dec_deg = world_dec_deg + self.unreliable_world = unreliable_world + self.unreliable_pixel = unreliable_pixel diff --git a/jdaviz/configs/imviz/plugins/coords_info/coords_info.vue b/jdaviz/configs/imviz/plugins/coords_info/coords_info.vue index 7fc28fe3f4..d875d80a78 100644 --- a/jdaviz/configs/imviz/plugins/coords_info/coords_info.vue +++ b/jdaviz/configs/imviz/plugins/coords_info/coords_info.vue @@ -6,18 +6,19 @@
- - + - - + + diff --git a/jdaviz/configs/imviz/plugins/parsers.py b/jdaviz/configs/imviz/plugins/parsers.py index ba7a8d6513..6613153560 100644 --- a/jdaviz/configs/imviz/plugins/parsers.py +++ b/jdaviz/configs/imviz/plugins/parsers.py @@ -7,6 +7,7 @@ from astropy.nddata import NDData from astropy.wcs import WCS from glue.core.data import Component, Data +from gwcs.wcs import WCS as GWCS from jdaviz.core.registries import data_parser_registry from jdaviz.core.events import SnackbarMessage @@ -124,6 +125,15 @@ def _parse_image(app, file_obj, data_label, ext=None): data_iter = get_image_data_iterator(app, file_obj, data_label, ext=ext) for data, data_label in data_iter: + if data.coords is not None: + if isinstance(data.coords, GWCS) and (data.coords.bounding_box is not None): + # keep a copy of the original bounding box so we can detect + # when extrapolating beyond, but then remove the bounding box + # so that image layers are not cropped. + # NOTE: if extending this beyond GWCS, the mouseover logic + # for outside_*_bounding_box should also be updated. + data.coords._orig_bounding_box = data.coords.bounding_box + data.coords.bounding_box = None data_label = app.return_data_label(data_label, alt_name="image_data") app.add_data(data, data_label) diff --git a/jdaviz/configs/imviz/plugins/tools.py b/jdaviz/configs/imviz/plugins/tools.py index fc6bb9c2cb..5f4b28cbb8 100644 --- a/jdaviz/configs/imviz/plugins/tools.py +++ b/jdaviz/configs/imviz/plugins/tools.py @@ -101,7 +101,7 @@ def on_click(self, data): y = data['domain']['y'] if x is None or y is None: # Out of bounds return - x, y, _ = self.viewer._get_real_xy(image, x, y) + x, y, _, _ = self.viewer._get_real_xy(image, x, y) self.viewer.center_on((x, y)) diff --git a/jdaviz/configs/imviz/plugins/viewers.py b/jdaviz/configs/imviz/plugins/viewers.py index 04446988db..46bf0d8e6f 100644 --- a/jdaviz/configs/imviz/plugins/viewers.py +++ b/jdaviz/configs/imviz/plugins/viewers.py @@ -108,7 +108,7 @@ def on_mouse_or_key_event(self, data): maxsize = int(np.ceil(np.log10(np.max(image.shape)))) + 3 fmt = 'x={0:0' + str(maxsize) + '.1f} y={1:0' + str(maxsize) + '.1f}' - x, y, coords_status = self._get_real_xy(image, x, y) + x, y, coords_status, (unreliable_world, unreliable_pixel) = self._get_real_xy(image, x, y) # noqa self.label_mouseover.pixel = (fmt.format(x, y)) if coords_status: @@ -117,7 +117,9 @@ def on_mouse_or_key_event(self, data): except Exception: # WCS might not be celestial self.label_mouseover.reset_coords_display() else: - self.label_mouseover.set_coords(coo) + self.label_mouseover.set_coords(coo, + unreliable_world=unreliable_world, + unreliable_pixel=unreliable_pixel) # noqa else: self.label_mouseover.reset_coords_display() @@ -157,7 +159,7 @@ def on_mouse_or_key_event(self, data): y = data['domain']['y'] if x is None or y is None: # Out of bounds return - x, y, _ = self._get_real_xy(image, x, y) + x, y, _, _ = self._get_real_xy(image, x, y) self.line_profile_xy.selected_x = x self.line_profile_xy.selected_y = y self.line_profile_xy.selected_viewer = self.reference_id @@ -234,7 +236,9 @@ def top_visible_data_label(self): return data_label def _get_real_xy(self, image, x, y, reverse=False): - """Return real (X, Y) position and status in case of dithering. + """Return real (X, Y) position and status in case of dithering as well as whether the + results were within the bounding box of the reference data or required possibly inaccurate + extrapolation. ``coords_status`` is for ``self.label_mouseover`` coords handling only. When `True`, it sets the coords, otherwise it resets. @@ -243,27 +247,46 @@ def _get_real_xy(self, image, x, y, reverse=False): in Subset Tools plugin). Never use this for coordinates display panel. """ + # By default we'll assume the coordinates are valid and within any applicable bounding box. + unreliable_world = False + unreliable_pixel = False 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: + link_type = self.get_link_type(image.label) + # 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': + if link_type == 'wcs': if not reverse: + outside_ref_bounding_box = wcs_utils.data_outside_gwcs_bounding_box( + self.state.reference_data, x, y) x, y = list(map(float, image.coords.world_to_pixel( self.state.reference_data.coords.pixel_to_world(x, y)))) + outside_image_bounding_box = wcs_utils.data_outside_gwcs_bounding_box( + image, x, y) + unreliable_pixel = outside_image_bounding_box or outside_ref_bounding_box + unreliable_world = unreliable_pixel else: + # We don't bother with unreliable_pixel and unreliable_world computation + # because this takes input (x, y) in the frame of visible layer and wants + # to convert it back to the frame of reference layer to pass back to the + # viewer. At this point, we no longer know if input (x, y) is accurate + # or not. x, y = list(map(float, self.state.reference_data.coords.world_to_pixel( image.coords.pixel_to_world(x, y)))) + else: # pixels or self + unreliable_world = wcs_utils.data_outside_gwcs_bounding_box(image, x, y) + coords_status = True except Exception: coords_status = False else: coords_status = False - return x, y, coords_status + return x, y, coords_status, (unreliable_world, unreliable_pixel) def _get_zoom_limits(self, image): """Return a list of ``(x, y)`` that defines four corners of @@ -339,10 +362,12 @@ def get_link_type(self, data_label): Link look-up failed. """ - if self.state.reference_data is None: + if len(self.session.application.data_collection) == 0: raise ValueError('No reference data for link look-up') - ref_label = self.state.reference_data.label + # the original links were created against data_collection[0], not necessarily + # against the current viewer reference_data + ref_label = self.session.application.data_collection[0].label if data_label == ref_label: return 'self' diff --git a/jdaviz/configs/imviz/tests/test_linking.py b/jdaviz/configs/imviz/tests/test_linking.py index 58f187541c..096d56b0f6 100644 --- a/jdaviz/configs/imviz/tests/test_linking.py +++ b/jdaviz/configs/imviz/tests/test_linking.py @@ -8,7 +8,7 @@ from jdaviz.configs.imviz.helper import get_reference_image_data from jdaviz.configs.imviz.tests.utils import ( - BaseImviz_WCS_NoWCS, BaseImviz_WCS_WCS, BaseImviz_WCS_GWCS) + BaseImviz_WCS_NoWCS, BaseImviz_WCS_WCS, BaseImviz_WCS_GWCS, BaseImviz_GWCS_GWCS) class BaseLinkHandler: @@ -237,6 +237,8 @@ def test_wcslink_rotated(self): assert self.viewer.label_mouseover.value == '+1.00000e+00 ' assert self.viewer.label_mouseover.world_ra_deg == '' assert self.viewer.label_mouseover.world_dec_deg == '' + assert not self.viewer.label_mouseover.unreliable_world + assert not self.viewer.label_mouseover.unreliable_pixel self.viewer.on_mouse_or_key_event({'event': 'keydown', 'key': 'b', 'domain': {'x': 0, 'y': 0}}) @@ -244,6 +246,8 @@ def test_wcslink_rotated(self): assert self.viewer.label_mouseover.value == '+1.00000e+00 electron / s' assert self.viewer.label_mouseover.world_ra_deg == '3.5817255823' assert self.viewer.label_mouseover.world_dec_deg == '-30.3920580740' + assert not self.viewer.label_mouseover.unreliable_world + assert not self.viewer.label_mouseover.unreliable_pixel self.viewer.on_mouse_or_key_event({'event': 'keydown', 'key': 'b', 'domain': {'x': 0, 'y': 0}}) @@ -251,6 +255,88 @@ def test_wcslink_rotated(self): assert self.viewer.label_mouseover.value == '' assert self.viewer.label_mouseover.world_ra_deg == '3.5817255823' assert self.viewer.label_mouseover.world_dec_deg == '-30.3920580740' + assert not self.viewer.label_mouseover.unreliable_world + assert not self.viewer.label_mouseover.unreliable_pixel + + # Make sure GWCS now can extrapolate. Domain x,y is for FITS WCS data + # but they are linked by WCS. + self.viewer.on_mouse_or_key_event({'event': 'mousemove', + 'domain': {'x': 11.281551269520731, + 'y': 2.480347927198246}}) + assert self.viewer.label_mouseover.pixel == 'x=-1.0 y=-1.0' + assert self.viewer.label_mouseover.value == '' + assert self.viewer.label_mouseover.world_ra_deg == '3.5815955408' + assert self.viewer.label_mouseover.world_dec_deg == '-30.3919405616' + # FITS WCS is reference data and has no concept of bounding box + # but cursor is outside GWCS bounding box + assert self.viewer.label_mouseover.unreliable_world + assert self.viewer.label_mouseover.unreliable_pixel + + +class TestLink_GWCS_GWCS(BaseImviz_GWCS_GWCS): + def test_wcslink_offset(self): + self.imviz.link_data(link_type='wcs', error_on_fail=True) + + # Check the coordinates display: Last loaded is on top. + # Within bounds of non-reference image but out of bounds of reference image. + self.viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 10, 'y': 3}}) + assert self.viewer.label_mouseover.pixel in ('x=07.0 y=00.0', 'x=07.0 y=-0.0') + assert self.viewer.label_mouseover.value == '+0.00000e+00 ' + assert self.viewer.label_mouseover.world_ra_deg == '3.5817877198' + assert self.viewer.label_mouseover.world_dec_deg == '-30.3919358920' + assert self.viewer.label_mouseover.unreliable_world + assert self.viewer.label_mouseover.unreliable_pixel + + # Non-reference image out of bounds of its own bounds but not of the + # reference image's bounds. Head hurting yet? + self.viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 0.5, 'y': 0.5}}) + assert self.viewer.label_mouseover.pixel == 'x=-2.5 y=-2.5' + assert self.viewer.label_mouseover.value == '' + assert self.viewer.label_mouseover.world_ra_deg == '3.5816283341' + assert self.viewer.label_mouseover.world_dec_deg == '-30.3919519949' + assert self.viewer.label_mouseover.unreliable_world + assert self.viewer.label_mouseover.unreliable_pixel + + # Back to reference image + self.viewer.on_mouse_or_key_event({'event': 'keydown', 'key': 'b', + 'domain': {'x': 0, 'y': 0}}) + assert self.viewer.label_mouseover.pixel == 'x=00.0 y=00.0' + assert self.viewer.label_mouseover.value == '+1.00000e+00 electron / s' + assert self.viewer.label_mouseover.world_ra_deg == '3.5816174030' + assert self.viewer.label_mouseover.world_dec_deg == '-30.3919481838' + assert not self.viewer.label_mouseover.unreliable_world + assert not self.viewer.label_mouseover.unreliable_pixel + + # Still reference image but outside its own bounds. + self.viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 10, 'y': 3}}) + assert self.viewer.label_mouseover.pixel == 'x=10.0 y=03.0' + assert self.viewer.label_mouseover.value == '' + assert self.viewer.label_mouseover.world_ra_deg == '3.5817877198' + assert self.viewer.label_mouseover.world_dec_deg == '-30.3919358920' + assert self.viewer.label_mouseover.unreliable_world + assert not self.viewer.label_mouseover.unreliable_pixel + + def test_pixel_linking(self): + self.imviz.link_data(link_type='pixels') + + # Check the coordinates display: Last loaded is on top. + self.viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': -1, 'y': -1}}) + assert self.viewer.label_mouseover.pixel == 'x=-1.0 y=-1.0' + assert self.viewer.label_mouseover.value == '' + assert self.viewer.label_mouseover.world_ra_deg == '3.5816611274' + assert self.viewer.label_mouseover.world_dec_deg == '-30.3919634282' + assert self.viewer.label_mouseover.unreliable_world + assert not self.viewer.label_mouseover.unreliable_pixel + + # Back to reference image with bounds check. + self.viewer.on_mouse_or_key_event({'event': 'keydown', 'key': 'b', + 'domain': {'x': -1, 'y': -1}}) + assert self.viewer.label_mouseover.pixel == 'x=-1.0 y=-1.0' + assert self.viewer.label_mouseover.value == '' + assert self.viewer.label_mouseover.world_ra_deg == '3.5815955408' + assert self.viewer.label_mouseover.world_dec_deg == '-30.3919405616' + assert self.viewer.label_mouseover.unreliable_world + assert not self.viewer.label_mouseover.unreliable_pixel def test_imviz_no_data(imviz_helper): diff --git a/jdaviz/configs/imviz/tests/utils.py b/jdaviz/configs/imviz/tests/utils.py index b24706f77b..dac74048e2 100644 --- a/jdaviz/configs/imviz/tests/utils.py +++ b/jdaviz/configs/imviz/tests/utils.py @@ -9,7 +9,7 @@ from astropy.wcs import WCS from gwcs import coordinate_frames as cf -__all__ = ['BaseImviz_WCS_NoWCS', 'BaseImviz_WCS_WCS'] +__all__ = ['BaseImviz_WCS_NoWCS', 'BaseImviz_WCS_WCS', 'BaseImviz_WCS_GWCS', 'BaseImviz_GWCS_GWCS'] class BaseImviz_WCS_NoWCS: @@ -133,6 +133,7 @@ def setup_class(self, imviz_helper): sky_frame = cf.CelestialFrame(reference_frame=ICRS(), name='icrs', unit=(u.deg, u.deg)) pipeline = [(detector_frame, det2sky), (sky_frame, None)] w_gwcs = gwcs.WCS(pipeline) + w_gwcs.bounding_box = ((0, 8), (0, 10)) * u.pix # x, y # Load data into Imviz: # 1. Data with FITS WCS and unit. @@ -150,3 +151,53 @@ def setup_class(self, imviz_helper): # Since we are not really displaying, need this to test zoom. self.viewer.shape = (100, 100) self.viewer.state._set_axes_aspect_ratio(1) + + +class BaseImviz_GWCS_GWCS: + @pytest.fixture(autouse=True) + def setup_class(self, imviz_helper): + arr = np.zeros((10, 8)) # (ny, nx) + arr[0, 0] = 1 # Bright corner for sanity check + + # GWCS that is adapted from its Getting Started. + shift_by_crpix = models.Shift(-(5 - 1) * u.pix) & models.Shift(-(5 - 1) * u.pix) + matrix = np.array([[1.290551569736E-05, 5.9525007864732E-06], + [5.0226382102765E-06, -1.2644844123757E-05]]) + rotation = models.AffineTransformation2D(matrix * u.deg, translation=[0, 0] * u.deg) + rotation.input_units_equivalencies = {"x": u.pixel_scale(1 * (u.deg / u.pix)), + "y": u.pixel_scale(1 * (u.deg / u.pix))} + rotation.inverse = models.AffineTransformation2D(np.linalg.inv(matrix) * u.pix, + translation=[0, 0] * u.pix) + rotation.inverse.input_units_equivalencies = {"x": u.pixel_scale(1 * (u.pix / u.deg)), + "y": u.pixel_scale(1 * (u.pix / u.deg))} + tan = models.Pix2Sky_TAN() + celestial_rotation = models.RotateNative2Celestial( + 3.581704851882 * u.deg, -30.39197867265 * u.deg, 180 * u.deg) + det2sky = shift_by_crpix | rotation | tan | celestial_rotation + det2sky.name = "linear_transform" + detector_frame = cf.Frame2D(name="detector", axes_names=("x", "y"), unit=(u.pix, u.pix)) + sky_frame = cf.CelestialFrame(reference_frame=ICRS(), name='icrs', unit=(u.deg, u.deg)) + pipeline = [(detector_frame, det2sky), (sky_frame, None)] + w_gwcs_1 = gwcs.WCS(pipeline) + w_gwcs_1.bounding_box = ((0, 8), (0, 10)) * u.pix # x, y + + # Second GWCS that is offset + shift_by_crpix = models.Shift(-1 * u.pix) & models.Shift(-1 * u.pix) + det2sky = shift_by_crpix | rotation | tan | celestial_rotation + det2sky.name = "linear_transform" + pipeline = [(detector_frame, det2sky), (sky_frame, None)] + w_gwcs_2 = gwcs.WCS(pipeline) + w_gwcs_2.bounding_box = ((0, 8), (0, 10)) * u.pix # x, y + + # Load data into Imviz + imviz_helper.load_data(NDData(arr, wcs=w_gwcs_1, unit='electron/s'), data_label='gwcs1') + imviz_helper.load_data(NDData(arr, wcs=w_gwcs_2), data_label='gwcs2') + + self.wcs_1 = w_gwcs_1 + self.wcs_2 = w_gwcs_2 + self.imviz = imviz_helper + self.viewer = imviz_helper.default_viewer + + # Since we are not really displaying, need this to test zoom. + self.viewer.shape = (100, 100) + self.viewer.state._set_axes_aspect_ratio(1) diff --git a/jdaviz/configs/imviz/wcs_utils.py b/jdaviz/configs/imviz/wcs_utils.py index 4d6444a68f..fb671018bb 100644 --- a/jdaviz/configs/imviz/wcs_utils.py +++ b/jdaviz/configs/imviz/wcs_utils.py @@ -9,6 +9,7 @@ import matplotlib.pyplot as plt import numpy as np +from astropy import units as u from astropy.coordinates import SkyCoord from matplotlib.patches import Polygon @@ -252,3 +253,25 @@ def draw_compass_mpl(image, orig_shape=None, wcs=None, show=True, zoom_limits=No plt.close() return base64.b64encode(buff.getvalue()).decode('utf-8') + + +def data_outside_gwcs_bounding_box(data, x, y): + """This is for internal use by Imviz coordinates transformation only.""" + outside_bounding_box = False + if hasattr(data.coords, '_orig_bounding_box'): + # then coords is a GWCS object and had its bounding box cleared + # by the Imviz parser + ints = data.coords._orig_bounding_box.intervals + if isinstance(ints[0].lower, u.Quantity): + bb_xmin = ints[0].lower.value + bb_xmax = ints[0].upper.value + bb_ymin = ints[1].lower.value + bb_ymax = ints[1].upper.value + else: # pragma: no cover + bb_xmin = ints[0].lower + bb_xmax = ints[0].upper + bb_ymin = ints[1].lower + bb_ymax = ints[1].upper + if not (bb_xmin <= x <= bb_xmax and bb_ymin <= y <= bb_ymax): + outside_bounding_box = True # Has to be Python bool, not Numpy bool_ + return outside_bounding_box diff --git a/jdaviz/core/region_translators.py b/jdaviz/core/region_translators.py index a3c755c177..17e6d3defe 100644 --- a/jdaviz/core/region_translators.py +++ b/jdaviz/core/region_translators.py @@ -65,7 +65,7 @@ def _get_region_from_spatial_subset(plugin_obj, subset_label): # not loaded in all the viewers, so use default viewer. viewer = plugin_obj.app._jdaviz_helper.default_viewer - x, y, _ = viewer._get_real_xy( + x, y, _, _ = viewer._get_real_xy( plugin_obj.app.data_collection[plugin_obj.dataset_selected], reg.center.x, reg.center.y) reg.center.x = x diff --git a/notebooks/concepts/imviz_dithered_gwcs.ipynb b/notebooks/concepts/imviz_dithered_gwcs.ipynb new file mode 100644 index 0000000000..a058215f82 --- /dev/null +++ b/notebooks/concepts/imviz_dithered_gwcs.ipynb @@ -0,0 +1,160 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8a6a099a", + "metadata": {}, + "source": [ + "This notebook show how Imviz can visualized dithered JWST data, all of them have GWCS." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cdcac3b4", + "metadata": {}, + "outputs": [], + "source": [ + "import warnings\n", + "\n", + "from astroquery.mast import Observations \n", + "\n", + "from jdaviz import Imviz" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5024c064", + "metadata": {}, + "outputs": [], + "source": [ + "data_dir = \"/Users/username/Data\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd35669a", + "metadata": {}, + "outputs": [], + "source": [ + "files = ['jw01345-o001_t021_nircam_clear-f200w_i2d.fits',\n", + " #'jw01345-o002_t022_nircam_clear-f200w_i2d.fits',\n", + " #'jw01345-o003_t023_nircam_clear-f200w_i2d.fits',\n", + " #'jw01345-o004_t024_nircam_clear-f200w_i2d.fits',\n", + " 'jw01345-o052_t022_nircam_clear-f200w_i2d.fits'\n", + " ]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b151dc2", + "metadata": {}, + "outputs": [], + "source": [ + "for fn in files:\n", + " uri = f\"mast:JWST/product/{fn}\"\n", + " result = Observations.download_file(uri, local_path=f'{data_dir}/{fn}', cache=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a8c0a4e", + "metadata": {}, + "outputs": [], + "source": [ + "imviz = Imviz()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72de03f4", + "metadata": {}, + "outputs": [], + "source": [ + "with warnings.catch_warnings():\n", + " warnings.simplefilter('ignore')\n", + " with imviz.batch_load():\n", + " for fn in files:\n", + " imviz.load_data(f'{data_dir}/{fn}', data_label=fn[:17])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f4b77fcb", + "metadata": {}, + "outputs": [], + "source": [ + "imviz.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fab70d10", + "metadata": {}, + "outputs": [], + "source": [ + "# Link by WCS\n", + "imviz.plugins['Links Control'].link_type = 'WCS'\n", + "imviz.plugins['Links Control'].wcs_use_affine = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e7d1198", + "metadata": {}, + "outputs": [], + "source": [ + "# Set color mode to easily distinguish between them\n", + "imviz.plugins['Plot Options'].image_color_mode = 'Monochromatic'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0fdd56dc", + "metadata": {}, + "outputs": [], + "source": [ + "# Home button\n", + "imviz.default_viewer.state.reset_limits()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a07e9de9", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}
- Pixel {{ pixel }}  Value {{ value }} + + Pixel {{ pixel }}   + Value {{ value }}
{{ world_label_prefix }} {{ world_ra }} {{ world_dec }} {{ world_label_icrs }}
{{ unreliable_world ? '(est.)' : '' }} {{ world_ra_deg }} {{ world_dec_deg }} {{ world_label_deg }}