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

expose zoom_to_selected in catalogs plugin api, fix default zoom level #3369

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ Imviz
- Catalog Search plugin now exposes a maximum sources limit for all catalogs and resolves an edge case
when loading a catalog from a file that only contains one source. [#3337]

- Catalog Search plugin ``zoom_to_selected`` is now in the public API. The default
zoom level changed from a fixed 50 pixels to a zoom window that is a fraction of
the image size (default 2%) to address and issue with zooming when using a small
image or WCS linked. [#3369]

Mosviz
^^^^^^

Expand Down
59 changes: 50 additions & 9 deletions jdaviz/configs/imviz/plugins/catalogs/catalogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class Catalogs(PluginTemplateMixin, ViewerSelectMixin, HasFileImportSelect, Tabl
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.show`
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.open_in_tray`
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.close_in_tray`
* :meth:`zoom_to_selected`
"""
template_file = __file__, "catalogs.vue"
uses_active_status = Bool(True).tag(sync=True)
Expand All @@ -49,7 +50,8 @@ class Catalogs(PluginTemplateMixin, ViewerSelectMixin, HasFileImportSelect, Tabl

@property
def user_api(self):
return PluginUserApi(self, expose=('clear_table', 'export_table',))
return PluginUserApi(self, expose=('clear_table', 'export_table',
'zoom_to_selected'))

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -324,23 +326,62 @@ def plot_selected_points(self):
getattr(y, 'value', y))

def vue_zoom_in(self, *args, **kwargs):
"""This function will zoom into the image based on the selected points"""

self.zoom_to_selected()

def zoom_to_selected(self, padding=0.02, return_bounding_box=False):
"""
Zoom on the default viewer to a region containing the currently selected
points in the catalog.

Parameters
----------
padding : float, optional
A fractional value representing the padding around the bounding box
of the selected points. It is applied as a proportion of the largest
dimension of the current extent of loaded data. Defaults to 0.02.
return_bounding_box : bool, optional
If True, returns the bounding box of the zoomed region as
((x_min, x_max), (y_min, y_max)). Defaults to False.
cshanahan1 marked this conversation as resolved.
Show resolved Hide resolved

Returns
-------
list of float or None
If there are activley selected rows, and ``return_bounding_box`` is
True, returns a list containing the bounding
box coordinates: [x_min, x_max, y_min, y_max].
Otherwise, returns None.

"""

viewer = self.app._jdaviz_helper._default_viewer
cshanahan1 marked this conversation as resolved.
Show resolved Hide resolved

selected_rows = self.table.selected_rows

if not len(selected_rows):
return
cshanahan1 marked this conversation as resolved.
Show resolved Hide resolved

if padding <= 0 or padding > 1:
raise ValueError("`padding` must be between 0 and 1.")

x = [float(coord['x_coord']) for coord in selected_rows]
y = [float(coord['y_coord']) for coord in selected_rows]

limits = viewer.state._get_reset_limits()
Copy link
Contributor

@gibsongreen gibsongreen Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the viewer and the image have different dimensions?

I was testing the default Imviz notebook and one case I noticed was the viewer limits were dependent on my browser instance and how I defined the width but the image keeps the same aspect ratio in the viewer when I resize the browser.

Screen.Recording.2024-12-20.at.4.31.48.PM.mov

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm honestly not sure what the answer to this is

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can reproduce where this case causes and issue with the zoom with the following:

catalog = imviz.plugins['Catalog Search']._obj
catalog.open_in_tray()
catalog.catalog.selected = 'Gaia'
catalog.search()

catalog.table.selected_rows = catalog.table.items[:2]
catalog.zoom_to_selected()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the concern just about reproducibility? Or is it somehow failing to zoom to include all selected sources?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get the limits from the selected_rows instead of the viewer?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you see what is being used as padding? And the output of viewer.state._get_reset_limits() (maybe this is a bug there)? My other idea is that this is just an issue with providing x/y limits as opposed to center and radius, since the rectangle of limits we pass is not exactly what ends up adopted.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd still say this is an improvement over the previous behavior and maybe we get it in as is so that its in for the demos and make a tech-debt ticket to make more general in the future? If we're worried, we can hide padding from the user API until then.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok i was able to reproduce the issue, very weird.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say this is still an improvement, because the same zoom scenario on main in the example doesn't work at all and the image is totally zoomed out. This is at least in the right area of the image now when it fails after resizing your browser window. We should look into what is happening though.

Copy link
Contributor

@gibsongreen gibsongreen Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After each step inside zoom_to_selected:
limits:
(-0.5, 4733.5, -0.5, 4718.5)
max_dim:
4734.0
padding:
94.68
before applying padding (x_min, x_max) (y_min, y_max)
2202.56701 2589.79442
2243.31321 2716.02305
after applying padding (x_min, x_max) (y_min, y_max)
2107.8870100000004 2684.47442
2148.63321 2810.7030499999996

After Zooming:

viewer.get_limits()
(2156.4823647015282, 2635.8790652984726, 2365.1348456839964, 2594.2014143160036)

viewer.state._get_reset_limits()
(-0.5, 4733.5, -0.5, 4718.5)

(x_1, x_2) (y_1, y_2) of sources:
(2202.56701,2589.79442) (2243.31321, 2716.02305)

max_dim = max((limits[1] - limits[0]), (limits[3] - limits[2]))
padding = max_dim * padding

# this works with single selected points
# zooming when the range is too large is not performing correctly
x_min = min(x) - 50
x_max = max(x) + 50
y_min = min(y) - 50
y_max = max(y) + 50
x_min = min(x) - padding
cshanahan1 marked this conversation as resolved.
Show resolved Hide resolved
x_max = max(x) + padding
y_min = min(y) - padding
y_max = max(y) + padding

self.app._jdaviz_helper._default_viewer.set_limits(
x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max)
viewer.set_limits(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max)

return (x_min, x_max), (y_min, y_max)
if return_bounding_box:
pllim marked this conversation as resolved.
Show resolved Hide resolved
return [x_min, x_max, y_min, y_max]

def import_catalog(self, catalog):
"""
Expand Down
91 changes: 84 additions & 7 deletions jdaviz/configs/imviz/tests/test_catalogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@
'''

import numpy as np
from numpy.testing import assert_allclose
import pytest

from astropy.coordinates import SkyCoord
from astropy.io import fits
from astropy.nddata import NDData
from astropy.coordinates import SkyCoord
from astropy.table import Table, QTable


Expand Down Expand Up @@ -157,7 +158,9 @@ def test_plugin_image_with_result(self, imviz_helper, tmp_path):
assert imviz_helper.viewers['imviz-0']._obj.state.y_min == -0.5
assert imviz_helper.viewers['imviz-0']._obj.state.y_max == 1488.5

catalogs_plugin.vue_zoom_in()
# set 'padding' to reproduce original hard-coded 50 pixel window
# so test results don't change
catalogs_plugin.zoom_to_selected(padding=50 / 2048)
pllim marked this conversation as resolved.
Show resolved Hide resolved

assert imviz_helper.viewers['imviz-0']._obj.state.x_min == 858.24969
assert imviz_helper.viewers['imviz-0']._obj.state.x_max == 958.38461
Expand Down Expand Up @@ -251,9 +254,83 @@ def test_offline_ecsv_catalog(imviz_helper, image_2d_wcs, tmp_path):
assert imviz_helper.viewers['imviz-0']._obj.state.y_min == -0.5
assert imviz_helper.viewers['imviz-0']._obj.state.y_max == 9.5

catalogs_plugin.vue_zoom_in()
# test the zooming using the default 'padding' of 2% of the viewer size
# around selected points
catalogs_plugin.zoom_to_selected()
assert imviz_helper.viewers['imviz-0']._obj.state.x_min == -0.19966
assert imviz_helper.viewers['imviz-0']._obj.state.x_max == 0.20034000000000002
assert imviz_helper.viewers['imviz-0']._obj.state.y_min == 0.8000100000000001
assert imviz_helper.viewers['imviz-0']._obj.state.y_max == 1.20001


def test_zoom_to_selected(imviz_helper, image_2d_wcs, tmp_path):

arr = np.ones((500, 500))
ndd = NDData(arr, wcs=image_2d_wcs)
imviz_helper.load_data(ndd)

# write out catalog to file so we can read it back in
# todo: if tables can be loaded directly at some point, do that

# sources at pixel coords ~(100, 100), ~(200, 200)
sky_coord = SkyCoord(ra=[337.49056532, 337.46086081],
dec=[-20.80555273, -20.7777673], unit='deg')
tbl = Table({'sky_centroid': [sky_coord],
'label': ['Source_1', 'Source_2']})
tbl_file = str(tmp_path / 'test_catalog.ecsv')
tbl.write(tbl_file, overwrite=True)

catalogs_plugin = imviz_helper.plugins['Catalog Search']

catalogs_plugin._obj.from_file = tbl_file

catalogs_plugin._obj.search()

# select both sources
catalogs_plugin._obj.table.selected_rows = catalogs_plugin._obj.table.items

# check viewer limits before zoom
xmin, xmax, ymin, ymax = imviz_helper.app._jdaviz_helper._default_viewer.get_limits()
assert xmin == ymin == -0.5
assert xmax == ymax == 499.5

# zoom to selected sources
catalogs_plugin.zoom_to_selected()

# make sure the viewer bounds reflect the zoom, which, in pixel coords,
# should be centered at roughly pixel coords (150, 150)
xmin, xmax, ymin, ymax = imviz_helper.app._jdaviz_helper._default_viewer.get_limits()

assert_allclose((xmin + xmax) / 2, 150., atol=0.1)
assert_allclose((ymin + ymax) / 2, 150., atol=0.1)

# and the zoom box size should reflect the default padding of 2% of the image
# size around the bounding box containing the source(s), which in this case is
# 10 pixels around
assert_allclose(xmin, 100 - 10, atol=0.1) # min x of selected sources minus pad
assert_allclose(xmax, 200 + 10, atol=0.1) # max x of selected sources plus pad
assert_allclose(ymin, 100 - 10, atol=0.1) # min y of selected sources minus pad
assert_allclose(ymax, 200 + 10, atol=0.1) # max y of selected sources plus pad

# select one source now
catalogs_plugin._obj.table.selected_rows = catalogs_plugin._obj.table.items[0:1]

# zoom to single selected source, using a new value for 'padding'
catalogs_plugin.zoom_to_selected(padding=0.05)

# check that zoom window is centered correctly on the source at 100, 100
xmin, xmax, ymin, ymax = imviz_helper.app._jdaviz_helper._default_viewer.get_limits()
assert_allclose((xmin + xmax) / 2, 100., atol=0.1)
assert_allclose((ymin + ymax) / 2, 100., atol=0.1)

# and the zoom box size should reflect the default padding of 5% of the image
# size around the bounding box containing the source(s), which in this case is
# 25 pixels around
assert_allclose(xmin, 100 - 25, atol=0.1) # min x of selected source minus pad
assert_allclose(xmax, 100 + 25, atol=0.1) # max x of selected source plus pad
assert_allclose(ymin, 100 - 25, atol=0.1) # min y of selected source minus pad
assert_allclose(ymax, 100 + 25, atol=0.1) # max y of selected source plus pad

assert imviz_helper.viewers['imviz-0']._obj.state.x_min == -49.99966
assert imviz_helper.viewers['imviz-0']._obj.state.x_max == 50.00034
assert imviz_helper.viewers['imviz-0']._obj.state.y_min == -48.99999
assert imviz_helper.viewers['imviz-0']._obj.state.y_max == 51.00001
# test that appropriate error is raised when padding is not a valud percentage
with pytest.raises(ValueError, match="`padding` must be between 0 and 1."):
catalogs_plugin.zoom_to_selected(padding=5)
Loading