.. _overlap-masks:

Computing Overlap Masks
=======================

Defining a region mask within its bounding box
----------------------------------------------

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.RegionMask` 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.data
    array([[0., 1., 1., 1., 0.],
           [1., 1., 1., 1., 1.],
           [1., 1., 1., 1., 1.],
           [1., 1., 1., 1., 1.],
           [0., 1., 1., 1., 0.]])

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

    >>> reg.to_mask()  # doctest: +IGNORE_OUTPUT

is equivalent to::

    >>> reg.to_mask(mode='center')  # doctest: +IGNORE_OUTPUT

The other mask modes that are available are:

* ``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. The mask data values will be between 0 and 1 for
  partial-pixel overlap.

* ``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. The mask
  data values will be between 0 and 1 for partial-pixel overlap.

Here are what the region masks produced by 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(26.6, 27.2)
    reg = CirclePixelRegion(center, 5.2)

    fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(6, 6))

    mask1 = reg.to_mask(mode='center')
    axes[0, 0].set_title("mode='center'", size=9)
    axes[0, 0].imshow(mask1.data, origin='lower')

    mask2 = reg.to_mask(mode='exact')
    axes[0, 1].set_title("mode='exact'", size=9)
    axes[0, 1].imshow(mask2.data, origin='lower')

    mask3 = reg.to_mask(mode='subpixels', subpixels=3)
    axes[1, 0].set_title("mode='subpixels', subpixels=3", size=9)
    axes[1, 0].imshow(mask3.data, origin='lower')

    mask4 = reg.to_mask(mode='subpixels', subpixels=20)
    axes[1, 1].set_title("mode='subpixels', subpixels=20", size=9)
    axes[1, 1].imshow(mask4.data, origin='lower')

As we've seen above, the :class:`~regions.RegionMask` object has a
``data`` attribute that contains a Numpy array with the mask values.
However, if you have, for example, a small circular region with a radius
of 3 pixels at a pixel position of (1000, 1000), it would be inefficient
to store a large mask array that has a size to cover this position (most
of the mask values would be zero). Instead, we store the mask using
the minimal array that contains the region mask along with a ``bbox``
attribute that is a :class:`~regions.RegionBoundingBox` object used to
indicate where the mask should be applied in an image.


Defining a region mask within an image
--------------------------------------

:class:`~regions.RegionMask` objects also have a number of
methods to make it easy to use the masks with data. The
:meth:`~regions.RegionMask.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. For this example, let's place the mask in an image with shape
(50, 50):

.. plot::
    :include-source:

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

    center = PixCoord(26.6, 27.2)
    reg = CirclePixelRegion(center, 5.2)

    mask = reg.to_mask(mode='exact')
    shape = (50, 50)
    fig, ax = plt.subplots(figsize=(4, 4))
    ax.imshow(mask.to_image(shape), origin='lower')


Making image cutouts and multiplying the region mask
----------------------------------------------------

The :meth:`~regions.RegionMask.cutout` method can be used to create a
cutout from the input data over the mask bounding box, and the
:meth:`~regions.RegionMask.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.

These masks can be used, for example, as the building blocks for
photometry, which we demonstrate with a simple example. We start off by
getting an example image:

.. doctest-remote-data::

    >>> from astropy.io import fits
    >>> from astropy.utils.data import get_pkg_data_filename
    >>> filename = get_pkg_data_filename('photometry/M6707HH.fits')  # doctest: +IGNORE_OUTPUT
    >>> hdulist = fits.open(filename)
    >>> hdu = hdulist[0]

We then define a circular aperture region::

    >>> from regions.core import PixCoord
    >>> from regions.shapes.circle import CirclePixelRegion
    >>> center = PixCoord(158.5, 1053.5)
    >>> aperture = CirclePixelRegion(center, 4.)

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

.. doctest-remote-data::

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

Note that ``weighted_data`` will have zeros where the mask is zero; it
therefore should not be used to compute statistics (see :ref:`Masked
Statistics <masked_statistics>` below). To get the mask-weighted pixel
values as a 1D array, excluding the pixels where the mask is zero,
use the :meth:`~regions.RegionMask.get_values` method:

.. doctest-remote-data::

    >>> weighted_data_1d = mask.get_values(hdu.data)
    >>> hdulist.close()

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

.. doctest-skip::

    >>> import matplotlib.pyplot as plt
    >>> fig, axes = plt.subplots(nrows=1, ncols=3)
    >>> axes[0].set_title('Mask', size=9)
    >>> axes[0].imshow(mask.data, origin='lower',
    ...                extent=mask.bbox.extent)
    >>> axes[1].set_title('Data cutout', size=9)
    >>> axes[1].imshow(data, origin='lower',
    ...                extent=mask.bbox.extent)
    >>> axes[2].set_title('Data cutout multiplied by mask', size=9)
    >>> axes[2].imshow(weighted_data, origin='lower',
    ...                extent=mask.bbox.extent)


.. plot::
    :include-source:

    import matplotlib.pyplot as plt
    from astropy.io import fits
    from astropy.utils.data import get_pkg_data_filename
    from regions.core import PixCoord
    from regions.shapes.circle import CirclePixelRegion

    filename = get_pkg_data_filename('photometry/M6707HH.fits')
    hdulist = fits.open(filename)
    hdu = hdulist[0]
    center = PixCoord(158.5, 1053.5)
    aperture = CirclePixelRegion(center, 4.)
    mask = aperture.to_mask(mode='exact')
    data = mask.cutout(hdu.data)
    weighted_data = mask.multiply(hdu.data)

    fig, axes = plt.subplots(nrows=1, ncols=3)
    axes[0].set_title('Mask', size=9)
    axes[0].imshow(mask.data, origin='lower',
                   extent=mask.bbox.extent)
    axes[1].set_title('Data cutout', size=9)
    axes[1].imshow(data, origin='lower',
                   extent=mask.bbox.extent)
    axes[2].set_title('Data cutout multiplied by mask', size=9)
    axes[2].imshow(weighted_data, origin='lower',
                   extent=mask.bbox.extent)
    hdulist.close()

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

.. plot::
    :include-source:

    import matplotlib.pyplot as plt
    from astropy.io import fits
    from astropy.utils.data import get_pkg_data_filename
    from regions.core import PixCoord
    from regions.shapes.circle import CirclePixelRegion

    filename = get_pkg_data_filename('photometry/M6707HH.fits')
    hdulist = fits.open(filename)
    hdu = hdulist[0]
    center = PixCoord(158.5, 1053.5)
    aperture = CirclePixelRegion(center, 4.)
    mask = aperture.to_mask(mode='exact')

    fig, ax = plt.subplots()
    ax.imshow(hdu.data, origin='lower')
    mask.bbox.plot(ax=ax, color='white')
    aperture.plot(ax=ax, color='orange')
    ax.set_xlim(120, 180)
    ax.set_ylim(1000, 1059)
    hdulist.close()


.. _masked_statistics:

Masked Statistics
`````````````````

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

.. doctest-remote-data::

    >>> import numpy as np
    >>> np.average(data, weights=mask)  # doctest: +FLOAT_CMP
    np.float64(9364.012674888021)


.. _interactive-masks:

Interactive Mask Control
------------------------

In the last example we will show how to use a
:ref:`Matplotlib selector<regions-as_mpl_selector>` widget with a custom
``callback`` function for creating a mask and updating it interactively through
the selector.
We first create an :class:`~regions.EllipsePixelRegion` and add an ``as_mpl_selector``
property linked to the Matplotlib axes. This can be moved around to
position it on different sources, and resized just like its Rectangle
counterpart, using the handles of the bounding box.

The user-defined callback function here generates a mask from this region and overlays
it on the image as an alpha filter (keeping the areas outside shaded).
We will use this mask as an aperture as well to calculate integrated
and averaged flux, which is updated live in the text field of the plot as well.

.. plot::
    :include-source:

    import matplotlib.pyplot as plt
    import numpy as np
    from astropy import units as u
    from astropy.io import fits
    from astropy.utils.data import get_pkg_data_filename
    from regions import EllipsePixelRegion, PixCoord

    filename = get_pkg_data_filename('photometry/M6707HH.fits')
    hdulist = fits.open(filename)
    hdu = hdulist[0]

    fig, ax = plt.subplots()
    im = ax.imshow(hdu.data, origin='lower')
    text = ax.text(122, 1002, '', size='small', color='yellow')
    ax.set_xlim(120, 180)
    ax.set_ylim(1000, 1059)

    def update_sel(region):
        mask = region.to_mask(mode='subpixels', subpixels=10)
        im.set_alpha((mask.to_image(hdu.data.shape) + 1) / 2)
        total = mask.multiply(hdu.data).sum()
        mean = np.average(hdu.data, weights=mask.to_image(hdu.data.shape))
        text.set_text(f'Total: {total:g}\nMean: {mean:g}')

    ellipse = EllipsePixelRegion(center=PixCoord(x=126, y=1031),
                                 width=8, height=4,
                                 angle=-0 * u.deg,
                                 visual={'color': 'yellow'})
    selector = ellipse.as_mpl_selector(ax, callback=update_sel)

    hdulist.close()
