diff --git a/CHANGES.rst b/CHANGES.rst index d99a6190be..88eee77a6e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,8 @@ Cubeviz Imviz ^^^^^ +- New Simple Image Rotation plugin to rotate the axes of images. [#1551] + Mosviz ^^^^^^ diff --git a/docs/imviz/plugins.rst b/docs/imviz/plugins.rst index e0da639237..fa0cd496fd 100644 --- a/docs/imviz/plugins.rst +++ b/docs/imviz/plugins.rst @@ -97,6 +97,26 @@ data label, the X and Y directions, and the zoom box. When you have multiple viewers created in Imviz, use the Viewer dropdown menu to change the active viewer that it tracks. +.. _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 the given angle. +You can select viewer but that option only shows when applicable. +You can enter the desired rotation angle in degrees clockwise. +This angle is absolute, i.e., relative to original image orientation, not the current rotation. +Click on the :guilabel:`ROTATE` button to finalize. + .. _line-profile-xy: Line Profiles diff --git a/docs/reference/api.rst b/docs/reference/api.rst index d36b463266..4d8743973c 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -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: diff --git a/jdaviz/app.py b/jdaviz/app.py index de4fce3005..a3d1377384 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -1118,7 +1118,7 @@ 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) diff --git a/jdaviz/configs/imviz/imviz.yaml b/jdaviz/configs/imviz/imviz.yaml index b95852648e..755d86b571 100644 --- a/jdaviz/configs/imviz/imviz.yaml +++ b/jdaviz/configs/imviz/imviz.yaml @@ -23,6 +23,7 @@ tray: - g-subset-plugin - imviz-links-control - imviz-compass + - imviz-rotate-image - imviz-line-profile-xy - imviz-aper-phot-simple - imviz-catalogs diff --git a/jdaviz/configs/imviz/plugins/__init__.py b/jdaviz/configs/imviz/plugins/__init__.py index 37c02ed392..c6a7f46336 100644 --- a/jdaviz/configs/imviz/plugins/__init__.py +++ b/jdaviz/configs/imviz/plugins/__init__.py @@ -5,6 +5,7 @@ from .coords_info import * # noqa from .links_control import * # noqa from .compass import * # noqa +from .rotate_image import * # noqa from .aper_phot_simple import * # noqa from .line_profile_xy import * # noqa -from .catalogs import * # noqa \ No newline at end of file +from .catalogs import * # noqa diff --git a/jdaviz/configs/imviz/plugins/rotate_image/__init__.py b/jdaviz/configs/imviz/plugins/rotate_image/__init__.py new file mode 100644 index 0000000000..11ffcbe2ff --- /dev/null +++ b/jdaviz/configs/imviz/plugins/rotate_image/__init__.py @@ -0,0 +1 @@ +from .rotate_image import * # noqa diff --git a/jdaviz/configs/imviz/plugins/rotate_image/rotate_image.py b/jdaviz/configs/imviz/plugins/rotate_image/rotate_image.py new file mode 100644 index 0000000000..c930319922 --- /dev/null +++ b/jdaviz/configs/imviz/plugins/rotate_image/rotate_image.py @@ -0,0 +1,35 @@ +import math + +from traitlets import Any + +from jdaviz.core.registries import tray_registry +from jdaviz.core.template_mixin import TemplateMixin, ViewerSelectMixin + + +@tray_registry('imviz-rotate-image', label="Simple Image Rotation") +class RotateImageSimple(TemplateMixin, ViewerSelectMixin): + template_file = __file__, "rotate_image.vue" + + angle = Any(0).tag(sync=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._theta = 0 # degrees, clockwise + + 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 + + viewer = self.app._viewer_by_id(self.viewer_selected) + + # Rotate selected viewer canvas. + # TODO: Translation still a bit broken if zoomed in. + viewer.state.rotation = math.radians(self._theta) + + # TODO: Zoom box in Compass still broken, how to fix? If not, we need to disable it. + # Update Compass plugin. + viewer.on_limits_change() diff --git a/jdaviz/configs/imviz/plugins/rotate_image/rotate_image.vue b/jdaviz/configs/imviz/plugins/rotate_image/rotate_image.vue new file mode 100644 index 0000000000..20962c505b --- /dev/null +++ b/jdaviz/configs/imviz/plugins/rotate_image/rotate_image.vue @@ -0,0 +1,30 @@ +<template> + <j-tray-plugin> + <v-row> + <j-docs-link :link="'https://jdaviz.readthedocs.io/en/'+vdocs+'/'+config+'/plugins.html#simple-image-rotation'">Rotate image.</j-docs-link> + </v-row> + + <plugin-viewer-select + :items="viewer_items" + :selected.sync="viewer_selected" + label="Viewer" + hint="Select viewer." + /> + + <v-row> + <v-col> + <v-text-field + v-model="angle" + type="number" + label="Angle" + hint="Rotation angle in degrees clockwise" + ></v-text-field> + </v-col> + </v-row> + + <v-row justify="end"> + <v-btn color="primary" text @click="rotate_image">Rotate</v-btn> + </v-row> + + </j-tray-plugin> +</template> diff --git a/jdaviz/configs/imviz/plugins/viewers.py b/jdaviz/configs/imviz/plugins/viewers.py index f43dcf8adb..88f4644edc 100644 --- a/jdaviz/configs/imviz/plugins/viewers.py +++ b/jdaviz/configs/imviz/plugins/viewers.py @@ -209,7 +209,7 @@ def blink_once(self, reversed=False): self.state.layers[ilayer].visible = False # We can display the active data label in Compass plugin. - self.set_compass(self.state.layers[next_layer].layer) + self.set_compass(self.state.layers[next_layer].layer, transform=self.state._affine_pretransform._transform if self.state._affine_pretransform else None) # Update line profile plots too. if self.line_profile_xy is None: @@ -228,7 +228,7 @@ def on_limits_change(self, *args): if self.compass is not None: self.compass.clear_compass() return - self.set_compass(self.state.layers[i].layer) + self.set_compass(self.state.layers[i].layer, transform=self.state._affine_pretransform._transform if self.state._affine_pretransform else None) def _get_real_xy(self, image, x, y): """Return real (X, Y) position and status in case of dithering. @@ -275,7 +275,7 @@ def _get_zoom_limits(self, image): (self.state.x_max, self.state.y_min))) return zoom_limits - def set_compass(self, image): + def set_compass(self, image, transform=None): """Update the Compass plugin with info from the given image Data object.""" if self.compass is None: # Maybe another viewer has it return @@ -290,7 +290,7 @@ def set_compass(self, image): norm = ImageNormalize(vmin=vmin, vmax=vmax, stretch=LinearStretch()) self.compass.draw_compass(image.label, wcs_utils.draw_compass_mpl( arr, orig_shape=image.shape, wcs=image.coords, show=False, zoom_limits=zoom_limits, - norm=norm)) + norm=norm, transform=transform)) def set_plot_axes(self): self.figure.axes[1].tick_format = None diff --git a/jdaviz/configs/imviz/wcs_utils.py b/jdaviz/configs/imviz/wcs_utils.py index 4d6444a68f..f3563fbf3d 100644 --- a/jdaviz/configs/imviz/wcs_utils.py +++ b/jdaviz/configs/imviz/wcs_utils.py @@ -164,7 +164,7 @@ def get_compass_info(image_wcs, image_shape, r_fac=0.4): return x, y, xn, yn, xe, ye, degn, dege, xflip -def draw_compass_mpl(image, orig_shape=None, wcs=None, show=True, zoom_limits=None, **kwargs): +def draw_compass_mpl(image, orig_shape=None, wcs=None, show=True, zoom_limits=None, transform=None, **kwargs): """Visualize the compass using Matplotlib. Parameters @@ -203,8 +203,12 @@ def draw_compass_mpl(image, orig_shape=None, wcs=None, show=True, zoom_limits=No plt.ioff() fig, ax = plt.subplots() + if transform is not None: + transform += ax.transData + else: + transform = ax.transData ax.imshow(image, extent=[-0.5, orig_shape[1] - 0.5, -0.5, orig_shape[0] - 0.5], - origin='lower', cmap='gray', **kwargs) + origin='lower', cmap='gray', transform=transform, **kwargs) if wcs is not None: try: