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

Rotate image in Imviz #1340

Closed
wants to merge 4 commits into from
Closed
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
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ Cubeviz
Imviz
^^^^^

- New Simple Image Rotation plugin to rotate the celestial axes of
images if they have valid WCS. [#1340]

Mosviz
^^^^^^

Expand Down
36 changes: 36 additions & 0 deletions docs/imviz/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,39 @@ The columns are as follow:

Once you have the results in a table, you can further manipulated them as
documented in :ref:`astropy:astropy-table`.


.. _rotate-image-simple:

Simple Image Rotation
=====================

.. warning::

Distortion is ignored, so using this plugin on distorted data is
not recommended.

.. note::

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. 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 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.

Toggling the rotation mode off would re-orient the viewer to have
Y-up and X-right for the original reference data but the other data
would still be linked to it by WCS. To change the linking back to pixels,
use the :ref:`imviz-link-control` plugin.
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
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 used to have this in the app somewhere but maybe it was removed in one of the refactoring. @kecnry , I know your pet peeve is the reference vs ID stuff, so I want to make sure you are okay with this change.

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 hasattr(layer, 'meta') and 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
1 change: 1 addition & 0 deletions jdaviz/configs/imviz/imviz.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ tray:
- imviz-compass
- imviz-line-profile-xy
- imviz-aper-phot-simple
- imviz-rotate-image
- g-export-plot
viewer_area:
- container: col
Expand Down
1 change: 1 addition & 0 deletions jdaviz/configs/imviz/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
from .compass import * # noqa
from .aper_phot_simple import * # noqa
from .line_profile_xy import * # noqa
from .rotate_image import * # noqa
19 changes: 14 additions & 5 deletions jdaviz/configs/imviz/plugins/line_profile_xy/line_profile_xy.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,14 @@ def vue_draw_plot(self, *args, **kwargs):
self.reset_results()
return

x_min, y_min, x_max, y_max = viewer._get_zoom_limits(data)
xy_limits = viewer._get_zoom_limits(data)
x_limits = xy_limits[:, 0]
y_limits = xy_limits[:, 1]
x_min = x_limits.min()
x_max = x_limits.max()
y_min = y_limits.min()
y_max = y_limits.max()

comp = data.get_component(data.main_components[0])
if comp.units:
y_label = comp.units
Expand Down Expand Up @@ -101,8 +108,9 @@ def vue_draw_plot(self, *args, **kwargs):
y_min = max(int(y_min), 0)
y_max = min(int(y_max), ny)
zoomed_data_x = comp.data[y_min:y_max, x]
line_x.scales['y'].min = zoomed_data_x.min() * 0.95
line_x.scales['y'].max = zoomed_data_x.max() * 1.05
if zoomed_data_x.size > 0:
line_x.scales['y'].min = zoomed_data_x.min() * 0.95
line_x.scales['y'].max = zoomed_data_x.max() * 1.05

fig_y.title = f'Y={y}'
fig_y.title_style = {'font-size': '12px'}
Expand All @@ -119,8 +127,9 @@ def vue_draw_plot(self, *args, **kwargs):
x_min = max(int(x_min), 0)
x_max = min(int(x_max), nx)
zoomed_data_y = comp.data[y, x_min:x_max]
line_y.scales['y'].min = zoomed_data_y.min() * 0.95
line_y.scales['y'].max = zoomed_data_y.max() * 1.05
if zoomed_data_y.size > 0:
line_y.scales['y'].min = zoomed_data_y.min() * 0.95
line_y.scales['y'].max = zoomed_data_y.max() * 1.05

self.line_plot_across_x = fig_x
self.line_plot_across_y = fig_y
Expand Down
1 change: 1 addition & 0 deletions jdaviz/configs/imviz/plugins/rotate_image/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .rotate_image import * # noqa
101 changes: 101 additions & 0 deletions jdaviz/configs/imviz/plugins/rotate_image/rotate_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import numpy as np
from astropy.nddata import NDData
from traitlets import Any, Bool, observe

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

__all__ = ['RotateImageSimple']


# TODO: Refactor to use Kyle's CSS rotation?
# TODO: Maybe we do not need DataSelectMixin.
@tray_registry('imviz-rotate-image', label="Simple Image Rotation")
class RotateImageSimple(TemplateMixin, ViewerSelectMixin, DatasetSelectMixin):
template_file = __file__, "rotate_image.vue"

rotate_mode_on = Bool(False).tag(sync=True)
angle = Any(0).tag(sync=True)

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

# Dummy array to go with the rotated WCS, modified as needed later.
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 _update_all_viewers(self):
for viewer in self.app._viewer_store.values():
viewer.update() # Force viewer to update.

@observe('rotate_mode_on')
def vue_toggle_on_off(self, *args, **kwargs):
# It is hard to keep track what the previous reference data was or
# if it is still valid, so just brute force here.
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.
try:
self.app.remove_data_from_viewer(vid, self._data_ref_label)
except Exception: # nosec B110
pass

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:
return

w_data = self.dataset.selected_dc_item.coords
if w_data is None: # Nothing to do
return

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)
if viewer.state.reference_data.label != self._data_ref_label:
viewer.state.reference_data = self.app.data_collection[self._data_ref_label]

# Force all viewers to update.
self._update_all_viewers()
except Exception as err: # pragma: no cover
self.hub.broadcast(SnackbarMessage(
f"Image rotation failed: {repr(err)}", color='error', sender=self))
48 changes: 48 additions & 0 deletions jdaviz/configs/imviz/plugins/rotate_image/rotate_image.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<template>
<j-tray-plugin>
<v-row>
<j-docs-link :link="'https://jdaviz.readthedocs.io/en/'+vdocs+'/'+config+'/plugins.html#simple-image-rotation'">Rotate image N-up/E-left.</j-docs-link>
</v-row>

<v-row>
<v-switch
label="Enable celestial axes rotation"
hint="Toggle rotation mode on or off"
v-model="rotate_mode_on"
persistent-hint>
</v-switch>
</v-row>

<div v-if="rotate_mode_on">
<plugin-viewer-select
:items="viewer_items"
:selected.sync="viewer_selected"
label="Viewer"
hint="Select viewer."
/>

<plugin-dataset-select
:items="dataset_items"
:selected.sync="dataset_selected"
:show_if_single_entry="false"
label="Data"
hint="Select the data to rotate."
/>

<v-row>
<v-col>
<v-text-field
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>
Loading