Source code for convster.filters.gaussian

"""
This module provides various functions to facilitate the usage of
`skimage.filters.gaussian`
"""

from __future__ import annotations

import numpy as np
from numpy.typing import NDArray

from skimage.filters import gaussian

from ..helper import (first_nonzero,
                      last_nonzero)

# we abstract the specific filters:
[docs] img_filter = gaussian
[docs] def get_kernel_diameter(sigma: float, **params) -> int: """ Compute the effective Gaussian kernel diameter in number of cells. The diameter is estimated by applying a Gaussian filter to a single impulse in the center of a zero image, and measuring the spread of the nonzero region. The result is always odd to ensure symmetry. Parameters ---------- sigma : float Standard deviation for the Gaussian kernel. Currently only single scalar values are supported. **params Additional keyword arguments passed to the Gaussian filter. Returns ------- int Estimated kernel diameter in pixels (always an odd number). Notes ----- This function determines the kernel diameter adaptively. Starting from an initial guess of `10 * sigma`, the size is increased until the blurred impulse response fits within the kernel. Uses :func:`skimage.filters.gaussian` internally to compute the impulse response. See Also -------- :func:`get_kernel_size` : Return the radius (half-diameter) of the Gaussian kernel. :func:`compatible_border_size` : Assert or compute a border size compatible with a kernel. Examples -------- >>> get_kernel_diameter(1) 7 >>> get_kernel_diameter(2) 15 """ msize = int(10 * sigma) # make sure it is odd if not msize & 1: msize += 1 diameter = msize while diameter == msize: msize += int(max(1, round(0.1 * msize))) tmap = np.zeros((msize, msize), dtype=np.uint8) half = int(0.5 * (msize - 1)) tmap[half, half] = 255 blurred = gaussian(tmap, sigma=sigma, **params) diameter = max(np.unique(last_nonzero(blurred, axis=1) - first_nonzero(blurred, axis=1))) + 1 return diameter
[docs] def get_kernel_size(sigma: float, **params): """ Return the radius of a Gaussian kernel (center to border distance). The kernel size is defined as half of the kernel diameter minus one, i.e. size = (diameter - 1) / 2 where `diameter` is determined by :func:`get_kernel_diameter`. Parameters ---------- sigma : float Standard deviation for the Gaussian kernel. **params Additional keyword arguments passed to :func:`get_kernel_diameter` (e.g. `truncate`, `mode`). Returns ------- int The radius of the Gaussian kernel in pixels. See Also -------- :func:`get_kernel_diameter` : Compute the full kernel diameter. :func:`compatible_border_size` : Assert or compute a border size compatible with a kernel. Examples -------- >>> get_kernel_size(1) 3 >>> get_kernel_size(2) 7 """ return int(0.5 * (get_kernel_diameter(sigma, **params) - 1))
[docs] def compatible_border_size(sigma: float | int, border: tuple[int, int] | None = None, **params) -> tuple[int, int]: """ Assert that the border size is compatible with the specified parameter This method asserts that the kernel size determined by `sigma` and further parametrization is smaller than the border. If no border is provided, then the minimal border size (in number of pixels) is returned. Parameters ---------- sigma : float or int Standard deviation for Gaussian kernel border : tuple[int, int] or None The border size (width, height) in number of pixels along each axis **params Additional keyword arguments passed to :func:`get_kernel_size`. Returns ------- tuple[int, int] The border (width, height) compatible with the specified parameters. If a border was provided already, it is returned again, if no border was provided, the smallest compatible border is returned. See Also -------- :func:`get_kernel_size` : Compute the kernel radius used in compatibility checks. :func:`get_kernel_diameter` : Compute the full diameter of the Gaussian kernel. """ ks = get_kernel_size(sigma=sigma, **params) if border: assert all(ks <= b for b in border), f"A dimension of {border=} " \ f"exceeds the kernel size {ks}" bs = border else: bs = (ks + 1,) * 2 return bs
[docs] def bpgaussian(data: NDArray, **filter_params) -> NDArray: """Applies a border-preserving Gaussian filter The approach considers a Gaussian blur to be a weighted average over all pixels within the kernel diameter with the weight being given by the Gaussian function. Pixels close to `np.nan` values should simply "ignore" `np.nan` pixels and perform the weighted average over all non-`np.nan` pixels within the Gaussian kernel. This can be achieved with a normal Gaussian filter in a three-step process: 1. Perform a Gaussian filter on the data with `np.nan` substituted by the neutral element in terms of addition, i.e. 0.0. This leads to a weighted average that is not properly normalized as the former `np.nan` pixels do not contribute to the value, but are considered in the normalizing sum of the weights. 2. To properly normalize all pixels, create a binary array in the same shape as `data` with `np.nan` values becoming `0` and all other pixels `1`. Apply the same Gaussian filter to this binary array which results in an array holding the sum of weights for the weighted average. 3. Dividing the blurred array from point 1. by the sum-of-weights array form step 2 results in a properly normalized weighted average and thus a blurred version of the input data with preserved borders. Parameters ---------- data : NDArray Array to apply the Gaussian filter on. **filter_params : dict Additional keyword arguments passed to :func:`skimage.filters.gaussian`. Common parameters include: - ``sigma`` : float Standard deviation for Gaussian kernel. - ``truncate`` : float Truncate filter at this many standard deviations. See :func:`skimage.filters.gaussian` for further parameters. Returns ------- NDArray Blurred version of `data` with borders preserved at NaN boundaries. See Also -------- :func:`get_kernel_diameter` : Compute the effective kernel diameter for a given sigma. :func:`get_blur_params` : Compute Gaussian blur parameters from diameter or sigma. """ # Substitute `np.nan`s with 0.0 and apply filter _data_nonnan = np.where(np.isnan(data), 0.0, data) _data_nonnan_blurred = gaussian(image=_data_nonnan, **filter_params) _data_binary = np.where(np.isnan(data), 0.0, 1.0) _data_binary_blurred = gaussian(image=_data_binary, **filter_params) _blurred_data = np.divide(_data_nonnan_blurred, _data_binary_blurred, out=np.full(data.shape, np.nan), where=~np.isnan(data)) return _blurred_data
[docs] def get_blur_params(diameter: float | None = None, sigma: float | None = None, truncate: float = 3) -> dict[str, float]: """ Compute Gaussian blur parameters from either `diameter` or `sigma`. Either `diameter` or `sigma` must be provided. Missing values are inferred from the others, and `truncate` is used or recomputed to maintain consistency. Parameters ---------- diameter : float or None Kernel diameter. If provided with `sigma`, `truncate` is recomputed. sigma : float or None Standard deviation of the Gaussian kernel. If provided with `diameter`, `truncate` is recomputed. truncate : float Number of standard deviations at which to truncate the kernel. Default is 3. Ignored if both `diameter` and `sigma` are provided (recomputed). Returns ------- dict[str, float] Dictionary containing: - `diameter`: computed kernel diameter - `sigma`: computed standard deviation - `truncate`: final truncate value Raises ------ TypeError If neither `diameter` nor `sigma` is provided. Notes ----- The function ensures that `diameter`, `sigma`, and `truncate` are consistent according to Gaussian kernel conventions. See Also -------- :func:`get_kernel_diameter` : Compute the effective kernel diameter from sigma. :func:`get_kernel_size` : Compute the kernel radius from sigma. :func:`compatible_border_size` : Assert or compute a border compatible with the kernel. Examples -------- >>> get_blur_params(diameter=15) {'diameter': 15, 'sigma': 2.5, 'truncate': 3} >>> get_blur_params(sigma=2.0) {'diameter': 12.0, 'sigma': 2.0, 'truncate': 3} >>> get_blur_params(diameter=15, sigma=3) {'diameter': 15, 'sigma': 3, 'truncate': 2.5} """ if diameter is None and sigma is None: raise TypeError("Either the `diameter` or the `sigma` parameter " f"must be provided. \nGot: {diameter=}, {sigma=}") if diameter: if sigma: truncate = 0.5 * diameter / sigma else: if truncate: sigma = 0.5 * diameter / truncate else: if sigma: diameter = 2 * sigma * truncate return dict(diameter=diameter, sigma=sigma, truncate=truncate)