Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

set bounding box to None to avoid layer cropping #1908

Merged
merged 15 commits into from
Dec 20, 2022
Merged
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
^^^^^^

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.
kecnry marked this conversation as resolved.
Show resolved Hide resolved

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
Original file line number Diff line number Diff line change
Expand Up @@ -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})')
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):
kecnry marked this conversation as resolved.
Show resolved Hide resolved
# 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
pllim marked this conversation as resolved.
Show resolved Hide resolved
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
41 changes: 33 additions & 8 deletions jdaviz/configs/imviz/plugins/viewers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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()

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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(
pllim marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down Expand Up @@ -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:
kecnry marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -237,20 +237,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