Source code for giant.utilities.wcs
"""
This module provides functions for ingesting WCS header data into a format GIANT can understand.
"""
from typing import cast
import numpy as np
from astropy.wcs import WCS
from astropy.coordinates import SkyCoord, CartesianRepresentation
from astropy.units import Quantity
from giant.rotations import Rotation
from giant._typing import ARRAY_LIKE
from giant.calibration.estimators import IterativeNonlinearLSTSQ
from giant.camera_models import CameraModel
[docs]
def get_wcs_orientation(input: WCS, boresight_pixel: ARRAY_LIKE) -> Rotation:
r"""
This function converts an astropy WCS object into a GIANT Rotation object.
The conversion happens using astropy's pixel_to_world method of the wcs object to
define a 2 vector frame to account for various projections and coordinate systems
used in astronomical imaging.
If you have a working wcs defined in a fits file you could do something like:
>>> from giant.utilities.wcs import get_wcs_orientation
>>> from astropy.io import fits
>>> from astropy.wcs
>>> with fits.open("my_file.fits") as fits_file:
... wcs_l = WCS(fits_file[0])
>>> rotation = get_wcs_orientation(wcs_l)
>>> print(rotation)
Rotation([...])
:param input: The astropy WCS object to be converted
:return: A Rotation object representing the rotation from the inertial to the camera
frame defined by the WCS
"""
# boresight
boresight_pixel = np.array(boresight_pixel).ravel()
z_sc = cast(SkyCoord, input.pixel_to_world(*boresight_pixel))
z_dir: np.ndarray = cast(Quantity, cast(CartesianRepresentation, z_sc.cartesian).get_xyz()).value
x_const_sc = cast(SkyCoord, input.pixel_to_world(boresight_pixel[0]+1, boresight_pixel[1]))
x_const: np.ndarray = cast(Quantity, cast(CartesianRepresentation, x_const_sc.cartesian).get_xyz()).value
y_dir = np.cross(z_dir, x_const)
y_dir /= np.linalg.norm(y_dir)
x_dir = np.cross(y_dir, z_dir)
x_dir /= np.linalg.norm(x_dir)
return Rotation([x_dir, y_dir, z_dir])
[docs]
def get_wcs_model(input: WCS, camera_model_guess: CameraModel, sample_step: int = 100, rotation_world_to_camera: Rotation | None = None) -> CameraModel:
"""
This function creates a giant :ref:`.CameraModel` that mimics the input WCS object.
We do this by doing a recalibration using the output of the WCS model to feed as an input into the calibration process.
Essentially, we sample across the detector, use the WCS to convert each sample into a unit vector in the world frame,
rotate the unit vectors in the world frame into the camera frame, and then do the calibration process using these vectors
and the original pixels. The sampling is done in a grid between (0, 0) and (camera_model_guess.n_cols, camera_model_guess.n_rows)
with a step size of sample_step.
The camera_model_guess should be a subclass instance of the :ref:`.CameraModel` class for the camera model you would like to use
which is seeded with an initial guess (that at least can do a projection to a meaningful value, even if its wrong). Additionally,
this instance should have the :ref:`.CameraModel.estimation_parameters` set correctly for what parameters you would like estimated.
The same rules apply as for a normal calibration.
If you have already extracted the rotation between the world and camera frame from the wcs object you can provide it in the
optional rotation_world_to_camera frame, otherwise it will be extracted from the WCS using the :ref:`get_wcs_orientation` function
from this module assuming the boresight pixel is the center of the image
:param input: The WCS to fit the model to
:param camera_model_guess: the initial guess for the camera model/type of camera model to be extracted
:param sample_step: the number of pixels between each sample
"""
# get the rotation from the wcs if not provided
if rotation_world_to_camera is None:
rotation_world_to_camera = get_wcs_orientation(input, [(camera_model_guess.n_cols-1)/2, (camera_model_guess.n_rows-1)/2])
# create a grid of pixels to sample
cols, rows = np.meshgrid(np.arange(0, camera_model_guess.n_cols, sample_step),
np.arange(0, camera_model_guess.n_rows, sample_step))
# use the wcs to get the corresponding unit vectors for each pixel in the camera frame
world_coordinates = input.pixel_to_world(cols.ravel(), rows.ravel())
world_units = np.array([w.cartesian.get_xyz().value for w in world_coordinates]).T
camera_units = rotation_world_to_camera.matrix @ world_units
# set up the calibration estimator
estimator = IterativeNonlinearLSTSQ(camera_model_guess.copy(), measurements=np.array([cols.ravel(), rows.ravel()]),
camera_frame_directions=[camera_units], temperatures=[0],)
# do the estimation
estimator.estimate()
# reocompute the field of view
estimator.model.field_of_view = None
# return the result
return estimator.model