Skip to content

Commit

Permalink
WIP: Cunning plan [ci skip]
Browse files Browse the repository at this point in the history
  • Loading branch information
pllim committed Jun 3, 2022
1 parent aa753fb commit 556ccb2
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 38 deletions.
92 changes: 72 additions & 20 deletions jdaviz/configs/imviz/plugins/rotate_image/rotate_image.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,92 @@
from traitlets import Bool, observe
import numpy as np
from astropy.nddata import NDData
from astropy.wcs import WCS
from traitlets import Any, Bool, observe

from jdaviz.core.custom_traitlets import FloatHandleEmpty
from jdaviz.configs.imviz.wcs_utils import rotate_wcs
from jdaviz.core.registries import tray_registry
from jdaviz.core.template_mixin import TemplateMixin
from jdaviz.core.template_mixin import TemplateMixin, ViewerSelectMixin, DatasetSelectMixin

__all__ = ['RotateImageSimple']


@tray_registry('imviz-rotate-image', label="Simple Image Rotation")
class RotateImageSimple(TemplateMixin):
class RotateImageSimple(TemplateMixin, ViewerSelectMixin, DatasetSelectMixin):
template_file = __file__, "rotate_image.vue"

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

@observe('angle')
def vue_rotate_image(*args, **kwargs):
pass # TODO
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._theta = 0

# 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'})

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)

# Create a small dummy image
for viewer in self.app._viewer_store.values():
viewer._compass_show_zoom = not hide_zoom
# Force redraw if the compass is visible.
if viewer.compass is not None and viewer.compass.plugin_opened:
viewer.on_limits_change()

# Create a fake WCS with desired orientation
@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.
self.app.remove_data_from_viewer(vid, self._ndd_ref_label)

self._handle_compass_zoom_box()

# FIXME: How to wait till user has stopped typing?
@observe('angle')
def vue_rotate_image(self, *args, **kwargs):
try:
self._theta = float(self.angle)
except Exception:
return

# Add it into data collection
w_data = self.dataset.selected_dc_item.coords
if w_data is None: # Nothing to do
return

# Make it reference. Remember which one as old reference.
# If no API to make something reference, remove everything, readd
# with it first, and relink.
# Adjust the fake WCS to data with desired orientation
w_new = rotate_wcs(self._ndd_ref.wcs, self._theta)

# When angle changes, is updating Data.coords with new WCS good enough?
# 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()

# Find a way to hide this image from Data dropdown, blink, etc
# 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:
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)

# When toggle off, discard this from data_collection and restore old reference.
# 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]

# Have compass hide zoom box when rotate_mode_on is True
# and un-hide it when False.
self._handle_compass_zoom_box()

# Write tests.
# TODO: Find a way to hide this image from Data dropdown, blink, etc
# TODO: Manual testing.
# TODO: Write tests.
36 changes: 26 additions & 10 deletions jdaviz/configs/imviz/plugins/rotate_image/rotate_image.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,32 @@
</v-switch>
</v-row>

<v-row v-if="rotate_mode_on">
<v-col>
<v-text-field
v-model='angle'
type="number"
label="Angle"
hint="Rotation angle of N-axis in degree clockwise (0 is N-up)"
></v-text-field>
</v-col>
</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>
</div>
</j-tray-plugin>
</template>
6 changes: 5 additions & 1 deletion jdaviz/configs/imviz/plugins/viewers.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def __init__(self, *args, **kwargs):

self.label_mouseover = None
self.compass = None
self._compass_show_zoom = True
self.line_profile_xy = None

self.add_event_callback(self.on_mouse_or_key_event, events=['mousemove', 'mouseenter',
Expand Down Expand Up @@ -262,7 +263,10 @@ def set_compass(self, image):
if self.compass is None: # Maybe another viewer has it
return

zoom_limits = self._get_zoom_limits(image)
if self._compass_show_zoom:
zoom_limits = self._get_zoom_limits(image)
else:
zoom_limits = None

# Downsample input data to about 400px (as per compass.vue) for performance.
xstep = max(1, round(image.shape[1] / 400))
Expand Down
54 changes: 50 additions & 4 deletions jdaviz/configs/imviz/wcs_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# This is adapted from Ginga (ginga.util.wcs, ginga.trcalc, and ginga.Bindings.ImageViewBindings).
# Please see the file licenses/GINGA_LICENSE.txt for details.
#
"""This module handles calculations based on world coordinate system (WCS)."""

import base64
Expand All @@ -12,8 +9,57 @@
from astropy.coordinates import SkyCoord
from matplotlib.patches import Rectangle

__all__ = ['get_compass_info', 'draw_compass_mpl']
__all__ = ['rotate_wcs', 'get_compass_info', 'draw_compass_mpl']


# This is adapted from MPDAF lib/mpdaf/obj/coords.py module.
# Please see the file licenses/MPDAF_LICENSE.txt for details.

def rotate_wcs(w_in, theta):
"""Apply rotation to given FITS WCS.
This does not take distortions into account.
Parameters
----------
w_in : `astropy.wcs.WCS`
FITS WCS to apply rotation to.
theta : float
This can be used to specify a value for the rotation angle of the
image in degrees. This is the angle between celestial north and the Y
axis of the image, in the sense of an eastward rotation of
celestial north from the Y-axis (positive is clockwise).
Along with the CDELT parameter, this is used to construct a
FITS CD rotation matrix. This is done as described in equation 189
of Calabretta & Greisen 2002,[1]_ where it serves as the value of the CROTA term.
Returns
-------
w_out : `astropy.wcs.WCS`
FITS WCS with rotation applied.
References
----------
.. [1] Calabretta, M. R., & Greisen, E. W. 2002, A&A, 395, 1077-1122
"""
w_out = w_in.deepcopy()
rho = np.deg2rad(theta)
sin_rho = np.sin(rho)
cos_rho = np.cos(rho)

cdelt = w_in.wcs.cdelt
w_out.wcs.pc = np.array([[cdelt[1] * cos_rho, -cdelt[0] * sin_rho],
[cdelt[1] * sin_rho, cdelt[0] * cos_rho]])
w_out.wcs.cdelt = np.array([-1.0, 1.0])
w_out.wcs.set()

return w_out


# These below are adapted from Ginga (ginga.util.wcs, ginga.trcalc,
# and ginga.Bindings.ImageViewBindings).
# Please see the file licenses/GINGA_LICENSE.txt for details.

def rotate_pt(x_arr, y_arr, theta_deg, xoff=0, yoff=0):
"""
Expand Down
47 changes: 44 additions & 3 deletions notebooks/concepts/imviz_link_north_up_east_left.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@
"ndd_gwcs = NDData(a, wcs=w2)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "511e4671",
"metadata": {},
"outputs": [],
"source": [
"# DOES NOT WORK: https://github.com/spacetelescope/gwcs/issues/408\n",
"#w2.to_fits(bounding_box=([0, 100] * u.pix, [0, 100] * u.pix))"
]
},
{
"cell_type": "markdown",
"id": "eb2caebb",
Expand Down Expand Up @@ -250,15 +261,33 @@
"imviz = Imviz(verbosity='warning')"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5091abf3",
"metadata": {},
"outputs": [],
"source": [
"imviz.load_data(ndd, data_label='wcs_ref')"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fa69eae0",
"metadata": {},
"outputs": [],
"source": [
"imviz.load_data(hdu, data_label='jb5g05ubq_flt')"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e3585cda",
"metadata": {},
"outputs": [],
"source": [
"imviz.load_data(ndd, data_label='wcs_ref')\n",
"imviz.load_data(hdu, data_label='jb5g05ubq_flt')\n",
"imviz.load_data(ndd_gwcs, data_label='gwcs')"
]
},
Expand Down Expand Up @@ -305,7 +334,19 @@
{
"cell_type": "code",
"execution_count": null,
"id": "7402ecd4",
"id": "b1c79dc7",
"metadata": {},
"outputs": [],
"source": [
"#true_ref = imviz.default_viewer.state.reference_data.label\n",
"#imviz.default_viewer.state.reference_data = imviz.app.data_collection['gwcs[DATA]']\n",
"#print(true_ref, imviz.default_viewer.state.reference_data.label)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "acac6156",
"metadata": {},
"outputs": [],
"source": []
Expand Down

0 comments on commit 556ccb2

Please sign in to comment.