Source code for photutils.psf.image_models

# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Image-based PSF models.
"""

import copy

import numpy as np
from astropy.modeling import Fittable2DModel, Parameter
from astropy.utils.decorators import lazyproperty
from scipy.interpolate import RectBivariateSpline

from photutils.utils._parameters import as_pair

__all__ = ['ImagePSF']


[docs] class ImagePSF(Fittable2DModel): """ A model for a 2D image PSF. This class takes 2D image data and computes the values of the model at arbitrary locations, including fractional pixel positions, within the image using spline interpolation provided by :py:class:`~scipy.interpolate.RectBivariateSpline`. The model has three model parameters: an image intensity scaling factor (``flux``) which is applied to the input image, and two positional parameters (``x_0`` and ``y_0``) indicating the location of a feature in the coordinate grid on which the model is evaluated. Parameters ---------- data : 2D `~numpy.ndarray` Array containing the 2D image. The length of the x and y axes must both be at least 4. All elements of the input image data must be finite. By default, the PSF peak is assumed to be located at the center of the input image (see the ``origin`` keyword). Please see the Notes section below for details on the normalization of the input image data. flux : float, optional The total flux of the source, assuming the input image was properly normalized. x_0, y_0 : float The x and y positions of a feature in the image in the output coordinate grid on which the model is evaluated. Typically, this refers to the position of the PSF peak, which is assumed to be located at the center of the input image (see the ``origin`` keyword). origin : tuple of 2 float or None, optional The ``(x, y)`` coordinate with respect to the input image data array that represents the reference pixel of the input data. The reference ``origin`` pixel will be placed at the model ``x_0`` and ``y_0`` coordinates in the output coordinate system on which the model is evaluated. Most typically, the input PSF should be centered in the input image, and thus the origin should be set to the central pixel of the ``data`` array. If the origin is set to `None`, then the origin will be set to the center of the ``data`` array (``(npix - 1) / 2.0``). oversampling : int or array_like (int), optional The integer oversampling factor(s) of the input PSF image. If ``oversampling`` is a scalar then it will be used for both axes. If ``oversampling`` has two elements, they must be in ``(y, x)`` order. fill_value : float, optional The value to use for points outside the input pixel grid. The default is 0.0. **kwargs : dict, optional Additional optional keyword arguments to be passed to the `astropy.modeling.Model` base class. See Also -------- GriddedPSFModel : A model for a grid of ePSF models. Notes ----- The fitted PSF model flux represents the total flux of the source, assuming the input image was properly normalized. This flux is determined as a multiplicative scale factor applied to the input image PSF, after accounting for any oversampling. Theoretically, the sum of all values in the PSF image over an infinite grid should equal 1.0 (assuming no oversampling). However, when the PSF is represented over a finite region, the sum of the values may be less than 1.0. For oversampled PSF images, the normalization should be adjusted so that the sum of the array values equals the product of the oversampling factors (e.g., oversampling squared if the oversampling is the same along both axes). If the input image only covers a finite region of the PSF, the sum may again be less than the product of the oversampling factors. Correction factors based on the encircled or ensquared energy of the PSF can be used to estimate the proper scaling for the finite region of the input PSF image and ensure correct flux normalization. Examples -------- In this simple example, we create a PSF image model from a Circular Gaussian PSF. In this case, one should use the `CircularGaussianPSF` model directly as a PSF model. However, this example demonstrates how to create an image PSF model from an input image. .. plot:: :include-source: import matplotlib.pyplot as plt import numpy as np from photutils.psf import CircularGaussianPSF, ImagePSF gaussian_psf = CircularGaussianPSF(x_0=12, y_0=12, fwhm=3.2) yy, xx = np.mgrid[:25, :25] psf_data = gaussian_psf(xx, yy) psf_model = ImagePSF(psf_data, x_0=12, y_0=12, flux=10) data = psf_model(xx, yy) fig, ax = plt.subplots() ax.imshow(data, origin='lower') """ flux = Parameter(default=1, description='Intensity scaling factor of the image.') x_0 = Parameter(default=0, description=('Position of a feature in the image along ' 'the x axis')) y_0 = Parameter(default=0, description=('Position of a feature in the image along ' 'the y axis')) def __init__(self, data, *, flux=flux.default, x_0=x_0.default, y_0=y_0.default, origin=None, oversampling=1, fill_value=0.0, **kwargs): self._validate_data(data) self.data = data self.origin = origin self.oversampling = as_pair('oversampling', oversampling, lower_bound=(0, 0)) self.fill_value = fill_value super().__init__(flux, x_0, y_0, **kwargs) @staticmethod def _validate_data(data): if not isinstance(data, np.ndarray): msg = 'Input data must be a 2D numpy array' raise TypeError(msg) if data.ndim != 2: msg = 'Input data must be a 2D numpy array' raise ValueError(msg) if not np.all(np.isfinite(data)): msg = 'All elements of input data must be finite' raise ValueError(msg) # this is required by RectBivariateSpline for kx=3, ky=3 if np.any(np.array(data.shape) < 4): msg = 'The length of the x and y axes must both be at least 4' raise ValueError(msg) def __str__(self): keywords = [('PSF shape (oversampled pixels)', self.data.shape), ('Origin', self.origin.tolist()), ('Oversampling', self.oversampling.tolist()), ('Fill Value', self.fill_value), ] return self._format_str(keywords=keywords) def __repr__(self): kwargs = {'origin': self.origin.tolist(), 'oversampling': self.oversampling.tolist(), 'fill_value': self.fill_value} return self._format_repr(kwargs=kwargs)
[docs] def copy(self): """ Return a copy of this model where only the model parameters are copied. All other copied model attributes are references to the original model. This prevents copying the image data, which may be a large array. This method is useful if one is interested in only changing the model parameters in a model copy. It is used in the PSF photometry classes during model fitting. Use the `deepcopy` method if you want to copy all the model attributes, including the PSF image data. Returns ------- result : `ImagePSF` A copy of this model with only the model parameters copied. """ newcls = object.__new__(self.__class__) for key, val in self.__dict__.items(): if key in self.param_names: # copy only the parameter values newcls.__dict__[key] = copy.copy(val) else: newcls.__dict__[key] = val return newcls
[docs] def deepcopy(self): """ Return a deep copy of this model. Returns ------- result : `ImagePSF` A deep copy of this model. """ return copy.deepcopy(self)
@property def shape(self): """ The shape of the (oversampled) PSF data array. Returns ------- shape : tuple The shape of the (oversampled) PSF data array. """ return self.data.shape @property def origin(self): """ A 1D `~numpy.ndarray` (x, y) pixel coordinates within the model's 2D image of the origin of the coordinate system. The reference ``origin`` pixel will be placed at the model ``x_0`` and ``y_0`` coordinates in the output coordinate system on which the model is evaluated. Most typically, the input PSF should be centered in the input image, and thus the origin should be set to the central pixel of the ``data`` array. If the origin is set to `None`, then the origin will be set to the center of the ``data`` array (``(npix - 1) / 2.0``). """ return self._origin @origin.setter def origin(self, origin): if origin is None: origin = (np.array(self.data.shape) - 1.0) / 2.0 origin = origin[::-1] # flip to (x, y) order else: origin = np.asarray(origin) if origin.ndim != 1 or len(origin) != 2: msg = 'origin must be 1D and have 2-elements' raise ValueError(msg) if not np.all(np.isfinite(origin)): msg = 'All elements of origin must be finite' raise ValueError(msg) self._origin = origin @lazyproperty def interpolator(self): """ The interpolating spline function. The interpolator is computed with a 3rd-degree `~scipy.interpolate.RectBivariateSpline` (kx=3, ky=3, s=0) using the input image data. The interpolator is used to evaluate the model at arbitrary locations, including fractional pixel positions. Notes ----- This property can be overridden in a subclass to define custom interpolators. """ x = np.arange(self.data.shape[1]) y = np.arange(self.data.shape[0]) # RectBivariateSpline expects the data to be in (x, y) axis order return RectBivariateSpline(x, y, self.data.T, kx=3, ky=3, s=0) def _calc_bounding_box(self): """ Set a bounding box defining the limits of the model. Returns ------- bbox : tuple A bounding box defining the ((y_min, y_max), (x_min, x_max)) limits of the model. """ dy, dx = np.array(self.data.shape) / 2 / self.oversampling # apply the origin shift # if origin is None, the origin is set to the center of the # image and the shift is 0 xshift = np.array(self.data.shape[1] - 1) / 2 - self.origin[0] yshift = np.array(self.data.shape[0] - 1) / 2 - self.origin[1] xshift /= self.oversampling[1] yshift /= self.oversampling[0] return ((self.y_0 - dy + yshift, self.y_0 + dy + yshift), (self.x_0 - dx + xshift, self.x_0 + dx + xshift)) @property def bounding_box(self): """ The bounding box of the model. Examples -------- >>> from photutils.psf import ImagePSF >>> psf_data = np.arange(30, dtype=float).reshape(5, 6) >>> psf_data /= np.sum(psf_data) >>> model = ImagePSF(psf_data, flux=1, x_0=0, y_0=0) >>> model.bounding_box # doctest: +FLOAT_CMP ModelBoundingBox( intervals={ x: Interval(lower=-3.0, upper=3.0) y: Interval(lower=-2.5, upper=2.5) } model=ImagePSF(inputs=('x', 'y')) order='C' ) """ return self._calc_bounding_box()
[docs] def evaluate(self, x, y, flux, x_0, y_0): """ Calculate the value of the image model at the input coordinates for the given model parameters. Parameters ---------- x, y : float or array_like The x and y coordinates at which to evaluate the model. flux : float The total flux of the source, assuming the input image was properly normalized. x_0, y_0 : float The x and y positions of the feature in the image in the output coordinate grid on which the model is evaluated. Returns ------- result : `~numpy.ndarray` The value of the model evaluated at the input coordinates. """ xi = self.oversampling[1] * (np.asarray(x, dtype=float) - x_0) yi = self.oversampling[0] * (np.asarray(y, dtype=float) - y_0) xi += self._origin[0] yi += self._origin[1] evaluated_model = flux * self.interpolator(xi, yi, grid=False) if self.fill_value is not None: # set pixels that are outside the input pixel grid to the # fill_value to avoid extrapolation; these bounds match the # RegularGridInterpolator bounds ny, nx = self.data.shape invalid = (xi < 0) | (xi > nx - 1) | (yi < 0) | (yi > ny - 1) evaluated_model[invalid] = self.fill_value return evaluated_model