Skip to content

Commit

Permalink
Backport PR spacetelescope#1908: set bounding box to None to avoid la…
Browse files Browse the repository at this point in the history
…yer cropping

* 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>
  • Loading branch information
kecnry and pllim committed Dec 20, 2022
1 parent 20e330b commit acf7809
Show file tree
Hide file tree
Showing 11 changed files with 407 additions and 17 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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
^^^^^^

Expand Down
32 changes: 32 additions & 0 deletions docs/imviz/displayimages.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
====

Expand Down
10 changes: 8 additions & 2 deletions jdaviz/configs/imviz/plugins/coords_info/coords_info.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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'
Expand All @@ -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]
Expand All @@ -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
11 changes: 6 additions & 5 deletions jdaviz/configs/imviz/plugins/coords_info/coords_info.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@
<div style="display: inline-block; white-space: nowrap; line-height: 14pt; margin: 0; position: absolute; margin-left: 26px; top: 50%; transform: translateY(-50%); -ms-transform: translateY(-50%);">
<table>
<tr>
<td colspan="4">
<b v-if="pixel">Pixel </b>{{ pixel }}&nbsp;&nbsp;<b v-if="value">Value </b>{{ value }}
<td colspan="4" :style="unreliable_pixel ? 'color: #B8B8B8' : ''">
<b v-if="pixel">Pixel </b>{{ pixel }}&nbsp;&nbsp;
<b v-if="value">Value </b>{{ value }}
</td>
</tr>
<tr>
<tr :style="unreliable_world ? 'color: #B8B8B8' : ''">
<td width="42"><b>{{ world_label_prefix }}</b></td>
<td width="115">{{ world_ra }}</td>
<td width="120">{{ world_dec }}</td>
<td>{{ world_label_icrs }}</td>
</tr>
<tr>
<td width="42"></td>
<tr :style="unreliable_world ? 'color: #B8B8B8' : ''">
<td width="42">{{ unreliable_world ? '(est.)' : '' }}</td>
<td width="115">{{ world_ra_deg }}</td>
<td width="120">{{ world_dec_deg }}</td>
<td>{{ world_label_deg }}</td>
Expand Down
10 changes: 10 additions & 0 deletions jdaviz/configs/imviz/plugins/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion jdaviz/configs/imviz/plugins/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))


Expand Down
32 changes: 25 additions & 7 deletions jdaviz/configs/imviz/plugins/viewers.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,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:
Expand All @@ -114,7 +114,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()

Expand Down Expand Up @@ -154,7 +156,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
Expand Down Expand Up @@ -226,23 +228,37 @@ def _get_real_xy(self, image, x, y):
When `True`, it sets the coords, otherwise it resets.
"""
# 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':
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: # 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
Expand Down Expand Up @@ -318,10 +334,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'

Expand Down
88 changes: 87 additions & 1 deletion jdaviz/configs/imviz/tests/test_linking.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -212,20 +212,106 @@ 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}})
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.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}})
assert self.viewer.label_mouseover.pixel == 'x=02.7 y=09.8'
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):
Expand Down
Loading

0 comments on commit acf7809

Please sign in to comment.