Skip to content

Commit 34a0536

Browse files
committed
initial implementation of astro image display api (AIDA)
1 parent 1e9288d commit 34a0536

File tree

4 files changed

+318
-0
lines changed

4 files changed

+318
-0
lines changed

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ New Features
3333

3434
- Allow custom resolutions when exporting viewers to png or mp4. [#3478]
3535

36+
- Add ``aid`` attribute to image viewers for compatibility with the Astro Image Display API. [#3622]
3637

3738
Cubeviz
3839
^^^^^^^

jdaviz/configs/default/aida.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import astropy.units as u
2+
from astropy.coordinates import SkyCoord, Angle
3+
from astropy.wcs import WCS
4+
from gwcs.wcs import WCS as GWCS
5+
6+
from jdaviz.utils import data_has_valid_wcs, get_top_layer_index
7+
8+
9+
class AID:
10+
"""
11+
Common API methods for image viewers in astronomy, called
12+
the Astro Image Display API (AIDA)[1]_.
13+
14+
References
15+
----------
16+
.. [1] https://github.com/astropy/astro-image-display-api/
17+
18+
"""
19+
20+
def __init__(self, viewer):
21+
self.viewer = viewer
22+
self.app = viewer.jdaviz_app
23+
24+
def _get_image_glue_data(self, image_label):
25+
if image_label is None:
26+
i_top = get_top_layer_index(self.viewer)
27+
image = self.viewer.layers[i_top].layer
28+
29+
else:
30+
for lyr in self.viewer.layers:
31+
image = lyr.layer
32+
if image.label == image_label:
33+
break
34+
else:
35+
raise ValueError(f"No data with data_label {image_label}` found in viewer.")
36+
37+
return image, image.label
38+
39+
def set_viewport(self, center=None, fov=None, image_label=None, **kwargs):
40+
"""
41+
Parameters
42+
----------
43+
center : `~astropy.coordinates.SkyCoord` or tuple of floats
44+
Center the viewer on this coordinate.
45+
46+
fov : `~astropy.units.Quantity` or tuple of floats
47+
Set the width of the viewport to span `field_of_view`.
48+
49+
image_label : str
50+
Set the viewport with respect to the image
51+
with the data label: ``image_label``.
52+
"""
53+
image, image_label = self._get_image_glue_data(image_label)
54+
55+
if center is None:
56+
# get the current center in the pixel coords on reference data
57+
x_min, x_max, y_min, y_max = self.viewer.get_limits()
58+
center_x = 0.5 * (x_min + x_max)
59+
center_y = 0.5 * (y_min + y_max)
60+
center = (center_x, center_y)
61+
62+
if isinstance(center, SkyCoord):
63+
reference_wcs = self.viewer.state.reference_data.coords
64+
65+
if isinstance(reference_wcs, GWCS):
66+
reference_wcs = WCS(reference_wcs.to_fits_sip())
67+
68+
reference_center_pix = reference_wcs.world_to_pixel(center)
69+
70+
elif hasattr(center, '__len__') and isinstance(center[0], (float, int)):
71+
reference_center_pix = center
72+
73+
current_width = self.viewer.state.x_max - self.viewer.state.x_min
74+
current_height = self.viewer.state.y_max - self.viewer.state.y_min
75+
76+
if fov is None:
77+
new_width = current_width
78+
new_height = current_height
79+
else:
80+
if isinstance(fov, (u.Quantity, Angle)):
81+
current_fov = self._get_current_fov('sky')
82+
scale_factor = float(fov / current_fov)
83+
84+
elif isinstance(fov, (float, int)):
85+
current_fov = self._get_current_fov('pixel')
86+
scale_factor = float(fov / current_fov)
87+
88+
new_width = current_width * scale_factor
89+
new_height = current_height * scale_factor
90+
91+
new_xmin = reference_center_pix[0] - (new_width * 0.5)
92+
new_ymin = reference_center_pix[1] - (new_height * 0.5)
93+
94+
self.viewer.set_limits(
95+
x_min=new_xmin,
96+
x_max=new_xmin + new_width,
97+
y_min=new_ymin,
98+
y_max=new_ymin + new_height
99+
)
100+
101+
def _get_current_fov(self, sky_or_pixel):
102+
x_min, x_max, y_min, y_max = self.viewer.get_limits()
103+
104+
if sky_or_pixel == 'sky':
105+
wcs = self.viewer.state.reference_data.coords
106+
107+
# for now, convert GWCS to FITS SIP so pixel to world
108+
# transformations can be done outside of the bounding box
109+
if isinstance(wcs, GWCS):
110+
wcs = WCS(wcs.to_fits_sip())
111+
112+
lower_left, lower_right, upper_left = wcs.pixel_to_world(
113+
[x_min, y_min, x_min], [x_max, y_min, y_max]
114+
)
115+
116+
return lower_left.separation(lower_right)
117+
else:
118+
return x_max - x_min
119+
120+
def get_viewport(self, sky_or_pixel=None, image_label=None, **kwargs):
121+
"""
122+
sky_or_pixel : str, optional
123+
If 'sky', the center will be returned as a `SkyCoord` object.
124+
If 'pixel', the center will be returned as a tuple of pixel coordinates.
125+
If `None`, the default behavior is to return the center as a `SkyCoord` if
126+
possible, or as a tuple of floats if the image is in pixel coordinates and has
127+
no WCS information.
128+
129+
image_label : str, optional
130+
The label of the image to get the viewport for. If not given and there is only one
131+
image loaded, the viewport for that image is returned. If there are multiple images
132+
and no label is provided, an error is raised.
133+
134+
Returns
135+
-------
136+
dict
137+
A dictionary containing the current viewport settings.
138+
The keys are 'center', 'fov', and 'image_label'.
139+
- 'center' is an `astropy.coordinates.SkyCoord` object or a tuple of floats.
140+
- 'fov' is an `astropy.units.Quantity` object or a float.
141+
- 'image_label' is a string representing the label of the image.
142+
"""
143+
# viewer_aligned_by_wcs = self.app._align_by == 'wcs'
144+
145+
image, image_label = self._get_image_glue_data(image_label)
146+
reference_data = self.viewer.state.reference_data
147+
reference_wcs = reference_data.coords
148+
149+
x_min, x_max, y_min, y_max = self.viewer.get_limits()
150+
center_x = 0.5 * (x_min + x_max)
151+
center_y = 0.5 * (y_min + y_max)
152+
153+
# default to 'sky' if sky/pixel not specified and WCS is available:
154+
if sky_or_pixel is None and data_has_valid_wcs(image):
155+
sky_or_pixel = 'sky'
156+
157+
# if the image data have WCS, get the center sky coordinate:
158+
if sky_or_pixel == 'sky':
159+
if self.app._align_by == 'wcs':
160+
center = self.viewer._get_center_skycoord()
161+
else:
162+
center = reference_wcs.pixel_to_world(center_x, center_y)
163+
164+
fov = self._get_current_fov(sky_or_pixel)
165+
166+
else:
167+
center = (center_x, center_y)
168+
fov = x_max - x_min
169+
170+
return dict(center=center, fov=fov, image_label=image_label)

jdaviz/configs/imviz/plugins/viewers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from glue_jupyter.bqplot.image import BqplotImageView
88

99
from jdaviz.configs.imviz import wcs_utils
10+
from jdaviz.configs.default import aida
1011
from jdaviz.core.astrowidgets_api import AstrowidgetsImageViewerMixin
1112
from jdaviz.core.events import SnackbarMessage
1213
from jdaviz.core.registries import viewer_registry
@@ -66,6 +67,7 @@ def __init__(self, *args, **kwargs):
6667
self.state.image_external_padding = 0.5
6768

6869
self.data_menu._obj.dataset.add_filter('is_image')
70+
self.aid = aida.AID(self)
6971

7072
def on_mouse_or_key_event(self, data):
7173
active_image_layer = self.active_image_layer

notebooks/concepts/aida.ipynb

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "5bd4b7fa-d7ed-48e8-92bc-442ed336ec23",
6+
"metadata": {},
7+
"source": [
8+
"# Shared API for viewer orientation\n",
9+
"\n",
10+
"#### Load observations of the Cartwheel Galaxy into `imviz`:"
11+
]
12+
},
13+
{
14+
"cell_type": "code",
15+
"execution_count": null,
16+
"id": "ad82c2ef-be3a-4316-a82d-c11ede32064d",
17+
"metadata": {},
18+
"outputs": [],
19+
"source": [
20+
"from jdaviz import Imviz\n",
21+
"\n",
22+
"viz = Imviz()\n",
23+
"viz.load_data('../../jdaviz/notebooks/jw02727-o002_t062_nircam_clear-f277w_i2d.fits')\n",
24+
"viz.show(height=400)\n",
25+
"\n",
26+
"imviz_viewer = viz.app.get_viewer(\"imviz-0\")\n",
27+
"orientation = viz.plugins['Orientation']\n",
28+
"\n",
29+
"orientation.align_by = 'WCS'\n",
30+
"orientation.set_north_up_east_left()\n",
31+
"viz.app.state.drawer_content = '' # close plugin tray"
32+
]
33+
},
34+
{
35+
"cell_type": "markdown",
36+
"id": "67c884e0-d75d-40a2-9748-0286a30571d9",
37+
"metadata": {},
38+
"source": [
39+
"#### Load observations of the Cartwheel Galaxy into `mast-aladin-lite`:"
40+
]
41+
},
42+
{
43+
"cell_type": "code",
44+
"execution_count": null,
45+
"id": "80c845ff-9a29-4e7d-bf78-8d83ffa3e020",
46+
"metadata": {},
47+
"outputs": [],
48+
"source": [
49+
"from mast_aladin_lite import MastAladin\n",
50+
"\n",
51+
"mast_aladin = MastAladin(\n",
52+
" target='Cartwheel Galaxy',\n",
53+
" fov=0.13, # [deg]\n",
54+
" height=400 # [pix]\n",
55+
")\n",
56+
"mast_aladin"
57+
]
58+
},
59+
{
60+
"cell_type": "markdown",
61+
"id": "f1c51a76-c93b-4711-90af-938e43f27ecf",
62+
"metadata": {},
63+
"source": [
64+
"#### get `mast-aladin-lite` viewport state:"
65+
]
66+
},
67+
{
68+
"cell_type": "code",
69+
"execution_count": null,
70+
"id": "8c331613-21bd-488d-ba61-e9bde81ef642",
71+
"metadata": {},
72+
"outputs": [],
73+
"source": [
74+
"mast_aladin.aid.get_viewport()"
75+
]
76+
},
77+
{
78+
"cell_type": "markdown",
79+
"id": "d3c92dc1-666d-4f89-92e6-7fe80fa2078c",
80+
"metadata": {},
81+
"source": [
82+
"#### get `imviz` viewport state:"
83+
]
84+
},
85+
{
86+
"cell_type": "code",
87+
"execution_count": null,
88+
"id": "07eb824b-bb8f-4b6e-ad93-5d16e2d6b500",
89+
"metadata": {},
90+
"outputs": [],
91+
"source": [
92+
"imviz_viewer.aid.get_viewport()"
93+
]
94+
},
95+
{
96+
"cell_type": "markdown",
97+
"id": "7def548c-82b3-44c0-9d1a-db6b72276301",
98+
"metadata": {},
99+
"source": [
100+
"#### set `imviz` viewport from the `mast-aladin-lite` viewport:"
101+
]
102+
},
103+
{
104+
"cell_type": "code",
105+
"execution_count": null,
106+
"id": "66926064-0364-4ac3-a9e8-b7896ee71b88",
107+
"metadata": {},
108+
"outputs": [],
109+
"source": [
110+
"imviz_viewer.aid.set_viewport(\n",
111+
" **mast_aladin.aid.get_viewport()\n",
112+
")"
113+
]
114+
},
115+
{
116+
"cell_type": "code",
117+
"execution_count": null,
118+
"id": "49667e6d-e75b-4531-b0a1-95e63b7da2d3",
119+
"metadata": {},
120+
"outputs": [],
121+
"source": []
122+
}
123+
],
124+
"metadata": {
125+
"kernelspec": {
126+
"display_name": "Python 3 (ipykernel)",
127+
"language": "python",
128+
"name": "python3"
129+
},
130+
"language_info": {
131+
"codemirror_mode": {
132+
"name": "ipython",
133+
"version": 3
134+
},
135+
"file_extension": ".py",
136+
"mimetype": "text/x-python",
137+
"name": "python",
138+
"nbconvert_exporter": "python",
139+
"pygments_lexer": "ipython3",
140+
"version": "3.12.11"
141+
}
142+
},
143+
"nbformat": 4,
144+
"nbformat_minor": 5
145+
}

0 commit comments

Comments
 (0)