Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename Mask class and add origin arg to plot methods #203

Merged
merged 10 commits into from Aug 27, 2018
4 changes: 2 additions & 2 deletions docs/plot_compound.py
Expand Up @@ -56,8 +56,8 @@
ax.scatter(skycoords_and.l.value, skycoords_and.b.value, color='magenta',
label='and', transform=ax.get_transform('galactic'))

circle1.to_pixel(wcs=wcs).plot(ax, edgecolor='green', facecolor='none', alpha=0.8, lw=3)
circle2.to_pixel(wcs=wcs).plot(ax, edgecolor='red', facecolor='none', alpha=0.8, lw=3)
circle1.to_pixel(wcs=wcs).plot(ax=ax, edgecolor='green', facecolor='none', alpha=0.8, lw=3)
circle2.to_pixel(wcs=wcs).plot(ax=ax, edgecolor='red', facecolor='none', alpha=0.8, lw=3)

ax.legend(loc='lower right')

Expand Down
2 changes: 1 addition & 1 deletion docs/plot_example.py
Expand Up @@ -27,6 +27,6 @@
region = CircleSkyRegion(center=center, radius=radius)
pix_region = region.to_pixel(wcs=wcs)

pix_region.plot(ax, edgecolor='yellow', facecolor='yellow', alpha=0.5, lw=3)
pix_region.plot(ax=ax, edgecolor='yellow', facecolor='yellow', alpha=0.5, lw=3)

plt.show()
2 changes: 1 addition & 1 deletion docs/plot_example_pix.py
Expand Up @@ -9,5 +9,5 @@

data = np.arange(10 * 15).reshape((10, 15))
ax.imshow(data, cmap='gray', interpolation='nearest', origin='lower')
region.plot(ax, color='red')
region.plot(ax=ax, color='red')
plt.show()
2 changes: 1 addition & 1 deletion docs/plot_reg.py
Expand Up @@ -19,6 +19,6 @@
regs = read_ds9('plot_image.reg')

for i, reg in enumerate(regs):
reg.plot(ax)
reg.plot(ax=ax)

plt.show()
27 changes: 23 additions & 4 deletions docs/plotting.rst
Expand Up @@ -21,6 +21,8 @@ To draw a matplotlib patch object, add it to an `matplotlib.axes.Axes` object.
axes = plt.gca()
axes.set_aspect('equal')
axes.add_patch(patch)
axes.set_xlim([-0.5, 1])
axes.set_ylim([-0.5, 1])

plt.show()

Expand All @@ -29,10 +31,27 @@ The :meth:`~regions.PixelRegion.plot`, a convenience method just does these two
steps at once (creating a matplotlib patch and adding it to an axis),
and calls ``plt.gca()`` if no axis is passed in.

Note that not all pixel regions have ``as_patch()`` methods, e.g.
the `~regions.CircleAnnulusRegion`, `~regions.PointPixelRegion` and
`~regions.CompundPixelRegion` don't have because there are no equivalent classes
in `matplotlib.patches`.
You can shift the origin of the region very conveniently while plotting by simply
supplying the ``origin`` pixel coordinates to :meth:`~regions.PixelRegion.plot`
and :meth:`~regions.PixelRegion.as_patch`. The ``**kwargs`` argument takes any
keyword argument that the `~matplotlib.patches.Patch` object accepts. For e.g.:

.. plot::
:include-source:

from regions import PixCoord, BoundingBox
import matplotlib.pyplot as plt

bbox = BoundingBox(ixmin=-1, ixmax=1, iymin=-2, iymax=2)
# shifting the origin to (1, 1) pixel position
ax = bbox.plot(origin=(1, 1), edgecolor='yellow', facecolor='red', fill=True)
ax.set_xlim([-4, 2])
ax.set_ylim([-4, 2])
plt.show()

Pixel regions such as `~regions.TextPixelRegion` and
`~regions.CompoundPixelRegion` don't have ``as_patch()`` method as
there are no equivalent classes in `matplotlib.patches`.

Here's a full example how to plot a `~regions.CirclePixelRegion` on an image.

Expand Down
43 changes: 42 additions & 1 deletion regions/core/bounding_box.py
Expand Up @@ -188,7 +188,7 @@ def as_patch(self, **kwargs):

Parameters
----------
kwargs
kwargs : `dict`
Any keyword arguments accepted by
`matplotlib.patches.Patch`.

Expand Down Expand Up @@ -216,3 +216,44 @@ def as_patch(self, **kwargs):

return Rectangle(xy=(self.extent[0], self.extent[2]),
width=self.shape[1], height=self.shape[0], **kwargs)

def to_region(self):
"""
Return a `~regions.RectanglePixelRegion` that
represents the bounding box.
"""

from ..shapes import RectanglePixelRegion
from .pixcoord import PixCoord

xpos = (self.extent[1] + self.extent[0]) / 2.
ypos = (self.extent[3] + self.extent[2]) / 2.
xypos = PixCoord(xpos, ypos)
h, w = self.shape

return RectanglePixelRegion(center=xypos, width=w, height=h)

def plot(self, origin=(0, 0), ax=None, **kwargs):
"""
Plot the `BoundingBox` on a matplotlib `~matplotlib.axes.Axes`
instance.

Parameters
----------
origin : array_like, optional
The ``(x, y)`` position of the origin of the displayed
image.
ax : `matplotlib.axes.Axes` instance, optional
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whitespace is inconsistent; no before :

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, sphinx doesn't render properly if there is no before :

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh... my bad, you have to add spaces in other places, not remove them here

If `None`, then the current `~matplotlib.axes.Axes` instance
is used.
kwargs : `dict`
Any keyword arguments accepted by `matplotlib.patches.Patch`.

Returns
-------
ax : `~matplotlib.axes.Axes`
Axes on which the patch is added.
"""

reg = self.to_region()
return reg.plot(origin=origin, ax=ax, **kwargs)
28 changes: 22 additions & 6 deletions regions/core/compound.py
Expand Up @@ -5,9 +5,9 @@

import numpy as np

from . import PixelRegion, SkyRegion, BoundingBox, Mask
from . import PixelRegion, SkyRegion, BoundingBox, RegionMask
from ..core.attributes import (CompoundRegionPix, CompoundRegionSky,
RegionVisual, RegionMeta)
RegionMeta, RegionVisual)

__all__ = ['CompoundPixelRegion', 'CompoundSkyRegion']

Expand Down Expand Up @@ -91,7 +91,7 @@ def to_mask(self, mode='center', subpixels=1):
'constant'))

data = self.operator(*np.array(padded_data, dtype=np.int))
return Mask(data=data, bbox=bbox)
return RegionMask(data=data, bbox=bbox)

def to_sky(self, wcs):
skyreg1 = self.region1.to_sky(wcs=wcs)
Expand Down Expand Up @@ -129,13 +129,29 @@ def _make_annulus_path(patch_inner, patch_outer):

return mpath.Path(verts, codes)

def as_patch(self, **kwargs):
def as_patch(self, origin=(0, 0), **kwargs):
"""
Matplotlib patch object for annulus region (`matplotlib.patches.PathPatch`).

Parameters
----------
origin : array_like, optional
The ``(x, y)`` pixel position of the origin of the displayed image.
Default is (0, 0).
kwargs : `dict`
All keywords that a `~matplotlib.patches.PathPatch` object accepts

Returns
-------
patch : `~matplotlib.patches.PathPatch`
Matplotlib patch object
"""

if self.region1.center == self.region2.center and self.operator == op.xor:
import matplotlib.patches as mpatches

patch_inner = self.region1.as_patch()
patch_outer = self.region2.as_patch()
patch_inner = self.region1.as_patch(origin=origin)
patch_outer = self.region2.as_patch(origin=origin)
path = self._make_annulus_path(patch_inner, patch_outer)
patch = mpatches.PathPatch(path, **kwargs)
return patch
Expand Down
24 changes: 17 additions & 7 deletions regions/core/core.py
Expand Up @@ -236,10 +236,19 @@ def _validate_mode(mode, subpixels):
" a strictly positive integer)".format(subpixels))

@abc.abstractmethod
def as_patch(self, **kwargs):
def as_patch(self, origin=(0, 0), **kwargs):
"""
Convert to mpl patch

Parameters
----------
origin : array_like, optional
The ``(x, y)`` pixel position of the origin of the displayed image.
Default is (0, 0).

kwargs: `dict`
keywords that a `~matplotlib.patches.Patch` accepts

Returns
-------
patch : `~matplotlib.patches.Patch`
Expand Down Expand Up @@ -279,31 +288,32 @@ def mpl_properties_default(self, shape='patch'):

return kwargs

def plot(self, ax=None, **kwargs):
def plot(self, origin=(0, 0), ax=None, **kwargs):
"""
Calls ``as_patch`` method forwarding all kwargs and adds patch
to given axis.

Parameters
----------
origin : array_like, optional
The ``(x, y)`` pixel position of the origin of the displayed image.
Default is (0, 0).
ax : `~matplotlib.axes`, optional
Axis
kwargs: `dict`
keywords that a `~matplotlib.text.Text` accepts
keywords that a `~matplotlib.patches.Patch` accepts

Returns
-------
ax: `~matplotlib.axes`
Axis on which the patch is added.
Axes on which the patch is added.
"""
import matplotlib.pyplot as plt

if ax is None:
ax = plt.gca()

mpl_params = self.mpl_properties_default('patch')
mpl_params.update(kwargs)
patch = self.as_patch(**mpl_params)
patch = self.as_patch(origin=origin, **kwargs)
ax.add_patch(patch)

return ax
Expand Down
4 changes: 2 additions & 2 deletions regions/core/mask.py
Expand Up @@ -6,10 +6,10 @@
import astropy.units as u


__all__ = ['Mask']
__all__ = ['RegionMask']


class Mask(object):
class RegionMask(object):
"""
Class for a region mask.

Expand Down
8 changes: 4 additions & 4 deletions regions/core/tests/test_mask.py
Expand Up @@ -7,7 +7,7 @@
import pytest

from ..bounding_box import BoundingBox
from ..mask import Mask
from ..mask import RegionMask
from ..pixcoord import PixCoord
from ...shapes import CirclePixelRegion

Expand All @@ -25,21 +25,21 @@ def test_mask_input_shapes():
with pytest.raises(ValueError):
mask_data = np.ones((10, 10))
bbox = BoundingBox(5, 10, 5, 10)
Mask(mask_data, bbox)
RegionMask(mask_data, bbox)


def test_mask_array():
mask_data = np.ones((10, 10))
bbox = BoundingBox(5, 15, 5, 15)
mask = Mask(mask_data, bbox)
mask = RegionMask(mask_data, bbox)
data = np.array(mask)
assert_allclose(data, mask.data)


def test_mask_cutout_shape():
mask_data = np.ones((10, 10))
bbox = BoundingBox(5, 15, 5, 15)
mask = Mask(mask_data, bbox)
mask = RegionMask(mask_data, bbox)

with pytest.raises(ValueError):
mask.cutout(np.arange(10))
Expand Down
33 changes: 26 additions & 7 deletions regions/shapes/circle.py
Expand Up @@ -7,7 +7,7 @@
from astropy.coordinates import Angle, SkyCoord
from astropy.wcs.utils import pixel_to_skycoord

from ..core import PixCoord, PixelRegion, SkyRegion, Mask, BoundingBox
from ..core import PixCoord, PixelRegion, SkyRegion, RegionMask, BoundingBox
from .._utils.wcs_helpers import skycoord_to_pixel_scale_angle
from .._geometry import circular_overlap_grid
from ..core.attributes import (ScalarSky, ScalarPix, QuantityLength,
Expand Down Expand Up @@ -121,14 +121,33 @@ def to_mask(self, mode='center', subpixels=1):
fraction = circular_overlap_grid(xmin, xmax, ymin, ymax, nx, ny,
self.radius, use_exact, subpixels)

return Mask(fraction, bbox=bbox)

def as_patch(self, **kwargs):
"""Matplotlib patch object for this region (`matplotlib.patches.Circle`)"""
return RegionMask(fraction, bbox=bbox)

def as_patch(self, origin=(0, 0), **kwargs):
"""
Matplotlib patch object for this region (`matplotlib.patches.Circle`)

Parameters:
-----------
origin : array_like, optional
The ``(x, y)`` pixel position of the origin of the displayed image.
Default is (0, 0).
kwargs: dict
All keywords that a `~matplotlib.patches.Circle` object accepts

Returns
-------
patch : `~matplotlib.patches.Circle`
Matplotlib circle patch
"""
from matplotlib.patches import Circle
xy = self.center.x, self.center.y
xy = self.center.x - origin[0], self.center.y - origin[1]
radius = self.radius
return Circle(xy=xy, radius=radius, **kwargs)

mpl_params = self.mpl_properties_default('patch')
mpl_params.update(kwargs)

return Circle(xy=xy, radius=radius, **mpl_params)


class CircleSkyRegion(SkyRegion):
Expand Down
33 changes: 27 additions & 6 deletions regions/shapes/ellipse.py
Expand Up @@ -8,7 +8,7 @@
from astropy.coordinates import Angle
from astropy.wcs.utils import pixel_to_skycoord

from ..core import PixCoord, PixelRegion, SkyRegion, Mask, BoundingBox
from ..core import PixCoord, PixelRegion, SkyRegion, RegionMask, BoundingBox
from .._geometry import elliptical_overlap_grid
from .._utils.wcs_helpers import skycoord_to_pixel_scale_angle
from ..core.attributes import (ScalarPix, ScalarLength, QuantityLength,
Expand Down Expand Up @@ -180,17 +180,38 @@ def to_mask(self, mode='center', subpixels=5):
use_exact, subpixels,
)

return Mask(fraction, bbox=bbox)
return RegionMask(fraction, bbox=bbox)

def as_patch(self, **kwargs):
"""Matplotlib patch object for this region (`matplotlib.patches.Ellipse`)."""
def as_patch(self, origin=(0, 0), **kwargs):
"""
Matplotlib patch object for this region (`matplotlib.patches.Ellipse`).

Parameters:
-----------
origin : array_like, optional
The ``(x, y)`` pixel position of the origin of the displayed image.
Default is (0, 0).
kwargs: dict
All keywords that a `~matplotlib.patches.Ellipse` object accepts

Returns
-------
patch : `~matplotlib.patches.Ellipse`
Matplotlib ellipse patch

"""
from matplotlib.patches import Ellipse
xy = self.center.x, self.center.y
xy = self.center.x - origin[0], self.center.y - origin[1]
width = self.width
height = self.height
# From the docstring: MPL expects "rotation in degrees (anti-clockwise)"
angle = self.angle.to('deg').value
return Ellipse(xy=xy, width=width, height=height, angle=angle, **kwargs)

mpl_params = self.mpl_properties_default('patch')
mpl_params.update(kwargs)

return Ellipse(xy=xy, width=width, height=height, angle=angle,
**mpl_params)


class EllipseSkyRegion(SkyRegion):
Expand Down