Source code for photutils.centroids.core

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

import inspect
import warnings

import numpy as np
from astropy.nddata import overlap_slices
from astropy.utils.exceptions import AstropyUserWarning

from photutils.centroids._utils import _process_data_mask
from photutils.utils._deprecation import (deprecated_positional_kwargs,
                                          deprecated_renamed_argument)
from photutils.utils._parameters import as_pair
from photutils.utils._quantity_helpers import process_quantities
from photutils.utils._repr import make_repr
from photutils.utils._round import round_half_away

__all__ = ['CentroidQuadratic', 'centroid_com', 'centroid_quadratic',
           'centroid_sources']


[docs] @deprecated_positional_kwargs(since='3.0', until='4.0') def centroid_com(data, mask=None): """ Calculate the centroid of an array as the flux-weighted center of mass derived from `image moments <https://en.wikipedia.org/wiki/Image_moment>`_. Non-finite values (e.g., NaN or inf) in the ``data`` array 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 : array_like The input n-dimensional array. ``data`` can be a `~numpy.ma.MaskedArray`. The image should be a background-subtracted cutout image containing a single source. The source should be significantly stronger than the background noise. If the data contains nearly equal positive and negative values (i.e., the sum is close to zero), the centroid calculation will be numerically unstable and may produce undefined results that fall outside the array bounds. mask : 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 coordinates of the centroid in pixel order (e.g., ``(x, y)`` or ``(x, y, z)``), not numpy axis order. If the sum of the (unmasked) data is zero, then a `~numpy.ndarray` of NaN values will be returned. If the sum is close to zero, the centroid may be poorly defined and fall outside the array bounds. Notes ----- The centroid is calculated as: .. math:: x_c = \\frac{\\sum x_i I_i}{\\sum I_i}, \\quad y_c = \\frac{\\sum y_i I_i}{\\sum I_i} where :math:`I_i` is the intensity at pixel :math:`(x_i, y_i)`. Examples -------- >>> import numpy as np >>> from photutils.datasets import make_4gaussians_image >>> from photutils.centroids import centroid_com >>> data = make_4gaussians_image() >>> data -= np.median(data[0:30, 0:125]) >>> data = data[40:80, 70:110] >>> x1, y1 = centroid_com(data) >>> print(np.array((x1, y1))) [19.9796724 20.00992593] .. plot:: import matplotlib.pyplot as plt import numpy as np from photutils.centroids import centroid_com 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_com(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,), _ = process_quantities((data,), ('data',)) data = _process_data_mask(data, mask, ndim=None, fill_value=0.0) total = np.sum(data) if abs(total) < 1.e-30: return np.full(data.ndim, np.nan) indices = np.ogrid[tuple(slice(0, i) for i in data.shape)] # Output array is reversed to give (x, y) order (e.g., for 2D data) return np.array([np.sum(indices[axis] * data) / total for axis in range(data.ndim)])[::-1]
[docs] @deprecated_positional_kwargs(since='3.0', until='4.0') @deprecated_renamed_argument('xpeak', None, '3.0', until='4.0') @deprecated_renamed_argument('ypeak', None, '3.0', until='4.0') @deprecated_renamed_argument('search_boxsize', None, '3.0', until='4.0') def centroid_quadratic(data, mask=None, fit_boxsize=5, xpeak=None, ypeak=None, search_boxsize=None): """ Calculate the centroid of a 2D array by fitting a 2D quadratic polynomial. Non-finite values (e.g., NaN or inf) in the ``data`` array 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. A second degree 2D polynomial is fit within a small region of the data defined by ``fit_boxsize`` to calculate the centroid position. The initial center of the fitting box can be specified using the ``xpeak`` and ``ypeak`` keywords. If both ``xpeak`` and ``ypeak`` are `None`, then the box will be centered at the position of the maximum value in the input ``data``. If ``xpeak`` and ``ypeak`` are specified, the ``search_boxsize`` optional keyword can be used to further refine the initial center of the fitting box by searching for the position of the maximum pixel within a box of size ``search_boxsize``. `Vakili & Hogg (2016) <https://arxiv.org/abs/1610.05873>`_ demonstrate that 2D quadratic centroiding comes very close to saturating the `Cramér-Rao lower bound <https://en.wikipedia.org/wiki/Cram%C3%A9r%E2%80%93Rao_bound>`_ in a wide range of conditions. 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. 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. Masked data are excluded from calculations. If ``data`` is a `~numpy.ma.MaskedArray`, its mask will be combined (using bitwise OR) with the input ``mask``. fit_boxsize : int or tuple of int, optional The size (in pixels) of the box used to define the fitting region. If ``fit_boxsize`` has two elements, they must be in ``(ny, nx)`` order. If ``fit_boxsize`` is a scalar then a square box of size ``fit_boxsize`` will be used. ``fit_boxsize`` must have odd values for both axes. xpeak, ypeak : float or `None`, optional The initial guess of the position of the centroid. If either ``xpeak`` or ``ypeak`` is `None` then the position of the maximum value in the input ``data`` will be used as the initial guess. .. deprecated:: 3.0 The ``xpeak`` and ``ypeak`` keywords are deprecated and will be removed in a future version. Use `~photutils.centroids.centroid_sources` to centroid sources at specific positions. search_boxsize : int or tuple of int, optional The size (in pixels) of the box used to search for the maximum pixel value if ``xpeak`` and ``ypeak`` are both specified. If ``search_boxsize`` has two elements, they must be in ``(ny, nx)`` order. If ``search_boxsize`` is a scalar then a square box of size ``search_boxsize`` will be used. ``search_boxsize`` must have odd values for both axes. This parameter is ignored if either ``xpeak`` or ``ypeak`` is `None`. In that case, the entire array is searched for the maximum value. .. deprecated:: 3.0 The ``search_boxsize`` keyword is deprecated and will be removed in a future version. Use `~photutils.centroids.centroid_sources` to centroid sources at specific positions. Returns ------- centroid : `~numpy.ndarray` The ``x, y`` coordinates of the centroid. Notes ----- Use ``fit_boxsize = (3, 3)`` to match the work of `Vakili & Hogg (2016) <https://arxiv.org/abs/1610.05873>`_ for their 2D second-order polynomial centroiding method. Because this centroid is based on fitting data, it can fail for many reasons, returning (np.nan, np.nan): * quadratic fit failed * quadratic fit does not have a maximum * quadratic fit maximum falls outside image * not enough unmasked data points (6 are required) Also note that a fit is not performed if the maximum data value is at the edge of the data. In this case, the position of the maximum pixel will be returned. References ---------- .. [1] Vakili and Hogg 2016, "Do fast stellar centroiding methods saturate the Cramér-Rao lower bound?", `arXiv:1610.05873 <https://arxiv.org/abs/1610.05873>`_ Examples -------- >>> import numpy as np >>> from photutils.datasets import make_4gaussians_image >>> from photutils.centroids import centroid_quadratic >>> data = make_4gaussians_image() >>> data -= np.median(data[0:30, 0:125]) >>> data = data[40:80, 70:110] >>> x1, y1 = centroid_quadratic(data) >>> print(np.array((x1, y1))) [19.94009505 20.06884997] .. plot:: import matplotlib.pyplot as plt import numpy as np from photutils.centroids import centroid_quadratic 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_quadratic(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,), _ = process_quantities((data,), ('data',)) if ((xpeak is None and ypeak is not None) or (xpeak is not None and ypeak is None)): msg = 'xpeak and ypeak must both be input or "None"' raise ValueError(msg) data = _process_data_mask(data, mask) ny, nx = data.shape fit_boxsize = as_pair('fit_boxsize', fit_boxsize, lower_bound=(0, 0), upper_bound=data.shape, check_odd=True) if np.prod(fit_boxsize) < 6: msg = ('fit_boxsize is too small. 6 values are required to fit a ' '2D quadratic polynomial.') raise ValueError(msg) if xpeak is not None and ((xpeak < 0) or (xpeak > data.shape[1] - 1)): msg = 'xpeak is outside the input data' raise ValueError(msg) if ypeak is not None and ((ypeak < 0) or (ypeak > data.shape[0] - 1)): msg = 'ypeak is outside the input data' raise ValueError(msg) if xpeak is None or ypeak is None: yidx, xidx = np.unravel_index(np.nanargmax(data), data.shape) else: xidx = round_half_away(xpeak) yidx = round_half_away(ypeak) if search_boxsize is not None: search_boxsize = as_pair('search_boxsize', search_boxsize, lower_bound=(0, 0), upper_bound=data.shape, check_odd=True) slc_data, _ = overlap_slices(data.shape, search_boxsize, (yidx, xidx), mode='trim') cutout = data[slc_data] yidx, xidx = np.unravel_index(np.nanargmax(cutout), cutout.shape) xidx += slc_data[1].start yidx += slc_data[0].start # Return the position of the maximum if it is at the edge of the # data if xidx in (0, nx - 1) or yidx in (0, ny - 1): msg = ('maximum value is at the edge of the data and its ' 'position was returned; no quadratic fit was performed') warnings.warn(msg, AstropyUserWarning) return np.array((xidx, yidx), dtype=float) # Extract the fitting region slc_data, _ = overlap_slices(data.shape, fit_boxsize, (yidx, xidx), mode='trim') xidx0, xidx1 = (slc_data[1].start, slc_data[1].stop) yidx0, yidx1 = (slc_data[0].start, slc_data[0].stop) # Shift the fitting box if it was clipped by the data edge if (xidx1 - xidx0) < fit_boxsize[1]: if xidx0 == 0: xidx1 = min(nx, xidx0 + fit_boxsize[1]) if xidx1 == nx: xidx0 = max(0, xidx1 - fit_boxsize[1]) if (yidx1 - yidx0) < fit_boxsize[0]: if yidx0 == 0: yidx1 = min(ny, yidx0 + fit_boxsize[0]) if yidx1 == ny: yidx0 = max(0, yidx1 - fit_boxsize[0]) cutout = data[yidx0:yidx1, xidx0:xidx1].ravel() if np.count_nonzero(~np.isnan(cutout)) < 6: msg = ('at least 6 unmasked data points are required to ' 'perform a 2D quadratic fit') warnings.warn(msg, AstropyUserWarning) return np.array((np.nan, np.nan)) # Fit a 2D quadratic polynomial to the fitting region xi = np.arange(xidx0, xidx1) yi = np.arange(yidx0, yidx1) x, y = np.meshgrid(xi, yi) x = x.ravel() y = y.ravel() # Pre-allocate coefficient matrix for optimization coeff_matrix = np.empty((x.size, 6), dtype=float) coeff_matrix[:, 0] = 1 coeff_matrix[:, 1] = x coeff_matrix[:, 2] = y coeff_matrix[:, 3] = x * y coeff_matrix[:, 4] = x * x coeff_matrix[:, 5] = y * y # Include only finite values in the fit. finite_mask = np.isfinite(cutout) if not np.all(finite_mask): coeff_matrix = coeff_matrix[finite_mask] cutout = cutout[finite_mask] try: c = np.linalg.lstsq(coeff_matrix, cutout, rcond=None)[0] except np.linalg.LinAlgError: msg = 'quadratic fit failed' warnings.warn(msg, AstropyUserWarning) return np.array((np.nan, np.nan)) # Analytically find the maximum of the polynomial _, c10, c01, c11, c20, c02 = c det = 4 * c20 * c02 - c11**2 # If the determinant is <= 0, the surface has a saddle point. If # the determinant is > 0, the surface has a minimum or maximum. The # curvature is negative (maximum) if c20 < 0 and c02 < 0. However, # if det > 0, then 4 * c20 * c02 > c11**2 >= 0, so c20 and c02 must # have the same sign. Therefore, we only need to check if c20 > 0 # (or c02 > 0) to determine if the surface has a minimum. if det <= 0 or c20 > 0: msg = 'quadratic fit does not have a maximum' warnings.warn(msg, AstropyUserWarning) return np.array((np.nan, np.nan)) xm = (c01 * c11 - 2.0 * c02 * c10) / det ym = (c10 * c11 - 2.0 * c20 * c01) / det if 0.0 < xm < (nx - 1.0) and 0.0 < ym < (ny - 1.0): xycen = np.array((xm, ym), dtype=float) else: msg = 'quadratic polynomial maximum value falls outside of the image' warnings.warn(msg, AstropyUserWarning) return np.array((np.nan, np.nan)) return xycen
[docs] class CentroidQuadratic: """ Class to calculate the centroid of a 2D array by fitting a 2D quadratic polynomial. This class provides a callable interface to the `~photutils.centroids.centroid_quadratic` function, allowing a centroid function with specific fit parameters to be defined and reused. This is useful, for example, when using a customized centroid function with `~photutils.centroids.centroid_sources`. Parameters ---------- fit_boxsize : int or tuple of int, optional The size (in pixels) of the box used to define the fitting region. If ``fit_boxsize`` has two elements, they must be in ``(ny, nx)`` order. If ``fit_boxsize`` is a scalar then a square box of size ``fit_boxsize`` will be used. ``fit_boxsize`` must have odd values for both axes. Examples -------- >>> import numpy as np >>> from photutils.datasets import make_4gaussians_image >>> from photutils.centroids import CentroidQuadratic >>> data = make_4gaussians_image() >>> data -= np.median(data[0:30, 0:125]) >>> data = data[40:80, 70:110] >>> centroid_func = CentroidQuadratic(fit_boxsize=5) >>> x1, y1 = centroid_func(data) >>> print(np.array((x1, y1))) # doctest: +FLOAT_CMP [19.94009505 20.06884997] Using with `~photutils.centroids.centroid_sources`:: >>> from photutils.centroids import centroid_sources >>> data = make_4gaussians_image() >>> data -= np.median(data[0:30, 0:125]) >>> x_init = (25, 91, 151, 160) >>> y_init = (40, 61, 24, 71) >>> centroid_func = CentroidQuadratic(fit_boxsize=3) >>> x, y = centroid_sources(data, x_init, y_init, box_size=25, ... centroid_func=centroid_func) """ def __init__(self, *, fit_boxsize=5): self.fit_boxsize = fit_boxsize def __repr__(self): return make_repr(self, ['fit_boxsize']) def __str__(self): return make_repr(self, ['fit_boxsize'], long=True)
[docs] def __call__(self, data, *, mask=None): """ Calculate the centroid. Non-finite values (e.g., NaN or inf) in the ``data`` array are automatically masked. The automatically masked values are combined (using bitwise OR) with the input ``mask``. If ``data`` is a `~numpy.ma.MaskedArray`, its mask will also be combined (using bitwise OR) with the input ``mask``. 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. 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``. Masked data are excluded from calculations. Returns ------- centroid : `~numpy.ndarray` The ``x, y`` coordinates of the centroid. Notes ----- Unlike `~photutils.centroids.centroid_1dg` and `~photutils.centroids.centroid_2dg`, this method does not support an error array. """ kwargs = {'mask': mask, 'fit_boxsize': self.fit_boxsize, } return centroid_quadratic(data, **kwargs)
[docs] @deprecated_positional_kwargs(since='3.0', until='4.0') def centroid_sources(data, xpos, ypos, box_size=11, footprint=None, mask=None, centroid_func=centroid_com, **kwargs): """ Calculate the centroid of sources at the defined positions in a 2D array using a specified centroid function. A cutout image centered on each input position will be used to calculate the centroid position. The cutout image is defined either using the ``box_size`` or ``footprint`` keyword. The ``footprint`` keyword can be used to create a non-rectangular cutout image. Masks and non-finite values are handled by the input ``centroid_func``. When using a centroid function provided by Photutils, non-finite values (e.g., NaN or inf) in the ``data`` array are automatically masked. The ``centroid_1dg`` and ``centroid_2dg`` functions also automatically mask any pixels with non-finite ``error`` array values. The final mask is a logical OR combination of the input ``mask``, the automatically generated mask(s) 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 background-subtracted. xpos, ypos : float or array_like of float The initial ``x`` and ``y`` pixel position(s) of the center position. A cutout image centered on this position will be used to calculate the centroid. box_size : int or array_like of int, optional The size of the cutout image along each axis. If ``box_size`` is a number, then a square cutout of ``box_size`` will be created. If ``box_size`` has two elements, they must be in ``(ny, nx)`` order. ``box_size`` must have odd values for both axes. Either ``box_size`` or ``footprint`` must be defined. If they are both defined, then ``footprint`` overrides ``box_size``. footprint : bool `~numpy.ndarray`, optional A 2D boolean array where `True` values describe the local footprint region to cutout. ``footprint`` can be used to create a non-rectangular cutout image, in which case the input ``xpos`` and ``ypos`` represent the center of the minimal bounding box for the input ``footprint``. ``box_size=(n, m)`` is equivalent to ``footprint=np.ones((n, m))``. Either ``box_size`` or ``footprint`` must be defined. If they are both defined, then ``footprint`` overrides ``box_size``. The same ``footprint`` is used for all sources. mask : 2D bool `~numpy.ndarray`, optional A 2D boolean array 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``. centroid_func : callable, optional A callable object (e.g., function or class) that is used to calculate the centroid of a 2D array. The ``centroid_func`` must accept a 2D `~numpy.ndarray`, have a ``mask`` keyword and optionally an ``error`` keyword. The callable object must return two scalar values representing the (x, y) centroid. The default is `~photutils.centroids.centroid_com`. **kwargs : dict, optional Any additional keyword arguments accepted by the ``centroid_func``. Returns ------- xcentroid, ycentroid : `~numpy.ndarray` The ``x`` and ``y`` pixel position(s) of the centroids. NaNs will be returned where the centroid failed. This is usually due a ``box_size`` that is too small when using a fitting-based centroid function (e.g., `centroid_1dg`, `centroid_2dg`, or `centroid_quadratic`). Examples -------- >>> import numpy as np >>> from photutils.centroids import centroid_2dg, centroid_sources >>> from photutils.datasets import make_4gaussians_image >>> data = make_4gaussians_image() >>> data -= np.median(data[0:30, 0:125]) >>> x_init = (25, 91, 151, 160) >>> y_init = (40, 61, 24, 71) >>> x, y = centroid_sources(data, x_init, y_init, box_size=25, ... centroid_func=centroid_2dg) >>> print(x) # doctest: +FLOAT_CMP [ 24.96807828 89.98684636 149.96545721 160.18810915] >>> print(y) # doctest: +FLOAT_CMP [40.03657613 60.01836631 24.96777946 69.80208702] .. plot:: import matplotlib.pyplot as plt import numpy as np from photutils.centroids import centroid_2dg, centroid_sources from photutils.datasets import make_4gaussians_image data = make_4gaussians_image() data -= np.median(data[0:30, 0:125]) x_init = (25, 91, 151, 160) y_init = (40, 61, 24, 71) x, y = centroid_sources(data, x_init, y_init, box_size=25, centroid_func=centroid_2dg) fig, ax = plt.subplots(figsize=(8, 4)) ax.imshow(data, origin='lower') ax.scatter(x, y, marker='+', s=80, color='red', label='Centroids') ax.legend() fig.tight_layout() """ if np.ndim(data) != 2: msg = 'data must be a 2D array' raise ValueError(msg) xpos = np.atleast_1d(xpos) ypos = np.atleast_1d(ypos) if xpos.ndim != 1: msg = 'xpos must be a 1D array' raise ValueError(msg) if ypos.ndim != 1: msg = 'ypos must be a 1D array' raise ValueError(msg) if len(xpos) != len(ypos): msg = 'xpos and ypos must have the same length' raise ValueError(msg) if (xpos.min() < 0 or ypos.min() < 0 or xpos.max() > data.shape[1] - 1 or ypos.max() > data.shape[0] - 1): msg = 'xpos, ypos values contain points outside the input data' raise ValueError(msg) if footprint is None: if box_size is None: msg = 'box_size or footprint must be defined' raise ValueError(msg) box_size = as_pair('box_size', box_size, lower_bound=(0, 0), check_odd=True) footprint = np.ones(box_size, dtype=bool) else: footprint = np.asanyarray(footprint, dtype=bool) if footprint.ndim != 2: msg = 'footprint must be a 2D array' raise ValueError(msg) if mask is not None and mask.shape != data.shape: msg = 'mask and data must have the same shape' raise ValueError(msg) spec = inspect.signature(centroid_func) if 'mask' not in spec.parameters: msg = "The input 'centroid_func' must have a 'mask' keyword." raise ValueError(msg) # Drop any **kwargs not supported by the centroid_func centroid_kwargs = {key: val for key, val in kwargs.items() if key in spec.parameters} # Save the original error array before the loop so that each # iteration independently slices the full-image array error_array = centroid_kwargs.get('error') # Extract xpeak/ypeak before the loop so the original absolute # coordinates are available for every source. The per-iteration # block below re-adds them with the correct cutout offset each time. # Remove this block once xpeak and ypeak are fully deprecated. xpeak_orig = centroid_kwargs.pop('xpeak', None) ypeak_orig = centroid_kwargs.pop('ypeak', None) n_sources = len(xpos) xcentroids = np.zeros(n_sources, dtype=float) ycentroids = np.zeros(n_sources, dtype=float) inverted_footprint = np.logical_not(footprint) for i, (xp, yp) in enumerate(zip(xpos, ypos, strict=True)): slices_large, slices_small = overlap_slices(data.shape, footprint.shape, (yp, xp)) data_cutout = data[slices_large] # Trim footprint mask if it has only partial overlap on the data footprint_mask = inverted_footprint[slices_small] if mask is not None: # Combine the input mask cutout and footprint mask mask_cutout = np.logical_or(mask[slices_large], footprint_mask) else: mask_cutout = footprint_mask if np.all(mask_cutout): msg = (f'The cutout for the source at ({xp}, {yp}) is completely ' 'masked. Please check your input mask and footprint. ' 'Also note that footprint must be a small, local ' 'footprint.') raise ValueError(msg) centroid_kwargs.update({'mask': mask_cutout}) if error_array is not None: centroid_kwargs['error'] = error_array[slices_large] # Remove this block once xpeak and ypeak are fully deprecated. # Clear any xpeak/ypeak left by the previous iteration, then # re-add with the offset relative to this source's cutout. centroid_kwargs.pop('xpeak', None) centroid_kwargs.pop('ypeak', None) if xpeak_orig is not None and ypeak_orig is not None: centroid_kwargs['xpeak'] = xpeak_orig - slices_large[1].start centroid_kwargs['ypeak'] = ypeak_orig - slices_large[0].start try: xcen, ycen = centroid_func(data_cutout, **centroid_kwargs) except (ValueError, TypeError) as exc: msg = f'Centroid failed for source at ({xp}, {yp}): {exc}' warnings.warn(msg, AstropyUserWarning, stacklevel=2) xcen, ycen = np.nan, np.nan xcentroids[i] = xcen + slices_large[1].start ycentroids[i] = ycen + slices_large[0].start return xcentroids, ycentroids