# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Base class and star finder kernel for detecting stars in an astronomical
image.
Each star-finding class should define a method called ``find_stars``
that finds stars in an image.
"""
import abc
import inspect
import math
import warnings
import astropy.units as u
import numpy as np
from astropy.stats import gaussian_fwhm_to_sigma
from astropy.utils import lazyproperty
from astropy.utils.exceptions import AstropyDeprecationWarning
from photutils.detection.peakfinder import find_peaks
from photutils.utils._deprecation import (create_empty_deprecated_qtable,
deprecated_getattr,
deprecated_positional_kwargs,
deprecated_renamed_argument)
from photutils.utils._misc import _get_meta
from photutils.utils._quantity_helpers import check_units
from photutils.utils._repr import make_repr
from photutils.utils.cutouts import _make_cutouts
from photutils.utils.exceptions import NoDetectionsWarning
__all__ = ['StarFinderBase', 'StarFinderCatalogBase']
# Remove in 4.0
_DEPRECATED_ATTRIBUTES: dict = {
'xcentroid': 'x_centroid',
'ycentroid': 'y_centroid',
'cutout_xcentroid': 'cutout_x_centroid',
'cutout_ycentroid': 'cutout_y_centroid',
'pa': 'orientation',
'npix': 'n_pixels',
}
[docs]
class StarFinderBase(metaclass=abc.ABCMeta):
"""
Abstract base class for star finders.
"""
[docs]
@deprecated_positional_kwargs(since='3.0', until='4.0')
def __call__(self, data, mask=None):
"""
Find stars in an astronomical image.
Parameters
----------
data : 2D array_like
The 2D image array.
mask : 2D bool array, optional
A boolean mask with the same shape as ``data``, where a
`True` value indicates the corresponding element of ``data``
is masked. Masked pixels are ignored when searching for
stars.
Returns
-------
table : `~astropy.table.Table` or `None`
A table of found stars. If no stars are found then `None` is
returned.
"""
return self.find_stars(data, mask=mask)
@staticmethod
def _find_stars(convolved_data, kernel, threshold, *, min_separation=0.0,
mask=None, exclude_border=False):
"""
Find stars in an image.
Parameters
----------
convolved_data : 2D array_like
The convolved 2D array. Should be NaN-free; any NaN values
should be handled before calling this method.
kernel : `_StarFinderKernel` or 2D `~numpy.ndarray`
The convolution kernel. ``StarFinder`` inputs the kernel
as a 2D array.
threshold : float or 2D array_like
The absolute image value above which to select sources. The
exact value depends on the calling star finder class (e.g.,
`DAOStarFinder` multiplies the ``threshold`` by the kernel
relative error, whereas `IRAFStarFinder` and `StarFinder`
directly use the input ``threshold``). A 2D ``threshold``
must have the same shape as ``convolved_data``. If
``convolved_data`` is a `~astropy.units.Quantity` array,
then ``threshold`` must have the same units.
min_separation : float, optional
The minimum separation for detected objects in pixels.
mask : 2D bool array, optional
A boolean mask with the same shape as ``data``, where a
`True` value indicates the corresponding element of ``data``
is masked. Masked pixels are ignored when searching for
stars.
exclude_border : bool, optional
Set to `True` to exclude sources found within half the
size of the convolution kernel from the image borders.
The default is `False`, which is the mode used by IRAF's
`DAOFIND
<https://iraf.readthedocs.io/en/latest/tasks/noao/digiphot/apphot/daofind.html>`_
and `STARFIND
<https://iraf.readthedocs.io/en/latest/tasks/images/imcoords/starfind.html>`_.
Returns
-------
result : Nx2 `~numpy.ndarray` or `None`
An Nx2 array containing the (x, y) pixel coordinates. `None`
is returned if no sources are found.
"""
# Define a local footprint for the peak finder
find_peaks_kwargs = {}
if min_separation == 0: # use kernel-shape footprint
if isinstance(kernel, np.ndarray):
footprint = np.ones(kernel.shape)
else:
footprint = kernel.mask.astype(bool)
find_peaks_kwargs['footprint'] = footprint
else:
find_peaks_kwargs['min_separation'] = min_separation
# Define the border exclusion region
if exclude_border:
if isinstance(kernel, np.ndarray):
yborder = (kernel.shape[0] - 1) // 2
xborder = (kernel.shape[1] - 1) // 2
else:
yborder = kernel.y_radius
xborder = kernel.x_radius
border_width = (yborder, xborder)
else:
border_width = None
# Find local peaks in the convolved data.
# Suppress any NoDetectionsWarning from find_peaks.
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=NoDetectionsWarning)
tbl = find_peaks(convolved_data, threshold, mask=mask,
border_width=border_width, **find_peaks_kwargs)
if tbl is None:
return None
return np.transpose((tbl['x_peak'], tbl['y_peak']))
[docs]
@abc.abstractmethod
def find_stars(self, data, *, mask=None):
"""
Find stars in an astronomical image.
Parameters
----------
data : 2D array_like
The 2D image array.
mask : 2D bool array, optional
A boolean mask with the same shape as ``data``, where a
`True` value indicates the corresponding element of ``data``
is masked. Masked pixels are ignored when searching for
stars.
Returns
-------
table : `~astropy.table.Table` or `None`
A table of found stars. If no stars are found then `None` is
returned.
"""
[docs]
class StarFinderCatalogBase(metaclass=abc.ABCMeta):
"""
Abstract base class for star finder catalogs.
This class provides common functionality for catalog classes that
store and compute properties of detected sources. External packages
may subclass it to create custom star finder catalogs.
Subclasses **must** implement:
* :attr:`x_centroid` property -- Object centroid in the x direction.
* :attr:`y_centroid` property -- Object centroid in the y direction.
* `apply_filters` method -- Filter the catalog using
algorithm-specific criteria.
* ``default_columns`` attribute -- A tuple of column names used
by `to_table` when no explicit columns are given. This should
be set in the subclass ``__init__``.
Subclasses **may** override:
* `_get_init_attributes` -- Return attribute names to copy
during slicing. The override should include
``'default_columns'`` in the returned tuple.
* `make_cutouts` -- Customize how cutout arrays are extracted.
* `cutout_data` -- Customize the cutouts used for photometry
(e.g., zeroing negative pixels).
Parameters
----------
data : 2D `~numpy.ndarray`
The 2D image. The image should be background-subtracted.
xypos : Nx2 `~numpy.ndarray`
An Nx2 array of (x, y) pixel coordinates denoting the central
positions of the stars.
kernel : 2D `~numpy.ndarray`
A 2D array of the PSF kernel. Internally, the star finder
classes may also pass a kernel object.
n_brightest : int, None, optional
The number of brightest objects to keep after sorting the source
list by flux. If ``n_brightest`` is set to `None`, all objects
will be selected.
peak_max : float, None, optional
The maximum allowed peak pixel value in an object. Objects with
peak pixel values greater than ``peak_max`` will be rejected.
This keyword may be used, for example, to exclude saturated
sources. If the star finder is run on an image that is a
`~astropy.units.Quantity` array, then ``peak_max`` must have the
same units. If ``peak_max`` is set to `None`, then no peak pixel
value filtering will be performed.
"""
@deprecated_renamed_argument('brightest', 'n_brightest', '3.0',
until='4.0')
@deprecated_renamed_argument('peakmax', 'peak_max', '3.0', until='4.0')
def __init__(self, data, xypos, kernel, *, n_brightest=None,
peak_max=None):
# Validate the units
check_units((data, peak_max), ('data', 'peak_max'))
self.data = data
unit = data.unit if isinstance(data, u.Quantity) else None
self.unit = unit
self.kernel = kernel
self.cutout_shape = kernel.shape
self.xypos = np.atleast_2d(xypos)
self.n_brightest = n_brightest
self.peak_max = peak_max
self.default_columns = ()
self.id = np.arange(len(self)) + 1
def __repr__(self):
params = ('nsources',)
overrides = {'nsources': len(self)}
return make_repr(self, params, brackets=True, overrides=overrides)
def __str__(self):
params = ('nsources',)
overrides = {'nsources': len(self)}
return make_repr(self, params, overrides=overrides, long=True)
def __len__(self):
return len(self.xypos)
def __getitem__(self, index):
"""
Index or slice the catalog.
This method should be overridden in subclasses to handle
class-specific attributes.
"""
# NOTE: we allow indexing/slicing of scalar (self.isscalar = True)
# instances in order to perform catalog filtering even for
# a single source
newcls = object.__new__(self.__class__)
# Get attributes to copy from subclass
init_attr = self._get_init_attributes()
for attr in init_attr:
setattr(newcls, attr, getattr(self, attr))
# xypos determines ordering and isscalar
# NOTE: always keep as 2D array, even for a single source
attr = 'xypos'
value = getattr(self, attr)[index]
setattr(newcls, attr, np.atleast_2d(value))
# Index/slice the remaining attributes
keys = set(self.__dict__.keys()) & set(self._lazyproperties)
keys.add('id')
for key in keys:
value = self.__dict__[key]
# Do not insert lazy attributes that are always scalar (e.g.,
# isscalar), i.e., not an array/list for each source
if np.isscalar(value):
continue
# Ensure value is always at least a 1D array, even for a
# single source
value = np.atleast_1d(value[index])
newcls.__dict__[key] = value
return newcls
def _get_init_attributes(self):
"""
Return a tuple of attribute names to copy during slicing.
This method should be overridden in subclasses.
"""
return ('data', 'unit', 'kernel', 'n_brightest', 'peak_max',
'cutout_shape', 'default_columns')
@property
def _lazyproperties(self):
"""
Return all lazyproperties (even in superclasses).
The result is cached on the class to avoid repeated
introspection via `inspect.getmembers`.
"""
cls = self.__class__
attr = '_cached_lazyproperties'
# Subclasses get their own lazyproperty list
if attr not in cls.__dict__:
def islazyproperty(obj):
return isinstance(obj, lazyproperty)
setattr(cls, attr,
[i[0] for i in inspect.getmembers(
cls, predicate=islazyproperty)])
return getattr(cls, attr)
@lazyproperty
def isscalar(self):
"""
Whether the instance is scalar (e.g., a single source).
"""
return self.xypos.shape == (1, 2)
[docs]
def reset_ids(self):
"""
Reset the ID column to be consecutive integers.
"""
self.id = np.arange(len(self)) + 1
[docs]
def make_cutouts(self, data):
"""
Make cutouts from the data array.
Parameters
----------
data : 2D `~numpy.ndarray`
The 2D image array.
Returns
-------
cutouts : 3D `~numpy.ndarray`
The cutout arrays.
"""
data_arr = data.value if isinstance(data, u.Quantity) else data
cutouts, _ = _make_cutouts(data_arr, self.xypos[:, 0],
self.xypos[:, 1], self.cutout_shape)
if self.unit is not None:
cutouts <<= self.unit
return cutouts
@lazyproperty
def cutout_data(self):
"""
The cutout data arrays.
Subclasses may override this property to customize the cutouts
used for moment-based photometry calculations (e.g., zeroing
negative pixels or subtracting a local sky background).
"""
return self.make_cutouts(self.data)
@lazyproperty
def moments(self):
"""
The raw image moments.
"""
data = self.cutout_data
if isinstance(data, u.Quantity):
data = data.value
ky, kx = data.shape[1], data.shape[2]
y = np.arange(ky, dtype=float)
x = np.arange(kx, dtype=float)
ypowers = np.column_stack([np.ones(ky), y]) # (ky, 2)
xpowers = np.column_stack([np.ones(kx), x]) # (kx, 2)
# M[n, p, q] = sum_jk data[n,j,k] * y[j]^p * x[k]^q
return ypowers.T @ data @ xpowers
@lazyproperty
def moments_central(self):
"""
The central image moments.
"""
data = self.cutout_data
if isinstance(data, u.Quantity):
data = data.value
ky, kx = data.shape[1], data.shape[2]
y = np.arange(ky, dtype=float)
x = np.arange(kx, dtype=float)
# Per-source shifted coordinates
dy = y[np.newaxis, :] - self.cutout_y_centroid[:, np.newaxis]
dx = x[np.newaxis, :] - self.cutout_x_centroid[:, np.newaxis]
# Per-source power arrays: (n, ky, 3) and (n, kx, 3)
ypowers = np.stack([np.ones_like(dy), dy, dy**2], axis=-1)
xpowers = np.stack([np.ones_like(dx), dx, dx**2], axis=-1)
# Batched matmul: ypowers^T @ data @ xpowers per source
moments = (np.transpose(ypowers, (0, 2, 1)) @ data @ xpowers)
with warnings.catch_warnings():
warnings.simplefilter('ignore', RuntimeWarning)
return moments / self.moments[:, 0, 0][:, np.newaxis, np.newaxis]
@lazyproperty
def cutout_centroid(self):
"""
The cutout centroids.
"""
moments = self.moments
# Ignore divide-by-zero RuntimeWarning
with warnings.catch_warnings():
warnings.simplefilter('ignore', RuntimeWarning)
y_centroid = moments[:, 1, 0] / moments[:, 0, 0]
x_centroid = moments[:, 0, 1] / moments[:, 0, 0]
return np.transpose((y_centroid, x_centroid))
@lazyproperty
def cutout_x_centroid(self):
"""
The cutout x centroids.
"""
return np.transpose(self.cutout_centroid)[1]
@lazyproperty
def cutout_y_centroid(self):
"""
The cutout y centroids.
"""
return np.transpose(self.cutout_centroid)[0]
@property
@abc.abstractmethod
def x_centroid(self):
"""
Object centroid in the x direction.
This property must be implemented in subclasses.
"""
@property
@abc.abstractmethod
def y_centroid(self):
"""
Object centroid in the y direction.
This property must be implemented in subclasses.
"""
# Remove in 4.0
def __getattr__(self, name):
return deprecated_getattr(self, name, _DEPRECATED_ATTRIBUTES,
since='3.0', until='4.0')
@lazyproperty
def mu_sum(self):
"""
The sum of the central moments.
"""
return (self.moments_central[:, 0, 2]
+ self.moments_central[:, 2, 0])
@lazyproperty
def mu_diff(self):
"""
The difference of the central moments.
"""
return (self.moments_central[:, 0, 2]
- self.moments_central[:, 2, 0])
@lazyproperty
def fwhm(self):
"""
The FWHM of the sources.
"""
return 2.0 * np.sqrt(np.log(2.0) * self.mu_sum)
@lazyproperty
def orientation(self):
"""
The angle between the ``x`` axis and the major axis of the 2D
Gaussian function that has the same second-order moments as the
source.
The angle increases in the counter-clockwise direction and
will be in the range [0, 360) degrees.
"""
angle = 0.5 * np.arctan2(2.0 * self.moments_central[:, 1, 1],
self.mu_diff)
return (np.rad2deg(angle) % 360) << u.deg
@lazyproperty
def roundness(self):
"""
The roundness of the sources.
"""
with warnings.catch_warnings():
warnings.simplefilter('ignore', RuntimeWarning)
return (np.sqrt(self.mu_diff**2
+ 4.0 * self.moments_central[:, 1, 1]**2)
/ self.mu_sum)
@lazyproperty
def peak(self):
"""
The peak pixel values.
"""
return np.max(self.cutout_data, axis=(1, 2))
@lazyproperty
def flux(self):
"""
The instrumental fluxes.
"""
return np.sum(self.cutout_data, axis=(1, 2))
@lazyproperty
def mag(self):
"""
The instrumental magnitudes.
"""
# Ignore RuntimeWarning if flux is <= 0
with warnings.catch_warnings():
warnings.simplefilter('ignore', RuntimeWarning)
flux = self.flux
if isinstance(flux, u.Quantity):
flux = flux.value
return -2.5 * np.log10(flux)
[docs]
def select_brightest(self):
"""
Sort the catalog by the brightest fluxes and select the top
brightest sources.
"""
newcat = self
if self.n_brightest is not None:
idx = np.argsort(self.flux)[::-1][:self.n_brightest]
newcat = self[idx]
return newcat
def _filter_finite(self, attrs, *, initial_mask=None,
skip_attrs=()):
"""
Filter the catalog by removing sources with non-finite values.
Parameters
----------
attrs : tuple of str
Attribute names to check for finiteness.
initial_mask : 1D `~numpy.ndarray` of bool or `None`, optional
A pre-existing boolean mask to combine with. If `None`,
starts with all `True`.
skip_attrs : tuple of str, optional
Attribute names to skip during finiteness checking.
Returns
-------
catalog : ``self.__class__`` or `None`
The filtered catalog, or `None` if no sources remain.
"""
if initial_mask is None:
mask = np.ones(len(self), dtype=bool)
else:
mask = initial_mask.copy()
for attr in attrs:
if attr in skip_attrs:
continue
mask &= np.isfinite(getattr(self, attr))
newcat = self[mask]
if len(newcat) == 0:
msg = 'No sources were found.'
warnings.warn(msg, NoDetectionsWarning)
return None
return newcat
def _filter_bounds(self, bounds, *, initial_mask=None, peakattr='peak'):
"""
Filter the catalog by sharpness, roundness, and peak_max bounds.
Parameters
----------
bounds : list of tuple
Each tuple is ``(attr_name, range)`` giving the attribute to
check and the range of allowed values. The range is a tuple
of the form ``(lower_bound, upper_bound)``, or `None` to
skip filtering for that attribute.
initial_mask : 1D `~numpy.ndarray` of bool or `None`, optional
A pre-existing boolean mask to combine with. If `None`,
starts with all `True`.
peakattr : str, optional
The attribute name for the peak value used for peak_max
filtering. The default is ``'peak'``.
Returns
-------
catalog : ``self.__class__`` or `None`
The filtered catalog, or `None` if no sources remain.
"""
if initial_mask is None:
mask = np.ones(len(self), dtype=bool)
else:
mask = initial_mask.copy()
for attr, range_val in bounds:
if range_val is None:
continue
min_val, max_val = range_val
values = getattr(self, attr)
mask &= (values >= min_val)
mask &= (values <= max_val)
# peak_max filtering is applied separately from the bounds list
# because it uses a different attribute (peakattr) and is always
# a single upper bound, not a range.
if self.peak_max is not None:
mask &= (getattr(self, peakattr) <= self.peak_max)
newcat = self[mask]
if len(newcat) == 0:
msg = 'Sources were found, but none pass the filtering criteria'
warnings.warn(msg, NoDetectionsWarning)
return None
return newcat
[docs]
@abc.abstractmethod
def apply_filters(self):
"""
Filter the catalog.
This method must be implemented in subclasses to apply
algorithm-specific filtering criteria.
"""
[docs]
def apply_all_filters(self):
"""
Apply all filters, select the brightest, and reset the source
IDs.
"""
cat = self.apply_filters()
if cat is None:
return None
cat = cat.select_brightest()
cat.reset_ids()
return cat
[docs]
def to_table(self, *, columns=None):
"""
Create a QTable of catalog properties.
Parameters
----------
columns : list of str, optional
List of column names to include in the table. If `None`,
uses ``self.default_columns``.
Returns
-------
table : `~astropy.table.QTable`
A table of the catalog properties.
"""
# Replace with QTable in 4.0
table = create_empty_deprecated_qtable(
_DEPRECATED_ATTRIBUTES, since='3.0', until='4.0')
table.meta.update(_get_meta()) # keep table.meta type
if columns is None:
if not self.default_columns:
msg = ('default_columns attribute is not set; either '
'pass explicit column names or set '
'default_columns in the subclass __init__')
raise AttributeError(msg)
columns = self.default_columns
for column in columns:
table[column] = getattr(self, column)
return table
class _StarFinderKernel:
"""
Container class for a 2D Gaussian density enhancement kernel.
The kernel has negative wings and sums to zero. It is used by both
`DAOStarFinder` and `IRAFStarFinder`.
Parameters
----------
fwhm : float
The full-width half-maximum (FWHM) of the major axis of the
Gaussian kernel in units of pixels.
ratio : float, optional
The ratio of the minor and major axis standard deviations of
the Gaussian kernel. ``ratio`` must be strictly positive and
less than or equal to 1.0. The default is 1.0 (i.e., a circular
Gaussian kernel).
theta : float, optional
The position angle (in degrees) of the major axis of the
Gaussian kernel, measured counter-clockwise from the positive x
axis.
sigma_radius : float, optional
The truncation radius of the Gaussian kernel in units
of sigma (standard deviation) [``1 sigma = FWHM /
(2.0 * sqrt(2.0 * log(2.0)))``]. The default is 1.5.
normalize_zerosum : bool, optional
Whether to normalize the Gaussian kernel to have zero sum. The
default is `True`, which generates a density-enhancement kernel.
Notes
-----
The class attributes include the dimensions of the elliptical kernel
and the coefficients of a 2D elliptical Gaussian function expressed
as:
.. math::
f(x, y) = A \\exp\\bigl(-g(x, y)\\bigr)
where
.. math::
g(x, y) = a (x - x_0)^{2} + 2 b (x - x_0)(y - y_0)
+ c (y - y_0)^{2}
Attributes
----------
data : 2D `~numpy.ndarray`
The kernel data array, used for convolution.
shape : tuple of int
The ``(ny, nx)`` shape of ``data``.
mask : 2D `~numpy.ndarray` of int
Binary mask (1 inside the kernel footprint, 0 outside).
Used for the peak-finding footprint, sharpness computation
in `_DAOStarFinderCatalog`, and sky estimation in
`_IRAFStarFinderCatalog`.
rel_err : float
The kernel relative error, used by `DAOStarFinder` to scale the
detection threshold.
gaussian_kernel_unmasked : 2D `~numpy.ndarray`
The unmasked Gaussian kernel (peak normalized to 1), used by
`_DAOStarFinderCatalog` for marginal fitting.
x_sigma, y_sigma : float
Standard deviations along the major and minor axes.
x_radius, y_radius : int
Half-widths of the kernel array in pixels.
n_pixels : int
Total number of pixels within the kernel ``mask``.
References
----------
.. [1] https://en.wikipedia.org/wiki/Gaussian_function
"""
def __init__(self, fwhm, *, ratio=1.0, theta=0.0, sigma_radius=1.5,
normalize_zerosum=True):
if np.ndim(fwhm) != 0:
msg = 'fwhm must be a scalar value'
raise TypeError(msg)
if fwhm <= 0:
msg = 'fwhm must be positive'
raise ValueError(msg)
if ratio <= 0 or ratio > 1:
msg = 'ratio must be > 0 and <= 1.0'
raise ValueError(msg)
if sigma_radius <= 0:
msg = 'sigma_radius must be positive'
raise ValueError(msg)
self.fwhm = fwhm
self.ratio = ratio
self.theta = theta % 360.0
self.sigma_radius = sigma_radius
self.x_sigma = self.fwhm * gaussian_fwhm_to_sigma
self.y_sigma = self.x_sigma * self.ratio
theta_radians = np.deg2rad(self.theta)
cost = np.cos(theta_radians)
sint = np.sin(theta_radians)
x_sigma2 = self.x_sigma**2
y_sigma2 = self.y_sigma**2
a = (cost**2 / (2.0 * x_sigma2)) + (sint**2 / (2.0 * y_sigma2))
# Counterclockwise rotation
b = 0.5 * cost * sint * ((1.0 / x_sigma2) - (1.0 / y_sigma2))
c = (sint**2 / (2.0 * x_sigma2)) + (cost**2 / (2.0 * y_sigma2))
# Find the extent of an ellipse with radius = sigma_radius*sigma.
# Solve for the horizontal and vertical tangents of an ellipse
# defined by g(x,y) = f.
f = self.sigma_radius**2 / 2.0
denom = (a * c) - b**2
# Ensure nx and ny are always odd.
# The minimum kernel size is 5x5.
nx = 2 * int(max(2, math.sqrt(c * f / denom))) + 1
ny = 2 * int(max(2, math.sqrt(a * f / denom))) + 1
self.x_radius = nx // 2
self.y_radius = ny // 2
# Define the kernel on a 2D grid
xc = self.x_radius
yc = self.y_radius
yy, xx = np.mgrid[0:ny, 0:nx]
circular_radius = np.sqrt((xx - xc)**2 + (yy - yc)**2)
elliptical_radius = (a * (xx - xc)**2
+ 2.0 * b * (xx - xc) * (yy - yc)
+ c * (yy - yc)**2)
self.mask = np.where(
(elliptical_radius <= f)
| (circular_radius <= 2.0), 1, 0).astype(int)
self.n_pixels = self.mask.sum()
# Central (peak) pixel of gaussian_kernel has a value of 1.0
self.gaussian_kernel_unmasked = np.exp(-elliptical_radius)
gaussian_kernel = self.gaussian_kernel_unmasked * self.mask
# The denom represents (variance * n_pixels)
denom = ((gaussian_kernel**2).sum()
- (gaussian_kernel.sum()**2 / self.n_pixels))
self.rel_err = 1.0 / np.sqrt(denom)
# Normalize the kernel to zero sum
if normalize_zerosum:
self.data = ((gaussian_kernel
- (gaussian_kernel.sum() / self.n_pixels))
/ denom) * self.mask
else:
self.data = gaussian_kernel
self.shape = self.data.shape
def __repr__(self):
params = ('fwhm', 'ratio', 'theta', 'sigma_radius')
return make_repr(self, params)
def __str__(self):
params = ('fwhm', 'ratio', 'theta', 'sigma_radius')
return make_repr(self, params, long=True)
def _validate_n_brightest(n_brightest):
"""
Validate the ``n_brightest`` parameter.
It must be >0 and an integer.
Parameters
----------
n_brightest : int, None, or bool
The number of brightest sources to select. If `None`, all
sources are selected. If a boolean is passed, a `TypeError` is
raised.
"""
if n_brightest is not None:
if isinstance(n_brightest, bool):
msg = 'n_brightest must be an integer'
raise TypeError(msg)
if n_brightest <= 0:
msg = 'n_brightest must be > 0'
raise ValueError(msg)
bright_int = int(n_brightest)
if bright_int != n_brightest:
msg = 'n_brightest must be an integer'
raise ValueError(msg)
n_brightest = bright_int
return n_brightest
def _handle_deprecated_range(old_lower, old_upper, new_range,
old_name, new_name, default_range):
"""
Handle deprecated lower/upper bound parameters replaced by a single
range parameter.
Parameters
----------
old_lower : float or `_DeprecatedDefault`
The deprecated lower-bound parameter value.
old_upper : float or `_DeprecatedDefault`
The deprecated upper-bound parameter value.
new_range : tuple of 2 floats or `None`
The new range parameter value.
old_name : str
The base name of the deprecated parameters (e.g., ``'sharp'``
for ``'sharplo'`` / ``'sharphi'``).
new_name : str
The name of the new range parameter (e.g.,
``'sharpness_range'``).
default_range : tuple of 2 floats
The default range values when ``new_range`` is `None`.
Returns
-------
result : tuple of 2 floats or `None`
The resolved range.
"""
if old_lower is not _DEPR_DEFAULT or old_upper is not _DEPR_DEFAULT:
msg = (f"The '{old_name}lo' and '{old_name}hi' parameters are "
'deprecated and will be removed in a future version. '
f"Use '{new_name}=(lower, upper)' instead.")
warnings.warn(msg, AstropyDeprecationWarning)
_default = new_range if new_range is not None else default_range
lower = (old_lower if old_lower is not _DEPR_DEFAULT
else _default[0])
upper = (old_upper if old_upper is not _DEPR_DEFAULT
else _default[1])
return (lower, upper)
return new_range
class _DeprecatedDefault:
"""
Sentinel default value for a deprecated parameter.
"""
def __repr__(self):
return '<deprecated>'
_DEPR_DEFAULT = _DeprecatedDefault()