-
Notifications
You must be signed in to change notification settings - Fork 0
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
Fix matched pan zoom #29
Conversation
if wcs_layer not in self.app.get_viewer_by_id(viewer_ref).state.wcs_only_layers: | ||
self.app.add_data_to_viewer(viewer_ref, wcs_layer) | ||
|
||
self.viewer.selected = viewer_ref |
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.
this will actually cycle the viewer select in the orientation plugin, right? Is that for the sake of having the choices update in self.orientation
? Why do we need to do that now for all viewers_to_update
?
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.
We need that because we call self.orientation.update_wcs_only_filter
below, which is particular to each viewer.
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.
but self.orientation
won't remember per-viewer filters, so setting for anything but the last iteration of the for-loop will just be wasted effort... I think
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.
The one time that this is necessary is when a new viewer is created and the WCS-only reference data have been added to the viewer but not yet selected as reference data. We first need to change the filter to "see" the orientation options, then we need to select the default orientation – I do this a few lines below (https://github.com/bmorris3/jdaviz/pull/29/files/e2f36a547e94cb0d38e45b807cb7dfba38c85122#diff-022bcba39f3fd68183229b5433146c2a21ac800db2778790152edc9a6b1b01caR398).
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.
ok, and in that case viewers_to_update
is passed and only has one entry it seems. I wonder if instead of having
self.viewer.selected = viewer_ref
...
self.orientation.selected = base_wcs_layer_label
in the for-loop and run for all (passed) viewers, we should instead have
if self.viewer.selected == viewer:
....
self.orientation.selected = base_wcs_layer_label
Does that make sense?
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 understand what you're going for, but this method is also used when new orientation layers are created. In that case, it adds the new orientation layer to each viewer, so that it can be selected as refdata in that viewer. So the function is written for the general case.
@@ -2254,7 +2255,7 @@ def _create_viewer_item(self, viewer, vid=None, name=None, reference=None): | |||
'config': self.config, # give viewer access to app config/layout | |||
'data_open': False, | |||
'collapse': True, | |||
'reference': reference, | |||
'reference': reference or name or vid, |
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.
why was this needed (specifically for this PR - I agree we should try to merge these eventually, I just worry this might add more confusion since now we'll have "reference" in the viewer item that isn't necessarily the same as "reference" on the viewer itself).
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.
This is needed because when a new image viewer is created, there are hooks in place to set vid
and name
, but not reference
(https://github.com/spacetelescope/jdaviz/blob/6372879516cdb96081e90b4b5ced404dc66448fe/jdaviz/configs/imviz/helper.py#L54C28-L56), but the reference
is the version used in some places.
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.
Technical review as follows. Thanks!
jdaviz/app.py
Outdated
@@ -2319,6 +2320,12 @@ def _on_new_viewer(self, msg, vid=None, name=None, add_layers_to_viewer=False): | |||
ref_data = self._jdaviz_helper.default_viewer._obj.state.reference_data | |||
new_viewer_item['reference_data_label'] = getattr(ref_data, 'label', None) | |||
|
|||
if ( | |||
getattr(ref_data, 'meta', {}).get('_WCS_ONLY', False) and |
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.
Is it more consistent to use layer_is_wcs_only
function here that you added in spacetelescope#2179 in Imviz helper, or is there a reason to not do that here?
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 had avoided doing that only because of circular imports, originally. But I've pushed this commit which imports the function within the method and dodges the circularity.
@@ -51,7 +51,10 @@ def on_click(self, data): | |||
if x is None or y is None: # Out of bounds | |||
return | |||
x, y, _, _ = self.viewer._get_real_xy(image, x, y) | |||
self.viewer.center_on((x, y)) | |||
if image.coords is not None: | |||
self.viewer.center_on(image.coords.pixel_to_world(x, y)) |
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.
Why this logic? It kinda defeats the purpose of _get_real_xy
above.
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.
The use of _get_real_xy
above ensures that the behavior for data with WCS (in the else
) and without WCS (in the if
) is consistent. image
here is the first visible layer, which is not the reference data layer when linked by WCS. So this call tries to be explicit.
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.
If you want to switch the call to using sky coordinates, you don't need _get_real_xy
. You can theoretically pass (x, y)
directly to reference WCS because input (x, y)
is always w.r.t. 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.
So do you want to see line 53 moved inside the conditional?
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.
If this line makes the final cut, I think you can change the call to something like reference_data.coords.pixel_to_world(x, y)
without ever calling viewer._get_real_xy(image, ...)
.
But I am not sure what you need to do to fix #29 (comment)
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.
This line is already out of the final cut 👍🏻
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.
Why did the results change?
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.
These test results have changed because the matched pan/zoom approach for data with WCS is slightly different than before, producing limits different by one pixel. The main differences come from trying to match zoom approximately even when viewers have different 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.
I have to think about this:
When you give 'domain': {'x': 1, 'y': 1}
, that is where you are telling the reference data to go. In this case, v
has both images loaded, so second image is on top. Second image is offset by +1 pix in the WCS. So if the adjusted limits reflects second image limits, then I think this change makes sense.
But then on L63-L62, v2
should only have the first image, and both v
and v2
give the same limits. Then the assumption above breaks down. Let me run this example interactively and see what is actually going on.
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 still don't get this change. This test case is linked by pixel. Even though both images have WCS, it should not have mattered. No orientation layer was created.
With this PR, when I run this test case interactively, this centered the image at x=2.5 y=1.5
:
t_normpan = v.toolbar.tools['jdaviz:imagepanzoom']
t_normpan.activate()
t_normpan.on_click({'event': 'click', 'domain': {'x': 1, 'y': 1}})
And if I have the pan/zoom tool active, where a single click would center to the pixel under cursor, that pixel is not actually the center when "pan to click location" is done.
I do not think this change in test behavior is correct.
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.
On main
, the same test case does not have this x offset. Pixel under cursor is centered though I suspect there might be a 0.5 offset in y but hard to tell without built in grids on the viewer.
sequential_data = np.arange( | ||
np.prod(refdata_shape), dtype=np.int8 | ||
).reshape(refdata_shape) | ||
placeholder_data = np.nan * np.ones(refdata_shape) |
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.
What is the benefit of changing sequential to NaN? Kinda make it hard to debug when the fake layer accidentally shows up.
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.
This was discussed in an earlier PR that we merged, but then I botched a rebase, so I'm fixing that mistake in this PR. Since this was approved before, I'm just reinserting it.
There are two main benefits if the "WCS-only reference data" have no data:
- when a WCS-only layer is created, it is visible for a few milliseconds, before the visibility is set to
False
. If the data arenan
, they are never rendered, and the viewer does less crazy flashing during rotation. - If you somehow do math on the WCS-only layer's pixels, the result will never appear to be legitimate. This should not be easy to do, given the checks we have in place, but switching to
nan
makes it much easier to debug if we accidentally do operations on the WCS-only 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.
Yes, I think the benefit not having the weird flashing is good. Thanks for the clarification!
i_top = get_top_layer_index(self) | ||
image = self.layers[i_top].layer | ||
if self.state.reference_data is not None: | ||
image, i_ref = get_reference_image_data(self.jdaviz_app, self.reference) |
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 don't understand this change. Zoom should be based on the visible layer, not the hidden 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.
Zoom should be based on the visible layer
Why? If they're linked, is this distinction meaningful?
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.
Because I want the zoom level of the image under view, not zoom level of the reference data.
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
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.
Ah, no, it is completely wrong. I forgot to link by WCS when I commented 8 mins ago. When I link by WCS, the zoom_level is wrong. This change needs to be reverted.
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.
jdaviz/core/template_mixin.py
Outdated
`True` will filter only the WCS-only layers, `False` will | ||
give the non-WCS-only layers. | ||
""" | ||
def is_wcs_only(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.
I feel like this re-implemented layer_is_wcs_only
that you also implemented in Imviz helper.
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.
This needs to be a separate function that you can turn on and off as a filter
for LayerSelect
.
self.add_filter(is_wcs_only) | ||
|
||
@property | ||
def only_wcs_layers(self): |
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.
How is this different from get_wcs_only_layer_labels
? Please clarify in a docstring.
viewer.center_on(sky_cen) | ||
|
||
else: | ||
with delay_callback(viewer.state, *self.match_keys): |
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.
Some comments to explain the intention of this logic block would ease future debugging, in case this math does not work for all use cases.
e366643
to
5c3d48b
Compare
It is unfortunate the remote data job is failing for a different reason, so we cannot completely convince ourselves via CI until after rebase. |
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.
Also, any chance we can turn Cami's use case that started all this into a test?
I already forgot her workflow. 😆
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.
When you run the ImvizDitheredExample notebook (on this branch without rebasing on top of main), linked by WCS, 2 viewers, zoomed in a little -- Do you see the second viewer flicker when you linked pan/zoom the first one? I see it flicker a few times before settling.
Also, once I switch the tool from linked to normal pan/zoom (or box zoom), I can no longer switch back, as in when I right-click on the tool, there is no linked option anymore.
Neither of these are a problem on main
.
i_top = get_top_layer_index(self) | ||
image = self.layers[i_top].layer | ||
if self.state.reference_data is not None: | ||
image, i_ref = get_reference_image_data(self.jdaviz_app, self.reference) |
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_ref
is not used.
image, i_ref = get_reference_image_data(self.jdaviz_app, self.reference) | |
image, _ = get_reference_image_data(self.jdaviz_app, self.reference) |
i_top = get_top_layer_index(self) | ||
image = self.layers[i_top].layer | ||
if self.state.reference_data is not None: | ||
image, i_ref = get_reference_image_data(self.jdaviz_app, self.reference) |
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.
image, i_ref = get_reference_image_data(self.jdaviz_app, self.reference) | |
image, _ = get_reference_image_data(self.jdaviz_app, self.reference) |
image = self.layers[i_top].layer | ||
if self.state.reference_data is not None: | ||
image, i_ref = get_reference_image_data(self.jdaviz_app, self.reference) | ||
else: |
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.
Is this else
ever used? I don't think we would encounter a viewer without reference data but has a top layer displayed.
old_level = viewer.zoom_level | ||
viewer.zoom_level = old_level * float(to_fov_sky / orig_fov_sky) | ||
viewer.center_on(sky_cen) |
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.
This is the part I was referring to when I say you can roll your own calculations if we ever decide that Astrowidgets API cannot make the results here reasonable. cc @kecnry
Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com>
@bmorris3 , alas, does not work for me. Maybe another dev can check if it is just me or it is really still a problem. |
@pllim - that first screenshot looks like it was fixed by spacetelescope#2605 (the main image rotation PR probably just needs a rebase to get that in). |
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.
Why don't we just merge then, so rebase can happen? Thanks!
Description
This PR fixes matched pan/zoom in Imviz. 🐱
Change log entry
CHANGES.rst
? If you want to avoid merge conflicts,list the proposed change log here for review and add to
CHANGES.rst
before merge. If no, maintainershould add a
no-changelog-entry-needed
label.Checklist for package maintainer(s)
This checklist is meant to remind the package maintainer(s) who will review this pull request of some common things to look for. This list is not exhaustive.
trivial
label.