import astropy.units as u
import copy
import numpy as np
__all__ = [
'fits_to_python',
'python_to_fits',
'world_to_pixel',
'pixel_to_world',
'angle_to_pixels',
'pixels_to_angle',
]
[docs]
def fits_to_python(
points):
"""
Simple function to convert one or more points from FITS coordinates to
python coordinates (numpy and matplotlib image).
Parameters
----------
points : `~numpy.ndarray`, `tuple`, or `list`.
Input Points.
Returns
-------
python_points : `~numpy.ndarray`
Points converted to python coordinates.
"""
if (isinstance(points, np.ndarray) or
isinstance(points, tuple) or
isinstance(points, list)):
fits_points = np.array(points)
else:
raise ValueError(f'Invalid type for points: {type(points)}.')
# Check if input parameter is one point or an array with the last
# dimension being 2 = array of points.
if ((isinstance(fits_points, np.ndarray) and fits_points.shape[-1] == 2) or
(isinstance(fits_points, int) and fits_points.shape == 2)):
python_points = fits_points - 1
else:
raise ValueError(f'Invalid shape: {fits_points.shape}. '
+ 'Last dimension has to be 2: (..., 2).')
return python_points
[docs]
def python_to_fits(
points):
"""
Simple function to convert one or more points from python coordinates
(numpy and matplotlib image) to FITS coordinates.
Parameters
----------
points : `~numpy.ndarray`, `tuple`, or `list`.
Input points.
Returns
-------
fits_points : `~numpy.ndarray`
Points converted to fits coordinates.
"""
if (isinstance(points, np.ndarray) or
isinstance(points, tuple) or
isinstance(points, list)):
python_points = np.array(points)
else:
raise ValueError(f'Invalid type for points: {type(points)}.')
# Check if input parameter is one point or an array with the last
# dimension being 2 = array of points.
if ((isinstance(python_points, np.ndarray) and python_points.shape[-1] == 2) or
(isinstance(python_points, int) and python_points.shape == 2)):
fits_points = python_points + 1
else:
raise ValueError(f'Invalid shape: {python_points.shape}. '
+ 'Last dimension has to be 2: (..., 2).')
return fits_points
[docs]
def world_to_pixel(
points,
fitsmap,
origin=0,
log=False):
"""
Convert a set of points in world coordinates to pixel coordinates (python
or FITS).
Parameters
----------
points : `~astropy.units.Quantity` or `numpy.ndarray` or `tuple` or `list`.
Input points.
fitsmap : `~madcubapy.io.MadcubaMap` or `~astropy.nddata.CCDData`.
Map object used to get WCS data.
origin : `int`, optional, default=0
Starting coordinate for image data: 0 for python pixel coordinates,
1 for FITS coordinates.
log : `bool`, optional
If True, print on screen information messages on how the data is
parsed.
Returns
-------
pixel_points : `~numpy.ndarray`
Points converted to pixel coordinates.
"""
# Create a deepcopy because for a list of lists [:] and copy() fails
pts = copy.deepcopy(points)
# If it is a quantity, continue the function to step 2 with correct units
if isinstance(pts, u.Quantity):
unit = pts.unit
if unit == u.pix:
if log: print("Input units are pixels, returning pixels.")
return pts.value
elif (unit == u.rad or unit == u.deg or
unit == u.arcmin or unit == u.arcsec):
pts = pts.to(u.deg)
else:
raise ValueError(
f'"Only "{u.pix}" or angle units are allowed: "{u.rad}", '
+ f'""{u.deg}", "{u.arcmin}", or "{u.arcsec}"'
)
# If it is a list first check if the list has a point (quantities or
# numbers), or a list of points (arrays, or other lists or tuples)
elif isinstance(pts, list) or isinstance(pts, tuple):
# Convert tuple to list
if isinstance(pts, tuple):
pts = list(pts)
# Raise error for a mix of types in list
if not all(isinstance(elem, type(pts[0])) for elem in pts):
raise ValueError(f'A list of different types is not supported.')
# List of quantities
elif all(isinstance(elem, u.Quantity) for elem in pts):
# Return pixels
if all((elem.unit == u.pix) for elem in pts):
if log: print("Input units are pixels, returning pixels.")
for j in range(len(pts)):
pts[j] = pts[j].value
pts = pts * u.pix
# Angular units
elif all((elem.unit != u.pix) for elem in pts):
raise_error = False
for elem in pts:
unit = elem.unit
if (unit != u.rad and unit != u.deg and
unit != u.arcmin and unit != u.arcsec):
raise_error = True
# Raise an error if at least one unit is not angular
if raise_error:
raise ValueError(
f'"Only "{u.pix}" or angle units are allowed: '
+ f'"{u.rad}", ""{u.deg}", "{u.arcmin}", or "{u.arcsec}"'
)
# Convert angular units to degrees
for j in range(len(pts)):
pts[j] = pts[j].to(u.deg).value
pts = pts * u.deg
# Raise an error if there is a mix with pixels and other units
else:
raise ValueError(f'"Cannot mix "{u.pix}" with other units')
# List of arrays
elif all(isinstance(elem, np.ndarray) for elem in pts):
if log: print("Input point is a list of a arrays without units")
if log: print("Defaulting to degrees")
pts = np.array(pts) * u.deg
# List of lists (several points)
elif (all(isinstance(elem, list) for elem in pts) or
all(isinstance(elem, tuple) for elem in pts)):
# Convert tuples to lists
if all(isinstance(elem, tuple) for elem in pts):
pts = [list(i) for i in pts]
# Check if dimensions in list are coherent (same dimensions on
# each sublist)
num = 0
lengths_list = []
for i in range(len(pts)):
num = len(pts[i])
lengths_list.append(num)
# If dimensions are coherent continue
if len(np.unique(lengths_list)) == 1:
# Raise error for a mix of types in list
if not all(isinstance(elem, type(pts[0][0]))
for point in pts for elem in point):
raise ValueError(
f'A list of different types is not supported.'
)
# List of lists of quantities
elif all(isinstance(elem, u.Quantity)
for point in pts for elem in point):
# Return pixels
if all((elem.unit == u.pix)
for point in pts for elem in point):
if log:
print(f'Input unit is "{u.pix}", returning pixels.')
for j in range(len(pts)):
for k in range(len(pts[j])):
pts[j][k] = pts[j][k].value
pts = pts * u.pix
# Angular units
elif all((elem.unit != u.pix)
for point in pts for elem in point):
raise_error = False
for point in pts:
for elem in point:
unit = elem.unit
if unit != u.rad and unit != u.deg \
and unit != u.arcmin and unit != u.arcsec:
raise_error = True
# Raise an error if at least one unit is not angular
if raise_error:
raise ValueError(
f'"Only "{u.pix}" or angle units are '
+ f'allowed: "{u.rad}", ""{u.deg}", '
+ f'"{u.arcmin}", or "{u.arcsec}"'
)
# Convert angular units to degrees
for j in range(len(pts)):
for k in range(len(pts[j])):
pts[j][k] = pts[j][k].to(u.deg).value
pts = pts * u.deg
# Raise an error if there is a mix with pixels and other units
else:
raise ValueError(
f'"Cannot mix "{u.pix}" with other units'
)
# List of lists of numbers
elif all((isinstance(elem, int) or isinstance(elem, float))
for point in pts for elem in point):
if log: print("Input is a list of lists of non-quantities")
if log: print("Defaulting to degrees")
pts = pts * u.deg
# Raise error for further lists
elif all(isinstance(elem, list)
for point in pts for elem in point):
raise ValueError(
f'Input is a list of lists of lists. '
+ f'At most it can be a list of lists to represent '
+ f'a series of points.'
)
# Raise error for invalid types
else:
raise ValueError(
f'Input is a list of lists with an invalid type: '
+ f'{type(pts[0][0])}. \n'
+ f'Supported types inside a nested sublist are: \n'
+ f'{u.Quantity}, {int}, {float}'
)
# Raise error for not coherent dimensions
else:
raise ValueError(
f'Input is a list of lists with uneven dimensions. '
+ 'Each sublist must have the same dimension.'
)
# List of numbers
elif all((isinstance(elem, int) or isinstance(elem, float))
for elem in pts):
if log: print("Input is a list of non-quantities")
if log: print("Defaulting to degrees")
pts = pts * u.deg
# Raise error for invalid types
else:
raise ValueError(
f'Input is a list with an invalid type: {type(pts[0])}. \n'
+ f'Supported types inside a list are: \n'
+ f'{u.Quantity}, {int}, {float}'
+ f'sublists of {u.Quantity}, {int}, or {float} objects'
)
# Array of points
elif isinstance(pts, np.ndarray):
if log: print("Input point is an array without units")
if log: print("Defaulting to degrees")
pts = pts * u.deg
# Raise error for invalid types
else:
raise ValueError(
f'Input is an invalid type: {type(pts)}. \n'
+ f"Supported types for 'points' are: \n"
+ f'{u.Quantity}, {np.ndarray}, {tuple} or {list}'
)
# Check if input parameter is one point or an array with the last
# dimension being 2 = array of points.
if not pts.size > 1:
raise ValueError(f'Invalid shape: ({1},). '
+ 'The Last (or only) dimension has to be 2 (X, 2).')
elif not pts.shape[-1] == 2:
raise ValueError(f'Invalid shape: {pts.shape}. '
+ 'The Last (or only) dimension has to be 2 (X, 2).')
if pts.unit == u.pix:
return pts.value
else:
if fitsmap.header['NAXIS'] == 2:
fits_x, fits_y = \
fitsmap.wcs.wcs_world2pix(pts.T[0], pts.T[1], origin)
elif fitsmap.header['NAXIS'] == 4:
fits_x, fits_y, dum1, dum2 = \
fitsmap.wcs.wcs_world2pix(pts.T[0], pts.T[1], 1, 1, origin)
pixel_points = np.array([fits_x, fits_y]).T
return pixel_points
[docs]
def pixel_to_world(
points,
fitsmap,
origin=0,
log=False):
"""
Convert a set of points in pixel coordinates (python or FITS) to world
coordinates.
Parameters
----------
points : `~numpy.ndarray`, `tuple`, or `list`.
Input points.
fitsmap : `~madcubapy.io.MadcubaMap` or `~astropy.nddata.CCDData`.
Map object used to get WCS data.
origin : `int`, optional, default=0
Starting coordinate for image data: 0 for python pixel coordinates,
1 for FITS coordinates.
log : `bool`, optional
If True, print on screen information messages on how the data is
parsed.
Returns
-------
fits_points : `~astropy.units.Quantity`
Points converted to world coordinates with units.
"""
# Create a deepcopy because for a list of lists [:] and copy() fail
pts = copy.deepcopy(points)
# If it is a quantity, continue the function to step 2 with correct units
if isinstance(pts, u.Quantity):
if not pts.unit == u.pix:
raise ValueError(
f'"{pts.unit}" not supported. Only unit allowed is "{u.pix}"'
)
# If it is a list first check if the list has a point (quantities or
# numbers), or a list of points (arrays, or other lists or tuples)
elif isinstance(pts, list) or isinstance(pts, tuple):
# Convert tuple to list
if isinstance(pts, tuple):
pts = list(pts)
# Raise error for a mix of types in list
if not all(isinstance(elem, type(pts[0])) for elem in pts):
raise ValueError(f'A list of different types is not supported.')
# List of quantities
elif all(isinstance(elem, u.Quantity) for elem in pts):
# Only pixel units
if not all((elem.unit == u.pix) for elem in pts):
raise ValueError(
f'Input unit not supported. Only unit allowed is "{u.pix}"'
)
for j in range(len(pts)):
pts[j] = pts[j].value
pts = pts * u.pix
# List of arrays
elif all(isinstance(elem, np.ndarray) for elem in pts):
if log: print("Input point is a list of a arrays without units")
if log: print("Defaulting to degrees")
pts = np.array(pts) * u.deg
# # List of lists (several points)
elif (all(isinstance(elem, list) for elem in pts) or
all(isinstance(elem, tuple) for elem in pts)):
# Convert tuples to lists
if all(isinstance(elem, tuple) for elem in pts):
pts = [list(i) for i in pts]
# Check if dimensions in list are coherent (same dimensions on
# each sublist)
num = 0
lengths_list = []
for i in range(len(pts)):
num = len(pts[i])
lengths_list.append(num)
# If dimensions are coherent continue
if len(np.unique(lengths_list)) == 1:
# Raise error for a mix of types in list
if not all(isinstance(elem, type(pts[0][0]))
for point in pts for elem in point):
raise ValueError(
f'A list of different types is not supported.'
)
# List of lists of quantities
elif all(isinstance(elem, u.Quantity)
for point in pts for elem in point):
# Only pixel units
if not all((elem.unit == u.pix)
for point in pts for elem in point):
raise ValueError(
f'"Input unit not supported. '
+ f'Only unit allowed is "{u.pix}"'
)
for j in range(len(pts)):
for k in range(len(pts[j])):
pts[j][k] = pts[j][k].value
pts = pts * u.pix
# List of lists of numbers
elif all((isinstance(elem, int) or isinstance(elem, float))
for point in pts for elem in point):
if log: print("Input is a list of lists of non-quantities")
if log: print("Defaulting to pixels")
pts = pts * u.pix
# Raise error for further lists
elif all(isinstance(elem, list)
for point in pts for elem in point):
raise ValueError(
f'Input is a list of lists of lists. '
+ f'At most it can be a list of lists to represent '
+ f'a series of points.'
)
# Raise error for invalid types
else:
raise ValueError(
f'Input is a list of lists with an invalid type: '
+ f'{type(pts[0][0])}. \n'
+ f'Supported types inside a nested sublist are: \n'
+ f'{u.Quantity}, {int}, {float}'
)
# Raise error for not coherent dimensions
else:
raise ValueError(
f'Input is a list of lists with uneven dimensions. '
+ 'Each sublist must have the same dimension.'
)
# List of numbers
elif all((isinstance(elem, int) or isinstance(elem, float))
for elem in pts):
if log: print("Input is a list of non-quantities")
if log: print("Defaulting to pixel coordinates")
pts = pts * u.pix
# Raise error for invalid types
else:
raise ValueError(
f'Input is a list with an invalid type: {type(pts[0])}. \n'
+ f'Supported types inside a list are: \n'
+ f'{u.Quantity}, {int}, {float}'
+ f'sublists of {u.Quantity}, {int}, or {float} objects'
)
# Array of points
elif isinstance(pts, np.ndarray):
if log: print("Input point is an array without units")
if log: print("Defaulting to pixel coordinates")
pts = pts * u.pix
# Raise error for invalid types
else:
raise ValueError(
f'Input is an invalid type: {type(pts)}. \n'
+ f"Supported types for 'points' are: \n"
+ f'{u.Quantity}, {np.ndarray}, {tuple} or {list}'
)
# Check if input parameter is one point or an array with the last
# dimension being 2 = array of points.
if not pts.size > 1:
raise ValueError(f'Invalid shape: ({1},). '
+ 'The Last (or only) dimension has to be 2 (X, 2).')
elif not pts.shape[-1] == 2:
raise ValueError(f'Invalid shape: {pts.shape}. '
+ 'The Last (or only) dimension has to be 2 (X, 2).')
if fitsmap.header['NAXIS'] == 2:
world_x, world_y = \
fitsmap.wcs.wcs_pix2world(pts.T[0], pts.T[1], origin)
elif fitsmap.header['NAXIS'] == 4:
world_x, world_y, dum1, dum2 = \
fitsmap.wcs.wcs_pix2world(pts.T[0], pts.T[1], 1, 1, origin)
world_points = np.array([world_x, world_y]).T * u.deg
return world_points
[docs]
def angle_to_pixels(
length,
fitsmap,
axis='y',
log=False):
"""
Convert a length value (or values) in angular units to data pixels.
Parameters
----------
length : `~astropy.units.Quantity`, `~numpy.ndarray`, `list`, `tuple`, \
`int` or `float`
Length (or list of lengths) separating two points.
fitsmap : `~madcubapy.io.MadcubaMap` or `~astropy.nddata.CCDData`
Map object used to get CDELT value.
axis : `str`, optional
Axis in which to calculate angle deviation. Useful for images with
non-square pixels. Possible values are ``'x'`` and ``'y'``.
log : `bool`, optional
If True, print on screen information messages on how the data is
parsed.
Returns
-------
pix_length : `~numpy.ndarray`
Length in pixels.
"""
# Quantity
if isinstance(length, u.Quantity):
unit = length.unit
# Raise an error if at least one unit is not angular or pixels
if (unit != u.pix and unit != u.rad and unit != u.deg and
unit != u.arcmin and unit != u.arcsec):
raise ValueError(
f'"Only angle units are allowed: "{u.rad}", '
+ f'"{u.deg}", "{u.arcmin}", or "{u.arcsec}"'
)
match unit:
case u.pix:
if log: print("Input length in pixels, returning pixels.")
case u.deg | u.arcmin | u.arcsec | u.rad :
length = length.to(u.deg)
# Array of lengths
elif isinstance(length, np.ndarray):
if log: print("No input units found. Defaulting to degrees.")
length = length * u.pix
# Length as a number
elif isinstance(length, int) or isinstance(length, float):
if log: print("No input units found. Defaulting to degrees.")
length = length * u.deg
# Several options for a list
elif isinstance(length, list) or isinstance(length, tuple):
# List of numbers
if all((isinstance(elem, int) or isinstance(elem, float))
for elem in length):
if log: print("No input units found. Defaulting to degrees.")
length = np.array(length) * u.deg
# Raise error for further lists or tuples
elif any((isinstance(elem, list) or isinstance(elem, tuple))
for elem in length):
raise ValueError(
f'Lists or tuples with more than one dimension not supported'
)
# List of quantities
elif all(isinstance(elem, u.Quantity) for elem in length):
# Convert tuple to list
if isinstance(length, tuple):
length = list(length)
# Return pixels
if all((elem.unit == u.pix) for elem in length):
if log: print("Input units are pixels, returning pixels.")
for j in range(len(length)):
length[j] = length[j].value
length = length * u.pix
# Angular units
elif all((elem.unit != u.pix) for elem in length):
raise_error = False
for elem in length:
unit = elem.unit
if (unit != u.rad and unit != u.deg and
unit != u.arcmin and unit != u.arcsec):
raise_error = True
# If at least one unit is not angular raise an error
if raise_error:
raise ValueError(
f'"Only "{u.pix}" or angle units are allowed: '
+ f'"{u.rad}", ""{u.deg}", "{u.arcmin}", or "{u.arcsec}"'
)
for j in range(len(length)):
length[j] = length[j].to(u.deg).value
length = length * u.deg
# Raise an error if there is a mix with pixels and other units
else:
raise ValueError(f'"Cannot mix "{u.pix}" with other units')
# Raise error for a mix of types
elif not all(isinstance(elem, type(length[0])) for elem in length):
raise ValueError(f'A list of different types is not supported.')
# Raise error for invalid types
else:
raise ValueError(
f'Input is an invalid type: {type(length)}. \n'
+ f"Supported types for 'points' are: \n"
+ f'{u.Quantity}, {int}, {float}, {np.ndarray}, {list}, or {tuple}'
)
if axis == 'x':
cdelt = fitsmap.wcs.wcs.cdelt[0]
elif axis == 'y':
cdelt = fitsmap.wcs.wcs.cdelt[1]
else:
raise ValueError(f'Axis can only be "x" or "y"')
if length.unit == u.pix:
pix_length = length.value
elif length.unit == u.deg:
pix_length = length.value / cdelt
return pix_length
[docs]
def pixels_to_angle(
pix_length,
fitsmap,
axis='y',
log=False):
"""
Convert an angle value (or values) in pixels to an angular quantity.
Parameters
----------
pix_length : `~astropy.units.Quantity`, `~numpy.ndarray`, `list`, `tuple` \
`int`, or `float`
Length (or list of lengths) in pixels separating two points.
fitsmap : `~madcubapy.io.MadcubaMap` or `~astropy.nddata.CCDData`
Map object used to get CDELT value.
axis : `str`, optional
Axis in which to calculate angle deviation. Useful for images with
non-square pixels. Possible values are ``'x'`` and ``'y'``.
log : `bool`, optional
If True, print on screen information messages on how the data is
parsed.
Returns
-------
length : `astropy.units.Quantity`
Length as an angular quantity.
"""
# Quantity, only pixels
if isinstance(pix_length, u.Quantity):
unit = pix_length.unit
if unit != u.pix:
raise ValueError(f'The only unit allowed is "{u.pix}"')
# Array of lengths
elif isinstance(pix_length, np.ndarray):
if log: print("No input units found. Defaulting to pixels.")
pix_length = pix_length * u.pix
# Length as a number
elif isinstance(pix_length, int) or isinstance(pix_length, float):
if log: print("No input units found. Defaulting to pixels.")
pix_length = pix_length * u.pix
# Several options for a list
elif isinstance(pix_length, list) or isinstance(pix_length, tuple):
# List of numbers
if all((isinstance(elem, int) or isinstance(elem, float))
for elem in pix_length):
if log: print("No input units found. Defaulting to pixels.")
pix_length = np.array(pix_length) * u.pix
# Raise error for further lists or tuples
elif any((isinstance(elem, list) or isinstance(elem, tuple))
for elem in pix_length):
raise ValueError(
f'Lists or tuples with more than one dimension not supported'
)
# List of quantities (only pix supported)
elif all(isinstance(elem, u.Quantity) for elem in pix_length):
# Convert tuple to list
if isinstance(pix_length, tuple):
pix_length = list(pix_length)
if not all((elem.unit == u.pix) for elem in pix_length):
raise ValueError(
f'Input unit not supported. Only unit allowed is "{u.pix}"'
)
for j in range(len(pix_length)):
pix_length[j] = pix_length[j].value
pix_length = pix_length * u.pix
# Raise error for a mix of types
elif not all(isinstance(elem, type(pix_length[0]))
for elem in pix_length):
raise ValueError(f'A list of different types is not supported.')
# Raise error for invalid types
else:
raise ValueError(
f'Input is an invalid type: {type(pix_length)}. \n'
+ f"Supported types for 'points' are: \n"
+ f'{u.Quantity}, {int}, {float}, {np.ndarray}, {list}, or {tuple}'
)
if axis == 'x':
cdelt = fitsmap.wcs.wcs.cdelt[0]
elif axis == 'y':
cdelt = fitsmap.wcs.wcs.cdelt[1]
else:
raise ValueError(f'Axis can only be "x" or "y"')
length = pix_length.value * cdelt * u.deg
return length