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

Implement to_mask for circular, elliptical, and rectangular regions #71

Merged
merged 28 commits into from
Nov 25, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e1f7674
Added optimized geometrical code copied over from photutils.
astrofrog Nov 6, 2016
79b8790
First try at implementing circle masking based on Cython functions
keflavich Mar 27, 2016
454d706
Tidy up implementation of CirclePixelRegion.to_mask and add simple test
astrofrog Nov 6, 2016
1f23d08
Return a Mask object
astrofrog Nov 7, 2016
f975e31
Fix tests
astrofrog Nov 7, 2016
6913758
Fix test
astrofrog Nov 7, 2016
4ddad69
Define a Mask object (taken from photutils)
larrybradley Nov 7, 2016
872c117
Adjust Mask class so that tests pass
astrofrog Nov 7, 2016
b63acf2
Add support for subpixels mode in circular to_mask
astrofrog Nov 7, 2016
4918944
Travis: Make sure we always install Cython
astrofrog Nov 7, 2016
687d6f5
Fix tests with latest developer version of Astropy
astrofrog Nov 8, 2016
e369383
Added to_mask implementation to ellipse and rectangle
astrofrog Nov 8, 2016
cbe97b5
Fixed a few bugs and removed 'any' and 'all' modes
astrofrog Nov 8, 2016
0eb845c
Added tests for to_mask with reference text files
astrofrog Nov 8, 2016
50767e0
Rename apply to multiply
astrofrog Nov 8, 2016
ce88deb
Added high-level documentation
astrofrog Nov 9, 2016
2ccdb34
RTD: install cython
astrofrog Nov 9, 2016
8e70009
Update to_mask() API docs
larrybradley Nov 10, 2016
5ea823d
PEP8
larrybradley Nov 10, 2016
bd01613
Update docstring for mask
astrofrog Nov 10, 2016
8091ae7
Validate modes and improve documentation for Mask
astrofrog Nov 10, 2016
1ff4751
Implement simple BoundingBox class and update reference images since …
astrofrog Nov 12, 2016
c7e9891
Make sure that bounding box shape matches mask shape
astrofrog Nov 12, 2016
4bd9260
Added detailed example of how one could use regions for photometry
astrofrog Nov 12, 2016
c2d101d
Updated mask tests to avoid too large reference files
astrofrog Nov 14, 2016
8ecbce0
Make sure data for regions.shapes.tests is installed
astrofrog Nov 25, 2016
e417850
Renamed imin/jmin in bounding box to ixmin/iymin and added an ``as_pa…
astrofrog Nov 25, 2016
89dfed1
Added plotting example for bounding box
astrofrog Nov 25, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .rtd-environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ dependencies:
- matplotlib
- astropy
- wcsaxes
- cython
8 changes: 4 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ env:
- ASTROPY_VERSION=stable
- SETUP_CMD='test'
- MAIN_CMD='python setup.py'
- PIP_DEPENDENCIES=''
- PIP_DEPENDENCIES='https://github.com/astrofrog/pytest-arraydiff/archive/master.zip'
- CONDA_DEPENDENCIES='Cython shapely wcsaxes'
- CONDA_CHANNELS='astropy'
- SETUP_XVFB=True
Expand Down Expand Up @@ -53,7 +53,7 @@ matrix:
# Do a test without the optional dependencies
- python: 3.5
env: SETUP_CMD='test'
CONDA_DEPENDENCIES=''
CONDA_DEPENDENCIES='Cython'


# Try Astropy development version
Expand All @@ -68,10 +68,10 @@ matrix:
# CONDA_CHANNELS environmental variable).
- python: 3.3
env: SETUP_CMD='egg_info'
CONDA_DEPENDENCIES=''
CONDA_DEPENDENCIES='Cython'
- python: 3.3
env: SETUP_CMD='test' NUMPY_VERSION=1.9
CONDA_DEPENDENCIES=''
CONDA_DEPENDENCIES='Cython'

# Try older numpy versions
- python: 2.7
Expand Down
1 change: 1 addition & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ environment:
# Cython code, you can set CONDA_DEPENDENCIES=''
CONDA_CHANNELS: "defaults conda-forge"
CONDA_DEPENDENCIES: "Cython shapely"
PIP_DEPENDENCIES: "https://github.com/astrofrog/pytest-arraydiff/archive/master.zip"

matrix:

Expand Down
12 changes: 12 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,15 @@

edit_on_github_source_root = ""
edit_on_github_doc_root = "docs"

plot_rcparams = {'savefig.bbox': 'tight',
'savefig.facecolor':'none',
'axes.formatter.useoffset': False,
'xtick.labelsize': 9,
'ytick.labelsize': 9,
'axes.labelsize': 11,
'figure.titlesize': 11,
'figure.subplot.wspace': 0.23,
'figure.subplot.hspace': 0.23}

plot_apply_rcparams = True
216 changes: 208 additions & 8 deletions docs/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -402,17 +402,217 @@ or `regions.PixelRegion.contains` methods:
Masks
=====

For aperture photometry, a common operation is to compute, for a given image and region,
a boolean mask or array of pixel indices defining which pixels (in the whole image or a
minimal rectangular bounding box) are inside and outside the region.
For aperture photometry, a common operation is to compute, for a given image and
region, a mask or array of pixel indices defining which pixels (in the whole
image or a minimal rectangular bounding box) are inside and outside the region.

All :class:`~regions.PixelRegion` objects have a
:meth:`~regions.PixelRegion.to_mask` method that returns a
:class:`~regions.Mask` object that contains information about whether
pixels are inside the region, and can be used to mask data arrays:

>>> from regions.core import PixCoord
>>> from regions.shapes.circle import CirclePixelRegion
>>> center = PixCoord(4., 5.)
>>> reg = CirclePixelRegion(center, 2.3411)
>>> mask = reg.to_mask()
>>> mask
<regions.mask.Mask object at 0x10900b5c0>
>>> mask.data
array([[ 0., 0., 0., 0., 0., 0., 0.],
[ 0., 0., 1., 1., 1., 0., 0.],
[ 0., 1., 1., 1., 1., 1., 0.],
[ 0., 1., 1., 1., 1., 1., 0.],
[ 0., 1., 1., 1., 1., 1., 0.],
[ 0., 0., 1., 1., 1., 0., 0.],
[ 0., 0., 0., 0., 0., 0., 0.]])

The mask data contains floating point that are between 0 (no overlap) and 1
(overlap). By default, this is determined by looking only at the central position
in each pixel, and::

>>> reg.to_mask()

is equivalent to::

>>> reg.to_mask(mode='center')

but other modes are available:

* ``mode='exact'``: the overlap is determined using the exact geometrical
overlap between pixels and the region. This is slower than using the central
position, but allows partial overlap to be treated correctly.

* ``mode='subpixels'``: the overlap is determined by sub-sampling the pixel
using a grid of sub-pixels. The number of sub-pixels to use in this mode
should be given using the ``subpixels`` argument.

Here are what the different modes look like:

.. plot::
:include-source:

import matplotlib.pyplot as plt
from regions.core import PixCoord
from regions.shapes.circle import CirclePixelRegion

center = PixCoord(6.6, 7.2)
reg = CirclePixelRegion(center, 5.2)

plt.figure(figsize=(6, 6))

mask1 = reg.to_mask(mode='center')
plt.subplot(2, 2, 1)
plt.title("mode='center'", size=9)
plt.imshow(mask1.data, cmap=plt.cm.viridis,
interpolation='nearest', origin='lower')

mask2 = reg.to_mask(mode='exact')
plt.subplot(2, 2, 2)
plt.title("mode='exact'", size=9)
plt.imshow(mask2.data, cmap=plt.cm.viridis,
interpolation='nearest', origin='lower')

mask3 = reg.to_mask(mode='subpixels', subpixels=3)
plt.subplot(2, 2, 3)
plt.title("mode='subpixels', subpixels=3", size=9)
plt.imshow(mask3.data, cmap=plt.cm.viridis,
interpolation='nearest', origin='lower')

mask4 = reg.to_mask(mode='subpixels', subpixels=20)
plt.subplot(2, 2, 4)
plt.title("mode='subpixels', subpixels=20", size=9)
plt.imshow(mask4.data, cmap=plt.cm.viridis,
interpolation='nearest', origin='lower')

As we've seen above, the :class:`~regions.Mask` objects have a ``data``
attribute that contains a Numpy array with the mask values. However, if you
have for example a circular region with a radius of 3 pixels at a pixel position
of (1000, 1000), it would be inefficient to store a mask that has a size larger
than this, so instead we store the mask using the minimal array that contains
the mask, and the :class:`~regions.Mask` objects also include a ``bbox``
attribute that is a :class:`~regions.BoundingBox` object used to indicate where
the mask should be applied in an image.

:class:`~regions.Mask` objects also have a number of methods to make it
easy to use the masks with data. The :meth:`~regions.Mask.to_image` method
can be used to obtain an image of the mask in a 2D array of the given shape.
This places the mask in the correct place in the image and deals properly with
boundary effects:

.. plot::
:include-source:

import matplotlib.pyplot as plt
from regions.core import PixCoord
from regions.shapes.circle import CirclePixelRegion

center = PixCoord(6.6, 7.2)
reg = CirclePixelRegion(center, 5.2)

mask = reg.to_mask(mode='exact')
plt.figure(figsize=(4, 4))
plt.imshow(mask.to_image((10, 10)), cmap=plt.cm.viridis,
interpolation='nearest', origin='lower')

The :meth:`~regions.Mask.cutout` method can be used to create a cutout from
the input data over the mask bounding box, and the
:meth:`~regions.Mask.multiply` method can be used to multiply the aperture
mask with the input data to create a mask-weighted data cutout. All of these
methods properly handle the cases of partial or no overlap of the aperture mask
with the data.

To a certain degree, such spatial filtering can be done using the methods described in the previous :ref:`gs-contain`
section. Apart from that, no high-level functionality for spatial filtering, bounding boxes or aperture photometry
is available yet.
These masks can be used as the building blocks for photometry, which we
demonstrate with a simple example. We start off by getting an example image:

.. plot::
:context: reset
:include-source:
:align: center
:nofigs:

>>> from astropy.io import fits
>>> from astropy.utils.data import get_pkg_data_filename
>>> filename = get_pkg_data_filename('photometry/M6707HH.fits')
>>> hdu = fits.open(filename)[0]

We then define the aperture:

.. plot::
:context:
:include-source:
:align: center
:nofigs:

For now, please use `photutils`_ or `pyregion`_.
>>> from regions.core import PixCoord
>>> from regions.shapes.circle import CirclePixelRegion
>>> center = PixCoord(158.5, 1053.5)
>>> aperture = CirclePixelRegion(center, 4.)

We convert the aperture to a mask and extract a cutout from the data, as well
as a cutout with the data multipled by the mask:

.. plot::
:context:
:include-source:
:align: center
:nofigs:

>>> mask = aperture.to_mask(mode='exact')
>>> data = mask.cutout(hdu.data)
>>> weighted_data = mask.multiply(hdu.data)

We can take a look at the results to make sure the source overlaps with the
aperture:

.. plot::
:context:
:include-source:
:align: center

>>> import matplotlib.pyplot as plt
>>> plt.subplot(1,3,1)
>>> plt.title("Mask", size=9)
>>> plt.imshow(mask.data, cmap=plt.cm.viridis,
... interpolation='nearest', origin='lower',
... extent=mask.bbox.extent)
>>> plt.subplot(1,3,2)
>>> plt.title("Data cutout", size=9)
>>> plt.imshow(data, cmap=plt.cm.viridis,
... interpolation='nearest', origin='lower',
... extent=mask.bbox.extent)
>>> plt.subplot(1,3,3)
>>> plt.title("Data cutout multiplied by mask", size=9)
>>> plt.imshow(weighted_data, cmap=plt.cm.viridis,
... interpolation='nearest', origin='lower',
... extent=mask.bbox.extent)

We can also use the ``Mask.bbox`` attribute to look at the extent
of the mask in the image:

.. plot::
:context:
:include-source:
:align: center

>>> ax = plt.subplot(1, 1, 1)
>>> ax.imshow(hdu.data, cmap=plt.cm.viridis,
... interpolation='nearest', origin='lower')
>>> ax.add_patch(mask.bbox.as_patch(facecolor='none', edgecolor='white'))
>>> ax.add_patch(aperture.as_patch(facecolor='none', edgecolor='orange'))
>>> ax.set_xlim(120, 180)
>>> ax.set_ylim(1020, 1080)

Finally, we can use the mask and data values to compute weighted statistics:

.. plot::
:context:
:include-source:
:align: center
:nofigs:

(We plan to merge that functionality from ``photutils`` or ``pyregion`` into this ``regions`` package, or re-implement it.)
>>> np.average(data, weights=mask)
9364.0126748880211

.. _gs-compound:

Expand Down
1 change: 1 addition & 0 deletions regions/_geometry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# This sub-package contains fast geometrical routines that are then used by different shapes
12 changes: 12 additions & 0 deletions regions/_geometry/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Geometry subpackage for low-level geometry functions.
"""

from .circular_overlap import *
from .elliptical_overlap import *
from .rectangular_overlap import *


__all__ = ['circular_overlap_grid', 'elliptical_overlap_grid',
'rectangular_overlap_grid']
Loading