Source code for photutils.aperture.photometry

# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Tools for performing aperture photometry.
"""

import warnings

import astropy.units as u
import numpy as np
from astropy.nddata import NDData, StdDevUncertainty
from astropy.utils.exceptions import AstropyUserWarning

from photutils.aperture.converters import region_to_aperture
from photutils.aperture.core import Aperture, SkyAperture, _aperture_metadata
from photutils.utils._deprecation import (create_empty_deprecated_qtable,
                                          deprecated_positional_kwargs)
from photutils.utils._misc import _get_meta

__all__ = ['aperture_photometry']


# Remove in 4.0
_DEPRECATED_COLUMNS: dict = {
    'xcenter': 'x_center',
    'ycenter': 'y_center',
}


[docs] @deprecated_positional_kwargs(since='3.0', until='4.0') def aperture_photometry(data, apertures, error=None, mask=None, method='exact', subpixels=5, wcs=None): """ Perform aperture photometry on the input data by summing the flux within the given aperture(s). Note that this function returns the sum of the (weighted) input ``data`` values within the aperture. It does not convert data in surface brightness units to flux or counts. Conversion from surface-brightness units should be performed before using this function. Parameters ---------- data : array_like, `~astropy.units.Quantity`, `~astropy.nddata.NDData` The 2D array on which to perform photometry. ``data`` should be background-subtracted. If ``data`` is a `~astropy.units.Quantity` array, then ``error`` (if input) must also be a `~astropy.units.Quantity` array with the same units. See the Notes section below for more information about `~astropy.nddata.NDData` input. apertures : `~photutils.aperture.Aperture`, supported `regions.Region`, \ list of `~photutils.aperture.Aperture` or `regions.Region` The aperture(s) to use for the photometry. If ``apertures`` is a list of `~photutils.aperture.Aperture` or `regions.Region`, then they all must have the same position(s). If ``apertures`` contains a `~photutils.aperture.SkyAperture` or `~regions.SkyRegion` object, then a WCS must be input using the ``wcs`` keyword. Region objects are converted to aperture objects. error : array_like or `~astropy.units.Quantity`, optional The pixel-wise Gaussian 1-sigma errors of the input ``data``. ``error`` is assumed to include *all* sources of error, including the Poisson error of the sources (see `~photutils.utils.calc_total_error`). ``error`` must have the same shape as the input ``data``. If a `~astropy.units.Quantity` array, then ``data`` must also be a `~astropy.units.Quantity` array with the same units. mask : array_like (bool), optional A boolean mask with the same shape as ``data`` where a `True` value indicates the corresponding element of ``data`` is masked. Masked data are excluded from all calculations. method : {'exact', 'center', 'subpixel'}, optional The method used to determine the pixel weights (the fraction of the pixel area covered by the aperture): * ``'exact'`` (default): Calculates the exact geometric overlap area. Weights are continuous in the range [0, 1]. * ``'center'``: Binary weighting based on the pixel center. Weights are either 0 or 1. * ``'subpixel'``: Approximates the overlap by averaging binary samples on a subgrid. The number of samples is set by the ``subpixels`` parameter. Weights are discrete in the range [0, 1]. subpixels : int, optional The subsampling factor per axis used when ``method='subpixel'``. Each pixel is divided into a grid of ``subpixels**2`` subpixels to approximate the overlap. This parameter is ignored for other methods. wcs : WCS object, optional A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS <https://docs.astropy.org/en/stable/wcs/wcsapi.html>`_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). If provided, the output table will include a ``'sky_center'`` column with the sky coordinates of the input aperture center(s). This keyword is required if the input ``apertures`` contains a `SkyAperture` or `~regions.SkyRegion`. Returns ------- table : `~astropy.table.QTable` A table of the photometry with the following columns: * ``'id'``: The source ID. * ``'x_center'``, ``'y_center'``: The ``x`` and ``y`` pixel coordinates of the input aperture center(s). * ``'sky_center'``: The sky coordinates of the input aperture center(s). Returned if a ``wcs`` is input. * ``'aperture_sum'``: The sum of the values within the aperture(s). The values are always float64, regardless of the input ``data`` dtype (a `~astropy.units.Quantity` with float64 values if ``data`` has units). * ``'aperture_sum_err'``: The corresponding uncertainty in the ``'aperture_sum'`` values (always float64). Returned only if the input ``error`` is not `None`. The table metadata includes the Astropy and Photutils version numbers and the `aperture_photometry` calling arguments. Notes ----- `~regions.Region` objects are converted to `Aperture` objects using the :func:`region_to_aperture` function. If the input ``data`` is a `~astropy.nddata.NDData` instance, then the ``error``, ``mask``, and ``wcs`` keyword inputs are ignored. Instead, these values should be defined as attributes in the `~astropy.nddata.NDData` object. In the case of ``error``, it must be defined in the ``uncertainty`` attribute with a `~astropy.nddata.StdDevUncertainty` instance. """ if isinstance(data, NDData): nddata_attr = {'error': error, 'mask': mask, 'wcs': wcs} for key, value in nddata_attr.items(): if value is not None: msg = (f'The {key!r} keyword is ignored. Its value ' 'is obtained from the input NDData object.') warnings.warn(msg, AstropyUserWarning) mask = data.mask wcs = data.wcs if isinstance(data.uncertainty, StdDevUncertainty): if data.uncertainty.unit is None: error = data.uncertainty.array else: error = data.uncertainty.array * data.uncertainty.unit if data.unit is not None: data = u.Quantity(data.data, unit=data.unit) else: data = data.data return aperture_photometry(data, apertures, error=error, mask=mask, method=method, subpixels=subpixels, wcs=wcs) single_aperture = False if not isinstance(apertures, (list, tuple, np.ndarray)): single_aperture = True apertures = (apertures,) # Create table metadata using the input apertures, not the converted # ones aper_meta = {} for i, aperture in enumerate(apertures): i = '' if single_aperture else i aper_meta.update(_aperture_metadata(aperture, index=i)) # Convert regions to apertures if necessary apertures = [region_to_aperture(aper) if not isinstance(aper, Aperture) else aper for aper in apertures] # Convert sky to pixel apertures skyaper = False if isinstance(apertures[0], SkyAperture): if wcs is None: msg = ('A WCS transform must be defined by the input data or ' 'the wcs keyword when using a SkyAperture object.') raise ValueError(msg) # Include SkyCoord position in the output table skyaper = True skycoord_pos = apertures[0].positions apertures = [aper.to_pixel(wcs) for aper in apertures] # Compare positions in pixels to avoid comparing SkyCoord objects positions = apertures[0].positions for aper in apertures[1:]: if not np.array_equal(aper.positions, positions): msg = 'Input apertures must all have identical positions' raise ValueError(msg) # Define output table meta data meta = _get_meta() calling_args = f"method='{method}', subpixels={subpixels}" meta['aperture_photometry_args'] = calling_args meta.update(aper_meta) # Replace with QTable in 4.0 tbl = create_empty_deprecated_qtable( _DEPRECATED_COLUMNS, since='3.0', until='4.0') tbl.meta.update(meta) # keep tbl.meta type positions = np.atleast_2d(apertures[0].positions) tbl['id'] = np.arange(positions.shape[0], dtype=int) + 1 xypos_pixel = np.transpose(positions) tbl['x_center'] = xypos_pixel[0] tbl['y_center'] = xypos_pixel[1] if skyaper: if skycoord_pos.isscalar: # Create length-1 SkyCoord array tbl['sky_center'] = skycoord_pos.reshape((-1,)) else: tbl['sky_center'] = skycoord_pos if wcs is not None and not skyaper: tbl['sky_center'] = wcs.pixel_to_world(*np.transpose(positions)) sum_key_main = 'aperture_sum' sum_err_key_main = 'aperture_sum_err' for i, aper in enumerate(apertures): aper_sum, aper_sum_err = aper.do_photometry(data, error=error, mask=mask, method=method, subpixels=subpixels) sum_key = sum_key_main sum_err_key = sum_err_key_main if not single_aperture: sum_key += f'_{i}' sum_err_key += f'_{i}' tbl[sum_key] = aper_sum if error is not None: tbl[sum_err_key] = aper_sum_err return tbl