Source code for spaceKLIP.wcs_utils

# This is adapted from Ginga (ginga.util.wcs, ginga.trcalc, and ginga.Bindings.ImageViewBindings).
# Please see the file licenses/GINGA_LICENSE.txt for details.
# Then copied from jdaviz to spaceklip.
# Below is the license copied from jdaviz.

"""
Copyright 2020-2023 JDADF Developers

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
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.
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.
"""

"""This module handles calculations based on world coordinate system (WCS). Copies from JDAVIZ"""

import base64
import math
from io import BytesIO

import matplotlib.pyplot as plt
import numpy as np
from astropy import units as u
from astropy.coordinates import SkyCoord
from matplotlib.patches import Polygon

__all__ = ['get_compass_info', 'draw_compass_mpl']


def rotate_pt(x_arr, y_arr, theta_deg, xoff=0, yoff=0):
    """
    Rotate an array of points (x_arr, y_arr) by theta_deg offsetted
    from a center point by (xoff, yoff).
    """
    a_arr = x_arr - xoff
    b_arr = y_arr - yoff
    cos_t = np.cos(np.radians(theta_deg))
    sin_t = np.sin(np.radians(theta_deg))
    ap = (a_arr * cos_t) - (b_arr * sin_t)
    bp = (a_arr * sin_t) + (b_arr * cos_t)
    return np.asarray((ap + xoff, bp + yoff))


def add_offset_radec(ra_deg, dec_deg, delta_deg_ra, delta_deg_dec):
    """
    Algorithm to compute RA/Dec from RA/Dec base position plus tangent
    plane offsets.
    """
    # To radians
    x = math.radians(delta_deg_ra)
    y = math.radians(delta_deg_dec)
    raz = math.radians(ra_deg)
    decz = math.radians(dec_deg)

    sdecz = math.sin(decz)
    cdecz = math.cos(decz)

    d = cdecz - y * sdecz

    ra2 = math.atan2(x, d) + raz
    # Normalize ra into the range 0 to 2*pi
    twopi = math.pi * 2
    ra2 = math.fmod(ra2, twopi)
    if ra2 < 0.0:
        ra2 += twopi
    dec2 = math.atan2(sdecz + y * cdecz, math.sqrt(x * x + d * d))

    # back to degrees
    ra2_deg = math.degrees(ra2)
    dec2_deg = math.degrees(dec2)

    return (ra2_deg, dec2_deg)


def add_offset_xy(image_wcs, x, y, delta_deg_x, delta_deg_y):
    # calculate ra/dec of x,y pixel
    c = image_wcs.pixel_to_world(x, y)
    if isinstance(c, SkyCoord):
        ra_deg = c.ra.deg
        dec_deg = c.dec.deg
    else:  # list of Quantity (e.g., from FITS primary header)
        ra_deg = c[0].value
        dec_deg = c[1].value

    # add offsets
    ra2_deg, dec2_deg = add_offset_radec(ra_deg, dec_deg, delta_deg_x, delta_deg_y)

    # then back to new pixel coords
    return image_wcs.world_to_pixel_values(ra2_deg, dec2_deg)  # x2, y2


def calc_compass(image_wcs, x, y, len_deg_e, len_deg_n):

    # Get east and north coordinates
    xe, ye = list(map(float, add_offset_xy(image_wcs, x, y, len_deg_e, 0.0)))
    xn, yn = list(map(float, add_offset_xy(image_wcs, x, y, 0.0, len_deg_n)))

    return (x, y, xn, yn, xe, ye)


def calc_compass_radius(image_wcs, x, y, radius_px):
    xe, ye = add_offset_xy(image_wcs, x, y, 1.0, 0.0)
    xn, yn = add_offset_xy(image_wcs, x, y, 0.0, 1.0)

    # now calculate the length in pixels of those arcs
    # (planar geometry is good enough here)
    px_per_deg_e = math.sqrt(math.fabs(ye - y) ** 2 + math.fabs(xe - x) ** 2)
    px_per_deg_n = math.sqrt(math.fabs(yn - y) ** 2 + math.fabs(xn - x) ** 2)

    # now calculate the arm length in degrees for each arm
    # (this produces same-length arms)
    len_deg_e = radius_px / px_per_deg_e
    len_deg_n = radius_px / px_per_deg_n

    return calc_compass(image_wcs, x, y, len_deg_e, len_deg_n)


def calc_compass_center(image_wcs, image_shape, r_fac=0.5):
    # calculate center of data
    x = image_shape[1] * 0.5
    y = image_shape[0] * 0.5

    # radius we want the arms to be
    radius_px = min(image_shape) * r_fac

    return calc_compass_radius(image_wcs, x, y, radius_px)


[docs] def get_compass_info(image_wcs, image_shape, r_fac=0.4): """Calculate WCS compass parameters. North (N) is up and East (E) is left. Parameters ---------- image_wcs : obj WCS that is compatible with APE 14. image_shape : tuple of int Shape of the image in the form of ``(ny, nx)``. r_fac : float Scale factor for compass arrow length. Returns ------- x, y : float Pixel positions for the center of the compass. xn, yn : float Pixel positions for N of the compass. xe, ye : float Pixel positions for E of the compass. degn, dege : float Rotation angles for N and E, in degrees, for the compass, respectively. xflip : bool Should display flip on X? """ x, y, xn, yn, xe, ye = calc_compass_center(image_wcs, image_shape, r_fac=r_fac) degn = math.degrees(math.atan2(xn - x, yn - y)) # rotate east point also by degn xe2, ye2 = rotate_pt(xe, ye, degn, xoff=x, yoff=y) dege = math.degrees(math.atan2(xe2 - x, ye2 - y)) # if right-hand image, flip it to make left hand xflip = False if dege > 0.0: xflip = not xflip if xflip and not np.isclose(degn, 0): degn = -degn return x, y, xn, yn, xe, ye, degn, dege, xflip
[docs] def draw_compass_mpl(image, orig_shape=None, wcs=None, show=True, zoom_limits=None, **kwargs): """Visualize the compass using Matplotlib. Parameters ---------- image : ndarray 2D Numpy array (can be resampled). orig_shape : tuple of int or `None` The original (non-resampled) array shape in ``(ny, nx)``, if different. wcs : obj or `None` Associated original image WCS that is compatible with APE 14. If `None` given, compass is not drawn. show : bool Display the plot. zoom_limits : ndarray or None If not `None`, also draw a rectangle to represent the current zoom limits in the form of list of ``(x, y)`` representing the four corners of the zoom box. kwargs : dict Keywords for ``matplotlib.pyplot.imshow``. Returns ------- image_base64 : str Decoded buffer for Compass plugin. """ if orig_shape is None: orig_shape = image.shape if not show: plt.ioff() fig, ax = plt.subplots() ax.imshow(image, extent=[-0.5, orig_shape[1] - 0.5, -0.5, orig_shape[0] - 0.5], origin='lower', cmap='gray', **kwargs) if wcs is not None: try: x, y, xn, yn, xe, ye, degn, dege, xflip = get_compass_info(wcs, orig_shape) except Exception: wcs = None else: # TODO: Not sure what xflip really do, ask Eric Jeschke later. # if xflip: # plt.imshow(np.fliplr(image), origin='lower') # Positive here is counter-clockwise, hence the minus sign in comment. ax.plot(x, y, marker='o', color='cyan', markersize=5) ax.annotate('N', xy=(x, y), xytext=(xn, yn), arrowprops={'arrowstyle': '<-', 'color': 'cyan', 'lw': 1.5}, color='cyan', fontsize=16, va='center', ha='center') # rotation=-degn ax.annotate('E', xy=(x, y), xytext=(xe, ye), arrowprops={'arrowstyle': '<-', 'color': 'cyan', 'lw': 1.5}, color='cyan', fontsize=16, va='center', ha='center') # rotation=-dege if wcs is None: x = orig_shape[1] * 0.5 y = orig_shape[0] * 0.5 ax.plot(x, y, marker='o', color='yellow', markersize=5) # Also draw X/Y compass. r_xy = float(min(orig_shape)) * 0.25 ax.annotate('X', xy=(x, y), xytext=(x + r_xy, y), arrowprops={'arrowstyle': '<-', 'color': 'yellow', 'lw': 1.5}, color='yellow', fontsize=16, va='center', ha='center') ax.annotate('Y', xy=(x, y), xytext=(x, y + r_xy), arrowprops={'arrowstyle': '<-', 'color': 'yellow', 'lw': 1.5}, color='yellow', fontsize=16, va='center', ha='center') if zoom_limits is not None: ax.add_patch(Polygon( zoom_limits, closed=True, linewidth=1.5, edgecolor='r', facecolor='none')) if show: plt.draw() plt.show() buff = BytesIO() plt.savefig(buff) plt.style.use('default') plt.close() return base64.b64encode(buff.getvalue()).decode('utf-8')
def data_outside_gwcs_bounding_box(data, x, y): """This is for internal use by Imviz coordinates transformation only.""" outside_bounding_box = False if hasattr(data.coords, '_orig_bounding_box'): # then coords is a GWCS object and had its bounding box cleared # by the Imviz parser ints = data.coords._orig_bounding_box.intervals if isinstance(ints[0].lower, u.Quantity): bb_xmin = ints[0].lower.value bb_xmax = ints[0].upper.value bb_ymin = ints[1].lower.value bb_ymax = ints[1].upper.value else: # pragma: no cover bb_xmin = ints[0].lower bb_xmax = ints[0].upper bb_ymin = ints[1].lower bb_ymax = ints[1].upper if not (bb_xmin <= x <= bb_xmax and bb_ymin <= y <= bb_ymax): outside_bounding_box = True # Has to be Python bool, not Numpy bool_ return outside_bounding_box