-
Notifications
You must be signed in to change notification settings - Fork 74
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
Imviz: astrowidget API for center_on and offset_to #687
Changes from all commits
9ac687a
45c779d
edc3c9c
cb518b5
8a0497f
7039327
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,10 +2,20 @@ | |
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') | ||
|
||
|
||
class Imviz(ConfigHelper): | ||
"""Imviz Helper class""" | ||
|
@@ -79,6 +89,102 @@ def load_data(self, data, parser_reference=None, **kwargs): | |
self.app.load_data( | ||
data, parser_reference=parser_reference, **kwargs) | ||
|
||
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 | ||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as above |
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: This was added in astropy/astropy#11635 |
||
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. | ||
|
@@ -125,3 +231,12 @@ def split_filename_with_fits_ext(filename): | |
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] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import numpy as np | ||
import pytest | ||
from astropy import units as u | ||
from astropy.io import fits | ||
from astropy.wcs import WCS | ||
from numpy.testing import assert_allclose | ||
|
||
|
||
class TestAstrowidgetsAPI: | ||
@pytest.fixture(autouse=True) | ||
def setup_class(self, imviz_app): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: This is basically the same test setup in #630 . When we get back to the other PR, will have to see how to consolidate this to avoid code duplication. |
||
hdu = fits.ImageHDU(np.zeros((10, 10)), name='SCI') | ||
|
||
# Apply some celestial WCS from | ||
# https://learn.astropy.org/rst-tutorials/celestial_coords1.html | ||
hdu.header.update({'CTYPE1': 'RA---TAN', | ||
'CUNIT1': 'deg', | ||
'CDELT1': -0.0002777777778, | ||
'CRPIX1': 1, | ||
'CRVAL1': 337.5202808, | ||
'NAXIS1': 10, | ||
'CTYPE2': 'DEC--TAN', | ||
'CUNIT2': 'deg', | ||
'CDELT2': 0.0002777777778, | ||
'CRPIX2': 1, | ||
'CRVAL2': -20.833333059999998, | ||
'NAXIS2': 10}) | ||
|
||
# Data with WCS | ||
imviz_app.load_data(hdu, data_label='has_wcs') | ||
|
||
# Data without WCS | ||
imviz_app.load_data(hdu, data_label='no_wcs') | ||
imviz_app.app.data_collection[1].coords = None | ||
|
||
self.wcs = WCS(hdu.header) | ||
self.imviz = imviz_app | ||
self.viewer = imviz_app.app.get_viewer('viewer-1') | ||
|
||
def test_center_offset_pixel(self): | ||
self.imviz.center_on((0, 1)) | ||
assert_allclose(self.viewer.state.x_min, -5) | ||
assert_allclose(self.viewer.state.y_min, -4) | ||
assert_allclose(self.viewer.state.x_max, 5) | ||
assert_allclose(self.viewer.state.y_max, 6) | ||
|
||
self.imviz.offset_to(1, -1) | ||
assert_allclose(self.viewer.state.x_min, -4) | ||
assert_allclose(self.viewer.state.y_min, -5) | ||
assert_allclose(self.viewer.state.x_max, 6) | ||
assert_allclose(self.viewer.state.y_max, 5) | ||
|
||
def test_center_offset_sky(self): | ||
# Blink to the one with WCS because the last loaded data is shown. | ||
self.viewer.blink_once() | ||
|
||
sky = self.wcs.pixel_to_world(0, 1) | ||
self.imviz.center_on(sky) | ||
assert_allclose(self.viewer.state.x_min, -5) | ||
assert_allclose(self.viewer.state.y_min, -4) | ||
assert_allclose(self.viewer.state.x_max, 5) | ||
assert_allclose(self.viewer.state.y_max, 6) | ||
|
||
dsky = 0.1 * u.arcsec | ||
self.imviz.offset_to(dsky, dsky, skycoord_offset=True) | ||
assert_allclose(self.viewer.state.x_min, -5.100000000142565) | ||
assert_allclose(self.viewer.state.y_min, -3.90000000002971) | ||
assert_allclose(self.viewer.state.x_max, 4.899999999857435) | ||
assert_allclose(self.viewer.state.y_max, 6.09999999997029) | ||
|
||
# astropy requires Quantity | ||
with pytest.raises(u.UnitTypeError): | ||
self.imviz.offset_to(0.1, 0.1, skycoord_offset=True) | ||
|
||
# Cannot pass Quantity without specifying skycoord_offset=True | ||
with pytest.raises(u.UnitConversionError): | ||
self.imviz.offset_to(dsky, dsky) | ||
|
||
# Blink to the one without WCS | ||
self.viewer.blink_once() | ||
|
||
with pytest.raises(AttributeError, match='does not have a valid WCS'): | ||
self.imviz.center_on(sky) | ||
|
||
with pytest.raises(AttributeError, match='does not have a valid WCS'): | ||
self.imviz.offset_to(dsky, dsky, skycoord_offset=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should probably use image = viewer.reference_data as that is the one which is used to define the pixel space
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your suggestion does not seem to work:
AttributeError: 'ImvizImageView' object has no attribute 'reference_data'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
viewer.state.reference_data
exists but it causes my test to fail:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It appears
reference_data
does not keep track of which layer is actually visible to the user after blinking.