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 with glue Affine transform #1551

Closed
wants to merge 8 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
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Cubeviz
Imviz
^^^^^

- New Simple Image Rotation plugin to rotate the axes of images. [#1551]

Mosviz
^^^^^^

Expand Down
20 changes: 20 additions & 0 deletions docs/imviz/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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
2 changes: 1 addition & 1 deletion jdaviz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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 @@ -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
Expand Down
3 changes: 2 additions & 1 deletion jdaviz/configs/imviz/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
from .catalogs import * # noqa
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
35 changes: 35 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,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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works but pretty sure it is clockwise again... Is this expected?

Also, the center translation is still a bit off when I zoom in first and then rotate on a rectangular image. If I zoom in on a star, I would expect rotation around the star. But the star moves quite dramatically when I rotate.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently applying the transform directly on x, y in the pretransform.__call__() seems to have it operate the other way around again, but that is easily fixed, even if I cannot follow the full chain of events.

I think for finding the right centre of rotation setting xy from (x_min + x_max) / 2 rather than shape[0] / 2 etc. is still the better option (x_min, y_min may be nonzero even if not zoomed in); at least that that keeps the viewport centred and rotated around the original zoom region when zooming in first and then rotating. Unfortunately with the same modification, zooming in after rotation is completely off – apparently this is setting the limits from the unrotated frame, I recon this is failing due to the data values not being returned from the transformed pixel coordinates just as with the mouseover.
I would expect that once the latter problem is solved, the zoom will also operate on the correct subset, so the alternative above still looks a step forward.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Derek, all this will be fixed upstream, yes?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure which of the "all" is fixable upstream. I have a patch for the sense of direction, and keeping the centre of rotation in the current viewport centre, but the mouseover stats, active selection area and zoom-after-rotation remain of.

  • We should probably provide a method in glue-core on the state class which given cursor coordinates can provide the actual array index into the data since these are now no longer the same - we can then use that to fix the mouse over coordinates here

I think I'll work on that next, but it sounds like this will still require some adaption from Imviz to use those viewer.state methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it helps, the Imviz mouseover logic is here:

if data['event'] == 'mousemove':

Out of scope now but if you are curious, Cubeviz version is here:

if data['event'] == 'mousemove':

They both send info to the same plugin here:

https://github.com/spacetelescope/jdaviz/blob/main/jdaviz/configs/imviz/plugins/coords_info/coords_info.py

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes; looked at the above, but can I find any more info on what type of object the data is that is passed to on_mouse_or_key_event? I've been struggling to find out what its 'domain' item is or where it is set.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That I am not sure... Could be glue-jupyter , bqplot-image-gl , bqplot ... 🤷

Hopefully @maartenbreddels or @astrofrog can tell us.


# TODO: Zoom box in Compass still broken, how to fix? If not, we need to disable it.
# Update Compass plugin.
viewer.on_limits_change()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do I get viewer limits that is consistent with the rotation, so I can show a sane zoom box in Compass plugin when image is rotated?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you going to show the image rotated in the compass plugin as well? Or are you going to show it in the original/native orientation and want to show the box rotated? (since it matters for the limits)

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 was thinking it is easier in Compass if we do not rotate the image but only rotate the zoom box. The Compass display real estate is already pretty small. To rotate the image in Compass would make the image display even smaller when rotated.

Also even if not rotated, images can have wild orientations after you link them by WCS.

30 changes: 30 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,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>
8 changes: 4 additions & 4 deletions jdaviz/configs/imviz/plugins/viewers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 6 additions & 2 deletions jdaviz/configs/imviz/wcs_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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],
pllim marked this conversation as resolved.
Show resolved Hide resolved
origin='lower', cmap='gray', **kwargs)
origin='lower', cmap='gray', transform=transform, **kwargs)

if wcs is not None:
try:
Expand Down