Skip to content

Commit

Permalink
Merge pull request #744 from pllim/imviz-zoom-not-the-meeting
Browse files Browse the repository at this point in the history
Imviz: Add zoom and zoom_level API
  • Loading branch information
rosteen authored Aug 2, 2021
2 parents bf6d52b + d3c7f37 commit fbccc86
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 38 deletions.
74 changes: 74 additions & 0 deletions jdaviz/configs/imviz/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,80 @@ def offset_by(self, dx, dy):
viewer.state.x_max = viewer.state.x_min + width
viewer.state.y_max = viewer.state.y_min + height

@property
def zoom_level(self):
"""Zoom level:
* 1 means real-pixel-size.
* 2 means zoomed in by a factor of 2.
* 0.5 means zoomed out by a factor of 2.
* 'fit' means zoomed to fit the whole image width into display.
"""
viewer = self.app.get_viewer("viewer-1")
if viewer.shape is None: # pragma: no cover
raise ValueError('Viewer is still loading, try again later')

screenx = viewer.shape[1]
screeny = viewer.shape[0]
zoom_x = screenx / (viewer.state.x_max - viewer.state.x_min)
zoom_y = screeny / (viewer.state.y_max - viewer.state.y_min)

return max(zoom_x, zoom_y) # Similar to Ginga get_scale()

# Loosely based on glue/viewers/image/state.py
@zoom_level.setter
def zoom_level(self, val):
if ((not isinstance(val, (int, float)) and val != 'fit') or
(isinstance(val, (int, float)) and val <= 0)):
raise ValueError(f'Unsupported zoom level: {val}')

viewer = self.app.get_viewer("viewer-1")
image = viewer.state.reference_data
if (image is None or viewer.shape is None or
viewer.state.x_att is None or viewer.state.y_att is None): # pragma: no cover
return

# Zoom on X and Y will auto-adjust.
if val == 'fit':
# Similar to ImageViewerState.reset_limits() in Glue.
new_x_min = 0
new_x_max = image.shape[viewer.state.x_att.axis]
else:
cur_xcen = (viewer.state.x_min + viewer.state.x_max) * 0.5
new_dx = viewer.shape[1] * 0.5 / val
new_x_min = cur_xcen - new_dx
new_x_max = cur_xcen + new_dx

with delay_callback(viewer.state, 'x_min', 'x_max'):
viewer.state.x_min = new_x_min - 0.5
viewer.state.x_max = new_x_max - 0.5

# We need to adjust the limits in here to avoid triggering all
# the update events then changing the limits again.
viewer.state._adjust_limits_aspect()

# Discussion on why we need two different ways to set zoom at
# https://github.com/astropy/astrowidgets/issues/144
def zoom(self, val):
"""Zoom in or out by the given factor.
Parameters
----------
val : int or float
The zoom level to zoom the image.
See `zoom_level`.
Raises
------
ValueError
Invalid zoom factor.
"""
if not isinstance(val, (int, float)):
raise ValueError(f"zoom only accepts int or float but got '{val}'")
self.zoom_level = self.zoom_level * val

@property
def marker(self):
"""Marker to use.
Expand Down
53 changes: 53 additions & 0 deletions jdaviz/configs/imviz/tests/test_astrowidgets_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,59 @@ def test_center_offset_sky(self):
self.imviz.offset_by(dsky, dsky)


class TestZoom(BaseImviz_WCS_NoWCS):

@pytest.mark.parametrize('val', (0, -0.1, 'foo', [1, 2]))
def test_invalid_zoom_level(self, val):
with pytest.raises(ValueError, match='Unsupported zoom level'):
self.imviz.zoom_level = val

def test_invalid_zoom(self):
with pytest.raises(ValueError, match='zoom only accepts int or float'):
self.imviz.zoom('fit')

def assert_zoom_results(self, zoom_level, x_min, x_max, y_min, y_max, dpix):
assert_allclose(self.imviz.zoom_level, zoom_level)
assert_allclose((self.viewer.state.x_min, self.viewer.state.x_max,
self.viewer.state.y_min, self.viewer.state.y_max),
(x_min + dpix, x_max + dpix,
y_min + dpix, y_max + dpix))

@pytest.mark.parametrize('is_offcenter', (False, True))
def test_zoom(self, is_offcenter):
if is_offcenter:
self.imviz.center_on((0, 0))
dpix = -4.5
else:
self.imviz.center_on((4.5, 4.5))
dpix = 0

self.assert_zoom_results(10, -0.5, 9.5, -0.5, 9.5, dpix)

# NOTE: Not sure why X/Y min/max not exactly the same as aspect ratio 1
self.imviz.zoom_level = 1
self.assert_zoom_results(1, -46, 54, -45.5, 54.5, dpix)

self.imviz.zoom_level = 2
self.assert_zoom_results(2, -21.5, 28.5, -20.5, 29.5, dpix)

self.imviz.zoom(2)
self.assert_zoom_results(4, -9.5, 15.5, -8.0, 17.0, dpix)

self.imviz.zoom(0.5)
self.assert_zoom_results(2, -22.5, 27.5, -20.5, 29.5, dpix)

self.imviz.zoom_level = 0.5
self.assert_zoom_results(0.5, -98, 102, -95.5, 104.5, dpix)

# This fits the whole image on screen, regardless.
# NOTE: But somehow Y min/max auto-adjust does not work properly
# in the unit test when off-center. Works in notebook though.
if not is_offcenter:
self.imviz.zoom_level = 'fit'
self.assert_zoom_results(10, -0.5, 9.5, -0.5, 9.5, 0)


class TestMarkers(BaseImviz_WCS_NoWCS):

def test_invalid_markers(self):
Expand Down
80 changes: 42 additions & 38 deletions jdaviz/configs/imviz/tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,42 @@
import numpy as np
import pytest
from astropy.io import fits
from astropy.wcs import WCS

__all__ = ['BaseImviz_WCS_NoWCS']


class BaseImviz_WCS_NoWCS:
@pytest.fixture(autouse=True)
def setup_class(self, imviz_app):
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')
import numpy as np
import pytest
from astropy.io import fits
from astropy.wcs import WCS

__all__ = ['BaseImviz_WCS_NoWCS']


class BaseImviz_WCS_NoWCS:
@pytest.fixture(autouse=True)
def setup_class(self, imviz_app):
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')

# Since we are not really displaying, need this to test zoom.
self.viewer.shape = (100, 100)
self.viewer.state._set_axes_aspect_ratio(1)
48 changes: 48 additions & 0 deletions notebooks/ImvizExample.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,54 @@
"imviz.offset_by(0.5 * u.arcsec, -1.5 * u.arcsec)"
]
},
{
"cell_type": "markdown",
"id": "e2744c0c-8721-480a-811e-87904373e66f",
"metadata": {},
"source": [
"You can programmatically zoom in and out.\n",
"\n",
"Zoom level:\n",
"\n",
"* 1 means real-pixel-size.\n",
"* 2 means zoomed in by a factor of 2.\n",
"* 0.5 means zoomed out by a factor of 2.\n",
"* 'fit' means zoomed to fit the whole image width into display."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1bad9bdf-b959-4b74-9a77-07212095c2ce",
"metadata": {},
"outputs": [],
"source": [
"# Get the current zoom level.\n",
"imviz.zoom_level"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7a834bcf-7d5a-492b-bc54-740e1500a2f4",
"metadata": {},
"outputs": [],
"source": [
"# Set the zoom level directly.\n",
"imviz.zoom_level = 1"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "32288b81-1817-4549-b2e4-5e7f55b0ee3d",
"metadata": {},
"outputs": [],
"source": [
"# Set the relative zoom based on current zoom level.\n",
"imviz.zoom(2)"
]
},
{
"cell_type": "markdown",
"id": "11fab067-7428-4ce4-bc9b-f5462fe52e2a",
Expand Down

0 comments on commit fbccc86

Please sign in to comment.