# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Tools for converting between `regions.Region` and Aperture objects and
between `shapely.Polygon` and `regions.PolygonRegion` objects.
"""
import astropy.units as u
import numpy as np
from photutils.aperture.circle import (CircularAnnulus, CircularAperture,
SkyCircularAnnulus, SkyCircularAperture)
from photutils.aperture.core import Aperture
from photutils.aperture.ellipse import (EllipticalAnnulus, EllipticalAperture,
SkyEllipticalAnnulus,
SkyEllipticalAperture)
from photutils.aperture.rectangle import (RectangularAnnulus,
RectangularAperture,
SkyRectangularAnnulus,
SkyRectangularAperture)
__all__ = ['aperture_to_region', 'region_to_aperture']
__doctest_requires__ = {'region_to_aperture': ['regions >= 0.12.dev'],
'aperture_to_region': ['regions >= 0.12.dev'],
'_scalar_aperture_to_region': ['regions >= 0.12.dev'],
'_shapely_polygon_to_region': ['regions >= 0.12.dev',
'shapely']}
[docs]
def region_to_aperture(region):
"""
Convert a given `regions.Region` object to an
`~photutils.aperture.Aperture` object.
Parameters
----------
region : `regions.Region`
A supported `regions.Region` object.
Returns
-------
aperture : `~photutils.aperture.Aperture`
An equivalent ``photutils`` aperture.
Raises
------
`TypeError`
The given `regions.Region` object is not supported.
Notes
-----
The ellipse ``width`` and ``height`` region parameters represent
the full extent of the shapes and thus are divided by 2 when
converting to elliptical aperture objects, which are defined using
the semi-major (``a``) and semi-minor (``b``) axes. The ``width``
and ``height`` parameters are mapped to the semi-major (``a``) and
semi-minor (``b``) axes parameters, respectively, of the elliptical
apertures.
The region ``angle`` for sky-based regions is defined as the angle
of the ``width`` axis relative to WCS longitude axis (PA=90).
However, the sky-based apertures define the ``theta`` as the
position angle of the semimajor axis relative to the North celestial
pole (PA=0). Therefore, for sky-based regions the region ``angle``
is converted to the aperture ``theta`` parameter by subtracting 90
degrees.
.. |rarr| unicode:: U+0279E .. RIGHTWARDS ARROW
The following `regions.Region` objects are supported, shown with
their equivalent `~photutils.aperture.Aperture` object:
* `~regions.CirclePixelRegion` |rarr|
`~photutils.aperture.CircularAperture`
* `~regions.CircleSkyRegion` |rarr|
`~photutils.aperture.SkyCircularAperture`
* `~regions.EllipsePixelRegion` |rarr|
`~photutils.aperture.EllipticalAperture`
* `~regions.EllipseSkyRegion` |rarr|
`~photutils.aperture.SkyEllipticalAperture`
* `~regions.RectanglePixelRegion` |rarr|
`~photutils.aperture.RectangularAperture`
* `~regions.RectangleSkyRegion` |rarr|
`~photutils.aperture.SkyRectangularAperture`
* `~regions.CircleAnnulusPixelRegion` |rarr|
`~photutils.aperture.CircularAnnulus`
* `~regions.CircleAnnulusSkyRegion` |rarr|
`~photutils.aperture.SkyCircularAnnulus`
* `~regions.EllipseAnnulusPixelRegion` |rarr|
`~photutils.aperture.EllipticalAnnulus`
* `~regions.EllipseAnnulusSkyRegion` |rarr|
`~photutils.aperture.SkyEllipticalAnnulus`
* `~regions.RectangleAnnulusPixelRegion` |rarr|
`~photutils.aperture.RectangularAnnulus`
* `~regions.RectangleAnnulusSkyRegion` |rarr|
`~photutils.aperture.SkyRectangularAnnulus`
Examples
--------
>>> from regions import CirclePixelRegion, PixCoord
>>> from photutils.aperture import region_to_aperture
>>> region = CirclePixelRegion(center=PixCoord(x=10, y=20), radius=5)
>>> aperture = region_to_aperture(region)
>>> aperture
<CircularAperture([10., 20.], r=5.0)>
"""
from regions import (CircleAnnulusPixelRegion, CircleAnnulusSkyRegion,
CirclePixelRegion, CircleSkyRegion,
EllipseAnnulusPixelRegion, EllipseAnnulusSkyRegion,
EllipsePixelRegion, EllipseSkyRegion,
RectangleAnnulusPixelRegion,
RectangleAnnulusSkyRegion, RectanglePixelRegion,
RectangleSkyRegion, Region)
if not isinstance(region, Region):
msg = 'Input region must be a Region object'
raise TypeError(msg)
if isinstance(region, CirclePixelRegion):
aperture = CircularAperture(region.center.xy, region.radius)
elif isinstance(region, CircleSkyRegion):
aperture = SkyCircularAperture(region.center, region.radius)
elif isinstance(region, EllipsePixelRegion):
aperture = EllipticalAperture(
region.center.xy, region.width * 0.5, region.height * 0.5,
theta=region.angle)
elif isinstance(region, EllipseSkyRegion):
aperture = SkyEllipticalAperture(
region.center, region.width * 0.5, region.height * 0.5,
theta=(region.angle - (90 * u.deg)))
elif isinstance(region, RectanglePixelRegion):
aperture = RectangularAperture(
region.center.xy, region.width, region.height,
theta=region.angle)
elif isinstance(region, RectangleSkyRegion):
aperture = SkyRectangularAperture(
region.center, region.width, region.height,
theta=(region.angle - (90 * u.deg)))
elif isinstance(region, CircleAnnulusPixelRegion):
aperture = CircularAnnulus(
region.center.xy, region.inner_radius, region.outer_radius)
elif isinstance(region, CircleAnnulusSkyRegion):
aperture = SkyCircularAnnulus(
region.center, region.inner_radius, region.outer_radius)
elif isinstance(region, EllipseAnnulusPixelRegion):
aperture = EllipticalAnnulus(
region.center.xy,
region.inner_width * 0.5, region.outer_width * 0.5,
region.outer_height * 0.5, b_in=region.inner_height * 0.5,
theta=region.angle)
elif isinstance(region, EllipseAnnulusSkyRegion):
aperture = SkyEllipticalAnnulus(
region.center,
region.inner_width * 0.5, region.outer_width * 0.5,
region.outer_height * 0.5, b_in=region.inner_height * 0.5,
theta=(region.angle - (90 * u.deg)))
elif isinstance(region, RectangleAnnulusPixelRegion):
aperture = RectangularAnnulus(
region.center.xy,
region.inner_width, region.outer_width,
region.outer_height, h_in=region.inner_height,
theta=region.angle)
elif isinstance(region, RectangleAnnulusSkyRegion):
aperture = SkyRectangularAnnulus(
region.center,
region.inner_width, region.outer_width,
region.outer_height, h_in=region.inner_height,
theta=(region.angle - (90 * u.deg)))
else:
msg = (f'Cannot convert {region.__class__.__name__!r} to an '
'Aperture object')
raise TypeError(msg)
return aperture
[docs]
def aperture_to_region(aperture):
"""
Convert a given `~photutils.aperture.Aperture` object to a
`regions.Region` or `regions.Regions` object.
Because a `regions.Region` object can only have one position, a
`regions.Regions` object will be returned if the input ``aperture``
has more than one position. Otherwise, a `regions.Region` object
will be returned.
Parameters
----------
aperture : `~photutils.aperture.Aperture`
An `~photutils.aperture.Aperture` object to convert.
Returns
-------
region : `regions.Region` or `regions.Regions`
An equivalent `regions.Region` object. If the input ``aperture``
has more than one position then a `regions.Regions` will be
returned.
Notes
-----
The elliptical aperture ``a`` and ``b`` parameters represent the
semi-major and semi-minor axes, respectively. The ``a`` and ``b``
parameters are mapped to the ellipse ``width`` and ``height`` region
parameters, respectively, by multiplying by 2 because they represent
the full extent of the ellipse.
The region ``angle`` for sky-based regions is defined as the angle
of the ``width`` axis relative to WCS longitude axis (PA=90).
However, the sky-based apertures define the ``theta`` as the
position angle of the semimajor axis relative to the North celestial
pole (PA=0). Therefore, for sky-based apertures the ``theta``
parameter is converted to the region ``angle`` by adding 90 degrees.
.. |rarr| unicode:: U+0279E .. RIGHTWARDS ARROW
The following `~photutils.aperture.Aperture` objects are supported,
shown with their equivalent `regions.Region` object:
* `~photutils.aperture.CircularAperture` |rarr|
`~regions.CirclePixelRegion`
* `~photutils.aperture.SkyCircularAperture` |rarr|
`~regions.CircleSkyRegion`
* `~photutils.aperture.EllipticalAperture` |rarr|
`~regions.EllipsePixelRegion`
* `~photutils.aperture.SkyEllipticalAperture` |rarr|
`~regions.EllipseSkyRegion`
* `~photutils.aperture.RectangularAperture` |rarr|
`~regions.RectanglePixelRegion`
* `~photutils.aperture.SkyRectangularAperture` |rarr|
`~regions.RectangleSkyRegion`
* `~photutils.aperture.CircularAnnulus` |rarr|
`~regions.CircleAnnulusPixelRegion`
* `~photutils.aperture.SkyCircularAnnulus` |rarr|
`~regions.CircleAnnulusSkyRegion`
* `~photutils.aperture.EllipticalAnnulus` |rarr|
`~regions.EllipseAnnulusPixelRegion`
* `~photutils.aperture.SkyEllipticalAnnulus` |rarr|
`~regions.EllipseAnnulusSkyRegion`
* `~photutils.aperture.RectangularAnnulus` |rarr|
`~regions.RectangleAnnulusPixelRegion`
* `~photutils.aperture.SkyRectangularAnnulus` |rarr|
`~regions.RectangleAnnulusSkyRegion`
Examples
--------
>>> from photutils.aperture import CircularAperture, aperture_to_region
>>> aperture = CircularAperture((10, 20), r=5)
>>> region = aperture_to_region(aperture)
>>> region
<CirclePixelRegion(center=PixCoord(x=10.0, y=20.0), radius=5.0)>
>>> aperture = CircularAperture(((10, 20), (30, 40)), r=5)
>>> region = aperture_to_region(aperture)
>>> region
<Regions([
<CirclePixelRegion(center=PixCoord(x=10.0, y=20.0), radius=5.0)>,
<CirclePixelRegion(center=PixCoord(x=30.0, y=40.0), radius=5.0)>
])>
"""
from regions import Regions
if not isinstance(aperture, Aperture):
msg = 'Input aperture must be an Aperture object'
raise TypeError(msg)
if aperture.shape == ():
return _scalar_aperture_to_region(aperture)
# Multiple aperture positions return a Regions object
regs = [_scalar_aperture_to_region(aper) for aper in aperture]
return Regions(regs)
def _scalar_aperture_to_region(aperture):
"""
Convert a given scalar `~photutils.aperture.Aperture` object to a
`regions.Region` object.
Parameters
----------
aperture : `~photutils.aperture.Aperture`
An `~photutils.aperture.Aperture` object to convert. The
``aperture`` must have a single position (scalar).
Returns
-------
region : `regions.Region` or `regions.Regions`
An equivalent `regions.Region` object.
"""
from regions import (CircleAnnulusPixelRegion, CircleAnnulusSkyRegion,
CirclePixelRegion, CircleSkyRegion,
EllipseAnnulusPixelRegion, EllipseAnnulusSkyRegion,
EllipsePixelRegion, EllipseSkyRegion, PixCoord,
RectangleAnnulusPixelRegion,
RectangleAnnulusSkyRegion, RectanglePixelRegion,
RectangleSkyRegion)
if aperture.shape != ():
msg = 'Only scalar (single-position) apertures are supported'
raise ValueError(msg)
if isinstance(aperture, CircularAperture):
region = CirclePixelRegion(PixCoord(*aperture.positions), aperture.r)
elif isinstance(aperture, SkyCircularAperture):
region = CircleSkyRegion(aperture.positions, aperture.r)
elif isinstance(aperture, EllipticalAperture):
region = EllipsePixelRegion(
PixCoord(*aperture.positions), aperture.a * 2, aperture.b * 2,
angle=aperture.theta)
elif isinstance(aperture, SkyEllipticalAperture):
region = EllipseSkyRegion(
aperture.positions, aperture.a * 2, aperture.b * 2,
angle=(aperture.theta + (90 * u.deg)))
elif isinstance(aperture, RectangularAperture):
region = RectanglePixelRegion(
PixCoord(*aperture.positions), aperture.w, aperture.h,
angle=aperture.theta)
elif isinstance(aperture, SkyRectangularAperture):
region = RectangleSkyRegion(
aperture.positions, aperture.w, aperture.h,
angle=(aperture.theta + (90 * u.deg)))
elif isinstance(aperture, CircularAnnulus):
region = CircleAnnulusPixelRegion(
PixCoord(*aperture.positions), aperture.r_in, aperture.r_out)
elif isinstance(aperture, SkyCircularAnnulus):
region = CircleAnnulusSkyRegion(
aperture.positions, aperture.r_in, aperture.r_out)
elif isinstance(aperture, EllipticalAnnulus):
region = EllipseAnnulusPixelRegion(
PixCoord(*aperture.positions),
aperture.a_in * 2, aperture.a_out * 2,
aperture.b_in * 2, aperture.b_out * 2,
angle=aperture.theta)
elif isinstance(aperture, SkyEllipticalAnnulus):
region = EllipseAnnulusSkyRegion(
aperture.positions,
aperture.a_in * 2, aperture.a_out * 2,
aperture.b_in * 2, aperture.b_out * 2,
angle=(aperture.theta + (90 * u.deg)))
elif isinstance(aperture, RectangularAnnulus):
region = RectangleAnnulusPixelRegion(
PixCoord(*aperture.positions),
aperture.w_in, aperture.w_out,
aperture.h_in, aperture.h_out,
angle=aperture.theta)
elif isinstance(aperture, SkyRectangularAnnulus):
region = RectangleAnnulusSkyRegion(
aperture.positions,
aperture.w_in, aperture.w_out,
aperture.h_in, aperture.h_out,
angle=(aperture.theta + (90 * u.deg)))
else:
msg = 'Cannot convert input aperture to a Region object'
raise TypeError(msg)
return region
def _shapely_polygon_to_region(polygon, *, label=None, visual_kwargs=None):
"""
Convert a `shapely.Polygon` object to a `regions.PolygonPixelRegion`
object.
Parameters
----------
polygon : `shapely.Polygon` or `shapely.MultiPolygon`
A Shapely Polygon or MultiPolygon object.
label : str or `None`, optional
A label for the region. If provided, it will be stored in the
meta attribute of the returned `regions.PolygonPixelRegion`
objects.
visual_kwargs : dict or `None`, optional
A dictionary of visual keyword arguments to pass to
`regions.RegionVisual`. If provided, the visual attributes will
be set on the returned region(s).
Returns
-------
result : list of `regions.PolygonPixelRegion` or `regions.Regions`
If the polygon is a `shapely.Polygon`, then a
`regions.PolygonPixelRegion` object is returned. If the polygon
is a `shapely.MultiPolygon`, then a `regions.Regions` object is
returned containing one or more `regions.PolygonPixelRegion`
objects.
Notes
-----
The `regions.PolygonPixelRegion` object does not include the
last Shapely vertex, which is the same as the first vertex. The
`regions.PolygonPixelRegion` does not need to include the last
vertex to close the polygon.
Examples
--------
>>> from shapely import Polygon
>>> from photutils.aperture.converters import _shapely_polygon_to_region
>>> polygon = Polygon([(1, 1), (3, 1), (2, 4), (1, 2)])
>>> region = _shapely_polygon_to_region(polygon)
>>> region
<PolygonPixelRegion(vertices=PixCoord(x=[1.0 3.0 2.0 1.0],
y=[1.0 1.0 4.0 2.0]))>
"""
from regions import PixCoord, PolygonPixelRegion, Regions, RegionVisual
from shapely import MultiPolygon, Polygon
meta = {'label': label} if label is not None else None
visual = RegionVisual(visual_kwargs) if visual_kwargs else None
if isinstance(polygon, Polygon):
x, y = np.transpose(polygon.exterior.coords[:-1])
return PolygonPixelRegion(vertices=PixCoord(x=x, y=y), meta=meta,
visual=visual)
if isinstance(polygon, MultiPolygon):
geoms = []
for poly in polygon.geoms:
x, y = np.transpose(poly.exterior.coords[:-1])
geoms.append(PolygonPixelRegion(vertices=PixCoord(x=x, y=y),
meta=meta, visual=visual))
return Regions(geoms)
msg = 'Input must be a Polygon or MultiPolygon object'
raise TypeError(msg)