From 734333b13f3a5bb6691def50df84914071e5a087 Mon Sep 17 00:00:00 2001 From: "Pey Lian Lim (Github)" <2090236+pllim@users.noreply.github.com> Date: Sat, 21 Jan 2023 18:52:58 -0500 Subject: [PATCH] Excerpt from #1340 that still does not work --- CHANGES.rst | 3 + docs/imviz/plugins.rst | 22 + docs/reference/api.rst | 3 + jdaviz/configs/imviz/imviz.yaml | 1 + jdaviz/configs/imviz/plugins/__init__.py | 3 +- .../imviz/plugins/rotate_image/__init__.py | 1 + .../plugins/rotate_image/rotate_image.py | 72 +++ .../plugins/rotate_image/rotate_image.vue | 32 ++ .../imviz/tests/test_simple_image_rotation.py | 92 ++++ jdaviz/configs/imviz/tests/test_wcs_utils.py | 8 + jdaviz/configs/imviz/wcs_utils.py | 50 +- licenses/MPDAF_LICENSE.txt | 28 ++ .../imviz_link_north_up_east_left.ipynb | 470 ++++++++++++++++++ 13 files changed, 780 insertions(+), 5 deletions(-) create mode 100644 jdaviz/configs/imviz/plugins/rotate_image/__init__.py create mode 100644 jdaviz/configs/imviz/plugins/rotate_image/rotate_image.py create mode 100644 jdaviz/configs/imviz/plugins/rotate_image/rotate_image.vue create mode 100644 jdaviz/configs/imviz/tests/test_simple_image_rotation.py create mode 100644 licenses/MPDAF_LICENSE.txt create mode 100644 notebooks/concepts/imviz_link_north_up_east_left.ipynb diff --git a/CHANGES.rst b/CHANGES.rst index 4436e46b67..e32cdcf3ce 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,6 +12,9 @@ Cubeviz Imviz ^^^^^ +- New Simple Image Rotation plugin to rotate the celestial axes of + images if they have valid WCS. [#1972] + Mosviz ^^^^^^ - Reliably retrieves identifier using each datasets' metadata entry. [#1851] diff --git a/docs/imviz/plugins.rst b/docs/imviz/plugins.rst index 38df466801..db1ac6b2b5 100644 --- a/docs/imviz/plugins.rst +++ b/docs/imviz/plugins.rst @@ -226,6 +226,28 @@ results are displayed under the :guilabel:`CALCULATE` button. :ref:`Export Photometry ` Documentation on exporting photometry results. +.. _rotate-image-simple: + +Simple Image Rotation +===================== + +.. warning:: + + Distortion is ignored, so using this plugin on distorted data is + not recommended. + +This plugins rotates image(s) by its celestial axes by the given angle. +You can select data but the option only shows 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. +Click on the :guilabel:`RESET` to restore the original WCS. + +Linking is global across Imviz, regardless of the viewer. +Therefore, when WCS is rotated, it will propagate to all +the viewers when multiple viewers are open. + .. _imviz-catalogs: Catalog Search diff --git a/docs/reference/api.rst b/docs/reference/api.rst index ef3963f30c..8484a2bcf0 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -129,6 +129,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/configs/imviz/imviz.yaml b/jdaviz/configs/imviz/imviz.yaml index b95852648e..504593ec9f 100644 --- a/jdaviz/configs/imviz/imviz.yaml +++ b/jdaviz/configs/imviz/imviz.yaml @@ -25,6 +25,7 @@ tray: - imviz-compass - imviz-line-profile-xy - imviz-aper-phot-simple + - imviz-rotate-image - imviz-catalogs - g-export-plot viewer_area: diff --git a/jdaviz/configs/imviz/plugins/__init__.py b/jdaviz/configs/imviz/plugins/__init__.py index 37c02ed392..b62fc0403d 100644 --- a/jdaviz/configs/imviz/plugins/__init__.py +++ b/jdaviz/configs/imviz/plugins/__init__.py @@ -7,4 +7,5 @@ from .compass 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 +from .rotate_image 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..72d001347b --- /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..0236736738 --- /dev/null +++ b/jdaviz/configs/imviz/plugins/rotate_image/rotate_image.py @@ -0,0 +1,72 @@ +import numpy as np +from traitlets import Any + +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 PluginTemplateMixin, DatasetSelectMixin + +__all__ = ['RotateImageSimple'] + + +@tray_registry('imviz-rotate-image', label="Simple Image Rotation") +class RotateImageSimple(PluginTemplateMixin, DatasetSelectMixin): + template_file = __file__, "rotate_image.vue" + + angle = Any(0).tag(sync=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._orig_wcs_key = '_orig_wcs' + 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 + + 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_shape = self.dataset.selected_dc_item.shape + w_in = generate_rotated_wcs(self._theta, shape=w_shape) + + # TODO: How to make this more robust? + # Match with selected data. + 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() + + # FIXME: This does not work -- We did not rotate the data. + # Store the original WCS, if not already. + if self._orig_wcs_key not in self.dataset.selected_dc_item.meta: + self.dataset.selected_dc_item.meta[self._orig_wcs_key] = self.dataset.selected_dc_item.coords # noqa: E501 + + # Update the WCS. + self.dataset.selected_dc_item.coords = w_in + + except Exception as err: # pragma: no cover + self.hub.broadcast(SnackbarMessage( + f"Image rotation failed: {repr(err)}", color='error', sender=self)) + + def vue_reset_image(self, *args, **kwargs): + w_data = self.dataset.selected_dc_item.coords + if w_data is None: # Nothing to do + return + + try: + if self._orig_wcs_key in self.dataset.selected_dc_item.meta: + self.dataset.selected_dc_item.coords = self.dataset.selected_dc_item.meta[self._orig_wcs_key] # noqa: E501 + + except Exception as err: # pragma: no cover + self.hub.broadcast(SnackbarMessage( + f"Image rotation reset failed: {repr(err)}", color='error', sender=self)) 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..12ba71db7c --- /dev/null +++ b/jdaviz/configs/imviz/plugins/rotate_image/rotate_image.vue @@ -0,0 +1,32 @@ + diff --git a/jdaviz/configs/imviz/tests/test_simple_image_rotation.py b/jdaviz/configs/imviz/tests/test_simple_image_rotation.py new file mode 100644 index 0000000000..889b7d27b5 --- /dev/null +++ b/jdaviz/configs/imviz/tests/test_simple_image_rotation.py @@ -0,0 +1,92 @@ +import gwcs +import numpy as np +from astropy import units as u +from astropy.coordinates import ICRS +from astropy.modeling import models +from astropy.nddata import NDData +from astropy.wcs import WCS +from gwcs import coordinate_frames as cf +from numpy.testing import assert_allclose + + +def test_simple_image_rotation_plugin(imviz_helper): + # Mimic interactive mode where viewer is displayed. + imviz_helper.default_viewer.shape = (100, 100) + imviz_helper.default_viewer.state._set_axes_aspect_ratio(1) + + a = np.zeros((10, 8)) + a[0, 0] = 1 # Bright corner for sanity check. + + # Adapted from HST/ACS FITS WCS without the distortion. + w_fits = WCS({'WCSAXES': 2, 'NAXIS1': 8, 'NAXIS2': 10, + 'CRPIX1': 5.0, 'CRPIX2': 5.0, + 'PC1_1': -1.14852e-05, 'PC1_2': 7.01477e-06, + 'PC2_1': 7.75765e-06, 'PC2_2': 1.20927e-05, + 'CDELT1': 1.0, 'CDELT2': 1.0, + 'CUNIT1': 'deg', 'CUNIT2': 'deg', + 'CTYPE1': 'RA---TAN', 'CTYPE2': 'DEC--TAN', + 'CRVAL1': 3.581704851882, 'CRVAL2': -30.39197867265, + 'LONPOLE': 180.0, 'LATPOLE': -30.39197867265, + 'MJDREF': 0.0, 'RADESYS': 'ICRS'}) + + # Adapted from GWCS example. + shift_by_crpix = models.Shift(-(5 - 1) * u.pix) & models.Shift(-(5 - 1) * u.pix) + matrix = np.array([[1.290551569736E-05, 5.9525007864732E-06], + [5.0226382102765E-06, -1.2644844123757E-05]]) + rotation = models.AffineTransformation2D(matrix * u.deg, translation=[0, 0] * u.deg) + rotation.input_units_equivalencies = {"x": u.pixel_scale(1 * (u.deg / u.pix)), + "y": u.pixel_scale(1 * (u.deg / u.pix))} + rotation.inverse = models.AffineTransformation2D(np.linalg.inv(matrix) * u.pix, + translation=[0, 0] * u.pix) + rotation.inverse.input_units_equivalencies = {"x": u.pixel_scale(1 * (u.pix / u.deg)), + "y": u.pixel_scale(1 * (u.pix / u.deg))} + tan = models.Pix2Sky_TAN() + celestial_rotation = models.RotateNative2Celestial( + 3.581704851882 * u.deg, -30.39197867265 * u.deg, 180 * u.deg) + det2sky = shift_by_crpix | rotation | tan | celestial_rotation + det2sky.name = "linear_transform" + detector_frame = cf.Frame2D(name="detector", axes_names=("x", "y"), unit=(u.pix, u.pix)) + sky_frame = cf.CelestialFrame(reference_frame=ICRS(), name='icrs', unit=(u.deg, u.deg)) + pipeline = [(detector_frame, det2sky), (sky_frame, None)] + w_gwcs = gwcs.WCS(pipeline) + + # Load data into Imviz. + imviz_helper.load_data(NDData(a, wcs=w_fits, unit='electron/s'), data_label='fits_wcs') + imviz_helper.load_data(NDData(a, wcs=w_gwcs), data_label='gwcs') + imviz_helper.load_data(a, data_label='no_wcs') + + plg = imviz_helper.plugins['Simple Image Rotation']._obj + plg.plugin_opened = True + + # Select data. + assert plg.dataset.selected == 'fits_wcs[DATA]' + + # Rotate with default settings. + plg.vue_rotate_image() + assert_allclose(plg._theta, 0) + assert plg.dataset.labels == ['fits_wcs[DATA]', 'gwcs[DATA]', 'no_wcs'] + + # The zoom box is now a rotated rombus. + # no_wcs is the same as fits_wcs because they are linked by pixel, but gwcs is different. + fits_wcs_zoom_limits = imviz_helper.default_viewer._get_zoom_limits( + imviz_helper.app.data_collection['fits_wcs[DATA]']) + assert_allclose(fits_wcs_zoom_limits, ((-0.37873422, 5.62910616), (3.42315751, 11.85389963), + (13.52329142, 5.37451188), (9.72139968, -0.85028158))) + assert_allclose(fits_wcs_zoom_limits, imviz_helper.default_viewer._get_zoom_limits( + imviz_helper.app.data_collection['no_wcs'])) + assert_allclose(imviz_helper.default_viewer._get_zoom_limits('gwcs[DATA]'), ( + (7.60196643, 6.55912761), (10.83179746, -0.44341285), + (0.25848145, -4.64322363), (-2.97134959, 2.35931683))) + + # TODO: Test the plugin. + # 2. Try positive angle + # 3. Add another viewer. + # 4. Try negative angle on second viewer. + # 5. Cross-test with Compass/Zoom-box, Line Profile, Coord Info? + # 6. Untoggle and check state. + # 7. Retoggle and check state. + # 8. Make sure we also cover engine code not mentioned above but changed in the PR. + + # Second test function: Load just data without WCS, toggle on, should be no-op. + + # Also test function to make sure invalid angle does not crash plugin. diff --git a/jdaviz/configs/imviz/tests/test_wcs_utils.py b/jdaviz/configs/imviz/tests/test_wcs_utils.py index bff80391aa..08c24a1449 100644 --- a/jdaviz/configs/imviz/tests/test_wcs_utils.py +++ b/jdaviz/configs/imviz/tests/test_wcs_utils.py @@ -1,5 +1,6 @@ import gwcs import numpy as np +import pytest from astropy import units as u from astropy.coordinates import ICRS from astropy.modeling import models @@ -104,3 +105,10 @@ def test_simple_gwcs(): 1262.0057201165127, 606.2863901330095, 155.2870478938214, -86.89813081941797)) assert not result[-1] + + +@pytest.mark.parametrize(('angle', 'ans'), [(0, 0), (45, 45), (-45, -45), (360, 0)]) +def test_rotate_wcs(angle, ans): + w = wcs_utils.generate_rotated_wcs(angle) + degn = wcs_utils.get_compass_info(w, (2, 2))[6] + assert_allclose(degn, ans, atol=1e-7) diff --git a/jdaviz/configs/imviz/wcs_utils.py b/jdaviz/configs/imviz/wcs_utils.py index fb671018bb..8443b81047 100644 --- a/jdaviz/configs/imviz/wcs_utils.py +++ b/jdaviz/configs/imviz/wcs_utils.py @@ -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 @@ -11,10 +8,55 @@ import numpy as np from astropy import units as u from astropy.coordinates import SkyCoord +from astropy.wcs import WCS from matplotlib.patches import Polygon -__all__ = ['get_compass_info', 'draw_compass_mpl'] +__all__ = ['generate_rotated_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 generate_rotated_wcs(theta, shape=(2, 2)): + """Create a FITS WCS with N-up, E-left, and the desired rotation. + This does not take distortions into account. + + Parameters + ---------- + 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 (positive is clockwise). + shape : tuple of int + Image shape that the WCS corresponds to in the form of ``(ny, nx)``. + + 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 = WCS({'CTYPE1': 'RA---TAN', 'CTYPE2': 'DEC--TAN', + 'CUNIT1': 'deg', 'CUNIT2': 'deg', + 'CDELT1': -1, 'CDELT2': 1, + 'CRPIX1': 1, 'CRPIX2': 1, + 'NAXIS1': shape[1], 'NAXIS2': shape[0]}) + + if not np.allclose(theta, 0): + rho = math.radians(theta) + sin_rho = math.sin(rho) + cos_rho = math.cos(rho) + w_out.wcs.pc = np.array([[cos_rho, -sin_rho], + [sin_rho, cos_rho]]) + w_out.wcs.set() + return w_out + + +# This is 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): """ diff --git a/licenses/MPDAF_LICENSE.txt b/licenses/MPDAF_LICENSE.txt new file mode 100644 index 0000000000..7ec136943e --- /dev/null +++ b/licenses/MPDAF_LICENSE.txt @@ -0,0 +1,28 @@ +Copyright (c) 2010-2019 CNRS / Centre de Recherche Astrophysique de Lyon + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/notebooks/concepts/imviz_link_north_up_east_left.ipynb b/notebooks/concepts/imviz_link_north_up_east_left.ipynb new file mode 100644 index 0000000000..f25846d04d --- /dev/null +++ b/notebooks/concepts/imviz_link_north_up_east_left.ipynb @@ -0,0 +1,470 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "206e73ce", + "metadata": {}, + "outputs": [], + "source": [ + "import gwcs\n", + "import numpy as np\n", + "from astropy import units as u\n", + "from astropy.coordinates import ICRS\n", + "from astropy.io import fits\n", + "from astropy.modeling import models\n", + "from astropy.nddata import NDData\n", + "from astropy.wcs import WCS\n", + "from gwcs import coordinate_frames as cf\n", + "\n", + "from jdaviz import Imviz\n", + "from jdaviz.configs.imviz.wcs_utils import generate_rotated_wcs\n", + "\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "id": "055fce4f", + "metadata": {}, + "source": [ + "# Plain array" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02a3f110", + "metadata": {}, + "outputs": [], + "source": [ + "a = np.random.random((2048, 4096))\n", + "a[:100, :400] = 1 # Bright corner for sanity check" + ] + }, + { + "cell_type": "markdown", + "id": "6f0e93c0", + "metadata": {}, + "source": [ + "# Data with FITS WCS and unit" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e816343", + "metadata": {}, + "outputs": [], + "source": [ + "w = WCS({'WCSAXES': 2,\n", + " 'CRPIX1': 2100.0,\n", + " 'CRPIX2': 1024.0,\n", + " 'PC1_1': -1.14852e-05,\n", + " 'PC1_2': 7.01477e-06,\n", + " 'PC2_1': 7.75765e-06,\n", + " 'PC2_2': 1.20927e-05,\n", + " 'CDELT1': 1.0,\n", + " 'CDELT2': 1.0,\n", + " 'CUNIT1': 'deg',\n", + " 'CUNIT2': 'deg',\n", + " 'CTYPE1': 'RA---TAN',\n", + " 'CTYPE2': 'DEC--TAN',\n", + " 'CRVAL1': 3.581704851882,\n", + " 'CRVAL2': -30.39197867265,\n", + " 'LONPOLE': 180.0,\n", + " 'LATPOLE': -30.39197867265,\n", + " 'MJDREF': 0.0,\n", + " 'RADESYS': 'ICRS'})\n", + "hdu = fits.ImageHDU(a, name='SCI')\n", + "hdu.header.update(w.to_header())\n", + "hdu.header['BUNIT'] = 'electron/s'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a69e6b9", + "metadata": {}, + "outputs": [], + "source": [ + "w.to_header()" + ] + }, + { + "cell_type": "markdown", + "id": "d7e31be2", + "metadata": {}, + "source": [ + "# Data with GWCS (no unit)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4ddf0239", + "metadata": {}, + "outputs": [], + "source": [ + "shift_by_crpix = models.Shift(-(2048 - 1) * u.pix) & models.Shift(-(1024 - 1) * u.pix)\n", + "matrix = np.array([[1.290551569736E-05, 5.9525007864732E-06],\n", + " [5.0226382102765E-06, -1.2644844123757E-05]])\n", + "rotation = models.AffineTransformation2D(matrix * u.deg, translation=[0, 0] * u.deg)\n", + "rotation.input_units_equivalencies = {\"x\": u.pixel_scale(1 * (u.deg / u.pix)),\n", + " \"y\": u.pixel_scale(1 * (u.deg / u.pix))}\n", + "rotation.inverse = models.AffineTransformation2D(np.linalg.inv(matrix) * u.pix,\n", + " translation=[0, 0] * u.pix)\n", + "rotation.inverse.input_units_equivalencies = {\"x\": u.pixel_scale(1 * (u.pix / u.deg)),\n", + " \"y\": u.pixel_scale(1 * (u.pix / u.deg))}\n", + "tan = models.Pix2Sky_TAN()\n", + "celestial_rotation = models.RotateNative2Celestial(\n", + " 3.581704851882 * u.deg, -30.39197867265 * u.deg, 180 * u.deg)\n", + "det2sky = shift_by_crpix | rotation | tan | celestial_rotation\n", + "det2sky.name = \"linear_transform\"\n", + "detector_frame = cf.Frame2D(name=\"detector\", axes_names=(\"x\", \"y\"), unit=(u.pix, u.pix))\n", + "sky_frame = cf.CelestialFrame(reference_frame=ICRS(), name='icrs', unit=(u.deg, u.deg))\n", + "pipeline = [(detector_frame, det2sky), (sky_frame, None)]\n", + "w2 = gwcs.WCS(pipeline)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ab762bf", + "metadata": {}, + "outputs": [], + "source": [ + "ndd_gwcs = NDData(a, wcs=w2)" + ] + }, + { + "cell_type": "markdown", + "id": "fef5b5f4", + "metadata": {}, + "source": [ + "This next cell does not work, see https://github.com/spacetelescope/gwcs/issues/408" + ] + }, + { + "cell_type": "raw", + "id": "30351eca", + "metadata": {}, + "source": [ + "w2.to_fits(bounding_box=([0, 100] * u.pix, [0, 100] * u.pix))" + ] + }, + { + "cell_type": "markdown", + "id": "046ace36", + "metadata": {}, + "source": [ + "# Show them in Imviz" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93fcdb1b", + "metadata": {}, + "outputs": [], + "source": [ + "imviz = Imviz(verbosity='warning')" + ] + }, + { + "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_gwcs, data_label='gwcs')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a09eb6e-5a98-43b9-8e88-3c7879ac600c", + "metadata": {}, + "outputs": [], + "source": [ + "imviz.load_data(a, data_label='no_wcs')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "022cc29f", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "No such comm: d9288537ace447c6bb4895aa419c9f87\n", + "No such comm: d9288537ace447c6bb4895aa419c9f87\n", + "No such comm: d9288537ace447c6bb4895aa419c9f87\n", + "No such comm: d9288537ace447c6bb4895aa419c9f87\n" + ] + } + ], + "source": [ + "imviz.app" + ] + }, + { + "cell_type": "markdown", + "id": "21c20ee4", + "metadata": {}, + "source": [ + "# Back to basics\n", + "\n", + "This shows the low-level function to produce a WCS with desired rotation angle." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48424037", + "metadata": {}, + "outputs": [], + "source": [ + "from jdaviz.configs.imviz.wcs_utils import draw_compass_mpl" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "acac6156", + "metadata": {}, + "outputs": [], + "source": [ + "aa = np.random.random((2, 2))\n", + "w1 = generate_rotated_wcs(0)\n", + "draw_compass_mpl(aa, wcs=w1);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf2da96b", + "metadata": {}, + "outputs": [], + "source": [ + "w2 = generate_rotated_wcs(-90)\n", + "draw_compass_mpl(aa, wcs=w2);" + ] + }, + { + "cell_type": "markdown", + "id": "1fa8e146", + "metadata": {}, + "source": [ + "# Testing the unit tests\n", + "\n", + "This setup was used to build unit tests for the new plugin in https://github.com/spacetelescope/jdaviz/pull/1340 ." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37bccf94", + "metadata": {}, + "outputs": [], + "source": [ + "imviz_helper = Imviz(verbosity='warning')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc078718", + "metadata": {}, + "outputs": [], + "source": [ + "# Makes the display ugly but used for testing.\n", + "imviz_helper.default_viewer.shape = (100, 100)\n", + "imviz_helper.default_viewer.state._set_axes_aspect_ratio(1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4d54f624", + "metadata": {}, + "outputs": [], + "source": [ + "a = np.random.random((10, 8)) # All zeroes in test but we need to see here.\n", + "a[0, 0] = 1 # Bright corner for sanity check." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b78c484", + "metadata": {}, + "outputs": [], + "source": [ + "# Adapted from HST/ACS FITS WCS without the distortion.\n", + "w_fits = WCS({'WCSAXES': 2, 'NAXIS1': 8, 'NAXIS2': 10,\n", + " 'CRPIX1': 5.0, 'CRPIX2': 5.0,\n", + " 'PC1_1': -1.14852e-05, 'PC1_2': 7.01477e-06,\n", + " 'PC2_1': 7.75765e-06, 'PC2_2': 1.20927e-05,\n", + " 'CDELT1': 1.0, 'CDELT2': 1.0,\n", + " 'CUNIT1': 'deg', 'CUNIT2': 'deg',\n", + " 'CTYPE1': 'RA---TAN', 'CTYPE2': 'DEC--TAN',\n", + " 'CRVAL1': 3.581704851882, 'CRVAL2': -30.39197867265,\n", + " 'LONPOLE': 180.0, 'LATPOLE': -30.39197867265,\n", + " 'MJDREF': 0.0, 'RADESYS': 'ICRS'})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0a1f44e5", + "metadata": {}, + "outputs": [], + "source": [ + "# Adapted from GWCS example.\n", + "shift_by_crpix = models.Shift(-(5 - 1) * u.pix) & models.Shift(-(5 - 1) * u.pix)\n", + "matrix = np.array([[1.290551569736E-05, 5.9525007864732E-06],\n", + " [5.0226382102765E-06, -1.2644844123757E-05]])\n", + "rotation = models.AffineTransformation2D(matrix * u.deg, translation=[0, 0] * u.deg)\n", + "rotation.input_units_equivalencies = {\"x\": u.pixel_scale(1 * (u.deg / u.pix)),\n", + " \"y\": u.pixel_scale(1 * (u.deg / u.pix))}\n", + "rotation.inverse = models.AffineTransformation2D(np.linalg.inv(matrix) * u.pix,\n", + " translation=[0, 0] * u.pix)\n", + "rotation.inverse.input_units_equivalencies = {\"x\": u.pixel_scale(1 * (u.pix / u.deg)),\n", + " \"y\": u.pixel_scale(1 * (u.pix / u.deg))}\n", + "tan = models.Pix2Sky_TAN()\n", + "celestial_rotation = models.RotateNative2Celestial(\n", + " 3.581704851882 * u.deg, -30.39197867265 * u.deg, 180 * u.deg)\n", + "det2sky = shift_by_crpix | rotation | tan | celestial_rotation\n", + "det2sky.name = \"linear_transform\"\n", + "detector_frame = cf.Frame2D(name=\"detector\", axes_names=(\"x\", \"y\"), unit=(u.pix, u.pix))\n", + "sky_frame = cf.CelestialFrame(reference_frame=ICRS(), name='icrs', unit=(u.deg, u.deg))\n", + "pipeline = [(detector_frame, det2sky), (sky_frame, None)]\n", + "w_gwcs = gwcs.WCS(pipeline)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f289953a", + "metadata": {}, + "outputs": [], + "source": [ + "# Load data into Imviz.\n", + "imviz_helper.load_data(NDData(a, wcs=w_fits, unit='electron/s'), data_label='fits_wcs')\n", + "imviz_helper.load_data(NDData(a, wcs=w_gwcs), data_label='gwcs')\n", + "imviz_helper.load_data(a, data_label='no_wcs')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4c7e9fb", + "metadata": {}, + "outputs": [], + "source": [ + "# This makes it interactive: Not in test but we need to see here.\n", + "imviz_helper.app" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a5419b9", + "metadata": {}, + "outputs": [], + "source": [ + "plg = imviz_helper.app.get_tray_item_from_name('imviz-rotate-image')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c88d8259", + "metadata": {}, + "outputs": [], + "source": [ + "# Also check the coordinates display: Last loaded is on top.\n", + "\n", + "imviz_helper.default_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 0, 'y': 0}})\n", + "print(imviz_helper.default_viewer.label_mouseover.pixel) # 'x=00.0 y=00.0'\n", + "print(imviz_helper.default_viewer.label_mouseover.value) # 1\n", + "print(imviz_helper.default_viewer.label_mouseover.world_ra_deg) # ''\n", + "print(imviz_helper.default_viewer.label_mouseover.world_dec_deg) # ''" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cefcea98", + "metadata": {}, + "outputs": [], + "source": [ + "imviz_helper.default_viewer._get_zoom_limits(imviz_helper.app.data_collection['no_wcs'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b3be7af7", + "metadata": {}, + "outputs": [], + "source": [ + "imviz_helper.default_viewer._get_zoom_limits(imviz_helper.app.data_collection['fits_wcs[DATA]'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e8b7636c", + "metadata": {}, + "outputs": [], + "source": [ + "imviz_helper.default_viewer._get_zoom_limits(imviz_helper.app.data_collection['gwcs[DATA]'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15f7877a", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}