"""The ``region_translators`` module houses translations of
:ref:`regions:shapes` to :ref:`photutils:photutils-aperture` apertures.
"""
import re
from astropy import units as u
from astropy.coordinates import SkyCoord
from glue.core.roi import CircularROI, EllipticalROI, RectangularROI, CircularAnnulusROI
from photutils.aperture import (CircularAperture, SkyCircularAperture,
EllipticalAperture, SkyEllipticalAperture,
RectangularAperture, SkyRectangularAperture,
CircularAnnulus, SkyCircularAnnulus,
EllipticalAnnulus, SkyEllipticalAnnulus,
RectangularAnnulus, SkyRectangularAnnulus)
from regions import (CirclePixelRegion, CircleSkyRegion,
EllipsePixelRegion, EllipseSkyRegion,
RectanglePixelRegion, RectangleSkyRegion,
CircleAnnulusPixelRegion, CircleAnnulusSkyRegion,
EllipseAnnulusPixelRegion, EllipseAnnulusSkyRegion,
RectangleAnnulusPixelRegion, RectangleAnnulusSkyRegion,
PixCoord, PolygonSkyRegion)
__all__ = ['regions2roi', 'regions2aperture', 'aperture2regions']
[docs]
def regions2roi(region_shape, wcs=None):
"""Convert a given ``regions`` shape to ``glue`` ROI.
This is the opposite of what is offered by
``glue_astronomy.translators.regions.AstropyRegionsHandler.to_object``
but does not cover all the same shapes exactly.
Parameters
----------
region_shape : `regions.Region`
A supported ``regions`` shape.
wcs : `~astropy.wcs.WCS` or `None`
A compatible WCS object, if required.
**This is only used for sky aperture.**
Returns
-------
roi : `glue.core.roi.Roi`
An equivalent ``glue`` ROI.
Raises
------
ValueError
WCS is required but not provided.
NotImplementedError
The given ``regions`` shape is not supported.
Examples
--------
Translate a `regions.CirclePixelRegion` to `glue.core.roi.CircularROI`:
>>> from regions import CirclePixelRegion, PixCoord
>>> from jdaviz.core.region_translators import regions2roi
>>> region_shape = CirclePixelRegion(center=PixCoord(x=42, y=43), radius=4.2)
>>> regions2roi(region_shape) # doctest: +ELLIPSIS
<glue.core.roi.CircularROI object at ...>
"""
if isinstance(region_shape, (CircleSkyRegion, EllipseSkyRegion, RectangleSkyRegion,
CircleAnnulusSkyRegion)):
if wcs is None:
raise ValueError(f'WCS must be provided for {region_shape}')
# Convert sky to pixel region first, if necessary.
region_shape = region_shape.to_pixel(wcs)
if isinstance(region_shape, CirclePixelRegion):
roi = CircularROI(
xc=region_shape.center.x, yc=region_shape.center.y, radius=region_shape.radius)
elif isinstance(region_shape, EllipsePixelRegion):
roi = EllipticalROI(
xc=region_shape.center.x, yc=region_shape.center.y,
radius_x=region_shape.width * 0.5, radius_y=region_shape.height * 0.5,
theta=region_shape.angle.to_value(u.radian))
elif isinstance(region_shape, RectanglePixelRegion):
half_w = region_shape.width * 0.5
half_h = region_shape.height * 0.5
xmin = region_shape.center.x - half_w
xmax = region_shape.center.x + half_w
ymin = region_shape.center.y - half_h
ymax = region_shape.center.y + half_h
roi = RectangularROI(
xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax,
theta=region_shape.angle.to_value(u.radian))
elif isinstance(region_shape, CircleAnnulusPixelRegion):
roi = CircularAnnulusROI(
xc=region_shape.center.x, yc=region_shape.center.y,
inner_radius=region_shape.inner_radius, outer_radius=region_shape.outer_radius)
else:
raise NotImplementedError(f'{region_shape.__class__.__name__} is not supported')
return roi
[docs]
def regions2aperture(region_shape):
"""Convert a given ``regions`` shape to ``photutils`` aperture.
Parameters
----------
region_shape : `regions.Region`
A supported ``regions`` shape.
Returns
-------
aperture : `photutils.aperture.Aperture`
An equivalent ``photutils`` aperture.
Raises
------
NotImplementedError
The given ``regions`` shape is not supported.
Examples
--------
Translate a `regions.CirclePixelRegion` to `photutils.aperture.CircularAperture`:
>>> from regions import CirclePixelRegion, PixCoord
>>> from jdaviz.core.region_translators import regions2aperture
>>> region_shape = CirclePixelRegion(center=PixCoord(x=42, y=43), radius=4.2)
>>> regions2aperture(region_shape)
<CircularAperture([42., 43.], r=4.2)>
See Also
--------
aperture2regions
"""
if isinstance(region_shape, CirclePixelRegion):
aperture = CircularAperture(region_shape.center.xy, region_shape.radius)
elif isinstance(region_shape, CircleSkyRegion):
aperture = SkyCircularAperture(region_shape.center, region_shape.radius)
elif isinstance(region_shape, EllipsePixelRegion):
th = region_shape.angle
aperture = EllipticalAperture(
region_shape.center.xy, region_shape.width * 0.5, region_shape.height * 0.5,
theta=th)
elif isinstance(region_shape, EllipseSkyRegion):
aperture = SkyEllipticalAperture(
region_shape.center, region_shape.width * 0.5, region_shape.height * 0.5,
theta=(region_shape.angle - (90 * u.deg)))
elif isinstance(region_shape, RectanglePixelRegion):
th = region_shape.angle
aperture = RectangularAperture(
region_shape.center.xy, region_shape.width, region_shape.height,
theta=th)
elif isinstance(region_shape, RectangleSkyRegion):
aperture = SkyRectangularAperture(
region_shape.center, region_shape.width, region_shape.height,
theta=(region_shape.angle - (90 * u.deg)))
elif isinstance(region_shape, CircleAnnulusPixelRegion):
aperture = CircularAnnulus(
region_shape.center.xy, region_shape.inner_radius, region_shape.outer_radius)
elif isinstance(region_shape, CircleAnnulusSkyRegion):
aperture = SkyCircularAnnulus(
region_shape.center, region_shape.inner_radius, region_shape.outer_radius)
elif isinstance(region_shape, EllipseAnnulusPixelRegion):
th = region_shape.angle
aperture = EllipticalAnnulus(
region_shape.center.xy, region_shape.inner_width * 0.5, region_shape.outer_width * 0.5,
region_shape.outer_height * 0.5, b_in=region_shape.inner_height * 0.5,
theta=th)
elif isinstance(region_shape, EllipseAnnulusSkyRegion):
aperture = SkyEllipticalAnnulus(
region_shape.center, region_shape.inner_width * 0.5, region_shape.outer_width * 0.5,
region_shape.outer_height * 0.5, b_in=region_shape.inner_height * 0.5,
theta=(region_shape.angle - (90 * u.deg)))
elif isinstance(region_shape, RectangleAnnulusPixelRegion):
th = region_shape.angle
aperture = RectangularAnnulus(
region_shape.center.xy, region_shape.inner_width, region_shape.outer_width,
region_shape.outer_height, h_in=region_shape.inner_height,
theta=th)
elif isinstance(region_shape, RectangleAnnulusSkyRegion):
aperture = SkyRectangularAnnulus(
region_shape.center, region_shape.inner_width, region_shape.outer_width,
region_shape.outer_height, h_in=region_shape.inner_height,
theta=(region_shape.angle - (90 * u.deg)))
else:
raise NotImplementedError(f'{region_shape.__class__.__name__} is not supported')
return aperture
[docs]
def aperture2regions(aperture):
"""Convert a given ``photutils`` aperture to ``regions`` shape.
Parameters
----------
aperture : `photutils.aperture.Aperture`
An equivalent ``photutils`` aperture.
Returns
-------
region_shape : `regions.Region`
A supported ``regions`` shape.
Raises
------
NotImplementedError
The given ``photutils`` aperture is not supported.
ValueError
Invalid inputs.
Examples
--------
Translate a `photutils.aperture.CircularAperture` to `regions.CirclePixelRegion`:
>>> from photutils.aperture import CircularAperture
>>> from jdaviz.core.region_translators import aperture2regions
>>> aperture = CircularAperture((42, 43), 4.2)
>>> aperture2regions(aperture)
<CirclePixelRegion(center=PixCoord(x=42.0, y=43.0), radius=4.2)>
See Also
--------
regions2aperture
"""
if isinstance(aperture, CircularAperture):
region_shape = CirclePixelRegion(
center=positions2pixcoord(aperture.positions), radius=aperture.r)
elif isinstance(aperture, SkyCircularAperture):
region_shape = CircleSkyRegion(center=aperture.positions, radius=aperture.r)
elif isinstance(aperture, EllipticalAperture):
region_shape = EllipsePixelRegion(
center=positions2pixcoord(aperture.positions), width=aperture.a * 2,
height=aperture.b * 2, angle=aperture.theta)
elif isinstance(aperture, SkyEllipticalAperture):
region_shape = EllipseSkyRegion(
center=aperture.positions, width=aperture.a * 2, height=aperture.b * 2,
angle=(aperture.theta + (90 * u.deg)))
elif isinstance(aperture, RectangularAperture):
region_shape = RectanglePixelRegion(
center=positions2pixcoord(aperture.positions), width=aperture.w, height=aperture.h,
angle=aperture.theta)
elif isinstance(aperture, SkyRectangularAperture):
region_shape = RectangleSkyRegion(
center=aperture.positions, width=aperture.w, height=aperture.h,
angle=(aperture.theta + (90 * u.deg)))
elif isinstance(aperture, CircularAnnulus):
region_shape = CircleAnnulusPixelRegion(
center=positions2pixcoord(aperture.positions), inner_radius=aperture.r_in,
outer_radius=aperture.r_out)
elif isinstance(aperture, SkyCircularAnnulus):
region_shape = CircleAnnulusSkyRegion(
center=aperture.positions, inner_radius=aperture.r_in, outer_radius=aperture.r_out)
elif isinstance(aperture, EllipticalAnnulus):
region_shape = EllipseAnnulusPixelRegion(
center=positions2pixcoord(aperture.positions), inner_width=aperture.a_in * 2,
inner_height=aperture.b_in * 2, outer_width=aperture.a_out * 2,
outer_height=aperture.b_out * 2, angle=aperture.theta)
elif isinstance(aperture, SkyEllipticalAnnulus):
region_shape = EllipseAnnulusSkyRegion(
center=aperture.positions, inner_width=aperture.a_in * 2,
inner_height=aperture.b_in * 2, outer_width=aperture.a_out * 2,
outer_height=aperture.b_out * 2,
angle=(aperture.theta + (90 * u.deg)))
elif isinstance(aperture, RectangularAnnulus):
region_shape = RectangleAnnulusPixelRegion(
center=positions2pixcoord(aperture.positions), inner_width=aperture.w_in,
inner_height=aperture.h_in, outer_width=aperture.w_out, outer_height=aperture.h_out,
angle=aperture.theta)
elif isinstance(aperture, SkyRectangularAnnulus):
region_shape = RectangleAnnulusSkyRegion(
center=aperture.positions, inner_width=aperture.w_in, inner_height=aperture.h_in,
outer_width=aperture.w_out, outer_height=aperture.h_out,
angle=(aperture.theta + (90 * u.deg)))
else: # pragma: no cover
raise NotImplementedError(f'{aperture.__class__.__name__} is not supported')
return region_shape
def _create_polygon_skyregion_from_coords(coords, frame='icrs', unit=u.deg):
"""Create a `regions.PolygonSkyRegion` from given coordinates.
Parameters
----------
coords : list
List of coordinates in the form of [ra1, dec1, ra2, dec2, ...].
frame : str, optional
Coordinate frame. Default is 'icrs'.
unit : `~astropy.units.Unit`, optional
Unit of the coordinates. Default is `~astropy.units.deg`.
Returns
-------
sky_region : `regions.PolygonSkyRegion`
A polygon sky region.
Examples
--------
Create a `regions.PolygonSkyRegion` from given coordinates:
>>> from jdaviz.core.region_translators import _create_polygon_skyregion_from_coords
>>> coords = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0]
>>> _create_polygon_skyregion_from_coords(coords)
<PolygonSkyRegion(vertices=<SkyCoord (ICRS): (ra, dec) in deg
[(10., 20.), (30., 40.), (50., 60.)]>)>
"""
ra, dec = coords[::2], coords[1::2]
sky_coordinates = SkyCoord(ra, dec, frame=frame, unit=unit)
sky_region = PolygonSkyRegion(sky_coordinates)
return sky_region
def _create_circle_skyregion_from_coords(coords, frame='icrs', unit=u.deg):
"""Create a `regions.CircleSkyRegion` from given coordinates.
Parameters
----------
coords : list
List of coordinates in the form of [ra, dec, radius].
frame : str, optional
Coordinate frame. Default is 'icrs'.
unit : `~astropy.units.Unit`, optional
Unit of the coordinates. Default is `~astropy.units.deg`.
Returns
-------
sky_region : `regions.CircleSkyRegion`
A circle sky region.
Examples
--------
Create a `regions.CircleSkyRegion` from given coordinates:
>>> from jdaviz.core.region_translators import _create_circle_skyregion_from_coords
>>> coords = [10.0, 20.0, 5.0]
>>> _create_circle_skyregion_from_coords(coords)
<CircleSkyRegion(center=<SkyCoord (ICRS): (ra, dec) in deg (10., 20.)>, radius=5.0 deg)>
"""
sky_coordinates = SkyCoord(coords[0], coords[1], frame=frame, unit=unit)
sky_region = CircleSkyRegion(center=sky_coordinates, radius=coords[2] * unit)
return sky_region
def _create_ellipse_skyregion_from_coords(coords, frame='icrs', unit=u.deg):
"""Create a `regions.EllipseSkyRegion` from given coordinates.
Parameters
----------
coords : list
List of coordinates in the form of [ra, dec, width, height, angle].
frame : str, optional
Coordinate frame. Default is 'icrs'.
unit : `~astropy.units.Unit`, optional
Unit of the coordinates. Default is `~astropy.units.deg`.
Returns
-------
sky_region : `regions.EllipseSkyRegion`
An ellipse sky region.
Examples
--------
Create a `regions.EllipseSkyRegion` from given coordinates:
>>> from jdaviz.core.region_translators import _create_ellipse_skyregion_from_coords
>>> coords = [10.0, 20.0, 5.0, 3.0, 45.0]
>>> _create_ellipse_skyregion_from_coords(coords)
<EllipseSkyRegion(center=<SkyCoord (ICRS): (ra, dec) in deg
(10., 20.)>, width=5.0 deg, height=3.0 deg, angle=45.0 deg)>
"""
sky_coordinates = SkyCoord(coords[0], coords[1], frame=frame, unit=unit)
sky_region = EllipseSkyRegion(center=sky_coordinates, width=coords[2] * unit,
height=coords[3] * unit, angle=coords[4] * unit)
return sky_region
SUPPORTED_STCS_SHAPE_VALUES = ('POLYGON', 'CIRCLE', 'ELLIPSE', )
SUPPORTED_STCS_FRAME_VALUES = ('ICRS', 'J2000', 'FK5', )
STCS_SHAPE_PATTERN = '|'.join(SUPPORTED_STCS_SHAPE_VALUES)
STCS_FRAME_PATTERN = '|'.join(SUPPORTED_STCS_FRAME_VALUES)
SUPPORTED_STCS_PATTERN = re.compile(
fr"^(?P<shape>({'|'.join(SUPPORTED_STCS_SHAPE_VALUES)}))"
fr"(?P<frame>(\s+({'|'.join(SUPPORTED_STCS_FRAME_VALUES)})))*"
r"(?P<coordinates>(\s+-?\d+\.\d+){2,}$)",
re.IGNORECASE)
SKY_REGION_FROM_COORDS_FACTORY = {
'polygon': _create_polygon_skyregion_from_coords,
'circle': _create_circle_skyregion_from_coords,
'ellipse': _create_ellipse_skyregion_from_coords,
}
def is_stcs_string(stcs_string):
"""Check if the given string is a valid STC-S string.
Parameters
----------
stcs_string : str
A string to check.
Returns
-------
is_stcs : bool
`True` if the given string is a valid STC-S string, otherwise `False`.
Examples
--------
Check if a given string is a valid STC-S string:
>>> from jdaviz.core.region_translators import is_stcs_string
>>> stcs_string = 'POLYGON ICRS 10.0 20.0 30.0 40.0 50.0 60.0'
>>> is_stcs_string(stcs_string)
True
"""
try:
return bool(SUPPORTED_STCS_PATTERN.match(stcs_string))
except Exception:
return False
def stcs_string2region(stcs_string):
"""Convert a given STC-S string to ``regions`` shape.
Parameters
----------
stcs_string : str
A valid STC-S string.
Returns
-------
sky_region : `regions.Region`
A supported ``regions`` shape.
Raises
------
ValueError
Invalid inputs.
Examples
--------
Translate an STC-S region to `regions.PolygonSkyRegion`:
>>> from jdaviz.core.region_translators import stcs_string2region
>>> stcs_string = 'POLYGON ICRS 10.0 20.0 30.0 40.0 50.0 60.0'
>>> stcs_string2region(stcs_string)
<PolygonSkyRegion(vertices=<SkyCoord (ICRS): (ra, dec) in deg
[(10., 20.), (30., 40.), (50., 60.)]>)>
"""
if not isinstance(stcs_string, str):
raise ValueError('STC-S string must be a string.')
_match = SUPPORTED_STCS_PATTERN.match(stcs_string)
# Check if the STC-S string is valid, otherwise raise an error
if not _match:
raise ValueError(f'Invalid STC-S string: {stcs_string}')
# Extract the shape, frame, and coordinates from the STC-S string
shape = _match.group('shape').strip().lower()
frame = _match.group('frame').strip().lower() if _match.group('frame') else 'icrs'
coordinates = list(map(float, _match.group('coordinates').strip().split()))
return SKY_REGION_FROM_COORDS_FACTORY[shape](coordinates, frame=frame)
def region2stcs_string(region):
"""Convert a `regions.Region` object to an STC-S string.
Currently supports only `CircleSkyRegion` and `EllipseSkyRegion`.
Parameters
----------
region : `regions.Region`
Sky region to convert to STC-S format.
Returns
-------
stcs_str : str
STC-S string representing the region, using the format:
- `CIRCLE <FRAME> <RA> <DEC> <RADIUS>`
- `ELLIPSE <FRAME> <RA> <DEC> <WIDTH> <HEIGHT> <ANGLE>`
"""
try:
shape, coords = SHAPE_COORDS_FROM_SKY_REGION_FACTORY[region.__class__](region)
except KeyError:
raise NotImplementedError(f"{region.__class__.__name__} is not supported for STC-S export")
frame = region.center.frame.name.upper()
if frame not in SUPPORTED_STCS_FRAME_VALUES:
raise ValueError(f"Frame '{frame}' is not supported for STC-S export."
f"Supported: {SUPPORTED_STCS_FRAME_VALUES}")
coord_parts = [f"{x:.6f}" for x in coords]
return f"{shape} {frame} {' '.join(coord_parts)}"
def _shape_coords_from_circle_sky_region(region):
return (
"CIRCLE", [
region.center.ra.deg,
region.center.dec.deg,
region.radius.to_value(u.deg),
],
)
def _shape_coords_from_ellipse_sky_region(region):
return (
"ELLIPSE", [
region.center.ra.deg,
region.center.dec.deg,
region.width.to_value(u.deg),
region.height.to_value(u.deg),
region.angle.to_value(u.deg),
],
)
SHAPE_COORDS_FROM_SKY_REGION_FACTORY = {
CircleSkyRegion: _shape_coords_from_circle_sky_region,
EllipseSkyRegion: _shape_coords_from_ellipse_sky_region,
}
def positions2pixcoord(positions):
"""Convert ``photutils`` aperture positions to `~regions.PixCoord`
that is acceptable by ``regions`` shape.
"""
if positions.shape != (2, ):
raise ValueError('regions shape only accepts scalar x and y positions')
if isinstance(positions, u.Quantity):
pixcoord = PixCoord(x=positions[0].value, y=positions[1].value)
else:
pixcoord = PixCoord(x=positions[0], y=positions[1])
return pixcoord