Skip to content

Commit

Permalink
Moar cunning [ci skip]
Browse files Browse the repository at this point in the history
  • Loading branch information
pllim committed Jun 7, 2022
1 parent 78c143e commit feb9850
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 200 deletions.
18 changes: 12 additions & 6 deletions docs/imviz/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -268,11 +268,17 @@ Simple Image Rotation
Zoom box in :ref:`imviz-compass` will not show when rotation mode is on.

This plugins rotates image(s) by its celestial axes by the given angle.
You can toggle the rotation mode on and off. When on, by default, it
will align image(s) to N-up and E-left orientation.
If rotation mode is on but an image does not have a valid WCS, it will
not be rotated.
You can toggle the rotation mode on and off. You can select viewer and data
but those options only show when applicable. You can enter the desired
rotation angle in degrees. When the angle is zero, it will align image(s) to
N-up and E-left orientation. If an image does not have a valid WCS, it will
not be rotated. Click on the :guilabel:`ROTATE` button to finalize.

Given that this plugin affects the linking of the entire data collection
in Imviz, it is not recommended to keep toggling this on and off
continuously. You might also notice some lag when a lot of images are loaded.
in Imviz by introducing a hidden data layer with the desired WCS,
it is not recommended to keep toggling this on and off continuously.
You might also notice some lag when a lot of images are loaded.

Furthermore, linking is global across Imviz, regardless of the viewer.
Therefore, when WCS is rotated in one viewer, it will propagate to
the other viewers when multiple viewers are open.
3 changes: 3 additions & 0 deletions docs/reference/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ Plugins
.. automodapi:: jdaviz.configs.imviz.plugins.links_control.links_control
:no-inheritance-diagram:

.. automodapi:: jdaviz.configs.imviz.plugins.rotate_image.rotate_image
:no-inheritance-diagram:

.. automodapi:: jdaviz.configs.mosviz.plugins.row_lock.row_lock
:no-inheritance-diagram:

Expand Down
17 changes: 13 additions & 4 deletions jdaviz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1017,12 +1017,19 @@ def _viewer_by_reference(self, reference):
Returns
-------
`~glue_jupyter.bqplot.common.BqplotBaseView`
viewer : `~glue_jupyter.bqplot.common.BqplotBaseView`
The viewer class instance.
"""
viewer_item = self._viewer_item_by_reference(reference)

return self._viewer_store[viewer_item['id']]
# There is a degeneracy between reference and ID.
# Imviz falls back to ID for user-created viewers.
if viewer_item is not None:
viewer = self._viewer_store[viewer_item['id']]
else: # Maybe it is ID
viewer = self._viewer_store[reference]

return viewer

def _viewer_item_by_reference(self, reference):
"""
Expand Down Expand Up @@ -1218,10 +1225,12 @@ def _update_selected_data_items(self, viewer_id, selected_items):

# Make everything visible again in Imviz.
if self.config == 'imviz':
from jdaviz.configs.imviz.helper import layer_is_image_data
from jdaviz.configs.imviz.helper import layer_is_image_data, layer_is_hidden

for lyr in viewer.state.layers:
if layer_is_image_data(lyr.layer):
if layer_is_hidden(lyr.layer):
lyr.visible = False
elif layer_is_image_data(lyr.layer):
lyr.visible = True
viewer.on_limits_change() # Trigger compass redraw

Expand Down
5 changes: 4 additions & 1 deletion jdaviz/components/viewer_data_select.vue
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ module.exports = {
return true
},
itemIsVisible(item, mosvizExtraItems) {
if (this.$props.viewer.config === 'mosviz') {
if (item.meta.Plugin === 'Simple Image Rotation') {
// hide dummy image with fake WCS from that plugin
return false
} else if (this.$props.viewer.config === 'mosviz') {
if (this.$props.viewer.reference === 'spectrum-viewer' && item.type !== '1d spectrum') {
// filters out table, spectrum 2d, images
return false
Expand Down
8 changes: 7 additions & 1 deletion jdaviz/configs/imviz/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,13 +481,19 @@ def layer_is_image_data(layer):
return isinstance(layer, BaseData) and layer.ndim == 2


def layer_is_hidden(layer):
# This is fake image with rotated WCS from the plugin.
return layer.meta.get('Plugin', '') == 'Simple Image Rotation'


def get_top_layer_index(viewer):
"""Get index of the top visible image 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 layer_is_image_data(lyr.layer)][-1]
if (lyr.visible and layer_is_image_data(lyr.layer) and
not layer_is_hidden(lyr.layer))][-1]


def get_reference_image_data(app):
Expand Down
101 changes: 58 additions & 43 deletions jdaviz/configs/imviz/plugins/rotate_image/rotate_image.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import numpy as np
from astropy.nddata import NDData
from astropy.wcs import WCS
from traitlets import Any, Bool, observe

from jdaviz.configs.imviz.wcs_utils import rotate_wcs
from jdaviz.configs.imviz.helper import link_image_data
from jdaviz.configs.imviz.plugins.parsers import _nddata_to_glue_data
from jdaviz.configs.imviz.wcs_utils import generate_rotated_wcs
from jdaviz.core.events import SnackbarMessage
from jdaviz.core.registries import tray_registry
from jdaviz.core.template_mixin import TemplateMixin, ViewerSelectMixin, DatasetSelectMixin

Expand All @@ -19,26 +21,25 @@ class RotateImageSimple(TemplateMixin, ViewerSelectMixin, DatasetSelectMixin):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._theta = 0
self._theta = 0 # degrees, clockwise

# Dummy array to go with the rotated WCS, modified as needed later.
w_ref = WCS({'CTYPE1': 'RA---TAN', 'CTYPE2': 'DEC--TAN',
'CUNIT1': 'deg', 'CUNIT2': 'deg',
'CRPIX1': 1, 'CRPIX2': 1,
'NAXIS1': 3, 'NAXIS2': 3})
self._ndd_ref_label = '_simple_rotated_wcs_ref'
self._ndd_ref = NDData(np.zeros((3, 3), dtype=np.uint8), wcs=w_ref,
meta={'Plugin': 'Simple Image Rotation'})
ndd = NDData(np.zeros((2, 2), dtype=np.uint8), wcs=generate_rotated_wcs(0),
meta={'Plugin': 'Simple Image Rotation'})
for cur_data, cur_label in _nddata_to_glue_data(ndd, '_simple_rotated_wcs_ref'):
self._data_ref = cur_data
self._data_ref_label = cur_label
break # Just the DATA

def _handle_compass_zoom_box(self):
# Hide zoom box in Compass because it is all screwy.
hide_zoom = self.rotate_mode_on and not np.allclose(self._theta, 0)

# Hide zoom box in Compass because it is all screwy unless
# N lines up with Y, which then you do not need this plugin anyway.
for viewer in self.app._viewer_store.values():
viewer._compass_show_zoom = not hide_zoom
viewer._compass_show_zoom = not self.rotate_mode_on
# Force redraw if the compass is visible.
if viewer.compass is not None and viewer.compass.plugin_opened:
viewer.on_limits_change()
viewer.update() # Force viewer to update.

@observe('rotate_mode_on')
def vue_toggle_on_off(self, *args, **kwargs):
Expand All @@ -47,13 +48,16 @@ def vue_toggle_on_off(self, *args, **kwargs):
if not self.rotate_mode_on:
for vid in self.app.get_viewer_ids():
# This is silent no-op if it is not in that viewer.
self.app.remove_data_from_viewer(vid, self._ndd_ref_label)
try:
self.app.remove_data_from_viewer(vid, self._data_ref_label)
except Exception:
pass

self._handle_compass_zoom_box()

# FIXME: How to wait till user has stopped typing?
@observe('angle')
def vue_rotate_image(self, *args, **kwargs):
# We only grab the value here to avoid constantly updating as
# user is still entering or updating the value.
try:
self._theta = float(self.angle)
except Exception:
Expand All @@ -63,30 +67,41 @@ def vue_rotate_image(self, *args, **kwargs):
if w_data is None: # Nothing to do
return

# Adjust the fake WCS to data with desired orientation
w_new = rotate_wcs(self._ndd_ref.wcs, self._theta)

# Center to selected data.
# FIXME (GWCS): https://github.com/spacetelescope/gwcs/issues/408
if isinstance(w_data, WCS):
w_new.wcs.crval = w_data.wcs.crval
w_new.wcs.set()

# Add it into data collection or just modify if already there.
if self._ndd_ref_label in self.app.data_collection.labels:
self.app.data_collection[self._ndd_ref_label].coords = w_new
else:
try:
# Adjust the fake WCS to data with desired orientation.
w_in = generate_rotated_wcs(self._theta)

# TODO: How to make this more robust?
# Match with selected data.
w_shape = self.dataset.selected_dc_item.shape
sky0 = w_data.pixel_to_world(0, 0)
sky1 = w_data.pixel_to_world(w_shape[1] * 0.5, w_shape[0] * 0.5)
avg_cdelt = (abs(sky1.ra.deg - sky0.ra.deg) + abs(sky1.dec.deg - sky0.dec.deg)) * 0.5
w_in.wcs.crval = np.array([sky1.ra.deg, sky1.dec.deg])
w_in.wcs.cdelt = np.array([-avg_cdelt, avg_cdelt])
w_in.wcs.set()

# Add it into data collection if not already there.
if self._data_ref_label not in self.app.data_collection.labels:
self.app.add_data(self._data_ref, data_label=self._data_ref_label, notify_done=False)

# Update the WCS.
self.app.data_collection[self._data_ref_label].coords = w_in

# Force link by WCS if not already. Otherwise, no point in rotating WCS.
# Default settings is enough since we already said distortions are ignored.
link_image_data(self.app, link_type='wcs')

# Make it a reference.
self.app.add_data_to_viewer(self.viewer_selected, self._data_ref_label,
clear_other_data=False)
viewer = self.app.get_viewer(self.viewer_selected)
# FIXME: Need to build native Data object here.
self.app.add_data(self._ndd_ref, data_label=self._ndd_ref_label, notify_done=False)
self.app.add_data_to_viewer(viewer, self._ndd_ref_label, clear_other_data=False)

# Make it a reference.
if viewer.state.reference_data.label != self._ndd_ref_label:
viewer.state.reference_data = self.app.data_collection[self._ndd_ref_label]

self._handle_compass_zoom_box()

# TODO: Find a way to hide this image from Data dropdown, blink, etc
# TODO: Manual testing.
# TODO: Write tests.
if viewer.state.reference_data.label != self._data_ref_label:
viewer.state.reference_data = self.app.data_collection[self._data_ref_label]

# Hide Compass zoom box here too, for multi-viewer use case.
# And force all viewers to update.
self._handle_compass_zoom_box()
except Exception as err:
self.hub.broadcast(SnackbarMessage(
f"Image rotation failed: {repr(err)}", color='error', sender=self))
10 changes: 7 additions & 3 deletions jdaviz/configs/imviz/plugins/rotate_image/rotate_image.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

<v-row>
<v-switch
label="Rotate celestial axes"
hint="Toggle N-E axes rotation on or off"
label="Enable celestial axes rotation"
hint="Toggle rotation mode on or off"
v-model="rotate_mode_on"
persistent-hint>
</v-switch>
Expand All @@ -32,13 +32,17 @@
<v-row>
<v-col>
<v-text-field
v-model='angle'
v-model="angle"
type="number"
label="Angle"
hint="Rotation angle of N-axis in degrees clockwise (0 is N-up)"
></v-text-field>
</v-col>
</v-row>

<v-row justify="end">
<v-btn color="primary" text @click="rotate_image">Rotate</v-btn>
</v-row>
</div>
</j-tray-plugin>
</template>
28 changes: 22 additions & 6 deletions jdaviz/configs/imviz/plugins/viewers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from glue_jupyter.bqplot.image import BqplotImageView

from jdaviz.configs.imviz import wcs_utils
from jdaviz.configs.imviz.helper import data_has_valid_wcs, layer_is_image_data, get_top_layer_index
from jdaviz.configs.imviz.helper import (
data_has_valid_wcs, layer_is_image_data, layer_is_hidden, get_top_layer_index)
from jdaviz.core.astrowidgets_api import AstrowidgetsImageViewerMixin
from jdaviz.core.events import SnackbarMessage
from jdaviz.core.registries import viewer_registry
Expand Down Expand Up @@ -73,7 +74,9 @@ def __init__(self, *args, **kwargs):
def on_mouse_or_key_event(self, data):

# Find visible layers
visible_layers = [layer for layer in self.state.layers if layer.visible]
visible_layers = [layer for layer in self.state.layers
if (layer.visible and layer_is_image_data(layer.layer) and
not layer_is_hidden(layer.layer))]

if len(visible_layers) == 0:
return
Expand Down Expand Up @@ -169,7 +172,7 @@ def blink_once(self):

# Exclude Subsets: They are global
valid = [ilayer for ilayer, layer in enumerate(self.state.layers)
if layer_is_image_data(layer.layer)]
if (layer_is_image_data(layer.layer) and not layer_is_hidden(layer.layer))]
n_layers = len(valid)

if n_layers == 1:
Expand Down Expand Up @@ -249,7 +252,12 @@ def _get_zoom_limits(self, image):
image, which can be inaccurate if given image is dithered and
they are linked by WCS.
"""
if data_has_valid_wcs(image) and self.get_link_type(image.label) == 'wcs':
try:
link_type = self.get_link_type(image.label)
except ValueError:
return None

if data_has_valid_wcs(image) and link_type == 'wcs':
# Convert X,Y from reference data to the one we are actually seeing.
x = image.coords.world_to_pixel(self.state.reference_data.coords.pixel_to_world(
(self.state.x_min, self.state.x_max), (self.state.y_min, self.state.y_max)))
Expand Down Expand Up @@ -291,8 +299,16 @@ def set_plot_axes(self):
def data(self, cls=None):
return [layer_state.layer # .get_object(cls=cls or self.default_class)
for layer_state in self.state.layers
if hasattr(layer_state, 'layer') and
layer_is_image_data(layer_state.layer)]
if (hasattr(layer_state, 'layer') and
layer_is_image_data(layer_state.layer) and
not layer_is_hidden(layer_state.layer))]

def update(self):
"""Force viewer to update all its layers.
This is useful when viewer does not update automatically on time.
"""
for lyr in self.layers:
lyr.update()

def get_link_type(self, data_label):
"""Find the type of ``glue`` linking between the given
Expand Down
Loading

0 comments on commit feb9850

Please sign in to comment.