Source code for photutils.psf_matching.windows

# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Window (tapering) functions for matching PSFs using Fourier methods.
"""

import warnings

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

__all__ = [
    'CosineBellWindow',
    'HanningWindow',
    'SplitCosineBellWindow',
    'TopHatWindow',
    'TukeyWindow',
]


def _distance_grid(shape):
    """
    Return an array where each value is the Euclidean distance from the
    array center.

    Parameters
    ----------
    shape : tuple of int
        The size of the output array along each axis. Must have only 2
        elements. To have a well defined array center, the size along
        each axis should be an odd integer, but this is not enforced.

    Returns
    -------
    result : 2D `~numpy.ndarray`
        An array containing the Euclidean radial distances from the
        array center.
    """
    if len(shape) != 2:
        msg = 'shape must have only 2 elements'
        raise ValueError(msg)

    y_cen, x_cen = (shape[0] - 1) / 2, (shape[1] - 1) / 2
    y_vals, x_vals = np.ogrid[:shape[0], :shape[1]]

    return np.hypot(x_vals - x_cen, y_vals - y_cen)


[docs] class SplitCosineBellWindow: """ Class to define a 2D split cosine bell taper function. This is the base class for window functions, providing full control over both the inner flat region (``beta``) and the taper width (``alpha``). The window equals 1.0 in the inner region, smoothly transitions to 0.0 using a cosine taper, and remains 0.0 outside. This window is useful when you need precise control over both the preserved central region and the taper characteristics. Parameters ---------- alpha : float, optional The percentage of array values that are tapered. ``alpha`` must be between 0.0 and 1.0, inclusive. beta : float, optional The inner diameter as a fraction of the array size beyond which the taper begins. ``beta`` must be between 0.0 and 1.0, inclusive. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf_matching import SplitCosineBellWindow taper = SplitCosineBellWindow(alpha=0.4, beta=0.3) data = taper((101, 101)) fig, ax = plt.subplots() axim = ax.imshow(data, origin='lower') fig.colorbar(axim) A 1D cut across the image center: .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf_matching import SplitCosineBellWindow taper = SplitCosineBellWindow(alpha=0.4, beta=0.3) data = taper((101, 101)) fig, ax = plt.subplots() ax.plot(data[50, :]) """ def __init__(self, alpha, beta): if not (0.0 <= alpha <= 1.0): msg = ('alpha must be between 0.0 and 1.0, inclusive. ' f'Got: {alpha}') raise ValueError(msg) if not (0.0 <= beta <= 1.0): msg = ('beta must be between 0.0 and 1.0, inclusive. ' f'Got: {beta}') raise ValueError(msg) if alpha + beta > 1.0: msg = ('alpha + beta > 1.0; the taper region will be ' 'clipped to the array boundary.') warnings.warn(msg, AstropyUserWarning) self.alpha = float(alpha) self.beta = float(beta) def __repr__(self): return (f'{self.__class__.__name__}(' f'alpha={self.alpha!r}, beta={self.beta!r})') def __str__(self): return self.__repr__()
[docs] def __call__(self, shape): """ Generate the window function for the given shape. Parameters ---------- shape : tuple of int The size of the output array along each axis. Returns ------- result : 2D `~numpy.ndarray` The window function as a 2D array. """ dist = _distance_grid(shape) # Define geometry based on the smallest shape dimension max_r = (min(shape) - 1.0) / 2.0 r_inner = self.beta * max_r taper_width = self.alpha * max_r r_outer = r_inner + taper_width if taper_width > 0: r = dist - r_inner result = 0.5 * (1.0 + np.cos(np.pi * r / taper_width)) else: result = np.ones(shape, dtype=float) result[dist < r_inner] = 1.0 result[dist > r_outer] = 0.0 return result
[docs] class HanningWindow(SplitCosineBellWindow): """ Class to define a 2D `Hanning (or Hann) window <https://en.wikipedia.org/wiki/Hann_function>`_ function. The Hann window is a taper formed by using a raised cosine with ends that touch zero. The taper begins at the center and smoothly decreases to zero at the edges. This window equals 1.0 only at the exact center point. This is a classic general-purpose window function widely used in signal processing. It provides good sidelobe suppression in Fourier space, reducing ringing artifacts at the cost of tapering the entire image. For PSF matching, use this window when edge effects and ringing artifacts are a primary concern and you can accept tapering most of the data. If you want to preserve more of the central region, consider using `TukeyWindow` instead. Notes ----- Equivalent to ``SplitCosineBellWindow(alpha=1.0, beta=0.0)``. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf_matching import HanningWindow taper = HanningWindow() data = taper((101, 101)) fig, ax = plt.subplots() axim = ax.imshow(data, origin='lower') fig.colorbar(axim) A 1D cut across the image center: .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf_matching import HanningWindow taper = HanningWindow() data = taper((101, 101)) fig, ax = plt.subplots() ax.plot(data[50, :]) """ def __init__(self): # alpha=1.0 (full taper), beta=0.0 (taper starts at center) super().__init__(alpha=1.0, beta=0.0) def __repr__(self): return f'{self.__class__.__name__}()' def __str__(self): return self.__repr__()
[docs] class TukeyWindow(SplitCosineBellWindow): """ Class to define a 2D `Tukey window <https://en.wikipedia.org/wiki/Window_function#Tukey_window>`_ function. The Tukey window features a flat inner plateau equal to 1.0, surrounded by a smooth cosine taper that transitions to 0.0 at the edges. This provides an excellent balance between preserving data in the central region and suppressing edge artifacts. The ``alpha`` parameter controls the trade-off: smaller values preserve more data but create stronger edge effects, while larger values reduce artifacts but taper more of the image. Compared to `HanningWindow`, Tukey preserves a larger central region. Compared to `TopHatWindow`, it provides much better artifact suppression at the cost of tapering the outer regions. Parameters ---------- alpha : float, optional The percentage of array values that are tapered. Must be between 0.0 and 1.0, inclusive. When ``alpha=0``, this becomes a `TopHatWindow`. When ``alpha=1``, this becomes a `HanningWindow`. Notes ----- Equivalent to ``SplitCosineBellWindow(alpha=alpha, beta=1.0 - alpha)``. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf_matching import TukeyWindow taper = TukeyWindow(alpha=0.4) data = taper((101, 101)) fig, ax = plt.subplots() axim = ax.imshow(data, origin='lower') fig.colorbar(axim) A 1D cut across the image center: .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf_matching import TukeyWindow taper = TukeyWindow(alpha=0.4) data = taper((101, 101)) fig, ax = plt.subplots() ax.plot(data[50, :]) """ def __init__(self, alpha): super().__init__(alpha=alpha, beta=1.0 - alpha) def __repr__(self): return (f'{self.__class__.__name__}' f'(alpha={self.alpha!r})') def __str__(self): return self.__repr__()
[docs] class CosineBellWindow(SplitCosineBellWindow): """ Class to define a 2D cosine bell window function. This window equals 1.0 only at the exact center point and smoothly tapers to 0.0. The taper begins immediately from the center (no inner plateau) and extends outward over a fraction ``alpha`` of the maximum radius. Use this window when you want to preserve the very center of an image while applying a gentle taper that starts relatively far from the edges. It provides less artifact suppression than `TukeyWindow` for the same ``alpha`` value because the taper region is positioned differently. Parameters ---------- alpha : float, optional The percentage of array values that are tapered. Must be between 0.0 and 1.0, inclusive. When ``alpha=1``, this becomes a `HanningWindow`. Notes ----- Equivalent to ``SplitCosineBellWindow(alpha=alpha, beta=0.0)``. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf_matching import CosineBellWindow taper = CosineBellWindow(alpha=0.3) data = taper((101, 101)) fig, ax = plt.subplots() axim = ax.imshow(data, origin='lower') fig.colorbar(axim) A 1D cut across the image center: .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf_matching import CosineBellWindow taper = CosineBellWindow(alpha=0.3) data = taper((101, 101)) fig, ax = plt.subplots() ax.plot(data[50, :]) """ def __init__(self, alpha): super().__init__(alpha=alpha, beta=0.0) def __repr__(self): return (f'{self.__class__.__name__}' f'(alpha={self.alpha!r})') def __str__(self): return self.__repr__()
[docs] class TopHatWindow(SplitCosineBellWindow): """ Class to define a 2D top hat window function. This window equals 1.0 inside a circular region defined by ``beta`` and drops sharply to 0.0 outside, with no smooth transition. It is also known as a rectangular or boxcar window. This window preserves the most data (everything inside the cutoff radius is untouched), but the sharp edge creates strong ringing artifacts in Fourier space. Use this only when you need to strictly preserve data within a specific region and can tolerate significant artifacts, or when the sharp cutoff is explicitly desired. For most PSF matching applications, `TukeyWindow` is preferred as it provides much better artifact suppression while still preserving a large central region. Use `TopHatWindow` primarily for masking or when studying the effects of abrupt truncation. Parameters ---------- beta : float, optional The inner diameter as a fraction of the array size beyond which the window drops to zero. Must be between 0.0 and 1.0, inclusive. Notes ----- Equivalent to ``SplitCosineBellWindow(alpha=0.0, beta=beta)``. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf_matching import TopHatWindow taper = TopHatWindow(beta=0.4) data = taper((101, 101)) fig, ax = plt.subplots() axim = ax.imshow(data, origin='lower') fig.colorbar(axim) A 1D cut across the image center: .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf_matching import TopHatWindow taper = TopHatWindow(beta=0.4) data = taper((101, 101)) fig, ax = plt.subplots() ax.plot(data[50, :]) """ def __init__(self, beta): super().__init__(alpha=0.0, beta=beta) def __repr__(self): return (f'{self.__class__.__name__}' f'(beta={self.beta!r})') def __str__(self): return self.__repr__()