Source code for photutils.centroids.gaussian

# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Tools for centroiding sources using Gaussians.
"""

import warnings

import numpy as np
from astropy.modeling.fitting import TRFLSQFitter
from astropy.modeling.models import Gaussian1D, Gaussian2D
from astropy.utils.exceptions import AstropyUserWarning

from photutils.centroids._utils import (_gaussian1d_moments,
                                        _gaussian2d_moments,
                                        _validate_gaussian_inputs)
from photutils.utils._deprecation import deprecated_positional_kwargs
from photutils.utils._quantity_helpers import process_quantities

__all__ = ['centroid_1dg', 'centroid_2dg']


[docs] @deprecated_positional_kwargs(since='3.0', until='4.0') def centroid_1dg(data, error=None, mask=None): """ Calculate the centroid of a 2D array by fitting 1D Gaussians to the marginal ``x`` and ``y`` distributions of the array. Non-finite values (e.g., NaN or inf) in the ``data`` or ``error`` arrays are automatically masked. The final mask is a logical OR combination of the input ``mask``, the automatically generated mask for non-finite values, and the mask of the input ``data`` if it is a `~numpy.ma.MaskedArray`. The centroid is calculated using only the unmasked data values. Parameters ---------- data : 2D array_like The 2D image data. ``data`` can be a `~numpy.ma.MaskedArray`. The image should be a background-subtracted cutout image containing a single source. error : 2D `~numpy.ndarray`, optional The 2D array of the 1-sigma errors of the input ``data``. mask : 2D bool `~numpy.ndarray`, optional A boolean mask, with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. If ``data`` is a `~numpy.ma.MaskedArray`, its mask will be combined (using bitwise OR) with the input ``mask``. Returns ------- centroid : `~numpy.ndarray` The ``x, y`` coordinates of the centroid. Examples -------- >>> import numpy as np >>> from photutils.datasets import make_4gaussians_image >>> from photutils.centroids import centroid_1dg >>> data = make_4gaussians_image() >>> data -= np.median(data[0:30, 0:125]) >>> data = data[40:80, 70:110] >>> x1, y1 = centroid_1dg(data) >>> print(np.array((x1, y1))) [19.96553246 20.04952841] .. plot:: import matplotlib.pyplot as plt import numpy as np from photutils.centroids import centroid_1dg from photutils.datasets import make_4gaussians_image data = make_4gaussians_image() data -= np.median(data[0:30, 0:125]) data = data[40:80, 70:110] xycen = centroid_1dg(data) fig, ax = plt.subplots(1, 1, figsize=(8, 8)) ax.imshow(data, origin='lower') ax.scatter(*xycen, color='red', marker='+', s=100, label='Centroid') ax.legend() """ (data, error), _ = process_quantities((data, error), ('data', 'error')) data, mask, error = _validate_gaussian_inputs(data, mask, error) if error is not None: error_squared = error**2 xy_error = [np.sqrt(np.sum(error_squared, axis=i)) for i in (0, 1)] xy_weights = [1.0 / xy_err.clip(min=1.0e-30) for xy_err in xy_error] else: xy_weights = [np.ones(data.shape[i]) for i in (1, 0)] # Assign zero weight where an entire row or column is masked if np.any(mask): bad_idx = [np.all(mask, axis=i) for i in (0, 1)] for i in (0, 1): xy_weights[i][bad_idx[i]] = 0.0 xy_data = [np.sum(data, axis=i) for i in (0, 1)] # Gaussian1D stddev is bounded to be strictly positive fitter = TRFLSQFitter() centroid = [] for (data_i, weights_i) in zip(xy_data, xy_weights, strict=True): params_init = _gaussian1d_moments(data_i) g_init = Gaussian1D(*params_init) x = np.arange(data_i.size) g_fit = fitter(g_init, x, data_i, weights=weights_i) centroid.append(g_fit.mean.value) return np.array(centroid)
[docs] @deprecated_positional_kwargs(since='3.0', until='4.0') def centroid_2dg(data, error=None, mask=None): """ Calculate the centroid of a 2D array by fitting a 2D Gaussian to the array. Non-finite values (e.g., NaN or inf) in the ``data`` or ``error`` arrays are automatically masked. The final mask is a logical OR combination of the input ``mask``, the automatically generated mask for non-finite values, and the mask of the input ``data`` if it is a `~numpy.ma.MaskedArray`. The centroid is calculated using only the unmasked data values. Parameters ---------- data : 2D array_like The 2D image data. ``data`` can be a `~numpy.ma.MaskedArray`. The image should be a background-subtracted cutout image containing a single source. error : 2D `~numpy.ndarray`, optional The 2D array of the 1-sigma errors of the input ``data``. mask : 2D bool `~numpy.ndarray`, optional A boolean mask, with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. If ``data`` is a `~numpy.ma.MaskedArray`, its mask will be combined (using bitwise OR) with the input ``mask``. Returns ------- centroid : `~numpy.ndarray` The ``x, y`` coordinates of the centroid. Examples -------- >>> import numpy as np >>> from photutils.datasets import make_4gaussians_image >>> from photutils.centroids import centroid_2dg >>> data = make_4gaussians_image() >>> data -= np.median(data[0:30, 0:125]) >>> data = data[40:80, 70:110] >>> x1, y1 = centroid_2dg(data) >>> print(np.array((x1, y1))) [19.9851944 20.01490157] .. plot:: import matplotlib.pyplot as plt import numpy as np from photutils.centroids import centroid_2dg from photutils.datasets import make_4gaussians_image data = make_4gaussians_image() data -= np.median(data[0:30, 0:125]) data = data[40:80, 70:110] xycen = centroid_2dg(data) fig, ax = plt.subplots(1, 1, figsize=(8, 8)) ax.imshow(data, origin='lower') ax.scatter(*xycen, color='red', marker='+', s=100, label='Centroid') ax.legend() """ (data, error), _ = process_quantities((data, error), ('data', 'error')) data, mask, error = _validate_gaussian_inputs(data, mask, error) if np.count_nonzero(~mask) < 6: msg = ('Input data must have a least 6 unmasked values to fit a ' '2D Gaussian.') raise ValueError(msg) # Subtract the minimum of the data to make the data values positive. # Moments from negative data values can yield undefined Gaussian # parameters, e.g., x_stddev and y_stddev. shifted = data - np.min(data) if np.sum(shifted) == 0: msg = ('Input data must have non-constant values to fit a ' '2D Gaussian.') raise ValueError(msg) if error is not None: weights = 1.0 / error.clip(min=1.0e-30) else: weights = np.ones(data.shape) # Assign zero weight to masked pixels if np.any(mask): weights[mask] = 0.0 amplitude, x_mean, y_mean, x_stddev, y_stddev, theta = _gaussian2d_moments( shifted) g_init = Gaussian2D(amplitude=amplitude, x_mean=x_mean, y_mean=y_mean, x_stddev=x_stddev, y_stddev=y_stddev, theta=theta) fitter = TRFLSQFitter() y, x = np.indices(data.shape) with warnings.catch_warnings(record=True) as fit_warnings: warnings.simplefilter('always', AstropyUserWarning) gfit = fitter(g_init, x, y, data, weights=weights) if any(issubclass(w.category, AstropyUserWarning) for w in fit_warnings): msg = 'The fit may not have converged. Please check your results.' warnings.warn(msg, AstropyUserWarning) return np.array([gfit.x_mean.value, gfit.y_mean.value])